Add widget-control logic action and multi-column CSV / multi-field dialogs

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 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-06-19 07:51:18 +02:00
parent afefba3184
commit ba836f00e6
9 changed files with 668 additions and 102 deletions
+38 -4
View File
@@ -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<WidgetCmd>(() => getWidgetCmdStore(widget.id).get());
useEffect(() => getWidgetCmdStore(widget.id).subscribe(setCmd), [widget.id]);
if (cmd.hidden) return null;
if (!cmd.disabled) return children;
return (
<Fragment>
{children}
<div
class="widget-disable-overlay"
style={`left:${widget.x}px;top:${widget.y}px;width:${widget.w}px;height:${widget.h}px;`}
onContextMenu={onContextMenu}
/>
</Fragment>
);
}
interface Props {
iface: Interface | null;
onNavigate?: (interfaceId: string) => void;
@@ -131,9 +158,8 @@ export default function Canvas({ iface, onNavigate, timeRange }: Props) {
<div class="canvas-area" style={`width:${iface.w}px;height:${iface.h}px;`}>
{iface.widgets.map(widget => {
const Comp = COMPONENTS[widget.type];
return Comp
const inner = Comp
? <Comp
key={widget.id}
widget={widget}
onContextMenu={(e: MouseEvent) => onCtxMenu(e, widget)}
onNavigate={onNavigate}
@@ -141,7 +167,6 @@ export default function Canvas({ iface, onNavigate, timeRange }: Props) {
/>
: (
<div
key={widget.id}
class="unknown-widget"
style={`left:${widget.x}px;top:${widget.y}px;width:${widget.w}px;height:${widget.h}px;`}
title={`Unknown widget type: ${widget.type}`}
@@ -150,6 +175,15 @@ export default function Canvas({ iface, onNavigate, timeRange }: Props) {
<span class="unknown-label">{widget.type}</span>
</div>
);
return (
<WidgetView
key={widget.id}
widget={widget}
onContextMenu={(e: MouseEvent) => onCtxMenu(e, widget)}
>
{inner}
</WidgetView>
);
})}
</div>