diff --git a/interface/src/framework/ap/APSettingsForm.tsx b/interface/src/framework/ap/APSettingsForm.tsx index 66bfc2178..ba5c116a2 100644 --- a/interface/src/framework/ap/APSettingsForm.tsx +++ b/interface/src/framework/ap/APSettingsForm.tsx @@ -4,6 +4,7 @@ import { range } from 'lodash'; import { Button, Checkbox, MenuItem } from '@mui/material'; import SaveIcon from '@mui/icons-material/Save'; +import CancelIcon from '@mui/icons-material/Cancel'; import { createAPSettingsValidator, validate } from '../../validators'; import { @@ -16,7 +17,7 @@ import { } from '../../components'; import { APProvisionMode, APSettings } from '../../types'; -import { numberValue, updateValue, useRest } from '../../utils'; +import { numberValue, updateValueDirty, useRest } from '../../utils'; import * as APApi from '../../api/ap'; import { useI18nContext } from '../../i18n/i18n-react'; @@ -26,16 +27,17 @@ export const isAPEnabled = ({ provision_mode }: APSettings) => { }; const APSettingsForm: FC = () => { - const { loadData, saving, data, setData, saveData, errorMessage } = useRest({ - read: APApi.readAPSettings, - update: APApi.updateAPSettings - }); + const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, saveData, errorMessage } = + useRest({ + read: APApi.readAPSettings, + update: APApi.updateAPSettings + }); const { LL } = useI18nContext(); const [fieldErrors, setFieldErrors] = useState(); - const updateFormValue = updateValue(setData); + const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData); const content = () => { if (!data) { @@ -163,18 +165,30 @@ const APSettingsForm: FC = () => { /> )} - - - + {dirtyFlags && dirtyFlags.length !== 0 && ( + + + + + )} ); }; diff --git a/interface/src/framework/mqtt/MqttSettingsForm.tsx b/interface/src/framework/mqtt/MqttSettingsForm.tsx index 7c79251a7..5a233c40d 100644 --- a/interface/src/framework/mqtt/MqttSettingsForm.tsx +++ b/interface/src/framework/mqtt/MqttSettingsForm.tsx @@ -3,6 +3,7 @@ import { ValidateFieldsError } from 'async-validator'; import { Button, Checkbox, MenuItem, Grid, Typography, InputAdornment } from '@mui/material'; import SaveIcon from '@mui/icons-material/Save'; +import CancelIcon from '@mui/icons-material/Cancel'; import { createMqttSettingsValidator, validate } from '../../validators'; import { @@ -14,22 +15,23 @@ import { ValidatedTextField } from '../../components'; import { MqttSettings } from '../../types'; -import { numberValue, updateValue, useRest } from '../../utils'; +import { numberValue, updateValueDirty, useRest } from '../../utils'; import * as MqttApi from '../../api/mqtt'; import { useI18nContext } from '../../i18n/i18n-react'; const MqttSettingsForm: FC = () => { - const { loadData, saving, data, setData, saveData, errorMessage } = useRest({ - read: MqttApi.readMqttSettings, - update: MqttApi.updateMqttSettings - }); + const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, saveData, errorMessage } = + useRest({ + read: MqttApi.readMqttSettings, + update: MqttApi.updateMqttSettings + }); const { LL } = useI18nContext(); const [fieldErrors, setFieldErrors] = useState(); - const updateFormValue = updateValue(setData); + const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData); const content = () => { if (!data) { @@ -372,18 +374,31 @@ const MqttSettingsForm: FC = () => { /> - - - + + {dirtyFlags && dirtyFlags.length !== 0 && ( + + + + + )} ); }; diff --git a/interface/src/framework/network/NetworkSettingsForm.tsx b/interface/src/framework/network/NetworkSettingsForm.tsx index 0fc9e4927..804dfa2e1 100644 --- a/interface/src/framework/network/NetworkSettingsForm.tsx +++ b/interface/src/framework/network/NetworkSettingsForm.tsx @@ -20,6 +20,7 @@ import DeleteIcon from '@mui/icons-material/Delete'; import SaveIcon from '@mui/icons-material/Save'; import LockIcon from '@mui/icons-material/Lock'; import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; +import CancelIcon from '@mui/icons-material/Cancel'; import { BlockFormControlLabel, @@ -32,7 +33,7 @@ import { } from '../../components'; import { NetworkSettings } from '../../types'; import * as NetworkApi from '../../api/network'; -import { numberValue, updateValue, useRest } from '../../utils'; +import { numberValue, updateValueDirty, useRest } from '../../utils'; import * as EMSESP from '../../project/api'; import { WiFiConnectionContext } from './WiFiConnectionContext'; @@ -52,7 +53,18 @@ const WiFiSettingsForm: FC = () => { const [initialized, setInitialized] = useState(false); const [restarting, setRestarting] = useState(false); - const { loadData, saving, data, setData, saveData, errorMessage, restartNeeded } = useRest({ + const { + loadData, + saving, + data, + setData, + origData, + dirtyFlags, + setDirtyFlags, + saveData, + errorMessage, + restartNeeded + } = useRest({ read: NetworkApi.readNetworkSettings, update: NetworkApi.updateNetworkSettings }); @@ -78,7 +90,7 @@ const WiFiSettingsForm: FC = () => { } }, [initialized, setInitialized, data, setData, selectedNetwork]); - const updateFormValue = updateValue(setData); + const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData); const [fieldErrors, setFieldErrors] = useState(); @@ -287,8 +299,19 @@ const WiFiSettingsForm: FC = () => { )} - {!restartNeeded && ( + + {!restartNeeded && dirtyFlags && dirtyFlags.length !== 0 && ( + )} diff --git a/interface/src/framework/ntp/NTPSettingsForm.tsx b/interface/src/framework/ntp/NTPSettingsForm.tsx index cda823dc4..e79ad4c45 100644 --- a/interface/src/framework/ntp/NTPSettingsForm.tsx +++ b/interface/src/framework/ntp/NTPSettingsForm.tsx @@ -3,11 +3,12 @@ import { ValidateFieldsError } from 'async-validator'; import { Button, Checkbox, MenuItem } from '@mui/material'; import SaveIcon from '@mui/icons-material/Save'; +import CancelIcon from '@mui/icons-material/Cancel'; import { validate } from '../../validators'; import { BlockFormControlLabel, ButtonRow, FormLoader, SectionContent, ValidatedTextField } from '../../components'; import { NTPSettings } from '../../types'; -import { updateValue, useRest } from '../../utils'; +import { updateValueDirty, useRest } from '../../utils'; import * as NTPApi from '../../api/ntp'; import { selectedTimeZone, timeZoneSelectItems, TIME_ZONES } from './TZ'; import { NTP_SETTINGS_VALIDATOR } from '../../validators/ntp'; @@ -15,14 +16,15 @@ import { NTP_SETTINGS_VALIDATOR } from '../../validators/ntp'; import { useI18nContext } from '../../i18n/i18n-react'; const NTPSettingsForm: FC = () => { - const { loadData, saving, data, setData, saveData, errorMessage } = useRest({ - read: NTPApi.readNTPSettings, - update: NTPApi.updateNTPSettings - }); + const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, saveData, errorMessage } = + useRest({ + read: NTPApi.readNTPSettings, + update: NTPApi.updateNTPSettings + }); const { LL } = useI18nContext(); - const updateFormValue = updateValue(setData); + const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData); const [fieldErrors, setFieldErrors] = useState(); @@ -79,18 +81,30 @@ const NTPSettingsForm: FC = () => { {LL.TIME_ZONE()}... {timeZoneSelectItems()} - - - + {dirtyFlags && dirtyFlags.length !== 0 && ( + + + + + )} ); }; diff --git a/interface/src/framework/ntp/NTPStatusForm.tsx b/interface/src/framework/ntp/NTPStatusForm.tsx index 645b04c97..95ff551c1 100644 --- a/interface/src/framework/ntp/NTPStatusForm.tsx +++ b/interface/src/framework/ntp/NTPStatusForm.tsx @@ -129,7 +129,7 @@ const NTPStatusForm: FC = () => { color="primary" autoFocus > - {LL.SAVE()} + {LL.UPDATE()} diff --git a/interface/src/framework/security/ManageUsersForm.tsx b/interface/src/framework/security/ManageUsersForm.tsx index 1321eedc5..314cb7ae6 100644 --- a/interface/src/framework/security/ManageUsersForm.tsx +++ b/interface/src/framework/security/ManageUsersForm.tsx @@ -185,7 +185,7 @@ const ManageUsersForm: FC = () => { type="submit" onClick={onSubmit} > - {LL.SAVE()} + {LL.UPDATE()} diff --git a/interface/src/framework/security/SecuritySettingsForm.tsx b/interface/src/framework/security/SecuritySettingsForm.tsx index 529dd1352..d432074cb 100644 --- a/interface/src/framework/security/SecuritySettingsForm.tsx +++ b/interface/src/framework/security/SecuritySettingsForm.tsx @@ -3,12 +3,13 @@ import { ValidateFieldsError } from 'async-validator'; import { Button } from '@mui/material'; import SaveIcon from '@mui/icons-material/Save'; +import CancelIcon from '@mui/icons-material/Cancel'; import * as SecurityApi from '../../api/security'; import { SecuritySettings } from '../../types'; import { ButtonRow, FormLoader, MessageBox, SectionContent, ValidatedPasswordField } from '../../components'; import { SECURITY_SETTINGS_VALIDATOR, validate } from '../../validators'; -import { updateValue, useRest } from '../../utils'; +import { updateValueDirty, useRest } from '../../utils'; import { AuthenticatedContext } from '../../contexts/authentication'; import { useI18nContext } from '../../i18n/i18n-react'; @@ -17,13 +18,15 @@ const SecuritySettingsForm: FC = () => { const { LL } = useI18nContext(); const [fieldErrors, setFieldErrors] = useState(); - const { loadData, saving, data, setData, saveData, errorMessage } = useRest({ - read: SecurityApi.readSecuritySettings, - update: SecurityApi.updateSecuritySettings - }); + const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, saveData, errorMessage } = + useRest({ + read: SecurityApi.readSecuritySettings, + update: SecurityApi.updateSecuritySettings + }); const authenticatedContext = useContext(AuthenticatedContext); - const updateFormValue = updateValue(setData); + + const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData); const content = () => { if (!data) { @@ -54,18 +57,30 @@ const SecuritySettingsForm: FC = () => { margin="normal" /> - - - + {dirtyFlags && dirtyFlags.length !== 0 && ( + + + + + )} ); }; diff --git a/interface/src/framework/system/OTASettingsForm.tsx b/interface/src/framework/system/OTASettingsForm.tsx index aa1f36dff..87a796c0c 100644 --- a/interface/src/framework/system/OTASettingsForm.tsx +++ b/interface/src/framework/system/OTASettingsForm.tsx @@ -2,6 +2,7 @@ import { FC, useState } from 'react'; import { Button, Checkbox } from '@mui/material'; import SaveIcon from '@mui/icons-material/Save'; +import CancelIcon from '@mui/icons-material/Cancel'; import * as SystemApi from '../../api/system'; import { @@ -14,7 +15,7 @@ import { } from '../../components'; import { OTASettings } from '../../types'; -import { numberValue, updateValue, useRest } from '../../utils'; +import { numberValue, updateValueDirty, useRest } from '../../utils'; import { ValidateFieldsError } from 'async-validator'; import { validate } from '../../validators'; @@ -23,14 +24,15 @@ import { OTA_SETTINGS_VALIDATOR } from '../../validators/system'; import { useI18nContext } from '../../i18n/i18n-react'; const OTASettingsForm: FC = () => { - const { loadData, saving, data, setData, saveData, errorMessage } = useRest({ - read: SystemApi.readOTASettings, - update: SystemApi.updateOTASettings - }); + const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, saveData, errorMessage } = + useRest({ + read: SystemApi.readOTASettings, + update: SystemApi.updateOTASettings + }); const { LL } = useI18nContext(); - const updateFormValue = updateValue(setData); + const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData); const [fieldErrors, setFieldErrors] = useState(); @@ -76,18 +78,30 @@ const OTASettingsForm: FC = () => { onChange={updateFormValue} margin="normal" /> - - - + {dirtyFlags && dirtyFlags.length !== 0 && ( + + + + + )} ); }; diff --git a/interface/src/i18n/de/index.ts b/interface/src/i18n/de/index.ts index cc05f91cc..66d8bf672 100644 --- a/interface/src/i18n/de/index.ts +++ b/interface/src/i18n/de/index.ts @@ -15,6 +15,8 @@ const de: Translation = { DASHBOARD: 'Kontrollzentrum', SETTINGS_OF: '{0} Einstellungen', SAVED: 'gespeichert', + APPLY_CHANGES: 'Apply Changes ({0})', // TODO translate + UPDATE: 'Update', // TODO translate HELP_OF: '{0} Hilfe', LOGGED_IN: 'Eingeloggt als {name}', PLEASE_SIGNIN: 'Bitte einloggen, um fortzufahren', diff --git a/interface/src/i18n/en/index.ts b/interface/src/i18n/en/index.ts index 70566b2c4..f6c21765c 100644 --- a/interface/src/i18n/en/index.ts +++ b/interface/src/i18n/en/index.ts @@ -48,6 +48,8 @@ const en: Translation = { RESET: 'Reset', SEND: 'Send', SAVE: 'Save', + APPLY_CHANGES: 'Apply Changes ({0})', + UPDATE: 'Update', REMOVE: 'Remove', PROBLEM_UPDATING: 'Problem updating', PROBLEM_LOADING: 'Problem loading', diff --git a/interface/src/i18n/fr/index.ts b/interface/src/i18n/fr/index.ts index 87781b93f..4bb8ccd08 100644 --- a/interface/src/i18n/fr/index.ts +++ b/interface/src/i18n/fr/index.ts @@ -48,6 +48,8 @@ const fr: Translation = { RESET: 'Réinitialiser', SEND: 'Envoyer', SAVE: 'Sauvegarder', + APPLY_CHANGES: 'Apply Changes ({0})', // TODO translate + UPDATE: 'Update', // TODO translate REMOVE: 'Enlever', PROBLEM_UPDATING: 'Problème lors de la mise à jour', PROBLEM_LOADING: 'Problème lors du chargement', diff --git a/interface/src/i18n/nl/index.ts b/interface/src/i18n/nl/index.ts index e8fcbd7c1..8aaa005ce 100644 --- a/interface/src/i18n/nl/index.ts +++ b/interface/src/i18n/nl/index.ts @@ -48,6 +48,8 @@ const nl: Translation = { RESET: 'Reset', SEND: 'Verzenden', SAVE: 'Opslaan', + APPLY_CHANGES: 'Apply Changes ({0})', // TODO translate + UPDATE: 'Update', // TODO translate REMOVE: 'Verwijderen', PROBLEM_UPDATING: 'Probleem met updaten', PROBLEM_LOADING: 'Probleem met laden', diff --git a/interface/src/i18n/no/index.ts b/interface/src/i18n/no/index.ts index 3f0b8f460..689abc67d 100644 --- a/interface/src/i18n/no/index.ts +++ b/interface/src/i18n/no/index.ts @@ -48,6 +48,8 @@ const no: Translation = { RESET: 'Nullstill', SEND: 'Send', SAVE: 'Lagre', + APPLY_CHANGES: 'Apply Changes ({0})', // TODO translate + UPDATE: 'Update', // TODO translate REMOVE: 'Fjern', PROBLEM_UPDATING: 'Problem med oppdatering', PROBLEM_LOADING: 'Problem med opplasting', diff --git a/interface/src/i18n/pl/index.ts b/interface/src/i18n/pl/index.ts index e02c32f6a..3b1302926 100644 --- a/interface/src/i18n/pl/index.ts +++ b/interface/src/i18n/pl/index.ts @@ -48,6 +48,8 @@ const pl: BaseTranslation = { RESET: 'Reset{{uj|owanie|}}', SEND: 'Wyślij', SAVE: 'Zapisz', + APPLY_CHANGES: 'Apply Changes ({0})', // TODO translate + UPDATE: 'Update', // TODO translate REMOVE: 'Usuń', PROBLEM_UPDATING: 'Problem z aktualizacją!', PROBLEM_LOADING: 'Problem z załadowaniem!', diff --git a/interface/src/i18n/sv/index.ts b/interface/src/i18n/sv/index.ts index 1b40a4dc3..f321fc959 100644 --- a/interface/src/i18n/sv/index.ts +++ b/interface/src/i18n/sv/index.ts @@ -48,6 +48,8 @@ const sv: Translation = { RESET: 'Nollsäll', SEND: 'Skicka', SAVE: 'Spara', + APPLY_CHANGES: 'Apply Changes ({0})', // TODO translate + UPDATE: 'Update', // TODO translate REMOVE: 'Ta bort', PROBLEM_UPDATING: 'Problem vid uppdatering', PROBLEM_LOADING: 'Problem vid hämtning', diff --git a/interface/src/project/DashboardData.tsx b/interface/src/project/DashboardData.tsx index ac3b2dbdc..51d3c618b 100644 --- a/interface/src/project/DashboardData.tsx +++ b/interface/src/project/DashboardData.tsx @@ -634,7 +634,7 @@ const DashboardData: FC = () => { onClick={() => sendSensor()} color="warning" > - {LL.SAVE()} + {LL.UPDATE()} @@ -1056,7 +1056,7 @@ const DashboardData: FC = () => { { onClick={() => sendAnalog()} color="warning" > - {LL.SAVE()} + {LL.UPDATE()} diff --git a/interface/src/project/SettingsApplication.tsx b/interface/src/project/SettingsApplication.tsx index b72f511cf..28e10aa38 100644 --- a/interface/src/project/SettingsApplication.tsx +++ b/interface/src/project/SettingsApplication.tsx @@ -6,6 +6,7 @@ import { useSnackbar } from 'notistack'; import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider, InputAdornment } from '@mui/material'; import SaveIcon from '@mui/icons-material/Save'; +import CancelIcon from '@mui/icons-material/Cancel'; import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; import { validate } from '../validators'; @@ -19,7 +20,7 @@ import { ButtonRow, MessageBox } from '../components'; -import { numberValue, extractErrorMessage, updateValue, useRest } from '../utils'; +import { numberValue, extractErrorMessage, updateValueDirty, useRest } from '../utils'; import * as EMSESP from './api'; import { Settings, BOARD_PROFILES } from './types'; @@ -36,7 +37,18 @@ export function boardProfileSelectItems() { } const SettingsApplication: FC = () => { - const { loadData, saveData, saving, setData, data, errorMessage, restartNeeded } = useRest({ + const { + loadData, + saveData, + saving, + setData, + data, + origData, + dirtyFlags, + setDirtyFlags, + errorMessage, + restartNeeded + } = useRest({ read: EMSESP.readSettings, update: EMSESP.writeSettings }); @@ -46,7 +58,7 @@ const SettingsApplication: FC = () => { const { enqueueSnackbar } = useSnackbar(); - const updateFormValue = updateValue(setData); + const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData); const [fieldErrors, setFieldErrors] = useState(); const [processingBoard, setProcessingBoard] = useState(false); @@ -431,7 +443,7 @@ const SettingsApplication: FC = () => { endAdornment: {LL.MINUTES()} }} variant="outlined" - value={data.shower_alert_trigger} + value={numberValue(data.shower_alert_trigger)} type="number" onChange={updateFormValue} size="small" @@ -447,7 +459,7 @@ const SettingsApplication: FC = () => { endAdornment: {LL.SECONDS()} }} variant="outlined" - value={data.shower_alert_coldshot} + value={numberValue(data.shower_alert_coldshot)} type="number" onChange={updateFormValue} size="small" @@ -566,7 +578,7 @@ const SettingsApplication: FC = () => { label="Port" fullWidth variant="outlined" - value={data.syslog_port} + value={numberValue(data.syslog_port)} type="number" onChange={updateFormValue} margin="normal" @@ -603,7 +615,7 @@ const SettingsApplication: FC = () => { }} fullWidth variant="outlined" - value={data.syslog_mark_interval} + value={numberValue(data.syslog_mark_interval)} type="number" onChange={updateFormValue} margin="normal" @@ -619,8 +631,19 @@ const SettingsApplication: FC = () => { )} - {!restartNeeded && ( + + {!restartNeeded && dirtyFlags && dirtyFlags.length !== 0 && ( + )} diff --git a/interface/src/utils/binding.ts b/interface/src/utils/binding.ts index 2fcb67235..3d5058f33 100644 --- a/interface/src/utils/binding.ts +++ b/interface/src/utils/binding.ts @@ -21,3 +21,29 @@ export const updateValue = [event.target.name]: extractEventValue(event) })); }; + +export const updateValueDirty = + (origData: any, dirtyFlags: any, setDirtyFlags: any, updateEntity: UpdateEntity) => + (event: React.ChangeEvent) => { + const updated_value = extractEventValue(event); + const name = event.target.name; + updateEntity((prevState) => ({ + ...prevState, + [name]: updated_value + })); + + const arr: string[] = dirtyFlags; + + if (origData[name] !== updated_value) { + if (!arr.includes(name)) { + arr.push(name); + } + } else { + const startIndex = arr.indexOf(name); + if (startIndex !== -1) { + arr.splice(startIndex, 1); + } + } + + setDirtyFlags(arr); + }; diff --git a/interface/src/utils/useRest.ts b/interface/src/utils/useRest.ts index 8b9c6edee..09498d47c 100644 --- a/interface/src/utils/useRest.ts +++ b/interface/src/utils/useRest.ts @@ -16,16 +16,22 @@ export const useRest = ({ read, update }: RestRequestOptions) => { const { enqueueSnackbar } = useSnackbar(); - const [saving, setSaving] = useState(false); const [data, setData] = useState(); + const [saving, setSaving] = useState(false); const [errorMessage, setErrorMessage] = useState(); const [restartNeeded, setRestartNeeded] = useState(false); + const [origData, setOrigData] = useState(); + const [dirtyFlags, setDirtyFlags] = useState(); + const loadData = useCallback(async () => { setData(undefined); + setDirtyFlags([]); setErrorMessage(undefined); try { - setData((await read()).data); + const fetch_data = (await read()).data; + setData(fetch_data); + setOrigData(fetch_data); } catch (error) { const message = extractErrorMessage(error, LL.PROBLEM_LOADING()); enqueueSnackbar(message, { variant: 'error' }); @@ -66,5 +72,16 @@ export const useRest = ({ read, update }: RestRequestOptions) => { loadData(); }, [loadData]); - return { loadData, saveData, saving, setData, data, errorMessage, restartNeeded } as const; + return { + loadData, + saveData, + saving, + setData, + data, + origData, + dirtyFlags, + setDirtyFlags, + errorMessage, + restartNeeded + } as const; };