aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Tananaev <anton@traccar.org>2022-10-28 13:28:28 -0700
committerAnton Tananaev <anton@traccar.org>2022-10-28 13:28:28 -0700
commitfac80024e0956d543b762296e0ee49cd72035b93 (patch)
tree41074dddc57d0c4fdb1b7162b1c3d74633e4ea31
parentef315b8e10329db80da1a97e96b3cc82481370ae (diff)
downloadtrackermap-web-fac80024e0956d543b762296e0ee49cd72035b93.tar.gz
trackermap-web-fac80024e0956d543b762296e0ee49cd72035b93.tar.bz2
trackermap-web-fac80024e0956d543b762296e0ee49cd72035b93.zip
Persist user preferences
-rw-r--r--modern/src/App.js40
-rw-r--r--modern/src/SocketController.js14
-rw-r--r--modern/src/common/attributes/useCommonUserAttributes.js44
-rw-r--r--modern/src/common/components/StatusCard.js6
-rw-r--r--modern/src/main/DeviceRow.js6
-rw-r--r--modern/src/main/MainMap.js4
-rw-r--r--modern/src/main/MainPage.js3
-rw-r--r--modern/src/map/MapPositions.js4
-rw-r--r--modern/src/map/core/MapView.js2
-rw-r--r--modern/src/map/main/MapSelectedDevice.js4
-rw-r--r--modern/src/map/overlay/MapOverlay.js4
-rw-r--r--modern/src/resources/l10n/en.json6
-rw-r--r--modern/src/settings/PreferencesPage.js106
13 files changed, 138 insertions, 105 deletions
diff --git a/modern/src/App.js b/modern/src/App.js
index 74a9acc0..e21cf1bf 100644
--- a/modern/src/App.js
+++ b/modern/src/App.js
@@ -1,12 +1,14 @@
import React from 'react';
-import { Outlet } from 'react-router-dom';
-import { useSelector } from 'react-redux';
+import { Outlet, useNavigate } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
import { LinearProgress, useMediaQuery } from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import theme from './common/theme';
import BottomMenu from './common/components/BottomMenu';
import SocketController from './SocketController';
import CachingController from './CachingController';
+import { useEffectAsync } from './reactHelper';
+import { sessionActions } from './store';
const useStyles = makeStyles(() => ({
page: {
@@ -20,26 +22,36 @@ const useStyles = makeStyles(() => ({
const App = () => {
const classes = useStyles();
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
const desktop = useMediaQuery(theme.breakpoints.up('md'));
const initialized = useSelector((state) => !!state.session.user);
- return (
+ useEffectAsync(async () => {
+ if (!initialized) {
+ const response = await fetch('/api/session');
+ if (response.ok) {
+ dispatch(sessionActions.updateUser(await response.json()));
+ } else {
+ navigate('/login');
+ }
+ }
+ return null;
+ }, [initialized]);
+
+ return !initialized ? (<LinearProgress />) : (
<>
<SocketController />
<CachingController />
- {!initialized ? (<LinearProgress />) : (
- <>
- <div className={classes.page}>
- <Outlet />
- </div>
- {!desktop && (
- <div className={classes.menu}>
- <BottomMenu />
- </div>
- )}
- </>
+ <div className={classes.page}>
+ <Outlet />
+ </div>
+ {!desktop && (
+ <div className={classes.menu}>
+ <BottomMenu />
+ </div>
)}
</>
);
diff --git a/modern/src/SocketController.js b/modern/src/SocketController.js
index 87b842fa..970298c3 100644
--- a/modern/src/SocketController.js
+++ b/modern/src/SocketController.js
@@ -1,23 +1,21 @@
import React, { useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector, connect } from 'react-redux';
import { Snackbar } from '@mui/material';
-import { useNavigate } from 'react-router-dom';
import { positionsActions, devicesActions, sessionActions } from './store';
import { useEffectAsync } from './reactHelper';
import { useTranslation } from './common/components/LocalizationProvider';
import { snackBarDurationLongMs } from './common/util/duration';
-import usePersistedState from './common/util/usePersistedState';
import alarm from './resources/alarm.mp3';
import { eventsActions } from './store/events';
import useFeatures from './common/util/useFeatures';
+import { useAttributePreference } from './common/util/preferences';
const logoutCode = 4000;
const SocketController = () => {
const dispatch = useDispatch();
- const navigate = useNavigate();
const t = useTranslation();
const authenticated = useSelector((state) => !!state.session.user);
@@ -28,8 +26,8 @@ const SocketController = () => {
const [events, setEvents] = useState([]);
const [notifications, setNotifications] = useState([]);
- const [soundEvents] = usePersistedState('soundEvents', []);
- const [soundAlarms] = usePersistedState('soundAlarms', ['sos']);
+ const soundEvents = useAttributePreference('soundEvents', '');
+ const soundAlarms = useAttributePreference('soundAlarms', 'sos');
const features = useFeatures();
@@ -96,12 +94,6 @@ const SocketController = () => {
}
};
}
- const response = await fetch('/api/session');
- if (response.ok) {
- dispatch(sessionActions.updateUser(await response.json()));
- } else {
- navigate('/login');
- }
return null;
}, [authenticated]);
diff --git a/modern/src/common/attributes/useCommonUserAttributes.js b/modern/src/common/attributes/useCommonUserAttributes.js
index 7fe2fcdf..845ab799 100644
--- a/modern/src/common/attributes/useCommonUserAttributes.js
+++ b/modern/src/common/attributes/useCommonUserAttributes.js
@@ -5,6 +5,50 @@ export default (t) => useMemo(() => ({
name: t('attributeShowGeofences'),
type: 'boolean',
},
+ mapLiveRoutes: {
+ name: t('mapLiveRoutes'),
+ type: 'boolean',
+ },
+ mapFollow: {
+ name: t('deviceFollow'),
+ type: 'boolean',
+ },
+ mapCluster: {
+ name: t('mapClustering'),
+ type: 'boolean',
+ },
+ mapOnSelect: {
+ name: t('mapOnSelect'),
+ type: 'boolean',
+ },
+ activeMapStyles: {
+ name: t('mapActive'),
+ type: 'string',
+ },
+ selectedMapStyle: {
+ name: t('mapDefault'),
+ type: 'string',
+ },
+ devicePrimary: {
+ name: t('devicePrimaryInfo'),
+ type: 'string',
+ },
+ deviceSecondary: {
+ name: t('deviceSecondaryInfo'),
+ type: 'string',
+ },
+ soundEvents: {
+ name: t('eventsSoundEvents'),
+ type: 'string',
+ },
+ soundAlarms: {
+ name: t('eventsSoundAlarms'),
+ type: 'string',
+ },
+ positionItems: {
+ name: t('attributePopupInfo'),
+ type: 'string',
+ },
locationIqKey: {
name: t('mapLocationIqKey'),
type: 'string',
diff --git a/modern/src/common/components/StatusCard.js b/modern/src/common/components/StatusCard.js
index c6ab1f51..7ff5769d 100644
--- a/modern/src/common/components/StatusCard.js
+++ b/modern/src/common/components/StatusCard.js
@@ -28,10 +28,10 @@ import { useTranslation } from './LocalizationProvider';
import RemoveDialog from './RemoveDialog';
import PositionValue from './PositionValue';
import { useDeviceReadonly, useRestriction } from '../util/permissions';
-import usePersistedState from '../util/usePersistedState';
import usePositionAttributes from '../attributes/usePositionAttributes';
import { devicesActions } from '../../store';
import { useCatch, useCatchCallback } from '../../reactHelper';
+import { useAttributePreference } from '../util/preferences';
const useStyles = makeStyles((theme) => ({
card: {
@@ -122,7 +122,7 @@ const StatusCard = ({ deviceId, position, onClose, disableActions, desktopPaddin
const deviceImage = device?.attributes?.deviceImage;
const positionAttributes = usePositionAttributes(t);
- const [positionItems] = usePersistedState('positionItems', ['speed', 'address', 'totalDistance', 'course']);
+ const positionItems = useAttributePreference('positionItems', 'speed,address,totalDistance,course');
const [anchorEl, setAnchorEl] = useState(null);
@@ -204,7 +204,7 @@ const StatusCard = ({ deviceId, position, onClose, disableActions, desktopPaddin
<CardContent className={classes.content}>
<Table size="small" classes={{ root: classes.table }}>
<TableBody>
- {positionItems.filter((key) => position.hasOwnProperty(key) || position.attributes.hasOwnProperty(key)).map((key) => (
+ {positionItems.split(',').filter((key) => position.hasOwnProperty(key) || position.attributes.hasOwnProperty(key)).map((key) => (
<StatusRow
key={key}
name={positionAttributes[key].name}
diff --git a/modern/src/main/DeviceRow.js b/modern/src/main/DeviceRow.js
index 11dbfdcb..5699c274 100644
--- a/modern/src/main/DeviceRow.js
+++ b/modern/src/main/DeviceRow.js
@@ -19,8 +19,8 @@ import {
import { useTranslation } from '../common/components/LocalizationProvider';
import { mapIconKey, mapIcons } from '../map/core/preloadImages';
import { useAdministrator } from '../common/util/permissions';
-import usePersistedState from '../common/util/usePersistedState';
import { ReactComponent as EngineIcon } from '../resources/images/data/engine.svg';
+import { useAttributePreference } from '../common/util/preferences';
const useStyles = makeStyles((theme) => ({
icon: {
@@ -59,8 +59,8 @@ const DeviceRow = ({ data, index, style }) => {
const geofences = useSelector((state) => state.geofences.items);
- const [devicePrimary] = usePersistedState('devicePrimary', 'name');
- const [deviceSecondary] = usePersistedState('deviceSecondary', '');
+ const devicePrimary = useAttributePreference('devicePrimary', 'name');
+ const deviceSecondary = useAttributePreference('deviceSecondary', '');
const formatProperty = (key) => {
if (key === 'geofenceIds') {
diff --git a/modern/src/main/MainMap.js b/modern/src/main/MainMap.js
index 81d214f9..279f3a85 100644
--- a/modern/src/main/MainMap.js
+++ b/modern/src/main/MainMap.js
@@ -11,7 +11,6 @@ import PoiMap from '../map/main/PoiMap';
import MapPadding from '../map/MapPadding';
import { devicesActions } from '../store';
import MapDefaultCamera from '../map/main/MapDefaultCamera';
-import usePersistedState from '../common/util/usePersistedState';
import MapLiveRoutes from '../map/main/MapLiveRoutes';
import MapPositions from '../map/MapPositions';
import MapOverlay from '../map/overlay/MapOverlay';
@@ -19,6 +18,7 @@ import MapGeocoder from '../map/geocoder/MapGeocoder';
import MapScale from '../map/MapScale';
import MapNotification from '../map/notification/MapNotification';
import useFeatures from '../common/util/useFeatures';
+import { useAttributePreference } from '../common/util/preferences';
const MainMap = ({ filteredPositions, selectedPosition, onEventsClick }) => {
const theme = useTheme();
@@ -30,7 +30,7 @@ const MainMap = ({ filteredPositions, selectedPosition, onEventsClick }) => {
const features = useFeatures();
- const [mapLiveRoutes] = usePersistedState('mapLiveRoutes', false);
+ const mapLiveRoutes = useAttributePreference('mapLiveRoutes', false);
const onMarkerClick = useCallback((_, deviceId) => {
dispatch(devicesActions.select(deviceId));
diff --git a/modern/src/main/MainPage.js b/modern/src/main/MainPage.js
index 9a21d570..0655fa92 100644
--- a/modern/src/main/MainPage.js
+++ b/modern/src/main/MainPage.js
@@ -15,6 +15,7 @@ import EventsDrawer from './EventsDrawer';
import useFilter from './useFilter';
import MainToolbar from './MainToolbar';
import MainMap from './MainMap';
+import { useAttributePreference } from '../common/util/preferences';
const useStyles = makeStyles((theme) => ({
root: {
@@ -63,7 +64,7 @@ const MainPage = () => {
const desktop = useMediaQuery(theme.breakpoints.up('md'));
- const [mapOnSelect] = usePersistedState('mapOnSelect', true);
+ const mapOnSelect = useAttributePreference('mapOnSelect', true);
const selectedDeviceId = useSelector((state) => state.devices.selectedId);
const positions = useSelector((state) => state.positions.items);
diff --git a/modern/src/map/MapPositions.js b/modern/src/map/MapPositions.js
index e4365fab..e334800c 100644
--- a/modern/src/map/MapPositions.js
+++ b/modern/src/map/MapPositions.js
@@ -2,7 +2,6 @@ import { useId, useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { map } from './core/MapView';
import { formatTime, getStatusColor } from '../common/util/formatter';
-import usePersistedState from '../common/util/usePersistedState';
import { mapIconKey } from './core/preloadImages';
import { findFonts } from './core/mapUtil';
import { useAttributePreference } from '../common/util/preferences';
@@ -15,8 +14,7 @@ const MapPositions = ({ positions, onClick, showStatus, selectedPosition, titleF
const devices = useSelector((state) => state.devices.items);
const iconScale = useAttributePreference('iconScale', 1);
-
- const [mapCluster] = usePersistedState('mapCluster', true);
+ const mapCluster = useAttributePreference('mapCluster', true);
const createFeature = (devices, position, selectedPositionId) => {
const device = devices[position.deviceId];
diff --git a/modern/src/map/core/MapView.js b/modern/src/map/core/MapView.js
index e91d0cc5..7ed5eae6 100644
--- a/modern/src/map/core/MapView.js
+++ b/modern/src/map/core/MapView.js
@@ -75,7 +75,7 @@ const MapView = ({ children }) => {
const [mapReady, setMapReady] = useState(false);
const mapStyles = useMapStyles();
- const [activeMapStyles] = usePersistedState('activeMapStyles', ['locationIqStreets', 'osm', 'carto']);
+ const activeMapStyles = useAttributePreference('activeMapStyles', 'locationIqStreets,osm,carto');
const [defaultMapStyle] = usePersistedState('selectedMapStyle', 'locationIqStreets');
const mapboxAccessToken = useAttributePreference('mapboxAccessToken');
const maxZoom = useAttributePreference('web.maxZoom');
diff --git a/modern/src/map/main/MapSelectedDevice.js b/modern/src/map/main/MapSelectedDevice.js
index c52b8df0..3f444409 100644
--- a/modern/src/map/main/MapSelectedDevice.js
+++ b/modern/src/map/main/MapSelectedDevice.js
@@ -4,7 +4,6 @@ import { useSelector } from 'react-redux';
import dimensions from '../../common/theme/dimensions';
import { map } from '../core/MapView';
import { usePrevious } from '../../reactHelper';
-import usePersistedState from '../../common/util/usePersistedState';
import { useAttributePreference } from '../../common/util/preferences';
const MapSelectedDevice = () => {
@@ -12,11 +11,10 @@ const MapSelectedDevice = () => {
const previousDeviceId = usePrevious(selectedDeviceId);
const selectZoom = useAttributePreference('web.selectZoom', 10);
+ const mapFollow = useAttributePreference('mapFollow', false);
const position = useSelector((state) => state.positions.items[selectedDeviceId]);
- const [mapFollow] = usePersistedState('mapFollow', false);
-
useEffect(() => {
if ((selectedDeviceId !== previousDeviceId || mapFollow) && position) {
map.easeTo({
diff --git a/modern/src/map/overlay/MapOverlay.js b/modern/src/map/overlay/MapOverlay.js
index 85797d4b..4db2153d 100644
--- a/modern/src/map/overlay/MapOverlay.js
+++ b/modern/src/map/overlay/MapOverlay.js
@@ -1,5 +1,5 @@
import { useId, useEffect } from 'react';
-import usePersistedState from '../../common/util/usePersistedState';
+import { useAttributePreference } from '../../common/util/preferences';
import { map } from '../core/MapView';
import useMapOverlays from './useMapOverlays';
@@ -7,7 +7,7 @@ const MapOverlay = () => {
const id = useId();
const mapOverlays = useMapOverlays();
- const [selectedMapOverlay] = usePersistedState('selectedMapOverlay');
+ const selectedMapOverlay = useAttributePreference('selectedMapOverlay');
const activeOverlay = mapOverlays.filter((overlay) => overlay.available).find((overlay) => overlay.id === selectedMapOverlay);
diff --git a/modern/src/resources/l10n/en.json b/modern/src/resources/l10n/en.json
index dd1f6723..e20faddb 100644
--- a/modern/src/resources/l10n/en.json
+++ b/modern/src/resources/l10n/en.json
@@ -153,6 +153,7 @@
"attributeUiHidePositionAttributes": "UI: Hide Position Attributes",
"attributeUiDisableLoginLanguage": "UI: Disable Login Language",
"attributeNotificationTokens": "Notification Tokens",
+ "attributePopupInfo": "Popup Info",
"errorTitle": "Error",
"errorGeneral": "Invalid parameters or constraints violation",
"errorConnection": "Connection error",
@@ -184,6 +185,8 @@
"loginLogo": "Logo",
"devicesAndState": "Devices and State",
"deviceTitle": "Devices",
+ "devicePrimaryInfo": "Device Title",
+ "deviceSecondaryInfo": "Device Detail",
"deviceIdentifier": "Identifier",
"deviceModel": "Model",
"deviceContact": "Contact",
@@ -345,6 +348,7 @@
"mapPoiLayer": "POI Layer",
"mapClustering": "Markers Clustering",
"mapOnSelect": "Show Map on Selection",
+ "mapDefault": "Default Map",
"stateTitle": "State",
"stateName": "Attribute",
"stateValue": "Value",
@@ -427,6 +431,8 @@
"eventDriverChanged": "Driver changed",
"eventMedia": "Media",
"eventsScrollToLast": "Scroll To Last",
+ "eventsSoundEvents": "Sound Events",
+ "eventsSoundAlarms": "Sound Alarms",
"alarmGeneral": "General",
"alarmSos": "SOS",
"alarmVibration": "Vibration",
diff --git a/modern/src/settings/PreferencesPage.js b/modern/src/settings/PreferencesPage.js
index 9a2b37e7..5229a03c 100644
--- a/modern/src/settings/PreferencesPage.js
+++ b/modern/src/settings/PreferencesPage.js
@@ -9,8 +9,7 @@ import makeStyles from '@mui/styles/makeStyles';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import CachedIcon from '@mui/icons-material/Cached';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
-import { useLocalization, useTranslation, useTranslationKeys } from '../common/components/LocalizationProvider';
-import usePersistedState from '../common/util/usePersistedState';
+import { useTranslation, useTranslationKeys } from '../common/components/LocalizationProvider';
import PageLayout from '../common/components/PageLayout';
import SettingsMenu from './components/SettingsMenu';
import usePositionAttributes from '../common/attributes/usePositionAttributes';
@@ -64,25 +63,13 @@ const PreferencesPage = () => {
const user = useSelector((state) => state.session.user);
const [attributes, setAttributes] = useState(user.attributes);
- const { languages, language, setLanguage } = useLocalization();
- const languageList = Object.entries(languages).map((values) => ({ code: values[0], name: values[1].name }));
-
const [token, setToken] = useState(null);
const [tokenExpiration, setTokenExpiration] = useState(moment().add(1, 'week').locale('en').format(moment.HTML5_FMT.DATE));
const mapStyles = useMapStyles();
- const [activeMapStyles, setActiveMapStyles] = usePersistedState('activeMapStyles', ['locationIqStreets', 'osm', 'carto']);
-
const mapOverlays = useMapOverlays();
- const [selectedMapOverlay, setSelectedMapOverlay] = usePersistedState('selectedMapOverlay');
const positionAttributes = usePositionAttributes(t);
- const [positionItems, setPositionItems] = usePersistedState('positionItems', ['speed', 'address', 'totalDistance', 'course']);
-
- const [mapLiveRoutes, setMapLiveRoutes] = usePersistedState('mapLiveRoutes', false);
- const [mapFollow, setMapFollow] = usePersistedState('mapFollow', false);
- const [mapCluster, setMapCluster] = usePersistedState('mapCluster', true);
- const [mapOnSelect, setMapOnSelect] = usePersistedState('mapOnSelect', true);
const filter = createFilterOptions();
@@ -104,12 +91,6 @@ const PreferencesPage = () => {
name: t(it),
}));
- const [devicePrimary, setDevicePrimary] = usePersistedState('devicePrimary', 'name');
- const [deviceSecondary, setDeviceSecondary] = usePersistedState('deviceSecondary', '');
-
- const [soundEvents, setSoundEvents] = usePersistedState('soundEvents', []);
- const [soundAlarms, setSoundAlarms] = usePersistedState('soundAlarms', ['sos']);
-
const handleSave = useCatch(async () => {
const response = await fetch(`/api/users/${user.id}`, {
method: 'PUT',
@@ -130,25 +111,6 @@ const PreferencesPage = () => {
<Accordion defaultExpanded>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="subtitle1">
- {t('sharedPreferences')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <FormControl>
- <InputLabel>{t('loginLanguage')}</InputLabel>
- <Select
- label={t('loginLanguage')}
- value={language}
- onChange={(e) => setLanguage(e.target.value)}
- >
- {languageList.map((it) => <MenuItem key={it.code} value={it.code}>{it.name}</MenuItem>)}
- </Select>
- </FormControl>
- </AccordionDetails>
- </Accordion>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
{t('userToken')}
</Typography>
</AccordionSummary>
@@ -196,11 +158,11 @@ const PreferencesPage = () => {
<InputLabel>{t('mapActive')}</InputLabel>
<Select
label={t('mapActive')}
- value={activeMapStyles}
+ value={attributes.activeMapStyles?.split(',') || ['locationIqStreets', 'osm', 'carto']}
onChange={(e, child) => {
const clicked = mapStyles.find((s) => s.id === child.props.value);
if (clicked.available) {
- setActiveMapStyles(e.target.value);
+ setAttributes({ ...attributes, activeMapStyles: e.target.value.join(',') });
} else if (clicked.id !== 'custom') {
const query = new URLSearchParams({ attribute: clicked.attribute });
navigate(`/settings/user/${user.id}?${query.toString()}`);
@@ -219,11 +181,11 @@ const PreferencesPage = () => {
<InputLabel>{t('mapOverlay')}</InputLabel>
<Select
label={t('mapOverlay')}
- value={selectedMapOverlay}
+ value={attributes.selectedMapOverlay}
onChange={(e) => {
const clicked = mapOverlays.find((o) => o.id === e.target.value);
if (!clicked || clicked.available) {
- setSelectedMapOverlay(e.target.value);
+ setAttributes({ ...attributes, selectedMapOverlay: e.target.value });
} else if (clicked.id !== 'custom') {
const query = new URLSearchParams({ attribute: clicked.attribute });
navigate(`/settings/user/${user.id}?${query.toString()}`);
@@ -243,9 +205,9 @@ const PreferencesPage = () => {
freeSolo
options={Object.keys(positionAttributes)}
getOptionLabel={(option) => (positionAttributes.hasOwnProperty(option) ? positionAttributes[option].name : option)}
- value={positionItems}
+ value={attributes.positionItems?.split(',') || ['speed', 'address', 'totalDistance', 'course']}
onChange={(_, option) => {
- setPositionItems(option);
+ setAttributes({ ...attributes, positionItems: option.join(',') });
}}
filterOptions={(options, params) => {
const filtered = filter(options, params);
@@ -257,7 +219,7 @@ const PreferencesPage = () => {
renderInput={(params) => (
<TextField
{...params}
- placeholder={t('sharedAttributes')}
+ label={t('attributePopupInfo')}
/>
)}
/>
@@ -272,19 +234,39 @@ const PreferencesPage = () => {
label={t('attributeShowGeofences')}
/>
<FormControlLabel
- control={<Checkbox checked={mapLiveRoutes} onChange={(e) => setMapLiveRoutes(e.target.checked)} />}
+ control={(
+ <Checkbox
+ checked={attributes.hasOwnProperty('mapLiveRoutes') ? attributes.mapLiveRoutes : false}
+ onChange={(e) => setAttributes({ ...attributes, mapLiveRoutes: e.target.checked })}
+ />
+ )}
label={t('mapLiveRoutes')}
/>
<FormControlLabel
- control={<Checkbox checked={mapFollow} onChange={(e) => setMapFollow(e.target.checked)} />}
+ control={(
+ <Checkbox
+ checked={attributes.hasOwnProperty('mapFollow') ? attributes.mapFollow : false}
+ onChange={(e) => setAttributes({ ...attributes, mapFollow: e.target.checked })}
+ />
+ )}
label={t('deviceFollow')}
/>
<FormControlLabel
- control={<Checkbox checked={mapCluster} onChange={(e) => setMapCluster(e.target.checked)} />}
+ control={(
+ <Checkbox
+ checked={attributes.hasOwnProperty('mapCluster') ? attributes.mapCluster : true}
+ onChange={(e) => setAttributes({ ...attributes, mapCluster: e.target.checked })}
+ />
+ )}
label={t('mapClustering')}
/>
<FormControlLabel
- control={<Checkbox checked={mapOnSelect} onChange={(e) => setMapOnSelect(e.target.checked)} />}
+ control={(
+ <Checkbox
+ checked={attributes.hasOwnProperty('mapOnSelect') ? attributes.mapOnSelect : true}
+ onChange={(e) => setAttributes({ ...attributes, mapOnSelect: e.target.checked })}
+ />
+ )}
label={t('mapOnSelect')}
/>
</FormGroup>
@@ -299,19 +281,19 @@ const PreferencesPage = () => {
<AccordionDetails className={classes.details}>
<SelectField
emptyValue={null}
- value={devicePrimary}
- onChange={(e) => setDevicePrimary(e.target.value)}
+ value={attributes.devicePrimary || 'name'}
+ onChange={(e) => setAttributes({ ...attributes, devicePrimary: e.target.value })}
data={deviceFields}
titleGetter={(it) => t(it.name)}
- label={t('sharedPrimary')}
+ label={t('devicePrimaryInfo')}
/>
<SelectField
emptyValue=""
- value={deviceSecondary}
- onChange={(e) => setDeviceSecondary(e.target.value)}
+ value={attributes.deviceSecondary || ''}
+ onChange={(e) => setAttributes({ ...attributes, deviceSecondary: e.target.value })}
data={deviceFields}
titleGetter={(it) => t(it.name)}
- label={t('sharedSecondary')}
+ label={t('deviceSecondaryInfo')}
/>
</AccordionDetails>
</Accordion>
@@ -324,20 +306,20 @@ const PreferencesPage = () => {
<AccordionDetails className={classes.details}>
<SelectField
multiple
- value={soundEvents}
- onChange={(e) => setSoundEvents(e.target.value)}
+ value={attributes.soundEvents?.split(',') || []}
+ onChange={(e) => setAttributes({ ...attributes, soundEvents: e.target.value.join(',') })}
endpoint="/api/notifications/types"
keyGetter={(it) => it.type}
titleGetter={(it) => t(prefixString('event', it.type))}
- label={t('reportEventTypes')}
+ label={t('eventsSoundEvents')}
/>
<SelectField
multiple
- value={soundAlarms}
- onChange={(e) => setSoundAlarms(e.target.value)}
+ value={attributes.soundAlarms?.split(',') || ['sos']}
+ onChange={(e) => setAttributes({ ...attributes, soundAlarms: e.target.value.join(',') })}
data={alarms}
keyGetter={(it) => it.key}
- label={t('sharedAlarms')}
+ label={t('eventsSoundAlarms')}
/>
</AccordionDetails>
</Accordion>