@object-ui/app-shell 7.0.0 → 7.2.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.
Files changed (141) hide show
  1. package/CHANGELOG.md +560 -0
  2. package/dist/console/AppContent.js +23 -17
  3. package/dist/console/ConsoleShell.d.ts +16 -0
  4. package/dist/console/ConsoleShell.js +43 -2
  5. package/dist/console/ai/AiChatPage.js +47 -16
  6. package/dist/console/ai/LiveCanvas.d.ts +8 -2
  7. package/dist/console/ai/LiveCanvas.js +6 -4
  8. package/dist/console/home/HomeLayout.js +5 -7
  9. package/dist/console/home/HomePage.js +1 -9
  10. package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
  11. package/dist/console/organizations/OrganizationsPage.js +22 -3
  12. package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
  13. package/dist/console/organizations/provisionEnvironment.js +64 -0
  14. package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
  15. package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
  16. package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
  17. package/dist/environment/EnvironmentListToolbar.js +59 -0
  18. package/dist/environment/entitlements.d.ts +90 -0
  19. package/dist/environment/entitlements.js +91 -0
  20. package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
  21. package/dist/environment/useEnvironmentEntitlements.js +108 -0
  22. package/dist/hooks/useActionModal.js +15 -1
  23. package/dist/hooks/useAiSurface.d.ts +59 -0
  24. package/dist/hooks/useAiSurface.js +78 -0
  25. package/dist/hooks/useChatConversation.d.ts +30 -0
  26. package/dist/hooks/useChatConversation.js +63 -0
  27. package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
  28. package/dist/hooks/useConsoleActionRuntime.js +42 -10
  29. package/dist/index.d.ts +5 -2
  30. package/dist/index.js +10 -2
  31. package/dist/layout/AppHeader.js +28 -4
  32. package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
  33. package/dist/layout/ConsoleFloatingChatbot.js +41 -10
  34. package/dist/layout/ConsoleLayout.js +5 -6
  35. package/dist/layout/ContextSelectors.js +59 -35
  36. package/dist/layout/agentPicker.d.ts +56 -0
  37. package/dist/layout/agentPicker.js +40 -0
  38. package/dist/preview/CommitTimeline.d.ts +15 -0
  39. package/dist/preview/CommitTimeline.js +82 -0
  40. package/dist/preview/DraftPreviewBar.js +20 -7
  41. package/dist/preview/UnpublishedAppBar.js +11 -7
  42. package/dist/preview/commitHistory.d.ts +28 -0
  43. package/dist/preview/commitHistory.js +48 -0
  44. package/dist/providers/ExpressionProvider.js +9 -3
  45. package/dist/providers/MetadataProvider.js +9 -0
  46. package/dist/utils/index.d.ts +2 -2
  47. package/dist/utils/index.js +1 -1
  48. package/dist/utils/recordFormNavigation.d.ts +60 -0
  49. package/dist/utils/recordFormNavigation.js +35 -0
  50. package/dist/utils/resolvePageVarTokens.d.ts +31 -0
  51. package/dist/utils/resolvePageVarTokens.js +72 -0
  52. package/dist/views/CreateViewDialog.js +14 -1
  53. package/dist/views/FlowRunner.d.ts +2 -30
  54. package/dist/views/FlowRunner.js +18 -50
  55. package/dist/views/ObjectView.js +26 -12
  56. package/dist/views/ScreenView.d.ts +70 -0
  57. package/dist/views/ScreenView.js +73 -0
  58. package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
  59. package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
  60. package/dist/views/metadata-admin/DirectoryPage.js +2 -14
  61. package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
  62. package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
  63. package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
  64. package/dist/views/metadata-admin/PackagesPage.js +58 -5
  65. package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
  66. package/dist/views/metadata-admin/ResourceEditPage.js +83 -24
  67. package/dist/views/metadata-admin/ResourceListPage.js +28 -19
  68. package/dist/views/metadata-admin/StudioHomePage.js +6 -14
  69. package/dist/views/metadata-admin/anchors.js +20 -2
  70. package/dist/views/metadata-admin/createBody.d.ts +26 -0
  71. package/dist/views/metadata-admin/createBody.js +30 -0
  72. package/dist/views/metadata-admin/i18n.js +108 -2
  73. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +10 -2
  74. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +136 -8
  75. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +99 -4
  76. package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
  77. package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
  78. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
  79. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
  80. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
  81. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
  82. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +81 -4
  83. package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
  84. package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
  85. package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
  86. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
  87. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
  88. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
  89. package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
  90. package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
  91. package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
  92. package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
  93. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
  94. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +102 -0
  95. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
  96. package/dist/views/metadata-admin/inspectors/flow-node-config.js +67 -11
  97. package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
  98. package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
  99. package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
  100. package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
  101. package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
  102. package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
  103. package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
  104. package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
  105. package/dist/views/metadata-admin/issuePath.d.ts +22 -0
  106. package/dist/views/metadata-admin/issuePath.js +65 -0
  107. package/dist/views/metadata-admin/package-scope.d.ts +41 -0
  108. package/dist/views/metadata-admin/package-scope.js +59 -0
  109. package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
  110. package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
  111. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +26 -1
  112. package/dist/views/metadata-admin/previews/FlowCanvas.js +143 -16
  113. package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
  114. package/dist/views/metadata-admin/previews/FlowPreview.js +47 -7
  115. package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
  116. package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
  117. package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
  118. package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
  119. package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
  120. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
  121. package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
  122. package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
  123. package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
  124. package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
  125. package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
  126. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +17 -1
  127. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +23 -6
  128. package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
  129. package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
  130. package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
  131. package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
  132. package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
  133. package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
  134. package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
  135. package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
  136. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +20 -0
  137. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
  138. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +76 -2
  139. package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
  140. package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
  141. package/package.json +38 -38
@@ -1,9 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { t } from '../i18n';
3
- import { InspectorShell, InspectorTextField, InspectorCheckboxField, InspectorRemoveButton, InspectorEmptyState, spliceArray, } from './_shared';
3
+ import { InspectorShell, InspectorTextField, InspectorSelectField, InspectorCheckboxField, InspectorRemoveButton, InspectorEmptyState, spliceArray, } from './_shared';
4
4
  import { Label } from '@object-ui/components';
5
5
  import { edgeKey, conditionText } from '../previews/flow-canvas-layout';
6
6
  import { validateExpressionClient } from './expression-validate';
7
+ import { useFlowScope } from './useFlowScope';
8
+ import { VariableTextInput } from './VariableTextInput';
9
+ import { findUnknownRefs, scopeRoots, describeUnknownRefs } from './flow-ref-check';
7
10
  /** Read-only display of an edge endpoint (source / target node id). */
8
11
  function EndpointRow({ label, value }) {
9
12
  return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: label }), _jsx("div", { className: "flex h-8 items-center rounded border bg-muted/30 px-2 font-mono text-sm text-muted-foreground", children: value })] }));
