@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.
Files changed (95) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/dist/console/AppContent.js +9 -15
  3. package/dist/console/ConsoleShell.d.ts +16 -0
  4. package/dist/console/ConsoleShell.js +43 -2
  5. package/dist/console/ai/AiChatPage.js +36 -9
  6. package/dist/console/home/HomeLayout.js +5 -7
  7. package/dist/console/home/HomePage.js +1 -9
  8. package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
  9. package/dist/console/organizations/OrganizationsPage.js +22 -3
  10. package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
  11. package/dist/console/organizations/provisionEnvironment.js +64 -0
  12. package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
  13. package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
  14. package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
  15. package/dist/environment/EnvironmentListToolbar.js +59 -0
  16. package/dist/environment/entitlements.d.ts +90 -0
  17. package/dist/environment/entitlements.js +91 -0
  18. package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
  19. package/dist/environment/useEnvironmentEntitlements.js +108 -0
  20. package/dist/hooks/useActionModal.js +15 -1
  21. package/dist/hooks/useAiSurface.d.ts +59 -0
  22. package/dist/hooks/useAiSurface.js +78 -0
  23. package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
  24. package/dist/hooks/useConsoleActionRuntime.js +36 -8
  25. package/dist/index.d.ts +3 -1
  26. package/dist/index.js +5 -1
  27. package/dist/layout/AppHeader.js +28 -4
  28. package/dist/layout/ConsoleFloatingChatbot.js +16 -2
  29. package/dist/layout/ConsoleLayout.js +5 -6
  30. package/dist/preview/DraftPreviewBar.js +20 -7
  31. package/dist/providers/ExpressionProvider.js +9 -3
  32. package/dist/utils/index.d.ts +2 -2
  33. package/dist/utils/index.js +1 -1
  34. package/dist/utils/recordFormNavigation.d.ts +60 -0
  35. package/dist/utils/recordFormNavigation.js +35 -0
  36. package/dist/utils/resolvePageVarTokens.d.ts +31 -0
  37. package/dist/utils/resolvePageVarTokens.js +72 -0
  38. package/dist/views/CreateViewDialog.js +14 -1
  39. package/dist/views/ObjectView.js +26 -12
  40. package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
  41. package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
  42. package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
  43. package/dist/views/metadata-admin/PackagesPage.js +49 -4
  44. package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
  45. package/dist/views/metadata-admin/ResourceEditPage.js +36 -4
  46. package/dist/views/metadata-admin/ResourceListPage.js +21 -4
  47. package/dist/views/metadata-admin/createBody.d.ts +26 -0
  48. package/dist/views/metadata-admin/createBody.js +30 -0
  49. package/dist/views/metadata-admin/i18n.js +20 -0
  50. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +8 -0
  51. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +17 -3
  52. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +16 -2
  53. package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
  54. package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
  55. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
  56. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
  57. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
  58. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
  59. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +15 -3
  60. package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
  61. package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
  62. package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
  63. package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
  64. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +6 -1
  65. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
  66. package/dist/views/metadata-admin/inspectors/flow-node-config.js +21 -10
  67. package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
  68. package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
  69. package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
  70. package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
  71. package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
  72. package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
  73. package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
  74. package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
  75. package/dist/views/metadata-admin/package-scope.d.ts +15 -0
  76. package/dist/views/metadata-admin/package-scope.js +16 -0
  77. package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
  78. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +22 -3
  79. package/dist/views/metadata-admin/previews/FlowCanvas.js +45 -6
  80. package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
  81. package/dist/views/metadata-admin/previews/FlowPreview.js +42 -30
  82. package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
  83. package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
  84. package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
  85. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
  86. package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
  87. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
  88. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +5 -3
  89. package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
  90. package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
  91. package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
  92. package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
  93. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +9 -0
  94. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +4 -2
  95. 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, validationErrors, onSelect, onSelectEdge, onPatch, }) {
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: [validationErrors && validationErrors.length > 0 && (_jsxs("div", { className: "absolute left-2 top-2 z-30 max-w-[min(60%,420px)] space-y-1", children: [validationErrors.slice(0, 3).map((msg, i) => (_jsxs("div", { role: "alert", className: "flex items-start gap-1.5 rounded-lg border border-destructive/40 bg-destructive/10 px-2.5 py-1.5 text-[11px] leading-snug text-destructive shadow-sm backdrop-blur-sm", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }), _jsx("span", { children: msg })] }, i))), validationErrors.length > 3 && (_jsxs("div", { className: "px-2.5 text-[10px] text-destructive/80", children: ["+", validationErrors.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) => {
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 }) }) })), editable && !back && (_jsx("foreignObject", {
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 { validateFlowDraft } from './simulator/flow-sim-validate';
29
- export function FlowPreview({ draft, editing, selection, onSelectionChange, onPatch, locale }) {
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
- // Continuous structural validation surfaced INLINE on the canvas (ADR-0044):
45
- // an un-declared cycle (and other structural errors) paints the offending
46
- // edges/nodes red and shows a banner so the author sees it without opening
47
- // the Debug panel. Same `validateFlowDraft` the simulator preflight uses.
48
- const { invalidNodeIds, invalidEdges, validationErrors } = React.useMemo(() => {
49
- const v = validateFlowDraft(nodes, edges);
50
- const nodeSet = new Set();
51
- const edgeSet = new Set();
52
- for (const diag of v.errors) {
53
- if (diag.nodeId)
54
- nodeSet.add(diag.nodeId);
55
- // A cycle error carries its closing node path mark each hop's edge red.
56
- if (diag.cycle) {
57
- for (let i = 0; i < diag.cycle.length - 1; i++) {
58
- nodeSet.add(diag.cycle[i]);
59
- edgeSet.add(`${diag.cycle[i]}->${diag.cycle[i + 1]}`);
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
- return {
64
- invalidNodeIds: [...nodeSet],
65
- invalidEdges: edgeSet,
66
- validationErrors: v.errors.map((diag) => diag.message),
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, validationErrors: validationErrors, onSelect: (n) => n
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); rendering through plugin-report's `ReportRenderer` keeps the
8
- * studio preview pixel-equal with the runtime — including the matrix
9
- * cross-tab and the numbers consistent with every other surface on the
10
- * same dataset (`adapter.queryDataset`). Drill-down stays inert here: the
11
- * preview passes no `onDrill` sink.
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 without a dataset binding (e.g. stored pre-9.0 query-form JSON)
14
- * gets an actionable empty state pointing at the inspector's Dataset control
15
- * instead of the retired legacy renderer.
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); rendering through plugin-report's `ReportRenderer` keeps the
10
- * studio preview pixel-equal with the runtime — including the matrix
11
- * cross-tab and the numbers consistent with every other surface on the
12
- * same dataset (`adapter.queryDataset`). Drill-down stays inert here: the
13
- * preview passes no `onDrill` sink.
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 without a dataset binding (e.g. stored pre-9.0 query-form JSON)
16
- * gets an actionable empty state pointing at the inspector's Dataset control
17
- * instead of the retired legacy renderer.
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
- // ADR-0021 single-form: a report binds a semantic-layer dataset.
27
- if (typeof draft.dataset === 'string' && draft.dataset) {
28
- const rows = Array.isArray(draft.rows) ? draft.rows.filter(Boolean) : [];
29
- return (_jsx(PreviewShell, { hint: `report · dataset "${draft.dataset}"${rows.length ? ' · by ' + rows.join(', ') : ''}`, 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 }) }) }) }) }));
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
- // No dataset bound either a fresh draft or stored pre-9.0 query-form
32
- // JSON (objectName/columns), whose inline-query renderer was retired with
33
- // the 9.0 cutover. Point the author at the dataset binding.
34
- return (_jsx(PreviewShell, { children: _jsx(PreviewEmptyState, { icon: _jsx(Database, { className: "h-8 w-8" }), title: "Bind a dataset to preview this report", description: "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." }) }));
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 }))] })] })] }), editable && type !== 'end' && (_jsx("button", { type: "button", title: "Add connected node", "aria-label": "Add connected node", onPointerDown: (e) => e.stopPropagation(), onClick: (e) => {
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[];