From d3c7705bedebd65c94f9eea691aaf2fe03b0cafe Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sat, 19 Aug 2023 13:58:45 -0700 Subject: Move to Vite --- modern/src/settings/AccumulatorsPage.js | 128 ------- modern/src/settings/AccumulatorsPage.jsx | 128 +++++++ modern/src/settings/CalendarPage.js | 217 ----------- modern/src/settings/CalendarPage.jsx | 217 +++++++++++ modern/src/settings/CalendarsPage.js | 64 ---- modern/src/settings/CalendarsPage.jsx | 64 ++++ modern/src/settings/CommandDevicePage.js | 132 ------- modern/src/settings/CommandDevicePage.jsx | 132 +++++++ modern/src/settings/CommandGroupPage.js | 126 ------- modern/src/settings/CommandGroupPage.jsx | 126 +++++++ modern/src/settings/CommandPage.js | 59 --- modern/src/settings/CommandPage.jsx | 59 +++ modern/src/settings/CommandsPage.js | 74 ---- modern/src/settings/CommandsPage.jsx | 74 ++++ modern/src/settings/ComputedAttributePage.js | 186 ---------- modern/src/settings/ComputedAttributePage.jsx | 186 ++++++++++ modern/src/settings/ComputedAttributesPage.js | 74 ---- modern/src/settings/ComputedAttributesPage.jsx | 74 ++++ modern/src/settings/DeviceConnectionsPage.js | 116 ------ modern/src/settings/DeviceConnectionsPage.jsx | 116 ++++++ modern/src/settings/DevicePage.js | 180 --------- modern/src/settings/DevicePage.jsx | 180 +++++++++ modern/src/settings/DevicesPage.js | 103 ------ modern/src/settings/DevicesPage.jsx | 103 ++++++ modern/src/settings/DriverPage.js | 71 ---- modern/src/settings/DriverPage.jsx | 71 ++++ modern/src/settings/DriversPage.js | 66 ---- modern/src/settings/DriversPage.jsx | 66 ++++ modern/src/settings/GeofencePage.js | 97 ----- modern/src/settings/GeofencePage.jsx | 97 +++++ modern/src/settings/GroupConnectionsPage.js | 116 ------ modern/src/settings/GroupConnectionsPage.jsx | 116 ++++++ modern/src/settings/GroupPage.js | 102 ------ modern/src/settings/GroupPage.jsx | 102 ++++++ modern/src/settings/GroupsPage.js | 91 ----- modern/src/settings/GroupsPage.jsx | 91 +++++ modern/src/settings/MaintenancePage.js | 174 --------- modern/src/settings/MaintenancePage.jsx | 174 +++++++++ modern/src/settings/MaintenancesPage.js | 93 ----- modern/src/settings/MaintenancesPage.jsx | 93 +++++ modern/src/settings/NotificationPage.js | 154 -------- modern/src/settings/NotificationPage.jsx | 154 ++++++++ modern/src/settings/NotificationsPage.js | 83 ----- modern/src/settings/NotificationsPage.jsx | 83 +++++ modern/src/settings/PreferencesPage.js | 398 -------------------- modern/src/settings/PreferencesPage.jsx | 398 ++++++++++++++++++++ modern/src/settings/ServerPage.js | 338 ----------------- modern/src/settings/ServerPage.jsx | 338 +++++++++++++++++ modern/src/settings/UserConnectionsPage.js | 139 ------- modern/src/settings/UserConnectionsPage.jsx | 139 +++++++ modern/src/settings/UserPage.js | 402 --------------------- modern/src/settings/UserPage.jsx | 402 +++++++++++++++++++++ modern/src/settings/UsersPage.js | 112 ------ modern/src/settings/UsersPage.jsx | 112 ++++++ .../src/settings/components/AddAttributeDialog.js | 104 ------ .../src/settings/components/AddAttributeDialog.jsx | 104 ++++++ modern/src/settings/components/BaseCommandView.js | 79 ---- modern/src/settings/components/BaseCommandView.jsx | 79 ++++ .../src/settings/components/CollectionActions.js | 104 ------ .../src/settings/components/CollectionActions.jsx | 104 ++++++ modern/src/settings/components/CollectionFab.js | 35 -- modern/src/settings/components/CollectionFab.jsx | 35 ++ .../settings/components/EditAttributesAccordion.js | 229 ------------ .../components/EditAttributesAccordion.jsx | 229 ++++++++++++ modern/src/settings/components/EditItemView.js | 120 ------ modern/src/settings/components/EditItemView.jsx | 120 ++++++ modern/src/settings/components/SearchHeader.js | 38 -- modern/src/settings/components/SearchHeader.jsx | 38 ++ modern/src/settings/components/SettingsMenu.js | 153 -------- modern/src/settings/components/SettingsMenu.jsx | 153 ++++++++ 70 files changed, 4757 insertions(+), 4757 deletions(-) delete mode 100644 modern/src/settings/AccumulatorsPage.js create mode 100644 modern/src/settings/AccumulatorsPage.jsx delete mode 100644 modern/src/settings/CalendarPage.js create mode 100644 modern/src/settings/CalendarPage.jsx delete mode 100644 modern/src/settings/CalendarsPage.js create mode 100644 modern/src/settings/CalendarsPage.jsx delete mode 100644 modern/src/settings/CommandDevicePage.js create mode 100644 modern/src/settings/CommandDevicePage.jsx delete mode 100644 modern/src/settings/CommandGroupPage.js create mode 100644 modern/src/settings/CommandGroupPage.jsx delete mode 100644 modern/src/settings/CommandPage.js create mode 100644 modern/src/settings/CommandPage.jsx delete mode 100644 modern/src/settings/CommandsPage.js create mode 100644 modern/src/settings/CommandsPage.jsx delete mode 100644 modern/src/settings/ComputedAttributePage.js create mode 100644 modern/src/settings/ComputedAttributePage.jsx delete mode 100644 modern/src/settings/ComputedAttributesPage.js create mode 100644 modern/src/settings/ComputedAttributesPage.jsx delete mode 100644 modern/src/settings/DeviceConnectionsPage.js create mode 100644 modern/src/settings/DeviceConnectionsPage.jsx delete mode 100644 modern/src/settings/DevicePage.js create mode 100644 modern/src/settings/DevicePage.jsx delete mode 100644 modern/src/settings/DevicesPage.js create mode 100644 modern/src/settings/DevicesPage.jsx delete mode 100644 modern/src/settings/DriverPage.js create mode 100644 modern/src/settings/DriverPage.jsx delete mode 100644 modern/src/settings/DriversPage.js create mode 100644 modern/src/settings/DriversPage.jsx delete mode 100644 modern/src/settings/GeofencePage.js create mode 100644 modern/src/settings/GeofencePage.jsx delete mode 100644 modern/src/settings/GroupConnectionsPage.js create mode 100644 modern/src/settings/GroupConnectionsPage.jsx delete mode 100644 modern/src/settings/GroupPage.js create mode 100644 modern/src/settings/GroupPage.jsx delete mode 100644 modern/src/settings/GroupsPage.js create mode 100644 modern/src/settings/GroupsPage.jsx delete mode 100644 modern/src/settings/MaintenancePage.js create mode 100644 modern/src/settings/MaintenancePage.jsx delete mode 100644 modern/src/settings/MaintenancesPage.js create mode 100644 modern/src/settings/MaintenancesPage.jsx delete mode 100644 modern/src/settings/NotificationPage.js create mode 100644 modern/src/settings/NotificationPage.jsx delete mode 100644 modern/src/settings/NotificationsPage.js create mode 100644 modern/src/settings/NotificationsPage.jsx delete mode 100644 modern/src/settings/PreferencesPage.js create mode 100644 modern/src/settings/PreferencesPage.jsx delete mode 100644 modern/src/settings/ServerPage.js create mode 100644 modern/src/settings/ServerPage.jsx delete mode 100644 modern/src/settings/UserConnectionsPage.js create mode 100644 modern/src/settings/UserConnectionsPage.jsx delete mode 100644 modern/src/settings/UserPage.js create mode 100644 modern/src/settings/UserPage.jsx delete mode 100644 modern/src/settings/UsersPage.js create mode 100644 modern/src/settings/UsersPage.jsx delete mode 100644 modern/src/settings/components/AddAttributeDialog.js create mode 100644 modern/src/settings/components/AddAttributeDialog.jsx delete mode 100644 modern/src/settings/components/BaseCommandView.js create mode 100644 modern/src/settings/components/BaseCommandView.jsx delete mode 100644 modern/src/settings/components/CollectionActions.js create mode 100644 modern/src/settings/components/CollectionActions.jsx delete mode 100644 modern/src/settings/components/CollectionFab.js create mode 100644 modern/src/settings/components/CollectionFab.jsx delete mode 100644 modern/src/settings/components/EditAttributesAccordion.js create mode 100644 modern/src/settings/components/EditAttributesAccordion.jsx delete mode 100644 modern/src/settings/components/EditItemView.js create mode 100644 modern/src/settings/components/EditItemView.jsx delete mode 100644 modern/src/settings/components/SearchHeader.js create mode 100644 modern/src/settings/components/SearchHeader.jsx delete mode 100644 modern/src/settings/components/SettingsMenu.js create mode 100644 modern/src/settings/components/SettingsMenu.jsx (limited to 'modern/src/settings') diff --git a/modern/src/settings/AccumulatorsPage.js b/modern/src/settings/AccumulatorsPage.js deleted file mode 100644 index 5067e4fd..00000000 --- a/modern/src/settings/AccumulatorsPage.js +++ /dev/null @@ -1,128 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { useNavigate, useParams } from 'react-router-dom'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Container, - TextField, - Button, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import { useCatch } from '../reactHelper'; -import { useAttributePreference } from '../common/util/preferences'; -import { distanceFromMeters, distanceToMeters, distanceUnitString } from '../common/util/converter'; - -const useStyles = makeStyles((theme) => ({ - container: { - marginTop: theme.spacing(2), - }, - buttons: { - marginTop: theme.spacing(2), - marginBottom: theme.spacing(2), - display: 'flex', - justifyContent: 'space-evenly', - '& > *': { - flexBasis: '33%', - }, - }, - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const AccumulatorsPage = () => { - const navigate = useNavigate(); - const classes = useStyles(); - const t = useTranslation(); - - const distanceUnit = useAttributePreference('distanceUnit'); - - const { deviceId } = useParams(); - const position = useSelector((state) => state.session.positions[deviceId]); - - const [item, setItem] = useState(); - - useEffect(() => { - if (position) { - setItem({ - deviceId: parseInt(deviceId, 10), - hours: position.attributes.hours || 0, - totalDistance: position.attributes.totalDistance || 0, - }); - } - }, [deviceId, position]); - - const handleSave = useCatch(async () => { - const response = await fetch(`/api/devices/${deviceId}/accumulators`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(item), - }); - - if (response.ok) { - navigate(-1); - } else { - throw Error(await response.text()); - } - }); - - return ( - } breadcrumbs={['sharedDeviceAccumulators']}> - {item && ( - - - }> - - {t('sharedRequired')} - - - - setItem({ ...item, hours: Number(event.target.value) * 3600000 })} - label={t('positionHours')} - /> - setItem({ ...item, totalDistance: distanceToMeters(Number(event.target.value), distanceUnit) })} - label={`${t('deviceTotalDistance')} (${distanceUnitString(distanceUnit, t)})`} - /> - - -
- - -
-
- )} -
- ); -}; - -export default AccumulatorsPage; diff --git a/modern/src/settings/AccumulatorsPage.jsx b/modern/src/settings/AccumulatorsPage.jsx new file mode 100644 index 00000000..5067e4fd --- /dev/null +++ b/modern/src/settings/AccumulatorsPage.jsx @@ -0,0 +1,128 @@ +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useNavigate, useParams } from 'react-router-dom'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + Container, + TextField, + Button, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import SettingsMenu from './components/SettingsMenu'; +import { useCatch } from '../reactHelper'; +import { useAttributePreference } from '../common/util/preferences'; +import { distanceFromMeters, distanceToMeters, distanceUnitString } from '../common/util/converter'; + +const useStyles = makeStyles((theme) => ({ + container: { + marginTop: theme.spacing(2), + }, + buttons: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + display: 'flex', + justifyContent: 'space-evenly', + '& > *': { + flexBasis: '33%', + }, + }, + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const AccumulatorsPage = () => { + const navigate = useNavigate(); + const classes = useStyles(); + const t = useTranslation(); + + const distanceUnit = useAttributePreference('distanceUnit'); + + const { deviceId } = useParams(); + const position = useSelector((state) => state.session.positions[deviceId]); + + const [item, setItem] = useState(); + + useEffect(() => { + if (position) { + setItem({ + deviceId: parseInt(deviceId, 10), + hours: position.attributes.hours || 0, + totalDistance: position.attributes.totalDistance || 0, + }); + } + }, [deviceId, position]); + + const handleSave = useCatch(async () => { + const response = await fetch(`/api/devices/${deviceId}/accumulators`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(item), + }); + + if (response.ok) { + navigate(-1); + } else { + throw Error(await response.text()); + } + }); + + return ( + } breadcrumbs={['sharedDeviceAccumulators']}> + {item && ( + + + }> + + {t('sharedRequired')} + + + + setItem({ ...item, hours: Number(event.target.value) * 3600000 })} + label={t('positionHours')} + /> + setItem({ ...item, totalDistance: distanceToMeters(Number(event.target.value), distanceUnit) })} + label={`${t('deviceTotalDistance')} (${distanceUnitString(distanceUnit, t)})`} + /> + + +
+ + +
+
+ )} +
+ ); +}; + +export default AccumulatorsPage; diff --git a/modern/src/settings/CalendarPage.js b/modern/src/settings/CalendarPage.js deleted file mode 100644 index 2868d3d5..00000000 --- a/modern/src/settings/CalendarPage.js +++ /dev/null @@ -1,217 +0,0 @@ -import moment from 'moment'; -import React, { useState } from 'react'; -import { useDispatch } from 'react-redux'; -import TextField from '@mui/material/TextField'; -import { - Accordion, AccordionSummary, AccordionDetails, Typography, FormControl, InputLabel, Select, MenuItem, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { DropzoneArea } from 'react-mui-dropzone'; -import EditItemView from './components/EditItemView'; -import EditAttributesAccordion from './components/EditAttributesAccordion'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import SettingsMenu from './components/SettingsMenu'; -import { prefixString } from '../common/util/stringUtils'; -import { calendarsActions } from '../store'; -import { useCatch } from '../reactHelper'; - -const formatCalendarTime = (time) => { - const tzid = Intl.DateTimeFormat().resolvedOptions().timeZone; - return `TZID=${tzid}:${time.locale('en').format('YYYYMMDDTHHmmss')}`; -}; - -const parseRule = (rule) => { - if (rule.endsWith('COUNT=1')) { - return { frequency: 'ONCE' }; - } - const fragments = rule.split(';'); - const frequency = fragments[0].substring(11); - const by = fragments.length > 1 ? fragments[1].split('=')[1].split(',') : null; - return { frequency, by }; -}; - -const formatRule = (rule) => { - const by = rule.by && rule.by.join(','); - switch (rule.frequency) { - case 'DAILY': - return `RRULE:FREQ=${rule.frequency}`; - case 'WEEKLY': - return `RRULE:FREQ=${rule.frequency};BYDAY=${by || 'SU'}`; - case 'MONTHLY': - return `RRULE:FREQ=${rule.frequency};BYMONTHDAY=${by || 1}`; - default: - return 'RRULE:FREQ=DAILY;COUNT=1'; - } -}; - -const updateCalendar = (lines, index, element) => window.btoa(lines.map((e, i) => (i !== index ? e : element)).join('\n')); - -const simpleCalendar = () => window.btoa([ - 'BEGIN:VCALENDAR', - 'VERSION:2.0', - 'PRODID:-//Traccar//NONSGML Traccar//EN', - 'BEGIN:VEVENT', - 'UID:00000000-0000-0000-0000-000000000000', - `DTSTART;${formatCalendarTime(moment())}`, - `DTEND;${formatCalendarTime(moment().add(1, 'hours'))}`, - 'RRULE:FREQ=DAILY', - 'SUMMARY:Event', - 'END:VEVENT', - 'END:VCALENDAR', -].join('\n')); - -const useStyles = makeStyles((theme) => ({ - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const CalendarPage = () => { - const classes = useStyles(); - const dispatch = useDispatch(); - const t = useTranslation(); - - const [item, setItem] = useState(); - - const decoded = item && item.data && window.atob(item.data); - - const simple = decoded && decoded.indexOf('//Traccar//') > 0; - - const lines = decoded && decoded.split('\n'); - - const rule = simple && parseRule(lines[7]); - - const handleFiles = (files) => { - if (files.length > 0) { - const reader = new FileReader(); - reader.onload = (event) => { - const { result } = event.target; - setItem({ ...item, data: result.substr(result.indexOf(',') + 1) }); - }; - reader.readAsDataURL(files[0]); - } - }; - - const onItemSaved = useCatch(async () => { - const response = await fetch('/api/calendars'); - if (response.ok) { - dispatch(calendarsActions.update(await response.json())); - } else { - throw Error(await response.text()); - } - }); - - const validate = () => item && item.name && item.data; - - return ( - } - breadcrumbs={['settingsTitle', 'sharedCalendar']} - > - {item && ( - <> - - }> - - {t('sharedRequired')} - - - - setItem({ ...item, name: event.target.value })} - label={t('sharedName')} - /> - - {t('sharedType')} - - - {simple ? ( - <> - { - const time = formatCalendarTime(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL)); - setItem({ ...item, data: updateCalendar(lines, 5, `DTSTART;${time}`) }); - }} - /> - { - const time = formatCalendarTime(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL)); - setItem({ ...item, data: updateCalendar(lines, 6, `DTEND;${time}`) }); - }} - /> - - {t('calendarRecurrence')} - - - {['WEEKLY', 'MONTHLY'].includes(rule.frequency) && ( - - {t('calendarDays')} - - - )} - - ) : ( - - )} - - - setItem({ ...item, attributes })} - definitions={{}} - /> - - )} - - ); -}; - -export default CalendarPage; diff --git a/modern/src/settings/CalendarPage.jsx b/modern/src/settings/CalendarPage.jsx new file mode 100644 index 00000000..2868d3d5 --- /dev/null +++ b/modern/src/settings/CalendarPage.jsx @@ -0,0 +1,217 @@ +import moment from 'moment'; +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import TextField from '@mui/material/TextField'; +import { + Accordion, AccordionSummary, AccordionDetails, Typography, FormControl, InputLabel, Select, MenuItem, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { DropzoneArea } from 'react-mui-dropzone'; +import EditItemView from './components/EditItemView'; +import EditAttributesAccordion from './components/EditAttributesAccordion'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import SettingsMenu from './components/SettingsMenu'; +import { prefixString } from '../common/util/stringUtils'; +import { calendarsActions } from '../store'; +import { useCatch } from '../reactHelper'; + +const formatCalendarTime = (time) => { + const tzid = Intl.DateTimeFormat().resolvedOptions().timeZone; + return `TZID=${tzid}:${time.locale('en').format('YYYYMMDDTHHmmss')}`; +}; + +const parseRule = (rule) => { + if (rule.endsWith('COUNT=1')) { + return { frequency: 'ONCE' }; + } + const fragments = rule.split(';'); + const frequency = fragments[0].substring(11); + const by = fragments.length > 1 ? fragments[1].split('=')[1].split(',') : null; + return { frequency, by }; +}; + +const formatRule = (rule) => { + const by = rule.by && rule.by.join(','); + switch (rule.frequency) { + case 'DAILY': + return `RRULE:FREQ=${rule.frequency}`; + case 'WEEKLY': + return `RRULE:FREQ=${rule.frequency};BYDAY=${by || 'SU'}`; + case 'MONTHLY': + return `RRULE:FREQ=${rule.frequency};BYMONTHDAY=${by || 1}`; + default: + return 'RRULE:FREQ=DAILY;COUNT=1'; + } +}; + +const updateCalendar = (lines, index, element) => window.btoa(lines.map((e, i) => (i !== index ? e : element)).join('\n')); + +const simpleCalendar = () => window.btoa([ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//Traccar//NONSGML Traccar//EN', + 'BEGIN:VEVENT', + 'UID:00000000-0000-0000-0000-000000000000', + `DTSTART;${formatCalendarTime(moment())}`, + `DTEND;${formatCalendarTime(moment().add(1, 'hours'))}`, + 'RRULE:FREQ=DAILY', + 'SUMMARY:Event', + 'END:VEVENT', + 'END:VCALENDAR', +].join('\n')); + +const useStyles = makeStyles((theme) => ({ + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const CalendarPage = () => { + const classes = useStyles(); + const dispatch = useDispatch(); + const t = useTranslation(); + + const [item, setItem] = useState(); + + const decoded = item && item.data && window.atob(item.data); + + const simple = decoded && decoded.indexOf('//Traccar//') > 0; + + const lines = decoded && decoded.split('\n'); + + const rule = simple && parseRule(lines[7]); + + const handleFiles = (files) => { + if (files.length > 0) { + const reader = new FileReader(); + reader.onload = (event) => { + const { result } = event.target; + setItem({ ...item, data: result.substr(result.indexOf(',') + 1) }); + }; + reader.readAsDataURL(files[0]); + } + }; + + const onItemSaved = useCatch(async () => { + const response = await fetch('/api/calendars'); + if (response.ok) { + dispatch(calendarsActions.update(await response.json())); + } else { + throw Error(await response.text()); + } + }); + + const validate = () => item && item.name && item.data; + + return ( + } + breadcrumbs={['settingsTitle', 'sharedCalendar']} + > + {item && ( + <> + + }> + + {t('sharedRequired')} + + + + setItem({ ...item, name: event.target.value })} + label={t('sharedName')} + /> + + {t('sharedType')} + + + {simple ? ( + <> + { + const time = formatCalendarTime(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL)); + setItem({ ...item, data: updateCalendar(lines, 5, `DTSTART;${time}`) }); + }} + /> + { + const time = formatCalendarTime(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL)); + setItem({ ...item, data: updateCalendar(lines, 6, `DTEND;${time}`) }); + }} + /> + + {t('calendarRecurrence')} + + + {['WEEKLY', 'MONTHLY'].includes(rule.frequency) && ( + + {t('calendarDays')} + + + )} + + ) : ( + + )} + + + setItem({ ...item, attributes })} + definitions={{}} + /> + + )} + + ); +}; + +export default CalendarPage; diff --git a/modern/src/settings/CalendarsPage.js b/modern/src/settings/CalendarsPage.js deleted file mode 100644 index de27a451..00000000 --- a/modern/src/settings/CalendarsPage.js +++ /dev/null @@ -1,64 +0,0 @@ -import React, { useState } from 'react'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import { useEffectAsync } from '../reactHelper'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import useSettingsStyles from './common/useSettingsStyles'; - -const CalendarsPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/calendars'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - return ( - } breadcrumbs={['settingsTitle', 'sharedCalendars']}> - - - - - {t('sharedName')} - - - - - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - - {item.name} - - - - - )) : ()} - -
- -
- ); -}; - -export default CalendarsPage; diff --git a/modern/src/settings/CalendarsPage.jsx b/modern/src/settings/CalendarsPage.jsx new file mode 100644 index 00000000..de27a451 --- /dev/null +++ b/modern/src/settings/CalendarsPage.jsx @@ -0,0 +1,64 @@ +import React, { useState } from 'react'; +import { + Table, TableRow, TableCell, TableHead, TableBody, +} from '@mui/material'; +import { useEffectAsync } from '../reactHelper'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import SettingsMenu from './components/SettingsMenu'; +import CollectionFab from './components/CollectionFab'; +import CollectionActions from './components/CollectionActions'; +import TableShimmer from '../common/components/TableShimmer'; +import SearchHeader, { filterByKeyword } from './components/SearchHeader'; +import useSettingsStyles from './common/useSettingsStyles'; + +const CalendarsPage = () => { + const classes = useSettingsStyles(); + const t = useTranslation(); + + const [timestamp, setTimestamp] = useState(Date.now()); + const [items, setItems] = useState([]); + const [searchKeyword, setSearchKeyword] = useState(''); + const [loading, setLoading] = useState(false); + + useEffectAsync(async () => { + setLoading(true); + try { + const response = await fetch('/api/calendars'); + if (response.ok) { + setItems(await response.json()); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + }, [timestamp]); + + return ( + } breadcrumbs={['settingsTitle', 'sharedCalendars']}> + + + + + {t('sharedName')} + + + + + {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( + + {item.name} + + + + + )) : ()} + +
+ +
+ ); +}; + +export default CalendarsPage; diff --git a/modern/src/settings/CommandDevicePage.js b/modern/src/settings/CommandDevicePage.js deleted file mode 100644 index ed802bfa..00000000 --- a/modern/src/settings/CommandDevicePage.js +++ /dev/null @@ -1,132 +0,0 @@ -import React, { useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Container, - Button, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import BaseCommandView from './components/BaseCommandView'; -import SelectField from '../common/components/SelectField'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import { useCatch } from '../reactHelper'; -import { useRestriction } from '../common/util/permissions'; - -const useStyles = makeStyles((theme) => ({ - container: { - marginTop: theme.spacing(2), - }, - buttons: { - marginTop: theme.spacing(2), - marginBottom: theme.spacing(2), - display: 'flex', - justifyContent: 'space-evenly', - '& > *': { - flexBasis: '33%', - }, - }, - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const CommandDevicePage = () => { - const navigate = useNavigate(); - const classes = useStyles(); - const t = useTranslation(); - - const { id } = useParams(); - - const [savedId, setSavedId] = useState(0); - const [item, setItem] = useState({}); - - const limitCommands = useRestriction('limitCommands'); - - const handleSend = useCatch(async () => { - let command; - if (savedId) { - const response = await fetch(`/api/commands/${savedId}`); - if (response.ok) { - command = await response.json(); - } else { - throw Error(await response.text()); - } - } else { - command = item; - } - - command.deviceId = parseInt(id, 10); - - const response = await fetch('/api/commands/send', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(command), - }); - - if (response.ok) { - navigate(-1); - } else { - throw Error(await response.text()); - } - }); - - const validate = () => savedId || (item && item.type); - - return ( - } breadcrumbs={['settingsTitle', 'deviceCommand']}> - - - }> - - {t('sharedRequired')} - - - - setSavedId(e.target.value)} - endpoint={`/api/commands/send?deviceId=${id}`} - titleGetter={(it) => it.description} - label={t('sharedSavedCommand')} - /> - {!limitCommands && !savedId && ( - - )} - - -
- - -
-
-
- ); -}; - -export default CommandDevicePage; diff --git a/modern/src/settings/CommandDevicePage.jsx b/modern/src/settings/CommandDevicePage.jsx new file mode 100644 index 00000000..ed802bfa --- /dev/null +++ b/modern/src/settings/CommandDevicePage.jsx @@ -0,0 +1,132 @@ +import React, { useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + Container, + Button, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import BaseCommandView from './components/BaseCommandView'; +import SelectField from '../common/components/SelectField'; +import PageLayout from '../common/components/PageLayout'; +import SettingsMenu from './components/SettingsMenu'; +import { useCatch } from '../reactHelper'; +import { useRestriction } from '../common/util/permissions'; + +const useStyles = makeStyles((theme) => ({ + container: { + marginTop: theme.spacing(2), + }, + buttons: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + display: 'flex', + justifyContent: 'space-evenly', + '& > *': { + flexBasis: '33%', + }, + }, + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const CommandDevicePage = () => { + const navigate = useNavigate(); + const classes = useStyles(); + const t = useTranslation(); + + const { id } = useParams(); + + const [savedId, setSavedId] = useState(0); + const [item, setItem] = useState({}); + + const limitCommands = useRestriction('limitCommands'); + + const handleSend = useCatch(async () => { + let command; + if (savedId) { + const response = await fetch(`/api/commands/${savedId}`); + if (response.ok) { + command = await response.json(); + } else { + throw Error(await response.text()); + } + } else { + command = item; + } + + command.deviceId = parseInt(id, 10); + + const response = await fetch('/api/commands/send', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(command), + }); + + if (response.ok) { + navigate(-1); + } else { + throw Error(await response.text()); + } + }); + + const validate = () => savedId || (item && item.type); + + return ( + } breadcrumbs={['settingsTitle', 'deviceCommand']}> + + + }> + + {t('sharedRequired')} + + + + setSavedId(e.target.value)} + endpoint={`/api/commands/send?deviceId=${id}`} + titleGetter={(it) => it.description} + label={t('sharedSavedCommand')} + /> + {!limitCommands && !savedId && ( + + )} + + +
+ + +
+
+
+ ); +}; + +export default CommandDevicePage; diff --git a/modern/src/settings/CommandGroupPage.js b/modern/src/settings/CommandGroupPage.js deleted file mode 100644 index e2ba3946..00000000 --- a/modern/src/settings/CommandGroupPage.js +++ /dev/null @@ -1,126 +0,0 @@ -import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { useNavigate, useParams } from 'react-router-dom'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Container, - Button, - FormControl, - InputLabel, - Select, - MenuItem, - FormControlLabel, - Checkbox, - TextField, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import { useCatch } from '../reactHelper'; - -const useStyles = makeStyles((theme) => ({ - container: { - marginTop: theme.spacing(2), - }, - buttons: { - marginTop: theme.spacing(2), - marginBottom: theme.spacing(2), - display: 'flex', - justifyContent: 'space-evenly', - '& > *': { - flexBasis: '33%', - }, - }, - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const CommandDevicePage = () => { - const navigate = useNavigate(); - const classes = useStyles(); - const t = useTranslation(); - - const { id } = useParams(); - - const textEnabled = useSelector((state) => state.session.server.textEnabled); - - const [item, setItem] = useState({ type: 'custom', attributes: {} }); - - const handleSend = useCatch(async () => { - const query = new URLSearchParams({ groupId: id }); - const response = await fetch(`/api/commands/send?${query.toString()}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(item), - }); - - if (response.ok) { - navigate(-1); - } else { - throw Error(await response.text()); - } - }); - - return ( - } breadcrumbs={['settingsTitle', 'deviceCommand']}> - - - }> - - {t('sharedRequired')} - - - - - {t('sharedType')} - - - setItem({ ...item, attributes: { ...item.attributes, data: e.target.value } })} - label={t('commandData')} - /> - {textEnabled && ( - setItem({ ...item, textChannel: event.target.checked })} />} - label={t('commandSendSms')} - /> - )} - - -
- - -
-
-
- ); -}; - -export default CommandDevicePage; diff --git a/modern/src/settings/CommandGroupPage.jsx b/modern/src/settings/CommandGroupPage.jsx new file mode 100644 index 00000000..e2ba3946 --- /dev/null +++ b/modern/src/settings/CommandGroupPage.jsx @@ -0,0 +1,126 @@ +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useNavigate, useParams } from 'react-router-dom'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + Container, + Button, + FormControl, + InputLabel, + Select, + MenuItem, + FormControlLabel, + Checkbox, + TextField, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import SettingsMenu from './components/SettingsMenu'; +import { useCatch } from '../reactHelper'; + +const useStyles = makeStyles((theme) => ({ + container: { + marginTop: theme.spacing(2), + }, + buttons: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + display: 'flex', + justifyContent: 'space-evenly', + '& > *': { + flexBasis: '33%', + }, + }, + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const CommandDevicePage = () => { + const navigate = useNavigate(); + const classes = useStyles(); + const t = useTranslation(); + + const { id } = useParams(); + + const textEnabled = useSelector((state) => state.session.server.textEnabled); + + const [item, setItem] = useState({ type: 'custom', attributes: {} }); + + const handleSend = useCatch(async () => { + const query = new URLSearchParams({ groupId: id }); + const response = await fetch(`/api/commands/send?${query.toString()}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(item), + }); + + if (response.ok) { + navigate(-1); + } else { + throw Error(await response.text()); + } + }); + + return ( + } breadcrumbs={['settingsTitle', 'deviceCommand']}> + + + }> + + {t('sharedRequired')} + + + + + {t('sharedType')} + + + setItem({ ...item, attributes: { ...item.attributes, data: e.target.value } })} + label={t('commandData')} + /> + {textEnabled && ( + setItem({ ...item, textChannel: event.target.checked })} />} + label={t('commandSendSms')} + /> + )} + + +
+ + +
+
+
+ ); +}; + +export default CommandDevicePage; diff --git a/modern/src/settings/CommandPage.js b/modern/src/settings/CommandPage.js deleted file mode 100644 index 1d788610..00000000 --- a/modern/src/settings/CommandPage.js +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useState } from 'react'; -import { - Accordion, AccordionSummary, AccordionDetails, Typography, TextField, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import EditItemView from './components/EditItemView'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import BaseCommandView from './components/BaseCommandView'; -import SettingsMenu from './components/SettingsMenu'; - -const useStyles = makeStyles((theme) => ({ - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const CommandPage = () => { - const classes = useStyles(); - const t = useTranslation(); - - const [item, setItem] = useState(); - - const validate = () => item && item.type; - - return ( - } - breadcrumbs={['settingsTitle', 'sharedSavedCommand']} - > - {item && ( - - }> - - {t('sharedRequired')} - - - - setItem({ ...item, description: event.target.value })} - label={t('sharedDescription')} - /> - - - - )} - - ); -}; - -export default CommandPage; diff --git a/modern/src/settings/CommandPage.jsx b/modern/src/settings/CommandPage.jsx new file mode 100644 index 00000000..1d788610 --- /dev/null +++ b/modern/src/settings/CommandPage.jsx @@ -0,0 +1,59 @@ +import React, { useState } from 'react'; +import { + Accordion, AccordionSummary, AccordionDetails, Typography, TextField, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import EditItemView from './components/EditItemView'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import BaseCommandView from './components/BaseCommandView'; +import SettingsMenu from './components/SettingsMenu'; + +const useStyles = makeStyles((theme) => ({ + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const CommandPage = () => { + const classes = useStyles(); + const t = useTranslation(); + + const [item, setItem] = useState(); + + const validate = () => item && item.type; + + return ( + } + breadcrumbs={['settingsTitle', 'sharedSavedCommand']} + > + {item && ( + + }> + + {t('sharedRequired')} + + + + setItem({ ...item, description: event.target.value })} + label={t('sharedDescription')} + /> + + + + )} + + ); +}; + +export default CommandPage; diff --git a/modern/src/settings/CommandsPage.js b/modern/src/settings/CommandsPage.js deleted file mode 100644 index 1b893831..00000000 --- a/modern/src/settings/CommandsPage.js +++ /dev/null @@ -1,74 +0,0 @@ -import React, { useState } from 'react'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import { useEffectAsync } from '../reactHelper'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import { formatBoolean } from '../common/util/formatter'; -import { prefixString } from '../common/util/stringUtils'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import { useRestriction } from '../common/util/permissions'; -import useSettingsStyles from './common/useSettingsStyles'; - -const CommandsPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - const limitCommands = useRestriction('limitCommands'); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/commands'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - return ( - } breadcrumbs={['settingsTitle', 'sharedSavedCommands']}> - - - - - {t('sharedDescription')} - {t('sharedType')} - {t('commandSendSms')} - {!limitCommands && } - - - - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - - {item.description} - {t(prefixString('command', item.type))} - {formatBoolean(item.textChannel, t)} - {!limitCommands && ( - - - - )} - - )) : ()} - -
- -
- ); -}; - -export default CommandsPage; diff --git a/modern/src/settings/CommandsPage.jsx b/modern/src/settings/CommandsPage.jsx new file mode 100644 index 00000000..1b893831 --- /dev/null +++ b/modern/src/settings/CommandsPage.jsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; +import { + Table, TableRow, TableCell, TableHead, TableBody, +} from '@mui/material'; +import { useEffectAsync } from '../reactHelper'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import { formatBoolean } from '../common/util/formatter'; +import { prefixString } from '../common/util/stringUtils'; +import PageLayout from '../common/components/PageLayout'; +import SettingsMenu from './components/SettingsMenu'; +import CollectionFab from './components/CollectionFab'; +import CollectionActions from './components/CollectionActions'; +import TableShimmer from '../common/components/TableShimmer'; +import SearchHeader, { filterByKeyword } from './components/SearchHeader'; +import { useRestriction } from '../common/util/permissions'; +import useSettingsStyles from './common/useSettingsStyles'; + +const CommandsPage = () => { + const classes = useSettingsStyles(); + const t = useTranslation(); + + const [timestamp, setTimestamp] = useState(Date.now()); + const [items, setItems] = useState([]); + const [searchKeyword, setSearchKeyword] = useState(''); + const [loading, setLoading] = useState(false); + const limitCommands = useRestriction('limitCommands'); + + useEffectAsync(async () => { + setLoading(true); + try { + const response = await fetch('/api/commands'); + if (response.ok) { + setItems(await response.json()); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + }, [timestamp]); + + return ( + } breadcrumbs={['settingsTitle', 'sharedSavedCommands']}> + + + + + {t('sharedDescription')} + {t('sharedType')} + {t('commandSendSms')} + {!limitCommands && } + + + + {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( + + {item.description} + {t(prefixString('command', item.type))} + {formatBoolean(item.textChannel, t)} + {!limitCommands && ( + + + + )} + + )) : ()} + +
+ +
+ ); +}; + +export default CommandsPage; diff --git a/modern/src/settings/ComputedAttributePage.js b/modern/src/settings/ComputedAttributePage.js deleted file mode 100644 index 3bae8231..00000000 --- a/modern/src/settings/ComputedAttributePage.js +++ /dev/null @@ -1,186 +0,0 @@ -import React, { useState } from 'react'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - FormControl, - InputLabel, - MenuItem, - Select, - TextField, - createFilterOptions, - Autocomplete, - Button, - Snackbar, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import EditItemView from './components/EditItemView'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import usePositionAttributes from '../common/attributes/usePositionAttributes'; -import SettingsMenu from './components/SettingsMenu'; -import SelectField from '../common/components/SelectField'; -import { useCatch } from '../reactHelper'; -import { snackBarDurationLongMs } from '../common/util/duration'; - -const allowedProperties = ['valid', 'latitude', 'longitude', 'altitude', 'speed', 'course', 'address', 'accuracy']; - -const useStyles = makeStyles((theme) => ({ - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const ComputedAttributePage = () => { - const classes = useStyles(); - const t = useTranslation(); - - const positionAttributes = usePositionAttributes(t); - - const [item, setItem] = useState(); - const [deviceId, setDeviceId] = useState(); - const [result, setResult] = useState(); - - const options = Object.entries(positionAttributes).filter(([key, value]) => !value.property || allowedProperties.includes(key)).map(([key, value]) => ({ - key, - name: value.name, - type: value.type, - })); - - const filter = createFilterOptions({ - stringify: (option) => option.name, - }); - - const testAttribute = useCatch(async () => { - const query = new URLSearchParams({ deviceId }); - const url = `/api/attributes/computed/test?${query.toString()}`; - const response = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(item), - }); - if (response.ok) { - setResult(await response.text()); - } else { - throw Error(await response.text()); - } - }); - - const validate = () => item && item.description && item.expression; - - return ( - } - breadcrumbs={['settingsTitle', 'sharedComputedAttribute']} - > - {item && ( - <> - - }> - - {t('sharedRequired')} - - - - setItem({ ...item, description: e.target.value })} - label={t('sharedDescription')} - /> - option.key === item.attribute) || item.attribute} - onChange={(_, option) => { - const attribute = option ? option.key || option : null; - if (option && option.type) { - setItem({ ...item, attribute, type: option.type }); - } else { - setItem({ ...item, attribute }); - } - }} - 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) => option.name || option} - renderOption={(props, option) => ( -
  • - {option.name} -
  • - )} - renderInput={(params) => ( - - )} - freeSolo - /> - setItem({ ...item, expression: e.target.value })} - label={t('sharedExpression')} - multiline - rows={4} - /> - - {t('sharedType')} - - -
    -
    - - }> - - {t('sharedTest')} - - - - setDeviceId(Number(e.target.value))} - endpoint="/api/devices" - label={t('sharedDevice')} - /> - - setResult(null)} - autoHideDuration={snackBarDurationLongMs} - message={result} - /> - - - - )} -
    - ); -}; - -export default ComputedAttributePage; diff --git a/modern/src/settings/ComputedAttributePage.jsx b/modern/src/settings/ComputedAttributePage.jsx new file mode 100644 index 00000000..3bae8231 --- /dev/null +++ b/modern/src/settings/ComputedAttributePage.jsx @@ -0,0 +1,186 @@ +import React, { useState } from 'react'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + FormControl, + InputLabel, + MenuItem, + Select, + TextField, + createFilterOptions, + Autocomplete, + Button, + Snackbar, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import EditItemView from './components/EditItemView'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import usePositionAttributes from '../common/attributes/usePositionAttributes'; +import SettingsMenu from './components/SettingsMenu'; +import SelectField from '../common/components/SelectField'; +import { useCatch } from '../reactHelper'; +import { snackBarDurationLongMs } from '../common/util/duration'; + +const allowedProperties = ['valid', 'latitude', 'longitude', 'altitude', 'speed', 'course', 'address', 'accuracy']; + +const useStyles = makeStyles((theme) => ({ + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const ComputedAttributePage = () => { + const classes = useStyles(); + const t = useTranslation(); + + const positionAttributes = usePositionAttributes(t); + + const [item, setItem] = useState(); + const [deviceId, setDeviceId] = useState(); + const [result, setResult] = useState(); + + const options = Object.entries(positionAttributes).filter(([key, value]) => !value.property || allowedProperties.includes(key)).map(([key, value]) => ({ + key, + name: value.name, + type: value.type, + })); + + const filter = createFilterOptions({ + stringify: (option) => option.name, + }); + + const testAttribute = useCatch(async () => { + const query = new URLSearchParams({ deviceId }); + const url = `/api/attributes/computed/test?${query.toString()}`; + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(item), + }); + if (response.ok) { + setResult(await response.text()); + } else { + throw Error(await response.text()); + } + }); + + const validate = () => item && item.description && item.expression; + + return ( + } + breadcrumbs={['settingsTitle', 'sharedComputedAttribute']} + > + {item && ( + <> + + }> + + {t('sharedRequired')} + + + + setItem({ ...item, description: e.target.value })} + label={t('sharedDescription')} + /> + option.key === item.attribute) || item.attribute} + onChange={(_, option) => { + const attribute = option ? option.key || option : null; + if (option && option.type) { + setItem({ ...item, attribute, type: option.type }); + } else { + setItem({ ...item, attribute }); + } + }} + 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) => option.name || option} + renderOption={(props, option) => ( +
  • + {option.name} +
  • + )} + renderInput={(params) => ( + + )} + freeSolo + /> + setItem({ ...item, expression: e.target.value })} + label={t('sharedExpression')} + multiline + rows={4} + /> + + {t('sharedType')} + + +
    +
    + + }> + + {t('sharedTest')} + + + + setDeviceId(Number(e.target.value))} + endpoint="/api/devices" + label={t('sharedDevice')} + /> + + setResult(null)} + autoHideDuration={snackBarDurationLongMs} + message={result} + /> + + + + )} +
    + ); +}; + +export default ComputedAttributePage; diff --git a/modern/src/settings/ComputedAttributesPage.js b/modern/src/settings/ComputedAttributesPage.js deleted file mode 100644 index 6d098547..00000000 --- a/modern/src/settings/ComputedAttributesPage.js +++ /dev/null @@ -1,74 +0,0 @@ -import React, { useState } from 'react'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import { useEffectAsync } from '../reactHelper'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import { useAdministrator } from '../common/util/permissions'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import useSettingsStyles from './common/useSettingsStyles'; - -const ComputedAttributesPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - const administrator = useAdministrator(); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/attributes/computed'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - return ( - } breadcrumbs={['settingsTitle', 'sharedComputedAttributes']}> - - - - - {t('sharedDescription')} - {t('sharedAttribute')} - {t('sharedExpression')} - {t('sharedType')} - {administrator && } - - - - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - - {item.description} - {item.attribute} - {item.expression} - {item.type} - {administrator && ( - - - - )} - - )) : ()} - -
    - -
    - ); -}; - -export default ComputedAttributesPage; diff --git a/modern/src/settings/ComputedAttributesPage.jsx b/modern/src/settings/ComputedAttributesPage.jsx new file mode 100644 index 00000000..6d098547 --- /dev/null +++ b/modern/src/settings/ComputedAttributesPage.jsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; +import { + Table, TableRow, TableCell, TableHead, TableBody, +} from '@mui/material'; +import { useEffectAsync } from '../reactHelper'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import { useAdministrator } from '../common/util/permissions'; +import PageLayout from '../common/components/PageLayout'; +import SettingsMenu from './components/SettingsMenu'; +import CollectionFab from './components/CollectionFab'; +import CollectionActions from './components/CollectionActions'; +import TableShimmer from '../common/components/TableShimmer'; +import SearchHeader, { filterByKeyword } from './components/SearchHeader'; +import useSettingsStyles from './common/useSettingsStyles'; + +const ComputedAttributesPage = () => { + const classes = useSettingsStyles(); + const t = useTranslation(); + + const [timestamp, setTimestamp] = useState(Date.now()); + const [items, setItems] = useState([]); + const [searchKeyword, setSearchKeyword] = useState(''); + const [loading, setLoading] = useState(false); + const administrator = useAdministrator(); + + useEffectAsync(async () => { + setLoading(true); + try { + const response = await fetch('/api/attributes/computed'); + if (response.ok) { + setItems(await response.json()); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + }, [timestamp]); + + return ( + } breadcrumbs={['settingsTitle', 'sharedComputedAttributes']}> + + + + + {t('sharedDescription')} + {t('sharedAttribute')} + {t('sharedExpression')} + {t('sharedType')} + {administrator && } + + + + {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( + + {item.description} + {item.attribute} + {item.expression} + {item.type} + {administrator && ( + + + + )} + + )) : ()} + +
    + +
    + ); +}; + +export default ComputedAttributesPage; diff --git a/modern/src/settings/DeviceConnectionsPage.js b/modern/src/settings/DeviceConnectionsPage.js deleted file mode 100644 index 88d47872..00000000 --- a/modern/src/settings/DeviceConnectionsPage.js +++ /dev/null @@ -1,116 +0,0 @@ -import React from 'react'; -import { useParams } from 'react-router-dom'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Container, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import LinkField from '../common/components/LinkField'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import SettingsMenu from './components/SettingsMenu'; -import { formatNotificationTitle } from '../common/util/formatter'; -import PageLayout from '../common/components/PageLayout'; -import useFeatures from '../common/util/useFeatures'; - -const useStyles = makeStyles((theme) => ({ - container: { - marginTop: theme.spacing(2), - }, - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const DeviceConnectionsPage = () => { - const classes = useStyles(); - const t = useTranslation(); - - const { id } = useParams(); - - const features = useFeatures(); - - return ( - } - breadcrumbs={['settingsTitle', 'sharedDevice', 'sharedConnections']} - > - - - }> - - {t('sharedConnections')} - - - - - formatNotificationTitle(t, it)} - label={t('sharedNotifications')} - /> - {!features.disableDrivers && ( - - )} - {!features.disableComputedAttributes && ( - it.description} - label={t('sharedComputedAttributes')} - /> - )} - it.description} - label={t('sharedSavedCommands')} - /> - {!features.disableMaintenance && ( - - )} - - - - - ); -}; - -export default DeviceConnectionsPage; diff --git a/modern/src/settings/DeviceConnectionsPage.jsx b/modern/src/settings/DeviceConnectionsPage.jsx new file mode 100644 index 00000000..88d47872 --- /dev/null +++ b/modern/src/settings/DeviceConnectionsPage.jsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + Container, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import LinkField from '../common/components/LinkField'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import SettingsMenu from './components/SettingsMenu'; +import { formatNotificationTitle } from '../common/util/formatter'; +import PageLayout from '../common/components/PageLayout'; +import useFeatures from '../common/util/useFeatures'; + +const useStyles = makeStyles((theme) => ({ + container: { + marginTop: theme.spacing(2), + }, + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const DeviceConnectionsPage = () => { + const classes = useStyles(); + const t = useTranslation(); + + const { id } = useParams(); + + const features = useFeatures(); + + return ( + } + breadcrumbs={['settingsTitle', 'sharedDevice', 'sharedConnections']} + > + + + }> + + {t('sharedConnections')} + + + + + formatNotificationTitle(t, it)} + label={t('sharedNotifications')} + /> + {!features.disableDrivers && ( + + )} + {!features.disableComputedAttributes && ( + it.description} + label={t('sharedComputedAttributes')} + /> + )} + it.description} + label={t('sharedSavedCommands')} + /> + {!features.disableMaintenance && ( + + )} + + + + + ); +}; + +export default DeviceConnectionsPage; diff --git a/modern/src/settings/DevicePage.js b/modern/src/settings/DevicePage.js deleted file mode 100644 index 11d122d3..00000000 --- a/modern/src/settings/DevicePage.js +++ /dev/null @@ -1,180 +0,0 @@ -import React, { useState } from 'react'; -import moment from 'moment'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - FormControlLabel, - Checkbox, - TextField, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { DropzoneArea } from 'react-mui-dropzone'; -import EditItemView from './components/EditItemView'; -import EditAttributesAccordion from './components/EditAttributesAccordion'; -import SelectField from '../common/components/SelectField'; -import deviceCategories from '../common/util/deviceCategories'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import useDeviceAttributes from '../common/attributes/useDeviceAttributes'; -import { useAdministrator } from '../common/util/permissions'; -import SettingsMenu from './components/SettingsMenu'; -import useCommonDeviceAttributes from '../common/attributes/useCommonDeviceAttributes'; -import { useCatch } from '../reactHelper'; - -const useStyles = makeStyles((theme) => ({ - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const DevicePage = () => { - const classes = useStyles(); - const t = useTranslation(); - - const admin = useAdministrator(); - - const commonDeviceAttributes = useCommonDeviceAttributes(t); - const deviceAttributes = useDeviceAttributes(t); - - const [item, setItem] = useState(); - - const handleFiles = useCatch(async (files) => { - if (files.length > 0) { - const response = await fetch(`/api/devices/${item.id}/image`, { - method: 'POST', - body: files[0], - }); - if (response.ok) { - setItem({ ...item, attributes: { ...item.attributes, deviceImage: await response.text() } }); - } else { - throw Error(await response.text()); - } - } - }); - - const validate = () => item && item.name && item.uniqueId; - - return ( - } - breadcrumbs={['settingsTitle', 'sharedDevice']} - > - {item && ( - <> - - }> - - {t('sharedRequired')} - - - - setItem({ ...item, name: event.target.value })} - label={t('sharedName')} - /> - setItem({ ...item, uniqueId: event.target.value })} - label={t('deviceIdentifier')} - helperText={t('deviceIdentifierHelp')} - /> - - - - }> - - {t('sharedExtra')} - - - - setItem({ ...item, groupId: Number(event.target.value) })} - endpoint="/api/groups" - label={t('groupParent')} - /> - setItem({ ...item, phone: event.target.value })} - label={t('sharedPhone')} - /> - setItem({ ...item, model: event.target.value })} - label={t('deviceModel')} - /> - setItem({ ...item, contact: event.target.value })} - label={t('deviceContact')} - /> - setItem({ ...item, category: event.target.value })} - data={deviceCategories.map((category) => ({ - id: category, - name: t(`category${category.replace(/^\w/, (c) => c.toUpperCase())}`), - }))} - label={t('deviceCategory')} - /> - setItem({ ...item, calendarId: Number(event.target.value) })} - endpoint="/api/calendars" - label={t('sharedCalendar')} - /> - setItem({ ...item, expirationTime: moment(e.target.value, moment.HTML5_FMT.DATE).locale('en').format() })} - disabled={!admin} - /> - setItem({ ...item, disabled: event.target.checked })} />} - label={t('sharedDisabled')} - disabled={!admin} - /> - - - {item.id && ( - - }> - - {t('attributeDeviceImage')} - - - - - - - )} - setItem({ ...item, attributes })} - definitions={{ ...commonDeviceAttributes, ...deviceAttributes }} - /> - - )} - - ); -}; - -export default DevicePage; diff --git a/modern/src/settings/DevicePage.jsx b/modern/src/settings/DevicePage.jsx new file mode 100644 index 00000000..11d122d3 --- /dev/null +++ b/modern/src/settings/DevicePage.jsx @@ -0,0 +1,180 @@ +import React, { useState } from 'react'; +import moment from 'moment'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + FormControlLabel, + Checkbox, + TextField, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { DropzoneArea } from 'react-mui-dropzone'; +import EditItemView from './components/EditItemView'; +import EditAttributesAccordion from './components/EditAttributesAccordion'; +import SelectField from '../common/components/SelectField'; +import deviceCategories from '../common/util/deviceCategories'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import useDeviceAttributes from '../common/attributes/useDeviceAttributes'; +import { useAdministrator } from '../common/util/permissions'; +import SettingsMenu from './components/SettingsMenu'; +import useCommonDeviceAttributes from '../common/attributes/useCommonDeviceAttributes'; +import { useCatch } from '../reactHelper'; + +const useStyles = makeStyles((theme) => ({ + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const DevicePage = () => { + const classes = useStyles(); + const t = useTranslation(); + + const admin = useAdministrator(); + + const commonDeviceAttributes = useCommonDeviceAttributes(t); + const deviceAttributes = useDeviceAttributes(t); + + const [item, setItem] = useState(); + + const handleFiles = useCatch(async (files) => { + if (files.length > 0) { + const response = await fetch(`/api/devices/${item.id}/image`, { + method: 'POST', + body: files[0], + }); + if (response.ok) { + setItem({ ...item, attributes: { ...item.attributes, deviceImage: await response.text() } }); + } else { + throw Error(await response.text()); + } + } + }); + + const validate = () => item && item.name && item.uniqueId; + + return ( + } + breadcrumbs={['settingsTitle', 'sharedDevice']} + > + {item && ( + <> + + }> + + {t('sharedRequired')} + + + + setItem({ ...item, name: event.target.value })} + label={t('sharedName')} + /> + setItem({ ...item, uniqueId: event.target.value })} + label={t('deviceIdentifier')} + helperText={t('deviceIdentifierHelp')} + /> + + + + }> + + {t('sharedExtra')} + + + + setItem({ ...item, groupId: Number(event.target.value) })} + endpoint="/api/groups" + label={t('groupParent')} + /> + setItem({ ...item, phone: event.target.value })} + label={t('sharedPhone')} + /> + setItem({ ...item, model: event.target.value })} + label={t('deviceModel')} + /> + setItem({ ...item, contact: event.target.value })} + label={t('deviceContact')} + /> + setItem({ ...item, category: event.target.value })} + data={deviceCategories.map((category) => ({ + id: category, + name: t(`category${category.replace(/^\w/, (c) => c.toUpperCase())}`), + }))} + label={t('deviceCategory')} + /> + setItem({ ...item, calendarId: Number(event.target.value) })} + endpoint="/api/calendars" + label={t('sharedCalendar')} + /> + setItem({ ...item, expirationTime: moment(e.target.value, moment.HTML5_FMT.DATE).locale('en').format() })} + disabled={!admin} + /> + setItem({ ...item, disabled: event.target.checked })} />} + label={t('sharedDisabled')} + disabled={!admin} + /> + + + {item.id && ( + + }> + + {t('attributeDeviceImage')} + + + + + + + )} + setItem({ ...item, attributes })} + definitions={{ ...commonDeviceAttributes, ...deviceAttributes }} + /> + + )} + + ); +}; + +export default DevicePage; diff --git a/modern/src/settings/DevicesPage.js b/modern/src/settings/DevicesPage.js deleted file mode 100644 index 5ef5aae5..00000000 --- a/modern/src/settings/DevicesPage.js +++ /dev/null @@ -1,103 +0,0 @@ -import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { useNavigate } from 'react-router-dom'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import LinkIcon from '@mui/icons-material/Link'; -import { useEffectAsync } from '../reactHelper'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import { usePreference } from '../common/util/preferences'; -import { formatTime } from '../common/util/formatter'; -import { useDeviceReadonly } from '../common/util/permissions'; -import useSettingsStyles from './common/useSettingsStyles'; - -const DevicesPage = () => { - const classes = useSettingsStyles(); - const navigate = useNavigate(); - const t = useTranslation(); - - const groups = useSelector((state) => state.groups.items); - - const hours12 = usePreference('twelveHourFormat'); - - const deviceReadonly = useDeviceReadonly(); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/devices'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - const actionConnections = { - key: 'connections', - title: t('sharedConnections'), - icon: , - handler: (deviceId) => navigate(`/settings/device/${deviceId}/connections`), - }; - - return ( - } breadcrumbs={['settingsTitle', 'deviceTitle']}> - - - - - {t('sharedName')} - {t('deviceIdentifier')} - {t('groupParent')} - {t('sharedPhone')} - {t('deviceModel')} - {t('deviceContact')} - {t('userExpirationTime')} - - - - - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - - {item.name} - {item.uniqueId} - {item.groupId ? groups[item.groupId]?.name : null} - {item.phone} - {item.model} - {item.contact} - {formatTime(item.expirationTime, 'date', hours12)} - - - - - )) : ()} - -
    - -
    - ); -}; - -export default DevicesPage; diff --git a/modern/src/settings/DevicesPage.jsx b/modern/src/settings/DevicesPage.jsx new file mode 100644 index 00000000..5ef5aae5 --- /dev/null +++ b/modern/src/settings/DevicesPage.jsx @@ -0,0 +1,103 @@ +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import { + Table, TableRow, TableCell, TableHead, TableBody, +} from '@mui/material'; +import LinkIcon from '@mui/icons-material/Link'; +import { useEffectAsync } from '../reactHelper'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import SettingsMenu from './components/SettingsMenu'; +import CollectionFab from './components/CollectionFab'; +import CollectionActions from './components/CollectionActions'; +import TableShimmer from '../common/components/TableShimmer'; +import SearchHeader, { filterByKeyword } from './components/SearchHeader'; +import { usePreference } from '../common/util/preferences'; +import { formatTime } from '../common/util/formatter'; +import { useDeviceReadonly } from '../common/util/permissions'; +import useSettingsStyles from './common/useSettingsStyles'; + +const DevicesPage = () => { + const classes = useSettingsStyles(); + const navigate = useNavigate(); + const t = useTranslation(); + + const groups = useSelector((state) => state.groups.items); + + const hours12 = usePreference('twelveHourFormat'); + + const deviceReadonly = useDeviceReadonly(); + + const [timestamp, setTimestamp] = useState(Date.now()); + const [items, setItems] = useState([]); + const [searchKeyword, setSearchKeyword] = useState(''); + const [loading, setLoading] = useState(false); + + useEffectAsync(async () => { + setLoading(true); + try { + const response = await fetch('/api/devices'); + if (response.ok) { + setItems(await response.json()); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + }, [timestamp]); + + const actionConnections = { + key: 'connections', + title: t('sharedConnections'), + icon: , + handler: (deviceId) => navigate(`/settings/device/${deviceId}/connections`), + }; + + return ( + } breadcrumbs={['settingsTitle', 'deviceTitle']}> + + + + + {t('sharedName')} + {t('deviceIdentifier')} + {t('groupParent')} + {t('sharedPhone')} + {t('deviceModel')} + {t('deviceContact')} + {t('userExpirationTime')} + + + + + {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( + + {item.name} + {item.uniqueId} + {item.groupId ? groups[item.groupId]?.name : null} + {item.phone} + {item.model} + {item.contact} + {formatTime(item.expirationTime, 'date', hours12)} + + + + + )) : ()} + +
    + +
    + ); +}; + +export default DevicesPage; diff --git a/modern/src/settings/DriverPage.js b/modern/src/settings/DriverPage.js deleted file mode 100644 index 83d1f88f..00000000 --- a/modern/src/settings/DriverPage.js +++ /dev/null @@ -1,71 +0,0 @@ -import React, { useState } from 'react'; -import TextField from '@mui/material/TextField'; -import { - Accordion, AccordionSummary, AccordionDetails, Typography, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import EditItemView from './components/EditItemView'; -import EditAttributesAccordion from './components/EditAttributesAccordion'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import SettingsMenu from './components/SettingsMenu'; - -const useStyles = makeStyles((theme) => ({ - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const DriverPage = () => { - const classes = useStyles(); - const t = useTranslation(); - - const [item, setItem] = useState(); - - const validate = () => item && item.name && item.uniqueId; - - return ( - } - breadcrumbs={['settingsTitle', 'sharedDriver']} - > - {item && ( - <> - - }> - - {t('sharedRequired')} - - - - setItem({ ...item, name: event.target.value })} - label={t('sharedName')} - /> - setItem({ ...item, uniqueId: event.target.value })} - label={t('deviceIdentifier')} - /> - - - setItem({ ...item, attributes })} - definitions={{}} - /> - - )} - - ); -}; - -export default DriverPage; diff --git a/modern/src/settings/DriverPage.jsx b/modern/src/settings/DriverPage.jsx new file mode 100644 index 00000000..83d1f88f --- /dev/null +++ b/modern/src/settings/DriverPage.jsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react'; +import TextField from '@mui/material/TextField'; +import { + Accordion, AccordionSummary, AccordionDetails, Typography, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import EditItemView from './components/EditItemView'; +import EditAttributesAccordion from './components/EditAttributesAccordion'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import SettingsMenu from './components/SettingsMenu'; + +const useStyles = makeStyles((theme) => ({ + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const DriverPage = () => { + const classes = useStyles(); + const t = useTranslation(); + + const [item, setItem] = useState(); + + const validate = () => item && item.name && item.uniqueId; + + return ( + } + breadcrumbs={['settingsTitle', 'sharedDriver']} + > + {item && ( + <> + + }> + + {t('sharedRequired')} + + + + setItem({ ...item, name: event.target.value })} + label={t('sharedName')} + /> + setItem({ ...item, uniqueId: event.target.value })} + label={t('deviceIdentifier')} + /> + + + setItem({ ...item, attributes })} + definitions={{}} + /> + + )} + + ); +}; + +export default DriverPage; diff --git a/modern/src/settings/DriversPage.js b/modern/src/settings/DriversPage.js deleted file mode 100644 index 72834860..00000000 --- a/modern/src/settings/DriversPage.js +++ /dev/null @@ -1,66 +0,0 @@ -import React, { useState } from 'react'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import { useEffectAsync } from '../reactHelper'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import useSettingsStyles from './common/useSettingsStyles'; - -const DriversPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/drivers'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - return ( - } breadcrumbs={['settingsTitle', 'sharedDrivers']}> - - - - - {t('sharedName')} - {t('deviceIdentifier')} - - - - - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - - {item.name} - {item.uniqueId} - - - - - )) : ()} - -
    - -
    - ); -}; - -export default DriversPage; diff --git a/modern/src/settings/DriversPage.jsx b/modern/src/settings/DriversPage.jsx new file mode 100644 index 00000000..72834860 --- /dev/null +++ b/modern/src/settings/DriversPage.jsx @@ -0,0 +1,66 @@ +import React, { useState } from 'react'; +import { + Table, TableRow, TableCell, TableHead, TableBody, +} from '@mui/material'; +import { useEffectAsync } from '../reactHelper'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import SettingsMenu from './components/SettingsMenu'; +import CollectionFab from './components/CollectionFab'; +import CollectionActions from './components/CollectionActions'; +import TableShimmer from '../common/components/TableShimmer'; +import SearchHeader, { filterByKeyword } from './components/SearchHeader'; +import useSettingsStyles from './common/useSettingsStyles'; + +const DriversPage = () => { + const classes = useSettingsStyles(); + const t = useTranslation(); + + const [timestamp, setTimestamp] = useState(Date.now()); + const [items, setItems] = useState([]); + const [searchKeyword, setSearchKeyword] = useState(''); + const [loading, setLoading] = useState(false); + + useEffectAsync(async () => { + setLoading(true); + try { + const response = await fetch('/api/drivers'); + if (response.ok) { + setItems(await response.json()); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + }, [timestamp]); + + return ( + } breadcrumbs={['settingsTitle', 'sharedDrivers']}> + + + + + {t('sharedName')} + {t('deviceIdentifier')} + + + + + {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( + + {item.name} + {item.uniqueId} + + + + + )) : ()} + +
    + +
    + ); +}; + +export default DriversPage; diff --git a/modern/src/settings/GeofencePage.js b/modern/src/settings/GeofencePage.js deleted file mode 100644 index b6c516a1..00000000 --- a/modern/src/settings/GeofencePage.js +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useState } from 'react'; -import { useDispatch } from 'react-redux'; -import { - Accordion, AccordionSummary, AccordionDetails, Typography, TextField, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import EditItemView from './components/EditItemView'; -import EditAttributesAccordion from './components/EditAttributesAccordion'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import useGeofenceAttributes from '../common/attributes/useGeofenceAttributes'; -import SettingsMenu from './components/SettingsMenu'; -import SelectField from '../common/components/SelectField'; -import { geofencesActions } from '../store'; - -const useStyles = makeStyles((theme) => ({ - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const GeofencePage = () => { - const classes = useStyles(); - const dispatch = useDispatch(); - const t = useTranslation(); - - const geofenceAttributes = useGeofenceAttributes(t); - - const [item, setItem] = useState(); - - const onItemSaved = (result) => { - dispatch(geofencesActions.update([result])); - }; - - const validate = () => item && item.name; - - return ( - } - breadcrumbs={['settingsTitle', 'sharedGeofence']} - > - {item && ( - <> - - }> - - {t('sharedRequired')} - - - - setItem({ ...item, name: event.target.value })} - label={t('sharedName')} - /> - - - - }> - - {t('sharedExtra')} - - - - setItem({ ...item, description: event.target.value })} - label={t('sharedDescription')} - /> - setItem({ ...item, calendarId: Number(event.target.value) })} - endpoint="/api/calendars" - label={t('sharedCalendar')} - /> - - - setItem({ ...item, attributes })} - definitions={geofenceAttributes} - /> - - )} - - ); -}; - -export default GeofencePage; diff --git a/modern/src/settings/GeofencePage.jsx b/modern/src/settings/GeofencePage.jsx new file mode 100644 index 00000000..b6c516a1 --- /dev/null +++ b/modern/src/settings/GeofencePage.jsx @@ -0,0 +1,97 @@ +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { + Accordion, AccordionSummary, AccordionDetails, Typography, TextField, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import EditItemView from './components/EditItemView'; +import EditAttributesAccordion from './components/EditAttributesAccordion'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import useGeofenceAttributes from '../common/attributes/useGeofenceAttributes'; +import SettingsMenu from './components/SettingsMenu'; +import SelectField from '../common/components/SelectField'; +import { geofencesActions } from '../store'; + +const useStyles = makeStyles((theme) => ({ + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const GeofencePage = () => { + const classes = useStyles(); + const dispatch = useDispatch(); + const t = useTranslation(); + + const geofenceAttributes = useGeofenceAttributes(t); + + const [item, setItem] = useState(); + + const onItemSaved = (result) => { + dispatch(geofencesActions.update([result])); + }; + + const validate = () => item && item.name; + + return ( + } + breadcrumbs={['settingsTitle', 'sharedGeofence']} + > + {item && ( + <> + + }> + + {t('sharedRequired')} + + + + setItem({ ...item, name: event.target.value })} + label={t('sharedName')} + /> + + + + }> + + {t('sharedExtra')} + + + + setItem({ ...item, description: event.target.value })} + label={t('sharedDescription')} + /> + setItem({ ...item, calendarId: Number(event.target.value) })} + endpoint="/api/calendars" + label={t('sharedCalendar')} + /> + + + setItem({ ...item, attributes })} + definitions={geofenceAttributes} + /> + + )} + + ); +}; + +export default GeofencePage; diff --git a/modern/src/settings/GroupConnectionsPage.js b/modern/src/settings/GroupConnectionsPage.js deleted file mode 100644 index 8ea3b88e..00000000 --- a/modern/src/settings/GroupConnectionsPage.js +++ /dev/null @@ -1,116 +0,0 @@ -import React from 'react'; -import { useParams } from 'react-router-dom'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Container, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import LinkField from '../common/components/LinkField'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import SettingsMenu from './components/SettingsMenu'; -import { formatNotificationTitle } from '../common/util/formatter'; -import PageLayout from '../common/components/PageLayout'; -import useFeatures from '../common/util/useFeatures'; - -const useStyles = makeStyles((theme) => ({ - container: { - marginTop: theme.spacing(2), - }, - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const GroupConnectionsPage = () => { - const classes = useStyles(); - const t = useTranslation(); - - const { id } = useParams(); - - const features = useFeatures(); - - return ( - } - breadcrumbs={['settingsTitle', 'groupDialog', 'sharedConnections']} - > - - - }> - - {t('sharedConnections')} - - - - - formatNotificationTitle(t, it)} - label={t('sharedNotifications')} - /> - {!features.disableDrivers && ( - - )} - {!features.disableComputedAttributes && ( - it.description} - label={t('sharedComputedAttributes')} - /> - )} - it.description} - label={t('sharedSavedCommands')} - /> - {!features.disableMaintenance && ( - - )} - - - - - ); -}; - -export default GroupConnectionsPage; diff --git a/modern/src/settings/GroupConnectionsPage.jsx b/modern/src/settings/GroupConnectionsPage.jsx new file mode 100644 index 00000000..8ea3b88e --- /dev/null +++ b/modern/src/settings/GroupConnectionsPage.jsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + Container, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import LinkField from '../common/components/LinkField'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import SettingsMenu from './components/SettingsMenu'; +import { formatNotificationTitle } from '../common/util/formatter'; +import PageLayout from '../common/components/PageLayout'; +import useFeatures from '../common/util/useFeatures'; + +const useStyles = makeStyles((theme) => ({ + container: { + marginTop: theme.spacing(2), + }, + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const GroupConnectionsPage = () => { + const classes = useStyles(); + const t = useTranslation(); + + const { id } = useParams(); + + const features = useFeatures(); + + return ( + } + breadcrumbs={['settingsTitle', 'groupDialog', 'sharedConnections']} + > + + + }> + + {t('sharedConnections')} + + + + + formatNotificationTitle(t, it)} + label={t('sharedNotifications')} + /> + {!features.disableDrivers && ( + + )} + {!features.disableComputedAttributes && ( + it.description} + label={t('sharedComputedAttributes')} + /> + )} + it.description} + label={t('sharedSavedCommands')} + /> + {!features.disableMaintenance && ( + + )} + + + + + ); +}; + +export default GroupConnectionsPage; diff --git a/modern/src/settings/GroupPage.js b/modern/src/settings/GroupPage.js deleted file mode 100644 index 51fbda0e..00000000 --- a/modern/src/settings/GroupPage.js +++ /dev/null @@ -1,102 +0,0 @@ -import React, { useState } from 'react'; -import { useDispatch } from 'react-redux'; -import TextField from '@mui/material/TextField'; - -import { - Accordion, AccordionSummary, AccordionDetails, Typography, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import EditItemView from './components/EditItemView'; -import EditAttributesAccordion from './components/EditAttributesAccordion'; -import SelectField from '../common/components/SelectField'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import SettingsMenu from './components/SettingsMenu'; -import useCommonDeviceAttributes from '../common/attributes/useCommonDeviceAttributes'; -import useGroupAttributes from '../common/attributes/useGroupAttributes'; -import { useCatch } from '../reactHelper'; -import { groupsActions } from '../store'; - -const useStyles = makeStyles((theme) => ({ - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const GroupPage = () => { - const classes = useStyles(); - const dispatch = useDispatch(); - const t = useTranslation(); - - const commonDeviceAttributes = useCommonDeviceAttributes(t); - const groupAttributes = useGroupAttributes(t); - - const [item, setItem] = useState(); - - const onItemSaved = useCatch(async () => { - const response = await fetch('/api/groups'); - if (response.ok) { - dispatch(groupsActions.update(await response.json())); - } else { - throw Error(await response.text()); - } - }); - - const validate = () => item && item.name; - - return ( - } - breadcrumbs={['settingsTitle', 'groupDialog']} - > - {item && ( - <> - - }> - - {t('sharedRequired')} - - - - setItem({ ...item, name: event.target.value })} - label={t('sharedName')} - /> - - - - }> - - {t('sharedExtra')} - - - - setItem({ ...item, groupId: Number(event.target.value) })} - endpoint="/api/groups" - label={t('groupParent')} - /> - - - setItem({ ...item, attributes })} - definitions={{ ...commonDeviceAttributes, ...groupAttributes }} - /> - - )} - - ); -}; - -export default GroupPage; diff --git a/modern/src/settings/GroupPage.jsx b/modern/src/settings/GroupPage.jsx new file mode 100644 index 00000000..51fbda0e --- /dev/null +++ b/modern/src/settings/GroupPage.jsx @@ -0,0 +1,102 @@ +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import TextField from '@mui/material/TextField'; + +import { + Accordion, AccordionSummary, AccordionDetails, Typography, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import EditItemView from './components/EditItemView'; +import EditAttributesAccordion from './components/EditAttributesAccordion'; +import SelectField from '../common/components/SelectField'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import SettingsMenu from './components/SettingsMenu'; +import useCommonDeviceAttributes from '../common/attributes/useCommonDeviceAttributes'; +import useGroupAttributes from '../common/attributes/useGroupAttributes'; +import { useCatch } from '../reactHelper'; +import { groupsActions } from '../store'; + +const useStyles = makeStyles((theme) => ({ + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const GroupPage = () => { + const classes = useStyles(); + const dispatch = useDispatch(); + const t = useTranslation(); + + const commonDeviceAttributes = useCommonDeviceAttributes(t); + const groupAttributes = useGroupAttributes(t); + + const [item, setItem] = useState(); + + const onItemSaved = useCatch(async () => { + const response = await fetch('/api/groups'); + if (response.ok) { + dispatch(groupsActions.update(await response.json())); + } else { + throw Error(await response.text()); + } + }); + + const validate = () => item && item.name; + + return ( + } + breadcrumbs={['settingsTitle', 'groupDialog']} + > + {item && ( + <> + + }> + + {t('sharedRequired')} + + + + setItem({ ...item, name: event.target.value })} + label={t('sharedName')} + /> + + + + }> + + {t('sharedExtra')} + + + + setItem({ ...item, groupId: Number(event.target.value) })} + endpoint="/api/groups" + label={t('groupParent')} + /> + + + setItem({ ...item, attributes })} + definitions={{ ...commonDeviceAttributes, ...groupAttributes }} + /> + + )} + + ); +}; + +export default GroupPage; diff --git a/modern/src/settings/GroupsPage.js b/modern/src/settings/GroupsPage.js deleted file mode 100644 index baba7f72..00000000 --- a/modern/src/settings/GroupsPage.js +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import LinkIcon from '@mui/icons-material/Link'; -import PublishIcon from '@mui/icons-material/Publish'; -import { useEffectAsync } from '../reactHelper'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import { useRestriction } from '../common/util/permissions'; -import useSettingsStyles from './common/useSettingsStyles'; - -const GroupsPage = () => { - const classes = useSettingsStyles(); - const navigate = useNavigate(); - const t = useTranslation(); - - const limitCommands = useRestriction('limitCommands'); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/groups'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - const actionCommand = { - key: 'command', - title: t('deviceCommand'), - icon: , - handler: (groupId) => navigate(`/settings/group/${groupId}/command`), - }; - - const actionConnections = { - key: 'connections', - title: t('sharedConnections'), - icon: , - handler: (groupId) => navigate(`/settings/group/${groupId}/connections`), - }; - - return ( - } breadcrumbs={['settingsTitle', 'settingsGroups']}> - - - - - {t('sharedName')} - - - - - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - - {item.name} - - - - - )) : ()} - -
    - -
    - ); -}; - -export default GroupsPage; diff --git a/modern/src/settings/GroupsPage.jsx b/modern/src/settings/GroupsPage.jsx new file mode 100644 index 00000000..baba7f72 --- /dev/null +++ b/modern/src/settings/GroupsPage.jsx @@ -0,0 +1,91 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Table, TableRow, TableCell, TableHead, TableBody, +} from '@mui/material'; +import LinkIcon from '@mui/icons-material/Link'; +import PublishIcon from '@mui/icons-material/Publish'; +import { useEffectAsync } from '../reactHelper'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import SettingsMenu from './components/SettingsMenu'; +import CollectionFab from './components/CollectionFab'; +import CollectionActions from './components/CollectionActions'; +import TableShimmer from '../common/components/TableShimmer'; +import SearchHeader, { filterByKeyword } from './components/SearchHeader'; +import { useRestriction } from '../common/util/permissions'; +import useSettingsStyles from './common/useSettingsStyles'; + +const GroupsPage = () => { + const classes = useSettingsStyles(); + const navigate = useNavigate(); + const t = useTranslation(); + + const limitCommands = useRestriction('limitCommands'); + + const [timestamp, setTimestamp] = useState(Date.now()); + const [items, setItems] = useState([]); + const [searchKeyword, setSearchKeyword] = useState(''); + const [loading, setLoading] = useState(false); + + useEffectAsync(async () => { + setLoading(true); + try { + const response = await fetch('/api/groups'); + if (response.ok) { + setItems(await response.json()); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + }, [timestamp]); + + const actionCommand = { + key: 'command', + title: t('deviceCommand'), + icon: , + handler: (groupId) => navigate(`/settings/group/${groupId}/command`), + }; + + const actionConnections = { + key: 'connections', + title: t('sharedConnections'), + icon: , + handler: (groupId) => navigate(`/settings/group/${groupId}/connections`), + }; + + return ( + } breadcrumbs={['settingsTitle', 'settingsGroups']}> + + + + + {t('sharedName')} + + + + + {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( + + {item.name} + + + + + )) : ()} + +
    + +
    + ); +}; + +export default GroupsPage; diff --git a/modern/src/settings/MaintenancePage.js b/modern/src/settings/MaintenancePage.js deleted file mode 100644 index 987789d5..00000000 --- a/modern/src/settings/MaintenancePage.js +++ /dev/null @@ -1,174 +0,0 @@ -import React, { useState } from 'react'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - TextField, - FormControl, - InputLabel, - MenuItem, - Select, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { prefixString } from '../common/util/stringUtils'; -import EditItemView from './components/EditItemView'; -import EditAttributesAccordion from './components/EditAttributesAccordion'; -import { useAttributePreference } from '../common/util/preferences'; -import { - speedFromKnots, speedToKnots, distanceFromMeters, distanceToMeters, -} from '../common/util/converter'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import usePositionAttributes from '../common/attributes/usePositionAttributes'; -import SettingsMenu from './components/SettingsMenu'; - -const useStyles = makeStyles((theme) => ({ - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const MaintenancePage = () => { - const classes = useStyles(); - const t = useTranslation(); - - const positionAttributes = usePositionAttributes(t); - - const [item, setItem] = useState(); - const [labels, setLabels] = useState({ start: '', period: '' }); - - const speedUnit = useAttributePreference('speedUnit', 'kn'); - const distanceUnit = useAttributePreference('distanceUnit', 'km'); - - const convertToList = (attributes) => { - const otherList = []; - Object.keys(attributes).forEach((key) => { - const value = attributes[key]; - if (value.type === 'number') { - otherList.push({ key, name: value.name, type: value.type }); - } - }); - return otherList; - }; - - const onMaintenanceTypeChange = (event) => { - const newValue = event.target.value; - setItem({ - ...item, type: newValue, start: 0, period: 0, - }); - - const attribute = positionAttributes[newValue]; - if (attribute && attribute.dataType) { - switch (attribute.dataType) { - case 'distance': - setLabels({ ...labels, start: t(prefixString('shared', distanceUnit)), period: t(prefixString('shared', distanceUnit)) }); - break; - case 'speed': - setLabels({ ...labels, start: t(prefixString('shared', speedUnit)), period: t(prefixString('shared', speedUnit)) }); - break; - default: - setLabels({ ...labels, start: null, period: null }); - break; - } - } else { - setLabels({ ...labels, start: null, period: null }); - } - }; - - const rawToValue = (value) => { - const attribute = positionAttributes[item.type]; - if (attribute && attribute.dataType) { - switch (attribute.dataType) { - case 'speed': - return speedFromKnots(value, speedUnit); - case 'distance': - return distanceFromMeters(value, distanceUnit); - default: - return value; - } - } - return value; - }; - - const valueToRaw = (value) => { - const attribute = positionAttributes[item.type]; - if (attribute && attribute.dataType) { - switch (attribute.dataType) { - case 'speed': - return speedToKnots(value, speedUnit); - case 'distance': - return distanceToMeters(value, distanceUnit); - default: - return value; - } - } - return value; - }; - - const validate = () => item && item.name && item.type && item.start && item.period; - - return ( - } - breadcrumbs={['settingsTitle', 'sharedMaintenance']} - > - {item && ( - <> - - }> - - {t('sharedRequired')} - - - - setItem({ ...item, name: event.target.value })} - label={t('sharedName')} - /> - - {t('sharedType')} - - - setItem({ ...item, start: valueToRaw(event.target.value) })} - label={labels.start ? `${t('maintenanceStart')} (${labels.start})` : t('maintenanceStart')} - /> - setItem({ ...item, period: valueToRaw(event.target.value) })} - label={labels.period ? `${t('maintenancePeriod')} (${labels.period})` : t('maintenancePeriod')} - /> - - - setItem({ ...item, attributes })} - definitions={{}} - /> - - )} - - ); -}; - -export default MaintenancePage; diff --git a/modern/src/settings/MaintenancePage.jsx b/modern/src/settings/MaintenancePage.jsx new file mode 100644 index 00000000..987789d5 --- /dev/null +++ b/modern/src/settings/MaintenancePage.jsx @@ -0,0 +1,174 @@ +import React, { useState } from 'react'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + TextField, + FormControl, + InputLabel, + MenuItem, + Select, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { prefixString } from '../common/util/stringUtils'; +import EditItemView from './components/EditItemView'; +import EditAttributesAccordion from './components/EditAttributesAccordion'; +import { useAttributePreference } from '../common/util/preferences'; +import { + speedFromKnots, speedToKnots, distanceFromMeters, distanceToMeters, +} from '../common/util/converter'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import usePositionAttributes from '../common/attributes/usePositionAttributes'; +import SettingsMenu from './components/SettingsMenu'; + +const useStyles = makeStyles((theme) => ({ + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const MaintenancePage = () => { + const classes = useStyles(); + const t = useTranslation(); + + const positionAttributes = usePositionAttributes(t); + + const [item, setItem] = useState(); + const [labels, setLabels] = useState({ start: '', period: '' }); + + const speedUnit = useAttributePreference('speedUnit', 'kn'); + const distanceUnit = useAttributePreference('distanceUnit', 'km'); + + const convertToList = (attributes) => { + const otherList = []; + Object.keys(attributes).forEach((key) => { + const value = attributes[key]; + if (value.type === 'number') { + otherList.push({ key, name: value.name, type: value.type }); + } + }); + return otherList; + }; + + const onMaintenanceTypeChange = (event) => { + const newValue = event.target.value; + setItem({ + ...item, type: newValue, start: 0, period: 0, + }); + + const attribute = positionAttributes[newValue]; + if (attribute && attribute.dataType) { + switch (attribute.dataType) { + case 'distance': + setLabels({ ...labels, start: t(prefixString('shared', distanceUnit)), period: t(prefixString('shared', distanceUnit)) }); + break; + case 'speed': + setLabels({ ...labels, start: t(prefixString('shared', speedUnit)), period: t(prefixString('shared', speedUnit)) }); + break; + default: + setLabels({ ...labels, start: null, period: null }); + break; + } + } else { + setLabels({ ...labels, start: null, period: null }); + } + }; + + const rawToValue = (value) => { + const attribute = positionAttributes[item.type]; + if (attribute && attribute.dataType) { + switch (attribute.dataType) { + case 'speed': + return speedFromKnots(value, speedUnit); + case 'distance': + return distanceFromMeters(value, distanceUnit); + default: + return value; + } + } + return value; + }; + + const valueToRaw = (value) => { + const attribute = positionAttributes[item.type]; + if (attribute && attribute.dataType) { + switch (attribute.dataType) { + case 'speed': + return speedToKnots(value, speedUnit); + case 'distance': + return distanceToMeters(value, distanceUnit); + default: + return value; + } + } + return value; + }; + + const validate = () => item && item.name && item.type && item.start && item.period; + + return ( + } + breadcrumbs={['settingsTitle', 'sharedMaintenance']} + > + {item && ( + <> + + }> + + {t('sharedRequired')} + + + + setItem({ ...item, name: event.target.value })} + label={t('sharedName')} + /> + + {t('sharedType')} + + + setItem({ ...item, start: valueToRaw(event.target.value) })} + label={labels.start ? `${t('maintenanceStart')} (${labels.start})` : t('maintenanceStart')} + /> + setItem({ ...item, period: valueToRaw(event.target.value) })} + label={labels.period ? `${t('maintenancePeriod')} (${labels.period})` : t('maintenancePeriod')} + /> + + + setItem({ ...item, attributes })} + definitions={{}} + /> + + )} + + ); +}; + +export default MaintenancePage; diff --git a/modern/src/settings/MaintenancesPage.js b/modern/src/settings/MaintenancesPage.js deleted file mode 100644 index 2a66590c..00000000 --- a/modern/src/settings/MaintenancesPage.js +++ /dev/null @@ -1,93 +0,0 @@ -import React, { useState } from 'react'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import { useEffectAsync } from '../reactHelper'; -import usePositionAttributes from '../common/attributes/usePositionAttributes'; -import { formatDistance, formatSpeed } from '../common/util/formatter'; -import { useAttributePreference } from '../common/util/preferences'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import useSettingsStyles from './common/useSettingsStyles'; - -const MaintenacesPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const positionAttributes = usePositionAttributes(t); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - const speedUnit = useAttributePreference('speedUnit'); - const distanceUnit = useAttributePreference('distanceUnit'); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/maintenance'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - const convertAttribute = (key, value) => { - const attribute = positionAttributes[key]; - if (attribute && attribute.dataType) { - switch (attribute.dataType) { - case 'speed': - return formatSpeed(value, speedUnit, t); - case 'distance': - return formatDistance(value, distanceUnit, t); - default: - return value; - } - } - - return value; - }; - - return ( - } breadcrumbs={['settingsTitle', 'sharedMaintenance']}> - - - - - {t('sharedName')} - {t('sharedType')} - {t('maintenanceStart')} - {t('maintenancePeriod')} - - - - - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - - {item.name} - {item.type} - {convertAttribute(item.type, item.start)} - {convertAttribute(item.type, item.period)} - - - - - )) : ()} - -
    - -
    - ); -}; - -export default MaintenacesPage; diff --git a/modern/src/settings/MaintenancesPage.jsx b/modern/src/settings/MaintenancesPage.jsx new file mode 100644 index 00000000..2a66590c --- /dev/null +++ b/modern/src/settings/MaintenancesPage.jsx @@ -0,0 +1,93 @@ +import React, { useState } from 'react'; +import { + Table, TableRow, TableCell, TableHead, TableBody, +} from '@mui/material'; +import { useEffectAsync } from '../reactHelper'; +import usePositionAttributes from '../common/attributes/usePositionAttributes'; +import { formatDistance, formatSpeed } from '../common/util/formatter'; +import { useAttributePreference } from '../common/util/preferences'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import SettingsMenu from './components/SettingsMenu'; +import CollectionFab from './components/CollectionFab'; +import CollectionActions from './components/CollectionActions'; +import TableShimmer from '../common/components/TableShimmer'; +import SearchHeader, { filterByKeyword } from './components/SearchHeader'; +import useSettingsStyles from './common/useSettingsStyles'; + +const MaintenacesPage = () => { + const classes = useSettingsStyles(); + const t = useTranslation(); + + const positionAttributes = usePositionAttributes(t); + + const [timestamp, setTimestamp] = useState(Date.now()); + const [items, setItems] = useState([]); + const [searchKeyword, setSearchKeyword] = useState(''); + const [loading, setLoading] = useState(false); + const speedUnit = useAttributePreference('speedUnit'); + const distanceUnit = useAttributePreference('distanceUnit'); + + useEffectAsync(async () => { + setLoading(true); + try { + const response = await fetch('/api/maintenance'); + if (response.ok) { + setItems(await response.json()); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + }, [timestamp]); + + const convertAttribute = (key, value) => { + const attribute = positionAttributes[key]; + if (attribute && attribute.dataType) { + switch (attribute.dataType) { + case 'speed': + return formatSpeed(value, speedUnit, t); + case 'distance': + return formatDistance(value, distanceUnit, t); + default: + return value; + } + } + + return value; + }; + + return ( + } breadcrumbs={['settingsTitle', 'sharedMaintenance']}> + + + + + {t('sharedName')} + {t('sharedType')} + {t('maintenanceStart')} + {t('maintenancePeriod')} + + + + + {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( + + {item.name} + {item.type} + {convertAttribute(item.type, item.start)} + {convertAttribute(item.type, item.period)} + + + + + )) : ()} + +
    + +
    + ); +}; + +export default MaintenacesPage; diff --git a/modern/src/settings/NotificationPage.js b/modern/src/settings/NotificationPage.js deleted file mode 100644 index fdefedaf..00000000 --- a/modern/src/settings/NotificationPage.js +++ /dev/null @@ -1,154 +0,0 @@ -import React, { useState } from 'react'; - -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - FormControlLabel, - Checkbox, - FormGroup, - Button, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { useTranslation, useTranslationKeys } from '../common/components/LocalizationProvider'; -import EditItemView from './components/EditItemView'; -import { prefixString, unprefixString } from '../common/util/stringUtils'; -import SelectField from '../common/components/SelectField'; -import SettingsMenu from './components/SettingsMenu'; -import { useCatch } from '../reactHelper'; - -const useStyles = makeStyles((theme) => ({ - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const NotificationPage = () => { - const classes = useStyles(); - const t = useTranslation(); - - const [item, setItem] = useState(); - - const alarms = useTranslationKeys((it) => it.startsWith('alarm')).map((it) => ({ - key: unprefixString('alarm', it), - name: t(it), - })); - - const testNotificators = useCatch(async () => { - await Promise.all(item.notificators.split(/[, ]+/).map(async (notificator) => { - const response = await fetch(`/api/notifications/test/${notificator}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(item), - }); - if (!response.ok) { - throw Error(await response.text()); - } - })); - }); - - const validate = () => item && item.type && item.notificators && (!item.notificators?.includes('command') || item.commandId); - - return ( - } - breadcrumbs={['settingsTitle', 'sharedNotification']} - > - {item && ( - <> - - }> - - {t('sharedRequired')} - - - - setItem({ ...item, type: e.target.value })} - endpoint="/api/notifications/types" - keyGetter={(it) => it.type} - titleGetter={(it) => t(prefixString('event', it.type))} - label={t('sharedType')} - /> - {item.type === 'alarm' && ( - setItem({ ...item, attributes: { ...item.attributes, alarms: e.target.value.join() } })} - data={alarms} - keyGetter={(it) => it.key} - label={t('sharedAlarms')} - /> - )} - setItem({ ...item, notificators: e.target.value.join() })} - endpoint="/api/notifications/notificators" - keyGetter={(it) => it.type} - titleGetter={(it) => t(prefixString('notificator', it.type))} - label={t('notificationNotificators')} - /> - {item.notificators?.includes('command') && ( - setItem({ ...item, commandId: Number(event.target.value) })} - endpoint="/api/commands" - titleGetter={(it) => it.description} - label={t('sharedSavedCommand')} - /> - )} - - - setItem({ ...item, always: event.target.checked })} - /> - )} - label={t('notificationAlways')} - /> - - - - - }> - - {t('sharedExtra')} - - - - setItem({ ...item, calendarId: Number(event.target.value) })} - endpoint="/api/calendars" - label={t('sharedCalendar')} - /> - - - - )} - - ); -}; - -export default NotificationPage; diff --git a/modern/src/settings/NotificationPage.jsx b/modern/src/settings/NotificationPage.jsx new file mode 100644 index 00000000..fdefedaf --- /dev/null +++ b/modern/src/settings/NotificationPage.jsx @@ -0,0 +1,154 @@ +import React, { useState } from 'react'; + +import { + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + FormControlLabel, + Checkbox, + FormGroup, + Button, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { useTranslation, useTranslationKeys } from '../common/components/LocalizationProvider'; +import EditItemView from './components/EditItemView'; +import { prefixString, unprefixString } from '../common/util/stringUtils'; +import SelectField from '../common/components/SelectField'; +import SettingsMenu from './components/SettingsMenu'; +import { useCatch } from '../reactHelper'; + +const useStyles = makeStyles((theme) => ({ + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const NotificationPage = () => { + const classes = useStyles(); + const t = useTranslation(); + + const [item, setItem] = useState(); + + const alarms = useTranslationKeys((it) => it.startsWith('alarm')).map((it) => ({ + key: unprefixString('alarm', it), + name: t(it), + })); + + const testNotificators = useCatch(async () => { + await Promise.all(item.notificators.split(/[, ]+/).map(async (notificator) => { + const response = await fetch(`/api/notifications/test/${notificator}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(item), + }); + if (!response.ok) { + throw Error(await response.text()); + } + })); + }); + + const validate = () => item && item.type && item.notificators && (!item.notificators?.includes('command') || item.commandId); + + return ( + } + breadcrumbs={['settingsTitle', 'sharedNotification']} + > + {item && ( + <> + + }> + + {t('sharedRequired')} + + + + setItem({ ...item, type: e.target.value })} + endpoint="/api/notifications/types" + keyGetter={(it) => it.type} + titleGetter={(it) => t(prefixString('event', it.type))} + label={t('sharedType')} + /> + {item.type === 'alarm' && ( + setItem({ ...item, attributes: { ...item.attributes, alarms: e.target.value.join() } })} + data={alarms} + keyGetter={(it) => it.key} + label={t('sharedAlarms')} + /> + )} + setItem({ ...item, notificators: e.target.value.join() })} + endpoint="/api/notifications/notificators" + keyGetter={(it) => it.type} + titleGetter={(it) => t(prefixString('notificator', it.type))} + label={t('notificationNotificators')} + /> + {item.notificators?.includes('command') && ( + setItem({ ...item, commandId: Number(event.target.value) })} + endpoint="/api/commands" + titleGetter={(it) => it.description} + label={t('sharedSavedCommand')} + /> + )} + + + setItem({ ...item, always: event.target.checked })} + /> + )} + label={t('notificationAlways')} + /> + + + + + }> + + {t('sharedExtra')} + + + + setItem({ ...item, calendarId: Number(event.target.value) })} + endpoint="/api/calendars" + label={t('sharedCalendar')} + /> + + + + )} + + ); +}; + +export default NotificationPage; diff --git a/modern/src/settings/NotificationsPage.js b/modern/src/settings/NotificationsPage.js deleted file mode 100644 index f1e70a85..00000000 --- a/modern/src/settings/NotificationsPage.js +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useState } from 'react'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import { useEffectAsync } from '../reactHelper'; -import { prefixString } from '../common/util/stringUtils'; -import { formatBoolean } from '../common/util/formatter'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import useSettingsStyles from './common/useSettingsStyles'; - -const NotificationsPage = () => { - const classes = useSettingsStyles(); - const t = useTranslation(); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/notifications'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - const formatList = (prefix, value) => { - if (value) { - return value - .split(/[, ]+/) - .filter(Boolean) - .map((it) => t(prefixString(prefix, it))) - .join(', '); - } - return ''; - }; - - return ( - } breadcrumbs={['settingsTitle', 'sharedNotifications']}> - - - - - {t('notificationType')} - {t('notificationAlways')} - {t('sharedAlarms')} - {t('notificationNotificators')} - - - - - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - - {t(prefixString('event', item.type))} - {formatBoolean(item.always, t)} - {formatList('alarm', item.attributes.alarms)} - {formatList('notificator', item.notificators)} - - - - - )) : ()} - -
    - -
    - ); -}; - -export default NotificationsPage; diff --git a/modern/src/settings/NotificationsPage.jsx b/modern/src/settings/NotificationsPage.jsx new file mode 100644 index 00000000..f1e70a85 --- /dev/null +++ b/modern/src/settings/NotificationsPage.jsx @@ -0,0 +1,83 @@ +import React, { useState } from 'react'; +import { + Table, TableRow, TableCell, TableHead, TableBody, +} from '@mui/material'; +import { useEffectAsync } from '../reactHelper'; +import { prefixString } from '../common/util/stringUtils'; +import { formatBoolean } from '../common/util/formatter'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import SettingsMenu from './components/SettingsMenu'; +import CollectionFab from './components/CollectionFab'; +import CollectionActions from './components/CollectionActions'; +import TableShimmer from '../common/components/TableShimmer'; +import SearchHeader, { filterByKeyword } from './components/SearchHeader'; +import useSettingsStyles from './common/useSettingsStyles'; + +const NotificationsPage = () => { + const classes = useSettingsStyles(); + const t = useTranslation(); + + const [timestamp, setTimestamp] = useState(Date.now()); + const [items, setItems] = useState([]); + const [searchKeyword, setSearchKeyword] = useState(''); + const [loading, setLoading] = useState(false); + + useEffectAsync(async () => { + setLoading(true); + try { + const response = await fetch('/api/notifications'); + if (response.ok) { + setItems(await response.json()); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + }, [timestamp]); + + const formatList = (prefix, value) => { + if (value) { + return value + .split(/[, ]+/) + .filter(Boolean) + .map((it) => t(prefixString(prefix, it))) + .join(', '); + } + return ''; + }; + + return ( + } breadcrumbs={['settingsTitle', 'sharedNotifications']}> + + + + + {t('notificationType')} + {t('notificationAlways')} + {t('sharedAlarms')} + {t('notificationNotificators')} + + + + + {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( + + {t(prefixString('event', item.type))} + {formatBoolean(item.always, t)} + {formatList('alarm', item.attributes.alarms)} + {formatList('notificator', item.notificators)} + + + + + )) : ()} + +
    + +
    + ); +}; + +export default NotificationsPage; diff --git a/modern/src/settings/PreferencesPage.js b/modern/src/settings/PreferencesPage.js deleted file mode 100644 index a05924a9..00000000 --- a/modern/src/settings/PreferencesPage.js +++ /dev/null @@ -1,398 +0,0 @@ -import React, { useState } from 'react'; -import moment from 'moment'; -import { useDispatch, useSelector } from 'react-redux'; -import { useNavigate } from 'react-router-dom'; -import { - Accordion, AccordionSummary, AccordionDetails, Typography, Container, FormControl, InputLabel, Select, MenuItem, Checkbox, FormControlLabel, FormGroup, InputAdornment, IconButton, OutlinedInput, Autocomplete, TextField, createFilterOptions, Button, -} from '@mui/material'; -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 { useTranslation, useTranslationKeys } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import usePositionAttributes from '../common/attributes/usePositionAttributes'; -import { prefixString, unprefixString } from '../common/util/stringUtils'; -import SelectField from '../common/components/SelectField'; -import useMapStyles from '../map/core/useMapStyles'; -import useMapOverlays from '../map/overlay/useMapOverlays'; -import { useCatch } from '../reactHelper'; -import { sessionActions } from '../store'; -import { useRestriction } from '../common/util/permissions'; - -const deviceFields = [ - { id: 'name', name: 'sharedName' }, - { id: 'uniqueId', name: 'deviceIdentifier' }, - { id: 'phone', name: 'sharedPhone' }, - { id: 'model', name: 'deviceModel' }, - { id: 'contact', name: 'deviceContact' }, -]; - -const useStyles = makeStyles((theme) => ({ - container: { - marginTop: theme.spacing(2), - }, - buttons: { - marginTop: theme.spacing(2), - marginBottom: theme.spacing(2), - display: 'flex', - justifyContent: 'space-evenly', - '& > *': { - flexBasis: '33%', - }, - }, - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, - tokenActions: { - display: 'flex', - flexDirection: 'column', - }, -})); - -const PreferencesPage = () => { - const classes = useStyles(); - const dispatch = useDispatch(); - const navigate = useNavigate(); - const t = useTranslation(); - - const readonly = useRestriction('readonly'); - - const user = useSelector((state) => state.session.user); - const [attributes, setAttributes] = useState(user.attributes); - - const versionApp = process.env.REACT_APP_VERSION.slice(0, -2); - const versionServer = useSelector((state) => state.session.server.version); - const socket = useSelector((state) => state.session.socket); - - const [token, setToken] = useState(null); - const [tokenExpiration, setTokenExpiration] = useState(moment().add(1, 'week').locale('en').format(moment.HTML5_FMT.DATE)); - - const mapStyles = useMapStyles(); - const mapOverlays = useMapOverlays(); - - const positionAttributes = usePositionAttributes(t); - - const filter = createFilterOptions(); - - const generateToken = useCatch(async () => { - const expiration = moment(tokenExpiration, moment.HTML5_FMT.DATE).toISOString(); - const response = await fetch('/api/session/token', { - method: 'POST', - body: new URLSearchParams(`expiration=${expiration}`), - }); - if (response.ok) { - setToken(await response.text()); - } else { - throw Error(await response.text()); - } - }); - - const alarms = useTranslationKeys((it) => it.startsWith('alarm')).map((it) => ({ - key: unprefixString('alarm', it), - name: t(it), - })); - - const handleSave = useCatch(async () => { - const response = await fetch(`/api/users/${user.id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ...user, attributes }), - }); - if (response.ok) { - dispatch(sessionActions.updateUser(await response.json())); - navigate(-1); - } else { - throw Error(await response.text()); - } - }); - - return ( - } breadcrumbs={['settingsTitle', 'sharedPreferences']}> - - - }> - - {t('userToken')} - - - - { - setTokenExpiration(e.target.value); - setToken(null); - }} - /> - - -
    - - - - navigator.clipboard.writeText(token)} disabled={!token}> - - -
    - - )} - /> -
    -
    -
    - {!readonly && ( - <> - - }> - - {t('mapTitle')} - - - - - {t('mapActive')} - - - - {t('mapOverlay')} - - - (positionAttributes.hasOwnProperty(option) ? positionAttributes[option].name : option)} - value={attributes.positionItems?.split(',') || ['speed', 'address', 'totalDistance', 'course']} - onChange={(_, option) => { - setAttributes({ ...attributes, positionItems: option.join(',') }); - }} - filterOptions={(options, params) => { - const filtered = filter(options, params); - if (params.inputValue && !filtered.includes(params.inputValue)) { - filtered.push(params.inputValue); - } - return filtered; - }} - renderInput={(params) => ( - - )} - /> - - {t('mapLiveRoutes')} - - - - {t('mapDirection')} - - - - setAttributes({ ...attributes, mapGeofences: e.target.checked })} - /> - )} - label={t('attributeShowGeofences')} - /> - setAttributes({ ...attributes, mapFollow: e.target.checked })} - /> - )} - label={t('deviceFollow')} - /> - setAttributes({ ...attributes, mapCluster: e.target.checked })} - /> - )} - label={t('mapClustering')} - /> - setAttributes({ ...attributes, mapOnSelect: e.target.checked })} - /> - )} - label={t('mapOnSelect')} - /> - - - - - }> - - {t('deviceTitle')} - - - - setAttributes({ ...attributes, devicePrimary: e.target.value })} - data={deviceFields} - titleGetter={(it) => t(it.name)} - label={t('devicePrimaryInfo')} - /> - setAttributes({ ...attributes, deviceSecondary: e.target.value })} - data={deviceFields} - titleGetter={(it) => t(it.name)} - label={t('deviceSecondaryInfo')} - /> - - - - }> - - {t('sharedSound')} - - - - setAttributes({ ...attributes, soundEvents: e.target.value.join(',') })} - endpoint="/api/notifications/types" - keyGetter={(it) => it.type} - titleGetter={(it) => t(prefixString('event', it.type))} - label={t('eventsSoundEvents')} - /> - setAttributes({ ...attributes, soundAlarms: e.target.value.join(',') })} - data={alarms} - keyGetter={(it) => it.key} - label={t('eventsSoundAlarms')} - /> - - - - }> - - {t('sharedInfoTitle')} - - - - - - - - -
    - - -
    - - )} -
    -
    - ); -}; - -export default PreferencesPage; diff --git a/modern/src/settings/PreferencesPage.jsx b/modern/src/settings/PreferencesPage.jsx new file mode 100644 index 00000000..a05924a9 --- /dev/null +++ b/modern/src/settings/PreferencesPage.jsx @@ -0,0 +1,398 @@ +import React, { useState } from 'react'; +import moment from 'moment'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import { + Accordion, AccordionSummary, AccordionDetails, Typography, Container, FormControl, InputLabel, Select, MenuItem, Checkbox, FormControlLabel, FormGroup, InputAdornment, IconButton, OutlinedInput, Autocomplete, TextField, createFilterOptions, Button, +} from '@mui/material'; +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 { useTranslation, useTranslationKeys } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import SettingsMenu from './components/SettingsMenu'; +import usePositionAttributes from '../common/attributes/usePositionAttributes'; +import { prefixString, unprefixString } from '../common/util/stringUtils'; +import SelectField from '../common/components/SelectField'; +import useMapStyles from '../map/core/useMapStyles'; +import useMapOverlays from '../map/overlay/useMapOverlays'; +import { useCatch } from '../reactHelper'; +import { sessionActions } from '../store'; +import { useRestriction } from '../common/util/permissions'; + +const deviceFields = [ + { id: 'name', name: 'sharedName' }, + { id: 'uniqueId', name: 'deviceIdentifier' }, + { id: 'phone', name: 'sharedPhone' }, + { id: 'model', name: 'deviceModel' }, + { id: 'contact', name: 'deviceContact' }, +]; + +const useStyles = makeStyles((theme) => ({ + container: { + marginTop: theme.spacing(2), + }, + buttons: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + display: 'flex', + justifyContent: 'space-evenly', + '& > *': { + flexBasis: '33%', + }, + }, + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, + tokenActions: { + display: 'flex', + flexDirection: 'column', + }, +})); + +const PreferencesPage = () => { + const classes = useStyles(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const t = useTranslation(); + + const readonly = useRestriction('readonly'); + + const user = useSelector((state) => state.session.user); + const [attributes, setAttributes] = useState(user.attributes); + + const versionApp = process.env.REACT_APP_VERSION.slice(0, -2); + const versionServer = useSelector((state) => state.session.server.version); + const socket = useSelector((state) => state.session.socket); + + const [token, setToken] = useState(null); + const [tokenExpiration, setTokenExpiration] = useState(moment().add(1, 'week').locale('en').format(moment.HTML5_FMT.DATE)); + + const mapStyles = useMapStyles(); + const mapOverlays = useMapOverlays(); + + const positionAttributes = usePositionAttributes(t); + + const filter = createFilterOptions(); + + const generateToken = useCatch(async () => { + const expiration = moment(tokenExpiration, moment.HTML5_FMT.DATE).toISOString(); + const response = await fetch('/api/session/token', { + method: 'POST', + body: new URLSearchParams(`expiration=${expiration}`), + }); + if (response.ok) { + setToken(await response.text()); + } else { + throw Error(await response.text()); + } + }); + + const alarms = useTranslationKeys((it) => it.startsWith('alarm')).map((it) => ({ + key: unprefixString('alarm', it), + name: t(it), + })); + + const handleSave = useCatch(async () => { + const response = await fetch(`/api/users/${user.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...user, attributes }), + }); + if (response.ok) { + dispatch(sessionActions.updateUser(await response.json())); + navigate(-1); + } else { + throw Error(await response.text()); + } + }); + + return ( + } breadcrumbs={['settingsTitle', 'sharedPreferences']}> + + + }> + + {t('userToken')} + + + + { + setTokenExpiration(e.target.value); + setToken(null); + }} + /> + + +
    + + + + navigator.clipboard.writeText(token)} disabled={!token}> + + +
    + + )} + /> +
    +
    +
    + {!readonly && ( + <> + + }> + + {t('mapTitle')} + + + + + {t('mapActive')} + + + + {t('mapOverlay')} + + + (positionAttributes.hasOwnProperty(option) ? positionAttributes[option].name : option)} + value={attributes.positionItems?.split(',') || ['speed', 'address', 'totalDistance', 'course']} + onChange={(_, option) => { + setAttributes({ ...attributes, positionItems: option.join(',') }); + }} + filterOptions={(options, params) => { + const filtered = filter(options, params); + if (params.inputValue && !filtered.includes(params.inputValue)) { + filtered.push(params.inputValue); + } + return filtered; + }} + renderInput={(params) => ( + + )} + /> + + {t('mapLiveRoutes')} + + + + {t('mapDirection')} + + + + setAttributes({ ...attributes, mapGeofences: e.target.checked })} + /> + )} + label={t('attributeShowGeofences')} + /> + setAttributes({ ...attributes, mapFollow: e.target.checked })} + /> + )} + label={t('deviceFollow')} + /> + setAttributes({ ...attributes, mapCluster: e.target.checked })} + /> + )} + label={t('mapClustering')} + /> + setAttributes({ ...attributes, mapOnSelect: e.target.checked })} + /> + )} + label={t('mapOnSelect')} + /> + + + + + }> + + {t('deviceTitle')} + + + + setAttributes({ ...attributes, devicePrimary: e.target.value })} + data={deviceFields} + titleGetter={(it) => t(it.name)} + label={t('devicePrimaryInfo')} + /> + setAttributes({ ...attributes, deviceSecondary: e.target.value })} + data={deviceFields} + titleGetter={(it) => t(it.name)} + label={t('deviceSecondaryInfo')} + /> + + + + }> + + {t('sharedSound')} + + + + setAttributes({ ...attributes, soundEvents: e.target.value.join(',') })} + endpoint="/api/notifications/types" + keyGetter={(it) => it.type} + titleGetter={(it) => t(prefixString('event', it.type))} + label={t('eventsSoundEvents')} + /> + setAttributes({ ...attributes, soundAlarms: e.target.value.join(',') })} + data={alarms} + keyGetter={(it) => it.key} + label={t('eventsSoundAlarms')} + /> + + + + }> + + {t('sharedInfoTitle')} + + + + + + + + +
    + + +
    + + )} +
    +
    + ); +}; + +export default PreferencesPage; diff --git a/modern/src/settings/ServerPage.js b/modern/src/settings/ServerPage.js deleted file mode 100644 index 07ce2dad..00000000 --- a/modern/src/settings/ServerPage.js +++ /dev/null @@ -1,338 +0,0 @@ -import React, { useState } from 'react'; -import TextField from '@mui/material/TextField'; - -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Button, - FormControl, - Container, - Checkbox, - FormControlLabel, - InputLabel, - Select, - MenuItem, - FormGroup, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { useNavigate } from 'react-router-dom'; -import { useDispatch, useSelector } from 'react-redux'; -import { DropzoneArea } from 'react-mui-dropzone'; -import { sessionActions } from '../store'; -import EditAttributesAccordion from './components/EditAttributesAccordion'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import SelectField from '../common/components/SelectField'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import useCommonDeviceAttributes from '../common/attributes/useCommonDeviceAttributes'; -import useCommonUserAttributes from '../common/attributes/useCommonUserAttributes'; -import { useCatch } from '../reactHelper'; -import useServerAttributes from '../common/attributes/useServerAttributes'; -import useMapStyles from '../map/core/useMapStyles'; -import { map } from '../map/core/MapView'; - -const useStyles = makeStyles((theme) => ({ - container: { - marginTop: theme.spacing(2), - }, - buttons: { - marginTop: theme.spacing(2), - marginBottom: theme.spacing(2), - display: 'flex', - justifyContent: 'space-evenly', - '& > *': { - flexBasis: '33%', - }, - }, - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const ServerPage = () => { - const classes = useStyles(); - const navigate = useNavigate(); - const dispatch = useDispatch(); - const t = useTranslation(); - - const mapStyles = useMapStyles(); - const commonUserAttributes = useCommonUserAttributes(t); - const commonDeviceAttributes = useCommonDeviceAttributes(t); - const serverAttributes = useServerAttributes(t); - - const original = useSelector((state) => state.session.server); - const [item, setItem] = useState({ ...original }); - - const handleFiles = useCatch(async (files) => { - if (files.length > 0) { - const file = files[0]; - const response = await fetch(`/api/server/file/${file.path}`, { - method: 'POST', - body: file, - }); - if (!response.ok) { - throw Error(await response.text()); - } - } - }); - - const handleSave = useCatch(async () => { - const response = await fetch('/api/server', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(item), - }); - - if (response.ok) { - dispatch(sessionActions.updateServer(await response.json())); - navigate(-1); - } else { - throw Error(await response.text()); - } - }); - - return ( - } breadcrumbs={['settingsTitle', 'settingsServer']}> - - {item && ( - <> - - }> - - {t('sharedPreferences')} - - - - setItem({ ...item, mapUrl: event.target.value })} - label={t('mapCustomLabel')} - /> - setItem({ ...item, overlayUrl: event.target.value })} - label={t('mapOverlayCustom')} - /> - - {t('mapDefault')} - - - - {t('settingsCoordinateFormat')} - - - - {t('settingsSpeedUnit')} - - - - {t('settingsDistanceUnit')} - - - - {t('settingsAltitudeUnit')} - - - - {t('settingsVolumeUnit')} - - - setItem({ ...item, attributes: { ...item.attributes, timezone: e.target.value } })} - endpoint="/api/server/timezones" - keyGetter={(it) => it} - titleGetter={(it) => it} - label={t('sharedTimezone')} - /> - setItem({ ...item, poiLayer: event.target.value })} - label={t('mapPoiLayer')} - /> - setItem({ ...item, announcement: event.target.value })} - label={t('serverAnnouncement')} - /> - - setItem({ ...item, twelveHourFormat: event.target.checked })} />} - label={t('settingsTwelveHourFormat')} - /> - setItem({ ...item, forceSettings: event.target.checked })} />} - label={t('serverForceSettings')} - /> - - - - - }> - - {t('sharedLocation')} - - - - setItem({ ...item, latitude: Number(event.target.value) })} - label={t('positionLatitude')} - /> - setItem({ ...item, longitude: Number(event.target.value) })} - label={t('positionLongitude')} - /> - setItem({ ...item, zoom: Number(event.target.value) })} - label={t('serverZoom')} - /> - - - - - }> - - {t('sharedPermissions')} - - - - - setItem({ ...item, registration: event.target.checked })} />} - label={t('serverRegistration')} - /> - setItem({ ...item, readonly: event.target.checked })} />} - label={t('serverReadonly')} - /> - setItem({ ...item, deviceReadonly: event.target.checked })} />} - label={t('userDeviceReadonly')} - /> - setItem({ ...item, limitCommands: event.target.checked })} />} - label={t('userLimitCommands')} - /> - setItem({ ...item, disableReports: event.target.checked })} />} - label={t('userDisableReports')} - /> - setItem({ ...item, fixedEmail: e.target.checked })} />} - label={t('userFixedEmail')} - /> - - - - - }> - - {t('sharedFile')} - - - - - - - setItem({ ...item, attributes })} - definitions={{ ...commonUserAttributes, ...commonDeviceAttributes, ...serverAttributes }} - /> - - )} -
    - - -
    -
    -
    - ); -}; - -export default ServerPage; diff --git a/modern/src/settings/ServerPage.jsx b/modern/src/settings/ServerPage.jsx new file mode 100644 index 00000000..07ce2dad --- /dev/null +++ b/modern/src/settings/ServerPage.jsx @@ -0,0 +1,338 @@ +import React, { useState } from 'react'; +import TextField from '@mui/material/TextField'; + +import { + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + Button, + FormControl, + Container, + Checkbox, + FormControlLabel, + InputLabel, + Select, + MenuItem, + FormGroup, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { useNavigate } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import { DropzoneArea } from 'react-mui-dropzone'; +import { sessionActions } from '../store'; +import EditAttributesAccordion from './components/EditAttributesAccordion'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import SelectField from '../common/components/SelectField'; +import PageLayout from '../common/components/PageLayout'; +import SettingsMenu from './components/SettingsMenu'; +import useCommonDeviceAttributes from '../common/attributes/useCommonDeviceAttributes'; +import useCommonUserAttributes from '../common/attributes/useCommonUserAttributes'; +import { useCatch } from '../reactHelper'; +import useServerAttributes from '../common/attributes/useServerAttributes'; +import useMapStyles from '../map/core/useMapStyles'; +import { map } from '../map/core/MapView'; + +const useStyles = makeStyles((theme) => ({ + container: { + marginTop: theme.spacing(2), + }, + buttons: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + display: 'flex', + justifyContent: 'space-evenly', + '& > *': { + flexBasis: '33%', + }, + }, + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const ServerPage = () => { + const classes = useStyles(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const t = useTranslation(); + + const mapStyles = useMapStyles(); + const commonUserAttributes = useCommonUserAttributes(t); + const commonDeviceAttributes = useCommonDeviceAttributes(t); + const serverAttributes = useServerAttributes(t); + + const original = useSelector((state) => state.session.server); + const [item, setItem] = useState({ ...original }); + + const handleFiles = useCatch(async (files) => { + if (files.length > 0) { + const file = files[0]; + const response = await fetch(`/api/server/file/${file.path}`, { + method: 'POST', + body: file, + }); + if (!response.ok) { + throw Error(await response.text()); + } + } + }); + + const handleSave = useCatch(async () => { + const response = await fetch('/api/server', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(item), + }); + + if (response.ok) { + dispatch(sessionActions.updateServer(await response.json())); + navigate(-1); + } else { + throw Error(await response.text()); + } + }); + + return ( + } breadcrumbs={['settingsTitle', 'settingsServer']}> + + {item && ( + <> + + }> + + {t('sharedPreferences')} + + + + setItem({ ...item, mapUrl: event.target.value })} + label={t('mapCustomLabel')} + /> + setItem({ ...item, overlayUrl: event.target.value })} + label={t('mapOverlayCustom')} + /> + + {t('mapDefault')} + + + + {t('settingsCoordinateFormat')} + + + + {t('settingsSpeedUnit')} + + + + {t('settingsDistanceUnit')} + + + + {t('settingsAltitudeUnit')} + + + + {t('settingsVolumeUnit')} + + + setItem({ ...item, attributes: { ...item.attributes, timezone: e.target.value } })} + endpoint="/api/server/timezones" + keyGetter={(it) => it} + titleGetter={(it) => it} + label={t('sharedTimezone')} + /> + setItem({ ...item, poiLayer: event.target.value })} + label={t('mapPoiLayer')} + /> + setItem({ ...item, announcement: event.target.value })} + label={t('serverAnnouncement')} + /> + + setItem({ ...item, twelveHourFormat: event.target.checked })} />} + label={t('settingsTwelveHourFormat')} + /> + setItem({ ...item, forceSettings: event.target.checked })} />} + label={t('serverForceSettings')} + /> + + + + + }> + + {t('sharedLocation')} + + + + setItem({ ...item, latitude: Number(event.target.value) })} + label={t('positionLatitude')} + /> + setItem({ ...item, longitude: Number(event.target.value) })} + label={t('positionLongitude')} + /> + setItem({ ...item, zoom: Number(event.target.value) })} + label={t('serverZoom')} + /> + + + + + }> + + {t('sharedPermissions')} + + + + + setItem({ ...item, registration: event.target.checked })} />} + label={t('serverRegistration')} + /> + setItem({ ...item, readonly: event.target.checked })} />} + label={t('serverReadonly')} + /> + setItem({ ...item, deviceReadonly: event.target.checked })} />} + label={t('userDeviceReadonly')} + /> + setItem({ ...item, limitCommands: event.target.checked })} />} + label={t('userLimitCommands')} + /> + setItem({ ...item, disableReports: event.target.checked })} />} + label={t('userDisableReports')} + /> + setItem({ ...item, fixedEmail: e.target.checked })} />} + label={t('userFixedEmail')} + /> + + + + + }> + + {t('sharedFile')} + + + + + + + setItem({ ...item, attributes })} + definitions={{ ...commonUserAttributes, ...commonDeviceAttributes, ...serverAttributes }} + /> + + )} +
    + + +
    +
    +
    + ); +}; + +export default ServerPage; diff --git a/modern/src/settings/UserConnectionsPage.js b/modern/src/settings/UserConnectionsPage.js deleted file mode 100644 index 80de8835..00000000 --- a/modern/src/settings/UserConnectionsPage.js +++ /dev/null @@ -1,139 +0,0 @@ -import React from 'react'; -import { useParams } from 'react-router-dom'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Container, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import LinkField from '../common/components/LinkField'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import SettingsMenu from './components/SettingsMenu'; -import { formatNotificationTitle } from '../common/util/formatter'; -import PageLayout from '../common/components/PageLayout'; - -const useStyles = makeStyles((theme) => ({ - container: { - marginTop: theme.spacing(2), - }, - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const UserConnectionsPage = () => { - const classes = useStyles(); - const t = useTranslation(); - - const { id } = useParams(); - - return ( - } - breadcrumbs={['settingsTitle', 'settingsUser', 'sharedConnections']} - > - - - }> - - {t('sharedConnections')} - - - - - - - formatNotificationTitle(t, it, true)} - label={t('sharedNotifications')} - /> - - - it.description} - label={t('sharedComputedAttributes')} - /> - - it.description} - label={t('sharedSavedCommands')} - /> - - - - - - ); -}; - -export default UserConnectionsPage; diff --git a/modern/src/settings/UserConnectionsPage.jsx b/modern/src/settings/UserConnectionsPage.jsx new file mode 100644 index 00000000..80de8835 --- /dev/null +++ b/modern/src/settings/UserConnectionsPage.jsx @@ -0,0 +1,139 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + Container, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import LinkField from '../common/components/LinkField'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import SettingsMenu from './components/SettingsMenu'; +import { formatNotificationTitle } from '../common/util/formatter'; +import PageLayout from '../common/components/PageLayout'; + +const useStyles = makeStyles((theme) => ({ + container: { + marginTop: theme.spacing(2), + }, + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const UserConnectionsPage = () => { + const classes = useStyles(); + const t = useTranslation(); + + const { id } = useParams(); + + return ( + } + breadcrumbs={['settingsTitle', 'settingsUser', 'sharedConnections']} + > + + + }> + + {t('sharedConnections')} + + + + + + + formatNotificationTitle(t, it, true)} + label={t('sharedNotifications')} + /> + + + it.description} + label={t('sharedComputedAttributes')} + /> + + it.description} + label={t('sharedSavedCommands')} + /> + + + + + + ); +}; + +export default UserConnectionsPage; diff --git a/modern/src/settings/UserPage.js b/modern/src/settings/UserPage.js deleted file mode 100644 index 21859ea7..00000000 --- a/modern/src/settings/UserPage.js +++ /dev/null @@ -1,402 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - FormControl, - InputLabel, - Select, - MenuItem, - FormControlLabel, - Checkbox, - FormGroup, - TextField, - Button, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; -import { useDispatch, useSelector } from 'react-redux'; -import moment from 'moment'; -import EditItemView from './components/EditItemView'; -import EditAttributesAccordion from './components/EditAttributesAccordion'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import useUserAttributes from '../common/attributes/useUserAttributes'; -import { sessionActions } from '../store'; -import SelectField from '../common/components/SelectField'; -import SettingsMenu from './components/SettingsMenu'; -import useCommonUserAttributes from '../common/attributes/useCommonUserAttributes'; -import { useAdministrator, useRestriction, useManager } from '../common/util/permissions'; -import useQuery from '../common/util/useQuery'; -import { useCatch } from '../reactHelper'; -import useMapStyles from '../map/core/useMapStyles'; -import { map } from '../map/core/MapView'; - -const useStyles = makeStyles((theme) => ({ - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const UserPage = () => { - const classes = useStyles(); - const navigate = useNavigate(); - const dispatch = useDispatch(); - const t = useTranslation(); - - const admin = useAdministrator(); - const manager = useManager(); - const fixedEmail = useRestriction('fixedEmail'); - - const currentUser = useSelector((state) => state.session.user); - const registrationEnabled = useSelector((state) => state.session.server.registration); - const openIdForced = useSelector((state) => state.session.server.openIdForce); - - const mapStyles = useMapStyles(); - const commonUserAttributes = useCommonUserAttributes(t); - const userAttributes = useUserAttributes(t); - - const { id } = useParams(); - const [item, setItem] = useState(id === currentUser.id.toString() ? currentUser : null); - - const [deleteEmail, setDeleteEmail] = useState(); - const [deleteFailed, setDeleteFailed] = useState(false); - - const handleDelete = useCatch(async () => { - if (deleteEmail === currentUser.email) { - setDeleteFailed(false); - const response = await fetch(`/api/users/${currentUser.id}`, { method: 'DELETE' }); - if (response.ok) { - navigate('/login'); - dispatch(sessionActions.updateUser(null)); - } else { - throw Error(await response.text()); - } - } else { - setDeleteFailed(true); - } - }); - - const query = useQuery(); - const [queryHandled, setQueryHandled] = useState(false); - const attribute = query.get('attribute'); - - useEffect(() => { - if (!queryHandled && item && attribute) { - if (!item.attributes.hasOwnProperty('attribute')) { - const updatedAttributes = { ...item.attributes }; - updatedAttributes[attribute] = ''; - setItem({ ...item, attributes: updatedAttributes }); - } - setQueryHandled(true); - } - }, [item, queryHandled, setQueryHandled, attribute]); - - const onItemSaved = (result) => { - if (result.id === currentUser.id) { - dispatch(sessionActions.updateUser(result)); - } - }; - - const validate = () => item && item.name && item.email && (item.id || item.password); - - return ( - } - breadcrumbs={['settingsTitle', 'settingsUser']} - > - {item && ( - <> - - }> - - {t('sharedRequired')} - - - - setItem({ ...item, name: event.target.value })} - label={t('sharedName')} - /> - setItem({ ...item, email: event.target.value })} - label={t('userEmail')} - disabled={fixedEmail} - /> - {!openIdForced && ( - setItem({ ...item, password: event.target.value })} - label={t('userPassword')} - /> - )} - - - - }> - - {t('sharedPreferences')} - - - - setItem({ ...item, phone: event.target.value })} - label={t('sharedPhone')} - /> - - {t('mapDefault')} - - - - {t('settingsCoordinateFormat')} - - - - {t('settingsSpeedUnit')} - - - - {t('settingsDistanceUnit')} - - - - {t('settingsAltitudeUnit')} - - - - {t('settingsVolumeUnit')} - - - setItem({ ...item, attributes: { ...item.attributes, timezone: e.target.value } })} - endpoint="/api/server/timezones" - keyGetter={(it) => it} - titleGetter={(it) => it} - label={t('sharedTimezone')} - /> - setItem({ ...item, poiLayer: event.target.value })} - label={t('mapPoiLayer')} - /> - - setItem({ ...item, twelveHourFormat: event.target.checked })} />} - label={t('settingsTwelveHourFormat')} - /> - - - - - }> - - {t('sharedLocation')} - - - - setItem({ ...item, latitude: Number(event.target.value) })} - label={t('positionLatitude')} - /> - setItem({ ...item, longitude: Number(event.target.value) })} - label={t('positionLongitude')} - /> - setItem({ ...item, zoom: Number(event.target.value) })} - label={t('serverZoom')} - /> - - - - - }> - - {t('sharedPermissions')} - - - - setItem({ ...item, expirationTime: moment(e.target.value, moment.HTML5_FMT.DATE).locale('en').format() })} - disabled={!manager} - /> - setItem({ ...item, deviceLimit: Number(e.target.value) })} - label={t('userDeviceLimit')} - disabled={!admin} - /> - setItem({ ...item, userLimit: Number(e.target.value) })} - label={t('userUserLimit')} - disabled={!admin} - /> - - setItem({ ...item, disabled: e.target.checked })} />} - label={t('sharedDisabled')} - disabled={!manager} - /> - setItem({ ...item, administrator: e.target.checked })} />} - label={t('userAdmin')} - disabled={!admin} - /> - setItem({ ...item, readonly: e.target.checked })} />} - label={t('serverReadonly')} - disabled={!manager} - /> - setItem({ ...item, deviceReadonly: e.target.checked })} />} - label={t('userDeviceReadonly')} - disabled={!manager} - /> - setItem({ ...item, limitCommands: e.target.checked })} />} - label={t('userLimitCommands')} - disabled={!manager} - /> - setItem({ ...item, disableReports: e.target.checked })} />} - label={t('userDisableReports')} - disabled={!manager} - /> - setItem({ ...item, fixedEmail: e.target.checked })} />} - label={t('userFixedEmail')} - disabled={!manager} - /> - - - - setItem({ ...item, attributes })} - definitions={{ ...commonUserAttributes, ...userAttributes }} - focusAttribute={attribute} - /> - {registrationEnabled && item.id === currentUser.id && !manager && ( - - }> - - {t('userDeleteAccount')} - - - - setDeleteEmail(event.target.value)} - label={t('userEmail')} - error={deleteFailed} - /> - - - - )} - - )} - - ); -}; - -export default UserPage; diff --git a/modern/src/settings/UserPage.jsx b/modern/src/settings/UserPage.jsx new file mode 100644 index 00000000..21859ea7 --- /dev/null +++ b/modern/src/settings/UserPage.jsx @@ -0,0 +1,402 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + FormControl, + InputLabel, + Select, + MenuItem, + FormControlLabel, + Checkbox, + FormGroup, + TextField, + Button, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import { useDispatch, useSelector } from 'react-redux'; +import moment from 'moment'; +import EditItemView from './components/EditItemView'; +import EditAttributesAccordion from './components/EditAttributesAccordion'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import useUserAttributes from '../common/attributes/useUserAttributes'; +import { sessionActions } from '../store'; +import SelectField from '../common/components/SelectField'; +import SettingsMenu from './components/SettingsMenu'; +import useCommonUserAttributes from '../common/attributes/useCommonUserAttributes'; +import { useAdministrator, useRestriction, useManager } from '../common/util/permissions'; +import useQuery from '../common/util/useQuery'; +import { useCatch } from '../reactHelper'; +import useMapStyles from '../map/core/useMapStyles'; +import { map } from '../map/core/MapView'; + +const useStyles = makeStyles((theme) => ({ + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const UserPage = () => { + const classes = useStyles(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const t = useTranslation(); + + const admin = useAdministrator(); + const manager = useManager(); + const fixedEmail = useRestriction('fixedEmail'); + + const currentUser = useSelector((state) => state.session.user); + const registrationEnabled = useSelector((state) => state.session.server.registration); + const openIdForced = useSelector((state) => state.session.server.openIdForce); + + const mapStyles = useMapStyles(); + const commonUserAttributes = useCommonUserAttributes(t); + const userAttributes = useUserAttributes(t); + + const { id } = useParams(); + const [item, setItem] = useState(id === currentUser.id.toString() ? currentUser : null); + + const [deleteEmail, setDeleteEmail] = useState(); + const [deleteFailed, setDeleteFailed] = useState(false); + + const handleDelete = useCatch(async () => { + if (deleteEmail === currentUser.email) { + setDeleteFailed(false); + const response = await fetch(`/api/users/${currentUser.id}`, { method: 'DELETE' }); + if (response.ok) { + navigate('/login'); + dispatch(sessionActions.updateUser(null)); + } else { + throw Error(await response.text()); + } + } else { + setDeleteFailed(true); + } + }); + + const query = useQuery(); + const [queryHandled, setQueryHandled] = useState(false); + const attribute = query.get('attribute'); + + useEffect(() => { + if (!queryHandled && item && attribute) { + if (!item.attributes.hasOwnProperty('attribute')) { + const updatedAttributes = { ...item.attributes }; + updatedAttributes[attribute] = ''; + setItem({ ...item, attributes: updatedAttributes }); + } + setQueryHandled(true); + } + }, [item, queryHandled, setQueryHandled, attribute]); + + const onItemSaved = (result) => { + if (result.id === currentUser.id) { + dispatch(sessionActions.updateUser(result)); + } + }; + + const validate = () => item && item.name && item.email && (item.id || item.password); + + return ( + } + breadcrumbs={['settingsTitle', 'settingsUser']} + > + {item && ( + <> + + }> + + {t('sharedRequired')} + + + + setItem({ ...item, name: event.target.value })} + label={t('sharedName')} + /> + setItem({ ...item, email: event.target.value })} + label={t('userEmail')} + disabled={fixedEmail} + /> + {!openIdForced && ( + setItem({ ...item, password: event.target.value })} + label={t('userPassword')} + /> + )} + + + + }> + + {t('sharedPreferences')} + + + + setItem({ ...item, phone: event.target.value })} + label={t('sharedPhone')} + /> + + {t('mapDefault')} + + + + {t('settingsCoordinateFormat')} + + + + {t('settingsSpeedUnit')} + + + + {t('settingsDistanceUnit')} + + + + {t('settingsAltitudeUnit')} + + + + {t('settingsVolumeUnit')} + + + setItem({ ...item, attributes: { ...item.attributes, timezone: e.target.value } })} + endpoint="/api/server/timezones" + keyGetter={(it) => it} + titleGetter={(it) => it} + label={t('sharedTimezone')} + /> + setItem({ ...item, poiLayer: event.target.value })} + label={t('mapPoiLayer')} + /> + + setItem({ ...item, twelveHourFormat: event.target.checked })} />} + label={t('settingsTwelveHourFormat')} + /> + + + + + }> + + {t('sharedLocation')} + + + + setItem({ ...item, latitude: Number(event.target.value) })} + label={t('positionLatitude')} + /> + setItem({ ...item, longitude: Number(event.target.value) })} + label={t('positionLongitude')} + /> + setItem({ ...item, zoom: Number(event.target.value) })} + label={t('serverZoom')} + /> + + + + + }> + + {t('sharedPermissions')} + + + + setItem({ ...item, expirationTime: moment(e.target.value, moment.HTML5_FMT.DATE).locale('en').format() })} + disabled={!manager} + /> + setItem({ ...item, deviceLimit: Number(e.target.value) })} + label={t('userDeviceLimit')} + disabled={!admin} + /> + setItem({ ...item, userLimit: Number(e.target.value) })} + label={t('userUserLimit')} + disabled={!admin} + /> + + setItem({ ...item, disabled: e.target.checked })} />} + label={t('sharedDisabled')} + disabled={!manager} + /> + setItem({ ...item, administrator: e.target.checked })} />} + label={t('userAdmin')} + disabled={!admin} + /> + setItem({ ...item, readonly: e.target.checked })} />} + label={t('serverReadonly')} + disabled={!manager} + /> + setItem({ ...item, deviceReadonly: e.target.checked })} />} + label={t('userDeviceReadonly')} + disabled={!manager} + /> + setItem({ ...item, limitCommands: e.target.checked })} />} + label={t('userLimitCommands')} + disabled={!manager} + /> + setItem({ ...item, disableReports: e.target.checked })} />} + label={t('userDisableReports')} + disabled={!manager} + /> + setItem({ ...item, fixedEmail: e.target.checked })} />} + label={t('userFixedEmail')} + disabled={!manager} + /> + + + + setItem({ ...item, attributes })} + definitions={{ ...commonUserAttributes, ...userAttributes }} + focusAttribute={attribute} + /> + {registrationEnabled && item.id === currentUser.id && !manager && ( + + }> + + {t('userDeleteAccount')} + + + + setDeleteEmail(event.target.value)} + label={t('userEmail')} + error={deleteFailed} + /> + + + + )} + + )} + + ); +}; + +export default UserPage; diff --git a/modern/src/settings/UsersPage.js b/modern/src/settings/UsersPage.js deleted file mode 100644 index d04f2a2b..00000000 --- a/modern/src/settings/UsersPage.js +++ /dev/null @@ -1,112 +0,0 @@ -import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import LoginIcon from '@mui/icons-material/Login'; -import LinkIcon from '@mui/icons-material/Link'; -import { useCatch, useEffectAsync } from '../reactHelper'; -import { formatBoolean, formatTime } from '../common/util/formatter'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import SettingsMenu from './components/SettingsMenu'; -import CollectionFab from './components/CollectionFab'; -import CollectionActions from './components/CollectionActions'; -import TableShimmer from '../common/components/TableShimmer'; -import { useManager } from '../common/util/permissions'; -import SearchHeader, { filterByKeyword } from './components/SearchHeader'; -import { usePreference } from '../common/util/preferences'; -import useSettingsStyles from './common/useSettingsStyles'; - -const UsersPage = () => { - const classes = useSettingsStyles(); - const navigate = useNavigate(); - const t = useTranslation(); - - const manager = useManager(); - - const hours12 = usePreference('twelveHourFormat'); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [loading, setLoading] = useState(false); - - const handleLogin = useCatch(async (userId) => { - const response = await fetch(`/api/session/${userId}`); - if (response.ok) { - window.location.replace('/'); - } else { - throw Error(await response.text()); - } - }); - - const actionLogin = { - key: 'login', - title: t('loginLogin'), - icon: , - handler: handleLogin, - }; - - const actionConnections = { - key: 'connections', - title: t('sharedConnections'), - icon: , - handler: (userId) => navigate(`/settings/user/${userId}/connections`), - }; - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/users'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - return ( - } breadcrumbs={['settingsTitle', 'settingsUsers']}> - - - - - {t('sharedName')} - {t('userEmail')} - {t('userAdmin')} - {t('sharedDisabled')} - {t('userExpirationTime')} - - - - - {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( - - {item.name} - {item.email} - {formatBoolean(item.administrator, t)} - {formatBoolean(item.disabled, t)} - {formatTime(item.expirationTime, 'date', hours12)} - - - - - )) : ()} - -
    - -
    - ); -}; - -export default UsersPage; diff --git a/modern/src/settings/UsersPage.jsx b/modern/src/settings/UsersPage.jsx new file mode 100644 index 00000000..d04f2a2b --- /dev/null +++ b/modern/src/settings/UsersPage.jsx @@ -0,0 +1,112 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Table, TableRow, TableCell, TableHead, TableBody, +} from '@mui/material'; +import LoginIcon from '@mui/icons-material/Login'; +import LinkIcon from '@mui/icons-material/Link'; +import { useCatch, useEffectAsync } from '../reactHelper'; +import { formatBoolean, formatTime } from '../common/util/formatter'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import SettingsMenu from './components/SettingsMenu'; +import CollectionFab from './components/CollectionFab'; +import CollectionActions from './components/CollectionActions'; +import TableShimmer from '../common/components/TableShimmer'; +import { useManager } from '../common/util/permissions'; +import SearchHeader, { filterByKeyword } from './components/SearchHeader'; +import { usePreference } from '../common/util/preferences'; +import useSettingsStyles from './common/useSettingsStyles'; + +const UsersPage = () => { + const classes = useSettingsStyles(); + const navigate = useNavigate(); + const t = useTranslation(); + + const manager = useManager(); + + const hours12 = usePreference('twelveHourFormat'); + + const [timestamp, setTimestamp] = useState(Date.now()); + const [items, setItems] = useState([]); + const [searchKeyword, setSearchKeyword] = useState(''); + const [loading, setLoading] = useState(false); + + const handleLogin = useCatch(async (userId) => { + const response = await fetch(`/api/session/${userId}`); + if (response.ok) { + window.location.replace('/'); + } else { + throw Error(await response.text()); + } + }); + + const actionLogin = { + key: 'login', + title: t('loginLogin'), + icon: , + handler: handleLogin, + }; + + const actionConnections = { + key: 'connections', + title: t('sharedConnections'), + icon: , + handler: (userId) => navigate(`/settings/user/${userId}/connections`), + }; + + useEffectAsync(async () => { + setLoading(true); + try { + const response = await fetch('/api/users'); + if (response.ok) { + setItems(await response.json()); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + }, [timestamp]); + + return ( + } breadcrumbs={['settingsTitle', 'settingsUsers']}> + + + + + {t('sharedName')} + {t('userEmail')} + {t('userAdmin')} + {t('sharedDisabled')} + {t('userExpirationTime')} + + + + + {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => ( + + {item.name} + {item.email} + {formatBoolean(item.administrator, t)} + {formatBoolean(item.disabled, t)} + {formatTime(item.expirationTime, 'date', hours12)} + + + + + )) : ()} + +
    + +
    + ); +}; + +export default UsersPage; diff --git a/modern/src/settings/components/AddAttributeDialog.js b/modern/src/settings/components/AddAttributeDialog.js deleted file mode 100644 index 86ff64ea..00000000 --- a/modern/src/settings/components/AddAttributeDialog.js +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useState } from 'react'; -import { - Button, Dialog, DialogActions, DialogContent, FormControl, InputLabel, MenuItem, Select, TextField, Autocomplete, -} from '@mui/material'; - -import { createFilterOptions } from '@mui/material/useAutocomplete'; -import { makeStyles } from '@mui/styles'; -import { useTranslation } from '../../common/components/LocalizationProvider'; - -const useStyles = makeStyles((theme) => ({ - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(1), - paddingTop: theme.spacing(3), - }, -})); - -const AddAttributeDialog = ({ open, onResult, definitions }) => { - const classes = useStyles(); - const t = useTranslation(); - - 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 ( - - - { - 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) => (option && typeof option === 'object' ? option.name : option)} - renderOption={(props, option) => ( -
  • - {option.name} -
  • - )} - renderInput={(params) => ( - - )} - freeSolo - /> - - {t('sharedType')} - - -
    - - - - -
    - ); -}; - -export default AddAttributeDialog; diff --git a/modern/src/settings/components/AddAttributeDialog.jsx b/modern/src/settings/components/AddAttributeDialog.jsx new file mode 100644 index 00000000..86ff64ea --- /dev/null +++ b/modern/src/settings/components/AddAttributeDialog.jsx @@ -0,0 +1,104 @@ +import React, { useState } from 'react'; +import { + Button, Dialog, DialogActions, DialogContent, FormControl, InputLabel, MenuItem, Select, TextField, Autocomplete, +} from '@mui/material'; + +import { createFilterOptions } from '@mui/material/useAutocomplete'; +import { makeStyles } from '@mui/styles'; +import { useTranslation } from '../../common/components/LocalizationProvider'; + +const useStyles = makeStyles((theme) => ({ + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(1), + paddingTop: theme.spacing(3), + }, +})); + +const AddAttributeDialog = ({ open, onResult, definitions }) => { + const classes = useStyles(); + const t = useTranslation(); + + 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 ( + + + { + 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) => (option && typeof option === 'object' ? option.name : option)} + renderOption={(props, option) => ( +
  • + {option.name} +
  • + )} + renderInput={(params) => ( + + )} + freeSolo + /> + + {t('sharedType')} + + +
    + + + + +
    + ); +}; + +export default AddAttributeDialog; diff --git a/modern/src/settings/components/BaseCommandView.js b/modern/src/settings/components/BaseCommandView.js deleted file mode 100644 index acf39090..00000000 --- a/modern/src/settings/components/BaseCommandView.js +++ /dev/null @@ -1,79 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { - TextField, FormControlLabel, Checkbox, -} from '@mui/material'; -import { useTranslation } from '../../common/components/LocalizationProvider'; -import SelectField from '../../common/components/SelectField'; -import { prefixString } from '../../common/util/stringUtils'; -import useCommandAttributes from '../../common/attributes/useCommandAttributes'; - -const BaseCommandView = ({ deviceId, item, setItem }) => { - const t = useTranslation(); - - const textEnabled = useSelector((state) => state.session.server.textEnabled); - - const availableAttributes = useCommandAttributes(t); - - const [attributes, setAttributes] = useState([]); - - useEffect(() => { - if (item && item.type) { - setAttributes(availableAttributes[item.type] || []); - } else { - setAttributes([]); - } - }, [availableAttributes, item]); - - return ( - <> - setItem({ ...item, type: e.target.value, attributes: {} })} - endpoint={deviceId ? `/api/commands/types?${new URLSearchParams({ deviceId }).toString()}` : '/api/commands/types'} - keyGetter={(it) => it.type} - titleGetter={(it) => t(prefixString('command', it.type))} - label={t('sharedType')} - /> - {attributes.map(({ key, name, type }) => { - if (type === 'boolean') { - return ( - { - const updateItem = { ...item, attributes: { ...item.attributes } }; - updateItem.attributes[key] = e.target.checked; - setItem(updateItem); - }} - /> - )} - label={name} - /> - ); - } - return ( - { - const updateItem = { ...item, attributes: { ...item.attributes } }; - updateItem.attributes[key] = type === 'number' ? Number(e.target.value) : e.target.value; - setItem(updateItem); - }} - label={name} - /> - ); - })} - {textEnabled && ( - setItem({ ...item, textChannel: event.target.checked })} />} - label={t('commandSendSms')} - /> - )} - - ); -}; - -export default BaseCommandView; diff --git a/modern/src/settings/components/BaseCommandView.jsx b/modern/src/settings/components/BaseCommandView.jsx new file mode 100644 index 00000000..acf39090 --- /dev/null +++ b/modern/src/settings/components/BaseCommandView.jsx @@ -0,0 +1,79 @@ +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { + TextField, FormControlLabel, Checkbox, +} from '@mui/material'; +import { useTranslation } from '../../common/components/LocalizationProvider'; +import SelectField from '../../common/components/SelectField'; +import { prefixString } from '../../common/util/stringUtils'; +import useCommandAttributes from '../../common/attributes/useCommandAttributes'; + +const BaseCommandView = ({ deviceId, item, setItem }) => { + const t = useTranslation(); + + const textEnabled = useSelector((state) => state.session.server.textEnabled); + + const availableAttributes = useCommandAttributes(t); + + const [attributes, setAttributes] = useState([]); + + useEffect(() => { + if (item && item.type) { + setAttributes(availableAttributes[item.type] || []); + } else { + setAttributes([]); + } + }, [availableAttributes, item]); + + return ( + <> + setItem({ ...item, type: e.target.value, attributes: {} })} + endpoint={deviceId ? `/api/commands/types?${new URLSearchParams({ deviceId }).toString()}` : '/api/commands/types'} + keyGetter={(it) => it.type} + titleGetter={(it) => t(prefixString('command', it.type))} + label={t('sharedType')} + /> + {attributes.map(({ key, name, type }) => { + if (type === 'boolean') { + return ( + { + const updateItem = { ...item, attributes: { ...item.attributes } }; + updateItem.attributes[key] = e.target.checked; + setItem(updateItem); + }} + /> + )} + label={name} + /> + ); + } + return ( + { + const updateItem = { ...item, attributes: { ...item.attributes } }; + updateItem.attributes[key] = type === 'number' ? Number(e.target.value) : e.target.value; + setItem(updateItem); + }} + label={name} + /> + ); + })} + {textEnabled && ( + setItem({ ...item, textChannel: event.target.checked })} />} + label={t('commandSendSms')} + /> + )} + + ); +}; + +export default BaseCommandView; diff --git a/modern/src/settings/components/CollectionActions.js b/modern/src/settings/components/CollectionActions.js deleted file mode 100644 index 666052d5..00000000 --- a/modern/src/settings/components/CollectionActions.js +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useState } from 'react'; -import { - IconButton, Menu, MenuItem, useMediaQuery, useTheme, -} from '@mui/material'; -import Tooltip from '@mui/material/Tooltip'; -import MoreVertIcon from '@mui/icons-material/MoreVert'; -import EditIcon from '@mui/icons-material/Edit'; -import DeleteIcon from '@mui/icons-material/Delete'; -import { useNavigate } from 'react-router-dom'; -import { makeStyles } from '@mui/styles'; -import RemoveDialog from '../../common/components/RemoveDialog'; -import { useTranslation } from '../../common/components/LocalizationProvider'; - -const useStyles = makeStyles(() => ({ - row: { - display: 'flex', - }, -})); - -const CollectionActions = ({ - itemId, editPath, endpoint, setTimestamp, customActions, readonly, -}) => { - const theme = useTheme(); - const classes = useStyles(); - const navigate = useNavigate(); - const t = useTranslation(); - - const phone = useMediaQuery(theme.breakpoints.down('sm')); - - const [menuAnchorEl, setMenuAnchorEl] = useState(null); - const [removing, setRemoving] = useState(false); - - const handleEdit = () => { - navigate(`${editPath}/${itemId}`); - setMenuAnchorEl(null); - }; - - const handleRemove = () => { - setRemoving(true); - setMenuAnchorEl(null); - }; - - const handleCustom = (action) => { - action.handler(itemId); - setMenuAnchorEl(null); - }; - - const handleRemoveResult = (removed) => { - setRemoving(false); - if (removed) { - setTimestamp(Date.now()); - } - }; - - return ( - <> - {phone ? ( - <> - setMenuAnchorEl(event.currentTarget)}> - - - setMenuAnchorEl(null)}> - {customActions && customActions.map((action) => ( - handleCustom(action)} key={action.key}>{action.title} - ))} - {!readonly && ( - <> - {t('sharedEdit')} - {t('sharedRemove')} - - )} - - - ) : ( -
    - {customActions && customActions.map((action) => ( - - handleCustom(action)}> - {action.icon} - - - ))} - {!readonly && ( - <> - - - - - - - - - - - - )} -
    - )} - - - ); -}; - -export default CollectionActions; diff --git a/modern/src/settings/components/CollectionActions.jsx b/modern/src/settings/components/CollectionActions.jsx new file mode 100644 index 00000000..666052d5 --- /dev/null +++ b/modern/src/settings/components/CollectionActions.jsx @@ -0,0 +1,104 @@ +import React, { useState } from 'react'; +import { + IconButton, Menu, MenuItem, useMediaQuery, useTheme, +} from '@mui/material'; +import Tooltip from '@mui/material/Tooltip'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import { useNavigate } from 'react-router-dom'; +import { makeStyles } from '@mui/styles'; +import RemoveDialog from '../../common/components/RemoveDialog'; +import { useTranslation } from '../../common/components/LocalizationProvider'; + +const useStyles = makeStyles(() => ({ + row: { + display: 'flex', + }, +})); + +const CollectionActions = ({ + itemId, editPath, endpoint, setTimestamp, customActions, readonly, +}) => { + const theme = useTheme(); + const classes = useStyles(); + const navigate = useNavigate(); + const t = useTranslation(); + + const phone = useMediaQuery(theme.breakpoints.down('sm')); + + const [menuAnchorEl, setMenuAnchorEl] = useState(null); + const [removing, setRemoving] = useState(false); + + const handleEdit = () => { + navigate(`${editPath}/${itemId}`); + setMenuAnchorEl(null); + }; + + const handleRemove = () => { + setRemoving(true); + setMenuAnchorEl(null); + }; + + const handleCustom = (action) => { + action.handler(itemId); + setMenuAnchorEl(null); + }; + + const handleRemoveResult = (removed) => { + setRemoving(false); + if (removed) { + setTimestamp(Date.now()); + } + }; + + return ( + <> + {phone ? ( + <> + setMenuAnchorEl(event.currentTarget)}> + + + setMenuAnchorEl(null)}> + {customActions && customActions.map((action) => ( + handleCustom(action)} key={action.key}>{action.title} + ))} + {!readonly && ( + <> + {t('sharedEdit')} + {t('sharedRemove')} + + )} + + + ) : ( +
    + {customActions && customActions.map((action) => ( + + handleCustom(action)}> + {action.icon} + + + ))} + {!readonly && ( + <> + + + + + + + + + + + + )} +
    + )} + + + ); +}; + +export default CollectionActions; diff --git a/modern/src/settings/components/CollectionFab.js b/modern/src/settings/components/CollectionFab.js deleted file mode 100644 index 3c1fa783..00000000 --- a/modern/src/settings/components/CollectionFab.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { Fab } from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import AddIcon from '@mui/icons-material/Add'; -import { useNavigate } from 'react-router-dom'; -import { useRestriction } from '../../common/util/permissions'; - -const useStyles = makeStyles((theme) => ({ - fab: { - position: 'fixed', - bottom: theme.spacing(2), - right: theme.spacing(2), - [theme.breakpoints.down('md')]: { - bottom: `calc(${theme.dimensions.bottomBarHeight}px + ${theme.spacing(2)})`, - }, - }, -})); - -const CollectionFab = ({ editPath, disabled }) => { - const classes = useStyles(); - const navigate = useNavigate(); - - const readonly = useRestriction('readonly'); - - if (!readonly && !disabled) { - return ( - navigate(editPath)}> - - - ); - } - return ''; -}; - -export default CollectionFab; diff --git a/modern/src/settings/components/CollectionFab.jsx b/modern/src/settings/components/CollectionFab.jsx new file mode 100644 index 00000000..3c1fa783 --- /dev/null +++ b/modern/src/settings/components/CollectionFab.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Fab } from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import AddIcon from '@mui/icons-material/Add'; +import { useNavigate } from 'react-router-dom'; +import { useRestriction } from '../../common/util/permissions'; + +const useStyles = makeStyles((theme) => ({ + fab: { + position: 'fixed', + bottom: theme.spacing(2), + right: theme.spacing(2), + [theme.breakpoints.down('md')]: { + bottom: `calc(${theme.dimensions.bottomBarHeight}px + ${theme.spacing(2)})`, + }, + }, +})); + +const CollectionFab = ({ editPath, disabled }) => { + const classes = useStyles(); + const navigate = useNavigate(); + + const readonly = useRestriction('readonly'); + + if (!readonly && !disabled) { + return ( + navigate(editPath)}> + + + ); + } + return ''; +}; + +export default CollectionFab; diff --git a/modern/src/settings/components/EditAttributesAccordion.js b/modern/src/settings/components/EditAttributesAccordion.js deleted file mode 100644 index 214ddb0e..00000000 --- a/modern/src/settings/components/EditAttributesAccordion.js +++ /dev/null @@ -1,229 +0,0 @@ -import React, { useState } from 'react'; - -import { - Button, - Checkbox, - OutlinedInput, - FormControl, - FormControlLabel, - Grid, - IconButton, - InputAdornment, - InputLabel, - Accordion, - AccordionSummary, - Typography, - AccordionDetails, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import CloseIcon from '@mui/icons-material/Close'; -import AddIcon from '@mui/icons-material/Add'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import AddAttributeDialog from './AddAttributeDialog'; -import { useTranslation } from '../../common/components/LocalizationProvider'; -import { useAttributePreference } from '../../common/util/preferences'; -import { - distanceFromMeters, distanceToMeters, distanceUnitString, speedFromKnots, speedToKnots, speedUnitString, volumeFromLiters, volumeToLiters, volumeUnitString, -} from '../../common/util/converter'; -import useFeatures from '../../common/util/useFeatures'; - -const useStyles = makeStyles((theme) => ({ - removeButton: { - marginRight: theme.spacing(1.5), - }, - details: { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - paddingBottom: theme.spacing(3), - }, -})); - -const EditAttributesAccordion = ({ attribute, attributes, setAttributes, definitions, focusAttribute }) => { - const classes = useStyles(); - const t = useTranslation(); - - const features = useFeatures(); - - const speedUnit = useAttributePreference('speedUnit'); - const distanceUnit = useAttributePreference('distanceUnit'); - const volumeUnit = useAttributePreference('volumeUnit'); - - const [addDialogShown, setAddDialogShown] = useState(false); - - const updateAttribute = (key, value, type, subtype) => { - const updatedAttributes = { ...attributes }; - switch (subtype) { - case 'speed': - updatedAttributes[key] = speedToKnots(Number(value), speedUnit); - break; - case 'distance': - updatedAttributes[key] = distanceToMeters(Number(value), distanceUnit); - break; - case 'volume': - updatedAttributes[key] = volumeToLiters(Number(value), volumeUnit); - break; - default: - updatedAttributes[key] = type === 'number' ? Number(value) : value; - break; - } - setAttributes(updatedAttributes); - }; - - const deleteAttribute = (key) => { - const updatedAttributes = { ...attributes }; - delete updatedAttributes[key]; - setAttributes(updatedAttributes); - }; - - const getAttributeName = (key, subtype) => { - const definition = definitions[key]; - const name = definition ? definition.name : key; - switch (subtype) { - case 'speed': - return `${name} (${speedUnitString(speedUnit, t)})`; - case 'distance': - return `${name} (${distanceUnitString(distanceUnit, t)})`; - case 'volume': - return `${name} (${volumeUnitString(volumeUnit, t)})`; - default: - return name; - } - }; - - const getAttributeType = (value) => { - if (typeof value === 'number') { - return 'number'; - } if (typeof value === 'boolean') { - return 'boolean'; - } - return 'string'; - }; - - const getAttributeSubtype = (key) => { - const definition = definitions[key]; - return definition && definition.subtype; - }; - - const getDisplayValue = (value, subtype) => { - if (value) { - switch (subtype) { - case 'speed': - return speedFromKnots(value, speedUnit); - case 'distance': - return distanceFromMeters(value, distanceUnit); - case 'volume': - return volumeFromLiters(value, volumeUnit); - default: - return value; - } - } - return ''; - }; - - const convertToList = (attributes) => { - const booleanList = []; - const otherList = []; - const excludeAttributes = ['speedUnit', 'distanceUnit', 'volumeUnit', 'timezone']; - Object.keys(attributes || []).filter((key) => !excludeAttributes.includes(key)).forEach((key) => { - const value = attributes[key]; - const type = getAttributeType(value); - const subtype = getAttributeSubtype(key); - if (type === 'boolean') { - booleanList.push({ - key, value, type, subtype, - }); - } else { - otherList.push({ - key, value, type, subtype, - }); - } - }); - return [...otherList, ...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; - } - } - }; - - return features.disableAttributes ? '' : ( - - }> - - {t('sharedAttributes')} - - - - {convertToList(attributes).map(({ - key, value, type, subtype, - }) => { - if (type === 'boolean') { - return ( - - updateAttribute(key, e.target.checked)} - /> - )} - label={getAttributeName(key, subtype)} - /> - deleteAttribute(key)}> - - - - ); - } - return ( - - {getAttributeName(key, subtype)} - updateAttribute(key, e.target.value, type, subtype)} - autoFocus={focusAttribute === key} - endAdornment={( - - deleteAttribute(key)}> - - - - )} - /> - - ); - })} - - - - - ); -}; - -export default EditAttributesAccordion; diff --git a/modern/src/settings/components/EditAttributesAccordion.jsx b/modern/src/settings/components/EditAttributesAccordion.jsx new file mode 100644 index 00000000..214ddb0e --- /dev/null +++ b/modern/src/settings/components/EditAttributesAccordion.jsx @@ -0,0 +1,229 @@ +import React, { useState } from 'react'; + +import { + Button, + Checkbox, + OutlinedInput, + FormControl, + FormControlLabel, + Grid, + IconButton, + InputAdornment, + InputLabel, + Accordion, + AccordionSummary, + Typography, + AccordionDetails, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import CloseIcon from '@mui/icons-material/Close'; +import AddIcon from '@mui/icons-material/Add'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import AddAttributeDialog from './AddAttributeDialog'; +import { useTranslation } from '../../common/components/LocalizationProvider'; +import { useAttributePreference } from '../../common/util/preferences'; +import { + distanceFromMeters, distanceToMeters, distanceUnitString, speedFromKnots, speedToKnots, speedUnitString, volumeFromLiters, volumeToLiters, volumeUnitString, +} from '../../common/util/converter'; +import useFeatures from '../../common/util/useFeatures'; + +const useStyles = makeStyles((theme) => ({ + removeButton: { + marginRight: theme.spacing(1.5), + }, + details: { + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingBottom: theme.spacing(3), + }, +})); + +const EditAttributesAccordion = ({ attribute, attributes, setAttributes, definitions, focusAttribute }) => { + const classes = useStyles(); + const t = useTranslation(); + + const features = useFeatures(); + + const speedUnit = useAttributePreference('speedUnit'); + const distanceUnit = useAttributePreference('distanceUnit'); + const volumeUnit = useAttributePreference('volumeUnit'); + + const [addDialogShown, setAddDialogShown] = useState(false); + + const updateAttribute = (key, value, type, subtype) => { + const updatedAttributes = { ...attributes }; + switch (subtype) { + case 'speed': + updatedAttributes[key] = speedToKnots(Number(value), speedUnit); + break; + case 'distance': + updatedAttributes[key] = distanceToMeters(Number(value), distanceUnit); + break; + case 'volume': + updatedAttributes[key] = volumeToLiters(Number(value), volumeUnit); + break; + default: + updatedAttributes[key] = type === 'number' ? Number(value) : value; + break; + } + setAttributes(updatedAttributes); + }; + + const deleteAttribute = (key) => { + const updatedAttributes = { ...attributes }; + delete updatedAttributes[key]; + setAttributes(updatedAttributes); + }; + + const getAttributeName = (key, subtype) => { + const definition = definitions[key]; + const name = definition ? definition.name : key; + switch (subtype) { + case 'speed': + return `${name} (${speedUnitString(speedUnit, t)})`; + case 'distance': + return `${name} (${distanceUnitString(distanceUnit, t)})`; + case 'volume': + return `${name} (${volumeUnitString(volumeUnit, t)})`; + default: + return name; + } + }; + + const getAttributeType = (value) => { + if (typeof value === 'number') { + return 'number'; + } if (typeof value === 'boolean') { + return 'boolean'; + } + return 'string'; + }; + + const getAttributeSubtype = (key) => { + const definition = definitions[key]; + return definition && definition.subtype; + }; + + const getDisplayValue = (value, subtype) => { + if (value) { + switch (subtype) { + case 'speed': + return speedFromKnots(value, speedUnit); + case 'distance': + return distanceFromMeters(value, distanceUnit); + case 'volume': + return volumeFromLiters(value, volumeUnit); + default: + return value; + } + } + return ''; + }; + + const convertToList = (attributes) => { + const booleanList = []; + const otherList = []; + const excludeAttributes = ['speedUnit', 'distanceUnit', 'volumeUnit', 'timezone']; + Object.keys(attributes || []).filter((key) => !excludeAttributes.includes(key)).forEach((key) => { + const value = attributes[key]; + const type = getAttributeType(value); + const subtype = getAttributeSubtype(key); + if (type === 'boolean') { + booleanList.push({ + key, value, type, subtype, + }); + } else { + otherList.push({ + key, value, type, subtype, + }); + } + }); + return [...otherList, ...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; + } + } + }; + + return features.disableAttributes ? '' : ( + + }> + + {t('sharedAttributes')} + + + + {convertToList(attributes).map(({ + key, value, type, subtype, + }) => { + if (type === 'boolean') { + return ( + + updateAttribute(key, e.target.checked)} + /> + )} + label={getAttributeName(key, subtype)} + /> + deleteAttribute(key)}> + + + + ); + } + return ( + + {getAttributeName(key, subtype)} + updateAttribute(key, e.target.value, type, subtype)} + autoFocus={focusAttribute === key} + endAdornment={( + + deleteAttribute(key)}> + + + + )} + /> + + ); + })} + + + + + ); +}; + +export default EditAttributesAccordion; diff --git a/modern/src/settings/components/EditItemView.js b/modern/src/settings/components/EditItemView.js deleted file mode 100644 index d45855dd..00000000 --- a/modern/src/settings/components/EditItemView.js +++ /dev/null @@ -1,120 +0,0 @@ -import React from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import makeStyles from '@mui/styles/makeStyles'; -import { - Container, Button, Accordion, AccordionDetails, AccordionSummary, Skeleton, Typography, TextField, -} from '@mui/material'; -import { useCatch, useEffectAsync } from '../../reactHelper'; -import { useTranslation } from '../../common/components/LocalizationProvider'; -import PageLayout from '../../common/components/PageLayout'; - -const useStyles = makeStyles((theme) => ({ - container: { - marginTop: theme.spacing(2), - }, - buttons: { - marginTop: theme.spacing(2), - marginBottom: theme.spacing(2), - display: 'flex', - justifyContent: 'space-evenly', - '& > *': { - flexBasis: '33%', - }, - }, - details: { - display: 'flex', - flexDirection: 'column', - }, -})); - -const EditItemView = ({ - children, endpoint, item, setItem, defaultItem, validate, onItemSaved, menu, breadcrumbs, -}) => { - const navigate = useNavigate(); - const classes = useStyles(); - const t = useTranslation(); - - const { id } = useParams(); - - useEffectAsync(async () => { - if (!item) { - if (id) { - const response = await fetch(`/api/${endpoint}/${id}`); - if (response.ok) { - setItem(await response.json()); - } else { - throw Error(await response.text()); - } - } else { - setItem(defaultItem || {}); - } - } - }, [id, item, defaultItem]); - - const handleSave = useCatch(async () => { - let url = `/api/${endpoint}`; - if (id) { - url += `/${id}`; - } - - const response = await fetch(url, { - method: !id ? 'POST' : 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(item), - }); - - if (response.ok) { - if (onItemSaved) { - onItemSaved(await response.json()); - } - navigate(-1); - } else { - throw Error(await response.text()); - } - }); - - return ( - - - {item ? children : ( - - - - - - - - {[...Array(3)].map((_, i) => ( - - - - ))} - - - )} -
    - - -
    -
    -
    - ); -}; - -export default EditItemView; diff --git a/modern/src/settings/components/EditItemView.jsx b/modern/src/settings/components/EditItemView.jsx new file mode 100644 index 00000000..d45855dd --- /dev/null +++ b/modern/src/settings/components/EditItemView.jsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import makeStyles from '@mui/styles/makeStyles'; +import { + Container, Button, Accordion, AccordionDetails, AccordionSummary, Skeleton, Typography, TextField, +} from '@mui/material'; +import { useCatch, useEffectAsync } from '../../reactHelper'; +import { useTranslation } from '../../common/components/LocalizationProvider'; +import PageLayout from '../../common/components/PageLayout'; + +const useStyles = makeStyles((theme) => ({ + container: { + marginTop: theme.spacing(2), + }, + buttons: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + display: 'flex', + justifyContent: 'space-evenly', + '& > *': { + flexBasis: '33%', + }, + }, + details: { + display: 'flex', + flexDirection: 'column', + }, +})); + +const EditItemView = ({ + children, endpoint, item, setItem, defaultItem, validate, onItemSaved, menu, breadcrumbs, +}) => { + const navigate = useNavigate(); + const classes = useStyles(); + const t = useTranslation(); + + const { id } = useParams(); + + useEffectAsync(async () => { + if (!item) { + if (id) { + const response = await fetch(`/api/${endpoint}/${id}`); + if (response.ok) { + setItem(await response.json()); + } else { + throw Error(await response.text()); + } + } else { + setItem(defaultItem || {}); + } + } + }, [id, item, defaultItem]); + + const handleSave = useCatch(async () => { + let url = `/api/${endpoint}`; + if (id) { + url += `/${id}`; + } + + const response = await fetch(url, { + method: !id ? 'POST' : 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(item), + }); + + if (response.ok) { + if (onItemSaved) { + onItemSaved(await response.json()); + } + navigate(-1); + } else { + throw Error(await response.text()); + } + }); + + return ( + + + {item ? children : ( + + + + + + + + {[...Array(3)].map((_, i) => ( + + + + ))} + + + )} +
    + + +
    +
    +
    + ); +}; + +export default EditItemView; diff --git a/modern/src/settings/components/SearchHeader.js b/modern/src/settings/components/SearchHeader.js deleted file mode 100644 index 25757ed2..00000000 --- a/modern/src/settings/components/SearchHeader.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { TextField, useTheme, useMediaQuery } from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import { useTranslation } from '../../common/components/LocalizationProvider'; - -export const filterByKeyword = (keyword) => (item) => !keyword || JSON.stringify(item).toLowerCase().includes(keyword.toLowerCase()); - -const useStyles = makeStyles((theme) => ({ - header: { - position: 'sticky', - left: 0, - display: 'flex', - flexDirection: 'column', - alignItems: 'stretch', - padding: theme.spacing(3, 2, 2), - }, -})); - -const SearchHeader = ({ keyword, setKeyword }) => { - const theme = useTheme(); - const classes = useStyles(); - const t = useTranslation(); - - const phone = useMediaQuery(theme.breakpoints.down('sm')); - - return phone ? ( -
    - setKeyword(e.target.value)} - /> -
    - ) : ''; -}; - -export default SearchHeader; diff --git a/modern/src/settings/components/SearchHeader.jsx b/modern/src/settings/components/SearchHeader.jsx new file mode 100644 index 00000000..25757ed2 --- /dev/null +++ b/modern/src/settings/components/SearchHeader.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { TextField, useTheme, useMediaQuery } from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import { useTranslation } from '../../common/components/LocalizationProvider'; + +export const filterByKeyword = (keyword) => (item) => !keyword || JSON.stringify(item).toLowerCase().includes(keyword.toLowerCase()); + +const useStyles = makeStyles((theme) => ({ + header: { + position: 'sticky', + left: 0, + display: 'flex', + flexDirection: 'column', + alignItems: 'stretch', + padding: theme.spacing(3, 2, 2), + }, +})); + +const SearchHeader = ({ keyword, setKeyword }) => { + const theme = useTheme(); + const classes = useStyles(); + const t = useTranslation(); + + const phone = useMediaQuery(theme.breakpoints.down('sm')); + + return phone ? ( +
    + setKeyword(e.target.value)} + /> +
    + ) : ''; +}; + +export default SearchHeader; diff --git a/modern/src/settings/components/SettingsMenu.js b/modern/src/settings/components/SettingsMenu.js deleted file mode 100644 index 96580974..00000000 --- a/modern/src/settings/components/SettingsMenu.js +++ /dev/null @@ -1,153 +0,0 @@ -import React from 'react'; -import { - Divider, List, ListItemButton, ListItemIcon, ListItemText, -} from '@mui/material'; -import SettingsIcon from '@mui/icons-material/Settings'; -import CreateIcon from '@mui/icons-material/Create'; -import NotificationsIcon from '@mui/icons-material/Notifications'; -import FolderIcon from '@mui/icons-material/Folder'; -import PersonIcon from '@mui/icons-material/Person'; -import StorageIcon from '@mui/icons-material/Storage'; -import BuildIcon from '@mui/icons-material/Build'; -import PeopleIcon from '@mui/icons-material/People'; -import TodayIcon from '@mui/icons-material/Today'; -import PublishIcon from '@mui/icons-material/Publish'; -import SmartphoneIcon from '@mui/icons-material/Smartphone'; -import { Link, useLocation } from 'react-router-dom'; -import { useSelector } from 'react-redux'; -import { useTranslation } from '../../common/components/LocalizationProvider'; -import { - useAdministrator, useManager, useRestriction, -} from '../../common/util/permissions'; -import useFeatures from '../../common/util/useFeatures'; - -const MenuItem = ({ - title, link, icon, selected, -}) => ( - - {icon} - - -); - -const SettingsMenu = () => { - const t = useTranslation(); - const location = useLocation(); - - const readonly = useRestriction('readonly'); - const admin = useAdministrator(); - const manager = useManager(); - const userId = useSelector((state) => state.session.user.id); - - const features = useFeatures(); - - return ( - <> - - } - selected={location.pathname === '/settings/preferences'} - /> - {!readonly && ( - <> - } - selected={location.pathname.startsWith('/settings/notification')} - /> - } - selected={location.pathname === `/settings/user/${userId}`} - /> - } - selected={location.pathname.startsWith('/settings/device')} - /> - } - selected={location.pathname.startsWith('/settings/geofence')} - /> - {!features.disableGroups && ( - } - selected={location.pathname.startsWith('/settings/group')} - /> - )} - {!features.disableDrivers && ( - } - selected={location.pathname.startsWith('/settings/driver')} - /> - )} - {!features.disableCalendars && ( - } - selected={location.pathname.startsWith('/settings/calendar')} - /> - )} - {!features.disableComputedAttributes && ( - } - selected={location.pathname.startsWith('/settings/attribute')} - /> - )} - {!features.disableMaintenance && ( - } - selected={location.pathname.startsWith('/settings/maintenance')} - /> - )} - } - selected={location.pathname.startsWith('/settings/command')} - /> - - )} - - {manager && ( - <> - - - {admin && ( - } - selected={location.pathname === '/settings/server'} - /> - )} - } - selected={location.pathname.startsWith('/settings/user') && location.pathname !== `/settings/user/${userId}`} - /> - - - )} - - ); -}; - -export default SettingsMenu; diff --git a/modern/src/settings/components/SettingsMenu.jsx b/modern/src/settings/components/SettingsMenu.jsx new file mode 100644 index 00000000..96580974 --- /dev/null +++ b/modern/src/settings/components/SettingsMenu.jsx @@ -0,0 +1,153 @@ +import React from 'react'; +import { + Divider, List, ListItemButton, ListItemIcon, ListItemText, +} from '@mui/material'; +import SettingsIcon from '@mui/icons-material/Settings'; +import CreateIcon from '@mui/icons-material/Create'; +import NotificationsIcon from '@mui/icons-material/Notifications'; +import FolderIcon from '@mui/icons-material/Folder'; +import PersonIcon from '@mui/icons-material/Person'; +import StorageIcon from '@mui/icons-material/Storage'; +import BuildIcon from '@mui/icons-material/Build'; +import PeopleIcon from '@mui/icons-material/People'; +import TodayIcon from '@mui/icons-material/Today'; +import PublishIcon from '@mui/icons-material/Publish'; +import SmartphoneIcon from '@mui/icons-material/Smartphone'; +import { Link, useLocation } from 'react-router-dom'; +import { useSelector } from 'react-redux'; +import { useTranslation } from '../../common/components/LocalizationProvider'; +import { + useAdministrator, useManager, useRestriction, +} from '../../common/util/permissions'; +import useFeatures from '../../common/util/useFeatures'; + +const MenuItem = ({ + title, link, icon, selected, +}) => ( + + {icon} + + +); + +const SettingsMenu = () => { + const t = useTranslation(); + const location = useLocation(); + + const readonly = useRestriction('readonly'); + const admin = useAdministrator(); + const manager = useManager(); + const userId = useSelector((state) => state.session.user.id); + + const features = useFeatures(); + + return ( + <> + + } + selected={location.pathname === '/settings/preferences'} + /> + {!readonly && ( + <> + } + selected={location.pathname.startsWith('/settings/notification')} + /> + } + selected={location.pathname === `/settings/user/${userId}`} + /> + } + selected={location.pathname.startsWith('/settings/device')} + /> + } + selected={location.pathname.startsWith('/settings/geofence')} + /> + {!features.disableGroups && ( + } + selected={location.pathname.startsWith('/settings/group')} + /> + )} + {!features.disableDrivers && ( + } + selected={location.pathname.startsWith('/settings/driver')} + /> + )} + {!features.disableCalendars && ( + } + selected={location.pathname.startsWith('/settings/calendar')} + /> + )} + {!features.disableComputedAttributes && ( + } + selected={location.pathname.startsWith('/settings/attribute')} + /> + )} + {!features.disableMaintenance && ( + } + selected={location.pathname.startsWith('/settings/maintenance')} + /> + )} + } + selected={location.pathname.startsWith('/settings/command')} + /> + + )} + + {manager && ( + <> + + + {admin && ( + } + selected={location.pathname === '/settings/server'} + /> + )} + } + selected={location.pathname.startsWith('/settings/user') && location.pathname !== `/settings/user/${userId}`} + /> + + + )} + + ); +}; + +export default SettingsMenu; -- cgit v1.2.3