diff options
author | Anton Tananaev <anton.tananaev@gmail.com> | 2020-09-27 19:49:47 -0700 |
---|---|---|
committer | Anton Tananaev <anton.tananaev@gmail.com> | 2020-09-27 19:49:47 -0700 |
commit | 9bfda9e131ddda3076b4094a94795db41072a39c (patch) | |
tree | d6b4e12f2d62431cc0d320b5396e578944af78f4 | |
parent | bbc54a464a8a3cf41904c5335fb1845ff8af1887 (diff) | |
download | etbsa-traccar-web-9bfda9e131ddda3076b4094a94795db41072a39c.tar.gz etbsa-traccar-web-9bfda9e131ddda3076b4094a94795db41072a39c.tar.bz2 etbsa-traccar-web-9bfda9e131ddda3076b4094a94795db41072a39c.zip |
Implement attributes editing
-rw-r--r-- | modern/package.json | 1 | ||||
-rw-r--r-- | modern/src/DevicePage.js | 4 | ||||
-rw-r--r-- | modern/src/EditItemView.js | 24 | ||||
-rw-r--r-- | modern/src/MainToolbar.js | 6 | ||||
-rw-r--r-- | modern/src/RemoveDialog.js | 24 | ||||
-rw-r--r-- | modern/src/UserPage.js | 16 | ||||
-rw-r--r-- | modern/src/attributes/AddAttributeDialog.js | 83 | ||||
-rw-r--r-- | modern/src/attributes/EditAttributesView.js | 141 | ||||
-rw-r--r-- | modern/src/attributes/userAttributes.js | 68 | ||||
-rw-r--r-- | modern/src/reports/RouteReportPage.js | 6 | ||||
-rw-r--r-- | web/l10n/en.json | 1 |
11 files changed, 337 insertions, 37 deletions
diff --git a/modern/package.json b/modern/package.json index e14c074..ff701a3 100644 --- a/modern/package.json +++ b/modern/package.json @@ -6,6 +6,7 @@ "@craco/craco": "^5.6.4", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.56", "@reduxjs/toolkit": "^1.4.0", "mapbox-gl": "^1.12.0", "moment": "^2.28.0", diff --git a/modern/src/DevicePage.js b/modern/src/DevicePage.js index da41e37..f298540 100644 --- a/modern/src/DevicePage.js +++ b/modern/src/DevicePage.js @@ -91,7 +91,7 @@ const DevicePage = () => { onChange={event => setItem({...item, groupId: Number(event.target.value)})}> <option value={0}></option> {groups.map(group => ( - <option value={group.id}>{group.name}</option> + <option key={group.id} value={group.id}>{group.name}</option> ))} </Select> </FormControl> @@ -121,7 +121,7 @@ const DevicePage = () => { defaultValue={item.category} onChange={event => setItem({...item, category: event.target.value})}> {deviceCategories.map(category => ( - <option value={category}>{t(`category${category.replace(/^\w/, c => c.toUpperCase())}`)}</option> + <option key={category} value={category}>{t(`category${category.replace(/^\w/, c => c.toUpperCase())}`)}</option> ))} </Select> </FormControl> diff --git a/modern/src/EditItemView.js b/modern/src/EditItemView.js index f67927a..a6f1d22 100644 --- a/modern/src/EditItemView.js +++ b/modern/src/EditItemView.js @@ -59,19 +59,17 @@ const EditItemView = ({ children, endpoint, setItem, getItem }) => { <> <MainToolbar /> <Container maxWidth='xs' className={classes.container}> - <form> - {children} - <FormControl fullWidth margin='normal'> - <div className={classes.buttons}> - <Button type='button' color='primary' variant='outlined' onClick={() => history.goBack()}> - {t('sharedCancel')} - </Button> - <Button type='button' color='primary' variant='contained' onClick={handleSave}> - {t('sharedSave')} - </Button> - </div> - </FormControl> - </form> + {children} + <FormControl fullWidth margin='normal'> + <div className={classes.buttons}> + <Button type='button' color='primary' variant='outlined' onClick={() => history.goBack()}> + {t('sharedCancel')} + </Button> + <Button type='button' color='primary' variant='contained' onClick={handleSave}> + {t('sharedSave')} + </Button> + </div> + </FormControl> </Container> </> ); diff --git a/modern/src/MainToolbar.js b/modern/src/MainToolbar.js index 71e92c3..9b6005d 100644 --- a/modern/src/MainToolbar.js +++ b/modern/src/MainToolbar.js @@ -150,12 +150,6 @@ const MainToolbar = () => { <ListItemIcon> <SettingsIcon /> </ListItemIcon> - <ListItemText primary={t('settingsServer')} /> - </ListItem> - <ListItem button disabled> - <ListItemIcon> - <SettingsIcon /> - </ListItemIcon> <ListItemText primary={t('sharedNotifications')} /> </ListItem> </List> diff --git a/modern/src/RemoveDialog.js b/modern/src/RemoveDialog.js index a810735..8e7d97f 100644 --- a/modern/src/RemoveDialog.js +++ b/modern/src/RemoveDialog.js @@ -15,19 +15,17 @@ const RemoveDialog = ({ open, endpoint, itemId, onResult }) => { }; return ( - <> - <Dialog - open={open} - onClose={() => { onResult(false) }}> - <DialogContent> - <DialogContentText>{t('sharedRemoveConfirm')}</DialogContentText> - </DialogContent> - <DialogActions> - <Button color="primary" onClick={handleRemove}>{t('sharedRemove')}</Button> - <Button autoFocus onClick={() => onResult(false)}>{t('sharedCancel')}</Button> - </DialogActions> - </Dialog> - </> + <Dialog + open={open} + onClose={() => { onResult(false) }}> + <DialogContent> + <DialogContentText>{t('sharedRemoveConfirm')}</DialogContentText> + </DialogContent> + <DialogActions> + <Button color="primary" onClick={handleRemove}>{t('sharedRemove')}</Button> + <Button autoFocus onClick={() => onResult(false)}>{t('sharedCancel')}</Button> + </DialogActions> + </Dialog> ); }; diff --git a/modern/src/UserPage.js b/modern/src/UserPage.js index 8bef99c..81e3389 100644 --- a/modern/src/UserPage.js +++ b/modern/src/UserPage.js @@ -2,9 +2,11 @@ import React, { useState } from 'react'; import TextField from '@material-ui/core/TextField'; import t from './common/localization'; +import userAttributes from './attributes/userAttributes'; import EditItemView from './EditItemView'; import { Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import EditAttributesView from './attributes/EditAttributesView'; const useStyles = makeStyles(() => ({ details: { @@ -48,6 +50,20 @@ const UserPage = () => { variant="filled" /> </AccordionDetails> </Accordion> + <Accordion> + <AccordionSummary expandIcon={<ExpandMoreIcon />}> + <Typography variant="subtitle1"> + {t('sharedAttributes')} + </Typography> + </AccordionSummary> + <AccordionDetails className={classes.details}> + <EditAttributesView + attributes={item.attributes} + setAttributes={attributes => setItem({...item, attributes})} + definitions={userAttributes} + /> + </AccordionDetails> + </Accordion> </> } </EditItemView> diff --git a/modern/src/attributes/AddAttributeDialog.js b/modern/src/attributes/AddAttributeDialog.js new file mode 100644 index 0000000..ee4c48c --- /dev/null +++ b/modern/src/attributes/AddAttributeDialog.js @@ -0,0 +1,83 @@ +import React, { useState } from 'react'; +import { Button, Dialog, DialogActions, DialogContent, FormControl, InputLabel, MenuItem, Select, TextField } from "@material-ui/core"; + +import t from '../common/localization'; +import { Autocomplete, createFilterOptions } from '@material-ui/lab'; + +const AddAttributeDialog = ({ open, onResult, definitions }) => { + const filter = createFilterOptions({ + stringify: option => option.name, + }); + + const options = Object.entries(definitions).map(([key, value]) => ({ + key, + name: value.name, + type: value.type, + })); + + const [key, setKey] = useState(); + const [type, setType] = useState('string'); + + return ( + <Dialog open={open} fullWidth maxWidth="xs"> + <DialogContent> + <Autocomplete + onChange={(_, option) => { + setKey(option && typeof option === 'object' ? option.key : option); + if (option && option.type) { + setType(option.type); + } + }} + filterOptions={(options, params) => { + const filtered = filter(options, params); + if (params.inputValue) { + filtered.push({ + key: params.inputValue, + name: params.inputValue, + }); + } + return filtered; + }} + options={options} + getOptionLabel={option => { + return option && typeof option === 'object' ? option.name : option; + }} + renderOption={option => option.name} + freeSolo + renderInput={(params) => ( + <TextField {...params} label={t('sharedAttribute')} variant="filled" margin="normal" /> + )} + /> + <FormControl + variant="filled" + margin="normal" + fullWidth + disabled={key in definitions}> + <InputLabel>{t('sharedType')}</InputLabel> + <Select + value={type} + onChange={e => setType(e.target.value)}> + <MenuItem value={'string'}>{t('sharedTypeString')}</MenuItem> + <MenuItem value={'number'}>{t('sharedTypeNumber')}</MenuItem> + <MenuItem value={'boolean'}>{t('sharedTypeBoolean')}</MenuItem> + </Select> + </FormControl> + </DialogContent> + <DialogActions> + <Button + color="primary" + disabled={!key} + onClick={() => onResult({ key, type })}> + {t('sharedAdd')} + </Button> + <Button + autoFocus + onClick={() => onResult(null)}> + {t('sharedCancel')} + </Button> + </DialogActions> + </Dialog> + ) +} + +export default AddAttributeDialog; diff --git a/modern/src/attributes/EditAttributesView.js b/modern/src/attributes/EditAttributesView.js new file mode 100644 index 0000000..9491acc --- /dev/null +++ b/modern/src/attributes/EditAttributesView.js @@ -0,0 +1,141 @@ +import React, { useState } from 'react'; + +import t from '../common/localization'; + +import { Button, Checkbox, FilledInput, FormControl, FormControlLabel, Grid, IconButton, InputAdornment, InputLabel, makeStyles } from "@material-ui/core"; +import CloseIcon from '@material-ui/icons/Close'; +import AddIcon from '@material-ui/icons/Add'; +import AddAttributeDialog from './AddAttributeDialog'; + +const useStyles = makeStyles(theme => ({ + addButton: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(1), + }, + removeButton: { + marginRight: theme.spacing(1.5), + }, +})); + +const EditAttributesView = ({ attributes, setAttributes, definitions }) => { + const classes = useStyles(); + + const [addDialogShown, setAddDialogShown] = useState(false); + + const convertToList = (attributes) => { + let booleanList = []; + let otherList = []; + for (const key in attributes) { + const value = attributes[key]; + const type = getAttributeType(value); + if (type === 'boolean') { + booleanList.push({ key, value, type }); + } else { + otherList.push({ key, value, type }); + } + } + return otherList.concat(booleanList); + } + + const handleAddResult = (definition) => { + setAddDialogShown(false); + if (definition) { + switch(definition.type) { + case 'number': + updateAttribute(definition.key, 0); + break; + case 'boolean': + updateAttribute(definition.key, false); + break; + default: + updateAttribute(definition.key, ""); + break; + } + } + } + + const updateAttribute = (key, value) => { + let updatedAttributes = {...attributes}; + updatedAttributes[key] = value; + setAttributes(updatedAttributes); + }; + + const deleteAttribute = (key) => { + let updatedAttributes = {...attributes}; + delete updatedAttributes[key]; + setAttributes(updatedAttributes); + }; + + const getAttributeName = (key) => { + const definition = definitions[key]; + return definition ? definition.name : key; + }; + + const getAttributeType = (value) => { + if (typeof value === 'number') { + return 'number'; + } else if (typeof value === 'boolean') { + return 'boolean'; + } else { + return 'string'; + } + }; + + return ( + <> + {convertToList(attributes).map(({ key, value, type }) => { + if (type === 'boolean') { + return ( + <Grid container direction="row" justify="space-between"> + <FormControlLabel + control={ + <Checkbox + checked={value} + onChange={e => updateAttribute(key, e.target.checked)} + /> + } + label={getAttributeName(key)} /> + <IconButton className={classes.removeButton} onClick={() => deleteAttribute(key)}> + <CloseIcon /> + </IconButton> + </Grid> + ); + } else { + return ( + <FormControl variant="filled" margin="normal" key={key}> + <InputLabel>{getAttributeName(key)}</InputLabel> + <FilledInput + type={type === 'number' ? 'number' : 'text'} + defaultValue={value} + onChange={e => updateAttribute(key, e.target.value)} + endAdornment={ + <InputAdornment position="end"> + <IconButton onClick={() => deleteAttribute(key)}> + <CloseIcon /> + </IconButton> + </InputAdornment> + } + /> + </FormControl> + ); + } + })} + <Button + size="large" + variant="outlined" + color="primary" + onClick={() => setAddDialogShown(true)} + startIcon={<AddIcon />} + className={classes.addButton}> + {t('sharedAdd')} + </Button> + <AddAttributeDialog + open={addDialogShown} + onResult={handleAddResult} + definitions={definitions} + /> + </> + ); +} + +export default EditAttributesView; diff --git a/modern/src/attributes/userAttributes.js b/modern/src/attributes/userAttributes.js new file mode 100644 index 0000000..bcec29f --- /dev/null +++ b/modern/src/attributes/userAttributes.js @@ -0,0 +1,68 @@ +import t from '../common/localization' + +export default { + 'notificationTokens': { + name: t('attributeNotificationTokens'), + type: 'string', + }, + 'web.liveRouteLength': { + name: t('attributeWebLiveRouteLength'), + type: 'number', + }, + 'web.selectZoom': { + name: t('attributeWebSelectZoom'), + type: 'number', + }, + 'web.maxZoom': { + name: t('attributeWebMaxZoom'), + type: 'number', + }, + 'ui.disableReport': { + name: t('attributeUiDisableReport'), + type: 'boolean', + }, + 'ui.disableEvents': { + name: t('attributeUiDisableEvents'), + type: 'boolean', + }, + 'ui.disableVehicleFetures': { + name: t('attributeUiDisableVehicleFetures'), + type: 'boolean', + }, + 'ui.disableDrivers': { + name: t('attributeUiDisableDrivers'), + type: 'boolean', + }, + 'ui.disableComputedAttributes': { + name: t('attributeUiDisableComputedAttributes'), + type: 'boolean', + }, + 'ui.disableCalendars': { + name: t('attributeUiDisableCalendars'), + type: 'boolean', + }, + 'ui.disableMaintenance': { + name: t('attributeUiDisableMaintenance'), + type: 'boolean', + }, + 'ui.hidePositionAttributes': { + name: t('attributeUiHidePositionAttributes'), + type: 'string', + }, + 'distanceUnit': { + name: t('settingsDistanceUnit'), + type: 'string', + }, + 'speedUnit': { + name: t('settingsSpeedUnit'), + type: 'string', + }, + 'volumeUnit': { + name: t('settingsVolumeUnit'), + type: 'string', + }, + 'timezone': { + name: t('sharedTimezone'), + type: 'string', + }, +}; diff --git a/modern/src/reports/RouteReportPage.js b/modern/src/reports/RouteReportPage.js index a478ddb..37e74f4 100644 --- a/modern/src/reports/RouteReportPage.js +++ b/modern/src/reports/RouteReportPage.js @@ -86,7 +86,7 @@ const RouteReportPage = () => { <Paper className={classes.form}> <FormControl variant='filled' margin='normal' fullWidth> <InputLabel>{t('reportDevice')}</InputLabel> - <Select value={deviceId} onChange={(e) => setDeviceId(e.target.value)}> + <Select value={deviceId} onChange={e => setDeviceId(e.target.value)}> {devices.map((device) => ( <MenuItem value={device.id}>{device.name}</MenuItem> ))} @@ -94,7 +94,7 @@ const RouteReportPage = () => { </FormControl> <FormControl variant='filled' margin='normal' fullWidth> <InputLabel>{t('reportPeriod')}</InputLabel> - <Select value={period} onChange={(e) => setPeriod(e.target.value)}> + <Select value={period} onChange={e => setPeriod(e.target.value)}> <MenuItem value='today'>{t('reportToday')}</MenuItem> <MenuItem value='yesterday'>{t('reportYesterday')}</MenuItem> <MenuItem value='thisWeek'>{t('reportThisWeek')}</MenuItem> @@ -111,7 +111,7 @@ const RouteReportPage = () => { label={t('reportFrom')} type='datetime-local' value={from.format(moment.HTML5_FMT.DATETIME_LOCAL)} - onChange={(e) => setFrom(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL))} + onChange={e => setFrom(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL))} fullWidth /> } {period === 'custom' && diff --git a/web/l10n/en.json b/web/l10n/en.json index 2704343..6e0c62f 100644 --- a/web/l10n/en.json +++ b/web/l10n/en.json @@ -111,6 +111,7 @@ "attributeUiDisableCalendars": "UI: Disable Calendars", "attributeUiDisableMaintenance": "UI: Disable Maintenance", "attributeUiHidePositionAttributes": "UI: Hide Position Attributes", + "attributeNotificationTokens": "Notification Tokens", "errorTitle": "Error", "errorGeneral": "Invalid parameters or constraints violation", "errorConnection": "Connection error", |