@@ -12,6 +15,9 @@ export function FlowEdgeInspector({ selection, draft, onPatch, onClearSelection,
12
15
  const edges = Array.isArray(draft.edges) ? draft.edges : [];
13
16
  const index = edges.findIndex((e, i) => edgeKey(e, i) === selection.id);
14
17
  const edge = index >= 0 ? edges[index] : null;
18
+ // References available on this edge are those in scope at its SOURCE node
19
+ // (#1934). Called unconditionally — `edge?.source` is undefined when missing.
20
+ const { groups: scopeGroups } = useFlowScope(draft, edge?.source);
15
21
  if (!edge) {
16
22
  return (_jsx(InspectorShell, { kindLabel: t('engine.inspector.flowEdge.kind', locale), title: selection.label ?? selection.id, onClose: onClearSelection, closeLabel: t('engine.inspector.flowEdge.close', locale), children: _jsx(InspectorEmptyState, { message: t('engine.inspector.flowEdge.missing', locale) }) }));
17
23
  }
@@ -27,19 +33,108 @@ export function FlowEdgeInspector({ selection, draft, onPatch, onClearSelection,
27
33
  if (v === undefined || v === '' || v === false)
28
34
  delete next[k];
29
35
  }
36
+ // `type` defaults to 'default' (FlowEdgeSchema) — don't persist the noise so
37
+ // a normal edge stays `{ source, target }`; only `back`/`fault`/`conditional`
38
+ // are written.
39
+ if (next.type === 'default' || next.type === '' || next.type === undefined)
40
+ delete next.type;
30
41
  onPatch({ edges: spliceArray(edges, index, next) });
31
42
  };
32
43
  const isDefault = edge.isDefault === true;
44
+ // Decision out-edges can bind EXPLICITLY to one of the source decision's
45
+ // branches (vs the implicit by-order auto-wire): picking a branch writes its
46
+ // expression / label (or marks the default) onto this edge, so routing stays
47
+ // correct even when edges are connected out of branch order.
48
+ const nodes = Array.isArray(draft.nodes)
49
+ ? (draft.nodes)
50
+ : [];
51
+ const sourceNode = nodes.find((n) => n.id === edge.source);
52
+ const branches = sourceNode?.type === 'decision' &&
53
+ Array.isArray(sourceNode.config?.conditions)
54
+ ? (sourceNode.config.conditions)
55
+ : [];
56
+ const branchExpr = (b) => (typeof b.expression === 'string' ? b.expression.trim() : '');
57
+ const branchName = (b) => (typeof b.label === 'string' ? b.label.trim() : '');
58
+ // Which branch this edge currently represents: the default edge maps to the
59
+ // `true`/empty branch; otherwise match by condition, then by label. '' = custom.
60
+ const selectedBranch = (() => {
61
+ if (!branches.length)
62
+ return '';
63
+ if (isDefault) {
64
+ const i = branches.findIndex((b) => { const e = branchExpr(b); return e === '' || e === 'true'; });
65
+ return i >= 0 ? String(i) : '';
66
+ }
67
+ const cond = conditionText(edge.condition);
68
+ let i = cond ? branches.findIndex((b) => branchExpr(b) === cond) : -1;
69
+ if (i < 0 && edge.label)
70
+ i = branches.findIndex((b) => branchName(b) === edge.label);
71
+ return i >= 0 ? String(i) : '';
72
+ })();
73
+ const applyBranch = (key) => {
74
+ if (key === '')
75
+ return; // keep current custom values
76
+ const b = branches[Number(key)];
77
+ if (!b)
78
+ return;
79
+ const expr = branchExpr(b);
80
+ const lbl = branchName(b) || undefined;
81
+ if (expr === '' || expr === 'true')
82
+ patchEdge({ isDefault: true, condition: undefined, label: lbl });
83
+ else
84
+ patchEdge({ isDefault: false, condition: expr, label: lbl });
85
+ };
86
+ // Approval out-edges (ADR-0019/0044) route by branch *label*: the engine
87
+ // resumes down the out-edge whose label matches the decision — `approve` /
88
+ // `reject`, or `revise` (ADR-0044 send-back-for-revision). Offer those as a
89
+ // picker (mirrors APPROVAL_BRANCH_LABELS in @objectstack/spec) so the author
90
+ // need not recall the exact keyword; a free-text label is still allowed.
91
+ const isApprovalSource = sourceNode?.type === 'approval';
92
+ const APPROVAL_BRANCHES = ['approve', 'reject', 'revise'];
93
+ const currentApprovalBranch = (() => {
94
+ const l = (edge.label ?? '').trim().toLowerCase();
95
+ return APPROVAL_BRANCHES.includes(l) ? l : '';
96
+ })();
97
+ const edgeType = (typeof edge.type === 'string' && edge.type) || 'default';
33
98
  return (_jsxs(InspectorShell, { kindLabel: t('engine.inspector.flowEdge.kind', locale), title: selection.label ?? `${edge.source} → ${edge.target}`, onClose: onClearSelection, closeLabel: t('engine.inspector.flowEdge.close', locale), footer: _jsx(InspectorRemoveButton, { label: t('engine.inspector.flowEdge.remove', locale), onClick: () => {
34
99
  onPatch({ edges: spliceArray(edges, index, null) });
35
100
  onClearSelection();
36
- }, disabled: readOnly }), children: [_jsx(EndpointRow, { label: t('engine.inspector.flowEdge.source', locale), value: edge.source }), _jsx(EndpointRow, { label: t('engine.inspector.flowEdge.target', locale), value: edge.target }), _jsxs("div", { className: "flex items-center gap-2 pt-1", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: t('engine.inspector.flowEdge.routing', locale) }), _jsx("span", { className: "h-px flex-1 bg-border", "aria-hidden": true })] }), _jsx(InspectorTextField, { label: t('engine.inspector.flowEdge.label', locale), value: edge.label ?? '', onCommit: (v) => patchEdge({ label: v }), placeholder: t('engine.inspector.flowEdge.labelHint', locale), disabled: readOnly || isDefault }), _jsx(InspectorTextField, { label: t('engine.inspector.flowEdge.condition', locale), value: conditionText(edge.condition) ?? '', onCommit: (v) => patchEdge({ condition: v || undefined }), placeholder: t('engine.inspector.flowEdge.conditionHint', locale), disabled: readOnly || isDefault, mono: true }), (() => {
101
+ }, disabled: readOnly }), children: [_jsx(EndpointRow, { label: t('engine.inspector.flowEdge.source', locale), value: edge.source }), _jsx(EndpointRow, { label: t('engine.inspector.flowEdge.target', locale), value: edge.target }), _jsxs("div", { className: "flex items-center gap-2 pt-1", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: t('engine.inspector.flowEdge.routing', locale) }), _jsx("span", { className: "h-px flex-1 bg-border", "aria-hidden": true })] }), branches.length > 0 && (_jsx(InspectorSelectField, { label: t('engine.inspector.flowEdge.branch', locale), value: selectedBranch, options: [
102
+ ...branches.map((b, i) => {
103
+ const expr = branchExpr(b);
104
+ const nm = branchName(b) || `Branch ${i + 1}`;
105
+ const suffix = expr === '' || expr === 'true' ? ' \u00b7 default' : ` \u00b7 ${expr}`;
106
+ return { value: String(i), label: `${nm}${suffix}` };
107
+ }),
108
+ { value: '', label: '\u2014 Custom \u2014' },
109
+ ], onCommit: applyBranch, disabled: readOnly })), isApprovalSource && (_jsx(InspectorSelectField, { label: t('engine.inspector.flowEdge.approvalBranch', locale), value: currentApprovalBranch, options: [
110
+ { value: 'approve', label: t('engine.inspector.flowEdge.branchApprove', locale) },
111
+ { value: 'reject', label: t('engine.inspector.flowEdge.branchReject', locale) },
112
+ { value: 'revise', label: t('engine.inspector.flowEdge.branchRevise', locale) },
113
+ { value: '', label: t('engine.inspector.flowEdge.branchCustom', locale) },
114
+ ],
115
+ // Picking a branch writes the matching label; "Custom" keeps the
116
+ // free-text label the author typed below.
117
+ onCommit: (v) => { if (v)
118
+ patchEdge({ label: v }); }, disabled: readOnly })), _jsx(InspectorTextField, { label: t('engine.inspector.flowEdge.label', locale), value: edge.label ?? '', onCommit: (v) => patchEdge({ label: v }), placeholder: t('engine.inspector.flowEdge.labelHint', locale), disabled: readOnly || isDefault }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: t('engine.inspector.flowEdge.condition', locale) }), _jsx(VariableTextInput, { mode: "expression", mono: true, value: conditionText(edge.condition) ?? '', onValueChange: (v) => patchEdge({ condition: v || undefined }), groups: scopeGroups, placeholder: t('engine.inspector.flowEdge.conditionHint', locale), disabled: readOnly || isDefault })] }), (() => {
37
119
  // ADR-0032 — flag a malformed edge guard (e.g. `{record.x}` brace-in-CEL)
38
120
  // inline, with the same corrective message as build/agent validation.
39
121
  const issue = isDefault ? null : validateExpressionClient('predicate', edge.condition);
40
- return issue ? (_jsx("p", { className: "text-[11px] leading-snug text-destructive", role: "alert", children: issue.message })) : null;
122
+ if (issue) {
123
+ return (_jsx("p", { className: "text-[11px] leading-snug text-destructive", role: "alert", children: issue.message }));
124
+ }
125
+ // #1934 — gentle scope-aware "unknown reference" warning (refs in scope
126
+ // at the edge's SOURCE node), once the guard is structurally valid.
127
+ const unknown = isDefault
128
+ ? []
129
+ : findUnknownRefs(conditionText(edge.condition), 'predicate', scopeRoots(scopeGroups.flatMap((g) => g.refs)));
130
+ return unknown.length > 0 ? (_jsx("p", { className: "text-[11px] leading-snug text-amber-600 dark:text-amber-400", role: "note", children: describeUnknownRefs(unknown) })) : null;
41
131
  })(), _jsx(InspectorCheckboxField, { label: t('engine.inspector.flowEdge.isDefault', locale), value: isDefault,
42
132
  // The default ("else") branch is taken when no other guard matches, so
43
133
  // it carries neither a condition nor a branch label — clear both.
44
- onCommit: (v) => patchEdge(v ? { isDefault: true, condition: undefined, label: undefined } : { isDefault: false }), disabled: readOnly }), _jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: t('engine.inspector.flowEdge.hint', locale) })] }));
134
+ onCommit: (v) => patchEdge(v ? { isDefault: true, condition: undefined, label: undefined } : { isDefault: false }), disabled: readOnly }), _jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: t('engine.inspector.flowEdge.hint', locale) }), _jsxs("div", { className: "flex items-center gap-2 pt-1", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: t('engine.inspector.flowEdge.connection', locale) }), _jsx("span", { className: "h-px flex-1 bg-border", "aria-hidden": true })] }), _jsx(InspectorSelectField, { label: t('engine.inspector.flowEdge.type', locale), value: edgeType, options: [
135
+ { value: 'default', label: t('engine.inspector.flowEdge.typeDefault', locale) },
136
+ { value: 'conditional', label: t('engine.inspector.flowEdge.typeConditional', locale) },
137
+ { value: 'fault', label: t('engine.inspector.flowEdge.typeFault', locale) },
138
+ { value: 'back', label: t('engine.inspector.flowEdge.typeBack', locale) },
139
+ ], onCommit: (v) => patchEdge({ type: v }), disabled: readOnly }), edge.type === 'back' && (_jsx("p", { className: "text-[11px] leading-snug text-amber-600 dark:text-amber-400", role: "note", children: t('engine.inspector.flowEdge.backHint', locale) }))] }));
45
140
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * FlowExprIssue — the shared inline validation line for an expression / template
3
+ * value in the flow inspector (#1934). Renders, in precedence order:
4
+ * 1. an ADR-0032 brace/shape ERROR (red) — CEL fields only; a genuine template
5
+ * uses single-brace `{var}` legally, so the brace check never runs there;
6
+ * 2. else a scope-aware "unknown reference" WARNING (amber) — a referenced
7
+ * root not in scope at the node, with a "did you mean?" hint.
8
+ * Returns null when the value is clean (or scope is unknown). Used by the picker
9
+ * repeater cells (decision Branches, screen visibleWhen, key/value values) that
10
+ * carry the picker but otherwise had no inline validation.
11
+ */
12
+ import * as React from 'react';
13
+ import { type ExprFieldRole } from './expression-validate';
14
+ import type { ScopeGroup } from './useFlowScope';
15
+ export interface FlowExprIssueProps {
16
+ value: unknown;
17
+ /** `'predicate'` / `'value'` → CEL (brace-checked); `'template'` → `{…}` holes. */
18
+ role: ExprFieldRole;
19
+ scopeGroups?: ScopeGroup[];
20
+ }
21
+ export declare function FlowExprIssue({ value, role, scopeGroups }: FlowExprIssueProps): React.ReactElement | null;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { validateExpressionClient } from './expression-validate';
3
+ import { findUnknownRefs, scopeRoots, describeUnknownRefs } from './flow-ref-check';
4
+ export function FlowExprIssue({ value, role, scopeGroups }) {
5
+ // Brace / shape error — CEL roles only (single-brace is valid in a template).
6
+ const issue = role === 'template' ? null : validateExpressionClient(role, value);
7
+ if (issue) {
8
+ return (_jsx("p", { className: "text-[11px] leading-snug text-destructive", role: "alert", children: issue.message }));
9
+ }
10
+ const roots = scopeGroups && scopeGroups.length > 0 ? scopeRoots(scopeGroups.flatMap((g) => g.refs)) : null;
11
+ const unknown = roots ? findUnknownRefs(value, role, roots) : [];
12
+ return unknown.length > 0 ? (_jsx("p", { className: "text-[11px] leading-snug text-amber-600 dark:text-amber-400", role: "note", children: describeUnknownRefs(unknown) })) : null;
13
+ }
@@ -15,10 +15,26 @@
15
15
  * rows take precedence).
