@object-ui/app-shell 7.1.0 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/dist/console/AppContent.js +9 -15
  3. package/dist/console/ConsoleShell.d.ts +16 -0
  4. package/dist/console/ConsoleShell.js +43 -2
  5. package/dist/console/ai/AiChatPage.js +36 -9
  6. package/dist/console/home/HomeLayout.js +5 -7
  7. package/dist/console/home/HomePage.js +1 -9
  8. package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
  9. package/dist/console/organizations/OrganizationsPage.js +22 -3
  10. package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
  11. package/dist/console/organizations/provisionEnvironment.js +64 -0
  12. package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
  13. package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
  14. package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
  15. package/dist/environment/EnvironmentListToolbar.js +59 -0
  16. package/dist/environment/entitlements.d.ts +90 -0
  17. package/dist/environment/entitlements.js +91 -0
  18. package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
  19. package/dist/environment/useEnvironmentEntitlements.js +108 -0
  20. package/dist/hooks/useActionModal.js +15 -1
  21. package/dist/hooks/useAiSurface.d.ts +59 -0
  22. package/dist/hooks/useAiSurface.js +78 -0
  23. package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
  24. package/dist/hooks/useConsoleActionRuntime.js +36 -8
  25. package/dist/index.d.ts +3 -1
  26. package/dist/index.js +5 -1
  27. package/dist/layout/AppHeader.js +28 -4
  28. package/dist/layout/ConsoleFloatingChatbot.js +16 -2
  29. package/dist/layout/ConsoleLayout.js +5 -6
  30. package/dist/preview/DraftPreviewBar.js +20 -7
  31. package/dist/providers/ExpressionProvider.js +9 -3
  32. package/dist/utils/index.d.ts +2 -2
  33. package/dist/utils/index.js +1 -1
  34. package/dist/utils/recordFormNavigation.d.ts +60 -0
  35. package/dist/utils/recordFormNavigation.js +35 -0
  36. package/dist/utils/resolvePageVarTokens.d.ts +31 -0
  37. package/dist/utils/resolvePageVarTokens.js +72 -0
  38. package/dist/views/CreateViewDialog.js +14 -1
  39. package/dist/views/ObjectView.js +26 -12
  40. package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
  41. package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
  42. package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
  43. package/dist/views/metadata-admin/PackagesPage.js +49 -4
  44. package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
  45. package/dist/views/metadata-admin/ResourceEditPage.js +36 -4
  46. package/dist/views/metadata-admin/ResourceListPage.js +21 -4
  47. package/dist/views/metadata-admin/createBody.d.ts +26 -0
  48. package/dist/views/metadata-admin/createBody.js +30 -0
  49. package/dist/views/metadata-admin/i18n.js +20 -0
  50. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +8 -0
  51. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +17 -3
  52. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +16 -2
  53. package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
  54. package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
  55. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
  56. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
  57. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
  58. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
  59. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +15 -3
  60. package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
  61. package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
  62. package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
  63. package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
  64. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +6 -1
  65. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
  66. package/dist/views/metadata-admin/inspectors/flow-node-config.js +21 -10
  67. package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
  68. package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
  69. package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
  70. package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
  71. package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
  72. package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
  73. package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
  74. package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
  75. package/dist/views/metadata-admin/package-scope.d.ts +15 -0
  76. package/dist/views/metadata-admin/package-scope.js +16 -0
  77. package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
  78. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +22 -3
  79. package/dist/views/metadata-admin/previews/FlowCanvas.js +45 -6
  80. package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
  81. package/dist/views/metadata-admin/previews/FlowPreview.js +42 -30
  82. package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
  83. package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
  84. package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
  85. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
  86. package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
  87. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
  88. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +5 -3
  89. package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
  90. package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
  91. package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
  92. package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
  93. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +9 -0
  94. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +4 -2
  95. package/package.json +38 -38
@@ -20,6 +20,8 @@ import * as React from 'react';
20
20
  import { Plus, X } from 'lucide-react';
21
21
  import { Button, Input, Label } from '@object-ui/components';
22
22
  import { uniqueId } from './_shared';
23
+ import { VariableTextInput } from './VariableTextInput';
24
+ import { FlowExprIssue } from './FlowExprIssue';
23
25
  function isPlainObject(v) {
24
26
  return typeof v === 'object' && v !== null && !Array.isArray(v);
25
27
  }
