@object-ui/app-shell 7.1.0 → 7.3.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 +320 -0
- package/dist/components/ManagedByBadge.js +1 -1
- package/dist/console/AppContent.js +9 -15
- package/dist/console/ConsoleShell.d.ts +16 -0
- package/dist/console/ConsoleShell.js +43 -2
- package/dist/console/ai/AiChatPage.js +64 -14
- package/dist/console/ai/BuildDebugDrawer.d.ts +20 -0
- package/dist/console/ai/BuildDebugDrawer.js +75 -0
- package/dist/console/ai/buildDebugApi.d.ts +94 -0
- package/dist/console/ai/buildDebugApi.js +16 -0
- 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 +32 -4
- package/dist/console/organizations/manage/OrganizationLayout.js +1 -1
- 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/useConsoleActionRuntime.d.ts +3 -0
- package/dist/hooks/useConsoleActionRuntime.js +36 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/layout/AppHeader.js +30 -5
- package/dist/layout/ConsoleFloatingChatbot.js +22 -4
- package/dist/layout/ConsoleLayout.js +5 -6
- package/dist/layout/ContextSelectors.js +0 -19
- package/dist/layout/WorkspaceSwitcher.d.ts +14 -0
- package/dist/layout/WorkspaceSwitcher.js +76 -0
- package/dist/preview/DraftPreviewBar.js +20 -7
- package/dist/providers/ExpressionProvider.js +9 -3
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +1 -1
- package/dist/utils/managedByEmptyState.d.ts +1 -1
- package/dist/utils/managedByEmptyState.js +20 -2
- 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/ObjectView.js +27 -13
- package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
- package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
- package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
- package/dist/views/metadata-admin/PackagesPage.js +49 -4
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +36 -4
- package/dist/views/metadata-admin/ResourceListPage.js +25 -10
- package/dist/views/metadata-admin/StudioHomePage.js +1 -5
- 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 +20 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +8 -0
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +17 -3
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +16 -2
- 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 +15 -3
- 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/VariableTextInput.d.ts +47 -0
- package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +6 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +21 -10
- 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/package-scope.d.ts +9 -19
- package/dist/views/metadata-admin/package-scope.js +11 -25
- package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +22 -3
- package/dist/views/metadata-admin/previews/FlowCanvas.js +45 -6
- package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
- package/dist/views/metadata-admin/previews/FlowPreview.js +42 -30
- package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
- 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/flow-canvas-parts.d.ts +9 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +5 -3
- 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/simulator/flow-sim-types.d.ts +9 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +4 -2
- package/package.json +38 -38
|
@@ -123,11 +123,13 @@ function MeasureFormatField({ measure, onPatch, disabled }) {
|
|
|
123
123
|
const sample = formatMeasure(kind === 'percent' ? 0.1234 : 1234.5, measure.format, measure.currency);
|
|
124
124
|
return (_jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "grid grid-cols-2 gap-1.5", children: [_jsx(InspectorSelectField, { label: "Display format", value: kind, options: FORMAT_KIND_OPTIONS, onCommit: (v) => apply(v, decimals, currency), disabled: disabled }), kind !== 'raw' && (_jsx(InspectorSelectField, { label: "Decimals", value: String(decimals), options: DECIMALS_OPTIONS, onCommit: (v) => apply(kind, parseInt(v, 10) || 0, currency), disabled: disabled }))] }), kind === 'currency' && (_jsx(InspectorSelectField, { label: "Currency", value: currency, options: CURRENCY_OPTIONS, onCommit: (v) => apply(kind, decimals, v), disabled: disabled })), kind !== 'raw' && (_jsxs("p", { className: "text-[10px] text-muted-foreground", children: ["Sample: ", _jsx("span", { className: "font-mono tabular-nums", children: sample })] }))] }));
|
|
125
125
|
}
|
|
126
|
-
/** The relationship
|
|
126
|
+
/** The relationship PATH of a `relationship[.relationship].field` reference (all
|
|
127
|
+
* segments but the final column) that isn't yet in `include`, else null. ADR-0071
|
|
128
|
+
* multi-hop: `account.owner.region` → `account.owner`. */
|
|
127
129
|
function missingRelationship(field, include) {
|
|
128
130
|
if (!field || !field.includes('.'))
|
|
129
131
|
return null;
|
|
130
|
-
const rel = field.
|
|
132
|
+
const rel = field.slice(0, field.lastIndexOf('.'));
|
|
131
133
|
return rel && !include.includes(rel) ? rel : null;
|
|
132
134
|
}
|
|
133
135
|
/** Inline author-time warning: a `relationship.field` whose join isn't declared in `include`. */
|
|
@@ -146,6 +148,18 @@ function DatasetFilterField({ label, help, value, onCommit, fields, disabled })
|
|
|
146
148
|
const count = group.conditions.length;
|
|
147
149
|
return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: label }), !representable ? (_jsxs("p", { className: "rounded-md border border-dashed bg-muted/30 px-2.5 py-1.5 text-[11px] text-muted-foreground", children: ["Advanced filter (nested / OR) \u2014 edit it in the ", _jsx("span", { className: "font-medium", children: "Source" }), " tab."] })) : (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: disabled, className: "h-8 w-full justify-between text-xs font-normal", children: [_jsx("span", { className: "truncate text-left", children: count ? `${count} condition${count === 1 ? '' : 's'}` : _jsx("span", { className: "text-muted-foreground", children: "+ Add filter\u2026" }) }), _jsx(ChevronDown, { className: "h-3.5 w-3.5 opacity-60 shrink-0" })] }) }), _jsx(PopoverContent, { align: "start", className: "w-[440px] max-w-[90vw] p-3", children: fields.length === 0 ? (_jsx("p", { className: "text-xs text-muted-foreground", children: "Pick a base object to add filter conditions." })) : (_jsx(FilterBuilder, { fields: fields, value: group, onChange: (g) => onCommit(groupToCondition(g)) })) })] })), help && _jsx("p", { className: "text-[10px] text-muted-foreground", children: help })] }));
|
|
148
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Patch for a base-object change. A dataset's joins (`include`), `dimensions`,
|
|
153
|
+
* `measures`, and `filter` all reference the OLD object's fields, so a real
|
|
154
|
+
* object change re-bases the dataset and clears them — preventing stale field
|
|
155
|
+
* refs from silently producing broken/ambiguous queries. Selecting the SAME
|
|
156
|
+
* object is a no-op (only sets `object`).
|
|
157
|
+
*/
|
|
158
|
+
export function objectChangePatch(next, current) {
|
|
159
|
+
if (next === current)
|
|
160
|
+
return { object: next };
|
|
161
|
+
return { object: next, include: [], dimensions: [], measures: [], filter: undefined };
|
|
162
|
+
}
|
|
149
163
|
export function DatasetDefaultInspector({ draft, onPatch, readOnly, name }) {
|
|
150
164
|
const label = typeof draft.label === 'string' ? draft.label : '';
|
|
151
165
|
const description = typeof draft.description === 'string' ? draft.description : '';
|
|
@@ -203,7 +217,7 @@ export function DatasetDefaultInspector({ draft, onPatch, readOnly, name }) {
|
|
|
203
217
|
if (createMode && !nameTouched.current)
|
|
204
218
|
patch.name = toFieldName(v);
|
|
205
219
|
onPatch(patch);
|
|
206
|
-
}, disabled: readOnly }), _jsx(InspectorTextField, { label: "Description", value: description, onCommit: (v) => onPatch({ description: v }), disabled: readOnly }), _jsx(InspectorComboField, { label: "Base object", value: object, onCommit: (v) => onPatch(
|
|
220
|
+
}, disabled: readOnly }), _jsx(InspectorTextField, { label: "Description", value: description, onCommit: (v) => onPatch({ description: v }), disabled: readOnly }), _jsx(InspectorComboField, { label: "Base object", value: object, onCommit: (v) => onPatch(objectChangePatch(v, object)), options: objectComboOptions, loading: objectsLoading, placeholder: "Select an object\u2026", searchPlaceholder: "Search objects\u2026", disabled: readOnly, mono: true }), object && (dimensions.length > 0 || measures.length > 0 || include.length > 0 || !!datasetFilter) && (_jsx("p", { className: "text-[10px] text-muted-foreground", children: "Changing the base object clears its dimensions, measures, joins & filters." })), _jsxs("div", { className: "border-t pt-3 space-y-1.5", children: [_jsx(SectionHeader, { title: "Included relationships", count: include.length, addLabel: "Add", onAdd: readOnly ? undefined : () => onPatch({ include: appendArray(include, '') }) }), include.length === 0 ? (_jsxs("p", { className: "rounded-md border border-dashed bg-muted/30 px-3 py-2 text-center text-[11px] text-muted-foreground", children: ["No joins. Add a relationship (a lookup field on ", _jsx("code", { children: baseLabel || 'the base object' }), ") to use ", _jsx("code", { children: "relationship.field" }), " dimensions/measures."] })) : (include.map((rel, i) => (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(InspectorComboField, { value: rel, onCommit: (v) => onPatch({ include: include.map((r, idx) => (idx === i ? v : r)) }), options: relationshipComboOptions, loading: catalogLoading, placeholder: "Select a relationship\u2026", searchPlaceholder: "Search relationships\u2026", disabled: readOnly, mono: true }), !readOnly && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 shrink-0 p-0", onClick: () => onPatch({ include: spliceArray(include, i, null) }), "aria-label": "Remove relationship", children: _jsx(X, { className: "h-3.5 w-3.5" }) }))] }, i)))), object && include.length > 0 && (_jsxs("div", { className: "flex flex-wrap items-center gap-x-1 gap-y-0.5 pt-0.5 text-[10px] text-muted-foreground", children: [_jsx("span", { className: "font-mono font-medium", children: baseLabel }), include.map((rel, i) => {
|
|
207
221
|
const r = relationships.find((x) => x.name === rel);
|
|
208
222
|
return (_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx(ArrowRight, { className: "h-3 w-3 opacity-60" }), _jsxs("span", { className: "font-mono", children: [rel, r?.referenceTo ? ` (${r.referenceTo})` : ''] })] }, i));
|
|
209
223
|
})] }))] }), _jsx("div", { className: "border-t pt-3", children: _jsx(DatasetFilterField, { label: "Scope filter", help: "Intrinsic scope, ANDed into every query (e.g. exclude soft-deleted records).", value: datasetFilter, onCommit: (fc) => onPatch({ filter: fc }), fields: filterFields, disabled: readOnly }) }), _jsxs("div", { className: "border-t pt-3 space-y-2", children: [_jsx(SectionHeader, { title: "Dimensions", count: dimensions.length, addLabel: "Add dimension", onAdd: readOnly ? undefined : () => onPatch({ dimensions: appendArray(dimensions, { name: '', field: '', type: 'string' }) }) }), dimensions.map((d, i) => (_jsxs("div", { className: "rounded-md border p-2 space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-[11px] font-medium text-muted-foreground", children: ["Dimension ", i + 1] }), !readOnly && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": "Remove dimension", title: "Remove dimension", className: "h-6 w-6 shrink-0 p-0 text-muted-foreground hover:text-destructive", onClick: () => onPatch({ dimensions: spliceArray(dimensions, i, null) }), children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }), _jsx(InspectorTextField, { label: "Name", value: d.name ?? '', onCommit: (v) => patchDimension(i, { name: v }), placeholder: "e.g. region", disabled: readOnly, mono: true }), _jsx(InspectorComboField, { label: "Field", value: d.field ?? '', onCommit: (v) => pickDimensionField(i, v), options: fieldComboOptions, loading: catalogLoading, placeholder: "field or relationship.field", searchPlaceholder: "Search fields\u2026", disabled: readOnly, mono: true }), (() => { const rel = missingRelationship(d.field, include); return rel ? _jsx(RelWarning, { rel: rel, disabled: readOnly, onAdd: () => onPatch({ include: appendArray(include, rel) }) }) : null; })(), _jsx(InspectorSelectField, { label: "Type", value: d.type, options: DIMENSION_TYPE_OPTIONS, onCommit: (v) => patchDimension(i, { type: v }), disabled: readOnly }), _jsxs(Advanced, { children: [_jsx(InspectorTextField, { label: "Label (optional)", value: d.label ?? '', onCommit: (v) => patchDimension(i, { label: v || undefined }), placeholder: d.name || 'Display label', disabled: readOnly }), d.type === 'date' && (_jsx(InspectorSelectField, { label: "Date bucket", value: d.dateGranularity ?? '', options: DATE_GRANULARITY_OPTIONS, onCommit: (v) => patchDimension(i, { dateGranularity: v || undefined }), disabled: readOnly }))] })] }, i)))] }), _jsxs("div", { className: "border-t pt-3 space-y-2", children: [_jsx(SectionHeader, { title: "Measures", count: measures.length, addLabel: "Add measure", onAdd: readOnly ? undefined : () => onPatch({ measures: appendArray(measures, { name: '', aggregate: 'sum', field: '' }) }) }), measures.map((m, i) => {
|
|
@@ -4,6 +4,9 @@ import { InspectorShell, InspectorTextField, InspectorSelectField, InspectorChec
|
|
|
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
|
}
|
|
@@ -109,11 +115,19 @@ export function FlowEdgeInspector({ selection, draft, onPatch, onClearSelection,
|
|
|
109
115
|
// Picking a branch writes the matching label; "Custom" keeps the
|
|
110
116
|
// free-text label the author typed below.
|
|
111
117
|
onCommit: (v) => { if (v)
|
|
112
|
-
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 }), _jsx(
|
|
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 })] }), (() => {
|
|
113
119
|
// ADR-0032 — flag a malformed edge guard (e.g. `{record.x}` brace-in-CEL)
|
|
114
120
|
// inline, with the same corrective message as build/agent validation.
|
|
115
121
|
const issue = isDefault ? null : validateExpressionClient('predicate', edge.condition);
|
|
116
|
-
|
|
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;
|
|
117
131
|
})(), _jsx(InspectorCheckboxField, { label: t('engine.inspector.flowEdge.isDefault', locale), value: isDefault,
|
|
118
132
|
// The default ("else") branch is taken when no other guard matches, so
|
|
119
133
|
// it carries neither a condition nor a branch label — clear both.
|
|
@@ -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,7 @@ 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';
|
|
22
23
|
import { ScreenPreview } from '../previews/ScreenPreview';
|
|
23
24
|
/**
|
|
24
25
|
* Mirror a decision node's `conditions` (branches) onto its outgoing sequence
|
|
@@ -101,6 +102,8 @@ export function FlowNodeInspector({ selection, draft, onPatch, onClearSelection,
|
|
|
101
102
|
// with the backend. Falls back to the hardcoded field group when no schema is
|
|
102
103
|
// published (offline / plugin absent / older backend).
|
|
103
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);
|
|
104
107
|
const fields = React.useMemo(() => {
|
|
105
108
|
const schema = node?.type ? configSchemas[node.type] : undefined;
|
|
106
109
|
const serverFields = schema !== undefined ? jsonSchemaToFlowFields(schema) : null;
|
|
@@ -127,6 +130,10 @@ export function FlowNodeInspector({ selection, draft, onPatch, onClearSelection,
|
|
|
127
130
|
const k = configKeyOf(f);
|
|
128
131
|
if (k)
|
|
129
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]);
|
|
130
137
|
}
|
|
131
138
|
return s;
|
|
132
139
|
}, [fields]);
|
|
@@ -155,8 +162,13 @@ export function FlowNodeInspector({ selection, draft, onPatch, onClearSelection,
|
|
|
155
162
|
const hasExtras = extraJson.trim() !== '';
|
|
156
163
|
// Screen nodes (and the `user_task` alias) get a live end-user preview.
|
|
157
164
|
const isScreen = node.type === 'screen' || node.type === 'user_task';
|
|
158
|
-
const setField = (
|
|
159
|
-
const
|
|
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);
|
|
160
172
|
const patch = { nodes: spliceArray(nodes, index, nextNode) };
|
|
161
173
|
// Decision branches drive routing — mirror them onto the node's outgoing
|
|
162
174
|
// edges so the engine/simulator can actually branch (they read
|
|
@@ -198,7 +210,7 @@ export function FlowNodeInspector({ selection, draft, onPatch, onClearSelection,
|
|
|
198
210
|
const typeOptions = FLOW_NODE_TYPE_OPTIONS.includes(node.type)
|
|
199
211
|
? [...FLOW_NODE_TYPE_OPTIONS]
|
|
200
212
|
: [...FLOW_NODE_TYPE_OPTIONS, node.type ?? ''].filter(Boolean);
|
|
201
|
-
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: () => {
|
|
202
214
|
setAdvReveal(true);
|
|
203
215
|
setAdvOpen(true);
|
|
204
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;
|
|
@@ -15,6 +15,8 @@ import { Plus, X } from 'lucide-react';
|
|
|
15
15
|
import { Button, Input, Label, Checkbox } from '@object-ui/components';
|
|
16
16
|
import { uniqueId } from './_shared';
|
|
17
17
|
import { ReferenceCombobox, resolveRefKind } from './FlowReferenceField';
|
|
18
|
+
import { VariableTextInput } from './VariableTextInput';
|
|
19
|
+
import { FlowExprIssue } from './FlowExprIssue';
|
|
18
20
|
function toRows(list, columns) {
|
|
19
21
|
const ids = [];
|
|
20
22
|
return list.map((item) => {
|
|
@@ -56,7 +58,7 @@ function rowsToList(rows, columns) {
|
|
|
56
58
|
}
|
|
57
59
|
return out;
|
|
58
60
|
}
|
|
59
|
-
export function FlowObjectListField({ label, columns, value, onCommit, disabled, addLabel, removeLabel, emptyLabel, context, }) {
|
|
61
|
+
export function FlowObjectListField({ label, columns, value, onCommit, disabled, addLabel, removeLabel, emptyLabel, context, scopeGroups, }) {
|
|
60
62
|
const external = React.useMemo(() => Array.isArray(value)
|
|
61
63
|
? value.filter((v) => v && typeof v === 'object')
|
|
62
64
|
: [], [value]);
|
|
@@ -98,8 +100,11 @@ export function FlowObjectListField({ label, columns, value, onCommit, disabled,
|
|
|
98
100
|
flush(next);
|
|
99
101
|
return next;
|
|
100
102
|
});
|
|
101
|
-
}, disabled: disabled })) : col.kind === 'reference' ? (_jsx("div", { className: "flex-1", children: _jsx(ReferenceCombobox, { resolved: resolveRefKind(col.ref, (k) => row.values[k]), value: typeof row.values[col.key] === 'string' ? row.values[col.key] : '', onCommit: (v) => setCell(row.id, col.key, typeof v === 'string' ? v : ''), onBlur: () => flush(rows), placeholder: col.placeholder, disabled: disabled, context: context, showHint: false }) })) : (_jsx(
|
|
103
|
+
}, disabled: disabled })) : col.kind === 'reference' ? (_jsx("div", { className: "flex-1", children: _jsx(ReferenceCombobox, { resolved: resolveRefKind(col.ref, (k) => row.values[k]), value: typeof row.values[col.key] === 'string' ? row.values[col.key] : '', onCommit: (v) => setCell(row.id, col.key, typeof v === 'string' ? v : ''), onBlur: () => flush(rows), placeholder: col.placeholder, disabled: disabled, context: context, showHint: false }) })) : col.kind === 'expression' ? (_jsxs("div", { className: "flex-1 space-y-1", children: [_jsx(VariableTextInput, { mode: "expression", mono: true, value: typeof row.values[col.key] === 'string' ? row.values[col.key] : '', onValueChange: (v) => setCell(row.id, col.key, v), onBlur: () => flush(rows), onKeyDown: (e) => {
|
|
104
|
+
if (e.key === 'Enter')
|
|
105
|
+
e.target.blur();
|
|
106
|
+
}, groups: scopeGroups ?? [], placeholder: col.placeholder, disabled: disabled }), _jsx(FlowExprIssue, { value: typeof row.values[col.key] === 'string' ? row.values[col.key] : '', role: "predicate", scopeGroups: scopeGroups })] })) : (_jsx(Input, { value: typeof row.values[col.key] === 'string' ? row.values[col.key] : '', onChange: (e) => setCell(row.id, col.key, e.target.value), onBlur: () => flush(rows), onKeyDown: (e) => {
|
|
102
107
|
if (e.key === 'Enter')
|
|
103
108
|
e.target.blur();
|
|
104
|
-
}, placeholder: col.placeholder, disabled: disabled, className:
|
|
109
|
+
}, placeholder: col.placeholder, disabled: disabled, className: "h-8 flex-1 text-xs" }))] }, col.key))) })] }, 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] })] }));
|
|
105
110
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VariableTextInput — a single-line input (or textarea) for an expression /
|
|
3
|
+
* template flow-config value, with a "{x}" data-picker (#1934) that inserts an
|
|
4
|
+
* in-scope reference at the cursor.
|
|
5
|
+
*
|
|
6
|
+
* Brace handling is done FOR the author (ADR-0032): the picker inserts the BARE
|
|
7
|
+
* reference (`discount_pct`) in `expression` fields and the braced
|
|
8
|
+
* `{discount_pct}` in `template` (text / textarea) fields — killing the
|
|
9
|
+
* recurring `{record.x}` brace-in-CEL trap. Free-text typing is untouched, and
|
|
10
|
+
* the picker button is hidden entirely when nothing is in scope, so an empty
|
|
11
|
+
* scope degrades to a plain input.
|
|
12
|
+
*/
|
|
13
|
+
import * as React from 'react';
|
|
14
|
+
import type { ScopeGroup } from './useFlowScope';
|
|
15
|
+
export type VariableFieldMode = 'expression' | 'template';
|
|
16
|
+
/** Wrap a bare reference token for insertion into the given field mode. */
|
|
17
|
+
export declare function formatToken(token: string, mode: VariableFieldMode): string;
|
|
18
|
+
/**
|
|
19
|
+
* Splice a reference into `value` at the selection `[selStart, selEnd]`, in the
|
|
20
|
+
* brace mode for the field, returning the new string and the caret position
|
|
21
|
+
* just after the inserted token. Pure (the DOM caret restore lives in the
|
|
22
|
+
* component); selection bounds are clamped and order-normalized so a reversed or
|
|
23
|
+
* out-of-range selection can't corrupt the value.
|
|
24
|
+
*/
|
|
25
|
+
export declare function insertToken(value: string, mode: VariableFieldMode, token: string, selStart: number, selEnd: number): {
|
|
26
|
+
next: string;
|
|
27
|
+
caret: number;
|
|
28
|
+
};
|
|
29
|
+
export interface VariableTextInputProps {
|
|
30
|
+
value: string;
|
|
31
|
+
onValueChange: (v: string) => void;
|
|
32
|
+
onBlur?: () => void;
|
|
33
|
+
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
|
34
|
+
/** Brace rule: bare token for `expression`, `{token}` for `template`. */
|
|
35
|
+
mode: VariableFieldMode;
|
|
36
|
+
/** In-scope reference groups (from useFlowScope). Empty → no picker button. */
|
|
37
|
+
groups: ScopeGroup[];
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
/** Render a multi-line `<textarea>` instead of a single-line input. */
|
|
41
|
+
multiline?: boolean;
|
|
42
|
+
rows?: number;
|
|
43
|
+
/** Monospace the input text (expressions). Textareas are always mono. */
|
|
44
|
+
mono?: boolean;
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function VariableTextInput({ value, onValueChange, onBlur, onKeyDown, mode, groups, placeholder, disabled, multiline, rows, mono, className, }: VariableTextInputProps): React.JSX.Element;
|