16
16
  */
17
17
  import * as React from 'react';
18
+ import type { ScopeGroup } from './useFlowScope';
19
+ export interface Row {
20
+ id: string;
21
+ key: string;
22
+ /** Display string for the value cell. */
23
+ raw: string;
24
+ }
25
+ /**
26
+ * Read the stored value as `[key, value]` entries, accepting BOTH shapes a
27
+ * key/value config field can hold: the common object map (`{ var: value }`) and
28
+ * the assignment-node ARRAY form (`[{ variable|name|key, value }]`). The shape
29
+ * is preserved on write (see {@link rowsToValue}).
30
+ */
31
+ export declare function toEntries(value: unknown): Array<[string, unknown]>;
32
+ /** Flush rows back to the SAME shape, skipping empty/duplicate keys (first wins). */
33
+ export declare function rowsToValue(rows: Row[], arrayShape: boolean): Record<string, unknown> | Array<Record<string, unknown>>;
18
34
  export interface FlowKeyValueFieldProps {
19
35
  label: string;
20
36
  value: unknown;
21
- onCommit: (value: Record<string, unknown> | undefined) => void;
37
+ onCommit: (value: Record<string, unknown> | Array<Record<string, unknown>> | undefined) => void;
22
38
  disabled?: boolean;
23
39
  help?: string;
24
40
  addLabel: string;
@@ -26,5 +42,7 @@ export interface FlowKeyValueFieldProps {
26
42
  valueLabel: string;
27
43
  removeLabel: string;
28
44
  emptyLabel: string;
45
+ /** In-scope variable references for the data-picker (#1934). */
46
+ scopeGroups?: ScopeGroup[];
29
47
  }
30
- export declare function FlowKeyValueField({ label, value, onCommit, disabled, help, addLabel, keyLabel, valueLabel, removeLabel, emptyLabel, }: FlowKeyValueFieldProps): React.JSX.Element;
48
+ export declare function FlowKeyValueField({ label, value, onCommit, disabled, help, addLabel, keyLabel, valueLabel, removeLabel, emptyLabel, scopeGroups, }: FlowKeyValueFieldProps): React.JSX.Element;
@@ -20,6 +20,8 @@ import * as React from 'react';
20
20
  import { Plus, X } from 'lucide-react';
21
21
  import { Button, Input, Label } from '@object-ui/components';
22
22
  import { uniqueId } from './_shared';
23
+ import { VariableTextInput } from './VariableTextInput';
24
+ import { FlowExprIssue } from './FlowExprIssue';
23
25
  function isPlainObject(v) {
24
26
  return typeof v === 'object' && v !== null && !Array.isArray(v);
25
27
  }
@@ -58,49 +60,90 @@ function parseValue(raw) {
58
60
  }
59
61
  return raw;
60
62
  }
