@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
|
@@ -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 {};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/** Trigger types that fire on a single record (so `record` is in scope). */
|
|
3
|
+
const RECORD_TRIGGER_TYPES = new Set([
|
|
4
|
+
'record-after-create',
|
|
5
|
+
'record-after-update',
|
|
6
|
+
'record-before-update',
|
|
7
|
+
'record-after-delete',
|
|
8
|
+
'record-change',
|
|
9
|
+
]);
|
|
10
|
+
/** Trigger types that carry a meaningful `previous` snapshot of the record. */
|
|
11
|
+
const PREVIOUS_TRIGGER_TYPES = new Set([
|
|
12
|
+
'record-after-update',
|
|
13
|
+
'record-before-update',
|
|
14
|
+
'record-change',
|
|
15
|
+
]);
|
|
16
|
+
function asArray(v) {
|
|
17
|
+
return Array.isArray(v) ? v : [];
|
|
18
|
+
}
|
|
19
|
+
function asRecord(v) {
|
|
20
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
21
|
+
}
|
|
22
|
+
function str(v) {
|
|
23
|
+
return typeof v === 'string' && v ? v : undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Ancestor node ids of `nodeId` — every node from which `nodeId` is reachable
|
|
27
|
+
* by following edges forward (equivalently, a reverse breadth-first walk from
|
|
28
|
+
* `nodeId`). Cycle-safe (a declared `back`-edge revise loop won't spin) and
|
|
29
|
+
* never includes `nodeId` itself.
|
|
30
|
+
*/
|
|
31
|
+
export function flowAncestors(nodeId, edges) {
|
|
32
|
+
const rev = new Map();
|
|
33
|
+
for (const e of edges) {
|
|
34
|
+
const s = str(e.source);
|
|
35
|
+
const t = str(e.target);
|
|
36
|
+
if (!s || !t)
|
|
37
|
+
continue;
|
|
38
|
+
const list = rev.get(t);
|
|
39
|
+
if (list)
|
|
40
|
+
list.push(s);
|
|
41
|
+
else
|
|
42
|
+
rev.set(t, [s]);
|
|
43
|
+
}
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
const stack = [...(rev.get(nodeId) ?? [])];
|
|
46
|
+
while (stack.length) {
|
|
47
|
+
const cur = stack.pop();
|
|
48
|
+
if (cur === nodeId || seen.has(cur))
|
|
49
|
+
continue;
|
|
50
|
+
seen.add(cur);
|
|
51
|
+
for (const p of rev.get(cur) ?? [])
|
|
52
|
+
stack.push(p);
|
|
53
|
+
}
|
|
54
|
+
return seen;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The variable names a node INTRODUCES into scope for its successors — mirroring
|
|
58
|
+
* what the simulator (flow-simulator.ts) and engine actually write:
|
|
59
|
+
* `outputVariable` (single), `outputVariables` (list), an assignment node's
|
|
60
|
+
* `assignments` keys (map / array / flat shapes), a screen's collected
|
|
61
|
+
* `fields[].name`, a screen object-form's `idVariable`, and a loop/map
|
|
62
|
+
* `iteratorVariable` (flagged as a `loop` ref). The start node is NOT handled
|
|
63
|
+
* here — its trigger record is resolved separately.
|
|
64
|
+
*/
|
|
65
|
+
export function nodeOutputRefs(node) {
|
|
66
|
+
const type = str(node.type);
|
|
67
|
+
const cfg = asRecord(node.config);
|
|
68
|
+
const nodeId = str(node.id) ?? '';
|
|
69
|
+
const label = str(node.label);
|
|
70
|
+
const detail = label && label !== nodeId ? label : nodeId || undefined;
|
|
71
|
+
const out = [];
|
|
72
|
+
const add = (token, group = 'outputs') => {
|
|
73
|
+
if (token && !out.some((r) => r.token === token)) {
|
|
74
|
+
out.push({ token, label: token, detail, group });
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
// Loop / map iterator — its own group.
|
|
78
|
+
if (type === 'loop' || type === 'map')
|
|
79
|
+
add(str(cfg.iteratorVariable), 'loop');
|
|
80
|
+
// Single + multi output variables (create/get/http/subflow/map/end/script).
|
|
81
|
+
add(str(cfg.outputVariable));
|
|
82
|
+
for (const name of asArray(cfg.outputVariables))
|
|
83
|
+
add(str(name));
|
|
84
|
+
// Screen object-form: the saved record's id is bound to a variable.
|
|
85
|
+
add(str(cfg.idVariable));
|
|
86
|
+
// Assignment node — the assigned variable names. Accepts the three authoring
|
|
87
|
+
// shapes the simulator/engine accept: an array of `{variable|name|key}`, a
|
|
88
|
+
// flat `{ var: value }` map, or (legacy) keys directly on config.
|
|
89
|
+
if (type === 'assignment') {
|
|
90
|
+
const raw = cfg.assignments;
|
|
91
|
+
if (Array.isArray(raw)) {
|
|
92
|
+
for (const item of raw) {
|
|
93
|
+
const e = asRecord(item);
|
|
94
|
+
add(str(e.variable) ?? str(e.name) ?? str(e.key));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else if (raw && typeof raw === 'object') {
|
|
98
|
+
for (const k of Object.keys(raw))
|
|
99
|
+
add(k);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Screen — collected input field names become variables for downstream nodes.
|
|
103
|
+
if (type === 'screen' || type === 'user_task') {
|
|
104
|
+
for (const f of asArray(cfg.fields))
|
|
105
|
+
add(str(asRecord(f).name));
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
/** De-duplicate by token, keeping the first (group-priority) occurrence. */
|
|
110
|
+
function dedupeByToken(refs) {
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
return refs.filter((r) => (seen.has(r.token) ? false : (seen.add(r.token), true)));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve the in-scope reference set at `nodeId` (graph-aware). Pure: the
|
|
116
|
+
* trigger object's fields are NOT expanded here (that needs an async fetch) —
|
|
117
|
+
* the returned `trigger` carries the object name and per-field token prefix for
|
|
118
|
+
* the UI layer to expand. Order: flow variables, upstream outputs, loop
|
|
119
|
+
* iterators, then trigger refs, de-duplicated by token.
|
|
120
|
+
*/
|
|
121
|
+
export function resolveFlowScope(draft, nodeId) {
|
|
122
|
+
const nodes = asArray(draft.nodes).map(asRecord);
|
|
123
|
+
const edges = asArray(draft.edges);
|
|
124
|
+
const refs = [];
|
|
125
|
+
// 1. Flow variables — always in scope (declared up-front).
|
|
126
|
+
for (const v of asArray(draft.variables)) {
|
|
127
|
+
const rec = asRecord(v);
|
|
128
|
+
const name = str(rec.name);
|
|
129
|
+
if (!name)
|
|
130
|
+
continue;
|
|
131
|
+
const type = str(rec.type);
|
|
132
|
+
refs.push({ token: name, label: name, detail: type ? `variable · ${type}` : 'variable', group: 'variables' });
|
|
133
|
+
}
|
|
134
|
+
if (!nodeId)
|
|
135
|
+
return { refs: dedupeByToken(refs) };
|
|
136
|
+
// 2. Upstream outputs + loop iterators — from ANCESTOR nodes only.
|
|
137
|
+
const ancestors = flowAncestors(nodeId, edges);
|
|
138
|
+
const startNode = nodes.find((n) => str(n.type) === 'start');
|
|
139
|
+
const startId = str(startNode?.id);
|
|
140
|
+
for (const node of nodes) {
|
|
141
|
+
const id = str(node.id);
|
|
142
|
+
if (!id || id === nodeId || !ancestors.has(id))
|
|
143
|
+
continue;
|
|
144
|
+
if (id === startId)
|
|
145
|
+
continue; // the start node contributes the trigger record, below
|
|
146
|
+
for (const ref of nodeOutputRefs(node))
|
|
147
|
+
refs.push(ref);
|
|
148
|
+
}
|
|
149
|
+
// 3. Trigger record — on a record-triggered flow, when the start node is in
|
|
150
|
+
// scope: either we ARE the start node (editing its own entry condition) or
|
|
151
|
+
// the start node is an ancestor (it is the ancestor of everything reachable).
|
|
152
|
+
if (startNode) {
|
|
153
|
+
const cfg = asRecord(startNode.config);
|
|
154
|
+
const triggerType = str(cfg.triggerType);
|
|
155
|
+
const objectName = str(cfg.objectName);
|
|
156
|
+
const isRecordTrigger = !!triggerType && RECORD_TRIGGER_TYPES.has(triggerType);
|
|
157
|
+
const startInScope = startId === nodeId || (!!startId && ancestors.has(startId));
|
|
158
|
+
if (isRecordTrigger && objectName && startInScope) {
|
|
159
|
+
const onStart = startId === nodeId;
|
|
160
|
+
const includePrevious = PREVIOUS_TRIGGER_TYPES.has(triggerType);
|
|
161
|
+
// On the start node the record's fields ARE the bare evaluation context
|
|
162
|
+
// (`status`), so the whole record is not a named ref there; `previous` is
|
|
163
|
+
// (`previous.status`). Downstream the record is the named `record` object.
|
|
164
|
+
if (!onStart) {
|
|
165
|
+
refs.push({ token: 'record', label: 'record', detail: `trigger record · ${objectName}`, group: 'trigger' });
|
|
166
|
+
}
|
|
167
|
+
if (includePrevious) {
|
|
168
|
+
refs.push({ token: 'previous', label: 'previous', detail: 'record values before the change', group: 'trigger' });
|
|
169
|
+
}
|
|
170
|
+
return { refs: dedupeByToken(refs), trigger: { objectName, fieldPrefix: onStart ? '' : 'record.', includePrevious } };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { refs: dedupeByToken(refs) };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Expand a trigger object's fields into per-field refs — `record.<field>`
|
|
177
|
+
* downstream, bare `<field>` on the start node — given an already-fetched field
|
|
178
|
+
* list. Split out from {@link resolveFlowScope} so it is unit-testable without a
|
|
179
|
+
* metadata client.
|
|
180
|
+
*/
|
|
181
|
+
export function triggerFieldRefs(trigger, fields) {
|
|
182
|
+
const out = [];
|
|
183
|
+
for (const f of fields) {
|
|
184
|
+
if (!f?.name)
|
|
185
|
+
continue;
|
|
186
|
+
const token = `${trigger.fieldPrefix}${f.name}`;
|
|
187
|
+
const detail = f.label && f.label !== f.name ? f.label : f.type;
|
|
188
|
+
out.push({ token, label: token, detail, group: 'trigger' });
|
|
189
|
+
if (trigger.includePrevious) {
|
|
190
|
+
out.push({
|
|
191
|
+
token: `previous.${f.name}`,
|
|
192
|
+
label: `previous.${f.name}`,
|
|
193
|
+
detail: detail ? `prior ${detail}` : 'prior value',
|
|
194
|
+
group: 'trigger',
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
@@ -46,10 +46,21 @@ export interface NormalizedObject {
|
|
|
46
46
|
/** Normalize a raw object metadata doc into label + fields + relationships. */
|
|
47
47
|
export declare function normalizeObject(doc: Record<string, unknown> | null | undefined, name: string): NormalizedObject;
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
49
|
+
* Walk a dotted relationship PATH from the base object, returning the object at
|
|
50
|
+
* its end (whose fields a `path.field` references) plus each hop's relationship
|
|
51
|
+
* label, or undefined if any hop can't be resolved (ADR-0071 multi-hop).
|
|
52
|
+
* `objectsByName` holds the already-fetched objects along the chain.
|
|
51
53
|
*/
|
|
52
|
-
export declare function
|
|
54
|
+
export declare function resolvePath(base: NormalizedObject, path: string, objectsByName: Record<string, NormalizedObject>): {
|
|
55
|
+
target: NormalizedObject;
|
|
56
|
+
labels: string[];
|
|
57
|
+
} | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Build the flat `field` / `relationship[.relationship].field` option list from
|
|
60
|
+
* the base object and the (already-fetched) objects along each included PATH.
|
|
61
|
+
* Single-hop paths behave exactly as before.
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildFieldOptions(base: NormalizedObject, include: string[], objectsByName: Record<string, NormalizedObject>): DatasetFieldOption[];
|
|
53
64
|
/** Recursively test whether a metadata doc references `datasetName` via a `dataset` key. */
|
|
54
65
|
export declare function referencesDataset(doc: unknown, datasetName: string): boolean;
|
|
55
66
|
/** Every object as `{ name, label }`, sorted by label. Fetched once. */
|
|
Binary file
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ScopeGroupId, type ScopeRef } from './flow-scope';
|
|
2
|
+
export interface ScopeGroup {
|
|
3
|
+
id: ScopeGroupId;
|
|
4
|
+
label: string;
|
|
5
|
+
refs: ScopeRef[];
|
|
6
|
+
}
|
|
7
|
+
export interface UseFlowScopeResult {
|
|
8
|
+
/** Non-empty groups, in display order. */
|
|
9
|
+
groups: ScopeGroup[];
|
|
10
|
+
/** Flat, de-duplicated ref list (all groups). */
|
|
11
|
+
refs: ScopeRef[];
|
|
12
|
+
/** True while the trigger object's fields are still loading. */
|
|
13
|
+
loading: boolean;
|
|
14
|
+
/** No references in scope — the field should render as a plain input. */
|
|
15
|
+
isEmpty: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve + (async) expand the in-scope references at a flow node. `draft` is
|
|
19
|
+
* the whole flow draft; `nodeId` the node being edited (for an edge, pass its
|
|
20
|
+
* source node id — references available on an edge are those in scope at its
|
|
21
|
+
* source).
|
|
22
|
+
*/
|
|
23
|
+
export declare function useFlowScope(draft: Record<string, unknown> | undefined, nodeId: string | undefined): UseFlowScopeResult;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* useFlowScope — React adapter over the pure {@link resolveFlowScope} graph-walk
|
|
4
|
+
* that powers the inspector's variable data-picker (#1934).
|
|
5
|
+
*
|
|
6
|
+
* It resolves the graph-aware refs synchronously, then lazily fetches the
|
|
7
|
+
* trigger object's field catalog (via the shared metadata client) and merges
|
|
8
|
+
* the expanded `record.<field>` refs in — grouping everything into the ordered
|
|
9
|
+
* sections the picker renders. An empty result (`isEmpty`) tells the field to
|
|
10
|
+
* degrade to a plain input.
|
|
11
|
+
*/
|
|
12
|
+
import * as React from 'react';
|
|
13
|
+
import { resolveFlowScope, triggerFieldRefs, } from './flow-scope';
|
|
14
|
+
import { useObjectFields } from '../previews/useObjectFields';
|
|
15
|
+
const GROUP_ORDER = ['variables', 'outputs', 'loop', 'trigger'];
|
|
16
|
+
const GROUP_LABELS = {
|
|
17
|
+
variables: 'Flow variables',
|
|
18
|
+
outputs: 'Upstream outputs',
|
|
19
|
+
loop: 'Loop item',
|
|
20
|
+
trigger: 'Trigger record',
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Resolve + (async) expand the in-scope references at a flow node. `draft` is
|
|
24
|
+
* the whole flow draft; `nodeId` the node being edited (for an edge, pass its
|
|
25
|
+
* source node id — references available on an edge are those in scope at its
|
|
26
|
+
* source).
|
|
27
|
+
*/
|
|
28
|
+
export function useFlowScope(draft, nodeId) {
|
|
29
|
+
const scope = React.useMemo(() => resolveFlowScope(draft ?? {}, nodeId), [draft, nodeId]);
|
|
30
|
+
const { fields, loading } = useObjectFields(scope.trigger?.objectName);
|
|
31
|
+
return React.useMemo(() => {
|
|
32
|
+
const all = [...scope.refs];
|
|
33
|
+
if (scope.trigger)
|
|
34
|
+
all.push(...triggerFieldRefs(scope.trigger, fields));
|
|
35
|
+
// Global de-dup by token (a declared var also written upstream shows once).
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
const refs = all.filter((r) => (seen.has(r.token) ? false : (seen.add(r.token), true)));
|
|
38
|
+
const groups = GROUP_ORDER.map((id) => ({
|
|
39
|
+
id,
|
|
40
|
+
label: GROUP_LABELS[id],
|
|
41
|
+
refs: refs.filter((r) => r.group === id),
|
|
42
|
+
})).filter((g) => g.refs.length > 0);
|
|
43
|
+
return { groups, refs, loading: !!scope.trigger && loading, isEmpty: refs.length === 0 };
|
|
44
|
+
}, [scope, fields, loading]);
|
|
45
|
+
}
|
|
@@ -24,3 +24,18 @@ export declare function buildPackageScopeOptions(rawList: unknown[] | null | und
|
|
|
24
24
|
id: string;
|
|
25
25
|
name: string;
|
|
26
26
|
}[];
|
|
27
|
+
/**
|
|
28
|
+
* True for the runtime/null "Local / Custom" sentinel scope. Per ADR-0070 D5
|
|
29
|
+
* this is a *migration* surface (move loose items into a base), never a valid
|
|
30
|
+
* create destination — callers gate "create" on a real writable base.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isLocalScope(id: string | null | undefined): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The writable bases (project-scoped DB packages) from the raw package list —
|
|
35
|
+
* the only valid authoring destinations (ADR-0070 D2). Excludes code/installed
|
|
36
|
+
* (system|cloud) packages AND the Local sentinel.
|
|
37
|
+
*/
|
|
38
|
+
export declare function writableBaseOptions(rawList: unknown[] | null | undefined): {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
}[];
|
|
@@ -41,3 +41,19 @@ export function buildPackageScopeOptions(rawList) {
|
|
|
41
41
|
// preserved; the user opts into the local scope explicitly.
|
|
42
42
|
return [...opts, { id: LOCAL_PACKAGE_ID, name: t('engine.package.local', detectLocale()) }];
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* True for the runtime/null "Local / Custom" sentinel scope. Per ADR-0070 D5
|
|
46
|
+
* this is a *migration* surface (move loose items into a base), never a valid
|
|
47
|
+
* create destination — callers gate "create" on a real writable base.
|
|
48
|
+
*/
|
|
49
|
+
export function isLocalScope(id) {
|
|
50
|
+
return !id || id === LOCAL_PACKAGE_ID;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The writable bases (project-scoped DB packages) from the raw package list —
|
|
54
|
+
* the only valid authoring destinations (ADR-0070 D2). Excludes code/installed
|
|
55
|
+
* (system|cloud) packages AND the Local sentinel.
|
|
56
|
+
*/
|
|
57
|
+
export function writableBaseOptions(rawList) {
|
|
58
|
+
return buildPackageScopeOptions(rawList).filter((o) => o.id !== LOCAL_PACKAGE_ID);
|
|
59
|
+
}
|
|
@@ -84,6 +84,18 @@ export interface MetadataPreviewProps {
|
|
|
84
84
|
* "click widget → edit widget in the right panel" pattern.
|
|
85
85
|
*/
|
|
86
86
|
onSelectionChange?: (selection: MetadataSelection | null) => void;
|
|
87
|
+
/**
|
|
88
|
+
* Optional: server-computed validation diagnostics for the current draft
|
|
89
|
+
* (the layered record's `_diagnostics`, kept in sync with live client-side
|
|
90
|
+
* issues). Each entry carries a dotted JSON path so a preview can map it onto
|
|
91
|
+
* the offending sub-element. Used by the flow preview's Problems panel +
|
|
92
|
+
* on-canvas badges; ignored by previews that don't surface diagnostics.
|
|
93
|
+
*/
|
|
94
|
+
diagnostics?: Array<{
|
|
95
|
+
path?: string;
|
|
96
|
+
message: string;
|
|
97
|
+
severity?: 'error' | 'warning';
|
|
98
|
+
}>;
|
|
87
99
|
}
|
|
88
100
|
export type MetadataPreview = ComponentType<MetadataPreviewProps>;
|
|
89
101
|
/**
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import * as React from 'react';
|
|
22
22
|
import { type FlowNode, type FlowEdge } from './flow-canvas-layout';
|
|
23
|
+
import { type FlowProblem } from './flow-problems';
|
|
23
24
|
export interface FlowCanvasProps {
|
|
24
25
|
nodes: FlowNode[];
|
|
25
26
|
edges: FlowEdge[];
|
|
@@ -39,11 +40,29 @@ export interface FlowCanvasProps {
|
|
|
39
40
|
invalidNodeIds?: string[];
|
|
40
41
|
/** Structural-validation: edges (keyed `${source}->${target}`) to paint red. */
|
|
41
42
|
invalidEdges?: ReadonlySet<string>;
|
|
42
|
-
/**
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Select + reveal a problem when its inline-banner row is clicked — wired to
|
|
45
|
+
* the same handler the Problems panel uses, so the always-visible banner is
|
|
46
|
+
* actionable without opening the panel.
|
|
47
|
+
*/
|
|
48
|
+
onRevealProblem?: (problem: FlowProblem) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Unified validation issues (structural + server) rendered as per-element
|
|
51
|
+
* badges; the Problems panel shares the same list.
|
|
52
|
+
*/
|
|
53
|
+
problems?: FlowProblem[];
|
|
54
|
+
/**
|
|
55
|
+
* Imperative "reveal" request from the Problems panel: when `nonce` changes
|
|
56
|
+
* the canvas pans to center the targeted node/edge. Selection highlight is
|
|
57
|
+
* driven separately via `selectedId` / `selectedEdgeId`.
|
|
58
|
+
*/
|
|
59
|
+
revealSignal?: {
|
|
60
|
+
target: FlowProblem['target'];
|
|
61
|
+
nonce: number;
|
|
62
|
+
} | null;
|
|
44
63
|
onSelect: (node: FlowNode | null) => void;
|
|
45
64
|
/** Select an edge (its `edgeKey`), or clear selection with `null`. */
|
|
46
65
|
onSelectEdge?: (edge: FlowEdge | null, key: string) => void;
|
|
47
66
|
onPatch?: (partial: Record<string, unknown>) => void;
|
|
48
67
|
}
|
|
49
|
-
export declare function FlowCanvas({ nodes, edges, editable, designMode, selectedId, selectedEdgeId, locale, activeNodeId, visitedNodeIds, traversedEdgeIds, invalidNodeIds, invalidEdges,
|
|
68
|
+
export declare function FlowCanvas({ nodes, edges, editable, designMode, selectedId, selectedEdgeId, locale, activeNodeId, visitedNodeIds, traversedEdgeIds, invalidNodeIds, invalidEdges, onRevealProblem, problems, revealSignal, onSelect, onSelectEdge, onPatch, }: FlowCanvasProps): React.JSX.Element;
|