react-mapfilter
Version:
These components are designed for viewing data in Mapeo. They share a common interface:
523 lines (493 loc) • 16.1 kB
JavaScript
import "core-js/modules/es.array.iterator";
import "core-js/modules/web.dom-collections.iterator";
import _extends from "@babel/runtime-corejs3/helpers/extends";
import _objectWithoutPropertiesLoose from "@babel/runtime-corejs3/helpers/objectWithoutPropertiesLoose";
import _Array$isArray from "@babel/runtime-corejs3/core-js-stable/array/is-array";
import _mapInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/map";
import _reduceInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/reduce";
// @flow
import React, { useState } from 'react';
import { makeStyles, withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import TextField from './TextField';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
import Collapse from '@material-ui/core/Collapse';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import Fade from '@material-ui/core/Fade';
import DateFnsUtils from '@date-io/date-fns';
import enLocale from 'date-fns/locale/en-US';
import frLocale from 'date-fns/locale/fr';
import ptLocale from 'date-fns/locale/pt-BR';
import esLocale from 'date-fns/locale/es';
import { MuiPickersUtilsProvider } from '@material-ui/pickers';
import MuiAccordion from '@material-ui/core/Accordion';
import MuiAccordionSummary from '@material-ui/core/AccordionSummary';
import MuiAccordionDetails from '@material-ui/core/AccordionDetails';
import DialogActions from '@material-ui/core/DialogActions';
import MuiDialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import MenuIcon from '@material-ui/icons/MoreVert';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import { Typography } from '@material-ui/core';
import { useIntl, FormattedMessage, defineMessages } from 'react-intl';
import clone from 'clone-deep';
import FeatureHeader from '../internal/FeatureHeader';
import MediaCarousel from './MediaCarousel';
import { defaultGetPreset } from '../utils/helpers';
import { get, set } from '../utils/get_set';
import Field from './Field';
/*:: import type { Observation } from 'mapeo-schema'*/
/*:: import type {
PresetWithAdditionalFields,
GetMedia,
Attachment,
Key
} from '../types'*/
const m = defineMessages({
confirmCloseTitle: {
"id": "I3F5WA==",
"defaultMessage": 'Close without saving changes?'
},
confirmCloseDescription: {
"id": "qMB8GQ==",
"defaultMessage": 'You have made some changes, closing without saving will loose the changes you make'
},
confirmCloseButtonConfirm: {
"id": "yIQFKA==",
"defaultMessage": 'Discard changes'
},
confirmCloseButtonCancel: {
"id": "FF0Vyg==",
"defaultMessage": 'Cancel'
},
confirmDeleteTitle: {
"id": "ZjTMNg==",
"defaultMessage": 'Delete observation?'
},
confirmDeleteButtonConfirm: {
"id": "xxS3fw==",
"defaultMessage": 'Yes, delete'
},
confirmDeleteButtonCancel: {
"id": "qdS9PA==",
"defaultMessage": 'Cancel'
},
// Header for section that includes the additional details for an observation
detailsHeader: {
"id": "uHtAUQ==",
"defaultMessage": 'Details'
},
// Header for section with additional fields that are not defined in the preset
additionalHeader: {
"id": "7EPUxA==",
"defaultMessage": 'Additional data'
},
// Cancel button once observation has been edited
cancelEditButton: {
"id": "bjpM6A==",
"defaultMessage": 'Cancel'
},
// Save edit button
saveEditButton: {
"id": "3StVcg==",
"defaultMessage": 'Save'
},
// Menu item to delete an observation
deleteObservationMenuItem: {
"id": "o+27Wg==",
"defaultMessage": 'Delete observation'
}
});
/*:: type ImageMediaItem = {
src: string,
type: 'image'
}*/
/*:: type Props = {
open?: boolean,
onRequestClose: () => void,
onDelete: (id: string) => void,
observation: Observation,
// The initial image to show in the media carousel
initialImageIndex?: number,
onSave: (observation: Observation) => void,
getPreset?: Observation => PresetWithAdditionalFields | void,
/**
* For a given attachment, return `src` and `type`
*-/
getMedia: GetMedia
}*/
const localeMap = {
en: enLocale,
fr: frLocale,
pt: ptLocale,
es: esLocale
};
function getLocaleData(locale) {
if (!locale) return localeMap.en;
return localeMap[locale] || localeMap[locale.split('-')[0]] || localeMap.en;
}
function defaultGetMedia({
type,
id
}
/*: Attachment*/
) {
if (type && type.split('/')[0] !== 'image') return;
return {
type: 'image',
src: id
};
}
const ConfirmCloseDialog = ({
open,
onCancel,
onConfirm
}) => /*#__PURE__*/React.createElement(Dialog, {
disableBackdropClick: true,
open: open,
onClose: onCancel,
"aria-labelledby": "close-dialog-title",
"aria-describedby": "close-dialog-description"
}, /*#__PURE__*/React.createElement(DialogTitle, {
id: "close-dialog-title"
}, /*#__PURE__*/React.createElement(FormattedMessage, m.confirmCloseTitle)), /*#__PURE__*/React.createElement(MuiDialogContent, null, /*#__PURE__*/React.createElement(DialogContentText, {
id: "close-dialog-description"
}, /*#__PURE__*/React.createElement(FormattedMessage, m.confirmCloseDescription))), /*#__PURE__*/React.createElement(DialogActions, null, /*#__PURE__*/React.createElement(Button, {
onClick: onCancel,
color: "primary"
}, /*#__PURE__*/React.createElement(FormattedMessage, m.confirmCloseButtonCancel)), /*#__PURE__*/React.createElement(Button, {
onClick: onConfirm,
color: "primary",
autoFocus: true
}, /*#__PURE__*/React.createElement(FormattedMessage, m.confirmCloseButtonConfirm))));
const ConfirmDeleteDialog = ({
open,
onCancel,
onConfirm
}) => /*#__PURE__*/React.createElement(Dialog, {
disableBackdropClick: true,
open: open,
onClose: onCancel,
"aria-labelledby": "delete-dialog-title"
}, /*#__PURE__*/React.createElement(DialogTitle, {
id: "delete-dialog-title"
}, /*#__PURE__*/React.createElement(FormattedMessage, m.confirmDeleteTitle)), /*#__PURE__*/React.createElement(DialogActions, null, /*#__PURE__*/React.createElement(Button, {
onClick: onCancel,
color: "primary"
}, /*#__PURE__*/React.createElement(FormattedMessage, m.confirmDeleteButtonCancel)), /*#__PURE__*/React.createElement(Button, {
onClick: onConfirm,
color: "primary",
autoFocus: true
}, /*#__PURE__*/React.createElement(FormattedMessage, m.confirmDeleteButtonConfirm))));
const Accordion = withStyles({
root: {
border: '1px solid rgba(0, 0, 0, .125)',
boxShadow: 'none',
'&:before': {
display: 'none'
},
'&$expanded': {
margin: 'auto'
}
},
expanded: {}
})(MuiAccordion);
const AccordionSummary = withStyles({
root: {
'&$expanded': {
minHeight: 48
}
},
content: {
'&$expanded': {
margin: '12px 0'
}
},
expanded: {}
})(MuiAccordionSummary);
const AccordionDetails = withStyles({
root: {
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
padding: '0 21px 10px 21px',
'& > div:first-child': {
marginTop: 10
}
}
})(MuiAccordionDetails);
const ObservationActions = ({
onDeleteClick
}) => {
const [anchorEl, setAnchorEl] = React.useState(null);
const [confirm, setConfirm] = useState(null);
const handleOpenClick = event => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const createHandleItemClick = (action
/*: () => any*/
= () => {}) => () => {
// Can't setState to a function
setConfirm(state => didConfirm => {
setConfirm(null);
if (didConfirm) action();
handleClose();
});
};
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(IconButton, {
"aria-controls": "simple-menu",
"aria-haspopup": "true",
onClick: handleOpenClick
}, /*#__PURE__*/React.createElement(MenuIcon, null)), /*#__PURE__*/React.createElement(Menu, {
id: "simple-menu",
anchorEl: anchorEl,
keepMounted: true,
open: Boolean(anchorEl),
onClose: handleClose
}, /*#__PURE__*/React.createElement(MenuItem, {
onClick: createHandleItemClick(onDeleteClick)
}, /*#__PURE__*/React.createElement(FormattedMessage, m.deleteObservationMenuItem))), /*#__PURE__*/React.createElement(ConfirmDeleteDialog, {
open: !!confirm,
onConfirm: () => confirm && confirm(true),
onCancel: () => confirm && confirm(false)
}));
};
const DialogContent = ({
open = false,
onRequestClose,
onSave,
onDelete,
observation,
initialImageIndex,
getPreset = defaultGetPreset,
getMedia = defaultGetMedia
}
/*: {
...$Exact<Props>,
onRequestClose: (shouldConfirm: boolean) => void
}*/
) => {
var _context, _context2, _context3;
const cx = useStyles();
const [values, setValues] = useState(observation.tags ? clone(observation.tags) : {});
const [dirty, setDirty] = useState(false);
const {
locale
} = useIntl();
const handleSave = () => {
onSave(set(observation, 'tags', values));
onRequestClose(false);
};
const handleRequestClose = () => {
// Ask for confirmation if form is dirty
onRequestClose(dirty);
};
const handleDeleteClick = () => {
onRequestClose(false);
onDelete(observation.id);
};
const handleChange = (key
/*: Key*/
, newValue
/*: any*/
) => {
setDirty(true);
setValues(set(values, key, newValue));
};
const preset = getPreset(observation) || {};
const coords = observation.lat != null && observation.lon != null ? {
latitude: observation.lat,
longitude: observation.lon
} : undefined;
const descriptionKey = values.note ? 'note' : 'notes';
const mediaItems
/*: ImageMediaItem[]*/
= _reduceInstanceProperty(_context = observation.attachments || []).call(_context, (acc, cur) => {
const item = getMedia(cur, {
width: 800,
height: 600
}); // $FlowFixMe - need to fix type refinement here
if (item && item.type === 'image') acc.push(item);
return acc;
}, []);
return /*#__PURE__*/React.createElement(MuiPickersUtilsProvider, {
utils: DateFnsUtils,
locale: getLocaleData(locale)
}, /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(IconButton, {
className: cx.closeButton,
color: "inherit",
onClick: handleRequestClose,
"aria-label": "Close"
}, /*#__PURE__*/React.createElement(CloseIcon, null)), mediaItems.length > 0 && /*#__PURE__*/React.createElement("div", {
className: cx.mediaWrapper
}, /*#__PURE__*/React.createElement(MediaCarousel, {
items: mediaItems,
initialIndex: initialImageIndex,
className: cx.media
})), /*#__PURE__*/React.createElement(FeatureHeader, {
icon: preset.icon,
name: preset.name,
coords: coords,
createdAt: new Date(observation.created_at),
action: /*#__PURE__*/React.createElement(ObservationActions, {
onDeleteClick: handleDeleteClick
})
}), /*#__PURE__*/React.createElement(TextField, {
value: values[descriptionKey],
onChange: newValue => handleChange(descriptionKey, newValue),
multiline: true,
margin: "dense",
label: "Description",
className: cx.descriptionField
}), preset.fields.length > 0 && /*#__PURE__*/React.createElement(Accordion, {
TransitionProps: {
unmountOnExit: true
}
}, /*#__PURE__*/React.createElement(AccordionSummary, {
expandIcon: /*#__PURE__*/React.createElement(ExpandMoreIcon, null),
"aria-controls": "panel1a-content",
id: "panel1a-header"
}, /*#__PURE__*/React.createElement(Typography, {
component: "h2",
className: cx.sectionHeading
}, /*#__PURE__*/React.createElement(FormattedMessage, m.detailsHeader))), /*#__PURE__*/React.createElement(AccordionDetails, null, _mapInstanceProperty(_context2 = preset.fields).call(_context2, field => /*#__PURE__*/React.createElement(Field, {
key: field.id,
field: field,
value: get(values, field.key),
onChange: handleChange
})))), _Array$isArray(preset.additionalFields) && preset.additionalFields.length > 0 && /*#__PURE__*/React.createElement(Accordion, null, /*#__PURE__*/React.createElement(AccordionSummary, {
expandIcon: /*#__PURE__*/React.createElement(ExpandMoreIcon, null),
"aria-controls": "panel1a-content",
id: "panel1a-header"
}, /*#__PURE__*/React.createElement(Typography, {
component: "h2",
className: cx.sectionHeading
}, /*#__PURE__*/React.createElement(FormattedMessage, m.additionalHeader))), /*#__PURE__*/React.createElement(AccordionDetails, null, _mapInstanceProperty(_context3 = preset.additionalFields).call(_context3, field => /*#__PURE__*/React.createElement(Field, {
key: field.id,
field: field,
value: get(values, field.key),
onChange: handleChange
})))), /*#__PURE__*/React.createElement(Fade, {
in: dirty
}, /*#__PURE__*/React.createElement(Collapse, {
in: dirty,
className: cx.actions
}, /*#__PURE__*/React.createElement(Button, {
color: "default",
variant: "contained",
className: cx.button,
onClick: handleRequestClose
}, /*#__PURE__*/React.createElement(FormattedMessage, m.cancelEditButton)), /*#__PURE__*/React.createElement(Button, {
color: "primary",
variant: "contained",
className: cx.button,
onClick: handleSave
}, /*#__PURE__*/React.createElement(FormattedMessage, m.saveEditButton))))));
};
const ObservationDialog = (_ref) => {
let {
open,
onRequestClose
}
/*: Props*/
= _ref,
otherProps = _objectWithoutPropertiesLoose(_ref, ["open", "onRequestClose"]);
const [confirm, setConfirm] = useState(null);
const handleRequestClose = shouldConfirm => {
if (!shouldConfirm) {
setConfirm(null);
onRequestClose();
return;
}
setConfirm(state => didConfirm => {
setConfirm(null);
if (didConfirm) onRequestClose();
});
};
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Dialog, {
disableBackdropClick: true,
open: open,
onClose: handleRequestClose,
scroll: "body",
fullWidth: true,
maxWidth: "sm"
}, open && /*#__PURE__*/React.createElement(DialogContent, _extends({}, otherProps, {
onRequestClose: handleRequestClose
}))), /*#__PURE__*/React.createElement(ConfirmCloseDialog, {
open: !!confirm,
onConfirm: () => confirm && confirm(true),
onCancel: () => confirm && confirm(false)
}));
};
export default ObservationDialog;
const useStyles = makeStyles(theme => ({
closeButton: {
position: 'absolute',
zIndex: 999,
left: 0,
top: 0,
color: 'white',
backgroundColor: 'rgba(0,0,0,0.4)',
borderRadius: '0 0 4px 0',
'&:hover': {
backgroundColor: 'rgba(0,0,0,0.5)'
}
},
descriptionField: {
boxSizing: 'border-box',
margin: '0 10px 13.5px 10px',
width: 'calc(100% - 20px)',
'& .MuiInputBase-inputMultiline': theme.typography.body1,
'& .MuiInputLabel-root': {
opacity: 0,
transition: 'opacity 200ms cubic-bezier(0.0, 0, 0.2, 1)'
},
'&:hover .MuiInputLabel-root, & .MuiInputLabel-root.Mui-focused': {
opacity: 1
},
'& .MuiOutlinedInput-notchedOutline': {
borderColor: 'rgba(0,0,0,0)'
}
},
sectionHeading: {
fontWeight: 500
},
mediaWrapper: {
width: '100%',
paddingTop: '75%',
position: 'relative',
height: 0
},
media: {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%'
},
details: {
flexDirection: 'column',
flex: 1,
minWidth: 320,
flexBasis: '33%',
backgroundColor: 'white',
overflowY: 'scroll'
},
actions: {
margin: '0 10px',
textAlign: 'right'
},
button: {
marginLeft: theme.spacing(1),
marginTop: 14,
marginBottom: 14
}
}));
//# sourceMappingURL=ObservationDialog.js.map