61
- function toRows(obj, existingIds) {
63
+ /**
64
+ * Read the stored value as `[key, value]` entries, accepting BOTH shapes a
65
+ * key/value config field can hold: the common object map (`{ var: value }`) and
66
+ * the assignment-node ARRAY form (`[{ variable|name|key, value }]`). The shape
67
+ * is preserved on write (see {@link rowsToValue}).
68
+ */
69
+ export function toEntries(value) {
70
+ if (Array.isArray(value)) {
71
+ return value
72
+ .filter((it) => isPlainObject(it))
73
+ .map((it) => {
74
+ const k = it.variable ?? it.name ?? it.key;
75
+ return [typeof k === 'string' ? k : '', it.value];
76
+ });
77
+ }
78
+ if (isPlainObject(value))
79
+ return Object.entries(value);
80
+ return [];
81
+ }
82
+ function toRows(value, existingIds) {
62
83
  const ids = [...existingIds];
63
- return Object.entries(obj).map(([key, value]) => {
84
+ return toEntries(value).map(([key, val]) => {
64
85
  const id = uniqueId('kv', ids);
65
86
  ids.push(id);
66
- return { id, key, raw: toRaw(value) };
87
+ return { id, key, raw: toRaw(val) };
67
88
  });
68
89
  }
69
- /** Flush rows to an object, skipping empty/duplicate keys (first wins). */
70
- function rowsToObject(rows) {
90
+ /** Flush rows back to the SAME shape, skipping empty/duplicate keys (first wins). */
91
+ export function rowsToValue(rows, arrayShape) {
92
+ const seen = new Set();
93
+ if (arrayShape) {
94
+ const list = [];
95
+ for (const r of rows) {
96
+ const k = r.key.trim();
97
+ if (!k || seen.has(k))
98
+ continue;
99
+ seen.add(k);
100
+ list.push({ variable: k, value: parseValue(r.raw) });
101
+ }
102
+ return list;
103
+ }
71
104
  const out = {};
72
105
  for (const r of rows) {
73
106
  const k = r.key.trim();
74
- if (!k || k in out)
107
+ if (!k || seen.has(k))
75
108
  continue;
109
+ seen.add(k);
76
110
  out[k] = parseValue(r.raw);
77
111
  }
78
112
  return out;
79
113
  }
80
- function serialize(obj) {
114
+ /** Stable serialization for the resync guard (order-insensitive for objects). */
115
+ function serialize(value) {
116
+ if (Array.isArray(value))
117
+ return JSON.stringify(value);
118
+ const obj = value ?? {};
81
119
  const sorted = Object.keys(obj).sort().reduce((acc, k) => {
82
120
  acc[k] = obj[k];
83
121
  return acc;
84
122
  }, {});
85
123
  return JSON.stringify(sorted);
86
124
  }
87
- export function FlowKeyValueField({ label, value, onCommit, disabled, help, addLabel, keyLabel, valueLabel, removeLabel, emptyLabel, }) {
88
- const external = isPlainObject(value) ? value : {};
89
- const [rows, setRows] = React.useState(() => toRows(external, []));
90
- // Track the last value we committed so an external change (node switch) can
91
- // resync rows without clobbering an in-progress edit of the same node.
92
- const lastCommitted = React.useRef(serialize(external));
125
+ export function FlowKeyValueField({ label, value, onCommit, disabled, help, addLabel, keyLabel, valueLabel, removeLabel, emptyLabel, scopeGroups, }) {
126
+ // Preserve whichever shape the value was authored in (object map vs the
127
+ // assignment-node array form) across edits.
128
+ const arrayShape = Array.isArray(value);
129
+ // Normalized serialization of the stored value used only to detect an
130
+ // EXTERNAL change (node switch) that should resync the rows.
131
+ const external = React.useMemo(() => serialize(rowsToValue(toRows(value, []), arrayShape)), [value, arrayShape]);
132
+ const [rows, setRows] = React.useState(() => toRows(value, []));
133
+ // Track the last value we committed so an external change can resync rows
134
+ // without clobbering an in-progress edit of the same node.
135
+ const lastCommitted = React.useRef(external);
93
136
  React.useEffect(() => {
94
- const next = serialize(external);
95
- if (next !== lastCommitted.current) {
96
- setRows(toRows(external, []));
97
- lastCommitted.current = next;
137
+ if (external !== lastCommitted.current) {
138
+ setRows(toRows(value, []));
139
+ lastCommitted.current = external;
98
140
  }
99
- }, [external]);
141
+ }, [external, value]);
100
142
  const flush = (nextRows) => {
101
- const obj = rowsToObject(nextRows);
102
- lastCommitted.current = serialize(obj);
103
- onCommit(Object.keys(obj).length ? obj : undefined);
143
+ const out = rowsToValue(nextRows, arrayShape);
144
+ lastCommitted.current = serialize(out);
145
+ const empty = Array.isArray(out) ? out.length === 0 : Object.keys(out).length === 0;
146
+ onCommit(empty ? undefined : out);
104
147
  };
105
148
  const setRowField = (id, patch) => {
106
149
  setRows((rs) => rs.map((r) => (r.id === id ? { ...r, ...patch } : r)));
@@ -115,11 +158,11 @@ export function FlowKeyValueField({ label, value, onCommit, disabled, help, addL
115
158
  return next;
116
159
  });
117
160
  };
118
- return (_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: label }), _jsxs("div", { className: "space-y-1.5", children: [rows.length === 0 && (_jsx("p", { className: "text-[11px] italic text-muted-foreground", children: emptyLabel })), rows.map((row) => (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Input, { value: row.key, onChange: (e) => setRowField(row.id, { key: e.target.value }), onBlur: () => flush(rows), onKeyDown: (e) => {
119
- if (e.key === 'Enter')
120
- e.target.blur();
121
- }, placeholder: keyLabel, disabled: disabled, className: "h-8 flex-1 font-mono text-xs" }), _jsx(Input, { value: row.raw, onChange: (e) => setRowField(row.id, { raw: e.target.value }), onBlur: () => flush(rows), onKeyDown: (e) => {
122
- if (e.key === 'Enter')
123
- e.target.blur();
124
- }, placeholder: valueLabel, disabled: disabled, className: "h-8 flex-1 text-xs" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-8 w-8 shrink-0 p-0 text-muted-foreground", onClick: () => removeRow(row.id), disabled: disabled, "aria-label": removeLabel, title: removeLabel, children: _jsx(X, { className: "h-3.5 w-3.5" }) })] }, row.id)))] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "h-7 w-full text-xs", onClick: addRow, disabled: disabled, children: [_jsx(Plus, { className: "mr-1 h-3.5 w-3.5" }), addLabel] }), help && _jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: help })] }));
161
+ return (_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: label }), _jsxs("div", { className: "space-y-1.5", children: [rows.length === 0 && (_jsx("p", { className: "text-[11px] italic text-muted-foreground", children: emptyLabel })), rows.map((row) => (_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Input, { value: row.key, onChange: (e) => setRowField(row.id, { key: e.target.value }), onBlur: () => flush(rows), onKeyDown: (e) => {
162
+ if (e.key === 'Enter')
163
+ e.target.blur();
164
+ }, placeholder: keyLabel, disabled: disabled, className: "h-8 flex-1 font-mono text-xs" }), _jsx(VariableTextInput, { mode: "template", value: row.raw, onValueChange: (v) => setRowField(row.id, { raw: v }), onBlur: () => flush(rows), onKeyDown: (e) => {
165
+ if (e.key === 'Enter')
166
+ e.target.blur();
167
+ }, groups: scopeGroups ?? [], placeholder: valueLabel, disabled: disabled, className: "flex-1" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-8 w-8 shrink-0 p-0 text-muted-foreground", onClick: () => removeRow(row.id), disabled: disabled, "aria-label": removeLabel, title: removeLabel, children: _jsx(X, { className: "h-3.5 w-3.5" }) })] }), _jsx(FlowExprIssue, { value: row.raw, role: "template", scopeGroups: scopeGroups })] }, row.id)))] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "h-7 w-full text-xs", onClick: addRow, disabled: disabled, children: [_jsx(Plus, { className: "mr-1 h-3.5 w-3.5" }), addLabel] }), help && _jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: help })] }));
125
168
  }
@@ -6,6 +6,7 @@
6
6
  import * as React from 'react';
7
7
  import type { FlowConfigField } from './flow-node-config';
8
8
  import { type FlowReferenceContext } from './FlowReferenceField';
9
+ import type { ScopeGroup } from './useFlowScope';
9
10
  export interface FlowNodeConfigFieldProps {
10
11
  field: FlowConfigField;
11
12
  value: unknown;
@@ -14,5 +15,7 @@ export interface FlowNodeConfigFieldProps {
14
15
  locale?: string;
15
16
  /** Draft + node context so `reference` fields can resolve their options. */
16
17
  context?: FlowReferenceContext;
18
+ /** In-scope variable references for the data-picker (#1934). */
19
+ scopeGroups?: ScopeGroup[];
17
20
  }
18
- export declare function FlowNodeConfigField({ field, value, onCommit, disabled, locale, context }: FlowNodeConfigFieldProps): React.JSX.Element;
21
+ export declare function FlowNodeConfigField({ field, value, onCommit, disabled, locale, context, scopeGroups }: FlowNodeConfigFieldProps): React.JSX.Element;
@@ -1,23 +1,26 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { t } from '../i18n';
3
- import { InspectorTextField, InspectorNumberField, InspectorSelectField, InspectorCheckboxField, } from './_shared';
3
+ import { InspectorNumberField, InspectorSelectField, InspectorCheckboxField, } from './_shared';
4
4
  import { Label } from '@object-ui/components';
5
5
  import { FlowKeyValueField } from './FlowKeyValueField';
6
6
  import { FlowStringListField } from './FlowStringListField';
7
7
  import { FlowObjectListField } from './FlowObjectListField';
8
8
  import { FlowReferenceField } from './FlowReferenceField';
9
9
  import { validateExpressionClient } from './expression-validate';
10
- export function FlowNodeConfigField({ field, value, onCommit, disabled, locale, context }) {
10
+ import { VariableTextInput } from './VariableTextInput';
11
+ import { findUnknownRefs, scopeRoots, describeUnknownRefs } from './flow-ref-check';
12
+ export function FlowNodeConfigField({ field, value, onCommit, disabled, locale, context, scopeGroups }) {
13
+ const refMode = field.refMode ?? (field.kind === 'expression' ? 'expression' : 'template');
11
14
  const control = (() => {
12
15
  switch (field.kind) {
13
16
  case 'reference':
14
17
  return (_jsx(FlowReferenceField, { field: field, value: value, onCommit: (v) => onCommit(v), disabled: disabled, context: context }));
15
18
  case 'keyValue':
16
- return (_jsx(FlowKeyValueField, { label: field.label, value: value, onCommit: (v) => onCommit(v), disabled: disabled, addLabel: t('engine.inspector.flowNode.kv.add', locale), keyLabel: t('engine.inspector.flowNode.kv.key', locale), valueLabel: t('engine.inspector.flowNode.kv.value', locale), removeLabel: t('engine.inspector.flowNode.kv.remove', locale), emptyLabel: t('engine.inspector.flowNode.kv.empty', locale) }));
19
+ return (_jsx(FlowKeyValueField, { label: field.label, value: value, onCommit: (v) => onCommit(v), disabled: disabled, addLabel: t('engine.inspector.flowNode.kv.add', locale), keyLabel: t('engine.inspector.flowNode.kv.key', locale), valueLabel: t('engine.inspector.flowNode.kv.value', locale), removeLabel: t('engine.inspector.flowNode.kv.remove', locale), emptyLabel: t('engine.inspector.flowNode.kv.empty', locale), scopeGroups: scopeGroups }));
17
20
  case 'stringList':
18
21
  return (_jsx(FlowStringListField, { label: field.label, value: value, onCommit: (v) => onCommit(v), disabled: disabled, addLabel: t('engine.inspector.flowNode.list.add', locale), itemLabel: t('engine.inspector.flowNode.list.item', locale), removeLabel: t('engine.inspector.flowNode.list.remove', locale), emptyLabel: t('engine.inspector.flowNode.list.empty', locale) }));
19
22
  case 'objectList':
20
- return (_jsx(FlowObjectListField, { label: field.label, columns: field.columns ?? [], value: value, onCommit: (v) => onCommit(v), disabled: disabled, addLabel: t('engine.inspector.flowNode.list.add', locale), removeLabel: t('engine.inspector.flowNode.list.remove', locale), emptyLabel: t('engine.inspector.flowNode.list.empty', locale), context: context }));
23
+ return (_jsx(FlowObjectListField, { label: field.label, columns: field.columns ?? [], value: value, onCommit: (v) => onCommit(v), disabled: disabled, addLabel: t('engine.inspector.flowNode.list.add', locale), removeLabel: t('engine.inspector.flowNode.list.remove', locale), emptyLabel: t('engine.inspector.flowNode.list.empty', locale), context: context, scopeGroups: scopeGroups }));
21
24
  case 'number':
22
25
  return (_jsx(InspectorNumberField, { label: field.label, value: typeof value === 'number' ? value : value != null && value !== '' ? Number(value) : undefined, placeholder: field.placeholder, onCommit: (v) => onCommit(v), disabled: disabled }));
23
26
  case 'boolean':
@@ -25,16 +28,28 @@ export function FlowNodeConfigField({ field, value, onCommit, disabled, locale,
25
28
  case 'select':
26
29
  return (_jsx(InspectorSelectField, { label: field.label, value: value != null ? String(value) : '', options: field.options ?? [], onCommit: (v) => onCommit(v), disabled: disabled }));
27
30
  case 'textarea':
28
- return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: field.label }), _jsx("textarea", { value: value != null ? String(value) : '', onChange: (e) => onCommit(e.target.value), placeholder: field.placeholder, disabled: disabled, rows: 4, className: "w-full rounded border bg-background px-2 py-1.5 font-mono text-xs" })] }));
31
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: field.label }), _jsx(VariableTextInput, { multiline: true, rows: 4, mode: refMode, value: value != null ? String(value) : '', onValueChange: (v) => onCommit(v), groups: scopeGroups ?? [], placeholder: field.placeholder, disabled: disabled })] }));
29
32
  case 'expression':
30
33
  case 'text':
31
34
  default:
32
- return (_jsx(InspectorTextField, { label: field.label, value: value != null ? String(value) : '', placeholder: field.placeholder, onCommit: (v) => onCommit(v), disabled: disabled, mono: field.kind === 'expression' }));
35
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: field.label }), _jsx(VariableTextInput, { mode: refMode, mono: field.kind === 'expression', value: value != null ? String(value) : '', onValueChange: (v) => onCommit(v), groups: scopeGroups ?? [], placeholder: field.placeholder, disabled: disabled })] }));
33
36
  }
34
37
  })();
35
38
  // ADR-0032 — surface a malformed condition (e.g. the `{record.x}` brace-in-CEL
36
- // mistake) inline, as the author types, with the same corrective message the
37
- // build and the agent tool emit. Only checked for expression-bearing fields.
39
+ // mistake) inline, with the same corrective message the build/agent emit. Only
40
+ // for expression fields (a genuine template uses single-brace `{var}` legally).
38
41
  const exprIssue = field.kind === 'expression' ? validateExpressionClient('predicate', value) : null;
39
- return (_jsxs("div", { className: "space-y-1", children: [control, exprIssue && (_jsx("p", { className: "text-[11px] leading-snug text-destructive", role: "alert", children: exprIssue.message })), field.help && !exprIssue && (_jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: field.help }))] }));
42
+ // #1934 pair the picker with a gentle, scope-aware "unknown reference"
43
+ // warning: CEL for expression fields, `{…}` holes for template fields. Skipped
44
+ // for free-form code (refMode 'expression' on a textarea, e.g. a script body)
45
+ // and when scope is unknown. The brace error above takes precedence.
46
+ const scopeRole = field.kind === 'expression'
47
+ ? 'predicate'
48
+ : refMode === 'template' && (field.kind === 'text' || field.kind === 'textarea')
49
+ ? 'template'
50
+ : null;
51
+ const unknownRefs = !exprIssue && scopeRole && scopeGroups && scopeGroups.length > 0
52
+ ? findUnknownRefs(value, scopeRole, scopeRoots(scopeGroups.flatMap((g) => g.refs)))
53
+ : [];
54
+ return (_jsxs("div", { className: "space-y-1", children: [control, exprIssue && (_jsx("p", { className: "text-[11px] leading-snug text-destructive", role: "alert", children: exprIssue.message })), !exprIssue && unknownRefs.length > 0 && (_jsx("p", { className: "text-[11px] leading-snug text-amber-600 dark:text-amber-400", role: "note", children: describeUnknownRefs(unknownRefs) })), field.help && !exprIssue && unknownRefs.length === 0 && (_jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: field.help }))] }));
40
55
  }
