@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.
- package/CHANGELOG.md +560 -0
- package/dist/console/AppContent.js +23 -17
- package/dist/console/ConsoleShell.d.ts +16 -0
- package/dist/console/ConsoleShell.js +43 -2
- package/dist/console/ai/AiChatPage.js +47 -16
- package/dist/console/ai/LiveCanvas.d.ts +8 -2
- package/dist/console/ai/LiveCanvas.js +6 -4
- package/dist/console/home/HomeLayout.js +5 -7
- package/dist/console/home/HomePage.js +1 -9
- package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
- package/dist/console/organizations/OrganizationsPage.js +22 -3
- package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
- package/dist/console/organizations/provisionEnvironment.js +64 -0
- package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
- package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
- package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
- package/dist/environment/EnvironmentListToolbar.js +59 -0
- package/dist/environment/entitlements.d.ts +90 -0
- package/dist/environment/entitlements.js +91 -0
- package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
- package/dist/environment/useEnvironmentEntitlements.js +108 -0
- package/dist/hooks/useActionModal.js +15 -1
- package/dist/hooks/useAiSurface.d.ts +59 -0
- package/dist/hooks/useAiSurface.js +78 -0
- package/dist/hooks/useChatConversation.d.ts +30 -0
- package/dist/hooks/useChatConversation.js +63 -0
- package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
- package/dist/hooks/useConsoleActionRuntime.js +42 -10
- package/dist/index.d.ts +5 -2
- package/dist/index.js +10 -2
- package/dist/layout/AppHeader.js +28 -4
- package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
- package/dist/layout/ConsoleFloatingChatbot.js +41 -10
- package/dist/layout/ConsoleLayout.js +5 -6
- 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/DraftPreviewBar.js +20 -7
- 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/ExpressionProvider.js +9 -3
- package/dist/providers/MetadataProvider.js +9 -0
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +1 -1
- package/dist/utils/recordFormNavigation.d.ts +60 -0
- package/dist/utils/recordFormNavigation.js +35 -0
- package/dist/utils/resolvePageVarTokens.d.ts +31 -0
- package/dist/utils/resolvePageVarTokens.js +72 -0
- package/dist/views/CreateViewDialog.js +14 -1
- package/dist/views/FlowRunner.d.ts +2 -30
- package/dist/views/FlowRunner.js +18 -50
- package/dist/views/ObjectView.js +26 -12
- package/dist/views/ScreenView.d.ts +70 -0
- package/dist/views/ScreenView.js +73 -0
- package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
- package/dist/views/metadata-admin/AssignedUsersSection.js +151 -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.d.ts +5 -0
- package/dist/views/metadata-admin/PackagesPage.js +58 -5
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +83 -24
- package/dist/views/metadata-admin/ResourceListPage.js +28 -19
- package/dist/views/metadata-admin/StudioHomePage.js +6 -14
- package/dist/views/metadata-admin/anchors.js +20 -2
- package/dist/views/metadata-admin/createBody.d.ts +26 -0
- package/dist/views/metadata-admin/createBody.js +30 -0
- package/dist/views/metadata-admin/i18n.js +108 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +10 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +136 -8
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +99 -4
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +81 -4
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
- 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/VariableTextInput.d.ts +47 -0
- package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
- 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 +102 -0
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +67 -11
- package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
- package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
- package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
- package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
- 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 +41 -0
- package/dist/views/metadata-admin/package-scope.js +59 -0
- package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
- package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +26 -1
- package/dist/views/metadata-admin/previews/FlowCanvas.js +143 -16
- package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
- package/dist/views/metadata-admin/previews/FlowPreview.js +47 -7
- package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
- package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
- package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
- package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
- package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
- package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
- package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
- 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 +17 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +23 -6
- package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
- package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
- package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
- package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
- 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 +20 -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 +76 -2
- 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
|
@@ -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 })] }),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
87
|
+
return { id, key, raw: toRaw(val) };
|
|
67
88
|
});
|
|
68
89
|
}
|
|
69
|
-
/** Flush rows to
|
|
70
|
-
function
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
102
|
-
lastCommitted.current = serialize(
|
|
103
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
37
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
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;
|