@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,97 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* flow-expr-problems — pure, client-side detection of EXPRESSION issues across a
|
|
4
|
+
* whole flow draft, for surfacing in the Problems panel + canvas badges (#1934).
|
|
5
|
+
*
|
|
6
|
+
* Two kinds, mirroring the inline inspector checks but aggregated per node/edge:
|
|
7
|
+
* • ADR-0032 brace / shape ERRORS on every CEL field (decision conditions +
|
|
8
|
+
* branch expressions, screen `visibleWhen`, loop collection, edge guards…).
|
|
9
|
+
* Deterministic, scope-free → zero false positives.
|
|
10
|
+
* • Scope-aware "unknown reference" WARNINGS — a bare root not in scope at the
|
|
11
|
+
* node. The START node is skipped: its entry condition legitimately uses the
|
|
12
|
+
* trigger record's fields *bare* (`status`), which can't be told apart from a
|
|
13
|
+
* typo without the object schema (an async fetch this pure pass avoids); the
|
|
14
|
+
* inline inspector check, which does fetch, still covers it.
|
|
15
|
+
*
|
|
16
|
+
* Only CEL (`expression`) surfaces are scanned — template (`{var}`) values use
|
|
17
|
+
* single braces legally and are left to the inline check.
|
|
18
|
+
*/
|
|
19
|
+
import { fieldsForNodeType, getFieldValue } from '../inspectors/flow-node-config';
|
|
20
|
+
import { resolveFlowScope } from '../inspectors/flow-scope';
|
|
21
|
+
import { scopeRoots, findUnknownRefs, describeUnknownRefs } from '../inspectors/flow-ref-check';
|
|
22
|
+
import { validateExpressionClient } from '../inspectors/expression-validate';
|
|
23
|
+
function asArray(v) {
|
|
24
|
+
return Array.isArray(v) ? v : [];
|
|
25
|
+
}
|
|
26
|
+
function asRecord(v) {
|
|
27
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
28
|
+
}
|
|
29
|
+
function str(v) {
|
|
30
|
+
return typeof v === 'string' && v ? v : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** Brace error (error) else unknown-ref (warning, when `roots` given) for one CEL value. */
|
|
33
|
+
function checkCel(value, roots) {
|
|
34
|
+
const issue = validateExpressionClient('predicate', value);
|
|
35
|
+
if (issue)
|
|
36
|
+
return { level: 'error', message: issue.message };
|
|
37
|
+
if (roots && roots.size > 0) {
|
|
38
|
+
const unknown = findUnknownRefs(value, 'predicate', roots);
|
|
39
|
+
if (unknown.length > 0)
|
|
40
|
+
return { level: 'warning', message: describeUnknownRefs(unknown) };
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Scan a flow draft for expression problems, resolved onto node / edge targets.
|
|
46
|
+
* Pure: no network — the trigger object's fields are not expanded (root-only
|
|
47
|
+
* scope), which is why the start node is excluded from the ref check.
|
|
48
|
+
*/
|
|
49
|
+
export function flowExpressionProblems(draft) {
|
|
50
|
+
const nodes = asArray(draft.nodes).map(asRecord);
|
|
51
|
+
const edges = asArray(draft.edges).map(asRecord);
|
|
52
|
+
const startId = str(nodes.find((n) => str(n.type) === 'start')?.id);
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const node of nodes) {
|
|
55
|
+
const nodeId = str(node.id);
|
|
56
|
+
const type = str(node.type);
|
|
57
|
+
if (!nodeId || !type)
|
|
58
|
+
continue;
|
|
59
|
+
// Root-only scope at this node; skip the ref check on the start node (its
|
|
60
|
+
// bare trigger-record fields are indistinguishable from typos here).
|
|
61
|
+
const roots = nodeId === startId ? null : scopeRoots(resolveFlowScope(draft, nodeId).refs);
|
|
62
|
+
for (const field of fieldsForNodeType(type)) {
|
|
63
|
+
if (field.kind === 'expression') {
|
|
64
|
+
const hit = checkCel(getFieldValue(node, field), roots);
|
|
65
|
+
if (hit)
|
|
66
|
+
out.push({ target: { kind: 'node', nodeId }, level: hit.level, message: hit.message });
|
|
67
|
+
}
|
|
68
|
+
else if (field.kind === 'objectList' && field.columns) {
|
|
69
|
+
const exprCols = field.columns.filter((c) => c.kind === 'expression');
|
|
70
|
+
if (exprCols.length === 0)
|
|
71
|
+
continue;
|
|
72
|
+
for (const row of asArray(getFieldValue(node, field))) {
|
|
73
|
+
const r = asRecord(row);
|
|
74
|
+
const rowLabel = str(r.label);
|
|
75
|
+
for (const col of exprCols) {
|
|
76
|
+
const hit = checkCel(r[col.key], roots);
|
|
77
|
+
if (hit) {
|
|
78
|
+
const prefix = rowLabel || col.label;
|
|
79
|
+
out.push({ target: { kind: 'node', nodeId }, level: hit.level, message: prefix ? `${prefix}: ${hit.message}` : hit.message });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const edge of edges) {
|
|
87
|
+
const source = str(edge.source);
|
|
88
|
+
const target = str(edge.target);
|
|
89
|
+
if (!source || !target || edge.isDefault === true)
|
|
90
|
+
continue;
|
|
91
|
+
const roots = source === startId ? null : scopeRoots(resolveFlowScope(draft, source).refs);
|
|
92
|
+
const hit = checkCel(edge.condition, roots);
|
|
93
|
+
if (hit)
|
|
94
|
+
out.push({ target: { kind: 'edge', source, target }, level: hit.level, message: hit.message });
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { DiagnosticLevel } from './simulator/flow-sim-types';
|
|
2
|
+
import { type FlowEdge, type FlowNode } from './flow-canvas-layout';
|
|
3
|
+
/** What a problem points at on the canvas — drives badge placement + reveal. */
|
|
4
|
+
export type FlowProblemTarget = {
|
|
5
|
+
kind: 'node';
|
|
6
|
+
nodeId: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'edge';
|
|
9
|
+
edgeKey: string;
|
|
10
|
+
source: string;
|
|
11
|
+
target: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'flow';
|
|
14
|
+
};
|
|
15
|
+
/** Origin of a problem — labels the panel row and lets the UI group counts. */
|
|
16
|
+
export type FlowProblemSource = 'structural' | 'server' | 'expression';
|
|
17
|
+
/** One actionable issue, resolved onto a concrete canvas element. */
|
|
18
|
+
export interface FlowProblem {
|
|
19
|
+
/** Stable-enough key for React lists. */
|
|
20
|
+
id: string;
|
|
21
|
+
level: DiagnosticLevel;
|
|
22
|
+
message: string;
|
|
23
|
+
target: FlowProblemTarget;
|
|
24
|
+
source: FlowProblemSource;
|
|
25
|
+
/**
|
|
26
|
+
* Extra elements to flag with the red error ring/stroke beyond `target` —
|
|
27
|
+
* e.g. every hop of a cycle. The badge + click-reveal still use `target`.
|
|
28
|
+
*/
|
|
29
|
+
highlight?: {
|
|
30
|
+
nodeIds: string[];
|
|
31
|
+
edges: Array<{
|
|
32
|
+
source: string;
|
|
33
|
+
target: string;
|
|
34
|
+
}>;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** A server diagnostic entry (subset of the layered record's `_diagnostics`). */
|
|
38
|
+
export interface ServerDiagnostic {
|
|
39
|
+
/** Dotted (or array) JSON path, e.g. `nodes.2.config.objectName`. */
|
|
40
|
+
path?: string | Array<string | number>;
|
|
41
|
+
message: string;
|
|
42
|
+
/** Defaults to `'error'`. */
|
|
43
|
+
severity?: DiagnosticLevel;
|
|
44
|
+
}
|
|
45
|
+
/** Stable `source->target` key matching an edge problem to a rendered edge. */
|
|
46
|
+
export declare function edgeProblemKey(source: string, target: string): string;
|
|
47
|
+
export interface BuildFlowProblemsArgs {
|
|
48
|
+
nodes: FlowNode[];
|
|
49
|
+
edges: FlowEdge[];
|
|
50
|
+
/** Server `_diagnostics`, flattened to a severity-tagged, path-keyed list. */
|
|
51
|
+
serverDiagnostics?: ServerDiagnostic[];
|
|
52
|
+
/** Declared flow variables — needed to resolve scope for the expression check. */
|
|
53
|
+
variables?: unknown[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build the unified problem list from structural validation + server
|
|
57
|
+
* diagnostics. Errors are listed before warnings; within a level each source
|
|
58
|
+
* keeps its own emit order (structural before server).
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildFlowProblems({ nodes, edges, serverDiagnostics, variables }: BuildFlowProblemsArgs): FlowProblem[];
|
|
61
|
+
/** A folded badge for one canvas element (errors dominate warnings). */
|
|
62
|
+
export interface ProblemBadge {
|
|
63
|
+
level: DiagnosticLevel;
|
|
64
|
+
/** Tooltip text — each problem message on its own line. */
|
|
65
|
+
title: string;
|
|
66
|
+
count: number;
|
|
67
|
+
}
|
|
68
|
+
export interface ProblemIndex {
|
|
69
|
+
byNode: Map<string, ProblemBadge>;
|
|
70
|
+
byEdge: Map<string, ProblemBadge>;
|
|
71
|
+
}
|
|
72
|
+
/** Group problems into per-node / per-edge badges for the canvas overlay. */
|
|
73
|
+
export declare function indexProblemBadges(problems: FlowProblem[]): ProblemIndex;
|
|
74
|
+
/**
|
|
75
|
+
* Error elements to paint with the red ring/stroke, derived from the unified
|
|
76
|
+
* problem list. ERRORS ONLY — warnings get an amber badge but no ring. Includes
|
|
77
|
+
* each error's `highlight` set, so a cycle paints its whole loop (every hop node
|
|
78
|
+
* + edge) red while its badge still sits on the closing edge. Lets the preview
|
|
79
|
+
* derive the red sets from `problems` instead of a second validateFlowDraft pass.
|
|
80
|
+
*/
|
|
81
|
+
export declare function deriveInvalidElements(problems: FlowProblem[]): {
|
|
82
|
+
invalidNodeIds: string[];
|
|
83
|
+
invalidEdges: Set<string>;
|
|
84
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* flow-problems — unify the two flow-validation sources into one flat,
|
|
4
|
+
* per-element issue list that the canvas badges and the Problems panel both
|
|
5
|
+
* render.
|
|
6
|
+
*
|
|
7
|
+
* 1. `validateFlowDraft` (client, structural): no resolvable entry,
|
|
8
|
+
* unreachable nodes, a decision with no default branch, duplicate node
|
|
9
|
+
* ids, dangling edges, un-declared cycles.
|
|
10
|
+
* 2. The server `_diagnostics` already attached to the layered record
|
|
11
|
+
* (schema validation), each keyed by a dotted JSON path.
|
|
12
|
+
*
|
|
13
|
+
* "Surfacing, not detection": detection already exists. This module only maps
|
|
14
|
+
* each detected issue onto a concrete canvas element — a node id or a stable
|
|
15
|
+
* edge key — so a badge can sit on the offending element and a Problems-panel
|
|
16
|
+
* row can select + reveal it. Flow-level issues (no specific element) are kept
|
|
17
|
+
* too: listed in the panel, but without a badge.
|
|
18
|
+
*/
|
|
19
|
+
import { validateFlowDraft } from './simulator/flow-sim-validate';
|
|
20
|
+
import { edgeKey } from './flow-canvas-layout';
|
|
21
|
+
import { flowExpressionProblems } from './flow-expr-problems';
|
|
22
|
+
/** Stable `source->target` key matching an edge problem to a rendered edge. */
|
|
23
|
+
export function edgeProblemKey(source, target) {
|
|
24
|
+
return `${source}->${target}`;
|
|
25
|
+
}
|
|
26
|
+
/** Resolve an edge's selection key (`edgeKey`) from its endpoints. */
|
|
27
|
+
function resolveEdgeKey(edges, source, target) {
|
|
28
|
+
const idx = edges.findIndex((e) => e.source === source && e.target === target);
|
|
29
|
+
return idx >= 0 ? edgeKey(edges[idx], idx) : `${source}->${target}#-1`;
|
|
30
|
+
}
|
|
31
|
+
/** Normalize a dotted/array JSON path to segments (numbers stay numeric). */
|
|
32
|
+
function pathSegments(path) {
|
|
33
|
+
if (Array.isArray(path))
|
|
34
|
+
return path;
|
|
35
|
+
if (typeof path !== 'string' || !path)
|
|
36
|
+
return [];
|
|
37
|
+
return path.split('.').map((seg) => {
|
|
38
|
+
const n = Number(seg);
|
|
39
|
+
return Number.isInteger(n) && String(n) === seg ? n : seg;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Map a structural diagnostic's optional anchors (`edge`, `cycle`, `nodeId`)
|
|
44
|
+
* onto a badge target. A cycle points its badge at the *closing* hop — the edge
|
|
45
|
+
* the author marks as a back-edge to resolve it — but flags EVERY hop (nodes +
|
|
46
|
+
* edges) for the red error highlight so the whole loop reads as the problem.
|
|
47
|
+
*/
|
|
48
|
+
function structuralMapping(diag, edges) {
|
|
49
|
+
if (diag.edge) {
|
|
50
|
+
const { source, target } = diag.edge;
|
|
51
|
+
return { target: { kind: 'edge', source, target, edgeKey: resolveEdgeKey(edges, source, target) } };
|
|
52
|
+
}
|
|
53
|
+
if (diag.cycle && diag.cycle.length >= 2) {
|
|
54
|
+
const c = diag.cycle;
|
|
55
|
+
const source = c[c.length - 2];
|
|
56
|
+
const target = c[c.length - 1];
|
|
57
|
+
const nodeIds = [];
|
|
58
|
+
const hops = [];
|
|
59
|
+
for (let i = 0; i < c.length - 1; i++) {
|
|
60
|
+
nodeIds.push(c[i]);
|
|
61
|
+
hops.push({ source: c[i], target: c[i + 1] });
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
target: { kind: 'edge', source, target, edgeKey: resolveEdgeKey(edges, source, target) },
|
|
65
|
+
highlight: { nodeIds, edges: hops },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (diag.nodeId)
|
|
69
|
+
return { target: { kind: 'node', nodeId: diag.nodeId } };
|
|
70
|
+
return { target: { kind: 'flow' } };
|
|
71
|
+
}
|
|
72
|
+
/** Map a server diagnostic's JSON path onto a node/edge/flow target. */
|
|
73
|
+
function serverTarget(path, nodes, edges) {
|
|
74
|
+
const segs = pathSegments(path);
|
|
75
|
+
if (segs.length >= 2 && typeof segs[1] === 'number') {
|
|
76
|
+
const idx = segs[1];
|
|
77
|
+
if (segs[0] === 'nodes' && nodes[idx]?.id)
|
|
78
|
+
return { kind: 'node', nodeId: nodes[idx].id };
|
|
79
|
+
if (segs[0] === 'edges' && edges[idx]) {
|
|
80
|
+
const e = edges[idx];
|
|
81
|
+
return { kind: 'edge', source: e.source, target: e.target, edgeKey: edgeKey(e, idx) };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { kind: 'flow' };
|
|
85
|
+
}
|
|
86
|
+
/** Short stable token for a target, used in a problem's React key. */
|
|
87
|
+
function targetKey(t) {
|
|
88
|
+
if (t.kind === 'node')
|
|
89
|
+
return `n:${t.nodeId}`;
|
|
90
|
+
if (t.kind === 'edge')
|
|
91
|
+
return `e:${t.source}->${t.target}`;
|
|
92
|
+
return 'flow';
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Build the unified problem list from structural validation + server
|
|
96
|
+
* diagnostics. Errors are listed before warnings; within a level each source
|
|
97
|
+
* keeps its own emit order (structural before server).
|
|
98
|
+
*/
|
|
99
|
+
export function buildFlowProblems({ nodes, edges, serverDiagnostics, variables }) {
|
|
100
|
+
const problems = [];
|
|
101
|
+
const v = validateFlowDraft(nodes, edges);
|
|
102
|
+
const pushStructural = (level, list) => {
|
|
103
|
+
list.forEach((diag, i) => {
|
|
104
|
+
const { target, highlight } = structuralMapping(diag, edges);
|
|
105
|
+
problems.push({
|
|
106
|
+
id: `structural:${level}:${i}:${targetKey(target)}`,
|
|
107
|
+
level,
|
|
108
|
+
message: diag.message,
|
|
109
|
+
target,
|
|
110
|
+
source: 'structural',
|
|
111
|
+
...(highlight ? { highlight } : {}),
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
pushStructural('error', v.errors);
|
|
116
|
+
pushStructural('warning', v.warnings);
|
|
117
|
+
(serverDiagnostics ?? []).forEach((diag, i) => {
|
|
118
|
+
const level = diag.severity === 'warning' ? 'warning' : 'error';
|
|
119
|
+
const target = serverTarget(diag.path, nodes, edges);
|
|
120
|
+
problems.push({
|
|
121
|
+
id: `server:${i}:${targetKey(target)}`,
|
|
122
|
+
level,
|
|
123
|
+
message: diag.message,
|
|
124
|
+
target,
|
|
125
|
+
source: 'server',
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
// Client-side EXPRESSION issues (ADR-0032 braces + scope-aware unknown refs),
|
|
129
|
+
// resolved onto node / edge targets — see flow-expr-problems.
|
|
130
|
+
flowExpressionProblems({ nodes, edges, variables: variables ?? [] }).forEach((ep, i) => {
|
|
131
|
+
const target = ep.target.kind === 'edge'
|
|
132
|
+
? {
|
|
133
|
+
kind: 'edge',
|
|
134
|
+
source: ep.target.source,
|
|
135
|
+
target: ep.target.target,
|
|
136
|
+
edgeKey: resolveEdgeKey(edges, ep.target.source, ep.target.target),
|
|
137
|
+
}
|
|
138
|
+
: { kind: 'node', nodeId: ep.target.nodeId };
|
|
139
|
+
problems.push({
|
|
140
|
+
id: `expression:${ep.level}:${i}:${targetKey(target)}`,
|
|
141
|
+
level: ep.level,
|
|
142
|
+
message: ep.message,
|
|
143
|
+
target,
|
|
144
|
+
source: 'expression',
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
// Errors first so the panel + counts lead with blockers (stable within level).
|
|
148
|
+
return problems
|
|
149
|
+
.map((p, i) => [p, i])
|
|
150
|
+
.sort((a, b) => {
|
|
151
|
+
if (a[0].level !== b[0].level)
|
|
152
|
+
return a[0].level === 'error' ? -1 : 1;
|
|
153
|
+
return a[1] - b[1];
|
|
154
|
+
})
|
|
155
|
+
.map(([p]) => p);
|
|
156
|
+
}
|
|
157
|
+
function foldBadge(list) {
|
|
158
|
+
const level = list.some((p) => p.level === 'error') ? 'error' : 'warning';
|
|
159
|
+
return { level, title: list.map((p) => p.message).join('\n'), count: list.length };
|
|
160
|
+
}
|
|
161
|
+
/** Group problems into per-node / per-edge badges for the canvas overlay. */
|
|
162
|
+
export function indexProblemBadges(problems) {
|
|
163
|
+
const nodeLists = new Map();
|
|
164
|
+
const edgeLists = new Map();
|
|
165
|
+
for (const p of problems) {
|
|
166
|
+
if (p.target.kind === 'node') {
|
|
167
|
+
const l = nodeLists.get(p.target.nodeId) ?? [];
|
|
168
|
+
l.push(p);
|
|
169
|
+
nodeLists.set(p.target.nodeId, l);
|
|
170
|
+
}
|
|
171
|
+
else if (p.target.kind === 'edge') {
|
|
172
|
+
const k = edgeProblemKey(p.target.source, p.target.target);
|
|
173
|
+
const l = edgeLists.get(k) ?? [];
|
|
174
|
+
l.push(p);
|
|
175
|
+
edgeLists.set(k, l);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const byNode = new Map();
|
|
179
|
+
for (const [id, list] of nodeLists)
|
|
180
|
+
byNode.set(id, foldBadge(list));
|
|
181
|
+
const byEdge = new Map();
|
|
182
|
+
for (const [k, list] of edgeLists)
|
|
183
|
+
byEdge.set(k, foldBadge(list));
|
|
184
|
+
return { byNode, byEdge };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Error elements to paint with the red ring/stroke, derived from the unified
|
|
188
|
+
* problem list. ERRORS ONLY — warnings get an amber badge but no ring. Includes
|
|
189
|
+
* each error's `highlight` set, so a cycle paints its whole loop (every hop node
|
|
190
|
+
* + edge) red while its badge still sits on the closing edge. Lets the preview
|
|
191
|
+
* derive the red sets from `problems` instead of a second validateFlowDraft pass.
|
|
192
|
+
*/
|
|
193
|
+
export function deriveInvalidElements(problems) {
|
|
194
|
+
const nodeSet = new Set();
|
|
195
|
+
const edgeSet = new Set();
|
|
196
|
+
for (const p of problems) {
|
|
197
|
+
if (p.level !== 'error')
|
|
198
|
+
continue;
|
|
199
|
+
if (p.target.kind === 'node')
|
|
200
|
+
nodeSet.add(p.target.nodeId);
|
|
201
|
+
else if (p.target.kind === 'edge')
|
|
202
|
+
edgeSet.add(edgeProblemKey(p.target.source, p.target.target));
|
|
203
|
+
for (const id of p.highlight?.nodeIds ?? [])
|
|
204
|
+
nodeSet.add(id);
|
|
205
|
+
for (const e of p.highlight?.edges ?? [])
|
|
206
|
+
edgeSet.add(edgeProblemKey(e.source, e.target));
|
|
207
|
+
}
|
|
208
|
+
return { invalidNodeIds: [...nodeSet], invalidEdges: edgeSet };
|
|
209
|
+
}
|
|
@@ -35,6 +35,27 @@ export declare function newField(name: string, type: FieldTypeId, label?: string
|
|
|
35
35
|
export declare function toLabel(name: string): string;
|
|
36
36
|
/** Normalize an arbitrary string into a valid snake_case field name. */
|
|
37
37
|
export declare function toFieldName(raw: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Prefix-stable variant of {@link toFieldName} for *live keystroke* input.
|
|
40
|
+
*
|
|
41
|
+
* The strict `toFieldName` trims a trailing `_`, which makes it impossible
|
|
42
|
+
* to TYPE a multi-word identifier into a controlled input: the field's
|
|
43
|
+
* `onChange` re-normalizes on every keystroke, so the instant the user
|
|
44
|
+
* presses `_` the value is `"repair_"` -> trimmed to `"repair"` -> the
|
|
45
|
+
* underscore vanishes before the next letter arrives, yielding
|
|
46
|
+
* `"repairticket"` instead of `"repair_ticket"`. (Authors of non-Latin
|
|
47
|
+
* locales hit this hardest: their label cannot derive a Latin slug, so
|
|
48
|
+
* they MUST type the identifier by hand.)
|
|
49
|
+
*
|
|
50
|
+
* This variant keeps a single trailing `_` so typing can continue, and
|
|
51
|
+
* returns `''` (not the `'field'` placeholder) on empty input so clearing
|
|
52
|
+
* the box actually clears it. A trailing `_` is itself a valid identifier
|
|
53
|
+
* per the spec ("starts with a letter, may contain letters/digits/`_`"),
|
|
54
|
+
* so no separate commit-time trim is required for correctness; callers
|
|
55
|
+
* that need a canonical form for a *complete* string (label->name
|
|
56
|
+
* derivation, group keys) should keep using strict `toFieldName`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function toFieldNameLoose(raw: string): string;
|
|
38
59
|
/**
|
|
39
60
|
* A declared field group (a.k.a. "section"). Lives at the object's
|
|
40
61
|
* top level as `draft.fieldGroups`; individual fields opt into a group
|
|
@@ -38,9 +38,11 @@ export function indexOfField(view, name) {
|
|
|
38
38
|
/** Build a fresh field definition for the given type. */
|
|
39
39
|
export function newField(name, type, label) {
|
|
40
40
|
const def = { type, label: label ?? toLabel(name) };
|
|
41
|
-
//
|
|
41
|
+
// Picklist-style fields start with no options; the OptionsEditor shows a
|
|
42
|
+
// blank input row locally and only persists rows once they have a value, so
|
|
43
|
+
// an unfilled row never trips the spec's identifier validation.
|
|
42
44
|
if (type === 'select' || type === 'multiselect' || type === 'radio' || type === 'checkboxes') {
|
|
43
|
-
def.options = [
|
|
45
|
+
def.options = [];
|
|
44
46
|
}
|
|
45
47
|
return { name, def };
|
|
46
48
|
}
|
|
@@ -67,6 +69,39 @@ export function toFieldName(raw) {
|
|
|
67
69
|
return `f_${sanitized}`;
|
|
68
70
|
return sanitized;
|
|
69
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Prefix-stable variant of {@link toFieldName} for *live keystroke* input.
|
|
74
|
+
*
|
|
75
|
+
* The strict `toFieldName` trims a trailing `_`, which makes it impossible
|
|
76
|
+
* to TYPE a multi-word identifier into a controlled input: the field's
|
|
77
|
+
* `onChange` re-normalizes on every keystroke, so the instant the user
|
|
78
|
+
* presses `_` the value is `"repair_"` -> trimmed to `"repair"` -> the
|
|
79
|
+
* underscore vanishes before the next letter arrives, yielding
|
|
80
|
+
* `"repairticket"` instead of `"repair_ticket"`. (Authors of non-Latin
|
|
81
|
+
* locales hit this hardest: their label cannot derive a Latin slug, so
|
|
82
|
+
* they MUST type the identifier by hand.)
|
|
83
|
+
*
|
|
84
|
+
* This variant keeps a single trailing `_` so typing can continue, and
|
|
85
|
+
* returns `''` (not the `'field'` placeholder) on empty input so clearing
|
|
86
|
+
* the box actually clears it. A trailing `_` is itself a valid identifier
|
|
87
|
+
* per the spec ("starts with a letter, may contain letters/digits/`_`"),
|
|
88
|
+
* so no separate commit-time trim is required for correctness; callers
|
|
89
|
+
* that need a canonical form for a *complete* string (label->name
|
|
90
|
+
* derivation, group keys) should keep using strict `toFieldName`.
|
|
91
|
+
*/
|
|
92
|
+
export function toFieldNameLoose(raw) {
|
|
93
|
+
const sanitized = raw
|
|
94
|
+
.trim()
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
97
|
+
.replace(/^_+/g, '') // trim leading only -- a trailing `_` must survive
|
|
98
|
+
.replace(/_{2,}/g, '_');
|
|
99
|
+
if (!sanitized)
|
|
100
|
+
return '';
|
|
101
|
+
if (!/^[a-z_]/.test(sanitized))
|
|
102
|
+
return `f_${sanitized}`;
|
|
103
|
+
return sanitized;
|
|
104
|
+
}
|
|
70
105
|
/** Read `draft.fieldGroups` into a normalized, well-typed list. */
|
|
71
106
|
export function readGroups(fieldGroupsInput) {
|
|
72
107
|
if (!Array.isArray(fieldGroupsInput))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* screen-spec — pure helpers that map a flow `screen` node's authored `config`
|
|
3
|
+
* onto the runtime `ScreenSpec` (the contract {@link ScreenView} renders), plus
|
|
4
|
+
* `{var}` interpolation for the title/description and `visibleWhen` field
|
|
5
|
+
* gating. Kept framework-free so {@link ScreenPreview} stays a thin component
|
|
6
|
+
* and these stay unit-testable.
|
|
7
|
+
*/
|
|
8
|
+
import type { ScreenSpec } from '../../ScreenView';
|
|
9
|
+
/** Minimal node shape the preview needs (id + authored config). */
|
|
10
|
+
export interface ScreenPreviewNode {
|
|
11
|
+
id: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
config?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Interpolate `{var}` references, mirroring the simulator's `{var}` syntax
|
|
17
|
+
* (flow-simulator.ts). Known vars are substituted; unknown refs stay literal so
|
|
18
|
+
* the author still sees the dependency in the design preview.
|
|
19
|
+
*/
|
|
20
|
+
export declare function interpolate(text: string | undefined, vars: Record<string, unknown> | undefined): string;
|
|
21
|
+
/**
|
|
22
|
+
* A field's `visibleWhen` gate, evaluated against the current variables using
|
|
23
|
+
* the SAME evaluator the simulator uses for edge conditions (so the preview
|
|
24
|
+
* agrees with the simulator). Real metadata mixes `{var}` and bare-var styles
|
|
25
|
+
* (e.g. `{createOpportunity} == true`, `stage == "review"`), so brace
|
|
26
|
+
* placeholders are normalised to bare identifiers first.
|
|
27
|
+
*
|
|
28
|
+
* Fail-OPEN: a missing condition, an unparseable one, or one that references a
|
|
29
|
+
* not-yet-set variable (the inspector has no run state) keeps the field
|
|
30
|
+
* visible — the design preview never hides a configured field just because it
|
|
31
|
+
* lacks the data to decide. With live run state (the simulator) it gates
|
|
32
|
+
* faithfully: `createOpportunity == false` hides the field.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isFieldVisibleWhen(visibleWhen: unknown, variables: Record<string, unknown> | undefined): boolean;
|
|
35
|
+
/** Count of authored field rows hidden by their `visibleWhen` against `variables`. */
|
|
36
|
+
export declare function hiddenFieldCount(node: ScreenPreviewNode, variables: Record<string, unknown> | undefined): number;
|
|
37
|
+
/**
|
|
38
|
+
* Map a screen node's authored `config` onto the runtime `ScreenSpec` — the
|
|
39
|
+
* same keys the engine's `screen` executor reads (title / description / fields
|
|
40
|
+
* / objectName / mode / defaults / idVariable). `fields` are gated by their
|
|
41
|
+
* `visibleWhen` against `variables` (omit `variables` to keep every field).
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildScreenSpec(node: ScreenPreviewNode, variables?: Record<string, unknown>): ScreenSpec;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
import { evalCondition } from './simulator/flow-sim-validate';
|
|
3
|
+
/**
|
|
4
|
+
* Interpolate `{var}` references, mirroring the simulator's `{var}` syntax
|
|
5
|
+
* (flow-simulator.ts). Known vars are substituted; unknown refs stay literal so
|
|
6
|
+
* the author still sees the dependency in the design preview.
|
|
7
|
+
*/
|
|
8
|
+
export function interpolate(text, vars) {
|
|
9
|
+
if (!text)
|
|
10
|
+
return '';
|
|
11
|
+
if (!vars)
|
|
12
|
+
return text;
|
|
13
|
+
return text.replace(/\{([^{}]+)\}/g, (m, k) => {
|
|
14
|
+
const v = vars[String(k).trim()];
|
|
15
|
+
return v === undefined || v === null ? m : String(v);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A field's `visibleWhen` gate, evaluated against the current variables using
|
|
20
|
+
* the SAME evaluator the simulator uses for edge conditions (so the preview
|
|
21
|
+
* agrees with the simulator). Real metadata mixes `{var}` and bare-var styles
|
|
22
|
+
* (e.g. `{createOpportunity} == true`, `stage == "review"`), so brace
|
|
23
|
+
* placeholders are normalised to bare identifiers first.
|
|
24
|
+
*
|
|
25
|
+
* Fail-OPEN: a missing condition, an unparseable one, or one that references a
|
|
26
|
+
* not-yet-set variable (the inspector has no run state) keeps the field
|
|
27
|
+
* visible — the design preview never hides a configured field just because it
|
|
28
|
+
* lacks the data to decide. With live run state (the simulator) it gates
|
|
29
|
+
* faithfully: `createOpportunity == false` hides the field.
|
|
30
|
+
*/
|
|
31
|
+
export function isFieldVisibleWhen(visibleWhen, variables) {
|
|
32
|
+
if (typeof visibleWhen !== 'string' || !visibleWhen.trim())
|
|
33
|
+
return true;
|
|
34
|
+
if (!variables)
|
|
35
|
+
return true;
|
|
36
|
+
const normalized = visibleWhen.replace(/\{([\w.]+)\}/g, '$1');
|
|
37
|
+
const { result, error } = evalCondition(normalized, variables);
|
|
38
|
+
return error ? true : result;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Coerce the authored `config.fields` rows into runtime `ScreenFieldSpec`s,
|
|
42
|
+
* dropping any whose `visibleWhen` evaluates false against `variables` — exactly
|
|
43
|
+
* what the runtime `screen` executor emits (it filters server-side before
|
|
44
|
+
* sending the ScreenSpec).
|
|
45
|
+
*/
|
|
46
|
+
function toScreenFields(raw, variables) {
|
|
47
|
+
if (!Array.isArray(raw))
|
|
48
|
+
return [];
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const f of raw) {
|
|
51
|
+
if (!f || typeof f !== 'object')
|
|
52
|
+
continue;
|
|
53
|
+
const row = f;
|
|
54
|
+
if (typeof row.name !== 'string' || !row.name)
|
|
55
|
+
continue;
|
|
56
|
+
if (!isFieldVisibleWhen(row.visibleWhen, variables))
|
|
57
|
+
continue;
|
|
58
|
+
out.push({
|
|
59
|
+
name: row.name,
|
|
60
|
+
label: typeof row.label === 'string' ? row.label : undefined,
|
|
61
|
+
type: typeof row.type === 'string' ? row.type : undefined,
|
|
62
|
+
required: row.required === true,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/** Count of authored field rows hidden by their `visibleWhen` against `variables`. */
|
|
68
|
+
export function hiddenFieldCount(node, variables) {
|
|
69
|
+
const raw = node.config?.fields;
|
|
70
|
+
if (!Array.isArray(raw))
|
|
71
|
+
return 0;
|
|
72
|
+
let hidden = 0;
|
|
73
|
+
for (const f of raw) {
|
|
74
|
+
if (!f || typeof f !== 'object')
|
|
75
|
+
continue;
|
|
76
|
+
const row = f;
|
|
77
|
+
if (typeof row.name !== 'string' || !row.name)
|
|
78
|
+
continue;
|
|
79
|
+
if (!isFieldVisibleWhen(row.visibleWhen, variables))
|
|
80
|
+
hidden++;
|
|
81
|
+
}
|
|
82
|
+
return hidden;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Map a screen node's authored `config` onto the runtime `ScreenSpec` — the
|
|
86
|
+
* same keys the engine's `screen` executor reads (title / description / fields
|
|
87
|
+
* / objectName / mode / defaults / idVariable). `fields` are gated by their
|
|
88
|
+
* `visibleWhen` against `variables` (omit `variables` to keep every field).
|
|
89
|
+
*/
|
|
90
|
+
export function buildScreenSpec(node, variables) {
|
|
91
|
+
const c = (node.config && typeof node.config === 'object' ? node.config : {});
|
|
92
|
+
const objectName = typeof c.objectName === 'string' && c.objectName ? c.objectName : undefined;
|
|
93
|
+
const mode = c.mode === 'edit' ? 'edit' : c.mode === 'create' ? 'create' : undefined;
|
|
94
|
+
const defaults = c.defaults && typeof c.defaults === 'object' && !Array.isArray(c.defaults)
|
|
95
|
+
? c.defaults
|
|
96
|
+
: undefined;
|
|
97
|
+
return {
|
|
98
|
+
nodeId: node.id,
|
|
99
|
+
title: typeof c.title === 'string' ? c.title : undefined,
|
|
100
|
+
description: typeof c.description === 'string' ? c.description : undefined,
|
|
101
|
+
fields: toScreenFields(c.fields, variables),
|
|
102
|
+
kind: objectName ? 'object-form' : 'fields',
|
|
103
|
+
objectName,
|
|
104
|
+
mode,
|
|
105
|
+
defaults,
|
|
106
|
+
idVariable: typeof c.idVariable === 'string' ? c.idVariable : undefined,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -28,6 +28,12 @@ export interface SimEdge {
|
|
|
28
28
|
};
|
|
29
29
|
isDefault?: boolean;
|
|
30
30
|
label?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Connection type (FlowEdgeSchema). A `'back'` edge is an ADR-0044 declared
|
|
33
|
+
* back-edge: traversed normally at run time, but excluded from DAG cycle
|
|
34
|
+
* validation so a revise/rework loop can re-enter an earlier node.
|
|
35
|
+
*/
|
|
36
|
+
type?: string;
|
|
31
37
|
}
|
|
32
38
|
export type SimStatus = 'idle' | 'running' | 'paused' | 'done' | 'error';
|
|
33
39
|
/** One evaluated outgoing edge of a decision (kept for the debug timeline). */
|
|
@@ -74,8 +80,22 @@ export interface SimState {
|
|
|
74
80
|
export type DiagnosticLevel = 'error' | 'warning';
|
|
75
81
|
export interface Diagnostic {
|
|
76
82
|
level: DiagnosticLevel;
|
|
83
|
+
/** The node this diagnostic points at (for an inline badge + click-to-reveal). */
|
|
77
84
|
nodeId?: string;
|
|
85
|
+
/**
|
|
86
|
+
* The edge this diagnostic points at (e.g. a dangling endpoint). Carries the
|
|
87
|
+
* endpoints so the designer can key the inline badge by `source->target`.
|
|
88
|
+
*/
|
|
89
|
+
edge?: {
|
|
90
|
+
source: string;
|
|
91
|
+
target: string;
|
|
92
|
+
};
|
|
78
93
|
message: string;
|
|
94
|
+
/**
|
|
95
|
+
* For a cycle error: the node path that closes the loop (e.g. `['a','b','a']`),
|
|
96
|
+
* so the designer can paint the offending edges/nodes inline on the canvas.
|
|
97
|
+
*/
|
|
98
|
+
cycle?: string[];
|
|
79
99
|
}
|
|
80
100
|
export interface FlowValidation {
|
|
81
101
|
errors: Diagnostic[];
|
|
@@ -4,5 +4,12 @@ export declare function evalCondition(expr: string, variables: Record<string, un
|
|
|
4
4
|
result: boolean;
|
|
5
5
|
error?: string;
|
|
6
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* Find a directed cycle in `edges` over `nodeIds`, returned as the node path
|
|
9
|
+
* that closes the loop (e.g. `['a','b','a']`), or `null` when the graph is a
|
|
10
|
+
* DAG. Iterative DFS with a recursion-stack colour map; the first cycle found
|
|
11
|
+
* wins (enough to report — the author fixes one at a time).
|
|
12
|
+
*/
|
|
13
|
+
export declare function findCycle(nodeIds: string[], edges: SimEdge[]): string[] | null;
|
|
7
14
|
/** Static structural checks; `errors` block Run, `warnings` are advisory. */
|
|
8
15
|
export declare function validateFlowDraft(nodes: SimNode[], edges: SimEdge[]): FlowValidation;
|