mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-10-28 11:14:58 +00:00
This commit is contained in:
@@ -11,6 +11,7 @@ import WarningIcon from '@mui/icons-material/Warning';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
@@ -91,6 +92,8 @@ const Customizations = () => {
|
||||
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([]);
|
||||
const [confirmReset, setConfirmReset] = useState<boolean>(false);
|
||||
const [selectedFilters, setSelectedFilters] = useState<number>(0);
|
||||
const [modifiedOnly, setModifiedOnly] = useState<boolean>(false);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedDeviceEntity, setSelectedDeviceEntity] = useState<DeviceEntity>();
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
@@ -173,7 +176,7 @@ const Customizations = () => {
|
||||
|
||||
const entities_theme = useTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: 156px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto);
|
||||
--data-table-library_grid-template-columns: 32px 156px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto);
|
||||
`,
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
@@ -182,10 +185,10 @@ const Customizations = () => {
|
||||
}
|
||||
`,
|
||||
BaseCell: `
|
||||
&:nth-of-type(3) {
|
||||
&:nth-of-type(4) {
|
||||
text-align: right;
|
||||
}
|
||||
&:nth-of-type(4) {
|
||||
&:nth-of-type(5) {
|
||||
text-align: right;
|
||||
}
|
||||
&:last-of-type {
|
||||
@@ -221,13 +224,13 @@ const Customizations = () => {
|
||||
}
|
||||
`,
|
||||
Cell: `
|
||||
&:nth-of-type(2) {
|
||||
&:nth-of-type(3) {
|
||||
padding: 8px;
|
||||
}
|
||||
&:nth-of-type(3) {
|
||||
&:nth-of-type(4) {
|
||||
padding-right: 4px;
|
||||
}
|
||||
&:nth-of-type(4) {
|
||||
&:nth-of-type(5) {
|
||||
padding-right: 4px;
|
||||
}
|
||||
&:last-of-type {
|
||||
@@ -245,6 +248,11 @@ const Customizations = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Saved customization (c), or an edit that is not applied yet.
|
||||
function isModified(de: DeviceEntity) {
|
||||
return !!de.c || hasEntityChanged(de);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (deviceEntities.length) {
|
||||
const changedEntities = deviceEntities.filter((de) => hasEntityChanged(de));
|
||||
@@ -267,6 +275,7 @@ const Customizations = () => {
|
||||
setSelectedDeviceBrand(device.b);
|
||||
}
|
||||
setNumChanges(0);
|
||||
setSelectedIds(new Set());
|
||||
setRestartNeeded(false);
|
||||
}
|
||||
}
|
||||
@@ -331,24 +340,61 @@ const Customizations = () => {
|
||||
|
||||
const filter_entity = (de: DeviceEntity) =>
|
||||
(de.m & selectedFilters || !selectedFilters) &&
|
||||
(!modifiedOnly || isModified(de)) &&
|
||||
formatName(de, true).toLowerCase().includes(search.toLowerCase());
|
||||
|
||||
// Checked rows when any are checked, otherwise every row the filter is showing.
|
||||
const inBulkTarget = (de: DeviceEntity) =>
|
||||
selectedIds.size > 0 ? selectedIds.has(de.id) : filter_entity(de);
|
||||
|
||||
const maskDisabled = (set: boolean) => {
|
||||
const excludeMask =
|
||||
DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE;
|
||||
setDeviceEntities((prev) =>
|
||||
prev.map((de) => {
|
||||
if (filter_entity(de)) {
|
||||
const excludeMask =
|
||||
DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE;
|
||||
return {
|
||||
...de,
|
||||
m: set ? de.m | excludeMask : de.m & ~excludeMask
|
||||
};
|
||||
if (!inBulkTarget(de)) {
|
||||
return de;
|
||||
}
|
||||
return de;
|
||||
return {
|
||||
...de,
|
||||
m: set ? de.m | excludeMask : de.m & ~excludeMask
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const maskReadonly = (set: boolean) => {
|
||||
setDeviceEntities((prev) =>
|
||||
prev.map((de) => {
|
||||
if (!inBulkTarget(de) || !de.w) {
|
||||
return de;
|
||||
}
|
||||
return {
|
||||
...de,
|
||||
m: set
|
||||
? de.m | DeviceEntityMask.DV_READONLY
|
||||
: de.m & ~DeviceEntityMask.DV_READONLY
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const resetSelected = () => {
|
||||
setDeviceEntities((prev) =>
|
||||
prev.map((de) => {
|
||||
if (!selectedIds.has(de.id)) {
|
||||
return de;
|
||||
}
|
||||
const next: DeviceEntity = { ...de, m: 0, c: false };
|
||||
delete next.cn;
|
||||
delete next.mi;
|
||||
delete next.ma;
|
||||
return next;
|
||||
})
|
||||
);
|
||||
setSelectedIds(new Set());
|
||||
};
|
||||
|
||||
const resetCustomization = async () => {
|
||||
try {
|
||||
await sendResetCustomizations();
|
||||
@@ -414,6 +460,8 @@ const Customizations = () => {
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(LL.CUSTOMIZATIONS_SAVED());
|
||||
// Reload so cleared min/max fall back to the built-in limits and the modified flag matches the file.
|
||||
void sendDeviceEntities(selectedDevice);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
if (error.message === 'Reboot required') {
|
||||
@@ -541,6 +589,34 @@ const Customizations = () => {
|
||||
);
|
||||
|
||||
const filteredEntities = deviceEntities.filter((de) => filter_entity(de));
|
||||
const allFilteredSelected =
|
||||
filteredEntities.length > 0 &&
|
||||
filteredEntities.every((de) => selectedIds.has(de.id));
|
||||
const someFilteredSelected = filteredEntities.some((de) => selectedIds.has(de.id));
|
||||
|
||||
const toggleSelected = (id: string) => {
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) {
|
||||
next.delete(id);
|
||||
} else {
|
||||
next.add(id);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleSelectFiltered = () => {
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (allFilteredSelected) {
|
||||
filteredEntities.forEach((de) => next.delete(de.id));
|
||||
} else {
|
||||
filteredEntities.forEach((de) => next.add(de.id));
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const renderDeviceData = () => {
|
||||
return (
|
||||
@@ -608,6 +684,17 @@ const Customizations = () => {
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ fontSize: 10 }}
|
||||
variant="outlined"
|
||||
color={modifiedOnly ? 'secondary' : 'inherit'}
|
||||
onClick={() => setModifiedOnly((current) => !current)}
|
||||
>
|
||||
{LL.MODIFIED()}
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
@@ -634,6 +721,43 @@ const Customizations = () => {
|
||||
<OptionIcon type="web_exclude" isSet={true} />
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ fontSize: 10 }}
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
onClick={() => maskReadonly(false)}
|
||||
>
|
||||
{LL.SET_ALL()}
|
||||
<OptionIcon type="readonly" isSet={false} />
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ fontSize: 10 }}
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
onClick={() => maskReadonly(true)}
|
||||
>
|
||||
{LL.SET_ALL()}
|
||||
<OptionIcon type="readonly" isSet={true} />
|
||||
</Button>
|
||||
</Grid>
|
||||
{selectedIds.size > 0 && (
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ fontSize: 10 }}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={resetSelected}
|
||||
>
|
||||
{LL.RESET_SELECTED()} ({selectedIds.size})
|
||||
</Button>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid>
|
||||
<Typography variant="subtitle2" color="grey">
|
||||
{LL.SHOWING()} {filteredEntities.length}/{deviceEntities.length}
|
||||
@@ -650,6 +774,16 @@ const Customizations = () => {
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell stiff>
|
||||
<Checkbox
|
||||
size="small"
|
||||
sx={{ p: 0, color: 'inherit' }}
|
||||
checked={allFilteredSelected}
|
||||
indeterminate={!allFilteredSelected && someFilteredSelected}
|
||||
disabled={filteredEntities.length === 0}
|
||||
onChange={toggleSelectFiltered}
|
||||
/>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff>{LL.OPTIONS()}</HeaderCell>
|
||||
<HeaderCell resize>{LL.NAME(1)}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.MIN()}</HeaderCell>
|
||||
@@ -660,6 +794,14 @@ const Customizations = () => {
|
||||
<Body>
|
||||
{tableList.map((de: DeviceEntity) => (
|
||||
<Row key={de.id} item={de} onClick={() => editDeviceEntity(de)}>
|
||||
<Cell stiff onClick={(event) => event.stopPropagation()}>
|
||||
<Checkbox
|
||||
size="small"
|
||||
sx={{ p: 0 }}
|
||||
checked={selectedIds.has(de.id)}
|
||||
onChange={() => toggleSelected(de.id)}
|
||||
/>
|
||||
</Cell>
|
||||
<Cell stiff>
|
||||
<EntityMaskToggle onUpdate={updateDeviceEntity} de={de} />
|
||||
</Cell>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { memo, useEffect, useState } from 'react';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DoneIcon from '@mui/icons-material/Done';
|
||||
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -101,6 +102,14 @@ const CustomizationsDialog = ({
|
||||
setEditItem((prev) => ({ ...prev, m: updatedItem.m }));
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
const next: DeviceEntity = { ...editItem, m: 0, c: false };
|
||||
delete next.cn;
|
||||
delete next.mi;
|
||||
delete next.ma;
|
||||
onSave(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||
<DialogTitle>{`${LL.EDIT()} ${LL.ENTITY()}`}</DialogTitle>
|
||||
@@ -169,6 +178,15 @@ const CustomizationsDialog = ({
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
onClick={remove}
|
||||
color="error"
|
||||
>
|
||||
{LL.REMOVE()}
|
||||
</Button>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
|
||||
@@ -174,6 +174,7 @@ export interface DeviceEntity {
|
||||
w: boolean; // writeable
|
||||
mi?: number; // min value
|
||||
ma?: number; // max value
|
||||
c?: boolean; // true when a customization is saved (mask, name, or min/max)
|
||||
o_m?: number; // original mask before edits
|
||||
o_cn?: string; // original cn before edits
|
||||
o_mi?: number; // original min value
|
||||
|
||||
@@ -141,6 +141,8 @@ const cs: Translation = {
|
||||
CUSTOMIZATIONS_HELP_6: 'odstranit z paměti',
|
||||
SELECT_DEVICE: 'Vyberte zařízení',
|
||||
SET_ALL: 'nastavit vše',
|
||||
MODIFIED: 'upravené',
|
||||
RESET_SELECTED: 'Obnovit vybrané',
|
||||
OPTIONS: 'Možnosti',
|
||||
NAME: 'Název',
|
||||
CUSTOMIZATIONS_RESET: 'Opravdu chcete odstranit všechna přizpůsobení?',
|
||||
|
||||
@@ -141,6 +141,8 @@ const de: Translation = {
|
||||
CUSTOMIZATIONS_HELP_6: 'Aus dem Speicher entfernen',
|
||||
SELECT_DEVICE: 'Wählen Sie ein Gerät aus',
|
||||
SET_ALL: 'Setzen Sie alle',
|
||||
MODIFIED: 'geändert',
|
||||
RESET_SELECTED: 'Auswahl zurücksetzen',
|
||||
OPTIONS: 'Optionen',
|
||||
NAME: 'Name',
|
||||
CUSTOMIZATIONS_RESET: 'Möchten Sie wirklich alle Anpassungen entfernen?',
|
||||
|
||||
@@ -141,6 +141,8 @@ const en: Translation = {
|
||||
CUSTOMIZATIONS_HELP_6: 'remove from memory',
|
||||
SELECT_DEVICE: 'Select a device',
|
||||
SET_ALL: 'set all',
|
||||
MODIFIED: 'modified',
|
||||
RESET_SELECTED: 'Reset selected',
|
||||
OPTIONS: 'Options',
|
||||
NAME: 'Name',
|
||||
CUSTOMIZATIONS_RESET: 'Are you sure you want remove all the customizations?',
|
||||
|
||||
@@ -141,6 +141,8 @@ const fr: Translation = {
|
||||
CUSTOMIZATIONS_HELP_6: 'supprimer de la mémoire',
|
||||
SELECT_DEVICE: 'Sélectionnez un appareil',
|
||||
SET_ALL: 'tout régler',
|
||||
MODIFIED: 'modifié',
|
||||
RESET_SELECTED: 'Réinitialiser la sélection',
|
||||
OPTIONS: 'Options',
|
||||
NAME: 'Nom',
|
||||
CUSTOMIZATIONS_RESET: 'Êtes-vous sûr de vouloir supprimer toutes les personnalisations?',
|
||||
|
||||
@@ -141,6 +141,8 @@ const it: Translation = {
|
||||
CUSTOMIZATIONS_HELP_6: 'rimuovi dalla memoria',
|
||||
SELECT_DEVICE: 'Seleziona un dispositivo',
|
||||
SET_ALL: 'imposta tutto',
|
||||
MODIFIED: 'modificato',
|
||||
RESET_SELECTED: 'Reimposta selezionati',
|
||||
OPTIONS: 'Opzioni',
|
||||
NAME: 'Nome',
|
||||
CUSTOMIZATIONS_RESET: 'Sei sicuro di voler rimuovere tutte le personalizzazioni?',
|
||||
|
||||
@@ -141,6 +141,8 @@ const nl: Translation = {
|
||||
CUSTOMIZATIONS_HELP_6: 'verwijderen van memory',
|
||||
SELECT_DEVICE: 'Selecteer een apparaat',
|
||||
SET_ALL: 'Alles aanzetten',
|
||||
MODIFIED: 'gewijzigd',
|
||||
RESET_SELECTED: 'Selectie resetten',
|
||||
OPTIONS: 'Opties',
|
||||
NAME: 'Naam',
|
||||
CUSTOMIZATIONS_RESET: 'Weet je zeker dat je alle custom aanpassingen wilt verwijderen?',
|
||||
|
||||
@@ -141,6 +141,8 @@ const no: Translation = {
|
||||
CUSTOMIZATIONS_HELP_6: 'fjern fra minnet',
|
||||
SELECT_DEVICE: 'Velg en enhet',
|
||||
SET_ALL: 'sett alle',
|
||||
MODIFIED: 'endret',
|
||||
RESET_SELECTED: 'Tilbakestill valgte',
|
||||
OPTIONS: 'Alternativ',
|
||||
NAME: 'Navn',
|
||||
CUSTOMIZATIONS_RESET: 'Er du sikker på att du vil fjerne tilpassninger?',
|
||||
|
||||
@@ -141,6 +141,8 @@ const pl: BaseTranslation = {
|
||||
CUSTOMIZATIONS_HELP_6: 'usuń z pamięci',
|
||||
SELECT_DEVICE: 'wybierz urządzenie',
|
||||
SET_ALL: 'Ustaw wszystko jako',
|
||||
MODIFIED: 'zmienione',
|
||||
RESET_SELECTED: 'Resetuj zaznaczone',
|
||||
OPTIONS: 'Opcje',
|
||||
NAME: '{{Nazwa|nazwa|}}',
|
||||
CUSTOMIZATIONS_RESET: 'Na pewno chcesz usunąć wszystkie personalizacje?',
|
||||
|
||||
@@ -141,6 +141,8 @@ const sk: Translation = {
|
||||
CUSTOMIZATIONS_HELP_6: 'odstrániť z pamäte',
|
||||
SELECT_DEVICE: 'Zvoliť zariadenie',
|
||||
SET_ALL: 'nastaviť všetko',
|
||||
MODIFIED: 'upravené',
|
||||
RESET_SELECTED: 'Obnoviť vybrané',
|
||||
OPTIONS: 'Možnosti',
|
||||
NAME: 'Názov',
|
||||
CUSTOMIZATIONS_RESET: 'Naozaj chcete odstrániť všetky prispôsobenia?',
|
||||
|
||||
@@ -141,6 +141,8 @@ const sv: Translation = {
|
||||
CUSTOMIZATIONS_HELP_6: 'Ta bort',
|
||||
SELECT_DEVICE: 'Välj en enhet',
|
||||
SET_ALL: 'ställ in alla',
|
||||
MODIFIED: 'ändrade',
|
||||
RESET_SELECTED: 'Återställ markerade',
|
||||
OPTIONS: 'Alternativ',
|
||||
NAME: 'Namn',
|
||||
CUSTOMIZATIONS_RESET: 'Är du säker på att du vill ta bort alla anpassningar?',
|
||||
|
||||
@@ -141,6 +141,8 @@ const tr: Translation = {
|
||||
CUSTOMIZATIONS_HELP_6: 'remove from memory',
|
||||
SELECT_DEVICE: 'Bir cihaz seç',
|
||||
SET_ALL: 'hepsini ayarla',
|
||||
MODIFIED: 'değiştirilmiş',
|
||||
RESET_SELECTED: 'Seçilenleri sıfırla',
|
||||
OPTIONS: 'Seçenekler',
|
||||
NAME: 'İsim',
|
||||
CUSTOMIZATIONS_RESET: 'Bütün özelleştirmeleri kaldırmak istediğinizden emin misiniz?',
|
||||
|
||||
@@ -1193,6 +1193,12 @@ void EMSdevice::generate_values_web_customization(JsonArray output) {
|
||||
// 0x10 = 16 = DV_WEB_EXCLUDE
|
||||
obj["m"] = dv.state >> 4;
|
||||
|
||||
// Saved customization: mask bits, or a custom name/min/max stored in custom_fullname.
|
||||
// Min/max in "mi"/"ma" are the effective limits, so they alone do not show a customization.
|
||||
if (dv.has_custom_fullname() || (dv.state >> 4) != 0) {
|
||||
obj["c"] = true;
|
||||
}
|
||||
|
||||
obj["w"] = dv.has_cmd; // if writable
|
||||
|
||||
if (dv.has_cmd && (obj["v"].is<float>() || obj["v"].is<int>())) {
|
||||
@@ -1219,6 +1225,7 @@ void EMSdevice::generate_values_web_customization(JsonArray output) {
|
||||
JsonObject obj = output.add<JsonObject>();
|
||||
obj["id"] = DeviceValue::get_name(entity_id); // set the name, it could be custom following a '|'
|
||||
obj["m"] = mask; // update the mask
|
||||
obj["c"] = true;
|
||||
obj["w"] = false; // not writeable as it won't be shown
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user