Remove legacy array logic nodes; migrate + dropdown-ify
Delete the action.accumulate / action.clear node kinds (superseded by the typed action.array.* nodes). Old panels are migrated transparently on load (xml.ts migrateLegacyArrayNode): accumulate→array.push, clear→array.clear, and legacy single-`array` export folded into the JSON `columns` form. Export CSV column pickers now use array-local dropdowns instead of free-text + datalist, matching the action.array.* node inspectors. Removes the now-dead flow-array-names datalist and legacy single-`array` fallbacks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -657,7 +657,7 @@ function SectionLogic() {
|
|||||||
{[
|
{[
|
||||||
{ type: 'Triggers', desc: 'Button press, threshold crossing, value change, timer/interval, panel loop, and On-open / On-close lifecycle.' },
|
{ type: 'Triggers', desc: 'Button press, threshold crossing, value change, timer/interval, panel loop, and On-open / On-close lifecycle.' },
|
||||||
{ type: 'Logic', desc: 'AND gate, If (then/else branches), and Loop (count or while).' },
|
{ type: 'Logic', desc: 'AND gate, If (then/else branches), and Loop (count or while).' },
|
||||||
{ type: 'Actions', desc: 'Write a value to a signal/variable, Delay, Log (debug), and Accumulate / Export-CSV / Clear for in-memory data arrays.' },
|
{ type: 'Actions', desc: 'Write a value to a signal/variable, Delay, Log (debug), Array push/set/remove/pop/clear on an array local variable, and Export-CSV.' },
|
||||||
{ type: 'Dialogs', desc: 'Info and Error pop-ups, and a Set-point prompt that asks the user for a number and writes it to a target.' },
|
{ type: 'Dialogs', desc: 'Info and Error pop-ups, and a Set-point prompt that asks the user for a number and writes it to a target.' },
|
||||||
].map(w => (
|
].map(w => (
|
||||||
<div key={w.type} class="help-widget-row">
|
<div key={w.type} class="help-widget-row">
|
||||||
|
|||||||
+18
-56
@@ -73,9 +73,7 @@ const PALETTE: PaletteEntry[] = [
|
|||||||
{ kind: 'flow.loop', label: 'Loop', params: { mode: 'count', count: '3', cond: '' } },
|
{ kind: 'flow.loop', label: 'Loop', params: { mode: 'count', count: '3', cond: '' } },
|
||||||
{ kind: 'action.write', label: 'Write', params: { target: '', expr: '' } },
|
{ kind: 'action.write', label: 'Write', params: { target: '', expr: '' } },
|
||||||
{ kind: 'action.delay', label: 'Delay', params: { ms: '500' } },
|
{ kind: 'action.delay', label: 'Delay', params: { ms: '500' } },
|
||||||
{ kind: 'action.accumulate', label: 'Accumulate', params: { array: 'data', expr: '' } },
|
{ kind: 'action.export', label: 'Export CSV', params: { columns: '[{"array":"","label":""}]', align: 'common', filename: '' } },
|
||||||
{ kind: 'action.export', label: 'Export CSV', params: { columns: '[{"array":"data","label":""}]', align: 'common', filename: '' } },
|
|
||||||
{ kind: 'action.clear', label: 'Clear array', params: { array: 'data' } },
|
|
||||||
{ kind: 'action.log', label: 'Log', params: { expr: '', label: '' } },
|
{ kind: 'action.log', label: 'Log', params: { expr: '', label: '' } },
|
||||||
{ kind: 'action.widget', label: 'Widget control', params: { widget: '', op: 'disable' } },
|
{ kind: 'action.widget', label: 'Widget control', params: { widget: '', op: 'disable' } },
|
||||||
{ kind: 'action.dialog.info', label: 'Info dialog', params: { title: 'Info', message: '' } },
|
{ kind: 'action.dialog.info', label: 'Info dialog', params: { title: 'Info', message: '' } },
|
||||||
@@ -106,9 +104,7 @@ const KIND_LABEL: Record<LogicNodeKind, string> = {
|
|||||||
'flow.loop': 'Loop',
|
'flow.loop': 'Loop',
|
||||||
'action.write': 'Write',
|
'action.write': 'Write',
|
||||||
'action.delay': 'Delay',
|
'action.delay': 'Delay',
|
||||||
'action.accumulate': 'Accumulate',
|
|
||||||
'action.export': 'Export CSV',
|
'action.export': 'Export CSV',
|
||||||
'action.clear': 'Clear array',
|
|
||||||
'action.log': 'Log',
|
'action.log': 'Log',
|
||||||
'action.widget': 'Widget control',
|
'action.widget': 'Widget control',
|
||||||
'action.dialog.info': 'Info dialog',
|
'action.dialog.info': 'Info dialog',
|
||||||
@@ -575,16 +571,6 @@ export default function LogicEditor({ graph, onChange, widgets = [], statevars,
|
|||||||
|
|
||||||
const byId = new Map(nodes.map(n => [n.id, n]));
|
const byId = new Map(nodes.map(n => [n.id, n]));
|
||||||
|
|
||||||
// Array names already referenced by accumulate/export/clear nodes, offered as
|
|
||||||
// autocomplete suggestions so the same array is reused across nodes.
|
|
||||||
// Suggestions for the array-name autocomplete (export columns, accumulate,
|
|
||||||
// clear): declared array locals first, then any names already referenced by
|
|
||||||
// nodes (covers legacy/in-flight names not yet declared).
|
|
||||||
const arrayNames = Array.from(new Set([
|
|
||||||
...(statevars ?? []).filter(v => v.type === 'array').map(v => v.name),
|
|
||||||
...nodes.map(n => n.params.array).filter((a): a is string => !!a),
|
|
||||||
]));
|
|
||||||
|
|
||||||
// Append a {ds:name} reference into a node's expression field.
|
// Append a {ds:name} reference into a node's expression field.
|
||||||
function insertRef(id: string, field: string, ds: string, sig: string) {
|
function insertRef(id: string, field: string, ds: string, sig: string) {
|
||||||
const cur = (byId.get(id)?.params[field]) ?? '';
|
const cur = (byId.get(id)?.params[field]) ?? '';
|
||||||
@@ -684,9 +670,6 @@ export default function LogicEditor({ graph, onChange, widgets = [], statevars,
|
|||||||
{onStateVarsChange && (
|
{onStateVarsChange && (
|
||||||
<LocalVars statevars={statevars ?? []} onChange={onStateVarsChange} />
|
<LocalVars statevars={statevars ?? []} onChange={onStateVarsChange} />
|
||||||
)}
|
)}
|
||||||
<datalist id="flow-array-names">
|
|
||||||
{arrayNames.map(a => <option key={a} value={a} />)}
|
|
||||||
</datalist>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flow-canvas" ref={canvasRef}
|
<div class="flow-canvas" ref={canvasRef}
|
||||||
@@ -1033,25 +1016,10 @@ export default function LogicEditor({ graph, onChange, widgets = [], statevars,
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selected.kind === 'action.accumulate' && (
|
|
||||||
<Fragment>
|
|
||||||
<div class="wizard-field">
|
|
||||||
<label>Array name</label>
|
|
||||||
<input class="prop-input" value={selected.params.array ?? ''} list="flow-array-names"
|
|
||||||
placeholder="e.g. data"
|
|
||||||
onInput={(e) => patchParams(selected.id, { array: (e.target as HTMLInputElement).value })} />
|
|
||||||
</div>
|
|
||||||
<ExprField label="Value (expression)" value={selected.params.expr ?? ''}
|
|
||||||
onChange={(v) => patchParams(selected.id, { expr: v })}
|
|
||||||
onInsert={(ds2, sig) => insertRef(selected.id, 'expr', ds2, sig)}
|
|
||||||
allSignals={allSignalOptions()} onOpenSignals={openAllSignals}
|
|
||||||
hint="Appends this value (with a timestamp) to the array. e.g. {stub:level}, or {sys:time} for the clock." />
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selected.kind === 'action.export' && (
|
{selected.kind === 'action.export' && (
|
||||||
<ExportEditor
|
<ExportEditor
|
||||||
columns={parseExportColumns(selected)}
|
columns={parseExportColumns(selected)}
|
||||||
|
arrayLocals={(statevars ?? []).filter(v => v.type === 'array').map(v => v.name)}
|
||||||
align={selected.params.align ?? 'common'}
|
align={selected.params.align ?? 'common'}
|
||||||
filename={selected.params.filename ?? ''}
|
filename={selected.params.filename ?? ''}
|
||||||
onColumns={(cols) => patchParams(selected.id, { columns: JSON.stringify(cols) })}
|
onColumns={(cols) => patchParams(selected.id, { columns: JSON.stringify(cols) })}
|
||||||
@@ -1059,16 +1027,6 @@ export default function LogicEditor({ graph, onChange, widgets = [], statevars,
|
|||||||
onFilename={(f) => patchParams(selected.id, { filename: f })} />
|
onFilename={(f) => patchParams(selected.id, { filename: f })} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selected.kind === 'action.clear' && (
|
|
||||||
<div class="wizard-field">
|
|
||||||
<label>Array name</label>
|
|
||||||
<input class="prop-input" value={selected.params.array ?? ''} list="flow-array-names"
|
|
||||||
placeholder="e.g. data"
|
|
||||||
onInput={(e) => patchParams(selected.id, { array: (e.target as HTMLInputElement).value })} />
|
|
||||||
<p class="hint">Empties the named array (e.g. to start a fresh capture).</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(selected.kind === 'action.array.push' || selected.kind === 'action.array.set' ||
|
{(selected.kind === 'action.array.push' || selected.kind === 'action.array.set' ||
|
||||||
selected.kind === 'action.array.remove' || selected.kind === 'action.array.pop' ||
|
selected.kind === 'action.array.remove' || selected.kind === 'action.array.pop' ||
|
||||||
selected.kind === 'action.array.clear') && (() => {
|
selected.kind === 'action.array.clear') && (() => {
|
||||||
@@ -1268,9 +1226,9 @@ function ExprField({ label, value, onChange, onInsert, allSignals, onOpenSignals
|
|||||||
|
|
||||||
interface ExportColumn { array: string; label: string; }
|
interface ExportColumn { array: string; label: string; }
|
||||||
|
|
||||||
// The columns configured on an action.export node. Reads the JSON `columns`
|
// The columns configured on an action.export node, read from the JSON `columns`
|
||||||
// param; falls back to the legacy single `array` param (so old panels migrate
|
// param. Legacy single-`array` nodes are migrated to this form on load (see
|
||||||
// transparently the first time the node is edited).
|
// xml.ts migrateLegacyArrayNode), so only the JSON form is read here.
|
||||||
function parseExportColumns(n: LogicNode): ExportColumn[] {
|
function parseExportColumns(n: LogicNode): ExportColumn[] {
|
||||||
const raw = (n.params.columns ?? '').trim();
|
const raw = (n.params.columns ?? '').trim();
|
||||||
if (raw) {
|
if (raw) {
|
||||||
@@ -1281,8 +1239,7 @@ function parseExportColumns(n: LogicNode): ExportColumn[] {
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
const a = (n.params.array ?? '').trim();
|
return [{ array: '', label: '' }];
|
||||||
return [{ array: a, label: '' }];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALIGN_OPTS: Array<{ value: string; label: string; hint: string }> = [
|
const ALIGN_OPTS: Array<{ value: string; label: string; hint: string }> = [
|
||||||
@@ -1291,8 +1248,9 @@ const ALIGN_OPTS: Array<{ value: string; label: string; hint: string }> = [
|
|||||||
{ value: 'interpolate', label: 'Interpolate', hint: 'Union of all timestamps; missing cells linearly interpolated.' },
|
{ value: 'interpolate', label: 'Interpolate', hint: 'Union of all timestamps; missing cells linearly interpolated.' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function ExportEditor({ columns, align, filename, onColumns, onAlign, onFilename }: {
|
function ExportEditor({ columns, arrayLocals, align, filename, onColumns, onAlign, onFilename }: {
|
||||||
columns: ExportColumn[];
|
columns: ExportColumn[];
|
||||||
|
arrayLocals: string[];
|
||||||
align: string;
|
align: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
onColumns: (cols: ExportColumn[]) => void;
|
onColumns: (cols: ExportColumn[]) => void;
|
||||||
@@ -1306,12 +1264,15 @@ function ExportEditor({ columns, align, filename, onColumns, onAlign, onFilename
|
|||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div class="wizard-field">
|
<div class="wizard-field">
|
||||||
<label>Columns (arrays)</label>
|
<label>Columns (array local variables)</label>
|
||||||
{columns.map((c, i) => (
|
{columns.map((c, i) => (
|
||||||
<div key={i} class="flow-row-edit">
|
<div key={i} class="flow-row-edit">
|
||||||
<input class="prop-input" value={c.array} list="flow-array-names"
|
<select class="prop-select" value={c.array}
|
||||||
placeholder="array name"
|
onChange={(e) => patch(i, { array: (e.target as HTMLSelectElement).value })}>
|
||||||
onInput={(e) => patch(i, { array: (e.target as HTMLInputElement).value })} />
|
<option value="">— select array —</option>
|
||||||
|
{c.array && !arrayLocals.includes(c.array) && <option value={c.array}>{c.array}</option>}
|
||||||
|
{arrayLocals.map(name => <option key={name} value={name}>{name}</option>)}
|
||||||
|
</select>
|
||||||
<input class="prop-input" value={c.label}
|
<input class="prop-input" value={c.label}
|
||||||
placeholder="column label (optional)"
|
placeholder="column label (optional)"
|
||||||
onInput={(e) => patch(i, { label: (e.target as HTMLInputElement).value })} />
|
onInput={(e) => patch(i, { label: (e.target as HTMLInputElement).value })} />
|
||||||
@@ -1321,6 +1282,9 @@ function ExportEditor({ columns, align, filename, onColumns, onAlign, onFilename
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<button class="panel-btn flow-row-add" onClick={() => onColumns([...columns, { array: '', label: '' }])}>+ Add column</button>
|
<button class="panel-btn flow-row-add" onClick={() => onColumns([...columns, { array: '', label: '' }])}>+ Add column</button>
|
||||||
|
{arrayLocals.length === 0 && (
|
||||||
|
<p class="hint">No array locals declared yet — add one in the Local vars panel.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{columns.length > 1 && (
|
{columns.length > 1 && (
|
||||||
<div class="wizard-field">
|
<div class="wizard-field">
|
||||||
@@ -1455,13 +1419,11 @@ function nodeSummary(n: LogicNode): string {
|
|||||||
: `while ${n.params.cond || '?'}`;
|
: `while ${n.params.cond || '?'}`;
|
||||||
case 'action.write': return `${n.params.target || '?'} = ${n.params.expr || ''}`;
|
case 'action.write': return `${n.params.target || '?'} = ${n.params.expr || ''}`;
|
||||||
case 'action.delay': return `wait ${n.params.ms || '0'} ms`;
|
case 'action.delay': return `wait ${n.params.ms || '0'} ms`;
|
||||||
case 'action.accumulate': return `${n.params.array || '?'} ← ${n.params.expr || ''}`;
|
|
||||||
case 'action.export': {
|
case 'action.export': {
|
||||||
const cols = parseExportColumns(n).filter(c => c.array);
|
const cols = parseExportColumns(n).filter(c => c.array);
|
||||||
const names = cols.map(c => c.label || c.array).join(', ');
|
const names = cols.map(c => c.label || c.array).join(', ');
|
||||||
return `export ${names || '?'} → csv`;
|
return `export ${names || '?'} → csv`;
|
||||||
}
|
}
|
||||||
case 'action.clear': return `clear ${n.params.array || '?'}`;
|
|
||||||
case 'action.array.push': return `push ${n.params.expr || ''} → ${n.params.array || '?'}`;
|
case 'action.array.push': return `push ${n.params.expr || ''} → ${n.params.array || '?'}`;
|
||||||
case 'action.array.set': return `${n.params.array || '?'}[${n.params.index || '?'}] = ${n.params.expr || ''}`;
|
case 'action.array.set': return `${n.params.array || '?'}[${n.params.index || '?'}] = ${n.params.expr || ''}`;
|
||||||
case 'action.array.remove': return `remove ${n.params.array || '?'}[${n.params.index || '?'}]`;
|
case 'action.array.remove': return `remove ${n.params.array || '?'}[${n.params.index || '?'}]`;
|
||||||
|
|||||||
+7
-38
@@ -12,8 +12,8 @@
|
|||||||
// flow.loop — repeats the 'body' port (count / while, capped), then 'done'.
|
// flow.loop — repeats the 'body' port (count / while, capped), then 'done'.
|
||||||
// action.write— evaluates `expr` and writes the result to `target`.
|
// action.write— evaluates `expr` and writes the result to `target`.
|
||||||
// action.delay— awaits `ms` before continuing.
|
// action.delay— awaits `ms` before continuing.
|
||||||
// action.accumulate / export / clear — collect expression values into a named
|
// action.array.* — push/set/remove/pop/clear on a named array local variable.
|
||||||
// in-memory array and download it as CSV.
|
// action.export— download named array local variables as CSV (one col each).
|
||||||
// action.log — logs an expression value to the console.
|
// action.log — logs an expression value to the console.
|
||||||
//
|
//
|
||||||
// Expressions reference signals inline as {ds:name} and panel-local vars as
|
// Expressions reference signals inline as {ds:name} and panel-local vars as
|
||||||
@@ -398,7 +398,6 @@ export class LogicEngine {
|
|||||||
case 'flow.if': collectRefs(node.params.cond ?? '').forEach(want); break;
|
case 'flow.if': collectRefs(node.params.cond ?? '').forEach(want); break;
|
||||||
case 'flow.loop': if ((node.params.mode ?? 'count') === 'while') collectRefs(node.params.cond ?? '').forEach(want); break;
|
case 'flow.loop': if ((node.params.mode ?? 'count') === 'while') collectRefs(node.params.cond ?? '').forEach(want); break;
|
||||||
case 'action.write':
|
case 'action.write':
|
||||||
case 'action.accumulate':
|
|
||||||
case 'action.array.push':
|
case 'action.array.push':
|
||||||
case 'action.log': collectRefs(node.params.expr ?? '').forEach(want); break;
|
case 'action.log': collectRefs(node.params.expr ?? '').forEach(want); break;
|
||||||
case 'action.array.set':
|
case 'action.array.set':
|
||||||
@@ -686,35 +685,12 @@ export class LogicEngine {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Legacy array nodes (aliased to new implementations) ─────────────────
|
|
||||||
|
|
||||||
case 'action.accumulate': {
|
|
||||||
// Legacy alias → action.array.push (backs into array locals).
|
|
||||||
const name = (node.params.array ?? '').trim();
|
|
||||||
const val = evalValue(node.params.expr ?? '', ctx.resolve);
|
|
||||||
this.markActive(node.id, val);
|
|
||||||
if (name) writeLocalState(name, [...curArray(name), val]);
|
|
||||||
await this.follow(node.id, 'out', ctx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'action.export': {
|
case 'action.export': {
|
||||||
if (!this.dryRun) this.exportArrays(this.exportColumns(node), node.params.filename ?? '');
|
if (!this.dryRun) this.exportArrays(this.exportColumns(node), node.params.filename ?? '');
|
||||||
await this.follow(node.id, 'out', ctx);
|
await this.follow(node.id, 'out', ctx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'action.clear': {
|
|
||||||
// Legacy alias → action.array.clear (backed by array locals).
|
|
||||||
const name = (node.params.array ?? '').trim();
|
|
||||||
if (name) {
|
|
||||||
const sv = declaredVar(name);
|
|
||||||
writeLocalState(name, sv ? applySizing([], sv) : []);
|
|
||||||
}
|
|
||||||
await this.follow(node.id, 'out', ctx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'action.log': {
|
case 'action.log': {
|
||||||
const val = evalExpr(node.params.expr ?? '', ctx.resolve);
|
const val = evalExpr(node.params.expr ?? '', ctx.resolve);
|
||||||
const label = (node.params.label ?? '').trim();
|
const label = (node.params.label ?? '').trim();
|
||||||
@@ -783,8 +759,9 @@ export class LogicEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The columns an action.export node should emit. Reads the JSON `columns`
|
// The columns an action.export node should emit, read from the JSON `columns`
|
||||||
// param (a list of { array, label }); falls back to the legacy single `array`.
|
// param (a list of { array, label }). Legacy single-`array` nodes are migrated
|
||||||
|
// to this form on load (see xml.ts migrateLegacyArrayNode).
|
||||||
private exportColumns(node: LogicNode): Array<{ array: string; label: string }> {
|
private exportColumns(node: LogicNode): Array<{ array: string; label: string }> {
|
||||||
const raw = (node.params.columns ?? '').trim();
|
const raw = (node.params.columns ?? '').trim();
|
||||||
if (raw) {
|
if (raw) {
|
||||||
@@ -797,8 +774,7 @@ export class LogicEngine {
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
const a = (node.params.array ?? '').trim();
|
return [];
|
||||||
return a ? [{ array: a, label: a }] : [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dump one or more named array locals to a CSV the browser downloads.
|
// Dump one or more named array locals to a CSV the browser downloads.
|
||||||
@@ -928,20 +904,13 @@ export function ensureArrayDecls(graph: LogicGraph | undefined, vars: StateVar[]
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
for (const n of graph.nodes) {
|
for (const n of graph.nodes) {
|
||||||
if (
|
if (n.kind.startsWith('action.array.')) {
|
||||||
n.kind === 'action.accumulate' ||
|
|
||||||
n.kind === 'action.clear' ||
|
|
||||||
n.kind.startsWith('action.array.')
|
|
||||||
) {
|
|
||||||
need((n.params.array ?? '').trim());
|
need((n.params.array ?? '').trim());
|
||||||
}
|
}
|
||||||
if (n.kind === 'action.export') {
|
if (n.kind === 'action.export') {
|
||||||
try {
|
try {
|
||||||
(JSON.parse(n.params.columns || '[]') as any[]).forEach(c => need(String(c?.array ?? '').trim()));
|
(JSON.parse(n.params.columns || '[]') as any[]).forEach(c => need(String(c?.array ?? '').trim()));
|
||||||
} catch {}
|
} catch {}
|
||||||
// Also handle legacy single `array` param.
|
|
||||||
const a = (n.params.array ?? '').trim();
|
|
||||||
if (a) need(a);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
@@ -115,13 +115,14 @@ export interface StateVar {
|
|||||||
// action.write — evaluate `expr` and write the result to `target`
|
// action.write — evaluate `expr` and write the result to `target`
|
||||||
// ("ds:name" or a bare local var) (params: target, expr).
|
// ("ds:name" or a bare local var) (params: target, expr).
|
||||||
// action.delay — pause `ms` before continuing downstream (params: ms).
|
// action.delay — pause `ms` before continuing downstream (params: ms).
|
||||||
// action.accumulate — evaluate `expr` and append the value (with a timestamp)
|
// action.export — download one or more named array local variables as a
|
||||||
// to the named in-memory data array (params: array, expr).
|
// CSV file, one column per array (params: columns = JSON
|
||||||
// action.export — download one or more named arrays as a CSV file, one
|
// list of {array,label}, align 'common'|'any'|'interpolate',
|
||||||
// column per array (params: columns = JSON list of
|
|
||||||
// {array,label}, align 'common'|'any'|'interpolate',
|
|
||||||
// filename). Legacy single-array nodes use `array`.
|
// filename). Legacy single-array nodes use `array`.
|
||||||
// action.clear — empty the named array (params: array).
|
// action.array.* — push/set/remove/pop/clear on a named array local variable
|
||||||
|
// (params: array, plus index/expr where relevant). The old
|
||||||
|
// action.accumulate / action.clear kinds are migrated to
|
||||||
|
// action.array.push / action.array.clear on load.
|
||||||
// action.log — evaluate `expr` and log it to the browser console for
|
// action.log — evaluate `expr` and log it to the browser console for
|
||||||
// debugging a flow (params: expr, label).
|
// debugging a flow (params: expr, label).
|
||||||
// action.widget — drive a panel widget by id (params: widget, op). `op` is
|
// action.widget — drive a panel widget by id (params: widget, op). `op` is
|
||||||
@@ -156,9 +157,7 @@ export type LogicNodeKind =
|
|||||||
| 'flow.loop'
|
| 'flow.loop'
|
||||||
| 'action.write'
|
| 'action.write'
|
||||||
| 'action.delay'
|
| 'action.delay'
|
||||||
| 'action.accumulate'
|
|
||||||
| 'action.export'
|
| 'action.export'
|
||||||
| 'action.clear'
|
|
||||||
| 'action.log'
|
| 'action.log'
|
||||||
| 'action.widget'
|
| 'action.widget'
|
||||||
| 'action.dialog.info'
|
| 'action.dialog.info'
|
||||||
|
|||||||
+28
-1
@@ -128,6 +128,31 @@ function parseLayout(el: Element): PlotLayout {
|
|||||||
return { type: 'split', dir, ratio: isNaN(ratio) ? 0.5 : ratio, a, b };
|
return { type: 'split', dir, ratio: isNaN(ratio) ? 0.5 : ratio, a, b };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rewrite a legacy array node kind to its current equivalent, mutating `params`
|
||||||
|
// in place. The old action.accumulate / action.clear kinds (and the legacy
|
||||||
|
// single-`array` form of action.export) predate the typed array local-variable
|
||||||
|
// nodes; old panels are migrated transparently on load so the editor only ever
|
||||||
|
// sees the current kinds.
|
||||||
|
function migrateLegacyArrayNode(
|
||||||
|
kind: string,
|
||||||
|
params: Record<string, string>,
|
||||||
|
setKind: (k: LogicGraph['nodes'][number]['kind']) => void,
|
||||||
|
): void {
|
||||||
|
if (kind === 'action.accumulate') {
|
||||||
|
setKind('action.array.push'); // same params: { array, expr }
|
||||||
|
} else if (kind === 'action.clear') {
|
||||||
|
setKind('action.array.clear'); // same params: { array }
|
||||||
|
} else if (kind === 'action.export') {
|
||||||
|
// Legacy single-array export had only an `array` param; fold it into the
|
||||||
|
// current JSON `columns` form so the editor never sees the legacy field.
|
||||||
|
const a = (params.array ?? '').trim();
|
||||||
|
if (a && !(params.columns ?? '').trim()) {
|
||||||
|
params.columns = JSON.stringify([{ array: a, label: '' }]);
|
||||||
|
}
|
||||||
|
delete params.array;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Parse a <logic> element into a LogicGraph (nodes + wires). */
|
/** Parse a <logic> element into a LogicGraph (nodes + wires). */
|
||||||
function parseLogic(logicEl: Element): LogicGraph {
|
function parseLogic(logicEl: Element): LogicGraph {
|
||||||
const nodes: LogicGraph['nodes'] = [];
|
const nodes: LogicGraph['nodes'] = [];
|
||||||
@@ -160,9 +185,11 @@ function parseLogic(logicEl: Element): LogicGraph {
|
|||||||
const value = child.getAttribute('value');
|
const value = child.getAttribute('value');
|
||||||
if (key !== null && value !== null) params[key] = value;
|
if (key !== null && value !== null) params[key] = value;
|
||||||
}
|
}
|
||||||
|
let kind = (el.getAttribute('kind') ?? 'action.write') as LogicGraph['nodes'][number]['kind'];
|
||||||
|
migrateLegacyArrayNode(kind, params, (k) => { kind = k; });
|
||||||
nodes.push({
|
nodes.push({
|
||||||
id,
|
id,
|
||||||
kind: (el.getAttribute('kind') ?? 'action.write') as LogicGraph['nodes'][number]['kind'],
|
kind,
|
||||||
x: parseFloat(el.getAttribute('x') ?? '0'),
|
x: parseFloat(el.getAttribute('x') ?? '0'),
|
||||||
y: parseFloat(el.getAttribute('y') ?? '0'),
|
y: parseFloat(el.getAttribute('y') ?? '0'),
|
||||||
params,
|
params,
|
||||||
|
|||||||
Reference in New Issue
Block a user