@@ -58,49 +60,90 @@ function parseValue(raw) {
58
60
  }
59
61
  return raw;
60
62
  }
61
- function toRows(obj, existingIds) {
63
+ /**
64
+ * Read the stored value as `[key, value]` entries, accepting BOTH shapes a
65
+ * key/value config field can hold: the common object map (`{ var: value }`) and
66
+ * the assignment-node ARRAY form (`[{ variable|name|key, value }]`). The shape
67
+ * is preserved on write (see {@link rowsToValue}).
68
+ */
69
+ export function toEntries(value) {
70
+ if (Array.isArray(value)) {
71
+ return value
72
+ .filter((it) => isPlainObject(it))
73
+ .map((it) => {
74
+ const k = it.variable ?? it.name ?? it.key;
75
+ return [typeof k === 'string' ? k : '', it.value];
76
+ });
77
+ }
78
+ if (isPlainObject(value))
79
+ return Object.entries(value);
80
+ return [];
81
+ }
82
+ function toRows(value, existingIds) {
62
83
  const ids = [...existingIds];
63
- return Object.entries(obj).map(([key, value]) => {
84
+ return toEntries(value).map(([key, val]) => {
64
85
  const id = uniqueId('kv', ids);
65
86
  ids.push(id);
66
- return { id, key, raw: toRaw(value) };
87
+ return { id, key, raw: toRaw(val) };
67
88
  });
68
89
  }
69
- /** Flush rows to an object, skipping empty/duplicate keys (first wins). */
70
- function rowsToObject(rows) {
90
+ /** Flush rows back to the SAME shape, skipping empty/duplicate keys (first wins). */
91
+ export function rowsToValue(rows, arrayShape) {
92
+ const seen = new Set();
93
+ if (arrayShape) {
94
+ const list = [];
95
+ for (const r of rows) {
96
+ const k = r.key.trim();
97
+ if (!k || seen.has(k))
98
+ continue;
99
+ seen.add(k);
100
+ list.push({ variable: k, value: parseValue(r.raw) });
101
+ }
102
+ return list;
103
+ }
71
104
  const out = {};
72
105
  for (const r of rows) {
73
106
  const k = r.key.trim();
74
- if (!k || k in out)
107
+ if (!k || seen.has(k))
75
108
  continue;
109
+ seen.add(k);
76
110
  out[k] = parseValue(r.raw);
77
111
  }
78
112
  return out;
79
113
  }
80
- function serialize(obj) {
114
+ /** Stable serialization for the resync guard (order-insensitive for objects). */
115
+ function serialize(value) {
116
+ if (Array.isArray(value))
117
+ return JSON.stringify(value);
118
+ const obj = value ?? {};
81
119
  const sorted = Object.keys(obj).sort().reduce((acc, k) => {
82
120
  acc[k] = obj[k];
83
121
  return acc;
84
122
  }, {});
85
123
  return JSON.stringify(sorted);
86
124
  }
87
- export function FlowKeyValueField({ label, value, onCommit, disabled, help, addLabel, keyLabel, valueLabel, removeLabel, emptyLabel, }) {
88
- const external = isPlainObject(value) ? value : {};
89
- const [rows, setRows] = React.useState(() => toRows(external, []));
90
- // Track the last value we committed so an external change (node switch) can
91
- // resync rows without clobbering an in-progress edit of the same node.
92
- const lastCommitted = React.useRef(serialize(external));
125
+ export function FlowKeyValueField({ label, value, onCommit, disabled, help, addLabel, keyLabel, valueLabel, removeLabel, emptyLabel, scopeGroups, }) {
126
+ // Preserve whichever shape the value was authored in (object map vs the
127
+ // assignment-node array form) across edits.
128
+ const arrayShape = Array.isArray(value);
129
+ // Normalized serialization of the stored value used only to detect an
130
+ // EXTERNAL change (node switch) that should resync the rows.
131
+ const external = React.useMemo(() => serialize(rowsToValue(toRows(value, []), arrayShape)), [value, arrayShape]);
132
+ const [rows, setRows] = React.useState(() => toRows(value, []));
133
+ // Track the last value we committed so an external change can resync rows
134
+ // without clobbering an in-progress edit of the same node.
135
+ const lastCommitted = React.useRef(external);
93
136
  React.useEffect(() => {
94
- const next = serialize(external);
95
- if (next !== lastCommitted.current) {
96
- setRows(toRows(external, []));
97
- lastCommitted.current = next;
137
+ if (external !== lastCommitted.current) {
138
+ setRows(toRows(value, []));
139
+ lastCommitted.current = external;
98
140
  }
99
- }, [external]);
141
+ }, [external, value]);
100
142
  const flush = (nextRows) => {
101
- const obj = rowsToObject(nextRows);
102
- lastCommitted.current = serialize(obj);
103
- onCommit(Object.keys(obj).length ? obj : undefined);
143
+ const out = rowsToValue(nextRows, arrayShape);
144
+ lastCommitted.current = serialize(out);
145
+ const empty = Array.isArray(out) ? out.length === 0 : Object.keys(out).length === 0;
146
+ onCommit(empty ? undefined : out);
104
147
  };
105
148
  const setRowField = (id, patch) => {
106
149
  setRows((rs) => rs.map((r) => (r.id === id ? { ...r, ...patch } : r)));
@@ -115,11 +158,11 @@ export function FlowKeyValueField({ label, value, onCommit, disabled, help, addL
115
158
  return next;
116
159
  });
117
160
  };
118
- return (_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: label }), _jsxs("div", { className: "space-y-1.5", children: [rows.length === 0 && (_jsx("p", { className: "text-[11px] italic text-muted-foreground", children: emptyLabel })), rows.map((row) => (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Input, { value: row.key, onChange: (e) => setRowField(row.id, { key: e.target.value }), onBlur: () => flush(rows), onKeyDown: (e) => {
119
- if (e.key === 'Enter')
120
- e.target.blur();
121
- }, placeholder: keyLabel, disabled: disabled, className: "h-8 flex-1 font-mono text-xs" }), _jsx(Input, { value: row.raw, onChange: (e) => setRowField(row.id, { raw: e.target.value }), onBlur: () => flush(rows), onKeyDown: (e) => {
122
- if (e.key === 'Enter')
123
- e.target.blur();
124
- }, placeholder: valueLabel, disabled: disabled, className: "h-8 flex-1 text-xs" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-8 w-8 shrink-0 p-0 text-muted-foreground", onClick: () => removeRow(row.id), disabled: disabled, "aria-label": removeLabel, title: removeLabel, children: _jsx(X, { className: "h-3.5 w-3.5" }) })] }, row.id)))] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "h-7 w-full text-xs", onClick: addRow, disabled: disabled, children: [_jsx(Plus, { className: "mr-1 h-3.5 w-3.5" }), addLabel] }), help && _jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: help })] }));
161
+ return (_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: label }), _jsxs("div", { className: "space-y-1.5", children: [rows.length === 0 && (_jsx("p", { className: "text-[11px] italic text-muted-foreground", children: emptyLabel })), rows.map((row) => (_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Input, { value: row.key, onChange: (e) => setRowField(row.id, { key: e.target.value }), onBlur: () => flush(rows), onKeyDown: (e) => {
162
+ if (e.key === 'Enter')
163
+ e.target.blur();
164
+ }, placeholder: keyLabel, disabled: disabled, className: "h-8 flex-1 font-mono text-xs" }), _jsx(VariableTextInput, { mode: "template", value: row.raw, onValueChange: (v) => setRowField(row.id, { raw: v }), onBlur: () => flush(rows), onKeyDown: (e) => {
165
+ if (e.key === 'Enter')
166
+ e.target.blur();
167
+ }, groups: scopeGroups ?? [], placeholder: valueLabel, disabled: disabled, className: "flex-1" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-8 w-8 shrink-0 p-0 text-muted-foreground", onClick: () => removeRow(row.id), disabled: disabled, "aria-label": removeLabel, title: removeLabel, children: _jsx(X, { className: "h-3.5 w-3.5" }) })] }), _jsx(FlowExprIssue, { value: row.raw, role: "template", scopeGroups: scopeGroups })] }, row.id)))] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "h-7 w-full text-xs", onClick: addRow, disabled: disabled, children: [_jsx(Plus, { className: "mr-1 h-3.5 w-3.5" }), addLabel] }), help && _jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: help })] }));
125
168
  }
@@ -6,6 +6,7 @@
6
6
  import * as React from 'react';
7
7
  import type { FlowConfigField } from './flow-node-config';
8
8
  import { type FlowReferenceContext } from './FlowReferenceField';
9
+ import type { ScopeGroup } from './useFlowScope';
9
10
  export interface FlowNodeConfigFieldProps {
10
11
  field: FlowConfigField;
11
12
  value: unknown;
@@ -14,5 +15,7 @@ export interface FlowNodeConfigFieldProps {
14
15
  locale?: string;
15
16
  /** Draft + node context so `reference` fields can resolve their options. */
16
17
  context?: FlowReferenceContext;
18
+ /** In-scope variable references for the data-picker (#1934). */
19
+ scopeGroups?: ScopeGroup[];
17
20
  }
18
- export declare function FlowNodeConfigField({ field, value, onCommit, disabled, locale, context }: FlowNodeConfigFieldProps): React.JSX.Element;
21
+ export declare function FlowNodeConfigField({ field, value, onCommit, disabled, locale, context, scopeGroups }: FlowNodeConfigFieldProps): React.JSX.Element;
@@ -1,23 +1,26 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { t } from '../i18n';
3
- import { InspectorTextField, InspectorNumberField, InspectorSelectField, InspectorCheckboxField, } from './_shared';
3
+ import { InspectorNumberField, InspectorSelectField, InspectorCheckboxField, } from './_shared';
4
4
  import { Label } from '@object-ui/components';
5
5
  import { FlowKeyValueField } from './FlowKeyValueField';
6
6
  import { FlowStringListField } from './FlowStringListField';
7
7
  import { FlowObjectListField } from './FlowObjectListField';
8
8
  import { FlowReferenceField } from './FlowReferenceField';
9
9
  import { validateExpressionClient } from './expression-validate';
10
- export function FlowNodeConfigField({ field, value, onCommit, disabled, locale, context }) {
10
+ import { VariableTextInput } from './VariableTextInput';
11
+ import { findUnknownRefs, scopeRoots, describeUnknownRefs } from './flow-ref-check';
12
+ export function FlowNodeConfigField({ field, value, onCommit, disabled, locale, context, scopeGroups }) {
13
+ const refMode = field.refMode ?? (field.kind === 'expression' ? 'expression' : 'template');
11
14
  const control = (() => {
12
15
  switch (field.kind) {
13
16
  case 'reference':
14
17
  return (_jsx(FlowReferenceField, { field: field, value: value, onCommit: (v) => onCommit(v), disabled: disabled, context: context }));
15
18
  case 'keyValue':
16
- return (_jsx(FlowKeyValueField, { label: field.label, value: value, onCommit: (v) => onCommit(v), disabled: disabled, addLabel: t('engine.inspector.flowNode.kv.add', locale), keyLabel: t('engine.inspector.flowNode.kv.key', locale), valueLabel: t('engine.inspector.flowNode.kv.value', locale), removeLabel: t('engine.inspector.flowNode.kv.remove', locale), emptyLabel: t('engine.inspector.flowNode.kv.empty', locale) }));
19
+ return (_jsx(FlowKeyValueField, { label: field.label, value: value, onCommit: (v) => onCommit(v), disabled: disabled, addLabel: t('engine.inspector.flowNode.kv.add', locale), keyLabel: t('engine.inspector.flowNode.kv.key', locale), valueLabel: t('engine.inspector.flowNode.kv.value', locale), removeLabel: t('engine.inspector.flowNode.kv.remove', locale), emptyLabel: t('engine.inspector.flowNode.kv.empty', locale), scopeGroups: scopeGroups }));
17
20
  case 'stringList':
18
21
  return (_jsx(FlowStringListField, { label: field.label, value: value, onCommit: (v) => onCommit(v), disabled: disabled, addLabel: t('engine.inspector.flowNode.list.add', locale), itemLabel: t('engine.inspector.flowNode.list.item', locale), removeLabel: t('engine.inspector.flowNode.list.remove', locale), emptyLabel: t('engine.inspector.flowNode.list.empty', locale) }));
19
22
  case 'objectList':
20
- return (_jsx(FlowObjectListField, { label: field.label, columns: field.columns ?? [], value: value, onCommit: (v) => onCommit(v), disabled: disabled, addLabel: t('engine.inspector.flowNode.list.add', locale), removeLabel: t('engine.inspector.flowNode.list.remove', locale), emptyLabel: t('engine.inspector.flowNode.list.empty', locale), context: context }));
23
+ return (_jsx(FlowObjectListField, { label: field.label, columns: field.columns ?? [], value: value, onCommit: (v) => onCommit(v), disabled: disabled, addLabel: t('engine.inspector.flowNode.list.add', locale), removeLabel: t('engine.inspector.flowNode.list.remove', locale), emptyLabel: t('engine.inspector.flowNode.list.empty', locale), context: context, scopeGroups: scopeGroups }));
21
24
  case 'number':
22
25
  return (_jsx(InspectorNumberField, { label: field.label, value: typeof value === 'number' ? value : value != null && value !== '' ? Number(value) : undefined, placeholder: field.placeholder, onCommit: (v) => onCommit(v), disabled: disabled }));
23
26
  case 'boolean':
@@ -25,16 +28,28 @@ export function FlowNodeConfigField({ field, value, onCommit, disabled, locale,
25
28
  case 'select':
26
29
  return (_jsx(InspectorSelectField, { label: field.label, value: value != null ? String(value) : '', options: field.options ?? [], onCommit: (v) => onCommit(v), disabled: disabled }));
27
30
  case 'textarea':
28
- return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: field.label }), _jsx("textarea", { value: value != null ? String(value) : '', onChange: (e) => onCommit(e.target.value), placeholder: field.placeholder, disabled: disabled, rows: 4, className: "w-full rounded border bg-background px-2 py-1.5 font-mono text-xs" })] }));
31
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: field.label }), _jsx(VariableTextInput, { multiline: true, rows: 4, mode: refMode, value: value != null ? String(value) : '', onValueChange: (v) => onCommit(v), groups: scopeGroups ?? [], placeholder: field.placeholder, disabled: disabled })] }));
29
32
  case 'expression':
30
33
  case 'text':
31
34
  default:
32
- return (_jsx(InspectorTextField, { label: field.label, value: value != null ? String(value) : '', placeholder: field.placeholder, onCommit: (v) => onCommit(v), disabled: disabled, mono: field.kind === 'expression' }));
35
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: field.label }), _jsx(VariableTextInput, { mode: refMode, mono: field.kind === 'expression', value: value != null ? String(value) : '', onValueChange: (v) => onCommit(v), groups: scopeGroups ?? [], placeholder: field.placeholder, disabled: disabled })] }));
33
36
  }
34
37
  })();
35
38
  // ADR-0032 — surface a malformed condition (e.g. the `{record.x}` brace-in-CEL
36
- // mistake) inline, as the author types, with the same corrective message the
37
- // build and the agent tool emit. Only checked for expression-bearing fields.
39
+ // mistake) inline, with the same corrective message the build/agent emit. Only
40
+ // for expression fields (a genuine template uses single-brace `{var}` legally).
38
41
  const exprIssue = field.kind === 'expression' ? validateExpressionClient('predicate', value) : null;
39
- return (_jsxs("div", { className: "space-y-1", children: [control, exprIssue && (_jsx("p", { className: "text-[11px] leading-snug text-destructive", role: "alert", children: exprIssue.message })), field.help && !exprIssue && (_jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: field.help }))] }));
42
+ // #1934 pair the picker with a gentle, scope-aware "unknown reference"
43
+ // warning: CEL for expression fields, `{…}` holes for template fields. Skipped
44
+ // for free-form code (refMode 'expression' on a textarea, e.g. a script body)
45
+ // and when scope is unknown. The brace error above takes precedence.
46
+ const scopeRole = field.kind === 'expression'
47
+ ? 'predicate'
48
+ : refMode === 'template' && (field.kind === 'text' || field.kind === 'textarea')
49
+ ? 'template'
50
+ : null;
51
+ const unknownRefs = !exprIssue && scopeRole && scopeGroups && scopeGroups.length > 0
52
+ ? findUnknownRefs(value, scopeRole, scopeRoots(scopeGroups.flatMap((g) => g.refs)))
53
+ : [];
54
+ return (_jsxs("div", { className: "space-y-1", children: [control, exprIssue && (_jsx("p", { className: "text-[11px] leading-snug text-destructive", role: "alert", children: exprIssue.message })), !exprIssue && unknownRefs.length > 0 && (_jsx("p", { className: "text-[11px] leading-snug text-amber-600 dark:text-amber-400", role: "note", children: describeUnknownRefs(unknownRefs) })), field.help && !exprIssue && unknownRefs.length === 0 && (_jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: field.help }))] }));
40
55
  }
