@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.
- package/CHANGELOG.md +279 -0
- 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 +36 -9
- package/dist/console/home/HomeLayout.js +5 -7
- package/dist/console/home/HomePage.js +1 -9
- package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
- package/dist/console/organizations/OrganizationsPage.js +22 -3
- package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
- package/dist/console/organizations/provisionEnvironment.js +64 -0
- package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
- package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
- package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
- package/dist/environment/EnvironmentListToolbar.js +59 -0
- package/dist/environment/entitlements.d.ts +90 -0
- package/dist/environment/entitlements.js +91 -0
- package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
- package/dist/environment/useEnvironmentEntitlements.js +108 -0
- package/dist/hooks/useActionModal.js +15 -1
- package/dist/hooks/useAiSurface.d.ts +59 -0
- package/dist/hooks/useAiSurface.js +78 -0
- package/dist/hooks/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 +28 -4
- package/dist/layout/ConsoleFloatingChatbot.js +16 -2
- package/dist/layout/ConsoleLayout.js +5 -6
- 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/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 +26 -12
- 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 +21 -4
- 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 -0
- 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 +15 -0
- package/dist/views/metadata-admin/package-scope.js +16 -0
- 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
|
@@ -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;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
cur
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|