From ba836f00e6968c9cd3052d47978aa360ab96dc0b Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Fri, 19 Jun 2026 07:51:18 +0200 Subject: [PATCH] Add widget-control logic action and multi-column CSV / multi-field dialogs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the panel logic editor with two capabilities: - action.widget: drive a panel widget by id from a flow — enable/disable, show/hide, and (for plot widgets) clear, pause, or resume the live plot. Wired via a per-widget command store consumed by a Canvas WidgetView wrapper (disable overlay / hide) and PlotWidget (pause/clear), reset on panel unmount. - action.export now emits multiple named arrays as CSV columns with per-column labels and an alignment mode (common/any/interpolate); dialogs support asking for and displaying multiple values per dialog. Co-Authored-By: Claude Opus 4.6 --- web/src/Canvas.tsx | 42 ++++- web/src/EditMode.tsx | 1 + web/src/LogicDialogs.tsx | 32 +++- web/src/LogicEditor.tsx | 331 +++++++++++++++++++++++++++------ web/src/lib/logic.ts | 207 ++++++++++++++++++--- web/src/lib/types.ts | 25 ++- web/src/lib/widgetCommands.ts | 47 +++++ web/src/styles.css | 68 ++++++- web/src/widgets/PlotWidget.tsx | 17 ++ 9 files changed, 668 insertions(+), 102 deletions(-) create mode 100644 web/src/lib/widgetCommands.ts diff --git a/web/src/Canvas.tsx b/web/src/Canvas.tsx index 375bb9d..47933cd 100644 --- a/web/src/Canvas.tsx +++ b/web/src/Canvas.tsx @@ -1,7 +1,8 @@ -import { h } from 'preact'; +import { h, Fragment } from 'preact'; import { useState, useEffect } from 'preact/hooks'; import { initLocalState } from './lib/localstate'; import { logicEngine } from './lib/logic'; +import { getWidgetCmdStore, type WidgetCmd } from './lib/widgetCommands'; import type { Interface, Widget, SignalRef } from './lib/types'; import TextView from './widgets/TextView'; import TextLabel from './widgets/TextLabel'; @@ -42,6 +43,32 @@ interface CtxState { signal: SignalRef | null; } +// Wraps a single view-mode widget so panel logic (action.widget) can drive it: +// `hidden` removes it from the view, `disabled` dims it and blocks interaction +// via a transparent overlay (which still surfaces the right-click menu). Plot +// pause/clear are handled inside PlotWidget itself. +function WidgetView({ widget, children, onContextMenu }: { + widget: Widget; + children: any; + onContextMenu: (e: MouseEvent) => void; + key?: string; +}) { + const [cmd, setCmd] = useState(() => getWidgetCmdStore(widget.id).get()); + useEffect(() => getWidgetCmdStore(widget.id).subscribe(setCmd), [widget.id]); + if (cmd.hidden) return null; + if (!cmd.disabled) return children; + return ( + + {children} +
+ + ); +} + interface Props { iface: Interface | null; onNavigate?: (interfaceId: string) => void; @@ -131,9 +158,8 @@ export default function Canvas({ iface, onNavigate, timeRange }: Props) {
{iface.widgets.map(widget => { const Comp = COMPONENTS[widget.type]; - return Comp + const inner = Comp ? onCtxMenu(e, widget)} onNavigate={onNavigate} @@ -141,7 +167,6 @@ export default function Canvas({ iface, onNavigate, timeRange }: Props) { /> : (
{widget.type}
); + return ( + onCtxMenu(e, widget)} + > + {inner} + + ); })}
diff --git a/web/src/EditMode.tsx b/web/src/EditMode.tsx index 992ea73..4f9fc82 100644 --- a/web/src/EditMode.tsx +++ b/web/src/EditMode.tsx @@ -682,6 +682,7 @@ export default function EditMode({ initial, onDone }: Props) { diff --git a/web/src/LogicDialogs.tsx b/web/src/LogicDialogs.tsx index f902b95..0392c1f 100644 --- a/web/src/LogicDialogs.tsx +++ b/web/src/LogicDialogs.tsx @@ -13,10 +13,18 @@ export default function LogicDialogs() { } function DialogModal({ req }: { req: DialogRequest; key?: string }) { - const [val, setVal] = useState(req.defaultValue ?? ''); const isSetpoint = req.kind === 'setpoint'; + const inputIdx = req.fields + .map((f, i) => (f.type === 'input' ? i : -1)) + .filter(i => i >= 0); + // Per-input editable values, keyed by field index, seeded from the prefill. + const [vals, setVals] = useState>( + () => Object.fromEntries(inputIdx.map(i => [i, req.fields[i].value])) + ); - function ok() { req.resolve(isSetpoint ? val : ''); } + function ok() { + req.resolve(inputIdx.map(i => vals[i] ?? '')); + } function cancel() { req.resolve(null); } function onKey(e: KeyboardEvent) { @@ -29,12 +37,20 @@ function DialogModal({ req }: { req: DialogRequest; key?: string }) {