@object-ui/app-shell 7.1.0 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +320 -0
- package/dist/components/ManagedByBadge.js +1 -1
- package/dist/console/AppContent.js +9 -15
- package/dist/console/ConsoleShell.d.ts +16 -0
- package/dist/console/ConsoleShell.js +43 -2
- package/dist/console/ai/AiChatPage.js +64 -14
- package/dist/console/ai/BuildDebugDrawer.d.ts +20 -0
- package/dist/console/ai/BuildDebugDrawer.js +75 -0
- package/dist/console/ai/buildDebugApi.d.ts +94 -0
- package/dist/console/ai/buildDebugApi.js +16 -0
- package/dist/console/home/HomeLayout.js +5 -7
- package/dist/console/home/HomePage.js +1 -9
- package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
- package/dist/console/organizations/OrganizationsPage.js +32 -4
- package/dist/console/organizations/manage/OrganizationLayout.js +1 -1
- package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
- package/dist/console/organizations/provisionEnvironment.js +64 -0
- package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
- package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
- package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
- package/dist/environment/EnvironmentListToolbar.js +59 -0
- package/dist/environment/entitlements.d.ts +90 -0
- package/dist/environment/entitlements.js +91 -0
- package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
- package/dist/environment/useEnvironmentEntitlements.js +108 -0
- package/dist/hooks/useActionModal.js +15 -1
- package/dist/hooks/useAiSurface.d.ts +59 -0
- package/dist/hooks/useAiSurface.js +78 -0
- package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
- package/dist/hooks/useConsoleActionRuntime.js +36 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/layout/AppHeader.js +30 -5
- package/dist/layout/ConsoleFloatingChatbot.js +22 -4
- package/dist/layout/ConsoleLayout.js +5 -6
- package/dist/layout/ContextSelectors.js +0 -19
- package/dist/layout/WorkspaceSwitcher.d.ts +14 -0
- package/dist/layout/WorkspaceSwitcher.js +76 -0
- package/dist/preview/DraftPreviewBar.js +20 -7
- package/dist/providers/ExpressionProvider.js +9 -3
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +1 -1
- package/dist/utils/managedByEmptyState.d.ts +1 -1
- package/dist/utils/managedByEmptyState.js +20 -2
- package/dist/utils/recordFormNavigation.d.ts +60 -0
- package/dist/utils/recordFormNavigation.js +35 -0
- package/dist/utils/resolvePageVarTokens.d.ts +31 -0
- package/dist/utils/resolvePageVarTokens.js +72 -0
- package/dist/views/CreateViewDialog.js +14 -1
- package/dist/views/ObjectView.js +27 -13
- package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
- package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
- package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
- package/dist/views/metadata-admin/PackagesPage.js +49 -4
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +36 -4
- package/dist/views/metadata-admin/ResourceListPage.js +25 -10
- package/dist/views/metadata-admin/StudioHomePage.js +1 -5
- package/dist/views/metadata-admin/createBody.d.ts +26 -0
- package/dist/views/metadata-admin/createBody.js +30 -0
- package/dist/views/metadata-admin/i18n.js +20 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +8 -0
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +17 -3
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +16 -2
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +15 -3
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
- package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
- package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +6 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +21 -10
- package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
- package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
- package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
- package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
- package/dist/views/metadata-admin/package-scope.d.ts +9 -19
- package/dist/views/metadata-admin/package-scope.js +11 -25
- package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +22 -3
- package/dist/views/metadata-admin/previews/FlowCanvas.js +45 -6
- package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
- package/dist/views/metadata-admin/previews/FlowPreview.js +42 -30
- package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
- package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
- package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
- package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
- package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +5 -3
- package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
- package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
- package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
- package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +9 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +4 -2
- package/package.json +38 -38
|
@@ -18,15 +18,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
18
18
|
* edges or duplicate node ids; we never throw, we just degrade.
|
|
19
19
|
*/
|
|
20
20
|
import * as React from 'react';
|
|
21
|
-
import { Bug, CircleDot, GitBranch, History, PanelRight, Plus, Settings2, Variable, Zap, } from 'lucide-react';
|
|
21
|
+
import { AlertCircle, Bug, CircleDot, GitBranch, History, PanelRight, Plus, Settings2, Variable, Zap, } from 'lucide-react';
|
|
22
22
|
import { PreviewShell, PreviewMessage, PreviewErrorBoundary } from './PreviewShell';
|
|
23
23
|
import { uniqueId, appendArray } from '../inspectors/_shared';
|
|
24
24
|
import { t as tr } from '../i18n';
|
|
25
25
|
import { FlowCanvas } from './FlowCanvas';
|
|
26
26
|
import { FlowSimulatorPanel } from './FlowSimulatorPanel';
|
|
27
27
|
import { FlowRunsPanel } from './FlowRunsPanel';
|
|
28
|
-
import {
|
|
29
|
-
|
|
28
|
+
import { ProblemsPanel } from './ProblemsPanel';
|
|
29
|
+
import { buildFlowProblems, deriveInvalidElements } from './flow-problems';
|
|
30
|
+
export function FlowPreview({ draft, editing, selection, onSelectionChange, onPatch, locale, diagnostics }) {
|
|
30
31
|
const d = draft;
|
|
31
32
|
// Memoized so hook deps (validation memo, handleAddNode) get a stable array
|
|
32
33
|
// reference across renders instead of a fresh `[]`/cast each time.
|
|
@@ -40,32 +41,33 @@ export function FlowPreview({ draft, editing, selection, onSelectionChange, onPa
|
|
|
40
41
|
const [showDebug, setShowDebug] = React.useState(false);
|
|
41
42
|
const [showVars, setShowVars] = React.useState(true);
|
|
42
43
|
const [showRuns, setShowRuns] = React.useState(false);
|
|
44
|
+
const [showProblems, setShowProblems] = React.useState(false);
|
|
43
45
|
const [runHL, setRunHL] = React.useState(null);
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// the
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
46
|
+
// Unified problem list (structural + server `_diagnostics`) is the SINGLE
|
|
47
|
+
// source for every validation surface — the clickable inline banner, the
|
|
48
|
+
// per-element badges, the red error ring/stroke, and the Problems panel.
|
|
49
|
+
// Recomputed from the live draft so they all clear as the author fixes each issue.
|
|
50
|
+
const problems = React.useMemo(() => buildFlowProblems({ nodes, edges, serverDiagnostics: diagnostics, variables }), [nodes, edges, diagnostics, d.variables]);
|
|
51
|
+
const errorCount = problems.filter((p) => p.level === 'error').length;
|
|
52
|
+
// Red error ring/stroke derived from the same list (errors only; a cycle
|
|
53
|
+
// paints its whole loop) — no second validateFlowDraft pass.
|
|
54
|
+
const { invalidNodeIds, invalidEdges } = React.useMemo(() => deriveInvalidElements(problems), [problems]);
|
|
55
|
+
// "Reveal" handshake with the canvas: a changing nonce pans to the element.
|
|
56
|
+
const [reveal, setReveal] = React.useState(null);
|
|
57
|
+
const selectedKey = selectedId ? `node:${selectedId}` : (selectedEdgeId ?? null);
|
|
58
|
+
const handleSelectProblem = React.useCallback((p) => {
|
|
59
|
+
if (p.target.kind === 'node') {
|
|
60
|
+
// Destructure before the .find() closure — TS drops the union narrowing
|
|
61
|
+
// of `p.target` inside a nested callback, so capture nodeId as a string.
|
|
62
|
+
const { nodeId } = p.target;
|
|
63
|
+
const node = nodes.find((n) => n.id === nodeId);
|
|
64
|
+
onSelectionChange?.({ kind: 'node', id: nodeId, label: node?.label || nodeId });
|
|
62
65
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}, [nodes, edges]);
|
|
66
|
+
else if (p.target.kind === 'edge') {
|
|
67
|
+
onSelectionChange?.({ kind: 'edge', id: p.target.edgeKey, label: `${p.target.source} → ${p.target.target}` });
|
|
68
|
+
}
|
|
69
|
+
setReveal((r) => ({ target: p.target, nonce: (r?.nonce ?? 0) + 1 }));
|
|
70
|
+
}, [nodes, onSelectionChange]);
|
|
69
71
|
const handleAddNode = React.useCallback(() => {
|
|
70
72
|
if (!canEdit)
|
|
71
73
|
return;
|
|
@@ -89,26 +91,36 @@ export function FlowPreview({ draft, editing, selection, onSelectionChange, onPa
|
|
|
89
91
|
return (_jsx(PreviewShell, { hint: `flow${designMode ? ' · design' : ''}`, children: canEdit ? (_jsx("div", { className: "p-3", children: _jsxs("button", { type: "button", className: "inline-flex items-center gap-1 rounded border border-dashed px-3 py-2 text-xs text-muted-foreground hover:bg-muted/30 hover:text-foreground", onClick: handleAddNode, children: [_jsx(Plus, { className: "h-3 w-3" }), tr('engine.inspector.add.node', locale)] }) })) : (_jsx(PreviewMessage, { children: "Add nodes in the Form tab to see the flow preview." })) }));
|
|
90
92
|
}
|
|
91
93
|
return (_jsx(PreviewShell, { hint: `flow · ${nodes.length} node${nodes.length === 1 ? '' : 's'}`, children: _jsx(PreviewErrorBoundary, { fallbackHint: "One of the flow nodes or edges is malformed.", children: _jsxs("div", { className: 'grid gap-0 h-full min-h-[440px] ' +
|
|
92
|
-
(showDebug || showVars || showRuns ? 'lg:grid-cols-[1fr_240px]' : 'grid-cols-1'), children: [_jsxs("div", { className: "flex flex-col min-w-0 min-h-0", children: [_jsxs("div", { className: "rounded-none border-b bg-muted/30 px-3 py-2 text-xs flex flex-wrap items-center gap-x-4 gap-y-1", children: [_jsx(Pill, { icon: Zap, label: "Trigger", value: flowType }), _jsx(Pill, { icon: CircleDot, label: "Status", value: status, tone: status === 'active' ? 'green' : status === 'draft' ? 'gray' : 'amber' }), _jsx(Pill, { icon: Settings2, label: "Run as", value: runAs }), version && _jsx(Pill, { label: "v", value: version }), errorStrategy && _jsx(Pill, { icon: GitBranch, label: "On error", value: errorStrategy }), _jsxs("div", { className: "ml-auto flex items-center gap-1.5", children: [!showDebug && !showRuns && (_jsxs("button", { type: "button", onClick: () => setShowVars((v) => !v), className: 'inline-flex items-center gap-1 rounded border px-2 py-0.5 text-[11px] font-medium transition-colors ' +
|
|
94
|
+
(showDebug || showVars || showRuns || showProblems ? 'lg:grid-cols-[1fr_240px]' : 'grid-cols-1'), children: [_jsxs("div", { className: "flex flex-col min-w-0 min-h-0", children: [_jsxs("div", { className: "rounded-none border-b bg-muted/30 px-3 py-2 text-xs flex flex-wrap items-center gap-x-4 gap-y-1", children: [_jsx(Pill, { icon: Zap, label: "Trigger", value: flowType }), _jsx(Pill, { icon: CircleDot, label: "Status", value: status, tone: status === 'active' ? 'green' : status === 'draft' ? 'gray' : 'amber' }), _jsx(Pill, { icon: Settings2, label: "Run as", value: runAs }), version && _jsx(Pill, { label: "v", value: version }), errorStrategy && _jsx(Pill, { icon: GitBranch, label: "On error", value: errorStrategy }), _jsxs("div", { className: "ml-auto flex items-center gap-1.5", children: [!showDebug && !showRuns && !showProblems && (_jsxs("button", { type: "button", onClick: () => setShowVars((v) => !v), className: 'inline-flex items-center gap-1 rounded border px-2 py-0.5 text-[11px] font-medium transition-colors ' +
|
|
93
95
|
(showVars
|
|
94
96
|
? 'border-violet-500 bg-violet-50 text-violet-700'
|
|
95
97
|
: 'border-border text-muted-foreground hover:bg-muted/50 hover:text-foreground'), title: showVars ? 'Hide variables panel' : 'Show variables panel', children: [_jsx(PanelRight, { className: "h-3 w-3" }), " Variables"] })), flowName && (_jsxs("button", { type: "button", onClick: () => {
|
|
96
98
|
setShowRuns((v) => !v);
|
|
97
99
|
setShowDebug(false);
|
|
100
|
+
setShowProblems(false);
|
|
98
101
|
}, className: 'inline-flex items-center gap-1 rounded border px-2 py-0.5 text-[11px] font-medium transition-colors ' +
|
|
99
102
|
(showRuns
|
|
100
103
|
? 'border-emerald-500 bg-emerald-50 text-emerald-700'
|
|
101
104
|
: 'border-border text-muted-foreground hover:bg-muted/50 hover:text-foreground'), title: "Run history from the automation engine", children: [_jsx(History, { className: "h-3 w-3" }), " Runs"] })), _jsxs("button", { type: "button", onClick: () => {
|
|
105
|
+
setShowProblems((v) => !v);
|
|
106
|
+
setShowDebug(false);
|
|
107
|
+
setShowRuns(false);
|
|
108
|
+
}, className: 'inline-flex items-center gap-1 rounded border px-2 py-0.5 text-[11px] font-medium transition-colors ' +
|
|
109
|
+
(showProblems
|
|
110
|
+
? 'border-rose-500 bg-rose-50 text-rose-700'
|
|
111
|
+
: 'border-border text-muted-foreground hover:bg-muted/50 hover:text-foreground'), title: "Validation problems", children: [_jsx(AlertCircle, { className: "h-3 w-3" }), " Problems", problems.length > 0 && (_jsx("span", { className: 'ml-0.5 inline-flex min-w-[16px] items-center justify-center rounded-full px-1 text-[10px] font-semibold ' +
|
|
112
|
+
(errorCount > 0 ? 'bg-destructive/15 text-destructive' : 'bg-amber-500/15 text-amber-600'), children: problems.length }))] }), _jsxs("button", { type: "button", onClick: () => {
|
|
102
113
|
setShowDebug((v) => !v);
|
|
103
114
|
setShowRuns(false);
|
|
115
|
+
setShowProblems(false);
|
|
104
116
|
}, className: 'inline-flex items-center gap-1 rounded border px-2 py-0.5 text-[11px] font-medium transition-colors ' +
|
|
105
117
|
(showDebug
|
|
106
118
|
? 'border-sky-500 bg-sky-50 text-sky-700'
|
|
107
|
-
: 'border-border text-muted-foreground hover:bg-muted/50 hover:text-foreground'), children: [_jsx(Bug, { className: "h-3 w-3" }), " Debug"] })] })] }), _jsx("div", { className: "flex-1 min-h-0", children: _jsx(FlowCanvas, { nodes: nodes, edges: edges, editable: canEdit, designMode: designMode, selectedId: selectedId, selectedEdgeId: selectedEdgeId, locale: locale, activeNodeId: runHL?.activeNodeId ?? null, visitedNodeIds: runHL?.visitedNodeIds, traversedEdgeIds: runHL?.traversedEdgeIds, invalidNodeIds: invalidNodeIds, invalidEdges: invalidEdges,
|
|
119
|
+
: 'border-border text-muted-foreground hover:bg-muted/50 hover:text-foreground'), children: [_jsx(Bug, { className: "h-3 w-3" }), " Debug"] })] })] }), _jsx("div", { className: "flex-1 min-h-0", children: _jsx(FlowCanvas, { nodes: nodes, edges: edges, editable: canEdit, designMode: designMode, selectedId: selectedId, selectedEdgeId: selectedEdgeId, locale: locale, activeNodeId: runHL?.activeNodeId ?? null, visitedNodeIds: runHL?.visitedNodeIds, traversedEdgeIds: runHL?.traversedEdgeIds, invalidNodeIds: invalidNodeIds, invalidEdges: invalidEdges, onRevealProblem: handleSelectProblem, problems: problems, revealSignal: reveal, onSelect: (n) => n
|
|
108
120
|
? onSelectionChange?.({ kind: 'node', id: n.id, label: n.label || n.id })
|
|
109
121
|
: onSelectionChange?.(null), onSelectEdge: (e, key) => e
|
|
110
122
|
? onSelectionChange?.({ kind: 'edge', id: key, label: `${e.source} → ${e.target}` })
|
|
111
|
-
: onSelectionChange?.(null), onPatch: onPatch }) })] }), showDebug ? (_jsx("div", { className: "border-l bg-muted/20", children: _jsx(FlowSimulatorPanel, { nodes: nodes, edges: edges, variables: variables, onRunStateChange: setRunHL }) })) : showRuns && flowName ? (_jsx("div", { className: "border-l bg-muted/20", children: _jsx(FlowRunsPanel, { flowName: flowName }) })) : showVars ? (_jsxs("div", { className: "border-l bg-muted/20 p-3 text-xs space-y-2", children: [_jsxs("div", { className: "flex items-center gap-1.5 font-medium text-muted-foreground", children: [_jsx(Variable, { className: "h-3 w-3" }), " Variables"] }), variables.length === 0 ? (_jsx("div", { className: "text-muted-foreground italic", children: "No variables declared." })) : (_jsx("ul", { className: "space-y-1.5", children: variables.map((v, i) => (_jsxs("li", { className: "rounded border bg-background p-1.5", children: [_jsxs("div", { className: "flex items-baseline gap-1 flex-wrap", children: [_jsx("span", { className: "font-mono", children: v.name }), v.type && (_jsx("span", { className: "text-[10px] uppercase text-muted-foreground", children: v.type })), v.isInput && (_jsx("span", { className: "text-[9px] font-semibold uppercase px-1 rounded bg-sky-100 text-sky-700", children: "in" })), v.isOutput && (_jsx("span", { className: "text-[9px] font-semibold uppercase px-1 rounded bg-emerald-100 text-emerald-700", children: "out" }))] }), v.defaultValue !== undefined && (_jsxs("div", { className: "text-[10px] text-muted-foreground font-mono truncate", children: ["= ", String(v.defaultValue)] }))] }, v.name || i))) }))] })) : null] }) }) }));
|
|
123
|
+
: onSelectionChange?.(null), onPatch: onPatch }) })] }), showProblems ? (_jsx("div", { className: "border-l bg-muted/20", children: _jsx(ProblemsPanel, { problems: problems, selectedKey: selectedKey, onSelectProblem: handleSelectProblem }) })) : showDebug ? (_jsx("div", { className: "border-l bg-muted/20", children: _jsx(FlowSimulatorPanel, { nodes: nodes, edges: edges, variables: variables, onRunStateChange: setRunHL }) })) : showRuns && flowName ? (_jsx("div", { className: "border-l bg-muted/20", children: _jsx(FlowRunsPanel, { flowName: flowName }) })) : showVars ? (_jsxs("div", { className: "border-l bg-muted/20 p-3 text-xs space-y-2", children: [_jsxs("div", { className: "flex items-center gap-1.5 font-medium text-muted-foreground", children: [_jsx(Variable, { className: "h-3 w-3" }), " Variables"] }), variables.length === 0 ? (_jsx("div", { className: "text-muted-foreground italic", children: "No variables declared." })) : (_jsx("ul", { className: "space-y-1.5", children: variables.map((v, i) => (_jsxs("li", { className: "rounded border bg-background p-1.5", children: [_jsxs("div", { className: "flex items-baseline gap-1 flex-wrap", children: [_jsx("span", { className: "font-mono", children: v.name }), v.type && (_jsx("span", { className: "text-[10px] uppercase text-muted-foreground", children: v.type })), v.isInput && (_jsx("span", { className: "text-[9px] font-semibold uppercase px-1 rounded bg-sky-100 text-sky-700", children: "in" })), v.isOutput && (_jsx("span", { className: "text-[9px] font-semibold uppercase px-1 rounded bg-emerald-100 text-emerald-700", children: "out" }))] }), v.defaultValue !== undefined && (_jsxs("div", { className: "text-[10px] text-muted-foreground font-mono truncate", children: ["= ", String(v.defaultValue)] }))] }, v.name || i))) }))] })) : null] }) }) }));
|
|
112
124
|
}
|
|
113
125
|
function Pill({ icon: Icon, label, value, tone = 'gray', }) {
|
|
114
126
|
const cls = tone === 'green'
|
|
@@ -19,6 +19,7 @@ import * as React from 'react';
|
|
|
19
19
|
import { Badge, Button, cn, Popover, PopoverContent, PopoverTrigger, } from '@object-ui/components';
|
|
20
20
|
import { GripVertical, Plus, ChevronDown, ChevronRight, Trash2, ArrowUp, ArrowDown, FolderPlus, FolderInput, ChevronsDownUp, ChevronsUpDown, CheckSquare, GitCompareArrows, Sparkles, X, } from 'lucide-react';
|
|
21
21
|
import { requestAssistantOpen } from '../../../assistant/assistantBus';
|
|
22
|
+
import { useAiSurfaceEnabled } from '../../../hooks/useAiSurface';
|
|
22
23
|
import { readFields, writeFields, newField, toFieldName, groupEntries, readGroups, addGroup, renameGroup, removeGroup, moveGroup, clearFieldGroup, diffFields, } from './object-fields-io';
|
|
23
24
|
import { FIELD_TYPE_META, TYPES_BY_CATEGORY, CATEGORY_LABEL_EN, CATEGORY_LABEL_ZH, CATEGORY_TONE, } from './field-types';
|
|
24
25
|
import { FieldStub } from './FieldStub';
|
|
@@ -30,6 +31,10 @@ const typeLabel = (meta, locale) => meta ? (isZh(locale) ? meta.labelZh : meta.l
|
|
|
30
31
|
const categoryLabel = (cat, locale) => (isZh(locale) ? CATEGORY_LABEL_ZH : CATEGORY_LABEL_EN)[cat];
|
|
31
32
|
export function ObjectFormCanvas({ objectName, draft, baseline, onPatch, selection, onSelectionChange, locale, }) {
|
|
32
33
|
const readOnly = !onPatch;
|
|
34
|
+
// The "Ask AI" affordances arm the global chat FAB (requestAssistantOpen).
|
|
35
|
+
// Hide them when the runtime serves no AI — otherwise they'd dead-end with no
|
|
36
|
+
// FAB to open. Same server-pushed signal the FAB itself gates on.
|
|
37
|
+
const { enabled: aiEnabled } = useAiSurfaceEnabled();
|
|
33
38
|
const view = React.useMemo(() => readFields(draft.fields), [draft]);
|
|
34
39
|
/* ─── Review/diff mode — draft vs last published ─── */
|
|
35
40
|
const diff = React.useMemo(() => (baseline ? diffFields(baseline.fields, draft.fields) : null), [baseline, draft]);
|
|
@@ -311,14 +316,14 @@ export function ObjectFormCanvas({ objectName, draft, baseline, onPatch, selecti
|
|
|
311
316
|
// Section chrome (headers, collapse, drop-to-assign) only appears once
|
|
312
317
|
// groups exist — otherwise the canvas stays a flat field list.
|
|
313
318
|
const showSectionChrome = hasGroups || groups.length > 1;
|
|
314
|
-
return (_jsxs("div", { className: "h-full overflow-auto bg-muted/20", onClick: handleBgClick, "data-object-name": objectName, children: [!readOnly && multiSel.size > 0 && (_jsx(BulkActionBar, { count: multiSel.size, groups: declaredGroups, onMoveToGroup: bulkSetGroup, onDelete: bulkDelete, onClear: clearMulti, locale: locale })), _jsxs("div", { className: "mx-auto max-w-3xl px-6 py-6 space-y-4", onClick: handleBgClick, children: [!emptyState && (_jsx(CanvasToolbar, { fieldCount: view.entries.length, requiredCount: requiredCount, sectionCount: declaredGroups.length, allCollapsed: allCollapsed, onToggleAll: showSectionChrome ? () => setAllCollapsed(!allCollapsed) : undefined, reviewAvailable: changeCount > 0, reviewing: reviewing, diffCounts: diff?.counts, onToggleReview: () => setReviewMode((v) => !v), locale: locale })), emptyState ? (_jsx(EmptyCanvas, { onAdd: readOnly ? undefined : addField, locale: locale })) : (_jsx("div", { className: "space-y-5", children: groups.map((g) => {
|
|
319
|
+
return (_jsxs("div", { className: "h-full overflow-auto bg-muted/20", onClick: handleBgClick, "data-object-name": objectName, children: [!readOnly && multiSel.size > 0 && (_jsx(BulkActionBar, { count: multiSel.size, groups: declaredGroups, onMoveToGroup: bulkSetGroup, onDelete: bulkDelete, onClear: clearMulti, locale: locale })), _jsxs("div", { className: "mx-auto max-w-3xl px-6 py-6 space-y-4", onClick: handleBgClick, children: [!emptyState && (_jsx(CanvasToolbar, { fieldCount: view.entries.length, requiredCount: requiredCount, sectionCount: declaredGroups.length, allCollapsed: allCollapsed, onToggleAll: showSectionChrome ? () => setAllCollapsed(!allCollapsed) : undefined, reviewAvailable: changeCount > 0, reviewing: reviewing, diffCounts: diff?.counts, onToggleReview: () => setReviewMode((v) => !v), locale: locale })), emptyState ? (_jsx(EmptyCanvas, { onAdd: readOnly ? undefined : addField, locale: locale, aiEnabled: aiEnabled })) : (_jsx("div", { className: "space-y-5", children: groups.map((g) => {
|
|
315
320
|
const declaredIdx = g.key
|
|
316
321
|
? declaredGroups.findIndex((d) => d.key === g.key)
|
|
317
322
|
: -1;
|
|
318
323
|
return (_jsxs(GroupSection, { groupKey: g.key, label: g.key === null ? t('designer.canvas.ungrouped', locale) : g.label, count: g.entries.length, showHeader: showSectionChrome, collapsed: !!collapsed[collapseKey(g.key)], onToggleCollapse: () => toggleCollapse(g.key), readOnly: readOnly, locale: locale, canMoveUp: declaredIdx > 0, canMoveDown: declaredIdx >= 0 && declaredIdx < declaredGroups.length - 1, onRename: g.key ? (label) => renameSection(g.key, label) : undefined, onRemove: g.key ? () => removeSection(g.key) : undefined, onMove: g.key ? (dir) => moveSection(g.key, dir) : undefined, onAddField: readOnly ? undefined : (type) => addField(type, g.key), onDropField: readOnly ? undefined : moveToGroup, children: [g.entries.map((entry) => (_jsx(FieldRow, { entry: entry, selected: entry.name === selectedName, multiSelected: multiSel.has(entry.name), diffStatus: statusOf(entry.name), changedKeys: changedKeysOf(entry.name), readOnly: readOnly, locale: locale, onClick: (e) => handleRowClick(entry, e), onReorder: readOnly ? undefined : reorderField, onRenameLabel: readOnly ? undefined : renameLabel, onMoveOffset: readOnly ? undefined : (dir) => moveFieldByOffset(entry.name, dir) }, entry.name))), g.entries.length === 0 && (_jsx("div", { className: "rounded-md border border-dashed bg-background/40 px-3 py-4 text-center text-[11px] text-muted-foreground", children: readOnly
|
|
319
324
|
? t('designer.canvas.emptySection', locale)
|
|
320
325
|
: t('designer.canvas.dropHint', locale) }))] }, g.key ?? '__ungrouped__'));
|
|
321
|
-
}) })), reviewing && diff && diff.removed.length > 0 && (_jsxs("div", { className: "space-y-2.5", children: [_jsx("div", { className: "text-[11px] font-medium uppercase tracking-wider text-destructive/80 pl-1", children: t('designer.canvas.diffRemoved', locale) }), diff.removed.map((entry) => (_jsx(GhostFieldRow, { entry: entry, locale: locale }, entry.name)))] })), !emptyState && !readOnly && (_jsxs("div", { className: "flex items-center gap-2 pt-1", children: [_jsx(AddFieldButton, { onPick: (type) => addField(type), locale: locale }), _jsxs(Button, { variant: "ghost", size: "sm", className: "gap-1.5 text-muted-foreground hover:text-foreground", onClick: addSection, children: [_jsx(FolderPlus, { className: "h-3.5 w-3.5" }), t('designer.canvas.addSection', locale)] }), _jsxs(Button, { variant: "ghost", size: "sm", className: "gap-1.5 ml-auto text-primary/80 hover:text-primary", onClick: () => requestAssistantOpen(), children: [_jsx(Sparkles, { className: "h-3.5 w-3.5" }), t('designer.canvas.askAi', locale)] })] }))] })] }));
|
|
326
|
+
}) })), reviewing && diff && diff.removed.length > 0 && (_jsxs("div", { className: "space-y-2.5", children: [_jsx("div", { className: "text-[11px] font-medium uppercase tracking-wider text-destructive/80 pl-1", children: t('designer.canvas.diffRemoved', locale) }), diff.removed.map((entry) => (_jsx(GhostFieldRow, { entry: entry, locale: locale }, entry.name)))] })), !emptyState && !readOnly && (_jsxs("div", { className: "flex items-center gap-2 pt-1", children: [_jsx(AddFieldButton, { onPick: (type) => addField(type), locale: locale }), _jsxs(Button, { variant: "ghost", size: "sm", className: "gap-1.5 text-muted-foreground hover:text-foreground", onClick: addSection, children: [_jsx(FolderPlus, { className: "h-3.5 w-3.5" }), t('designer.canvas.addSection', locale)] }), aiEnabled && (_jsxs(Button, { variant: "ghost", size: "sm", className: "gap-1.5 ml-auto text-primary/80 hover:text-primary", onClick: () => requestAssistantOpen(), children: [_jsx(Sparkles, { className: "h-3.5 w-3.5" }), t('designer.canvas.askAi', locale)] }))] }))] })] }));
|
|
322
327
|
}
|
|
323
328
|
/* ─────────────── Review toolbar ─────────────── */
|
|
324
329
|
function CanvasToolbar({ fieldCount, requiredCount, sectionCount, allCollapsed, onToggleAll, reviewAvailable, reviewing, diffCounts, onToggleReview, locale, }) {
|
|
@@ -509,8 +514,8 @@ function FieldRow({ entry, selected, multiSelected, diffStatus, changedKeys, rea
|
|
|
509
514
|
? tFormat('designer.canvas.diffChangedKeys', locale, { keys: changedKeys.join(', ') })
|
|
510
515
|
: undefined, children: t('designer.canvas.diffChanged', locale) })), _jsx(Badge, { variant: "outline", className: cn('text-[10px] font-medium', tone.badge), children: typeLabel(meta, locale) ?? typeStr })] })] }), description && (_jsx("div", { className: "text-[11px] text-muted-foreground mb-1.5 line-clamp-1", children: description })), _jsx(FieldStub, { type: typeStr, label: label, placeholder: placeholder, options: options, referenceTo: referenceTo, formula: formula, locale: locale })] }), dropZone === 'after' && (_jsx("div", { className: "absolute left-0 right-0 -bottom-1 h-0.5 bg-primary rounded-full" }))] }));
|
|
511
516
|
}
|
|
512
|
-
function EmptyCanvas({ onAdd, locale }) {
|
|
513
|
-
return (_jsxs("div", { className: "rounded-lg border-2 border-dashed bg-background py-16 px-6 text-center space-y-3", children: [_jsx("div", { className: "text-sm font-medium", children: t('designer.canvas.noFields', locale) }), _jsx("div", { className: "text-xs text-muted-foreground", children: t('designer.canvas.noFieldsHint', locale) }), onAdd && (_jsxs("div", { className: "pt-2 flex items-center justify-center gap-2", children: [_jsx(AddFieldButton, { onPick: onAdd, locale: locale }), _jsxs(Button, { variant: "ghost", size: "sm", className: "gap-1.5 text-primary/80 hover:text-primary", onClick: () => requestAssistantOpen(), children: [_jsx(Sparkles, { className: "h-3.5 w-3.5" }), t('designer.canvas.askAiGenerate', locale)] })] }))] }));
|
|
517
|
+
function EmptyCanvas({ onAdd, locale, aiEnabled, }) {
|
|
518
|
+
return (_jsxs("div", { className: "rounded-lg border-2 border-dashed bg-background py-16 px-6 text-center space-y-3", children: [_jsx("div", { className: "text-sm font-medium", children: t('designer.canvas.noFields', locale) }), _jsx("div", { className: "text-xs text-muted-foreground", children: t('designer.canvas.noFieldsHint', locale) }), onAdd && (_jsxs("div", { className: "pt-2 flex items-center justify-center gap-2", children: [_jsx(AddFieldButton, { onPick: onAdd, locale: locale }), aiEnabled && (_jsxs(Button, { variant: "ghost", size: "sm", className: "gap-1.5 text-primary/80 hover:text-primary", onClick: () => requestAssistantOpen(), children: [_jsx(Sparkles, { className: "h-3.5 w-3.5" }), t('designer.canvas.askAiGenerate', locale)] }))] }))] }));
|
|
514
519
|
}
|
|
515
520
|
function AddFieldButton({ onPick, compact, locale }) {
|
|
516
521
|
const [open, setOpen] = React.useState(false);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProblemsPanel — lists every structural + server validation issue for the
|
|
3
|
+
* flow draft. Each row shows the severity icon and the message; clicking a row
|
|
4
|
+
* selects and reveals (pans to) the offending node/edge on the canvas. Mirrors
|
|
5
|
+
* the "Problems" tab of an IDE / the error panel in Salesforce Flow Builder.
|
|
6
|
+
*
|
|
7
|
+
* Pure presentation: the issue list is derived upstream (see `flow-problems`)
|
|
8
|
+
* from the live draft, so rows clear as the author fixes each problem.
|
|
9
|
+
*/
|
|
10
|
+
import * as React from 'react';
|
|
11
|
+
import type { FlowProblem } from './flow-problems';
|
|
12
|
+
export interface ProblemsPanelProps {
|
|
13
|
+
problems: FlowProblem[];
|
|
14
|
+
/** Selected element key (`node:<id>` or an edge's `edgeKey`) to highlight matching rows. */
|
|
15
|
+
selectedKey?: string | null;
|
|
16
|
+
onSelectProblem: (problem: FlowProblem) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare function ProblemsPanel({ problems, selectedKey, onSelectProblem }: ProblemsPanelProps): React.JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AlertCircle, AlertTriangle, CheckCircle2, CircleDot, GitBranch } from 'lucide-react';
|
|
3
|
+
import { cn } from '@object-ui/components';
|
|
4
|
+
function targetLabel(p) {
|
|
5
|
+
if (p.target.kind === 'node')
|
|
6
|
+
return p.target.nodeId;
|
|
7
|
+
if (p.target.kind === 'edge')
|
|
8
|
+
return `${p.target.source} → ${p.target.target}`;
|
|
9
|
+
return 'flow';
|
|
10
|
+
}
|
|
11
|
+
export function ProblemsPanel({ problems, selectedKey, onSelectProblem }) {
|
|
12
|
+
const errorCount = problems.filter((p) => p.level === 'error').length;
|
|
13
|
+
const warningCount = problems.length - errorCount;
|
|
14
|
+
return (_jsxs("div", { className: "flex h-full flex-col text-xs", children: [_jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-2 font-medium text-muted-foreground", children: [_jsx("span", { children: "Problems" }), errorCount > 0 && (_jsxs("span", { className: "inline-flex items-center gap-1 text-destructive", children: [_jsx(AlertCircle, { className: "h-3 w-3" }), " ", errorCount] })), warningCount > 0 && (_jsxs("span", { className: "inline-flex items-center gap-1 text-amber-600 dark:text-amber-400", children: [_jsx(AlertTriangle, { className: "h-3 w-3" }), " ", warningCount] }))] }), problems.length === 0 ? (_jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-1.5 p-4 text-center text-muted-foreground", children: [_jsx(CheckCircle2, { className: "h-5 w-5 text-emerald-500" }), _jsx("span", { children: "No problems \u2014 this flow is structurally valid." })] })) : (_jsx("ul", { className: "flex-1 overflow-auto p-1.5", children: problems.map((p) => {
|
|
15
|
+
const isEdge = p.target.kind === 'edge';
|
|
16
|
+
const isFlow = p.target.kind === 'flow';
|
|
17
|
+
const key = p.target.kind === 'node'
|
|
18
|
+
? `node:${p.target.nodeId}`
|
|
19
|
+
: p.target.kind === 'edge'
|
|
20
|
+
? p.target.edgeKey
|
|
21
|
+
: null;
|
|
22
|
+
const active = !!key && key === selectedKey;
|
|
23
|
+
const Icon = p.level === 'error' ? AlertCircle : AlertTriangle;
|
|
24
|
+
const TargetIcon = isEdge ? GitBranch : CircleDot;
|
|
25
|
+
return (_jsx("li", { children: _jsxs("button", { type: "button", disabled: isFlow, onClick: () => onSelectProblem(p), className: cn('flex w-full items-start gap-2 rounded-md px-2 py-1.5 text-left transition-colors', isFlow ? 'cursor-default' : 'cursor-pointer hover:bg-accent', active && 'bg-accent ring-1 ring-primary/40'), children: [_jsx(Icon, { className: cn('mt-0.5 h-3.5 w-3.5 shrink-0', p.level === 'error' ? 'text-destructive' : 'text-amber-500') }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block leading-snug text-foreground", children: p.message }), _jsxs("span", { className: "mt-0.5 flex items-center gap-1 text-[10px] text-muted-foreground", children: [_jsx(TargetIcon, { className: "h-2.5 w-2.5 shrink-0" }), _jsx("span", { className: "truncate font-mono", children: targetLabel(p) }), p.source === 'server' && _jsx("span", { className: "uppercase tracking-wide", children: "\u00B7 schema" }), p.source === 'expression' && _jsx("span", { className: "uppercase tracking-wide", children: "\u00B7 expression" })] })] })] }) }, p.id));
|
|
26
|
+
}) }))] }));
|
|
27
|
+
}
|
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
*
|
|
5
5
|
* A 9.0 report binds a semantic-layer `dataset` and selects its measures
|
|
6
6
|
* (`values`) grouped by dimensions (`rows`, plus `columns` across for a
|
|
7
|
-
* matrix);
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* matrix); a `joined` report instead stacks dataset-bound `blocks`. Both
|
|
8
|
+
* render through plugin-report's `ReportRenderer` (→ DatasetReportRenderer),
|
|
9
|
+
* keeping the studio preview pixel-equal with the runtime — including the
|
|
10
|
+
* matrix cross-tab and the joined block stack — and the numbers consistent
|
|
11
|
+
* with every other surface on the same dataset (`adapter.queryDataset`).
|
|
12
|
+
* Drill-down stays inert here: the preview passes no `onDrill` sink.
|
|
12
13
|
*
|
|
13
|
-
* A draft
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* A draft with neither a dataset nor any dataset-bound block gets an
|
|
15
|
+
* actionable empty state pointing at the right inspector control instead of
|
|
16
|
+
* the retired pre-9.0 inline-query renderer.
|
|
16
17
|
*/
|
|
17
18
|
import * as React from 'react';
|
|
18
19
|
import type { MetadataPreviewProps } from '../preview-registry';
|
|
@@ -6,15 +6,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
*
|
|
7
7
|
* A 9.0 report binds a semantic-layer `dataset` and selects its measures
|
|
8
8
|
* (`values`) grouped by dimensions (`rows`, plus `columns` across for a
|
|
9
|
-
* matrix);
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* matrix); a `joined` report instead stacks dataset-bound `blocks`. Both
|
|
10
|
+
* render through plugin-report's `ReportRenderer` (→ DatasetReportRenderer),
|
|
11
|
+
* keeping the studio preview pixel-equal with the runtime — including the
|
|
12
|
+
* matrix cross-tab and the joined block stack — and the numbers consistent
|
|
13
|
+
* with every other surface on the same dataset (`adapter.queryDataset`).
|
|
14
|
+
* Drill-down stays inert here: the preview passes no `onDrill` sink.
|
|
14
15
|
*
|
|
15
|
-
* A draft
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* A draft with neither a dataset nor any dataset-bound block gets an
|
|
17
|
+
* actionable empty state pointing at the right inspector control instead of
|
|
18
|
+
* the retired pre-9.0 inline-query renderer.
|
|
18
19
|
*/
|
|
19
20
|
import * as React from 'react';
|
|
20
21
|
import { Database, Loader2 } from 'lucide-react';
|
|
@@ -23,13 +24,29 @@ import { PreviewShell, PreviewErrorBoundary, PreviewEmptyState } from './Preview
|
|
|
23
24
|
const ReportRenderer = React.lazy(() => import('@object-ui/plugin-report').then((m) => ({ default: m.ReportRenderer })));
|
|
24
25
|
export function ReportPreview({ draft }) {
|
|
25
26
|
const adapter = useAdapter();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const d = draft;
|
|
28
|
+
// ADR-0021 single-form: a report binds a semantic-layer dataset; a `joined`
|
|
29
|
+
// report instead carries its data on dataset-bound `blocks`. Both render
|
|
30
|
+
// through plugin-report's ReportRenderer (→ DatasetReportRenderer, which
|
|
31
|
+
// stacks each block). Previously only the single-dataset shape was
|
|
32
|
+
// previewed, so a joined report fell through to the "bind a dataset" empty
|
|
33
|
+
// state and the author designed blind.
|
|
34
|
+
const hasDataset = typeof d.dataset === 'string' && !!d.dataset;
|
|
35
|
+
const isJoinedWithBlocks = d.type === 'joined' &&
|
|
36
|
+
Array.isArray(d.blocks) &&
|
|
37
|
+
d.blocks.some((b) => typeof b?.dataset === 'string' && !!b.dataset);
|
|
38
|
+
if (hasDataset || isJoinedWithBlocks) {
|
|
39
|
+
const rows = Array.isArray(d.rows) ? d.rows.filter(Boolean) : [];
|
|
40
|
+
const hint = isJoinedWithBlocks
|
|
41
|
+
? `report · joined · ${d.blocks.length} block${d.blocks.length === 1 ? '' : 's'}`
|
|
42
|
+
: `report · dataset "${d.dataset}"${rows.length ? ' · by ' + rows.join(', ') : ''}`;
|
|
43
|
+
return (_jsx(PreviewShell, { hint: hint, children: _jsx(PreviewErrorBoundary, { fallbackHint: "The Report references a dataset/measure that doesn't resolve, or its config is incomplete.", children: _jsx(React.Suspense, { fallback: _jsxs("div", { className: "flex items-center gap-2 p-4 text-xs text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " Loading report renderer\u2026"] }), children: _jsx("div", { className: "p-3 min-h-[200px] max-h-[70vh] overflow-auto", children: _jsx(ReportRenderer, { schema: draft, dataSource: adapter }) }) }) }) }));
|
|
30
44
|
}
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
45
|
+
// Nothing renderable yet. A joined report needs at least one dataset-bound
|
|
46
|
+
// block; every other type needs a top-level dataset. Point the author at the
|
|
47
|
+
// right control instead of the retired pre-9.0 inline-query renderer.
|
|
48
|
+
const joined = d.type === 'joined';
|
|
49
|
+
return (_jsx(PreviewShell, { children: _jsx(PreviewEmptyState, { icon: _jsx(Database, { className: "h-8 w-8" }), title: joined ? 'Add a block to preview this joined report' : 'Bind a dataset to preview this report', description: joined
|
|
50
|
+
? 'A joined report stacks dataset-bound blocks. Add a block and bind its dataset + measures in the right panel to start designing.'
|
|
51
|
+
: "Since the 9.0 single-form cutover a report renders its dataset's measures (values) grouped by dimensions (rows). Choose a Dataset in the right panel to start designing." }) }));
|
|
35
52
|
}
|
|
@@ -68,6 +68,14 @@ export interface NodeCardProps {
|
|
|
68
68
|
dimmed?: boolean;
|
|
69
69
|
/** Structural-validation error highlight (e.g. part of an un-declared cycle). */
|
|
70
70
|
invalid?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Validation badge shown at the card's top-right corner (error or warning),
|
|
73
|
+
* with the issue message(s) as its tooltip. Cleared when the issue resolves.
|
|
74
|
+
*/
|
|
75
|
+
badge?: {
|
|
76
|
+
level: 'error' | 'warning';
|
|
77
|
+
title: string;
|
|
78
|
+
};
|
|
71
79
|
onPointerDown?: (e: React.PointerEvent) => void;
|
|
72
80
|
onSelect?: () => void;
|
|
73
81
|
onAppend?: () => void;
|
|
@@ -83,7 +91,7 @@ export interface NodeCardProps {
|
|
|
83
91
|
* The card body drives selection + reposition; a dedicated bottom "+" handle
|
|
84
92
|
* (edit mode only) appends a connected child without ambiguity.
|
|
85
93
|
*/
|
|
86
|
-
export declare function NodeCard({ type, label, summary, position, selected, editable, runState, dimmed, invalid, onPointerDown, onSelect, onAppend, onAddReviseLoop, }: NodeCardProps): React.JSX.Element;
|
|
94
|
+
export declare function NodeCard({ type, label, summary, position, selected, editable, runState, dimmed, invalid, badge, onPointerDown, onSelect, onAppend, onAddReviseLoop, }: NodeCardProps): React.JSX.Element;
|
|
87
95
|
export interface NodePaletteProps {
|
|
88
96
|
locale?: string;
|
|
89
97
|
/** Node types to offer. Defaults to the hardcoded {@link NODE_PALETTE}. */
|
|
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
6
6
|
* palette popover. Kept dependency-free and Shadcn-native (Tailwind + lucide).
|
|
7
7
|
*/
|
|
8
8
|
import * as React from 'react';
|
|
9
|
-
import { Code, CircleDot, CircleStop, Diamond, FilePen, FilePlus, FileSearch, FileX, GitFork, Globe, IterationCcw, ListChecks, MonitorSmartphone, Play, Plug, Plus, Repeat, ShieldAlert, TimerReset, UserCheck, Variable, Workflow, Zap, } from 'lucide-react';
|
|
9
|
+
import { AlertCircle, AlertTriangle, Code, CircleDot, CircleStop, Diamond, FilePen, FilePlus, FileSearch, FileX, GitFork, Globe, IterationCcw, ListChecks, MonitorSmartphone, Play, Plug, Plus, Repeat, ShieldAlert, TimerReset, UserCheck, Variable, Workflow, Zap, } from 'lucide-react';
|
|
10
10
|
import { cn } from '@object-ui/components';
|
|
11
11
|
import { NODE_W, NODE_H } from './flow-canvas-layout';
|
|
12
12
|
export function nodeIcon(type) {
|
|
@@ -317,7 +317,7 @@ export function defaultNodeExtras(type) {
|
|
|
317
317
|
* The card body drives selection + reposition; a dedicated bottom "+" handle
|
|
318
318
|
* (edit mode only) appends a connected child without ambiguity.
|
|
319
319
|
*/
|
|
320
|
-
export function NodeCard({ type, label, summary, position, selected, editable, runState, dimmed, invalid, onPointerDown, onSelect, onAppend, onAddReviseLoop, }) {
|
|
320
|
+
export function NodeCard({ type, label, summary, position, selected, editable, runState, dimmed, invalid, badge, onPointerDown, onSelect, onAppend, onAddReviseLoop, }) {
|
|
321
321
|
const tone = nodeTone(type);
|
|
322
322
|
return (_jsxs("div", {
|
|
323
323
|
// `group` so the hover-revealed affordances (append "+", revise loop)
|
|
@@ -339,7 +339,9 @@ export function NodeCard({ type, label, summary, position, selected, editable, r
|
|
|
339
339
|
? 'border-primary shadow-md ring-2 ring-primary/30'
|
|
340
340
|
: invalid
|
|
341
341
|
? 'border-destructive ring-2 ring-destructive/50'
|
|
342
|
-
: 'border-border/80'), children: [_jsx("div", { className: cn('flex h-9 w-9 shrink-0 items-center justify-center rounded-lg transition-transform duration-150 group-hover:scale-105', tone.chip, runState === 'active' && 'animate-pulse'), children: _jsx(NodeTypeIcon, { type: type, className: cn('h-[18px] w-[18px]', tone.icon) }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { title: label, className: "truncate text-[13px] font-semibold leading-tight text-foreground", children: label }), _jsxs("div", { className: "mt-1 flex items-baseline gap-1.5 leading-tight", children: [_jsx("span", { className: cn('shrink-0 text-[10px] font-semibold uppercase tracking-[0.08em]', tone.label), children: type }), summary && (_jsx("span", { className: "min-w-0 truncate font-mono text-[10px] text-muted-foreground", title: summary, children: summary }))] })] })] }),
|
|
342
|
+
: 'border-border/80'), children: [_jsx("div", { className: cn('flex h-9 w-9 shrink-0 items-center justify-center rounded-lg transition-transform duration-150 group-hover:scale-105', tone.chip, runState === 'active' && 'animate-pulse'), children: _jsx(NodeTypeIcon, { type: type, className: cn('h-[18px] w-[18px]', tone.icon) }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { title: label, className: "truncate text-[13px] font-semibold leading-tight text-foreground", children: label }), _jsxs("div", { className: "mt-1 flex items-baseline gap-1.5 leading-tight", children: [_jsx("span", { className: cn('shrink-0 text-[10px] font-semibold uppercase tracking-[0.08em]', tone.label), children: type }), summary && (_jsx("span", { className: "min-w-0 truncate font-mono text-[10px] text-muted-foreground", title: summary, children: summary }))] })] })] }), badge && (_jsx("span", { title: badge.title, "data-problem": badge.level, className: cn('absolute -right-2 -top-2 z-20 inline-flex h-5 w-5 items-center justify-center rounded-full border bg-background shadow-sm', badge.level === 'error'
|
|
343
|
+
? 'border-destructive/50 text-destructive'
|
|
344
|
+
: 'border-amber-500/50 text-amber-600 dark:text-amber-400'), children: badge.level === 'error' ? (_jsx(AlertCircle, { className: "h-3 w-3" })) : (_jsx(AlertTriangle, { className: "h-3 w-3" })) })), editable && type !== 'end' && (_jsx("button", { type: "button", title: "Add connected node", "aria-label": "Add connected node", onPointerDown: (e) => e.stopPropagation(), onClick: (e) => {
|
|
343
345
|
e.stopPropagation();
|
|
344
346
|
onAppend?.();
|
|
345
347
|
}, className: cn('absolute left-1/2 -bottom-3 z-10 inline-flex h-6 w-6 -translate-x-1/2 items-center justify-center rounded-full border bg-background text-muted-foreground shadow-sm transition-colors', 'opacity-0 hover:border-primary hover:text-primary group-hover:opacity-100 focus-visible:opacity-100'), children: _jsx(Plus, { className: "h-3.5 w-3.5" }) })), onAddReviseLoop && (_jsx("button", { type: "button", title: "Add revision loop (send back for revision)", "aria-label": "Add revision loop", onPointerDown: (e) => e.stopPropagation(), onClick: (e) => {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DiagnosticLevel } from './simulator/flow-sim-types';
|
|
2
|
+
export interface ExprProblem {
|
|
3
|
+
target: {
|
|
4
|
+
kind: 'node';
|
|
5
|
+
nodeId: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'edge';
|
|
8
|
+
source: string;
|
|
9
|
+
target: string;
|
|
10
|
+
};
|
|
11
|
+
level: DiagnosticLevel;
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Scan a flow draft for expression problems, resolved onto node / edge targets.
|
|
16
|
+
* Pure: no network — the trigger object's fields are not expanded (root-only
|
|
17
|
+
* scope), which is why the start node is excluded from the ref check.
|
|
18
|
+
*/
|
|
19
|
+
export declare function flowExpressionProblems(draft: Record<string, unknown>): ExprProblem[];
|
|
@@ -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
|
+
};
|