@@ -19,6 +19,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 = (path, value) => {
159
- const nextNode = setAtPath(node, path, value);
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.path, v), disabled: readOnly, locale: locale, context: { draft, node } }, 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: () => {
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(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) => {
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: `h-8 flex-1 text-xs${col.kind === 'expression' ? ' font-mono' : ''}` }))] }, 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] })] }));
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;
@@ -0,0 +1,95 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * VariableTextInput — a single-line input (or textarea) for an expression /
5
+ * template flow-config value, with a "{x}" data-picker (#1934) that inserts an
6
+ * in-scope reference at the cursor.
7
+ *
8
+ * Brace handling is done FOR the author (ADR-0032): the picker inserts the BARE
9
+ * reference (`discount_pct`) in `expression` fields and the braced
10
+ * `{discount_pct}` in `template` (text / textarea) fields — killing the
11
+ * recurring `{record.x}` brace-in-CEL trap. Free-text typing is untouched, and
12
+ * the picker button is hidden entirely when nothing is in scope, so an empty
13
+ * scope degrades to a plain input.
14
+ */
15
+ import * as React from 'react';
16
+ import { Braces } from 'lucide-react';
17
+ import { cn, Input, Popover, PopoverTrigger, PopoverContent, Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, } from '@object-ui/components';
18
+ /** Wrap a bare reference token for insertion into the given field mode. */
19
+ export function formatToken(token, mode) {
20
+ return mode === 'template' ? `{${token}}` : token;
21
+ }
22
+ /**
23
+ * Splice a reference into `value` at the selection `[selStart, selEnd]`, in the
24
+ * brace mode for the field, returning the new string and the caret position
25
+ * just after the inserted token. Pure (the DOM caret restore lives in the
26
+ * component); selection bounds are clamped and order-normalized so a reversed or
27
+ * out-of-range selection can't corrupt the value.
28
+ */
29
+ export function insertToken(value, mode, token, selStart, selEnd) {
30
+ const text = formatToken(token, mode);
31
+ const a = Math.min(Math.max(selStart, 0), value.length);
32
+ const b = Math.min(Math.max(selEnd, 0), value.length);
33
+ const lo = Math.min(a, b);
34
+ const hi = Math.max(a, b);
35
+ return { next: value.slice(0, lo) + text + value.slice(hi), caret: lo + text.length };
36
+ }
37
+ const PICK_LABEL = 'Insert a reference';
38
+ const SEARCH_LABEL = 'Search references…';
39
+ const EMPTY_LABEL = 'No matching references.';
40
+ export function VariableTextInput({ value, onValueChange, onBlur, onKeyDown, mode, groups, placeholder, disabled, multiline, rows = 4, mono, className, }) {
41
+ const inputRef = React.useRef(null);
42
+ // Remember the caret across the button press (which blurs the field) so the
43
+ // token lands where the author was typing — not appended at the end.
44
+ const caret = React.useRef({ start: 0, end: 0 });
45
+ const [open, setOpen] = React.useState(false);
46
+ const setRef = (el) => {
47
+ inputRef.current = el;
48
+ };
49
+ const rememberCaret = () => {
50
+ const el = inputRef.current;
51
+ if (el) {
52
+ caret.current = {
53
+ start: el.selectionStart ?? value.length,
54
+ end: el.selectionEnd ?? value.length,
55
+ };
56
+ }
57
+ };
58
+ const insert = (token) => {
59
+ const { next, caret: pos } = insertToken(value, mode, token, caret.current.start, caret.current.end);
60
+ onValueChange(next);
61
+ setOpen(false);
62
+ // Restore focus + place the caret just after the inserted token.
63
+ requestAnimationFrame(() => {
64
+ const el = inputRef.current;
65
+ if (!el)
66
+ return;
67
+ el.focus();
68
+ try {
69
+ el.setSelectionRange(pos, pos);
70
+ }
71
+ catch {
72
+ /* some input types disallow setSelectionRange */
73
+ }
74
+ caret.current = { start: pos, end: pos };
75
+ });
76
+ };
77
+ const hasScope = groups.some((g) => g.refs.length > 0);
78
+ const shared = {
79
+ value,
80
+ onChange: (e) => onValueChange(e.target.value),
81
+ onBlur,
82
+ onKeyDown,
83
+ onSelect: rememberCaret,
84
+ onKeyUp: rememberCaret,
85
+ onClick: rememberCaret,
86
+ placeholder,
87
+ disabled,
88
+ };
89
+ return (_jsxs("div", { className: cn('relative', className), children: [multiline ? (_jsx("textarea", { ref: setRef, ...shared, rows: rows, className: "w-full rounded border bg-background px-2 py-1.5 pr-8 font-mono text-xs" })) : (_jsx(Input, { ref: setRef, ...shared, className: cn('h-8 pr-8 text-sm', mono && 'font-mono') })), hasScope && !disabled && (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": PICK_LABEL, title: PICK_LABEL,
90
+ // Capture the caret on mousedown (fires before the input blur), so
91
+ // the insertion point is the author's last position.
92
+ onMouseDown: rememberCaret, className: "absolute right-1 top-1 inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", children: _jsx(Braces, { className: "h-3.5 w-3.5" }) }) }), _jsx(PopoverContent, { align: "end", className: "w-72 p-0",
93
+ // Keep our rAF focus-restore from fighting Radix's focus return.
94
+ onCloseAutoFocus: (e) => e.preventDefault(), children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: SEARCH_LABEL, className: "h-9" }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: EMPTY_LABEL }), groups.map((g) => (_jsx(CommandGroup, { heading: g.label, children: g.refs.map((ref) => (_jsxs(CommandItem, { value: `${ref.token} ${ref.label} ${ref.detail ?? ''}`, onSelect: () => insert(ref.token), className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "truncate font-mono text-xs", children: formatToken(ref.token, mode) }), ref.detail && (_jsx("span", { className: "shrink-0 truncate text-[10px] text-muted-foreground", children: ref.detail }))] }, `${g.id}:${ref.token}`))) }, g.id)))] })] }) })] }))] }));
95
+ }
@@ -39,7 +39,12 @@ export function groupToCondition(group) {
39
39
  const mop = OP_TO_MONGO[c.operator];
40
40
  if (!mop)
41
41
  continue; // unmapped (e.g. notContains/between) — drop rather than emit a bad filter
42
- parts.push({ [c.field]: { [mop]: c.value } });
42
+ // Skip incomplete rows (no value typed yet) — emitting `{field:{$op:''}}` would
43
+ // be a silently-wrong filter (matches only empty), not "no filter".
44
+ const v = c.value;
45
+ if (v == null || v === '' || (Array.isArray(v) && v.length === 0))
46
+ continue;
47
+ parts.push({ [c.field]: { [mop]: v } });
43
48
  }
