@object-ui/app-shell 7.0.0 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +560 -0
- package/dist/console/AppContent.js +23 -17
- package/dist/console/ConsoleShell.d.ts +16 -0
- package/dist/console/ConsoleShell.js +43 -2
- package/dist/console/ai/AiChatPage.js +47 -16
- package/dist/console/ai/LiveCanvas.d.ts +8 -2
- package/dist/console/ai/LiveCanvas.js +6 -4
- package/dist/console/home/HomeLayout.js +5 -7
- package/dist/console/home/HomePage.js +1 -9
- package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
- package/dist/console/organizations/OrganizationsPage.js +22 -3
- package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
- package/dist/console/organizations/provisionEnvironment.js +64 -0
- package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
- package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
- package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
- package/dist/environment/EnvironmentListToolbar.js +59 -0
- package/dist/environment/entitlements.d.ts +90 -0
- package/dist/environment/entitlements.js +91 -0
- package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
- package/dist/environment/useEnvironmentEntitlements.js +108 -0
- package/dist/hooks/useActionModal.js +15 -1
- package/dist/hooks/useAiSurface.d.ts +59 -0
- package/dist/hooks/useAiSurface.js +78 -0
- package/dist/hooks/useChatConversation.d.ts +30 -0
- package/dist/hooks/useChatConversation.js +63 -0
- package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
- package/dist/hooks/useConsoleActionRuntime.js +42 -10
- package/dist/index.d.ts +5 -2
- package/dist/index.js +10 -2
- package/dist/layout/AppHeader.js +28 -4
- package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
- package/dist/layout/ConsoleFloatingChatbot.js +41 -10
- package/dist/layout/ConsoleLayout.js +5 -6
- package/dist/layout/ContextSelectors.js +59 -35
- package/dist/layout/agentPicker.d.ts +56 -0
- package/dist/layout/agentPicker.js +40 -0
- package/dist/preview/CommitTimeline.d.ts +15 -0
- package/dist/preview/CommitTimeline.js +82 -0
- package/dist/preview/DraftPreviewBar.js +20 -7
- package/dist/preview/UnpublishedAppBar.js +11 -7
- package/dist/preview/commitHistory.d.ts +28 -0
- package/dist/preview/commitHistory.js +48 -0
- package/dist/providers/ExpressionProvider.js +9 -3
- package/dist/providers/MetadataProvider.js +9 -0
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +1 -1
- package/dist/utils/recordFormNavigation.d.ts +60 -0
- package/dist/utils/recordFormNavigation.js +35 -0
- package/dist/utils/resolvePageVarTokens.d.ts +31 -0
- package/dist/utils/resolvePageVarTokens.js +72 -0
- package/dist/views/CreateViewDialog.js +14 -1
- package/dist/views/FlowRunner.d.ts +2 -30
- package/dist/views/FlowRunner.js +18 -50
- package/dist/views/ObjectView.js +26 -12
- package/dist/views/ScreenView.d.ts +70 -0
- package/dist/views/ScreenView.js +73 -0
- package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
- package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
- package/dist/views/metadata-admin/DirectoryPage.js +2 -14
- package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
- package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
- package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
- package/dist/views/metadata-admin/PackagesPage.js +58 -5
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +83 -24
- package/dist/views/metadata-admin/ResourceListPage.js +28 -19
- package/dist/views/metadata-admin/StudioHomePage.js +6 -14
- package/dist/views/metadata-admin/anchors.js +20 -2
- package/dist/views/metadata-admin/createBody.d.ts +26 -0
- package/dist/views/metadata-admin/createBody.js +30 -0
- package/dist/views/metadata-admin/i18n.js +108 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +10 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +136 -8
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +99 -4
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +81 -4
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
- package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
- package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
- package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
- package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
- package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +102 -0
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +67 -11
- package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
- package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
- package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
- package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
- package/dist/views/metadata-admin/issuePath.d.ts +22 -0
- package/dist/views/metadata-admin/issuePath.js +65 -0
- package/dist/views/metadata-admin/package-scope.d.ts +41 -0
- package/dist/views/metadata-admin/package-scope.js +59 -0
- package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
- package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +26 -1
- package/dist/views/metadata-admin/previews/FlowCanvas.js +143 -16
- package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
- package/dist/views/metadata-admin/previews/FlowPreview.js +47 -7
- package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
- package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
- package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
- package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
- package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
- package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
- package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
- package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
- package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +17 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +23 -6
- package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
- package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
- package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
- package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
- package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
- package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
- package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
- package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +20 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +76 -2
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
- package/package.json +38 -38
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* Bridge between the visual {@link FilterBuilder} (a flat `FilterGroup` of
|
|
4
|
+
* `{field, operator, value}` rows, camelCase operators) and the spec
|
|
5
|
+
* `FilterCondition` (Mongo-style `{ field: { $op: value } }`, conjoined with
|
|
6
|
+
* `$and`) stored on `dataset.filter` / `measure.filter`.
|
|
7
|
+
*
|
|
8
|
+
* Scope (deliberate): the visual editor supports the common case — a flat AND
|
|
9
|
+
* of simple `field op value` conditions. Anything it can't faithfully round-trip
|
|
10
|
+
* (nested groups, `$or`, multi-operator objects, unmapped operators) is reported
|
|
11
|
+
* as NOT representable so the caller can fall back to the source editor instead
|
|
12
|
+
* of silently corrupting the author's filter.
|
|
13
|
+
*/
|
|
14
|
+
/** FilterBuilder camelCase operator → FilterCondition Mongo operator. */
|
|
15
|
+
const OP_TO_MONGO = {
|
|
16
|
+
equals: '$eq', notEquals: '$ne',
|
|
17
|
+
greaterThan: '$gt', greaterOrEqual: '$gte', lessThan: '$lt', lessOrEqual: '$lte',
|
|
18
|
+
after: '$gt', before: '$lt',
|
|
19
|
+
contains: '$contains', in: '$in', notIn: '$nin',
|
|
20
|
+
};
|
|
21
|
+
const MONGO_TO_OP = {
|
|
22
|
+
$eq: 'equals', $ne: 'notEquals',
|
|
23
|
+
$gt: 'greaterThan', $gte: 'greaterOrEqual', $lt: 'lessThan', $lte: 'lessOrEqual',
|
|
24
|
+
$contains: 'contains', $in: 'in', $nin: 'notIn',
|
|
25
|
+
};
|
|
26
|
+
/** Serialize the visual group → a spec FilterCondition (flat `$and`). */
|
|
27
|
+
export function groupToCondition(group) {
|
|
28
|
+
const conds = (group?.conditions ?? []).filter((c) => c && c.field);
|
|
29
|
+
const parts = [];
|
|
30
|
+
for (const c of conds) {
|
|
31
|
+
if (c.operator === 'isEmpty') {
|
|
32
|
+
parts.push({ [c.field]: { $exists: false } });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (c.operator === 'isNotEmpty') {
|
|
36
|
+
parts.push({ [c.field]: { $exists: true } });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const mop = OP_TO_MONGO[c.operator];
|
|
40
|
+
if (!mop)
|
|
41
|
+
continue; // unmapped (e.g. notContains/between) — drop rather than emit a bad filter
|
|
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 } });
|
|
48
|
+
}
|
|
49
|
+
if (parts.length === 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (parts.length === 1)
|
|
52
|
+
return parts[0];
|
|
53
|
+
return { $and: parts };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parse a stored FilterCondition → the visual group. `representable: false` when
|
|
57
|
+
* the condition uses shapes the flat builder can't faithfully edit (nested
|
|
58
|
+
* `$and`/`$or`, multi-op objects, unmapped operators) — callers should then show
|
|
59
|
+
* the source editor instead.
|
|
60
|
+
*/
|
|
61
|
+
export function conditionToGroup(cond) {
|
|
62
|
+
const empty = { id: 'g', logic: 'and', conditions: [] };
|
|
63
|
+
if (cond == null)
|
|
64
|
+
return { group: empty, representable: true };
|
|
65
|
+
if (typeof cond !== 'object' || Array.isArray(cond))
|
|
66
|
+
return { group: empty, representable: false };
|
|
67
|
+
if ('$or' in cond)
|
|
68
|
+
return { group: empty, representable: false };
|
|
69
|
+
const list = Array.isArray(cond.$and) ? cond.$and : [cond];
|
|
70
|
+
const conditions = [];
|
|
71
|
+
for (let i = 0; i < list.length; i++) {
|
|
72
|
+
const c = list[i];
|
|
73
|
+
if (!c || typeof c !== 'object' || Array.isArray(c))
|
|
74
|
+
return { group: empty, representable: false };
|
|
75
|
+
if ('$and' in c || '$or' in c)
|
|
76
|
+
return { group: empty, representable: false };
|
|
77
|
+
const keys = Object.keys(c);
|
|
78
|
+
if (keys.length !== 1)
|
|
79
|
+
return { group: empty, representable: false };
|
|
80
|
+
const field = keys[0];
|
|
81
|
+
const v = c[field];
|
|
82
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
83
|
+
const opKeys = Object.keys(v);
|
|
84
|
+
if (opKeys.length !== 1)
|
|
85
|
+
return { group: empty, representable: false };
|
|
86
|
+
const mop = opKeys[0];
|
|
87
|
+
if (mop === '$exists') {
|
|
88
|
+
conditions.push({ id: `c${i}`, field, operator: v.$exists ? 'isNotEmpty' : 'isEmpty', value: '' });
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const op = MONGO_TO_OP[mop];
|
|
92
|
+
if (!op)
|
|
93
|
+
return { group: empty, representable: false };
|
|
94
|
+
conditions.push({ id: `c${i}`, field, operator: op, value: v[mop] });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
conditions.push({ id: `c${i}`, field, operator: 'equals', value: v }); // implicit equality
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { group: { id: 'g', logic: 'and', conditions }, representable: true };
|
|
102
|
+
}
|
|
@@ -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
|
|
@@ -40,10 +40,12 @@ const FLOW_NODE_CONFIG = {
|
|
|
40
40
|
ref: { kind: 'object' },
|
|
41
41
|
placeholder: 'crm_lead',
|
|
42
42
|
help: 'Target object for record / scheduled-scan triggers.',
|
|
43
|
+
showWhen: { field: 'triggerType', equals: ['record-after-create', 'record-after-update', 'record-before-update', 'record-after-delete', 'record-change', 'schedule', 'webhook', 'event'] },
|
|
43
44
|
}),
|
|
44
45
|
cfg('condition', 'Entry condition', 'expression', {
|
|
45
46
|
placeholder: 'status == "qualifying" && previous.status != "qualifying"',
|
|
46
47
|
help: 'CEL predicate — the flow runs only when this is true. Leave empty to run on every event.',
|
|
48
|
+
showWhen: { field: 'triggerType', equals: ['record-after-create', 'record-after-update', 'record-before-update', 'record-after-delete', 'record-change', 'schedule', 'webhook', 'event'] },
|
|
47
49
|
}),
|
|
48
50
|
cfg('cron', 'Cron schedule', 'text', {
|
|
49
51
|
placeholder: '0 7 * * *',
|
|
@@ -161,6 +163,7 @@ const FLOW_NODE_CONFIG = {
|
|
|
161
163
|
cfg('script', 'Code', 'textarea', {
|
|
162
164
|
placeholder: 'return { ok: true };',
|
|
163
165
|
help: 'Script body (JS/TS).',
|
|
166
|
+
refMode: 'expression',
|
|
164
167
|
showWhen: { field: 'actionType', equals: ['code'] },
|
|
165
168
|
}),
|
|
166
169
|
cfg('outputVariables', 'Output variables', 'stringList', {
|
|
@@ -169,9 +172,19 @@ const FLOW_NODE_CONFIG = {
|
|
|
169
172
|
}),
|
|
170
173
|
{ id: 'timeoutMs', path: ['timeoutMs'], label: 'Timeout (ms)', kind: 'number', placeholder: '30000' },
|
|
171
174
|
],
|
|
175
|
+
// Screen — collect input (a flat `fields` list) OR render an object's full
|
|
176
|
+
// create/edit form (`objectName`, master-detail). `title`/`description`
|
|
177
|
+
// head the screen (description interpolates {var}); `waitForInput` forces a
|
|
178
|
+
// pause on a field-less message/confirmation screen. All optional and shown
|
|
179
|
+
// together so neither a message screen nor an object-form step needs JSON.
|
|
172
180
|
screen: [
|
|
181
|
+
cfg('title', 'Title', 'text', { placeholder: 'Request a discount', help: 'Heading shown above the screen.' }),
|
|
182
|
+
cfg('description', 'Description', 'textarea', {
|
|
183
|
+
placeholder: 'Enter the deal amount and the discount you want.',
|
|
184
|
+
help: 'Body text. Interpolates {var} references (e.g. {approval_path}).',
|
|
185
|
+
}),
|
|
173
186
|
cfg('fields', 'Fields', 'objectList', {
|
|
174
|
-
help: '
|
|
187
|
+
help: 'Input fields collected on this screen. Leave empty for a message-only screen.',
|
|
175
188
|
columns: [
|
|
176
189
|
{ key: 'name', label: 'Name', kind: 'text', placeholder: 'discount' },
|
|
177
190
|
{ key: 'label', label: 'Label', kind: 'text', placeholder: 'Discount %' },
|
|
@@ -180,6 +193,29 @@ const FLOW_NODE_CONFIG = {
|
|
|
180
193
|
{ key: 'visibleWhen', label: 'Visible when', kind: 'expression', placeholder: 'stage == "review"' },
|
|
181
194
|
],
|
|
182
195
|
}),
|
|
196
|
+
cfg('waitForInput', 'Wait for input', 'boolean', {
|
|
197
|
+
help: 'Pause to show this screen even with no fields (a message / confirmation). A field-less screen with this off is a server pass-through.',
|
|
198
|
+
}),
|
|
199
|
+
cfg('objectName', 'Object form', 'reference', {
|
|
200
|
+
ref: { kind: 'object' },
|
|
201
|
+
placeholder: 'crm_account',
|
|
202
|
+
help: 'Render this object\u2019s full create/edit form (incl. master-detail) instead of a flat field list.',
|
|
203
|
+
}),
|
|
204
|
+
cfg('idVariable', 'Saved-record variable', 'text', {
|
|
205
|
+
placeholder: 'account_id',
|
|
206
|
+
help: 'Object form only: variable bound to the saved record\u2019s id, for later steps.',
|
|
207
|
+
}),
|
|
208
|
+
cfg('mode', 'Form mode', 'select', {
|
|
209
|
+
options: [
|
|
210
|
+
{ value: 'create', label: 'Create' },
|
|
211
|
+
{ value: 'edit', label: 'Edit' },
|
|
212
|
+
],
|
|
213
|
+
defaultValue: 'create',
|
|
214
|
+
help: 'Object form only.',
|
|
215
|
+
}),
|
|
216
|
+
cfg('defaults', 'Form defaults', 'keyValue', {
|
|
217
|
+
help: 'Object form only: prefilled values (e.g. account \u2192 {account_id}).',
|
|
218
|
+
}),
|
|
183
219
|
],
|
|
184
220
|
// Approval node (ADR-0019). The node opens an approval request on entry,
|
|
185
221
|
// suspends the run, and resumes down its `approve` / `reject` out-edge once a
|
|
@@ -260,6 +296,15 @@ const FLOW_NODE_CONFIG = {
|
|
|
260
296
|
},
|
|
261
297
|
{ id: 'escalation.escalateTo', path: ['config', 'escalation', 'escalateTo'], label: 'Escalate to', kind: 'reference', ref: { kind: 'role' }, placeholder: 'user id / role / manager level', showWhen: { field: 'escalation.enabled', equals: ['true'] } },
|
|
262
298
|
{ id: 'escalation.notifySubmitter', path: ['config', 'escalation', 'notifySubmitter'], label: 'Notify submitter', kind: 'boolean', showWhen: { field: 'escalation.enabled', equals: ['true'] } },
|
|
299
|
+
// ADR-0044 send-back-for-revision guard. Surfaces from the engine's
|
|
300
|
+
// published configSchema when online; this hardcoded copy keeps it visible
|
|
301
|
+
// offline / on an older backend. Only meaningful once the node has a
|
|
302
|
+
// `revise` out-edge (author one via the canvas "add revision loop").
|
|
303
|
+
cfg('maxRevisions', 'Max revisions', 'number', {
|
|
304
|
+
placeholder: '3',
|
|
305
|
+
defaultValue: '3',
|
|
306
|
+
help: 'Max send-backs for revision before the request auto-rejects (0 disables send-back). Needs a "revise" out-edge to take effect.',
|
|
307
|
+
}),
|
|
263
308
|
],
|
|
264
309
|
wait: [
|
|
265
310
|
at('waitEventConfig', 'eventType', 'Wait for', 'select', {
|
|
@@ -271,23 +316,27 @@ const FLOW_NODE_CONFIG = {
|
|
|
271
316
|
{ value: 'condition', label: 'Condition' },
|
|
272
317
|
],
|
|
273
318
|
defaultValue: 'timer',
|
|
319
|
+
fallbackPath: ['config', 'eventType'],
|
|
274
320
|
}),
|
|
275
321
|
at('waitEventConfig', 'timerDuration', 'Duration', 'text', {
|
|
276
322
|
placeholder: 'PT1H · P3D',
|
|
277
323
|
help: 'ISO 8601 duration (e.g. PT1H, P3D).',
|
|
278
324
|
showWhen: { field: 'waitEventConfig.eventType', equals: ['timer'] },
|
|
325
|
+
fallbackPath: ['config', 'timerDuration'],
|
|
279
326
|
}),
|
|
280
327
|
at('waitEventConfig', 'signalName', 'Signal name', 'text', {
|
|
281
328
|
placeholder: 'contract.renewed',
|
|
282
329
|
showWhen: { field: 'waitEventConfig.eventType', equals: ['signal', 'webhook'] },
|
|
330
|
+
fallbackPath: ['config', 'signalName'],
|
|
283
331
|
}),
|
|
284
|
-
at('waitEventConfig', 'timeoutMs', 'Timeout (ms)', 'number', { placeholder: '3600000' }),
|
|
332
|
+
at('waitEventConfig', 'timeoutMs', 'Timeout (ms)', 'number', { placeholder: '3600000', fallbackPath: ['config', 'timeoutMs'] }),
|
|
285
333
|
at('waitEventConfig', 'onTimeout', 'On timeout', 'select', {
|
|
286
334
|
options: [
|
|
287
335
|
{ value: 'fail', label: 'Fail' },
|
|
288
336
|
{ value: 'continue', label: 'Continue' },
|
|
289
337
|
],
|
|
290
338
|
defaultValue: 'fail',
|
|
339
|
+
fallbackPath: ['config', 'onTimeout'],
|
|
291
340
|
}),
|
|
292
341
|
],
|
|
293
342
|
subflow: [
|
|
@@ -368,6 +417,7 @@ const FLOW_NODE_CONFIG = {
|
|
|
368
417
|
*/
|
|
369
418
|
const TYPE_ALIASES = {
|
|
370
419
|
action: 'legacy_action',
|
|
420
|
+
http: 'http_request',
|
|
371
421
|
branch: 'decision',
|
|
372
422
|
gateway: 'decision',
|
|
373
423
|
condition: 'decision',
|
|
@@ -391,16 +441,22 @@ export function fieldsForNodeType(type) {
|
|
|
391
441
|
const canonical = TYPE_ALIASES[type] ?? type;
|
|
392
442
|
return FLOW_NODE_CONFIG[canonical] ?? [];
|
|
393
443
|
}
|
|
394
|
-
/** 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`. */
|
|
395
445
|
export function getFieldValue(node, field) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
cur
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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;
|
|
404
460
|
}
|
|
405
461
|
/**
|
|
406
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 {};
|