From 54a7455a5547fe1a597dc84d19fd54db50643a84 Mon Sep 17 00:00:00 2001 From: Manuel Fragner Date: Sat, 22 Nov 2025 22:33:16 +0100 Subject: [PATCH 1/6] feat: Rename AddScene component --- .../device-page/{AddScene.tsx => AddUpdateScene.tsx} | 2 +- src/components/device-page/tabs/Scene.tsx | 4 ++-- src/components/group-page/tabs/Devices.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/components/device-page/{AddScene.tsx => AddUpdateScene.tsx} (98%) diff --git a/src/components/device-page/AddScene.tsx b/src/components/device-page/AddUpdateScene.tsx similarity index 98% rename from src/components/device-page/AddScene.tsx rename to src/components/device-page/AddUpdateScene.tsx index 4de5072b8..361661dde 100644 --- a/src/components/device-page/AddScene.tsx +++ b/src/components/device-page/AddUpdateScene.tsx @@ -19,7 +19,7 @@ type AddSceneProps = { deviceState: DeviceState; }; -const AddScene = memo(({ sourceIdx, target, deviceState }: AddSceneProps) => { +const AddUpdateScene = memo(({ sourceIdx, target, deviceState }: AddSceneProps) => { const { t } = useTranslation("scene"); const [sceneId, setSceneId] = useState(0); const [sceneName, setSceneName] = useState(""); diff --git a/src/components/device-page/tabs/Scene.tsx b/src/components/device-page/tabs/Scene.tsx index b4e9d63ef..d9abb6048 100644 --- a/src/components/device-page/tabs/Scene.tsx +++ b/src/components/device-page/tabs/Scene.tsx @@ -1,7 +1,7 @@ import { useShallow } from "zustand/react/shallow"; import { useAppStore } from "../../../store.js"; import type { Device } from "../../../types.js"; -import AddScene from "../AddScene.js"; +import AddUpdateScene from "../AddUpdateScene"; import RecallRemove from "../RecallRemove.js"; type SceneProps = { @@ -16,7 +16,7 @@ export default function Scene({ sourceIdx, device }: SceneProps) {
- +
diff --git a/src/components/group-page/tabs/Devices.tsx b/src/components/group-page/tabs/Devices.tsx index 9e54597c0..429086614 100644 --- a/src/components/group-page/tabs/Devices.tsx +++ b/src/components/group-page/tabs/Devices.tsx @@ -1,7 +1,7 @@ import { useShallow } from "zustand/react/shallow"; import { useAppStore } from "../../../store.js"; import type { Group } from "../../../types.js"; -import AddScene from "../../device-page/AddScene.js"; +import AddUpdateScene from "../../device-page/AddUpdateScene"; import RecallRemove from "../../device-page/RecallRemove.js"; import AddDeviceToGroup from "../AddDeviceToGroup.js"; import GroupMembers from "../GroupMembers.js"; @@ -29,7 +29,7 @@ export default function Devices({ sourceIdx, group }: DevicesProps) {
- +
From 0eb4d3bc9be5558ea15342b5eba24a16c449e0b5 Mon Sep 17 00:00:00 2001 From: Manuel Fragner Date: Sat, 22 Nov 2025 22:34:39 +0100 Subject: [PATCH 2/6] feat: Update `.gitignore` to include JetBrains IDE configurations --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 2bc28bbb7..fd65db239 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,6 @@ dist *storybook.log storybook-static + +# JetBrains IDEs +.idea From 6805d12d7154be67a58365c8604de7e0397f7a5c Mon Sep 17 00:00:00 2001 From: Manuel Fragner Date: Sat, 22 Nov 2025 22:35:51 +0100 Subject: [PATCH 3/6] fix: Shorten import --- src/components/device-page/AddUpdateScene.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/device-page/AddUpdateScene.tsx b/src/components/device-page/AddUpdateScene.tsx index 361661dde..0b2ee2003 100644 --- a/src/components/device-page/AddUpdateScene.tsx +++ b/src/components/device-page/AddUpdateScene.tsx @@ -9,7 +9,7 @@ import { sendMessage } from "../../websocket/WebSocketManager.js"; import Button from "../Button.js"; import DashboardFeatureWrapper from "../dashboard-page/DashboardFeatureWrapper.js"; import Feature from "../features/Feature.js"; -import { getFeatureKey } from "../features/index.js"; +import { getFeatureKey } from "../features"; import InputField from "../form-fields/InputField.js"; import { getScenes } from "./index.js"; @@ -104,4 +104,4 @@ const AddUpdateScene = memo(({ sourceIdx, target, deviceState }: AddSceneProps) ); }); -export default AddScene; +export default AddUpdateScene; From 1da72fbdb4645664a35ff80dccf16220d0389d52 Mon Sep 17 00:00:00 2001 From: Manuel Fragner Date: Sat, 22 Nov 2025 22:38:56 +0100 Subject: [PATCH 4/6] feat: Add new scene texts --- src/i18n/locales/en.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 5ad713bf5..7faf99c6e 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -408,9 +408,11 @@ "scene_id": "Scene ID", "recall": "Recall", "store": "Store", + "update": "Update", "remove_all": "Remove all", "select_scene": "Select Scene", - "scene_name": "Scene Name" + "scene_name": "Scene Name", + "update_scene": "Update Scene" }, "stats": { "byType": "By device type", From e6c384ed9394ba73a16c466209cfe1e3dcd114c0 Mon Sep 17 00:00:00 2001 From: Manuel Fragner Date: Sat, 22 Nov 2025 22:49:56 +0100 Subject: [PATCH 5/6] feat: Enhance AddScene component to allow directly updating scenes --- src/components/device-page/AddUpdateScene.tsx | 117 ++++++++++++++---- 1 file changed, 90 insertions(+), 27 deletions(-) diff --git a/src/components/device-page/AddUpdateScene.tsx b/src/components/device-page/AddUpdateScene.tsx index 0b2ee2003..aaea26c8b 100644 --- a/src/components/device-page/AddUpdateScene.tsx +++ b/src/components/device-page/AddUpdateScene.tsx @@ -1,17 +1,43 @@ -import { memo, useCallback, useMemo, useState } from "react"; +import { type ChangeEvent, useCallback, useEffect, useEffectEvent, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import type { Zigbee2MQTTAPI } from "zigbee2mqtt"; import { useShallow } from "zustand/react/shallow"; -import { useAppStore } from "../../store.js"; -import type { Device, DeviceState, Group } from "../../types.js"; -import { isDevice } from "../../utils.js"; -import { sendMessage } from "../../websocket/WebSocketManager.js"; -import Button from "../Button.js"; -import DashboardFeatureWrapper from "../dashboard-page/DashboardFeatureWrapper.js"; -import Feature from "../features/Feature.js"; +import { useAppStore } from "../../store"; +import type { Device, DeviceState, Group, Scene } from "../../types"; +import { isDevice } from "../../utils"; +import { sendMessage } from "../../websocket/WebSocketManager"; +import Button from "../Button"; +import ConfirmButton from "../ConfirmButton"; +import DashboardFeatureWrapper from "../dashboard-page/DashboardFeatureWrapper"; import { getFeatureKey } from "../features"; -import InputField from "../form-fields/InputField.js"; -import { getScenes } from "./index.js"; +import Feature from "../features/Feature"; +import InputField from "../form-fields/InputField"; +import { getScenes } from "./index"; + +type AddUpdateSceneAction = "add" | "update"; + +type SceneInput = { + action: AddUpdateSceneAction; +} & Scene; + +const DEFAULT_SCENE_ID = 0; +const DEFAULT_SCENE_INPUT: SceneInput = { id: DEFAULT_SCENE_ID, name: "", action: "add" }; + +const createSceneInput = (scenes: Scene[], sceneId: number): SceneInput => { + const existingScene = scenes.find((scene) => scene.id === sceneId); + + if (existingScene) { + return { + id: sceneId, + name: existingScene.name, + action: "update", + }; + } + return { + ...DEFAULT_SCENE_INPUT, + id: sceneId, + }; +}; type AddSceneProps = { sourceIdx: number; @@ -19,13 +45,28 @@ type AddSceneProps = { deviceState: DeviceState; }; -const AddUpdateScene = memo(({ sourceIdx, target, deviceState }: AddSceneProps) => { +const AddUpdateScene = ({ sourceIdx, target, deviceState }: AddSceneProps) => { const { t } = useTranslation("scene"); - const [sceneId, setSceneId] = useState(0); - const [sceneName, setSceneName] = useState(""); + const scenes = useMemo(() => getScenes(target), [target]); + const [sceneInput, setSceneInput] = useState(() => createSceneInput(scenes, DEFAULT_SCENE_ID)); + + const onScenesChange = useEffectEvent((scenes: Scene[]) => { + setSceneInput(createSceneInput(scenes, sceneInput.id)); + }); + + useEffect(() => { + onScenesChange(scenes); + }, [scenes]); + + useEffect(() => { + if (scenes.length === 0) { + setSceneInput(DEFAULT_SCENE_INPUT); + } + }, [scenes.length]); + const scenesFeatures = useAppStore( - useShallow((state) => (isDevice(target) ? (state.deviceScenesFeatures[sourceIdx][target.ieee_address] ?? []) : [])), + useShallow((state) => (isDevice(target) ? (state.deviceScenesFeatures[sourceIdx]?.[target.ieee_address] ?? []) : [])), ); const onCompositeChange = useCallback( @@ -41,7 +82,11 @@ const AddUpdateScene = memo(({ sourceIdx, target, deviceState }: AddSceneProps) ); const onStoreClick = useCallback(async () => { - const payload: Zigbee2MQTTAPI["{friendlyNameOrId}/set"][string] = { ID: sceneId, name: sceneName || `Scene ${sceneId}` }; + setSceneInput({ ...sceneInput, action: "update" }); + const payload: Zigbee2MQTTAPI["{friendlyNameOrId}/set"][string] = { + ID: sceneInput.id, + name: sceneInput.name || `Scene ${sceneInput.id}`, + }; await sendMessage<"{friendlyNameOrId}/set">( sourceIdx, @@ -49,11 +94,16 @@ const AddUpdateScene = memo(({ sourceIdx, target, deviceState }: AddSceneProps) `${target.friendly_name}/set`, // TODO: swap to ID/ieee_address { scene_store: payload }, ); - }, [sourceIdx, target, sceneId, sceneName]); + }, [sourceIdx, target, sceneInput]); + + const handleOnSceneIdInputChange = (e: ChangeEvent) => { + const sceneId = e.target.valueAsNumber || DEFAULT_SCENE_ID; + setSceneInput(createSceneInput(scenes, sceneId)); + }; const isValidSceneId = useMemo(() => { - return sceneId >= 0 && sceneId <= 255 && !scenes.find((s) => s.id === sceneId); - }, [sceneId, scenes]); + return sceneInput.id >= 0 && sceneInput.id <= 255; + }, [sceneInput.id]); return ( <> @@ -63,8 +113,8 @@ const AddUpdateScene = memo(({ sourceIdx, target, deviceState }: AddSceneProps) name="scene_id" label={t(($) => $.scene_id)} type="number" - value={sceneId} - onChange={(e) => !!e.target.value && setSceneId(e.target.valueAsNumber)} + value={sceneInput.id} + onChange={handleOnSceneIdInputChange} min={0} max={255} required @@ -73,9 +123,9 @@ const AddUpdateScene = memo(({ sourceIdx, target, deviceState }: AddSceneProps) name="scene_name" label={t(($) => $.scene_name)} type="text" - value={sceneName} - placeholder={`Scene ${sceneId}`} - onChange={(e) => setSceneName(e.target.value)} + value={sceneInput.name} + placeholder={`Scene ${sceneInput.id}`} + onChange={(e) => setSceneInput({ ...sceneInput, name: e.target.value })} required /> {scenesFeatures.length > 0 && ( @@ -97,11 +147,24 @@ const AddUpdateScene = memo(({ sourceIdx, target, deviceState }: AddSceneProps) )} - + {sceneInput.action === "add" ? ( + + ) : ( + $.update_scene)} + modalDescription={t(($) => $.dialog_confirmation_prompt, { ns: "common" })} + modalCancelLabel={t(($) => $.cancel, { ns: "common" })} + > + {t(($) => $.update)} + + )} ); -}); +}; export default AddUpdateScene; From 043b810bae745a7a0dcc074a594a67393a1aa083 Mon Sep 17 00:00:00 2001 From: Manuel Fragner Date: Sat, 22 Nov 2025 23:43:31 +0100 Subject: [PATCH 6/6] feat: Add tooltip to Add button --- src/components/device-page/AddUpdateScene.tsx | 2 +- src/i18n/locales/en.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/device-page/AddUpdateScene.tsx b/src/components/device-page/AddUpdateScene.tsx index aaea26c8b..413d88eb4 100644 --- a/src/components/device-page/AddUpdateScene.tsx +++ b/src/components/device-page/AddUpdateScene.tsx @@ -148,7 +148,7 @@ const AddUpdateScene = ({ sourceIdx, target, deviceState }: AddSceneProps) => { )} {sceneInput.action === "add" ? ( - ) : ( diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 7faf99c6e..98c4017bc 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -412,7 +412,8 @@ "remove_all": "Remove all", "select_scene": "Select Scene", "scene_name": "Scene Name", - "update_scene": "Update Scene" + "update_scene": "Update Scene", + "add_scene": "Add Scene" }, "stats": { "byType": "By device type",