@object-ui/app-shell 7.1.0 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +279 -0
- package/dist/console/AppContent.js +9 -15
- package/dist/console/ConsoleShell.d.ts +16 -0
- package/dist/console/ConsoleShell.js +43 -2
- package/dist/console/ai/AiChatPage.js +36 -9
- package/dist/console/home/HomeLayout.js +5 -7
- package/dist/console/home/HomePage.js +1 -9
- package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
- package/dist/console/organizations/OrganizationsPage.js +22 -3
- package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
- package/dist/console/organizations/provisionEnvironment.js +64 -0
- package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
- package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
- package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
- package/dist/environment/EnvironmentListToolbar.js +59 -0
- package/dist/environment/entitlements.d.ts +90 -0
- package/dist/environment/entitlements.js +91 -0
- package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
- package/dist/environment/useEnvironmentEntitlements.js +108 -0
- package/dist/hooks/useActionModal.js +15 -1
- package/dist/hooks/useAiSurface.d.ts +59 -0
- package/dist/hooks/useAiSurface.js +78 -0
- package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
- package/dist/hooks/useConsoleActionRuntime.js +36 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/layout/AppHeader.js +28 -4
- package/dist/layout/ConsoleFloatingChatbot.js +16 -2
- package/dist/layout/ConsoleLayout.js +5 -6
- package/dist/preview/DraftPreviewBar.js +20 -7
- package/dist/providers/ExpressionProvider.js +9 -3
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +1 -1
- package/dist/utils/recordFormNavigation.d.ts +60 -0
- package/dist/utils/recordFormNavigation.js +35 -0
- package/dist/utils/resolvePageVarTokens.d.ts +31 -0
- package/dist/utils/resolvePageVarTokens.js +72 -0
- package/dist/views/CreateViewDialog.js +14 -1
- package/dist/views/ObjectView.js +26 -12
- package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
- package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
- package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
- package/dist/views/metadata-admin/PackagesPage.js +49 -4
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +36 -4
- package/dist/views/metadata-admin/ResourceListPage.js +21 -4
- package/dist/views/metadata-admin/createBody.d.ts +26 -0
- package/dist/views/metadata-admin/createBody.js +30 -0
- package/dist/views/metadata-admin/i18n.js +20 -0
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +8 -0
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +17 -3
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +16 -2
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +15 -3
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
- package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
- package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +6 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +21 -10
- package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
- package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
- package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
- package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
- package/dist/views/metadata-admin/package-scope.d.ts +15 -0
- package/dist/views/metadata-admin/package-scope.js +16 -0
- package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +22 -3
- package/dist/views/metadata-admin/previews/FlowCanvas.js +45 -6
- package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
- package/dist/views/metadata-admin/previews/FlowPreview.js +42 -30
- package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
- package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
- package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
- package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
- package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +5 -3
- package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
- package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
- package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
- package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +9 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +4 -2
- package/package.json +38 -38
|
@@ -21,17 +21,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
21
21
|
* `onPatch(partial)` and the host merges + persists.
|
|
22
22
|
*/
|
|
23
23
|
import * as React from 'react';
|
|
24
|
-
import { AlertTriangle, Maximize2, Plus, ZoomIn, ZoomOut } from 'lucide-react';
|
|
24
|
+
import { AlertCircle, AlertTriangle, Maximize2, Plus, ZoomIn, ZoomOut } from 'lucide-react';
|
|
25
25
|
import { cn } from '@object-ui/components';
|
|
26
26
|
import { uniqueId, appendArray, spliceArray } from '../inspectors/_shared';
|
|
27
27
|
import { t as tr } from '../i18n';
|
|
28
|
-
import { computeLayout, diagramSize, bottomAnchor, topAnchor, rightAnchor, edgePath, edgeMidpoint, backEdgePath, backEdgeLabelAnchor, isBackEdge, edgeKey, conditionText, } from './flow-canvas-layout';
|
|
28
|
+
import { computeLayout, diagramSize, NODE_W, NODE_H, bottomAnchor, topAnchor, rightAnchor, edgePath, edgeMidpoint, backEdgePath, backEdgeLabelAnchor, isBackEdge, edgeKey, conditionText, } from './flow-canvas-layout';
|
|
29
29
|
import { NodeCard, NodePalette, defaultNodeLabel, defaultNodeExtras } from './flow-canvas-parts';
|
|
30
30
|
import { useFlowNodePalette } from './useFlowNodePalette';
|
|
31
|
+
import { indexProblemBadges, edgeProblemKey } from './flow-problems';
|
|
31
32
|
const MIN_ZOOM = 0.4;
|
|
32
33
|
const MAX_ZOOM = 1.6;
|
|
33
34
|
const DRAG_THRESHOLD = 4;
|
|
34
|
-
export function FlowCanvas({ nodes, edges, editable, designMode, selectedId, selectedEdgeId, locale, activeNodeId, visitedNodeIds, traversedEdgeIds, invalidNodeIds, invalidEdges,
|
|
35
|
+
export function FlowCanvas({ nodes, edges, editable, designMode, selectedId, selectedEdgeId, locale, activeNodeId, visitedNodeIds, traversedEdgeIds, invalidNodeIds, invalidEdges, onRevealProblem, problems, revealSignal, onSelect, onSelectEdge, onPatch, }) {
|
|
35
36
|
const viewportRef = React.useRef(null);
|
|
36
37
|
const [zoom, setZoom] = React.useState(1);
|
|
37
38
|
const [pan, setPan] = React.useState({ x: 0, y: 0 });
|
|
@@ -53,6 +54,13 @@ export function FlowCanvas({ nodes, edges, editable, designMode, selectedId, sel
|
|
|
53
54
|
const traversedSet = React.useMemo(() => new Set(traversedEdgeIds ?? []), [traversedEdgeIds]);
|
|
54
55
|
const invalidNodeSet = React.useMemo(() => new Set(invalidNodeIds ?? []), [invalidNodeIds]);
|
|
55
56
|
const simRunning = (visitedNodeIds?.length ?? 0) > 0 || !!activeNodeId;
|
|
57
|
+
// Per-element validation badges (errors dominate warnings on the same
|
|
58
|
+
// element). Derived from the live `problems` list so badges clear as issues
|
|
59
|
+
// are resolved.
|
|
60
|
+
const { byNode: nodeBadges, byEdge: edgeBadges } = React.useMemo(() => indexProblemBadges(problems ?? []), [problems]);
|
|
61
|
+
// Error-level problems shown in the always-visible inline banner — driven by
|
|
62
|
+
// the same `problems` list as the panel/badges so the three stay in lock-step.
|
|
63
|
+
const bannerErrors = React.useMemo(() => (problems ?? []).filter((p) => p.level === 'error'), [problems]);
|
|
56
64
|
const positionOf = React.useCallback((id) => {
|
|
57
65
|
if (dragPos && dragPos.id === id)
|
|
58
66
|
return { x: dragPos.x, y: dragPos.y };
|
|
@@ -272,6 +280,31 @@ export function FlowCanvas({ nodes, edges, editable, designMode, selectedId, sel
|
|
|
272
280
|
y: Math.max(16, (vp.clientHeight - size.height * z) / 2),
|
|
273
281
|
});
|
|
274
282
|
}, [size.height, size.width]);
|
|
283
|
+
// Pan to center an element when the Problems panel asks to reveal it. Driven
|
|
284
|
+
// by a changing `nonce` so re-clicking the same problem re-centers it.
|
|
285
|
+
React.useEffect(() => {
|
|
286
|
+
if (!revealSignal)
|
|
287
|
+
return;
|
|
288
|
+
const vp = viewportRef.current;
|
|
289
|
+
if (!vp)
|
|
290
|
+
return;
|
|
291
|
+
const t = revealSignal.target;
|
|
292
|
+
let pt = null;
|
|
293
|
+
if (t.kind === 'node') {
|
|
294
|
+
const p = layout.get(t.nodeId);
|
|
295
|
+
if (p)
|
|
296
|
+
pt = { x: p.x + NODE_W / 2, y: p.y + NODE_H / 2 };
|
|
297
|
+
}
|
|
298
|
+
else if (t.kind === 'edge') {
|
|
299
|
+
const s = layout.get(t.source);
|
|
300
|
+
const d = layout.get(t.target);
|
|
301
|
+
if (s && d)
|
|
302
|
+
pt = { x: (s.x + d.x) / 2 + NODE_W / 2, y: (s.y + d.y) / 2 + NODE_H / 2 };
|
|
303
|
+
}
|
|
304
|
+
if (pt)
|
|
305
|
+
setPan({ x: vp.clientWidth / 2 - pt.x * zoom, y: vp.clientHeight / 2 - pt.y * zoom });
|
|
306
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
307
|
+
}, [revealSignal?.nonce]);
|
|
275
308
|
// ── Keyboard: delete selected node ─────────────────────────────────────────
|
|
276
309
|
const onKeyDown = React.useCallback((e) => {
|
|
277
310
|
if (!editable || !selectedId)
|
|
@@ -286,7 +319,10 @@ export function FlowCanvas({ nodes, edges, editable, designMode, selectedId, sel
|
|
|
286
319
|
}
|
|
287
320
|
}, [deleteNode, editable, selectedId]);
|
|
288
321
|
// ── Render ─────────────────────────────────────────────────────────────────
|
|
289
|
-
return (_jsxs("div", { className: "relative h-full min-h-[320px] w-full overflow-hidden", children: [
|
|
322
|
+
return (_jsxs("div", { className: "relative h-full min-h-[320px] w-full overflow-hidden", children: [bannerErrors.length > 0 && (_jsxs("div", { className: "absolute left-2 top-2 z-30 max-w-[min(60%,420px)] space-y-1", children: [bannerErrors.slice(0, 3).map((p) => {
|
|
323
|
+
const clickable = !!onRevealProblem && p.target.kind !== 'flow';
|
|
324
|
+
return (_jsxs("button", { type: "button", role: "alert", disabled: !clickable, onPointerDown: (e) => e.stopPropagation(), onClick: clickable ? (e) => { e.stopPropagation(); onRevealProblem(p); } : undefined, title: clickable ? 'Reveal on canvas' : undefined, className: cn('flex w-full items-start gap-1.5 rounded-lg border border-destructive/40 bg-destructive/10 px-2.5 py-1.5 text-left text-[11px] leading-snug text-destructive shadow-sm backdrop-blur-sm transition-colors', clickable && 'cursor-pointer hover:border-destructive/60 hover:bg-destructive/20'), children: [_jsx(AlertTriangle, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }), _jsx("span", { children: p.message })] }, p.id));
|
|
325
|
+
}), bannerErrors.length > 3 && (_jsxs("div", { className: "px-2.5 text-[10px] text-destructive/80", children: ["+", bannerErrors.length - 3, " more\u2026"] }))] })), _jsxs("div", { className: "absolute right-2 top-2 z-30 flex items-center gap-1.5", children: [editable && (_jsxs("div", { className: "relative", children: [_jsxs("button", { type: "button", onClick: () => setPaletteOpen((v) => !v), className: "inline-flex items-center gap-1.5 rounded-lg border bg-background/90 px-2.5 py-1.5 text-xs font-medium shadow-sm backdrop-blur-sm transition-colors hover:border-primary/50 hover:bg-accent hover:text-foreground", children: [_jsx(Plus, { className: "h-3.5 w-3.5" }), tr('engine.inspector.add.node', locale)] }), paletteOpen && (_jsx(NodePalette, { locale: locale, items: paletteItems, onClose: () => setPaletteOpen(false), onPick: (type) => addNode(type, { from: selectedId ?? undefined }) }))] })), _jsxs("div", { className: "flex items-center rounded-lg border bg-background/90 shadow-sm backdrop-blur-sm", children: [_jsx("button", { type: "button", title: "Zoom out", "aria-label": "Zoom out", onClick: () => setZoom((z) => clampZoom(z - 0.15)), className: "inline-flex h-7 w-7 items-center justify-center text-muted-foreground hover:text-foreground", children: _jsx(ZoomOut, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "w-10 text-center text-[11px] tabular-nums text-muted-foreground", children: [Math.round(zoom * 100), "%"] }), _jsx("button", { type: "button", title: "Zoom in", "aria-label": "Zoom in", onClick: () => setZoom((z) => clampZoom(z + 0.15)), className: "inline-flex h-7 w-7 items-center justify-center text-muted-foreground hover:text-foreground", children: _jsx(ZoomIn, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", title: "Fit to view", "aria-label": "Fit to view", onClick: fitToView, className: "inline-flex h-7 w-7 items-center justify-center border-l text-muted-foreground hover:text-foreground", children: _jsx(Maximize2, { className: "h-3.5 w-3.5" }) })] })] }), _jsx("div", { ref: viewportRef, tabIndex: 0, role: "application", "aria-label": "Flow canvas", onKeyDown: onKeyDown, onPointerDown: onBgPointerDown, onPointerMove: (e) => {
|
|
290
326
|
onBgPointerMove(e);
|
|
291
327
|
onNodePointerMove(e);
|
|
292
328
|
}, onPointerUp: (e) => {
|
|
@@ -325,6 +361,7 @@ export function FlowCanvas({ nodes, edges, editable, designMode, selectedId, sel
|
|
|
325
361
|
const cond = conditionText(edge.condition);
|
|
326
362
|
const branchLabel = edge.isDefault ? 'else' : cond ? `if ${cond}` : edge.label;
|
|
327
363
|
const eid = edgeKey(edge, i);
|
|
364
|
+
const edgeBadge = edgeBadges.get(edgeProblemKey(edge.source, edge.target));
|
|
328
365
|
const traversed = traversedSet.has(eid);
|
|
329
366
|
const selected = selectedEdgeId === eid;
|
|
330
367
|
const d = back ? backEdgePath(from, to) : edgePath(from, to);
|
|
@@ -351,7 +388,9 @@ export function FlowCanvas({ nodes, edges, editable, designMode, selectedId, sel
|
|
|
351
388
|
? 'border-destructive/60 text-destructive'
|
|
352
389
|
: back
|
|
353
390
|
? 'border-amber-500/50 text-amber-600 dark:text-amber-400'
|
|
354
|
-
: 'border-border text-muted-foreground'), children: branchLabel }) }) })),
|
|
391
|
+
: 'border-border text-muted-foreground'), children: branchLabel }) }) })), edgeBadge && (_jsx("foreignObject", { x: labelPos.x - 9, y: labelPos.y - 30, width: 18, height: 18, className: "pointer-events-auto overflow-visible", children: _jsx("span", { title: edgeBadge.title, "data-problem": edgeBadge.level, className: cn('inline-flex h-[18px] w-[18px] items-center justify-center rounded-full border bg-background shadow-sm', edgeBadge.level === 'error'
|
|
392
|
+
? 'border-destructive/50 text-destructive'
|
|
393
|
+
: 'border-amber-500/50 text-amber-600 dark:text-amber-400'), children: edgeBadge.level === 'error' ? (_jsx(AlertCircle, { className: "h-3 w-3" })) : (_jsx(AlertTriangle, { className: "h-3 w-3" })) }) })), editable && !back && (_jsx("foreignObject", {
|
|
355
394
|
// Sit the insert handle at the edge midpoint, but slide it
|
|
356
395
|
// to the right of the branch-label pill when one is present
|
|
357
396
|
// so the two don't stack on the same spot.
|
|
@@ -363,7 +402,7 @@ export function FlowCanvas({ nodes, edges, editable, designMode, selectedId, sel
|
|
|
363
402
|
const runState = activeNodeId === node.id ? 'active' : visitedSet.has(node.id) ? 'visited' : undefined;
|
|
364
403
|
return (_jsx(NodeCard, { id: node.id, type: node.type, label: node.label || node.id, summary: nodeSummary(node), position: positionOf(node.id), selected: selectedId === node.id, editable: editable, runState: runState, dimmed: simRunning && !runState, onPointerDown: onNodePointerDown(node.id), onSelect: () => designMode && onSelect(node), onAppend: () => addNode('create_record', { from: node.id }), onAddReviseLoop: editable && node.type === 'approval' && !reviseLoopSources.has(node.id)
|
|
365
404
|
? () => addReviseLoop(node.id)
|
|
366
|
-
: undefined, invalid: invalidNodeSet.has(node.id) }, node.id));
|
|
405
|
+
: undefined, invalid: invalidNodeSet.has(node.id), badge: nodeBadges.get(node.id) }, node.id));
|
|
367
406
|
})] }) })] }));
|
|
368
407
|
}
|
|
369
408
|
/** One-line config summary shown on the node card (best-effort, type-aware). */
|
|
@@ -17,4 +17,4 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import * as React from 'react';
|
|
19
19
|
import type { MetadataPreviewProps } from '../preview-registry';
|
|
20
|
-
export declare function FlowPreview({ draft, editing, selection, onSelectionChange, onPatch, locale }: MetadataPreviewProps): React.JSX.Element;
|
|
20
|
+
export declare function FlowPreview({ draft, editing, selection, onSelectionChange, onPatch, locale, diagnostics }: MetadataPreviewProps): React.JSX.Element;
|
|
@@ -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[];
|