@object-ui/app-shell 7.1.0 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +320 -0
- package/dist/components/ManagedByBadge.js +1 -1
- package/dist/console/AppContent.js +9 -15
- package/dist/console/ConsoleShell.d.ts +16 -0
- package/dist/console/ConsoleShell.js +43 -2
- package/dist/console/ai/AiChatPage.js +64 -14
- package/dist/console/ai/BuildDebugDrawer.d.ts +20 -0
- package/dist/console/ai/BuildDebugDrawer.js +75 -0
- package/dist/console/ai/buildDebugApi.d.ts +94 -0
- package/dist/console/ai/buildDebugApi.js +16 -0
- package/dist/console/home/HomeLayout.js +5 -7
- package/dist/console/home/HomePage.js +1 -9
- package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
- package/dist/console/organizations/OrganizationsPage.js +32 -4
- package/dist/console/organizations/manage/OrganizationLayout.js +1 -1
- package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
- package/dist/console/organizations/provisionEnvironment.js +64 -0
- package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
- package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
- package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
- package/dist/environment/EnvironmentListToolbar.js +59 -0
- package/dist/environment/entitlements.d.ts +90 -0
- package/dist/environment/entitlements.js +91 -0
- package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
- package/dist/environment/useEnvironmentEntitlements.js +108 -0
- package/dist/hooks/useActionModal.js +15 -1
- package/dist/hooks/useAiSurface.d.ts +59 -0
- package/dist/hooks/useAiSurface.js +78 -0
- package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
- package/dist/hooks/useConsoleActionRuntime.js +36 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/layout/AppHeader.js +30 -5
- package/dist/layout/ConsoleFloatingChatbot.js +22 -4
- package/dist/layout/ConsoleLayout.js +5 -6
- package/dist/layout/ContextSelectors.js +0 -19
- package/dist/layout/WorkspaceSwitcher.d.ts +14 -0
- package/dist/layout/WorkspaceSwitcher.js +76 -0
- package/dist/preview/DraftPreviewBar.js +20 -7
- package/dist/providers/ExpressionProvider.js +9 -3
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +1 -1
- package/dist/utils/managedByEmptyState.d.ts +1 -1
- package/dist/utils/managedByEmptyState.js +20 -2
- package/dist/utils/recordFormNavigation.d.ts +60 -0
- package/dist/utils/recordFormNavigation.js +35 -0
- package/dist/utils/resolvePageVarTokens.d.ts +31 -0
- package/dist/utils/resolvePageVarTokens.js +72 -0
- package/dist/views/CreateViewDialog.js +14 -1
- package/dist/views/ObjectView.js +27 -13
- package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
- package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
- package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
- package/dist/views/metadata-admin/PackagesPage.js +49 -4
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +36 -4
- package/dist/views/metadata-admin/ResourceListPage.js +25 -10
- package/dist/views/metadata-admin/StudioHomePage.js +1 -5
- package/dist/views/metadata-admin/createBody.d.ts +26 -0
- package/dist/views/metadata-admin/createBody.js +30 -0
- package/dist/views/metadata-admin/i18n.js +20 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +8 -0
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +17 -3
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +16 -2
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +15 -3
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
- package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
- package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +6 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +21 -10
- package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
- package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
- package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
- package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
- package/dist/views/metadata-admin/package-scope.d.ts +9 -19
- package/dist/views/metadata-admin/package-scope.js +11 -25
- package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +22 -3
- package/dist/views/metadata-admin/previews/FlowCanvas.js +45 -6
- package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
- package/dist/views/metadata-admin/previews/FlowPreview.js +42 -30
- package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
- package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
- package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
- package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
- package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +5 -3
- package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
- package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
- package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
- package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +9 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +4 -2
- package/package.json +38 -38
|
@@ -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
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* flow-ref-check — pure, scope-aware "unknown reference" detection for the flow
|
|
3
|
+
* inspector's inline validation (#1934 follow-up). Pairs the data-picker with a
|
|
4
|
+
* gentle warning when an authored expression / template references a name that
|
|
5
|
+
* is NOT in scope at the node — catching typos (`recrod.email`) and stale
|
|
6
|
+
* references the picker would have prevented.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately conservative — a warning that cries wolf is worse than none:
|
|
9
|
+
* • Only the ROOT of a reference path is checked (`record.email` → `record`),
|
|
10
|
+
* so a field list is never needed; if `record` is in scope the whole path is
|
|
11
|
+
* accepted.
|
|
12
|
+
* • Function / macro calls (`daysFromNow(90)`, `has(...)`, `size(...)`) are
|
|
13
|
+
* skipped — an identifier immediately followed by `(` is never a reference.
|
|
14
|
+
* • String-literal contents are stripped before scanning.
|
|
15
|
+
* • Runtime globals the engine injects (`env`, `$error`, `data`, …) and CEL
|
|
16
|
+
* keywords/literals are allow-listed.
|
|
17
|
+
* • For templates only the inside of single-brace `{…}` holes is scanned.
|
|
18
|
+
*
|
|
19
|
+
* The caller supplies the in-scope ROOT names (see {@link scopeRoots}); an empty
|
|
20
|
+
* set means "scope unknown" and the check is skipped (returns nothing).
|
|
21
|
+
*/
|
|
22
|
+
import type { ExprFieldRole } from './expression-validate';
|
|
23
|
+
import type { ScopeRef } from './flow-scope';
|
|
24
|
+
export interface UnknownRef {
|
|
25
|
+
/** The unresolved root identifier, as authored. */
|
|
26
|
+
token: string;
|
|
27
|
+
/** Nearest in-scope root within edit distance 2, when one exists (typo hint). */
|
|
28
|
+
suggestion?: string;
|
|
29
|
+
}
|
|
30
|
+
/** The set of valid root identifiers from a resolved scope's refs. */
|
|
31
|
+
export declare function scopeRoots(refs: ReadonlyArray<ScopeRef>): Set<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Find referenced roots that are not in scope. Returns [] when clean, when the
|
|
34
|
+
* source is empty, or when `knownRoots` is empty (scope unknown → don't guess).
|
|
35
|
+
*/
|
|
36
|
+
export declare function findUnknownRefs(source: unknown, role: ExprFieldRole, knownRoots: Set<string>): UnknownRef[];
|
|
37
|
+
/** Build a one-line inspector warning from unknown refs (shared by the field
|
|
38
|
+
* and edge inspectors). */
|
|
39
|
+
export declare function describeUnknownRefs(unknown: ReadonlyArray<UnknownRef>): string;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/** CEL keywords / literals that are never flow references. */
|
|
3
|
+
const CEL_RESERVED = new Set(['true', 'false', 'null', 'in']);
|
|
4
|
+
/**
|
|
5
|
+
* Roots the engine provides at runtime that won't show up in the graph-resolved
|
|
6
|
+
* scope. Conservative allow-list — better to miss a typo than flag a valid ref.
|
|
7
|
+
*/
|
|
8
|
+
const RUNTIME_GLOBALS = new Set(['env', 'request', 'context', 'user', 'now', 'today', 'self', 'data']);
|
|
9
|
+
/** The set of valid root identifiers from a resolved scope's refs. */
|
|
10
|
+
export function scopeRoots(refs) {
|
|
11
|
+
const roots = new Set();
|
|
12
|
+
for (const r of refs) {
|
|
13
|
+
const root = r.token.split('.')[0];
|
|
14
|
+
if (root)
|
|
15
|
+
roots.add(root);
|
|
16
|
+
}
|
|
17
|
+
return roots;
|
|
18
|
+
}
|
|
19
|
+
/** Bounded Levenshtein distance, giving up (returns max+1) once it exceeds `max`. */
|
|
20
|
+
function editDistance(a, b, max = 2) {
|
|
21
|
+
if (Math.abs(a.length - b.length) > max)
|
|
22
|
+
return max + 1;
|
|
23
|
+
const prev = new Array(b.length + 1);
|
|
24
|
+
for (let j = 0; j <= b.length; j++)
|
|
25
|
+
prev[j] = j;
|
|
26
|
+
for (let i = 1; i <= a.length; i++) {
|
|
27
|
+
let diag = prev[0];
|
|
28
|
+
prev[0] = i;
|
|
29
|
+
let rowMin = prev[0];
|
|
30
|
+
for (let j = 1; j <= b.length; j++) {
|
|
31
|
+
const tmp = prev[j];
|
|
32
|
+
prev[j] = Math.min(prev[j] + 1, prev[j - 1] + 1, diag + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
33
|
+
diag = tmp;
|
|
34
|
+
if (prev[j] < rowMin)
|
|
35
|
+
rowMin = prev[j];
|
|
36
|
+
}
|
|
37
|
+
if (rowMin > max)
|
|
38
|
+
return max + 1;
|
|
39
|
+
}
|
|
40
|
+
return prev[b.length];
|
|
41
|
+
}
|
|
42
|
+
/** Extract reference-position root identifiers (skipping members and calls). */
|
|
43
|
+
function rootIdentifiers(src) {
|
|
44
|
+
// Strip string literals so their contents aren't scanned as references.
|
|
45
|
+
const noStrings = src
|
|
46
|
+
.replace(/'(?:[^'\\]|\\.)*'/g, ' ')
|
|
47
|
+
.replace(/"(?:[^"\\]|\\.)*"/g, ' ');
|
|
48
|
+
const out = [];
|
|
49
|
+
// Lookbehind keeps this to ROOTS: an identifier not preceded by `.` (member),
|
|
50
|
+
// a word char, or `$`. Zero-width, so adjacent tokens are never swallowed.
|
|
51
|
+
const re = /(?<![.\w$])([A-Za-z_$][\w$]*)/g;
|
|
52
|
+
let m;
|
|
53
|
+
while ((m = re.exec(noStrings))) {
|
|
54
|
+
const name = m[1];
|
|
55
|
+
// A trailing `(` (after optional spaces) marks a function / macro call.
|
|
56
|
+
if (/^\s*\(/.test(noStrings.slice(m.index + name.length)))
|
|
57
|
+
continue;
|
|
58
|
+
if (CEL_RESERVED.has(name))
|
|
59
|
+
continue;
|
|
60
|
+
out.push(name);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Find referenced roots that are not in scope. Returns [] when clean, when the
|
|
66
|
+
* source is empty, or when `knownRoots` is empty (scope unknown → don't guess).
|
|
67
|
+
*/
|
|
68
|
+
export function findUnknownRefs(source, role, knownRoots) {
|
|
69
|
+
let raw = '';
|
|
70
|
+
if (typeof source === 'string')
|
|
71
|
+
raw = source;
|
|
72
|
+
else if (source && typeof source === 'object')
|
|
73
|
+
raw = String(source.source ?? '');
|
|
74
|
+
if (!raw.trim() || knownRoots.size === 0)
|
|
75
|
+
return [];
|
|
76
|
+
let scan = raw;
|
|
77
|
+
if (role === 'template') {
|
|
78
|
+
const holes = raw.match(/\{([^{}]+)\}/g);
|
|
79
|
+
if (!holes)
|
|
80
|
+
return [];
|
|
81
|
+
scan = holes.map((h) => h.slice(1, -1)).join(' ; ');
|
|
82
|
+
}
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
const unknown = [];
|
|
85
|
+
for (const root of rootIdentifiers(scan)) {
|
|
86
|
+
if (seen.has(root))
|
|
87
|
+
continue;
|
|
88
|
+
seen.add(root);
|
|
89
|
+
if (knownRoots.has(root) || RUNTIME_GLOBALS.has(root) || root.startsWith('$'))
|
|
90
|
+
continue;
|
|
91
|
+
let suggestion;
|
|
92
|
+
let best = 3;
|
|
93
|
+
for (const k of knownRoots) {
|
|
94
|
+
const d = editDistance(root, k, 2);
|
|
95
|
+
if (d < best) {
|
|
96
|
+
best = d;
|
|
97
|
+
suggestion = k;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
unknown.push({ token: root, suggestion: best <= 2 ? suggestion : undefined });
|
|
101
|
+
}
|
|
102
|
+
return unknown;
|
|
103
|
+
}
|
|
104
|
+
/** Build a one-line inspector warning from unknown refs (shared by the field
|
|
105
|
+
* and edge inspectors). */
|
|
106
|
+
export function describeUnknownRefs(unknown) {
|
|
107
|
+
if (unknown.length === 1) {
|
|
108
|
+
const u = unknown[0];
|
|
109
|
+
return u.suggestion
|
|
110
|
+
? `Unknown reference \`${u.token}\` — did you mean \`${u.suggestion}\`?`
|
|
111
|
+
: `\`${u.token}\` is not a reference in scope at this step.`;
|
|
112
|
+
}
|
|
113
|
+
return `Not in scope: ${unknown.map((u) => `\`${u.token}\``).join(', ')}.`;
|
|
114
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* flow-scope — pure, framework-free resolution of the in-scope variable
|
|
3
|
+
* references at a given flow node, for the inspector's variable data-picker
|
|
4
|
+
* (#1934).
|
|
5
|
+
*
|
|
6
|
+
* "In scope" is GRAPH-AWARE: a reference is offered at node N only if it can
|
|
7
|
+
* actually exist when N runs. Concretely:
|
|
8
|
+
*
|
|
9
|
+
* - Flow variables — every entry in `draft.variables[]` (declared up-front, so
|
|
10
|
+
* always in scope).
|
|
11
|
+
* - Upstream outputs — the `outputVariable(s)` / collected screen
|
|
12
|
+
* `fields[].name` / `assignments` keys / `idVariable` of every ANCESTOR node
|
|
13
|
+
* (a node from which N is reachable, found by walking edges backwards). A
|
|
14
|
+
* node's OWN outputs and any DOWNSTREAM node's outputs are deliberately
|
|
15
|
+
* excluded — they don't exist yet when N runs. This is the property the
|
|
16
|
+
* picker's "a downstream output is not offered upstream" guarantee rests on.
|
|
17
|
+
* - Loop / map iterators — the `iteratorVariable` of an enclosing loop/map
|
|
18
|
+
* ancestor, surfaced as its own group.
|
|
19
|
+
* - Trigger record — on a record-triggered flow, the trigger object's fields.
|
|
20
|
+
* Referenced BARE on the start node's own entry condition (`status`,
|
|
21
|
+
* matching the engine's trigger-evaluation context where the changed
|
|
22
|
+
* record's fields are top-level) and as `record.<field>` on every
|
|
23
|
+
* downstream node (the convention the showcase flows use). The object's
|
|
24
|
+
* field list is fetched lazily by the React layer (see useFlowScope); this
|
|
25
|
+
* module only resolves the object NAME and the correct token prefix.
|
|
26
|
+
*
|
|
27
|
+
* The graph-walk here is the unit-tested heart of the picker; async field-list
|
|
28
|
+
* expansion and rendering live in the React layer so this module stays pure.
|
|
29
|
+
*/
|
|
30
|
+
/** Which group a reference belongs to (drives the picker's section headers). */
|
|
31
|
+
export type ScopeGroupId = 'variables' | 'outputs' | 'loop' | 'trigger';
|
|
32
|
+
/**
|
|
33
|
+
* One pickable reference. `token` is the BARE form (no braces); the picker
|
|
34
|
+
* inserts it as-is into CEL `expression` fields and wraps it as `{token}` for
|
|
35
|
+
* `text` / `textarea` template fields (ADR-0032 — the brace rule is handled for
|
|
36
|
+
* the author).
|
|
37
|
+
*/
|
|
38
|
+
export interface ScopeRef {
|
|
39
|
+
token: string;
|
|
40
|
+
/** Primary display text (the token, or the bare field name). */
|
|
41
|
+
label: string;
|
|
42
|
+
/** Secondary, muted text — a type, an origin node label, etc. */
|
|
43
|
+
detail?: string;
|
|
44
|
+
group: ScopeGroupId;
|
|
45
|
+
}
|
|
46
|
+
/** The trigger object whose fields the UI layer should fetch and expand. */
|
|
47
|
+
export interface TriggerScope {
|
|
48
|
+
objectName: string;
|
|
49
|
+
/** Per-field token prefix: '' on the start node (bare), 'record.' downstream. */
|
|
50
|
+
fieldPrefix: string;
|
|
51
|
+
/** Also emit `previous.<field>` refs (update / change / before-update triggers). */
|
|
52
|
+
includePrevious: boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface FlowScope {
|
|
55
|
+
/**
|
|
56
|
+
* References resolvable WITHOUT a network fetch: flow variables, upstream
|
|
57
|
+
* outputs, loop iterators, and the whole-record `record` / `previous` tokens.
|
|
58
|
+
*/
|
|
59
|
+
refs: ScopeRef[];
|
|
60
|
+
/** Present when a record trigger is in scope — the UI expands its fields. */
|
|
61
|
+
trigger?: TriggerScope;
|
|
62
|
+
}
|
|
63
|
+
interface FlowNodeLike {
|
|
64
|
+
id?: unknown;
|
|
65
|
+
type?: unknown;
|
|
66
|
+
label?: unknown;
|
|
67
|
+
config?: unknown;
|
|
68
|
+
}
|
|
69
|
+
interface FlowEdgeLike {
|
|
70
|
+
source?: unknown;
|
|
71
|
+
target?: unknown;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Ancestor node ids of `nodeId` — every node from which `nodeId` is reachable
|
|
75
|
+
* by following edges forward (equivalently, a reverse breadth-first walk from
|
|
76
|
+
* `nodeId`). Cycle-safe (a declared `back`-edge revise loop won't spin) and
|
|
77
|
+
* never includes `nodeId` itself.
|
|
78
|
+
*/
|
|
79
|
+
export declare function flowAncestors(nodeId: string, edges: FlowEdgeLike[]): Set<string>;
|
|
80
|
+
/**
|
|
81
|
+
* The variable names a node INTRODUCES into scope for its successors — mirroring
|
|
82
|
+
* what the simulator (flow-simulator.ts) and engine actually write:
|
|
83
|
+
* `outputVariable` (single), `outputVariables` (list), an assignment node's
|
|
84
|
+
* `assignments` keys (map / array / flat shapes), a screen's collected
|
|
85
|
+
* `fields[].name`, a screen object-form's `idVariable`, and a loop/map
|
|
86
|
+
* `iteratorVariable` (flagged as a `loop` ref). The start node is NOT handled
|
|
87
|
+
* here — its trigger record is resolved separately.
|
|
88
|
+
*/
|
|
89
|
+
export declare function nodeOutputRefs(node: FlowNodeLike): ScopeRef[];
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the in-scope reference set at `nodeId` (graph-aware). Pure: the
|
|
92
|
+
* trigger object's fields are NOT expanded here (that needs an async fetch) —
|
|
93
|
+
* the returned `trigger` carries the object name and per-field token prefix for
|
|
94
|
+
* the UI layer to expand. Order: flow variables, upstream outputs, loop
|
|
95
|
+
* iterators, then trigger refs, de-duplicated by token.
|
|
96
|
+
*/
|
|
97
|
+
export declare function resolveFlowScope(draft: Record<string, unknown>, nodeId: string | undefined): FlowScope;
|
|
98
|
+
/**
|
|
99
|
+
* Expand a trigger object's fields into per-field refs — `record.<field>`
|
|
100
|
+
* downstream, bare `<field>` on the start node — given an already-fetched field
|
|
101
|
+
* list. Split out from {@link resolveFlowScope} so it is unit-testable without a
|
|
102
|
+
* metadata client.
|
|
103
|
+
*/
|
|
104
|
+
export declare function triggerFieldRefs(trigger: TriggerScope, fields: ReadonlyArray<{
|
|
105
|
+
name: string;
|
|
106
|
+
label?: string;
|
|
107
|
+
type?: string;
|
|
108
|
+
}>): ScopeRef[];
|
|
109
|
+
export {};
|