44
49
  if (parts.length === 0)
45
50
  return undefined;
@@ -93,6 +93,14 @@ export interface FlowConfigField {
93
93
  * the spec's top-level `node.waitEventConfig.eventType`.
94
94
  */
95
95
  path: string[];
96
+ /**
97
+ * Optional secondary read location used when `path` holds no value — lets the
98
+ * inspector tolerate a looser on-disk shape the engine also accepts (e.g. a
99
+ * `wait` node authored with `config.eventType` instead of the spec-canonical
100
+ * `waitEventConfig.eventType`). Reads fall back to it; the inspector writes the
101
+ * canonical `path` and prunes the fallback (migrate-on-edit).
102
+ */
103
+ fallbackPath?: string[];
96
104
  /** Human-readable field label (English — repo is English-only). */
97
105
  label: string;
98
106
  kind: FlowConfigFieldKind;
@@ -120,10 +128,17 @@ export interface FlowConfigField {
120
128
  columns?: FlowConfigColumn[];
121
129
  /** Reference target for `reference` fields — drives the combobox data source. */
122
130
  ref?: FlowReferenceSpec;
131
+ /**
132
+ * Data-picker brace mode override (#1934). Defaults by kind (`expression` →
133
+ * bare CEL, `text` / `textarea` → `{var}` template). Set `'expression'` on a
134
+ * code field (e.g. a script body) so the picker inserts bare references, not
135
+ * `{var}` — `{x}` is a syntax error in a JS/TS script.
136
+ */
137
+ refMode?: 'expression' | 'template';
123
138
  }
124
139
  /** Resolve the config fields for a node type (alias-aware). */
125
140
  export declare function fieldsForNodeType(type?: string): FlowConfigField[];
126
- /** Read the current value at a field's node path. */
141
+ /** Read the current value at a field's node path, falling back to `fallbackPath`. */
127
142
  export declare function getFieldValue(node: Record<string, unknown> | null | undefined, field: FlowConfigField): unknown;
128
143
  /**
129
144
  * The `config` key this field owns, or `undefined` for fields stored outside
@@ -163,6 +163,7 @@ const FLOW_NODE_CONFIG = {
163
163
  cfg('script', 'Code', 'textarea', {
164
164
  placeholder: 'return { ok: true };',
165
165
  help: 'Script body (JS/TS).',
166
+ refMode: 'expression',
166
167
  showWhen: { field: 'actionType', equals: ['code'] },
167
168
  }),
168
169
  cfg('outputVariables', 'Output variables', 'stringList', {
@@ -315,23 +316,27 @@ const FLOW_NODE_CONFIG = {
315
316
  { value: 'condition', label: 'Condition' },
316
317
  ],
317
318
  defaultValue: 'timer',
319
+ fallbackPath: ['config', 'eventType'],
318
320
  }),
319
321
  at('waitEventConfig', 'timerDuration', 'Duration', 'text', {
320
322
  placeholder: 'PT1H · P3D',
321
323
  help: 'ISO 8601 duration (e.g. PT1H, P3D).',
322
324
  showWhen: { field: 'waitEventConfig.eventType', equals: ['timer'] },
325
+ fallbackPath: ['config', 'timerDuration'],
323
326
  }),
324
327
  at('waitEventConfig', 'signalName', 'Signal name', 'text', {
325
328
  placeholder: 'contract.renewed',
326
329
  showWhen: { field: 'waitEventConfig.eventType', equals: ['signal', 'webhook'] },
330
+ fallbackPath: ['config', 'signalName'],
327
331
  }),
328
- at('waitEventConfig', 'timeoutMs', 'Timeout (ms)', 'number', { placeholder: '3600000' }),
332
+ at('waitEventConfig', 'timeoutMs', 'Timeout (ms)', 'number', { placeholder: '3600000', fallbackPath: ['config', 'timeoutMs'] }),
329
333
  at('waitEventConfig', 'onTimeout', 'On timeout', 'select', {
330
334
  options: [
331
335
  { value: 'fail', label: 'Fail' },
332
336
  { value: 'continue', label: 'Continue' },
333
337
  ],
334
338
  defaultValue: 'fail',
339
+ fallbackPath: ['config', 'onTimeout'],
335
340
  }),
336
341
  ],
337
342
  subflow: [
@@ -436,16 +441,22 @@ export function fieldsForNodeType(type) {
436
441
  const canonical = TYPE_ALIASES[type] ?? type;
437
442
  return FLOW_NODE_CONFIG[canonical] ?? [];
438
443
  }
439
- /** Read the current value at a field's node path. */
444
+ /** Read the current value at a field's node path, falling back to `fallbackPath`. */
440
445
  export function getFieldValue(node, field) {
441
- let cur = node;
442
- for (const seg of field.path) {
443
- if (cur && typeof cur === 'object' && !Array.isArray(cur))
444
- cur = cur[seg];
445
- else
446
- return undefined;
447
- }
448
- return cur;
446
+ const read = (path) => {
447
+ let cur = node;
448
+ for (const seg of path) {
449
+ if (cur && typeof cur === 'object' && !Array.isArray(cur))
450
+ cur = cur[seg];
451
+ else
452
+ return undefined;
453
+ }
454
+ return cur;
455
+ };
456
+ const primary = read(field.path);
457
+ if (primary !== undefined)
458
+ return primary;
459
+ return field.fallbackPath ? read(field.fallbackPath) : undefined;
449
460
  }
450
461
  /**
451
462
  * The `config` key this field owns, or `undefined` for fields stored outside