From de75c5f29d8ed854cba0138fef094d1fa9a5876e Mon Sep 17 00:00:00 2001 From: ajnart Date: Sat, 12 Sep 2026 14:09:41 +0200 Subject: [PATCH] fix(custom-widgets): stabilize commands and resolve dismissed confirmations --- .../custom-widgets/trusted-packages.mdx | 5 +++++ packages/widget-sdk/src/client.tsx | 10 +++++++--- .../src/custom-api/trusted-widget-host.tsx | 17 ++++++++++------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/apps/docs/docs/management/custom-widgets/trusted-packages.mdx b/apps/docs/docs/management/custom-widgets/trusted-packages.mdx index b1151458a..a84699c40 100644 --- a/apps/docs/docs/management/custom-widgets/trusted-packages.mdx +++ b/apps/docs/docs/management/custom-widgets/trusted-packages.mdx @@ -77,6 +77,11 @@ Homarr provides React, Mantine, and `@homarr/widget-sdk` as shared host modules. from Homarr's installed version, which is recorded in the artifact lock. The reference packages show how to use the SDK for host dimensions, theme and locale, options, state, commands, data queries, and advanced views. Put additional dependencies in the package's dependency map rather than copying Homarr's internal source tree. +Use `WidgetScope` to compose components with independent state and command IDs. Commands registered through +`useWidgetCommands` remain registered across unrelated host updates and are removed when their component unmounts. +`useWidgetServices().confirm(...)` resolves to `true` on confirmation and `false` on cancellation or dismissal, +including Escape, a backdrop click, or the close button. + ## Convert an existing Custom JSX widget Choose **Convert to a widget package** from a valid Custom JSX widget's list row or editor. Conversion uses its saved diff --git a/packages/widget-sdk/src/client.tsx b/packages/widget-sdk/src/client.tsx index 7e17d9361..55012aa53 100644 --- a/packages/widget-sdk/src/client.tsx +++ b/packages/widget-sdk/src/client.tsx @@ -53,6 +53,11 @@ export function WidgetScope({ }>) { const runtime = useWidgetRuntime(); const parent = runtime.services; + const registerParentCommands = parent.registerCommands; + const registerCommands = useCallback( + (commands) => registerParentCommands(commands.map((command) => ({ ...command, id: `${name}:${command.id}` }))), + [name, registerParentCommands], + ); const services = useMemo(() => { const operation = (input: Parameters[0]) => ({ ...input, @@ -65,8 +70,7 @@ export function WidgetScope({ set: (key, value) => parent.state.set(`${name}:${key}`, value), subscribe: parent.state.subscribe, }, - registerCommands: (commands) => - parent.registerCommands(commands.map((command) => ({ ...command, id: `${name}:${command.id}` }))), + registerCommands, transport: { query: (input, signal) => parent.transport.query(operation(input), signal), action: (input) => parent.transport.action(operation(input)), @@ -75,7 +79,7 @@ export function WidgetScope({ storageSet: (input) => parent.transport.storageSet({ ...input, key: `${name}:${input.key}` }), }, }; - }, [handlerPrefix, name, parent]); + }, [handlerPrefix, name, parent, registerCommands]); const scope = `${runtime.context.scope ?? ""}/${name}`; return ( resolve(true), - onCancel: () => resolve(false), - }); + openConfirmModal( + { + title, + children: message, + confirmProps: { color: destructive ? "red" : undefined }, + onConfirm: () => resolve(true), + onCancel: () => resolve(false), + }, + { onClose: () => resolve(false) }, + ); }), }), [