@object-ui/app-shell 7.0.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +281 -0
- package/dist/console/AppContent.js +14 -2
- package/dist/console/ai/AiChatPage.js +11 -7
- package/dist/console/ai/LiveCanvas.d.ts +8 -2
- package/dist/console/ai/LiveCanvas.js +6 -4
- package/dist/hooks/useChatConversation.d.ts +30 -0
- package/dist/hooks/useChatConversation.js +63 -0
- package/dist/hooks/useConsoleActionRuntime.js +6 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -1
- package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
- package/dist/layout/ConsoleFloatingChatbot.js +25 -8
- package/dist/layout/ContextSelectors.js +59 -35
- package/dist/layout/agentPicker.d.ts +56 -0
- package/dist/layout/agentPicker.js +40 -0
- package/dist/preview/CommitTimeline.d.ts +15 -0
- package/dist/preview/CommitTimeline.js +82 -0
- package/dist/preview/UnpublishedAppBar.js +11 -7
- package/dist/preview/commitHistory.d.ts +28 -0
- package/dist/preview/commitHistory.js +48 -0
- package/dist/providers/MetadataProvider.js +9 -0
- package/dist/views/FlowRunner.d.ts +2 -30
- package/dist/views/FlowRunner.js +18 -50
- package/dist/views/ScreenView.d.ts +70 -0
- package/dist/views/ScreenView.js +73 -0
- package/dist/views/metadata-admin/DirectoryPage.js +2 -14
- package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
- package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
- package/dist/views/metadata-admin/PackagesPage.js +9 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +47 -20
- package/dist/views/metadata-admin/ResourceListPage.js +8 -16
- package/dist/views/metadata-admin/StudioHomePage.js +6 -14
- package/dist/views/metadata-admin/anchors.js +20 -2
- package/dist/views/metadata-admin/i18n.js +88 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +2 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +122 -8
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +84 -3
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +67 -2
- package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
- package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
- package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +97 -0
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +46 -1
- package/dist/views/metadata-admin/issuePath.d.ts +22 -0
- package/dist/views/metadata-admin/issuePath.js +65 -0
- package/dist/views/metadata-admin/package-scope.d.ts +26 -0
- package/dist/views/metadata-admin/package-scope.js +43 -0
- package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +7 -1
- package/dist/views/metadata-admin/previews/FlowCanvas.js +104 -16
- package/dist/views/metadata-admin/previews/FlowPreview.js +31 -3
- package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
- package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
- package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
- package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +21 -6
- package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
- package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
- package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
- package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +11 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +72 -0
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
- package/package.json +38 -38
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* screen-spec — pure helpers that map a flow `screen` node's authored `config`
|
|
3
|
+
* onto the runtime `ScreenSpec` (the contract {@link ScreenView} renders), plus
|
|
4
|
+
* `{var}` interpolation for the title/description and `visibleWhen` field
|
|
5
|
+
* gating. Kept framework-free so {@link ScreenPreview} stays a thin component
|
|
6
|
+
* and these stay unit-testable.
|
|
7
|
+
*/
|
|
8
|
+
import type { ScreenSpec } from '../../ScreenView';
|
|
9
|
+
/** Minimal node shape the preview needs (id + authored config). */
|
|
10
|
+
export interface ScreenPreviewNode {
|
|
11
|
+
id: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
config?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Interpolate `{var}` references, mirroring the simulator's `{var}` syntax
|
|
17
|
+
* (flow-simulator.ts). Known vars are substituted; unknown refs stay literal so
|
|
18
|
+
* the author still sees the dependency in the design preview.
|
|
19
|
+
*/
|
|
20
|
+
export declare function interpolate(text: string | undefined, vars: Record<string, unknown> | undefined): string;
|
|
21
|
+
/**
|
|
22
|
+
* A field's `visibleWhen` gate, evaluated against the current variables using
|
|
23
|
+
* the SAME evaluator the simulator uses for edge conditions (so the preview
|
|
24
|
+
* agrees with the simulator). Real metadata mixes `{var}` and bare-var styles
|
|
25
|
+
* (e.g. `{createOpportunity} == true`, `stage == "review"`), so brace
|
|
26
|
+
* placeholders are normalised to bare identifiers first.
|
|
27
|
+
*
|
|
28
|
+
* Fail-OPEN: a missing condition, an unparseable one, or one that references a
|
|
29
|
+
* not-yet-set variable (the inspector has no run state) keeps the field
|
|
30
|
+
* visible — the design preview never hides a configured field just because it
|
|
31
|
+
* lacks the data to decide. With live run state (the simulator) it gates
|
|
32
|
+
* faithfully: `createOpportunity == false` hides the field.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isFieldVisibleWhen(visibleWhen: unknown, variables: Record<string, unknown> | undefined): boolean;
|
|
35
|
+
/** Count of authored field rows hidden by their `visibleWhen` against `variables`. */
|
|
36
|
+
export declare function hiddenFieldCount(node: ScreenPreviewNode, variables: Record<string, unknown> | undefined): number;
|
|
37
|
+
/**
|
|
38
|
+
* Map a screen node's authored `config` onto the runtime `ScreenSpec` — the
|
|
39
|
+
* same keys the engine's `screen` executor reads (title / description / fields
|
|
40
|
+
* / objectName / mode / defaults / idVariable). `fields` are gated by their
|
|
41
|
+
* `visibleWhen` against `variables` (omit `variables` to keep every field).
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildScreenSpec(node: ScreenPreviewNode, variables?: Record<string, unknown>): ScreenSpec;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
import { evalCondition } from './simulator/flow-sim-validate';
|
|
3
|
+
/**
|
|
4
|
+
* Interpolate `{var}` references, mirroring the simulator's `{var}` syntax
|
|
5
|
+
* (flow-simulator.ts). Known vars are substituted; unknown refs stay literal so
|
|
6
|
+
* the author still sees the dependency in the design preview.
|
|
7
|
+
*/
|
|
8
|
+
export function interpolate(text, vars) {
|
|
9
|
+
if (!text)
|
|
10
|
+
return '';
|
|
11
|
+
if (!vars)
|
|
12
|
+
return text;
|
|
13
|
+
return text.replace(/\{([^{}]+)\}/g, (m, k) => {
|
|
14
|
+
const v = vars[String(k).trim()];
|
|
15
|
+
return v === undefined || v === null ? m : String(v);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A field's `visibleWhen` gate, evaluated against the current variables using
|
|
20
|
+
* the SAME evaluator the simulator uses for edge conditions (so the preview
|
|
21
|
+
* agrees with the simulator). Real metadata mixes `{var}` and bare-var styles
|
|
22
|
+
* (e.g. `{createOpportunity} == true`, `stage == "review"`), so brace
|
|
23
|
+
* placeholders are normalised to bare identifiers first.
|
|
24
|
+
*
|
|
25
|
+
* Fail-OPEN: a missing condition, an unparseable one, or one that references a
|
|
26
|
+
* not-yet-set variable (the inspector has no run state) keeps the field
|
|
27
|
+
* visible — the design preview never hides a configured field just because it
|
|
28
|
+
* lacks the data to decide. With live run state (the simulator) it gates
|
|
29
|
+
* faithfully: `createOpportunity == false` hides the field.
|
|
30
|
+
*/
|
|
31
|
+
export function isFieldVisibleWhen(visibleWhen, variables) {
|
|
32
|
+
if (typeof visibleWhen !== 'string' || !visibleWhen.trim())
|
|
33
|
+
return true;
|
|
34
|
+
if (!variables)
|
|
35
|
+
return true;
|
|
36
|
+
const normalized = visibleWhen.replace(/\{([\w.]+)\}/g, '$1');
|
|
37
|
+
const { result, error } = evalCondition(normalized, variables);
|
|
38
|
+
return error ? true : result;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Coerce the authored `config.fields` rows into runtime `ScreenFieldSpec`s,
|
|
42
|
+
* dropping any whose `visibleWhen` evaluates false against `variables` — exactly
|
|
43
|
+
* what the runtime `screen` executor emits (it filters server-side before
|
|
44
|
+
* sending the ScreenSpec).
|
|
45
|
+
*/
|
|
46
|
+
function toScreenFields(raw, variables) {
|
|
47
|
+
if (!Array.isArray(raw))
|
|
48
|
+
return [];
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const f of raw) {
|
|
51
|
+
if (!f || typeof f !== 'object')
|
|
52
|
+
continue;
|
|
53
|
+
const row = f;
|
|
54
|
+
if (typeof row.name !== 'string' || !row.name)
|
|
55
|
+
continue;
|
|
56
|
+
if (!isFieldVisibleWhen(row.visibleWhen, variables))
|
|
57
|
+
continue;
|
|
58
|
+
out.push({
|
|
59
|
+
name: row.name,
|
|
60
|
+
label: typeof row.label === 'string' ? row.label : undefined,
|
|
61
|
+
type: typeof row.type === 'string' ? row.type : undefined,
|
|
62
|
+
required: row.required === true,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/** Count of authored field rows hidden by their `visibleWhen` against `variables`. */
|
|
68
|
+
export function hiddenFieldCount(node, variables) {
|
|
69
|
+
const raw = node.config?.fields;
|
|
70
|
+
if (!Array.isArray(raw))
|
|
71
|
+
return 0;
|
|
72
|
+
let hidden = 0;
|
|
73
|
+
for (const f of raw) {
|
|
74
|
+
if (!f || typeof f !== 'object')
|
|
75
|
+
continue;
|
|
76
|
+
const row = f;
|
|
77
|
+
if (typeof row.name !== 'string' || !row.name)
|
|
78
|
+
continue;
|
|
79
|
+
if (!isFieldVisibleWhen(row.visibleWhen, variables))
|
|
80
|
+
hidden++;
|
|
81
|
+
}
|
|
82
|
+
return hidden;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Map a screen node's authored `config` onto the runtime `ScreenSpec` — the
|
|
86
|
+
* same keys the engine's `screen` executor reads (title / description / fields
|
|
87
|
+
* / objectName / mode / defaults / idVariable). `fields` are gated by their
|
|
88
|
+
* `visibleWhen` against `variables` (omit `variables` to keep every field).
|
|
89
|
+
*/
|
|
90
|
+
export function buildScreenSpec(node, variables) {
|
|
91
|
+
const c = (node.config && typeof node.config === 'object' ? node.config : {});
|
|
92
|
+
const objectName = typeof c.objectName === 'string' && c.objectName ? c.objectName : undefined;
|
|
93
|
+
const mode = c.mode === 'edit' ? 'edit' : c.mode === 'create' ? 'create' : undefined;
|
|
94
|
+
const defaults = c.defaults && typeof c.defaults === 'object' && !Array.isArray(c.defaults)
|
|
95
|
+
? c.defaults
|
|
96
|
+
: undefined;
|
|
97
|
+
return {
|
|
98
|
+
nodeId: node.id,
|
|
99
|
+
title: typeof c.title === 'string' ? c.title : undefined,
|
|
100
|
+
description: typeof c.description === 'string' ? c.description : undefined,
|
|
101
|
+
fields: toScreenFields(c.fields, variables),
|
|
102
|
+
kind: objectName ? 'object-form' : 'fields',
|
|
103
|
+
objectName,
|
|
104
|
+
mode,
|
|
105
|
+
defaults,
|
|
106
|
+
idVariable: typeof c.idVariable === 'string' ? c.idVariable : undefined,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -28,6 +28,12 @@ export interface SimEdge {
|
|
|
28
28
|
};
|
|
29
29
|
isDefault?: boolean;
|
|
30
30
|
label?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Connection type (FlowEdgeSchema). A `'back'` edge is an ADR-0044 declared
|
|
33
|
+
* back-edge: traversed normally at run time, but excluded from DAG cycle
|
|
34
|
+
* validation so a revise/rework loop can re-enter an earlier node.
|
|
35
|
+
*/
|
|
36
|
+
type?: string;
|
|
31
37
|
}
|
|
32
38
|
export type SimStatus = 'idle' | 'running' | 'paused' | 'done' | 'error';
|
|
33
39
|
/** One evaluated outgoing edge of a decision (kept for the debug timeline). */
|
|
@@ -76,6 +82,11 @@ export interface Diagnostic {
|
|
|
76
82
|
level: DiagnosticLevel;
|
|
77
83
|
nodeId?: string;
|
|
78
84
|
message: string;
|
|
85
|
+
/**
|
|
86
|
+
* For a cycle error: the node path that closes the loop (e.g. `['a','b','a']`),
|
|
87
|
+
* so the designer can paint the offending edges/nodes inline on the canvas.
|
|
88
|
+
*/
|
|
89
|
+
cycle?: string[];
|
|
79
90
|
}
|
|
80
91
|
export interface FlowValidation {
|
|
81
92
|
errors: Diagnostic[];
|
|
@@ -4,5 +4,12 @@ export declare function evalCondition(expr: string, variables: Record<string, un
|
|
|
4
4
|
result: boolean;
|
|
5
5
|
error?: string;
|
|
6
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* Find a directed cycle in `edges` over `nodeIds`, returned as the node path
|
|
9
|
+
* that closes the loop (e.g. `['a','b','a']`), or `null` when the graph is a
|
|
10
|
+
* DAG. Iterative DFS with a recursion-stack colour map; the first cycle found
|
|
11
|
+
* wins (enough to report — the author fixes one at a time).
|
|
12
|
+
*/
|
|
13
|
+
export declare function findCycle(nodeIds: string[], edges: SimEdge[]): string[] | null;
|
|
7
14
|
/** Static structural checks; `errors` block Run, `warnings` are advisory. */
|
|
8
15
|
export declare function validateFlowDraft(nodes: SimNode[], edges: SimEdge[]): FlowValidation;
|
|
@@ -28,6 +28,63 @@ export function evalCondition(expr, variables) {
|
|
|
28
28
|
return { result: false, error: err.message || 'Evaluation failed.' };
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Find a directed cycle in `edges` over `nodeIds`, returned as the node path
|
|
33
|
+
* that closes the loop (e.g. `['a','b','a']`), or `null` when the graph is a
|
|
34
|
+
* DAG. Iterative DFS with a recursion-stack colour map; the first cycle found
|
|
35
|
+
* wins (enough to report — the author fixes one at a time).
|
|
36
|
+
*/
|
|
37
|
+
export function findCycle(nodeIds, edges) {
|
|
38
|
+
const adj = new Map();
|
|
39
|
+
for (const id of nodeIds)
|
|
40
|
+
adj.set(id, []);
|
|
41
|
+
for (const e of edges) {
|
|
42
|
+
if (adj.has(e.source) && adj.has(e.target))
|
|
43
|
+
adj.get(e.source).push(e.target);
|
|
44
|
+
}
|
|
45
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
46
|
+
const color = new Map(nodeIds.map((id) => [id, WHITE]));
|
|
47
|
+
const stack = [];
|
|
48
|
+
const visit = (start) => {
|
|
49
|
+
// Explicit stack of {node, next-child-index} frames so a deep graph can't
|
|
50
|
+
// blow the JS call stack.
|
|
51
|
+
const frames = [{ id: start, i: 0 }];
|
|
52
|
+
color.set(start, GRAY);
|
|
53
|
+
stack.push(start);
|
|
54
|
+
while (frames.length) {
|
|
55
|
+
const frame = frames[frames.length - 1];
|
|
56
|
+
const children = adj.get(frame.id) ?? [];
|
|
57
|
+
if (frame.i < children.length) {
|
|
58
|
+
const next = children[frame.i++];
|
|
59
|
+
const c = color.get(next);
|
|
60
|
+
if (c === GRAY) {
|
|
61
|
+
// Back into the active path → cycle. Slice from `next` to close it.
|
|
62
|
+
const from = stack.indexOf(next);
|
|
63
|
+
return [...stack.slice(from), next];
|
|
64
|
+
}
|
|
65
|
+
if (c === WHITE) {
|
|
66
|
+
color.set(next, GRAY);
|
|
67
|
+
stack.push(next);
|
|
68
|
+
frames.push({ id: next, i: 0 });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
color.set(frame.id, BLACK);
|
|
73
|
+
stack.pop();
|
|
74
|
+
frames.pop();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
};
|
|
79
|
+
for (const id of nodeIds) {
|
|
80
|
+
if (color.get(id) === WHITE) {
|
|
81
|
+
const cycle = visit(id);
|
|
82
|
+
if (cycle)
|
|
83
|
+
return cycle;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
31
88
|
/** Static structural checks; `errors` block Run, `warnings` are advisory. */
|
|
32
89
|
export function validateFlowDraft(nodes, edges) {
|
|
33
90
|
const errors = [];
|
|
@@ -109,5 +166,20 @@ export function validateFlowDraft(nodes, edges) {
|
|
|
109
166
|
}
|
|
110
167
|
}
|
|
111
168
|
}
|
|
169
|
+
// DAG-modulo-back-edges (ADR-0044): the engine requires the flow graph MINUS
|
|
170
|
+
// declared back-edges to be acyclic. A declared revise loop (its closing edge
|
|
171
|
+
// marked `type: 'back'`) is excluded and passes; any *unmarked* cycle is an
|
|
172
|
+
// error — the author must opt in, edge by edge, exactly as `registerFlow`
|
|
173
|
+
// enforces server-side.
|
|
174
|
+
const forwardEdges = edges.filter((e) => e.type !== 'back');
|
|
175
|
+
const cycle = findCycle(ids.filter((id) => !!id), forwardEdges);
|
|
176
|
+
if (cycle) {
|
|
177
|
+
errors.push({
|
|
178
|
+
level: 'error',
|
|
179
|
+
nodeId: cycle[0],
|
|
180
|
+
cycle,
|
|
181
|
+
message: `Cycle detected (${cycle.join(' → ')}). Mark the connection that closes the loop as a back-edge (Connection type → Back-edge) to declare an intentional revise/rework loop.`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
112
184
|
return { errors, warnings, startNodeId };
|
|
113
185
|
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Supported faithfully: start, decision (edge-first CEL routing), the CRUD /
|
|
6
6
|
* get / http / connector / script(notification|code) side-effects (MOCKED), and
|
|
7
|
-
* end. `wait` and `
|
|
7
|
+
* end. `wait`, `screen`, and `approval` PAUSE for manual continuation (an approval
|
|
8
|
+
* resumes down the chosen decision's branch — approve / reject / revise). `loop` resolves its
|
|
8
9
|
* collection and exposes the iterator but is a labelled single pass (the edge
|
|
9
10
|
* model has no separate body/exit edge). `parallel_gateway` fans out WITHOUT
|
|
10
11
|
* join synchronization; the ADR-0031 structured containers (`parallel`,
|
|
@@ -27,10 +28,38 @@ export declare class FlowSimulator {
|
|
|
27
28
|
step(): SimStep | null;
|
|
28
29
|
/** Run to completion (or until a pause / error). */
|
|
29
30
|
runToEnd(): SimState;
|
|
30
|
-
/**
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Continue a flow paused on a `wait`, `screen`, or `approval` node.
|
|
33
|
+
* - `screenOutputs` — inputs captured from a paused screen.
|
|
34
|
+
* - `decision` — the branch an approval resumes down (ADR-0019/0044:
|
|
35
|
+
* `approve` / `reject` / `revise`). The run takes ONLY the out-edge whose
|
|
36
|
+
* label matches, mirroring how the engine resumes a suspended approval by
|
|
37
|
+
* branch label — instead of fanning out to every out-edge.
|
|
38
|
+
*/
|
|
39
|
+
resume(opts?: {
|
|
40
|
+
screenOutputs?: Record<string, unknown>;
|
|
41
|
+
decision?: string;
|
|
42
|
+
}): void;
|
|
43
|
+
/**
|
|
44
|
+
* Resume a suspended approval down the chosen decision's out-edge: the one
|
|
45
|
+
* whose `label` equals `decision` (case-insensitive — `approve` / `reject` /
|
|
46
|
+
* `revise`). With no match (or no decision) it falls back to fanning out —
|
|
47
|
+
* mirroring the engine's unmatched-`branchLabel` fallback — and logs that so
|
|
48
|
+
* the author notices the unrouted decision.
|
|
49
|
+
*/
|
|
50
|
+
private resumeApproval;
|
|
32
51
|
private execute;
|
|
33
52
|
private executeDecision;
|
|
53
|
+
/**
|
|
54
|
+
* assignment node — set flow variables. Normalizes the three authoring
|
|
55
|
+
* shapes the engine accepts (Studio's `{ assignments: { var: value } }`
|
|
56
|
+
* map, the example `{ assignments: [{ variable, value }] }` array, and the
|
|
57
|
+
* legacy flat `{ var: value }`) and interpolates `{var}` templates — so the
|
|
58
|
+
* Debug run mirrors runtime instead of silently no-oping.
|
|
59
|
+
*/
|
|
60
|
+
private executeAssignment;
|
|
61
|
+
/** Resolve `{var}` templates in an assignment value against live variables. */
|
|
62
|
+
private interpolateValue;
|
|
34
63
|
private executeLoop;
|
|
35
64
|
/** Resolve a `{var}` template ref or a plain variable name from `variables`. */
|
|
36
65
|
private resolveRef;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
2
|
import { evalCondition, validateFlowDraft } from './flow-sim-validate';
|
|
3
3
|
const MAX_STEPS = 500;
|
|
4
|
-
const PASS_THROUGH = new Set(['start'
|
|
4
|
+
const PASS_THROUGH = new Set(['start']);
|
|
5
5
|
const MOCKED_SIDE_EFFECT = new Set([
|
|
6
6
|
'create_record',
|
|
7
7
|
'update_record',
|
|
@@ -124,24 +124,58 @@ export class FlowSimulator {
|
|
|
124
124
|
}
|
|
125
125
|
return this.state;
|
|
126
126
|
}
|
|
127
|
-
/**
|
|
128
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Continue a flow paused on a `wait`, `screen`, or `approval` node.
|
|
129
|
+
* - `screenOutputs` — inputs captured from a paused screen.
|
|
130
|
+
* - `decision` — the branch an approval resumes down (ADR-0019/0044:
|
|
131
|
+
* `approve` / `reject` / `revise`). The run takes ONLY the out-edge whose
|
|
132
|
+
* label matches, mirroring how the engine resumes a suspended approval by
|
|
133
|
+
* branch label — instead of fanning out to every out-edge.
|
|
134
|
+
*/
|
|
135
|
+
resume(opts = {}) {
|
|
129
136
|
const s = this.state;
|
|
130
137
|
if (s.status !== 'paused' || !s.activeNodeId)
|
|
131
138
|
return;
|
|
132
139
|
const node = this.nodes.get(s.activeNodeId);
|
|
133
|
-
if (screenOutputs && node?.type === 'screen') {
|
|
134
|
-
Object.assign(s.variables, screenOutputs);
|
|
140
|
+
if (opts.screenOutputs && node?.type === 'screen') {
|
|
141
|
+
Object.assign(s.variables, opts.screenOutputs);
|
|
135
142
|
}
|
|
136
143
|
s.pausedReason = undefined;
|
|
137
144
|
s.status = 'running';
|
|
138
|
-
if (node)
|
|
145
|
+
if (node?.type === 'approval')
|
|
146
|
+
this.resumeApproval(node, opts.decision);
|
|
147
|
+
else if (node)
|
|
139
148
|
this.enqueueSuccessors(node);
|
|
140
149
|
if (s.frontier.length === 0) {
|
|
141
150
|
s.status = 'done';
|
|
142
151
|
s.activeNodeId = null;
|
|
143
152
|
}
|
|
144
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Resume a suspended approval down the chosen decision's out-edge: the one
|
|
156
|
+
* whose `label` equals `decision` (case-insensitive — `approve` / `reject` /
|
|
157
|
+
* `revise`). With no match (or no decision) it falls back to fanning out —
|
|
158
|
+
* mirroring the engine's unmatched-`branchLabel` fallback — and logs that so
|
|
159
|
+
* the author notices the unrouted decision.
|
|
160
|
+
*/
|
|
161
|
+
resumeApproval(node, decision) {
|
|
162
|
+
const out = this.edges.map((e, i) => ({ e, i })).filter((x) => x.e.source === node.id);
|
|
163
|
+
const want = (decision ?? '').trim().toLowerCase();
|
|
164
|
+
const chosen = want ? out.find((x) => (x.e.label ?? '').trim().toLowerCase() === want) : undefined;
|
|
165
|
+
if (chosen) {
|
|
166
|
+
this.traverse(chosen.e, chosen.i);
|
|
167
|
+
this.record(node.id, 'approval', node.label, 'ok', { note: `Decision: ${decision} → ${chosen.e.target}` });
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
for (const x of out)
|
|
171
|
+
this.traverse(x.e, x.i);
|
|
172
|
+
this.record(node.id, 'approval', node.label, 'ok', {
|
|
173
|
+
note: want
|
|
174
|
+
? `No out-edge labelled "${decision}"; took all branches (engine label-fallback).`
|
|
175
|
+
: 'No decision supplied; took all branches.',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
145
179
|
// ---- node execution -----------------------------------------------------
|
|
146
180
|
execute(node) {
|
|
147
181
|
const type = node.type;
|
|
@@ -151,6 +185,8 @@ export class FlowSimulator {
|
|
|
151
185
|
}
|
|
152
186
|
if (type === 'decision')
|
|
153
187
|
return this.executeDecision(node);
|
|
188
|
+
if (type === 'assignment')
|
|
189
|
+
return this.executeAssignment(node);
|
|
154
190
|
if (UNSUPPORTED.has(type)) {
|
|
155
191
|
this.enqueueSuccessors(node);
|
|
156
192
|
return this.record(node.id, type, node.label, 'skipped', {
|
|
@@ -163,15 +199,34 @@ export class FlowSimulator {
|
|
|
163
199
|
note: 'Parallel split — branches fan out (no join synchronization is simulated).',
|
|
164
200
|
});
|
|
165
201
|
}
|
|
202
|
+
if (type === 'approval') {
|
|
203
|
+
// ADR-0019: an approval node opens a request and SUSPENDS the run until a
|
|
204
|
+
// decision is recorded. Model that as a pause; the author resumes down the
|
|
205
|
+
// chosen approve / reject / revise out-edge (see resumeApproval) rather
|
|
206
|
+
// than fanning out to every out-edge at once.
|
|
207
|
+
this.state.status = 'paused';
|
|
208
|
+
this.state.pausedReason = 'approval';
|
|
209
|
+
return this.record(node.id, type, node.label, 'paused', { note: 'Approval reached — choose a decision to continue.' });
|
|
210
|
+
}
|
|
166
211
|
if (type === 'wait') {
|
|
167
212
|
this.state.status = 'paused';
|
|
168
213
|
this.state.pausedReason = 'wait';
|
|
169
214
|
return this.record(node.id, type, node.label, 'paused', { note: 'Wait reached — continue manually.' });
|
|
170
215
|
}
|
|
171
216
|
if (type === 'screen') {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
217
|
+
// Mirror the engine's `shouldPause`: a screen suspends only when it
|
|
218
|
+
// collects input (`fields`) or explicitly opts in (`waitForInput`).
|
|
219
|
+
// A field-less / `waitForInput:false` screen is a server pass-through.
|
|
220
|
+
const fields = Array.isArray(node.config?.fields) ? node.config.fields : [];
|
|
221
|
+
const waitForInput = node.config?.waitForInput;
|
|
222
|
+
const shouldPause = waitForInput === true || (fields.length > 0 && waitForInput !== false);
|
|
223
|
+
if (shouldPause) {
|
|
224
|
+
this.state.status = 'paused';
|
|
225
|
+
this.state.pausedReason = 'screen';
|
|
226
|
+
return this.record(node.id, type, node.label, 'paused', { note: 'Screen reached — provide inputs, then continue.' });
|
|
227
|
+
}
|
|
228
|
+
this.enqueueSuccessors(node);
|
|
229
|
+
return this.record(node.id, type, node.label, 'ok', { note: 'Screen has no input — passed through (matches runtime).' });
|
|
175
230
|
}
|
|
176
231
|
if (type === 'loop') {
|
|
177
232
|
const step = this.executeLoop(node);
|
|
@@ -233,6 +288,61 @@ export class FlowSimulator {
|
|
|
233
288
|
note: multiMatch ? 'Multiple conditions matched; the first declared branch was taken.' : undefined,
|
|
234
289
|
});
|
|
235
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* assignment node — set flow variables. Normalizes the three authoring
|
|
293
|
+
* shapes the engine accepts (Studio's `{ assignments: { var: value } }`
|
|
294
|
+
* map, the example `{ assignments: [{ variable, value }] }` array, and the
|
|
295
|
+
* legacy flat `{ var: value }`) and interpolates `{var}` templates — so the
|
|
296
|
+
* Debug run mirrors runtime instead of silently no-oping.
|
|
297
|
+
*/
|
|
298
|
+
executeAssignment(node) {
|
|
299
|
+
const cfg = node.config ?? {};
|
|
300
|
+
const raw = cfg.assignments;
|
|
301
|
+
const pairs = [];
|
|
302
|
+
if (Array.isArray(raw)) {
|
|
303
|
+
for (const item of raw) {
|
|
304
|
+
if (item && typeof item === 'object') {
|
|
305
|
+
const e = item;
|
|
306
|
+
const name = e.variable ?? e.name ?? e.key;
|
|
307
|
+
if (typeof name === 'string' && name)
|
|
308
|
+
pairs.push([name, e.value]);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else if (raw && typeof raw === 'object') {
|
|
313
|
+
for (const [k, v] of Object.entries(raw))
|
|
314
|
+
pairs.push([k, v]);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
for (const [k, v] of Object.entries(cfg))
|
|
318
|
+
pairs.push([k, v]);
|
|
319
|
+
}
|
|
320
|
+
const wrote = {};
|
|
321
|
+
for (const [key, value] of pairs) {
|
|
322
|
+
const resolved = this.interpolateValue(value);
|
|
323
|
+
this.state.variables[key] = resolved;
|
|
324
|
+
wrote[key] = resolved;
|
|
325
|
+
}
|
|
326
|
+
this.enqueueSuccessors(node);
|
|
327
|
+
return this.record(node.id, 'assignment', node.label, 'ok', {
|
|
328
|
+
wrote: Object.keys(wrote).length ? wrote : undefined,
|
|
329
|
+
note: Object.keys(wrote).length ? undefined : 'No assignments defined.',
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
/** Resolve `{var}` templates in an assignment value against live variables. */
|
|
333
|
+
interpolateValue(value) {
|
|
334
|
+
if (typeof value !== 'string')
|
|
335
|
+
return value;
|
|
336
|
+
const whole = value.match(/^\{([^}]+)\}$/);
|
|
337
|
+
if (whole) {
|
|
338
|
+
const v = this.state.variables[whole[1].trim()];
|
|
339
|
+
return v !== undefined ? v : value;
|
|
340
|
+
}
|
|
341
|
+
return value.replace(/\{([^}]+)\}/g, (_m, k) => {
|
|
342
|
+
const v = this.state.variables[String(k).trim()];
|
|
343
|
+
return v === undefined || v === null ? '' : String(v);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
236
346
|
executeLoop(node) {
|
|
237
347
|
const ref = str(node.config?.collection);
|
|
238
348
|
const iterVar = str(node.config?.iteratorVariable);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/app-shell",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
|
|
@@ -33,36 +33,36 @@
|
|
|
33
33
|
"qrcode": "^1.5.4",
|
|
34
34
|
"sonner": "^2.0.7",
|
|
35
35
|
"zod": "^4.4.3",
|
|
36
|
-
"@object-ui/auth": "7.
|
|
37
|
-
"@object-ui/collaboration": "7.
|
|
38
|
-
"@object-ui/components": "7.
|
|
39
|
-
"@object-ui/core": "7.
|
|
40
|
-
"@object-ui/data-objectstack": "7.
|
|
41
|
-
"@object-ui/fields": "7.
|
|
42
|
-
"@object-ui/i18n": "7.
|
|
43
|
-
"@object-ui/layout": "7.
|
|
44
|
-
"@object-ui/permissions": "7.
|
|
45
|
-
"@object-ui/plugin-editor": "7.
|
|
46
|
-
"@object-ui/providers": "7.
|
|
47
|
-
"@object-ui/react": "7.
|
|
48
|
-
"@object-ui/types": "7.
|
|
36
|
+
"@object-ui/auth": "7.1.0",
|
|
37
|
+
"@object-ui/collaboration": "7.1.0",
|
|
38
|
+
"@object-ui/components": "7.1.0",
|
|
39
|
+
"@object-ui/core": "7.1.0",
|
|
40
|
+
"@object-ui/data-objectstack": "7.1.0",
|
|
41
|
+
"@object-ui/fields": "7.1.0",
|
|
42
|
+
"@object-ui/i18n": "7.1.0",
|
|
43
|
+
"@object-ui/layout": "7.1.0",
|
|
44
|
+
"@object-ui/permissions": "7.1.0",
|
|
45
|
+
"@object-ui/plugin-editor": "7.1.0",
|
|
46
|
+
"@object-ui/providers": "7.1.0",
|
|
47
|
+
"@object-ui/react": "7.1.0",
|
|
48
|
+
"@object-ui/types": "7.1.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"react": "^18.0.0 || ^19.0.0",
|
|
52
52
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
53
53
|
"react-router-dom": "^6.0.0 || ^7.0.0",
|
|
54
|
-
"@object-ui/plugin-calendar": "^7.
|
|
55
|
-
"@object-ui/plugin-charts": "^7.
|
|
56
|
-
"@object-ui/plugin-chatbot": "^7.
|
|
57
|
-
"@object-ui/plugin-dashboard": "^7.
|
|
58
|
-
"@object-ui/plugin-designer": "^7.
|
|
59
|
-
"@object-ui/plugin-detail": "^7.
|
|
60
|
-
"@object-ui/plugin-form": "^7.
|
|
61
|
-
"@object-ui/plugin-grid": "^7.
|
|
62
|
-
"@object-ui/plugin-kanban": "^7.
|
|
63
|
-
"@object-ui/plugin-list": "^7.
|
|
64
|
-
"@object-ui/plugin-report": "^7.
|
|
65
|
-
"@object-ui/plugin-view": "^7.
|
|
54
|
+
"@object-ui/plugin-calendar": "^7.1.0",
|
|
55
|
+
"@object-ui/plugin-charts": "^7.1.0",
|
|
56
|
+
"@object-ui/plugin-chatbot": "^7.1.0",
|
|
57
|
+
"@object-ui/plugin-dashboard": "^7.1.0",
|
|
58
|
+
"@object-ui/plugin-designer": "^7.1.0",
|
|
59
|
+
"@object-ui/plugin-detail": "^7.1.0",
|
|
60
|
+
"@object-ui/plugin-form": "^7.1.0",
|
|
61
|
+
"@object-ui/plugin-grid": "^7.1.0",
|
|
62
|
+
"@object-ui/plugin-kanban": "^7.1.0",
|
|
63
|
+
"@object-ui/plugin-list": "^7.1.0",
|
|
64
|
+
"@object-ui/plugin-report": "^7.1.0",
|
|
65
|
+
"@object-ui/plugin-view": "^7.1.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/node": "^26.0.0",
|
|
@@ -75,18 +75,18 @@
|
|
|
75
75
|
"sonner": "^2.0.7",
|
|
76
76
|
"typescript": "^6.0.3",
|
|
77
77
|
"vite": "^8.0.16",
|
|
78
|
-
"@object-ui/plugin-calendar": "7.
|
|
79
|
-
"@object-ui/plugin-charts": "7.
|
|
80
|
-
"@object-ui/plugin-chatbot": "7.
|
|
81
|
-
"@object-ui/plugin-dashboard": "7.
|
|
82
|
-
"@object-ui/plugin-designer": "7.
|
|
83
|
-
"@object-ui/plugin-detail": "7.
|
|
84
|
-
"@object-ui/plugin-form": "7.
|
|
85
|
-
"@object-ui/plugin-grid": "7.
|
|
86
|
-
"@object-ui/plugin-kanban": "7.
|
|
87
|
-
"@object-ui/plugin-list": "7.
|
|
88
|
-
"@object-ui/plugin-report": "7.
|
|
89
|
-
"@object-ui/plugin-view": "7.
|
|
78
|
+
"@object-ui/plugin-calendar": "7.1.0",
|
|
79
|
+
"@object-ui/plugin-charts": "7.1.0",
|
|
80
|
+
"@object-ui/plugin-chatbot": "7.1.0",
|
|
81
|
+
"@object-ui/plugin-dashboard": "7.1.0",
|
|
82
|
+
"@object-ui/plugin-designer": "7.1.0",
|
|
83
|
+
"@object-ui/plugin-detail": "7.1.0",
|
|
84
|
+
"@object-ui/plugin-form": "7.1.0",
|
|
85
|
+
"@object-ui/plugin-grid": "7.1.0",
|
|
86
|
+
"@object-ui/plugin-kanban": "7.1.0",
|
|
87
|
+
"@object-ui/plugin-list": "7.1.0",
|
|
88
|
+
"@object-ui/plugin-report": "7.1.0",
|
|
89
|
+
"@object-ui/plugin-view": "7.1.0"
|
|
90
90
|
},
|
|
91
91
|
"keywords": [
|
|
92
92
|
"objectui",
|