refactor diallogs

This commit is contained in:
Proddy
2023-04-22 09:44:24 +02:00
parent 04dd9eef09
commit f80764d72b
6 changed files with 462 additions and 491 deletions

View File

@@ -1,36 +1,30 @@
import type { FC } from 'react'; import AddIcon from '@mui/icons-material/Add';
import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning';
import { Button, Typography, Box } from '@mui/material';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { unstable_useBlocker as useBlocker } from 'react-router-dom'; import { unstable_useBlocker as useBlocker } from 'react-router-dom';
import { Button, Typography, Box } from '@mui/material';
import { useTheme } from '@table-library/react-table-library/theme';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import WarningIcon from '@mui/icons-material/Warning'; import SettingsEntitiesDialog from './SettingsEntitiesDialog';
import CancelIcon from '@mui/icons-material/Cancel'; import * as EMSESP from './api';
import AddIcon from '@mui/icons-material/Add'; import { DeviceValueUOM_s } from './types';
import { entityItemValidation } from './validators';
import type { EntityItem } from './types';
import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components'; import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
import SettingsEntitiesDialog from './SettingsEntitiesDialog';
import type { EntityItem } from './types';
import { DeviceValueUOM_s } from './types';
import { extractErrorMessage } from 'utils';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
import * as EMSESP from './api';
import { entityItemValidation } from './validators';
const SettingsEntities: FC = () => { const SettingsEntities: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0); const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0); const blocker = useBlocker(numChanges !== 0);
const [entities, setEntities] = useState<EntityItem[]>([]); const [entities, setEntities] = useState<EntityItem[]>();
const [selectedEntityItem, setSelectedEntityItem] = useState<EntityItem>(); const [selectedEntityItem, setSelectedEntityItem] = useState<EntityItem>();
const [errorMessage, setErrorMessage] = useState<string>(); const [errorMessage, setErrorMessage] = useState<string>();
const [creating, setCreating] = useState<boolean>(false); const [creating, setCreating] = useState<boolean>(false);
@@ -52,7 +46,9 @@ const SettingsEntities: FC = () => {
} }
useEffect(() => { useEffect(() => {
setNumChanges(entities ? entities.filter((ei) => hasEntityChanged(ei)).length : 0); if (entities) {
setNumChanges(entities ? entities.filter((ei) => hasEntityChanged(ei)).length : 0);
}
}, [entities]); }, [entities]);
const entity_theme = useTheme({ const entity_theme = useTheme({
@@ -109,28 +105,24 @@ const SettingsEntities: FC = () => {
` `
}); });
const setOriginalEntity = (data: EntityItem[]) => {
setEntities(
data.map((ei) => ({
...ei,
o_id: ei.id,
o_device_id: ei.device_id,
o_type_id: ei.type_id,
o_offset: ei.offset,
o_factor: ei.factor,
o_uom: ei.uom,
o_value_type: ei.value_type,
o_name: ei.name,
o_writeable: ei.writeable,
o_deleted: ei.deleted
}))
);
};
const fetchEntities = useCallback(async () => { const fetchEntities = useCallback(async () => {
try { try {
const response = await EMSESP.readEntities(); const response = await EMSESP.readEntities();
setOriginalEntity(response.data.entities); setEntities(
response.data.entities.map((ei) => ({
...ei,
o_id: ei.id,
o_device_id: ei.device_id,
o_type_id: ei.type_id,
o_offset: ei.offset,
o_factor: ei.factor,
o_uom: ei.uom,
o_value_type: ei.value_type,
o_name: ei.name,
o_writeable: ei.writeable,
o_deleted: ei.deleted
}))
);
} catch (error) { } catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING())); setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
} }
@@ -160,14 +152,14 @@ const SettingsEntities: FC = () => {
}); });
if (response.status === 200) { if (response.status === 200) {
toast.success(LL.SUCCESS()); toast.success(LL.ENTITIES_UPDATED());
} else { } else {
toast.error(LL.PROBLEM_UPDATING()); toast.error(LL.PROBLEM_UPDATING());
} }
void fetchEntities();
} catch (error) { } catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} }
setOriginalEntity(entities);
} }
}; };
@@ -182,26 +174,25 @@ const SettingsEntities: FC = () => {
}; };
const onDialogSave = (updatedItem: EntityItem) => { const onDialogSave = (updatedItem: EntityItem) => {
if (creating) { setDialogOpen(false);
if (entities && creating) {
setEntities([...entities.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem]); setEntities([...entities.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem]);
} else { } else {
setEntities(entities.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei))); setEntities(entities?.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei)));
} }
setDialogOpen(false);
}; };
// TODO need callback here too?
const addEntityItem = () => { const addEntityItem = () => {
setCreating(true); setCreating(true);
setSelectedEntityItem({ setSelectedEntityItem({
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100), id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
name: '',
device_id: 11, device_id: 11,
type_id: 0, type_id: 0,
offset: 0, offset: 0,
factor: 1, factor: 1,
value_type: 2,
uom: 0, uom: 0,
name: '', value_type: 2,
writeable: false, writeable: false,
deleted: false deleted: false
}); });
@@ -234,8 +225,8 @@ const SettingsEntities: FC = () => {
<Header> <Header>
<HeaderRow> <HeaderRow>
<HeaderCell>{LL.NAME(0)}</HeaderCell> <HeaderCell>{LL.NAME(0)}</HeaderCell>
<HeaderCell stiff>Device ID</HeaderCell> <HeaderCell stiff>{LL.ID_OF(LL.DEVICE())}</HeaderCell>
<HeaderCell stiff>Type ID</HeaderCell> <HeaderCell stiff>{LL.ID_OF(LL.TYPE(1))}</HeaderCell>
<HeaderCell stiff>Offset</HeaderCell> <HeaderCell stiff>Offset</HeaderCell>
<HeaderCell stiff>{LL.VALUE(0)}</HeaderCell> <HeaderCell stiff>{LL.VALUE(0)}</HeaderCell>
</HeaderRow> </HeaderRow>
@@ -244,8 +235,8 @@ const SettingsEntities: FC = () => {
{tableList.map((ei: EntityItem) => ( {tableList.map((ei: EntityItem) => (
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}> <Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
<Cell>{ei.name}</Cell> <Cell>{ei.name}</Cell>
<Cell>{showHex(ei.device_id, 2)}</Cell> <Cell>{showHex(ei.device_id as number, 2)}</Cell>
<Cell>{showHex(ei.type_id, 4)}</Cell> <Cell>{showHex(ei.type_id as number, 4)}</Cell>
<Cell>{ei.offset}</Cell> <Cell>{ei.offset}</Cell>
<Cell>{formatValue(ei.value, ei.uom)}</Cell> <Cell>{formatValue(ei.value, ei.uom)}</Cell>
</Row> </Row>
@@ -258,11 +249,12 @@ const SettingsEntities: FC = () => {
}; };
return ( return (
<SectionContent title={LL.CUSTOM_ENTITIES()} titleGutter> <SectionContent title={LL.CUSTOM_ENTITIES(0)} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
<Box mb={2} color="warning.main"> <Box mb={2} color="warning.main">
<Typography variant="body2">{LL.ENTITIES_HELP_1()}</Typography> <Typography variant="body2">{LL.ENTITIES_HELP_1()}</Typography>
</Box> </Box>
{renderEntity()} {renderEntity()}
{selectedEntityItem && ( {selectedEntityItem && (
@@ -272,7 +264,7 @@ const SettingsEntities: FC = () => {
onClose={onDialogClose} onClose={onDialogClose}
onSave={onDialogSave} onSave={onDialogSave}
selectedEntityItem={selectedEntityItem} selectedEntityItem={selectedEntityItem}
validator={entityItemValidation(entities, creating)} validator={entityItemValidation()}
/> />
)} )}

View File

@@ -1,35 +1,32 @@
import { useState, useEffect } from 'react'; import AddIcon from '@mui/icons-material/Add';
import CancelIcon from '@mui/icons-material/Cancel';
import DoneIcon from '@mui/icons-material/Done';
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
import { import {
Grid,
Button,
Box, Box,
Button,
Checkbox,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Checkbox, Grid,
InputAdornment, InputAdornment,
MenuItem MenuItem
} from '@mui/material'; } from '@mui/material';
import { useEffect, useState } from 'react';
import { ValidatedTextField, BlockFormControlLabel } from 'components'; import { DeviceValueUOM_s } from './types';
import type { EntityItem } from './types';
import type Schema from 'async-validator';
import type { ValidateFieldsError } from 'async-validator';
import DoneIcon from '@mui/icons-material/Done'; import { BlockFormControlLabel, ValidatedTextField } from 'components';
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
import CancelIcon from '@mui/icons-material/Cancel';
import AddIcon from '@mui/icons-material/Add';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { validate } from 'validators';
import type { ValidateFieldsError } from 'async-validator';
import type Schema from 'async-validator';
import { updateValue } from 'utils'; import { updateValue } from 'utils';
import { validate } from 'validators';
import type { EntityItem } from './types';
import { DeviceValueUOM_s } from './types';
type SettingsEntitiesDialogProps = { type SettingsEntitiesDialogProps = {
open: boolean; open: boolean;
@@ -58,6 +55,12 @@ const SettingsEntitiesDialog = ({
if (open) { if (open) {
setFieldErrors(undefined); setFieldErrors(undefined);
setEditItem(selectedEntityItem); setEditItem(selectedEntityItem);
// convert to hex strings straight away
setEditItem({
...selectedEntityItem,
device_id: ('0' + selectedEntityItem.device_id.toString(16).toUpperCase()).slice(-2),
type_id: ('000' + selectedEntityItem.type_id.toString(16).toUpperCase()).slice(-4)
});
} }
}, [open, selectedEntityItem]); }, [open, selectedEntityItem]);
@@ -69,6 +72,12 @@ const SettingsEntitiesDialog = ({
try { try {
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(validator, editItem); await validate(validator, editItem);
if (typeof editItem.device_id === 'string') {
editItem.device_id = parseInt(editItem.device_id, 16);
}
if (typeof editItem.type_id === 'string') {
editItem.type_id = parseInt(editItem.type_id, 16);
}
onSave(editItem); onSave(editItem);
} catch (errors: any) { } catch (errors: any) {
setFieldErrors(errors); setFieldErrors(errors);
@@ -83,13 +92,12 @@ const SettingsEntitiesDialog = ({
return ( return (
<Dialog open={open} onClose={close}> <Dialog open={open} onClose={close}>
<DialogTitle> <DialogTitle>
{creating ? LL.ADD(1) + ' ' + LL.NEW() : LL.EDIT()}&nbsp;{LL.ENTITY()} {creating ? LL.ADD(1) + ' ' + LL.NEW(1) : LL.EDIT()}&nbsp;{LL.ENTITY()}
</DialogTitle> </DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Box display="flex" flexWrap="wrap" mb={1}> <Box display="flex" flexWrap="wrap" mb={1}>
<Box flexWrap="nowrap" whiteSpace="nowrap" /> <Box flexWrap="nowrap" whiteSpace="nowrap" />
</Box> </Box>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={8}> <Grid item xs={8}>
<ValidatedTextField <ValidatedTextField
@@ -102,10 +110,9 @@ const SettingsEntitiesDialog = ({
onChange={updateFormValue} onChange={updateFormValue}
/> />
</Grid> </Grid>
<Grid item xs={4} mt={3}> <Grid item xs={4} mt={3}>
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox checked={selectedEntityItem.writeable} onChange={updateFormValue} name="write" />} control={<Checkbox checked={editItem.writeable} onChange={updateFormValue} name="writeable" />}
label={LL.WRITEABLE()} label={LL.WRITEABLE()}
/> />
</Grid> </Grid>
@@ -113,28 +120,26 @@ const SettingsEntitiesDialog = ({
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors}
name="device_id" name="device_id"
label="Device ID" label={LL.ID_OF(LL.DEVICE())}
margin="normal" margin="normal"
fullWidth fullWidth
value={editItem.device_id} value={editItem.device_id}
onChange={updateFormValue} onChange={updateFormValue}
InputProps={{ inputProps={{ style: { textTransform: 'uppercase' } }}
startAdornment: <InputAdornment position="start">0x</InputAdornment> InputProps={{ startAdornment: <InputAdornment position="start">0x</InputAdornment> }}
}}
/> />
</Grid> </Grid>
<Grid item xs={4}> <Grid item xs={4}>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors}
name="type_id" name="type_id"
label="Type ID" label={LL.ID_OF(LL.TYPE(1))}
margin="normal" margin="normal"
fullWidth fullWidth
value={editItem.type_id} value={editItem.type_id}
onChange={updateFormValue} onChange={updateFormValue}
InputProps={{ inputProps={{ style: { textTransform: 'uppercase' } }}
startAdornment: <InputAdornment position="start">0x</InputAdornment> InputProps={{ startAdornment: <InputAdornment position="start">0x</InputAdornment> }}
}}
/> />
</Grid> </Grid>
<Grid item xs={4}> <Grid item xs={4}>
@@ -169,7 +174,8 @@ const SettingsEntitiesDialog = ({
<MenuItem value={6}>TIME</MenuItem> <MenuItem value={6}>TIME</MenuItem>
</ValidatedTextField> </ValidatedTextField>
</Grid> </Grid>
{selectedEntityItem.value_type !== 0 && (
{editItem.value_type !== 0 && (
<> <>
<Grid item xs={4}> <Grid item xs={4}>
<ValidatedTextField <ValidatedTextField

View File

@@ -1,98 +1,36 @@
import type { FC } from 'react'; import AddIcon from '@mui/icons-material/Add';
import CancelIcon from '@mui/icons-material/Cancel';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import WarningIcon from '@mui/icons-material/Warning';
import { Box, Typography, Divider, Stack, Button } from '@mui/material';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { unstable_useBlocker as useBlocker } from 'react-router-dom'; import { unstable_useBlocker as useBlocker } from 'react-router-dom';
import {
Button,
Typography,
Box,
Stack,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
ToggleButton,
ToggleButtonGroup,
Checkbox,
Grid,
TextField,
Divider
} from '@mui/material';
import { useTheme } from '@table-library/react-table-library/theme';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import SettingsSchedulerDialog from './SettingsSchedulerDialog';
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline'; import * as EMSESP from './api';
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 CheckCircleIcon from '@mui/icons-material/CheckCircle';
import {
ValidatedTextField,
ButtonRow,
FormLoader,
BlockFormControlLabel,
SectionContent,
BlockNavigation
} from 'components';
import { extractErrorMessage, updateValue } from 'utils';
import { validate } from 'validators';
import { schedulerItemValidation } from './validators';
import type { ValidateFieldsError } from 'async-validator';
import type { ScheduleItem } from './types';
import { ScheduleFlag } from './types'; import { ScheduleFlag } from './types';
import { schedulerItemValidation } from './validators';
import type { ScheduleItem } from './types';
import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
import * as EMSESP from './api';
function makeid() {
return Math.floor(Math.random() * (Math.floor(200) - 100) + 100);
}
const SettingsScheduler: FC = () => { const SettingsScheduler: FC = () => {
const { LL, locale } = useI18nContext(); const { LL, locale } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0); const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0); const blocker = useBlocker(numChanges !== 0);
const [schedule, setSchedule] = useState<ScheduleItem[]>([]);
const emptySchedule = { const [selectedScheduleItem, setSelectedScheduleItem] = useState<ScheduleItem>();
id: 0,
active: false,
deleted: false,
flags: 0,
time: '12:00',
cmd: '',
value: '',
name: '',
o_name: ''
};
const [schedule, setSchedule] = useState<ScheduleItem[]>([emptySchedule]);
const [scheduleItem, setScheduleItem] = useState<ScheduleItem>();
const [dow, setDow] = useState<string[]>([]); const [dow, setDow] = useState<string[]>([]);
const [errorMessage, setErrorMessage] = useState<string>(); const [errorMessage, setErrorMessage] = useState<string>();
const [creating, setCreating] = useState<boolean>(false); const [creating, setCreating] = useState<boolean>(false);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [dialogOpen, setDialogOpen] = useState<boolean>(false);
// eslint-disable-next-line
const [flags, setFlags] = useState(() => ['']);
function getDayNames() {
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
const dd = day < 10 ? `0${day}` : day;
return new Date(`2017-01-${dd}T00:00:00+00:00`);
});
return days.map((date) => formatter.format(date));
}
function hasScheduleChanged(si: ScheduleItem) { function hasScheduleChanged(si: ScheduleItem) {
return ( return (
@@ -107,17 +45,11 @@ const SettingsScheduler: FC = () => {
); );
} }
// TODO fix
const getNumChanges = () => {
if (!schedule) {
return 0;
}
return schedule.filter((si) => hasScheduleChanged(si)).length;
};
useEffect(() => { useEffect(() => {
setNumChanges(getNumChanges()); if (schedule) {
}); setNumChanges(schedule ? schedule.filter((si) => hasScheduleChanged(si)).length : 0);
}
}, [schedule]);
const schedule_theme = useTheme({ const schedule_theme = useTheme({
Table: ` Table: `
@@ -164,72 +96,37 @@ const SettingsScheduler: FC = () => {
` `
}); });
const setOriginalSchedule = (data: ScheduleItem[]) => {
setSchedule(
data.map((si) => ({
...si,
o_id: si.id,
o_active: si.active,
o_deleted: si.deleted,
o_flags: si.flags,
o_time: si.time,
o_cmd: si.cmd,
o_value: si.value,
o_name: si.name
}))
);
};
const fetchSchedule = useCallback(async () => { const fetchSchedule = useCallback(async () => {
try { try {
const response = await EMSESP.readSchedule(); const response = await EMSESP.readSchedule();
setOriginalSchedule(response.data.schedule); setSchedule(
response.data.schedule.map((si) => ({
...si,
o_id: si.id,
o_active: si.active,
o_deleted: si.deleted,
o_flags: si.flags,
o_time: si.time,
o_cmd: si.cmd,
o_value: si.value,
o_name: si.name
}))
);
} catch (error) { } catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING())); setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
} }
}, [LL]); }, [LL]);
// on mount
useEffect(() => { useEffect(() => {
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
const dd = day < 10 ? `0${day}` : day;
return new Date(`2017-01-${dd}T00:00:00+00:00`);
});
setDow(days.map((date) => formatter.format(date)));
void fetchSchedule(); void fetchSchedule();
setDow(getDayNames()); }, [locale, fetchSchedule]);
}, [getDayNames, fetchSchedule]);
const getFlagNumber = (newFlag: string[]) => {
let new_flag = 0;
for (const entry of newFlag) {
new_flag |= Number(entry);
}
return new_flag;
};
const getFlagString = (f: number) => {
const new_flags: string[] = [];
if ((f & 1) === 1) {
new_flags.push('1');
}
if ((f & 2) === 2) {
new_flags.push('2');
}
if ((f & 4) === 4) {
new_flags.push('4');
}
if ((f & 8) === 8) {
new_flags.push('8');
}
if ((f & 16) === 16) {
new_flags.push('16');
}
if ((f & 32) === 32) {
new_flags.push('32');
}
if ((f & 64) === 64) {
new_flags.push('64');
}
if ((f & 128) === 128) {
new_flags.push('128');
}
return new_flags;
};
const saveSchedule = async () => { const saveSchedule = async () => {
if (schedule) { if (schedule) {
@@ -248,74 +145,40 @@ const SettingsScheduler: FC = () => {
})) }))
}); });
if (response.status === 200) { if (response.status === 200) {
toast.success(LL.SCHEDULE_SAVED()); toast.success(LL.SCHEDULE_UPDATED());
} else { } else {
toast.error(LL.PROBLEM_UPDATING()); toast.error(LL.PROBLEM_UPDATING());
} }
void fetchSchedule();
} catch (error) { } catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} }
setOriginalSchedule(schedule);
} }
}; };
function getFlagName(flag: number) { const editScheduleItem = useCallback((si: ScheduleItem) => {
if ((flag & ScheduleFlag.SCHEDULE_MON) === ScheduleFlag.SCHEDULE_MON) {
return dow[1];
}
if ((flag & ScheduleFlag.SCHEDULE_TUE) === ScheduleFlag.SCHEDULE_TUE) {
return dow[2];
}
if ((flag & ScheduleFlag.SCHEDULE_WED) === ScheduleFlag.SCHEDULE_WED) {
return dow[3];
}
if ((flag & ScheduleFlag.SCHEDULE_THU) === ScheduleFlag.SCHEDULE_THU) {
return dow[4];
}
if ((flag & ScheduleFlag.SCHEDULE_FRI) === ScheduleFlag.SCHEDULE_FRI) {
return dow[5];
}
if ((flag & ScheduleFlag.SCHEDULE_SAT) === ScheduleFlag.SCHEDULE_SAT) {
return dow[6];
}
if ((flag & ScheduleFlag.SCHEDULE_SUN) === ScheduleFlag.SCHEDULE_SUN) {
return dow[0];
}
if ((flag & ScheduleFlag.SCHEDULE_TIMER) === ScheduleFlag.SCHEDULE_TIMER) {
return LL.TIMER(0);
}
return '';
}
const dayBox = (si: ScheduleItem, flag: number) => (
<>
<Box>
<Typography sx={{ fontSize: 11 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}>
{getFlagName(flag)}
</Typography>
</Box>
<Divider orientation="vertical" flexItem />
</>
);
const showFlag = (si: ScheduleItem, flag: number) => (
<Typography variant="button" sx={{ fontSize: 10 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}>
{getFlagName(flag)}
</Typography>
);
const editScheduleItem = (si: ScheduleItem) => {
if (si.name === undefined) {
si.name = '';
}
setCreating(false); setCreating(false);
setScheduleItem(si); setSelectedScheduleItem(si);
setDialogOpen(true);
}, []);
const onDialogClose = () => {
setDialogOpen(false);
};
const onDialogSave = (updatedItem: ScheduleItem) => {
setDialogOpen(false);
if (schedule && creating) {
setSchedule([...schedule.filter((si) => creating || si.o_id !== updatedItem.o_id), updatedItem]);
} else {
setSchedule(schedule?.map((si) => (si.id === updatedItem.id ? { ...si, ...updatedItem } : si)));
}
}; };
const addScheduleItem = () => { const addScheduleItem = () => {
setCreating(true); setCreating(true);
setScheduleItem({ setSelectedScheduleItem({
id: makeid(), id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
active: false, active: false,
deleted: false, deleted: false,
flags: 0, flags: 0,
@@ -324,13 +187,7 @@ const SettingsScheduler: FC = () => {
value: '', value: '',
name: '' name: ''
}); });
}; setDialogOpen(true);
const updateScheduleItem = () => {
if (scheduleItem) {
setSchedule([...schedule.filter((si) => creating || si.o_id !== scheduleItem.o_id), scheduleItem]);
}
setScheduleItem(undefined);
}; };
const renderSchedule = () => { const renderSchedule = () => {
@@ -338,6 +195,17 @@ const SettingsScheduler: FC = () => {
return <FormLoader errorMessage={errorMessage} />; return <FormLoader errorMessage={errorMessage} />;
} }
const dayBox = (si: ScheduleItem, flag: number) => (
<>
<Box>
<Typography sx={{ fontSize: 11 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}>
{flag === ScheduleFlag.SCHEDULE_TIMER ? LL.TIMER(0) : dow[Math.log(flag) / Math.log(2)]}
</Typography>
</Box>
<Divider orientation="vertical" flexItem />
</>
);
return ( return (
<Table <Table
data={{ nodes: schedule.filter((si) => !si.deleted).sort((a, b) => a.time.localeCompare(b.time)) }} data={{ nodes: schedule.filter((si) => !si.deleted).sort((a, b) => a.time.localeCompare(b.time)) }}
@@ -388,173 +256,6 @@ const SettingsScheduler: FC = () => {
); );
}; };
const removeScheduleItem = (si: ScheduleItem) => {
si.deleted = true;
setScheduleItem(si);
updateScheduleItem();
};
const validateScheduleItem = async () => {
if (scheduleItem) {
try {
setFieldErrors(undefined);
await validate(schedulerItemValidation(schedule, scheduleItem), scheduleItem);
updateScheduleItem();
} catch (errors: any) {
setFieldErrors(errors);
}
}
};
const closeDialog = () => {
setScheduleItem(undefined);
setFieldErrors(undefined);
};
const renderEditSchedule = () => {
if (scheduleItem) {
const isTimer = scheduleItem.flags === ScheduleFlag.SCHEDULE_TIMER;
return (
<Dialog open={!!scheduleItem} onClose={() => closeDialog()}>
<DialogTitle>
{creating ? LL.ADD(1) + ' ' + LL.NEW() : LL.EDIT()}&nbsp;{LL.SCHEDULE(1)}
</DialogTitle>
<DialogContent dividers>
<Box display="flex" flexWrap="wrap" mb={1}>
<Box flexGrow={1}>
<ToggleButtonGroup
size="small"
color="secondary"
value={getFlagString(scheduleItem.flags)}
onChange={(event, flag) => {
scheduleItem.flags = getFlagNumber(flag) & 127;
setFlags(['']); // forces refresh
}}
>
<ToggleButton value="2">{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_MON)}</ToggleButton>
<ToggleButton value="4">{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_TUE)}</ToggleButton>
<ToggleButton value="8">{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_WED)}</ToggleButton>
<ToggleButton value="16">{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_THU)}</ToggleButton>
<ToggleButton value="32">{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_FRI)}</ToggleButton>
<ToggleButton value="64">{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_SAT)}</ToggleButton>
<ToggleButton value="1">{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_SUN)}</ToggleButton>
</ToggleButtonGroup>
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
{isTimer ? (
<Button
size="large"
sx={{ bgcolor: '#334f65' }}
variant="contained"
onClick={() => {
scheduleItem.flags = 0;
setFlags(['']); // forces refresh
}}
>
{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_TIMER)}
</Button>
) : (
<Button
size="large"
variant="outlined"
onClick={() => {
scheduleItem.flags = ScheduleFlag.SCHEDULE_TIMER;
setFlags(['']); // forces refresh
}}
>
{showFlag(scheduleItem, ScheduleFlag.SCHEDULE_TIMER)}
</Button>
)}
</Box>
</Box>
<Grid container>
<BlockFormControlLabel
control={
<Checkbox checked={scheduleItem.active} onChange={updateValue(setScheduleItem)} name="active" />
}
label={LL.ACTIVE()}
/>
{scheduleItem.active && (
<Grid item sx={{ mt: 1 }}>
<CheckCircleIcon sx={{ color: '#79D200', fontSize: 16, verticalAlign: 'middle' }} />
</Grid>
)}
</Grid>
<Grid container>
<TextField
name="time"
type="time"
label={isTimer ? LL.TIMER(1) : LL.TIME(1)}
value={scheduleItem.time}
margin="normal"
onChange={updateValue(setScheduleItem)}
/>
{isTimer && (
<Box color="warning.main" ml={2} mt={4}>
<Typography variant="body2">{LL.SCHEDULER_HELP_2()}</Typography>
</Box>
)}
</Grid>
<ValidatedTextField
fieldErrors={fieldErrors}
name="cmd"
label={LL.COMMAND(0)}
fullWidth
value={scheduleItem.cmd}
margin="normal"
onChange={updateValue(setScheduleItem)}
/>
<TextField
name="value"
label={LL.VALUE(0)}
multiline
margin="normal"
fullWidth
value={scheduleItem.value}
onChange={updateValue(setScheduleItem)}
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="name"
label={LL.NAME(0)}
value={scheduleItem.name}
fullWidth
margin="normal"
onChange={updateValue(setScheduleItem)}
/>
</DialogContent>
<DialogActions>
{!creating && (
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
<Button
startIcon={<RemoveIcon />}
variant="outlined"
color="error"
onClick={() => removeScheduleItem(scheduleItem)}
>
{LL.REMOVE()}
</Button>
</Box>
)}
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => closeDialog()} color="secondary">
{LL.CANCEL()}
</Button>
<Button
startIcon={creating ? <AddIcon /> : <DoneIcon />}
variant="outlined"
type="submit"
onClick={() => validateScheduleItem()}
color="primary"
>
{creating ? LL.ADD(0) : LL.UPDATE()}
</Button>
</DialogActions>
</Dialog>
);
}
};
return ( return (
<SectionContent title={LL.SCHEDULER()} titleGutter> <SectionContent title={LL.SCHEDULER()} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
@@ -562,7 +263,19 @@ const SettingsScheduler: FC = () => {
<Typography variant="body2">{LL.SCHEDULER_HELP_1()}</Typography> <Typography variant="body2">{LL.SCHEDULER_HELP_1()}</Typography>
</Box> </Box>
{renderSchedule()} {renderSchedule()}
{renderEditSchedule()}
{selectedScheduleItem && (
<SettingsSchedulerDialog
open={dialogOpen}
creating={creating}
onClose={onDialogClose}
onSave={onDialogSave}
selectedSchedulerItem={selectedScheduleItem}
validator={schedulerItemValidation()}
dow={dow}
/>
)}
<Box display="flex" flexWrap="wrap"> <Box display="flex" flexWrap="wrap">
<Box flexGrow={1}> <Box flexGrow={1}>
{numChanges !== 0 && ( {numChanges !== 0 && (

View File

@@ -0,0 +1,254 @@
import AddIcon from '@mui/icons-material/Add';
import CancelIcon from '@mui/icons-material/Cancel';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import DoneIcon from '@mui/icons-material/Done';
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
import {
Box,
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Grid,
TextField,
ToggleButton,
ToggleButtonGroup,
Typography
} from '@mui/material';
import { useEffect, useState } from 'react';
import { ScheduleFlag } from './types';
import type { ScheduleItem } from './types';
import type Schema from 'async-validator';
import type { ValidateFieldsError } from 'async-validator';
import { BlockFormControlLabel, ValidatedTextField } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { updateValue } from 'utils';
import { validate } from 'validators';
type SettingsSchedulerDialogProps = {
open: boolean;
creating: boolean;
onClose: () => void;
onSave: (ei: ScheduleItem) => void;
selectedSchedulerItem: ScheduleItem;
validator: Schema;
dow: string[];
};
const SettingsSchedulerDialog = ({
open,
creating,
onClose,
onSave,
selectedSchedulerItem,
validator,
dow
}: SettingsSchedulerDialogProps) => {
const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<ScheduleItem>(selectedSchedulerItem);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = updateValue(setEditItem);
useEffect(() => {
if (open) {
setFieldErrors(undefined);
setEditItem(selectedSchedulerItem);
}
}, [open, selectedSchedulerItem]);
const close = () => {
onClose();
};
const save = async () => {
try {
setFieldErrors(undefined);
await validate(validator, editItem);
onSave(editItem);
} catch (errors: any) {
setFieldErrors(errors);
}
};
const remove = () => {
editItem.deleted = true;
onSave(editItem);
};
const getFlagNumber = (newFlag: string[]) => {
let new_flag = 0;
for (const entry of newFlag) {
new_flag |= Number(entry);
}
return new_flag;
};
const getFlagString = (f: number) => {
const new_flags: string[] = [];
if ((f & 1) === 1) {
new_flags.push('1');
}
if ((f & 2) === 2) {
new_flags.push('2');
}
if ((f & 4) === 4) {
new_flags.push('4');
}
if ((f & 8) === 8) {
new_flags.push('8');
}
if ((f & 16) === 16) {
new_flags.push('16');
}
if ((f & 32) === 32) {
new_flags.push('32');
}
if ((f & 64) === 64) {
new_flags.push('64');
}
if ((f & 128) === 128) {
new_flags.push('128');
}
return new_flags;
};
const showFlag = (si: ScheduleItem, flag: number) => (
<Typography variant="button" sx={{ fontSize: 10 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}>
{flag === ScheduleFlag.SCHEDULE_TIMER ? LL.TIMER(0) : dow[Math.log(flag) / Math.log(2)]}
</Typography>
);
const isTimer = editItem.flags === ScheduleFlag.SCHEDULE_TIMER;
return (
<Dialog open={open} onClose={close}>
<DialogTitle>
{creating ? LL.ADD(1) + ' ' + LL.NEW(1) : LL.EDIT()}&nbsp;{LL.ENTITY()}
</DialogTitle>
<DialogContent dividers>
<Box display="flex" flexWrap="wrap" mb={1}>
<Box flexGrow={1}>
<ToggleButtonGroup
size="small"
color="secondary"
value={getFlagString(editItem.flags)}
onChange={(event, flag) => {
setEditItem({ ...editItem, flags: getFlagNumber(flag) & 127 });
}}
>
<ToggleButton value="2">{showFlag(editItem, ScheduleFlag.SCHEDULE_MON)}</ToggleButton>
<ToggleButton value="4">{showFlag(editItem, ScheduleFlag.SCHEDULE_TUE)}</ToggleButton>
<ToggleButton value="8">{showFlag(editItem, ScheduleFlag.SCHEDULE_WED)}</ToggleButton>
<ToggleButton value="16">{showFlag(editItem, ScheduleFlag.SCHEDULE_THU)}</ToggleButton>
<ToggleButton value="32">{showFlag(editItem, ScheduleFlag.SCHEDULE_FRI)}</ToggleButton>
<ToggleButton value="64">{showFlag(editItem, ScheduleFlag.SCHEDULE_SAT)}</ToggleButton>
<ToggleButton value="1">{showFlag(editItem, ScheduleFlag.SCHEDULE_SUN)}</ToggleButton>
</ToggleButtonGroup>
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
{isTimer ? (
<Button
size="large"
sx={{ bgcolor: '#334f65' }}
variant="contained"
onClick={() => {
setEditItem({ ...editItem, flags: 0 });
}}
>
{showFlag(editItem, ScheduleFlag.SCHEDULE_TIMER)}
</Button>
) : (
<Button
size="large"
variant="outlined"
onClick={() => {
setEditItem({ ...editItem, flags: ScheduleFlag.SCHEDULE_TIMER });
}}
>
{showFlag(editItem, ScheduleFlag.SCHEDULE_TIMER)}
</Button>
)}
</Box>
</Box>
<Grid container>
<BlockFormControlLabel
control={<Checkbox checked={editItem.active} onChange={updateFormValue} name="active" />}
label={LL.ACTIVE()}
/>
{editItem.active && (
<Grid item sx={{ mt: 1 }}>
<CheckCircleIcon sx={{ color: '#79D200', fontSize: 16, verticalAlign: 'middle' }} />
</Grid>
)}
</Grid>
<Grid container>
<TextField
name="time"
type="time"
label={isTimer ? LL.TIMER(1) : LL.TIME(1)}
value={editItem.time}
margin="normal"
onChange={updateFormValue}
/>
{isTimer && (
<Box color="warning.main" ml={2} mt={4}>
<Typography variant="body2">{LL.SCHEDULER_HELP_2()}</Typography>
</Box>
)}
</Grid>
<ValidatedTextField
fieldErrors={fieldErrors}
name="cmd"
label={LL.COMMAND(0)}
fullWidth
value={editItem.cmd}
margin="normal"
onChange={updateFormValue}
/>
<TextField
name="value"
label={LL.VALUE(0)}
multiline
margin="normal"
fullWidth
value={editItem.value}
onChange={updateFormValue}
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="name"
label={LL.NAME(0)}
value={editItem.name}
fullWidth
margin="normal"
onChange={updateFormValue}
/>
</DialogContent>
<DialogActions>
{!creating && (
<Box flexGrow={1}>
<Button startIcon={<RemoveIcon />} variant="outlined" color="warning" onClick={remove}>
{LL.REMOVE()}
</Button>
</Box>
)}
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
{LL.CANCEL()}
</Button>
<Button startIcon={creating ? <AddIcon /> : <DoneIcon />} variant="outlined" onClick={save} color="primary">
{creating ? LL.ADD(0) : LL.UPDATE()}
</Button>
</DialogActions>
</Dialog>
);
};
export default SettingsSchedulerDialog;

View File

@@ -60,7 +60,7 @@ export interface Status {
} }
export interface Device { export interface Device {
id: string; // id index id: number; // id index
tn: string; // device type translated name tn: string; // device type translated name
t: number; // device type id t: number; // device type id
b: string; // brand b: string; // brand
@@ -341,19 +341,19 @@ export enum ScheduleFlag {
export interface EntityItem { export interface EntityItem {
id: number; // unique number id: number; // unique number
name: string; name: string;
device_id: number; device_id: number | string;
type_id: number; type_id: number | string;
offset: number; offset: number;
factor: number; factor: number;
uom: number; uom: number;
value_type: number; value_type: number;
value?: number; value?: number; // optional
writeable: boolean; writeable: boolean;
deleted?: boolean; // optional deleted?: boolean; // optional
o_id?: number; o_id?: number;
o_name?: string; o_name?: string;
o_device_id?: number; o_device_id?: number | string;
o_type_id?: number; o_type_id?: number | string;
o_offset?: number; o_offset?: number;
o_factor?: number; o_factor?: number;
o_uom?: number; o_uom?: number;

View File

@@ -1,7 +1,7 @@
import type { InternalRuleItem } from 'async-validator';
import Schema from 'async-validator'; import Schema from 'async-validator';
import type { Settings } from './types';
import type { InternalRuleItem } from 'async-validator';
import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared'; import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared';
import type { Settings, ScheduleItem, EntityItem } from './types';
export const GPIO_VALIDATOR = { export const GPIO_VALIDATOR = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) { validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
@@ -86,25 +86,14 @@ export const createSettingsValidator = (settings: Settings) =>
}) })
}); });
export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({ export const schedulerItemValidation = () =>
validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) {
if (name && o_name && o_name !== name && schedule.find((si) => si.name === name)) {
callback('Name already in use');
} else {
callback();
}
}
});
export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem: ScheduleItem) =>
new Schema({ new Schema({
name: [ name: [
{ {
type: 'string', type: 'string',
pattern: /^[a-zA-Z0-9_\\.]{0,15}$/, pattern: /^[a-zA-Z0-9_\\.]{0,15}$/,
message: "Must be <15 characters: alpha numeric, '_' or '.'" message: "Must be <15 characters: alpha numeric, '_' or '.'"
}, }
...[uniqueNameValidator(schedule, scheduleItem.o_name)]
], ],
cmd: [ cmd: [
{ required: true, message: 'Command is required' }, { required: true, message: 'Command is required' },
@@ -112,7 +101,7 @@ export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem:
] ]
}); });
export const entityItemValidation = (entities: EntityItem[], creating: boolean) => export const entityItemValidation = () =>
new Schema({ new Schema({
name: [ name: [
{ required: true, message: 'Name is required' }, { required: true, message: 'Name is required' },
@@ -122,13 +111,30 @@ export const entityItemValidation = (entities: EntityItem[], creating: boolean)
message: "Must be <15 characters: alpha numeric, '_' or '.'" message: "Must be <15 characters: alpha numeric, '_' or '.'"
} }
], ],
device_id: [{ type: 'hex', required: true, message: 'ID must be a hex value' }] device_id: [
// type_id: [ { required: true, message: 'Device ID is required' },
// { required: true, message: 'Type_id is required' }, {
// { type: 'string', pattern: /^[A-F0-9]{1,4}$/, message: 'Must be a hex number' } validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) {
// ], if (isNaN(parseInt(value, 16))) {
// offset: [ callback('Must be a hex number');
// { required: true, message: 'Offset is required' }, }
// { type: 'number', min: 0, max: 255, message: 'Must be between 0 and 255' } callback();
// ] }
}
],
type_id: [
{ required: true, message: 'Type ID is required' },
{
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) {
if (isNaN(parseInt(value, 16))) {
callback('Must be a hex number');
}
callback();
}
}
],
offset: [
{ required: true, message: 'Offset is required' },
{ type: 'number', min: 0, max: 255, message: 'Must be between 0 and 255' }
]
}); });