diff --git a/interface/src/project/SettingsScheduler.tsx b/interface/src/project/SettingsScheduler.tsx index 334fbeb7f..149c41949 100644 --- a/interface/src/project/SettingsScheduler.tsx +++ b/interface/src/project/SettingsScheduler.tsx @@ -1,5 +1,4 @@ import { FC, useState, useEffect, useCallback } from 'react'; - import { unstable_useBlocker as useBlocker } from 'react-router-dom'; import { @@ -27,6 +26,7 @@ import CheckIcon from '@mui/icons-material/Check'; import WarningIcon from '@mui/icons-material/Warning'; import CancelIcon from '@mui/icons-material/Cancel'; import DoneIcon from '@mui/icons-material/Done'; +import AddIcon from '@mui/icons-material/Add'; import { ValidatedTextField, @@ -41,9 +41,13 @@ import { import * as EMSESP from './api'; import { extractErrorMessage, updateValue } from 'utils'; +import { validate } from 'validators'; +import { schedulerItemValidation } from './validators'; import { ScheduleItem, ScheduleFlag } from './types'; +import Schema, { ValidateFieldsError } from 'async-validator'; + import { useI18nContext } from 'i18n/i18n-react'; export const APIURL = window.location.origin + '/api/'; @@ -56,24 +60,27 @@ const SettingsScheduler: FC = () => { const [numChanges, setNumChanges] = useState(0); const blocker = useBlocker(numChanges !== 0); - const [errorMessage, setErrorMessage] = useState(); - const emptySchedule = { - id: '', + id: '0', active: false, deleted: false, flags: 0, - time: '', + time: '12:00', cmd: '', value: '', description: '' }; const [schedule, setSchedule] = useState([emptySchedule]); const [scheduleItem, setScheduleItem] = useState(); - const [ntpAvailable, setNTPavailable] = useState(true); - const [dow, setDow] = useState([]); + const [errorMessage, setErrorMessage] = useState(); + const [creating, setCreating] = useState(false); + + const [fieldErrors, setFieldErrors] = useState(); + + // eslint-disable-next-line + const [flags, setFlags] = useState(() => ['']); function getDayNames() { const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' }); @@ -84,9 +91,6 @@ const SettingsScheduler: FC = () => { return days.map((date) => formatter.format(date)); } - // eslint-disable-next-line - const [flags, setFlags] = useState(() => ['']); - useEffect(() => { setNumChanges(getNumChanges()); }); @@ -238,15 +242,15 @@ const SettingsScheduler: FC = () => { ntp_available: ntpAvailable, schedule: schedule .filter((si) => !si.deleted) - .map((new_si) => { + .map((condensed_si) => { return { - id: new_si.id, - active: new_si.active, - flags: new_si.flags, - time: new_si.time, - cmd: new_si.cmd, - value: new_si.value, - description: new_si.description + id: condensed_si.id, // will be ignored + active: condensed_si.active, + flags: condensed_si.flags, + time: condensed_si.time, + cmd: condensed_si.cmd, + value: condensed_si.value, + description: condensed_si.description }; }) }); @@ -301,28 +305,41 @@ const SettingsScheduler: FC = () => { si.description = ''; } setScheduleItem(si); + setCreating(false); + }; + + function makeid() { + let result = ''; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + let counter = 0; + while (counter < 4) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + counter += 1; + } + return result; + } + + const addScheduleItem = () => { + setScheduleItem({ + id: makeid(), // random ID of 4 strings + active: false, + deleted: false, + flags: 0, + time: '12:00', + cmd: '', + value: '', + description: '' + }); + setCreating(true); }; const updateScheduleItem = () => { if (scheduleItem) { - setSchedule((prevState) => { - const newState = prevState.map((obj) => { - if (obj.id === scheduleItem.id) { - return { - ...obj, - active: scheduleItem.active, - time: scheduleItem.time, - cmd: scheduleItem.cmd, - value: scheduleItem.value, - description: scheduleItem.description - }; - } - return obj; - }); - return newState; - }); + const new_schedule = [...schedule.filter((si) => si.id !== scheduleItem.id), scheduleItem]; + setSchedule(new_schedule); + setScheduleItem(undefined); } - setScheduleItem(undefined); }; const renderSchedule = () => { @@ -401,12 +418,24 @@ const SettingsScheduler: FC = () => { updateScheduleItem(); }; + const validateScheduleItem = async () => { + if (scheduleItem) { + try { + setFieldErrors(undefined); + await validate(schedulerItemValidation(scheduleItem, creating), scheduleItem); + updateScheduleItem(); + } catch (errors: any) { + setFieldErrors(errors); + } + } + }; + const renderEditSchedule = () => { if (scheduleItem) { return ( setScheduleItem(undefined)}> - {LL.EDIT() + + {(creating ? LL.ADD(0) : LL.EDIT()) + ' ' + ((scheduleItem.flags & ScheduleFlag.SCHEDULE_TIMER) === ScheduleFlag.SCHEDULE_TIMER ? LL.TIMER() @@ -428,9 +457,8 @@ const SettingsScheduler: FC = () => { control={} label={LL.ACTIVE()} /> - {(scheduleItem.flags & ScheduleFlag.SCHEDULE_TIMER) === ScheduleFlag.SCHEDULE_TIMER ? ( - { {LL.SCHEDULE_TIMER_1()} {LL.SCHEDULE_TIMER_2()} {LL.SCHEDULE_TIMER_3()} - + ) : ( { onChange={updateValue(setScheduleItem)} /> )} - - { /> - - - + {!creating && ( + + + + )} @@ -534,6 +564,13 @@ const SettingsScheduler: FC = () => { )} + + + + + ); diff --git a/interface/src/project/validators.ts b/interface/src/project/validators.ts index 2e955e285..437c57066 100644 --- a/interface/src/project/validators.ts +++ b/interface/src/project/validators.ts @@ -1,6 +1,6 @@ import Schema, { InternalRuleItem } from 'async-validator'; import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared'; -import { Settings } from './types'; +import { Settings, ScheduleItem } from './types'; export const GPIO_VALIDATOR = { validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) { @@ -84,3 +84,8 @@ export const createSettingsValidator = (settings: Settings) => shower_alert_coldshot: [{ type: 'number', min: 1, max: 10, message: 'Time must be between 1 and 10 seconds' }] }) }); + +export const schedulerItemValidation = (si: ScheduleItem, creating: boolean) => + new Schema({ + cmd: [{ required: true, message: 'Command is required' }] + });