@@ -19,6 +19,50 @@ import { fieldsForNodeType, isFieldVisible, getFieldValue, configKeyOf, FLOW_NOD
19
19
  import { jsonSchemaToFlowFields } from './json-schema-to-fields';
20
20
  import { useActionConfigSchemas } from '../previews/useFlowNodePalette';
21
21
  import { FlowNodeConfigField } from './FlowNodeConfigField';
22
+ import { useFlowScope } from './useFlowScope';
23
+ import { ScreenPreview } from '../previews/ScreenPreview';
24
+ /**
25
+ * Mirror a decision node's `conditions` (branches) onto its outgoing sequence
26
+ * edges, in declared order: branch i -> the i-th out-edge. A branch whose
27
+ * expression is `true` marks its edge as the default/else path; an empty
28
+ * expression clears the guard. Fault / back edges are left untouched.
29
+ *
30
+ * This is what lets a decision authored entirely in Studio actually route at
31
+ * runtime: the engine and the simulator branch on `edge.condition`, NOT on
32
+ * `node.config.conditions` (which is only a node-local branch list). Without
33
+ * the mirror, every out-edge stays unconditional and all branches fire.
34
+ */
35
+ function syncDecisionEdges(decisionId, conditions, edges) {
36
+ const branches = Array.isArray(conditions) ? conditions : [];
37
+ let bi = 0;
38
+ return edges.map((e) => {
39
+ if (e.source !== decisionId || e.type === 'fault' || e.type === 'back')
40
+ return e;
41
+ const branch = branches[bi++];
42
+ if (!branch || typeof branch !== 'object')
43
+ return e;
44
+ const expr = typeof branch.expression === 'string' ? branch.expression.trim() : '';
45
+ const label = typeof branch.label === 'string' ? branch.label.trim() : '';
46
+ const next = { ...e };
47
+ if (label)
48
+ next.label = label;
49
+ else
50
+ delete next.label;
51
+ if (expr && expr !== 'true') {
52
+ next.condition = expr;
53
+ delete next.isDefault;
54
+ }
55
+ else if (expr === 'true') {
56
+ next.isDefault = true;
57
+ delete next.condition;
58
+ }
59
+ else {
60
+ delete next.condition;
61
+ delete next.isDefault;
62
+ }
63
+ return next;
64
+ });
65
+ }
22
66
  function asConfig(node) {
23
67
  const c = node?.config;
24
68
  return c && typeof c === 'object' && !Array.isArray(c) ? c : {};
@@ -58,6 +102,8 @@ export function FlowNodeInspector({ selection, draft, onPatch, onClearSelection,
58
102
  // with the backend. Falls back to the hardcoded field group when no schema is
59
103
  // published (offline / plugin absent / older backend).
60
104
  const configSchemas = useActionConfigSchemas();
105
+ // In-scope variable references for this node, for the data-picker (#1934).
106
+ const { groups: scopeGroups } = useFlowScope(draft, node?.id);
61
107
  const fields = React.useMemo(() => {
62
108
  const schema = node?.type ? configSchemas[node.type] : undefined;
63
109
  const serverFields = schema !== undefined ? jsonSchemaToFlowFields(schema) : null;
@@ -65,6 +111,16 @@ export function FlowNodeInspector({ selection, draft, onPatch, onClearSelection,
65
111
  }, [configSchemas, node?.type]);
66
112
  const config = asConfig(node);
67
113
  const visibleFields = fields.filter((f) => isFieldVisible(f, node, fields));
114
+ // `{var}` interpolation source for the screen preview — the flow's declared
115
+ // variables and their defaults (the designer has no live run state).
116
+ const screenVars = React.useMemo(() => {
117
+ const decls = Array.isArray(draft.variables) ? draft.variables : [];
118
+ const out = {};
119
+ for (const v of decls)
120
+ if (v && typeof v.name === 'string')
121
+ out[v.name] = v.defaultValue;
122
+ return out;
123
+ }, [draft]);
68
124
  // Only fields stored under `config` "own" a config key; spec-structured
69
125
  // blocks (waitEventConfig, etc.) and top-level timeoutMs never suppress an
70
126
  // Advanced key.
@@ -74,6 +130,10 @@ export function FlowNodeInspector({ selection, draft, onPatch, onClearSelection,
74
130
  const k = configKeyOf(f);
75
131
  if (k)
76
132
  s.add(k);
133
+ // A loose-shape fallback rooted at `config` is claimed too, so a tolerated
134
+ // legacy key (e.g. a wait node's `config.eventType`) never leaks to Advanced.
135
+ if (f.fallbackPath && f.fallbackPath.length >= 2 && f.fallbackPath[0] === 'config')
136
+ s.add(f.fallbackPath[1]);
77
137
  }
78
138
  return s;
79
139
  }, [fields]);
@@ -100,9 +160,26 @@ export function FlowNodeInspector({ selection, draft, onPatch, onClearSelection,
100
160
  onPatch({ nodes: spliceArray(nodes, index, { ...node, ...updates }) });
101
161
  };
102
162
  const hasExtras = extraJson.trim() !== '';
103
- const setField = (path, value) => {
104
- const nextNode = setAtPath(node, path, value);
105
- onPatch({ nodes: spliceArray(nodes, index, nextNode) });
163
+ // Screen nodes (and the `user_task` alias) get a live end-user preview.
164
+ const isScreen = node.type === 'screen' || node.type === 'user_task';
165
+ const setField = (field, value) => {
166
+ const path = field.path;
167
+ let nextNode = setAtPath(node, path, value);
168
+ // Migrate-on-edit: writing the canonical path drops any looser fallback
169
+ // location, so the node never carries a stale duplicate (engine + designer agree).
170
+ if (field.fallbackPath)
171
+ nextNode = setAtPath(nextNode, field.fallbackPath, undefined);
172
+ const patch = { nodes: spliceArray(nodes, index, nextNode) };
173
+ // Decision branches drive routing — mirror them onto the node's outgoing
174
+ // edges so the engine/simulator can actually branch (they read
175
+ // edge.condition, not node.config.conditions).
176
+ if (node.type === 'decision' && path.length === 2 && path[0] === 'config' && path[1] === 'conditions') {
177
+ const draftEdges = Array.isArray(draft.edges)
178
+ ? (draft.edges)
179
+ : [];
180
+ patch.edges = syncDecisionEdges(node.id, value, draftEdges);
181
+ }
182
+ onPatch(patch);
106
183
  };
107
184
  const commitAdvanced = () => {
108
185
  try {
@@ -133,7 +210,7 @@ export function FlowNodeInspector({ selection, draft, onPatch, onClearSelection,
133
210
  const typeOptions = FLOW_NODE_TYPE_OPTIONS.includes(node.type)
134
211
  ? [...FLOW_NODE_TYPE_OPTIONS]
135
212
  : [...FLOW_NODE_TYPE_OPTIONS, node.type ?? ''].filter(Boolean);
136
- return (_jsxs(InspectorShell, { kindLabel: t('engine.inspector.flowNode.kind', locale), title: node.label || node.id, onClose: onClearSelection, closeLabel: t('engine.inspector.flowNode.close', locale), footer: _jsx(InspectorRemoveButton, { label: t('engine.inspector.flowNode.remove', locale), onClick: remove, disabled: readOnly }), children: [_jsx(InspectorTextField, { label: t('engine.inspector.flowNode.id', locale), value: node.id, onCommit: (v) => patchNode({ id: v }), disabled: readOnly, mono: true }), _jsx(InspectorTextField, { label: t('engine.inspector.flowNode.label', locale), value: node.label ?? '', onCommit: (v) => patchNode({ label: v }), disabled: readOnly }), _jsx(InspectorSelectField, { label: t('engine.inspector.flowNode.type', locale), value: node.type, options: typeOptions.map((v) => ({ value: v, label: v })), onCommit: (v) => patchNode({ type: v }), disabled: readOnly }), _jsx(InspectorTextField, { label: t('engine.inspector.flowNode.description', locale), value: node.description ?? '', onCommit: (v) => patchNode({ description: v || undefined }), disabled: readOnly }), fields.length === 0 ? (_jsx("p", { className: "pt-1 text-xs italic text-muted-foreground", children: t('engine.inspector.flowNode.noConfig', locale) })) : (visibleFields.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 pt-1", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: t('engine.inspector.flowNode.configuration', locale) }), _jsx("span", { className: "h-px flex-1 bg-border", "aria-hidden": true })] }))), visibleFields.map((field) => (_jsx(FlowNodeConfigField, { field: field, value: getFieldValue(node, field), onCommit: (v) => setField(field.path, v), disabled: readOnly, locale: locale, context: { draft, node } }, field.id))), hasExtras || advReveal ? (_jsxs("details", { className: "group rounded border bg-muted/20", open: advOpen, onToggle: (e) => setAdvOpen(e.target.open), children: [_jsx("summary", { className: "cursor-pointer select-none px-2 py-1.5 text-xs font-medium text-muted-foreground", children: t('engine.inspector.flowNode.advanced', locale) }), _jsxs("div", { className: "space-y-1 border-t p-2", children: [_jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: t('engine.inspector.flowNode.advancedHint', locale) }), _jsx("textarea", { value: advText, onChange: (e) => setAdvText(e.target.value), onBlur: commitAdvanced, disabled: readOnly, rows: 6, placeholder: "{ }", className: "w-full rounded border bg-background px-2 py-1.5 font-mono text-xs" }), advError && _jsx("div", { className: "text-xs text-destructive", children: advError })] })] })) : (!readOnly && (_jsxs("button", { type: "button", onClick: () => {
213
+ return (_jsxs(InspectorShell, { kindLabel: t('engine.inspector.flowNode.kind', locale), title: node.label || node.id, onClose: onClearSelection, closeLabel: t('engine.inspector.flowNode.close', locale), footer: _jsx(InspectorRemoveButton, { label: t('engine.inspector.flowNode.remove', locale), onClick: remove, disabled: readOnly }), children: [_jsx(InspectorTextField, { label: t('engine.inspector.flowNode.id', locale), value: node.id, onCommit: (v) => patchNode({ id: v }), disabled: readOnly, mono: true }), _jsx(InspectorTextField, { label: t('engine.inspector.flowNode.label', locale), value: node.label ?? '', onCommit: (v) => patchNode({ label: v }), disabled: readOnly }), _jsx(InspectorSelectField, { label: t('engine.inspector.flowNode.type', locale), value: node.type, options: typeOptions.map((v) => ({ value: v, label: v })), onCommit: (v) => patchNode({ type: v }), disabled: readOnly }), _jsx(InspectorTextField, { label: t('engine.inspector.flowNode.description', locale), value: node.description ?? '', onCommit: (v) => patchNode({ description: v || undefined }), disabled: readOnly }), fields.length === 0 ? (_jsx("p", { className: "pt-1 text-xs italic text-muted-foreground", children: t('engine.inspector.flowNode.noConfig', locale) })) : (visibleFields.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 pt-1", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: t('engine.inspector.flowNode.configuration', locale) }), _jsx("span", { className: "h-px flex-1 bg-border", "aria-hidden": true })] }))), visibleFields.map((field) => (_jsx(FlowNodeConfigField, { field: field, value: getFieldValue(node, field), onCommit: (v) => setField(field, v), disabled: readOnly, locale: locale, context: { draft, node }, scopeGroups: scopeGroups }, field.id))), isScreen && _jsx(ScreenPreview, { node: node, variables: screenVars, className: "mt-1" }), hasExtras || advReveal ? (_jsxs("details", { className: "group rounded border bg-muted/20", open: advOpen, onToggle: (e) => setAdvOpen(e.target.open), children: [_jsx("summary", { className: "cursor-pointer select-none px-2 py-1.5 text-xs font-medium text-muted-foreground", children: t('engine.inspector.flowNode.advanced', locale) }), _jsxs("div", { className: "space-y-1 border-t p-2", children: [_jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: t('engine.inspector.flowNode.advancedHint', locale) }), _jsx("textarea", { value: advText, onChange: (e) => setAdvText(e.target.value), onBlur: commitAdvanced, disabled: readOnly, rows: 6, placeholder: "{ }", className: "w-full rounded border bg-background px-2 py-1.5 font-mono text-xs" }), advError && _jsx("div", { className: "text-xs text-destructive", children: advError })] })] })) : (!readOnly && (_jsxs("button", { type: "button", onClick: () => {
137
214
  setAdvReveal(true);
138
215
  setAdvOpen(true);
139
216
  }, className: "inline-flex items-center gap-1 self-start text-[11px] text-muted-foreground transition-colors hover:text-foreground", children: [_jsx(Plus, { className: "h-3 w-3" }), t('engine.inspector.flowNode.advanced', locale)] })))] }));
@@ -11,6 +11,7 @@
11
11
  import * as React from 'react';
12
12
  import type { FlowConfigColumn } from './flow-node-config';
13
13
  import { type FlowReferenceContext } from './FlowReferenceField';
14
+ import type { ScopeGroup } from './useFlowScope';
14
15
  export interface FlowObjectListFieldProps {
15
16
  label: string;
16
17
  columns: FlowConfigColumn[];
@@ -22,5 +23,7 @@ export interface FlowObjectListFieldProps {
22
23
  emptyLabel: string;
23
24
  /** Draft + node context so `reference` columns can resolve their options. */
24
25
  context?: FlowReferenceContext;
26
+ /** In-scope variable references for `expression` columns (#1934). */
27
+ scopeGroups?: ScopeGroup[];
25
28
  }
26
- export declare function FlowObjectListField({ label, columns, value, onCommit, disabled, addLabel, removeLabel, emptyLabel, context, }: FlowObjectListFieldProps): React.JSX.Element;
29
+ export declare function FlowObjectListField({ label, columns, value, onCommit, disabled, addLabel, removeLabel, emptyLabel, context, scopeGroups, }: FlowObjectListFieldProps): React.JSX.Element;