From 1dd48201b654705ac65a9bb9e688896c90aafe5e Mon Sep 17 00:00:00 2001 From: David Anthony Date: Tue, 1 Sep 2026 15:27:24 -0700 Subject: [PATCH 1/5] refactor(storybook): extract ErrorAndValidationWorkbench to its own file Exporting the workbench component directly from LayoutPatterns.stories.tsx caused Storybook's CSF auto-detection to treat it as an implicit story (any named export from a .stories.tsx file is picked up), creating a duplicate "Error And Validation Workbench" entry in the sidebar alongside the real "UX Error and Validation" story. Moving it to a plain .tsx module lets it stay importable by both LayoutPatterns and the new Code Editors pattern pages without being auto-registered as a story. --- .../patterns/ErrorAndValidationWorkbench.tsx | 739 ++++++++++++++++++ .../src/patterns/LayoutPatterns.stories.tsx | 732 +---------------- 2 files changed, 741 insertions(+), 730 deletions(-) create mode 100644 apps/storybook/src/patterns/ErrorAndValidationWorkbench.tsx diff --git a/apps/storybook/src/patterns/ErrorAndValidationWorkbench.tsx b/apps/storybook/src/patterns/ErrorAndValidationWorkbench.tsx new file mode 100644 index 000000000..69b34f1d0 --- /dev/null +++ b/apps/storybook/src/patterns/ErrorAndValidationWorkbench.tsx @@ -0,0 +1,739 @@ +import { BaseCanvas } from '@uipath/apollo-react/canvas/components/BaseCanvas'; +import { + CanvasBottomPanel, + type CanvasBottomPanelTab, +} from '@uipath/apollo-react/canvas/components/CanvasBottomPanel'; +import { + CANVAS_LEFT_SIDEBAR_DEFAULT_BOTTOM_ITEMS, + CANVAS_LEFT_SIDEBAR_DEFAULT_PRIMARY_ITEMS, + CanvasLeftSidebar, + type CanvasLeftSidebarItemId, +} from '@uipath/apollo-react/canvas/components/CanvasLeftSidebar'; +import { + CanvasModeToolbar, + CountBadge, + TOOLBAR_ICON_BUTTON_CLASS, +} from '@uipath/apollo-react/canvas/components/CanvasModeToolbar'; +import { CanvasZoomControls } from '@uipath/apollo-react/canvas/components/CanvasZoomControls'; +import { NodePropertyPanel } from '@uipath/apollo-react/canvas/components/NodePropertyPanel'; +import { ToolbarButton } from '@uipath/apollo-react/canvas/components/ToolbarButton'; +import { + NodePropertyTrigger, + type NodePropertyTriggerLayout, +} from '@uipath/apollo-react/canvas/controls/NodePropertyTrigger'; +import { ValidationStatusContext } from '@uipath/apollo-react/canvas/hooks'; +import { createNode, useCanvasStory } from '@uipath/apollo-react/canvas/storybook-utils'; +import { ValidationErrorSeverity } from '@uipath/apollo-react/canvas/types/validation'; +import { + type Edge, + type Node, + useNodesInitialized, + useReactFlow, +} from '@uipath/apollo-react/canvas/xyflow/react'; +import { + Alert, + AlertDescription, + AlertTitle, + Input, + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, + Label, + type PanelImperativeHandle, + RequiredIndicator, + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Separator, + Switch, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + Textarea, +} from '@uipath/apollo-wind'; +import { + AlertCircle, + AtSign, + Bug, + ChevronDown, + ChevronUp, + FlaskConical, + Mail, + Play, + Plus, + Redo2, + Sparkles, + StickyNote, + Undo2, +} from 'lucide-react'; +import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +const ALL_SIDEBAR_ITEMS = [ + ...CANVAS_LEFT_SIDEBAR_DEFAULT_PRIMARY_ITEMS, + ...CANVAS_LEFT_SIDEBAR_DEFAULT_BOTTOM_ITEMS, +]; + +const withErrorDot = (icon: ReactNode) => ( + + {icon} + + +); + +const VALIDATION_SIDEBAR_ITEMS = CANVAS_LEFT_SIDEBAR_DEFAULT_PRIMARY_ITEMS.map((item) => + item.id === 'variables' ? { ...item, icon: withErrorDot(item.icon) } : item +); + +function createValidationFlowGraph(): { nodes: Node[]; edges: Edge[] } { + const nodes = [ + createNode({ + id: 'trigger', + type: 'uipath.manual-trigger', + position: { x: 80, y: 200 }, + display: { label: 'Invoice received', subLabel: 'Document trigger' }, + }), + createNode({ + id: 'extract', + type: 'uipath.blank-node', + position: { x: 340, y: 200 }, + display: { label: 'Extract invoice details', subLabel: 'Document understanding' }, + }), + createNode({ + id: 'approver', + type: 'uipath.blank-node', + position: { x: 600, y: 200 }, + display: { label: 'Get approver', subLabel: 'Directory lookup' }, + }), + createNode({ + id: 'email', + type: 'uipath.blank-node', + position: { x: 860, y: 200 }, + selected: true, + display: { label: 'Send email', subLabel: 'Gmail' }, + }), + ]; + return { + nodes, + edges: [ + ['trigger', 'extract'], + ['extract', 'approver'], + ['approver', 'email'], + ].map(([source, target]) => ({ + id: `e-${source}-${target}`, + source, + target, + sourceHandle: 'output', + targetHandle: 'input', + })), + }; +} + +function FlowCanvas() { + const graph = useMemo(() => createValidationFlowGraph(), []); + const { canvasProps } = useCanvasStory({ + initialNodes: graph.nodes, + initialEdges: graph.edges, + }); + + return ; +} + +const panelBehaviorOptions = [ + { value: 'auto-hide', label: 'Auto hide' }, + { value: 'always-persist', label: 'Always persist' }, +]; + +const panelLayoutOptions = [ + { value: 'right', label: 'Right' }, + { value: 'bottom', label: 'Bottom' }, + { value: 'split', label: 'Split' }, +]; + +function PanelTrigger({ + panels = [ + { id: 'input', label: 'Input', enabled: false }, + { id: 'properties', label: 'Properties', enabled: false }, + { id: 'output', label: 'Output', enabled: false }, + ], + layout, + onLayoutChange, + onPanelToggle, + onPropertiesClick, +}: { + panels?: { id: string; label: string; enabled: boolean }[]; + layout?: NodePropertyTriggerLayout; + onLayoutChange?: (layout: NodePropertyTriggerLayout) => void; + onPanelToggle?: (id: string, enabled: boolean) => void; + onPropertiesClick?: () => void; +}) { + const [menuOpen, setMenuOpen] = useState(false); + + return ( + { + onPanelToggle?.(id, enabled); + setMenuOpen(false); + }} + onPropertiesClick={onPropertiesClick ?? (() => onPanelToggle?.('properties', true))} + /> + ); +} + +function CanvasNavigationControls() { + return ( + + + + + + + + + + + + + + + + + + + + + + ); +} + +function CanvasViewport({ + trigger, + bottomControlsOffset = 20, + rightControlsOffset = 16, +}: { + trigger?: ReactNode; + bottomControlsOffset?: number; + rightControlsOffset?: number; +}) { + const { getNodes, getNodesBounds, setEdges, setNodes, setViewport } = useReactFlow(); + const nodesInitialized = useNodesInitialized(); + const viewportContainerRef = useRef(null); + const occupiedRight = Math.max(0, rightControlsOffset - 16); + + const fitWorkflow = useCallback( + (duration: number) => { + const container = viewportContainerRef.current; + const nodes = getNodes(); + if (!container || nodes.length === 0) return; + const bounds = getNodesBounds(nodes); + const occupiedBottom = Math.max(0, bottomControlsOffset - 20); + const availableWidth = container.clientWidth - occupiedRight; + const availableHeight = container.clientHeight - occupiedBottom; + const padding = 48; + const zoom = Math.min( + 0.85, + Math.max(0.1, (availableWidth - padding * 2) / bounds.width), + Math.max(0.1, (availableHeight - padding * 2) / bounds.height) + ); + const availableCenterX = availableWidth / 2; + const availableCenterY = availableHeight / 2; + void setViewport( + { + zoom, + x: availableCenterX - (bounds.x + bounds.width / 2) * zoom, + y: availableCenterY - (bounds.y + bounds.height / 2) * zoom, + }, + { duration } + ); + }, + [bottomControlsOffset, getNodes, getNodesBounds, occupiedRight, setViewport] + ); + + useEffect(() => { + if (!nodesInitialized) return; + const timeout = window.setTimeout(() => fitWorkflow(200), 100); + return () => window.clearTimeout(timeout); + }, [fitWorkflow, nodesInitialized]); + + useEffect(() => { + const container = viewportContainerRef.current; + if (!container || !nodesInitialized) return; + const resizeObserver = new ResizeObserver(() => fitWorkflow(0)); + resizeObserver.observe(container); + return () => resizeObserver.disconnect(); + }, [fitWorkflow, nodesInitialized]); + + const tidy = useCallback(() => { + const graph = createValidationFlowGraph(); + setNodes(graph.nodes); + setEdges(graph.edges); + window.setTimeout(() => fitWorkflow(200), 100); + }, [fitWorkflow, setEdges, setNodes]); + + return ( +
+ +
+ {trigger ?? } +
+
+ +
+
+ +
+
+ ); +} + +function ValidationTabLabel({ label, count }: { label: string; count?: number }) { + if (!count) return <>{label}; + return ( + + {label} + + + {`${count} issue${count === 1 ? '' : 's'}`} + + + ); +} + +function DapValueField({ + id, + label, + value, + placeholder, + required, + error, + onChange, +}: { + id: string; + label: string; + value: string; + placeholder: string; + required?: boolean; + error?: ReactNode; + onChange: (value: string) => void; +}) { + return ( +
+ + + onChange(event.target.value)} + className="text-xs" + /> + + onChange(`${value}$vars.`)} + > + + + + +
+ ); +} + +function DapValidationPanel({ onClose }: { onClose: () => void }) { + const [subject, setSubject] = useState('Invoice approval required'); + const [recipient, setRecipient] = useState(''); + const [body, setBody] = useState( + 'Hi $vars.approverName,\n\nPlease review invoice $vars.invoiceNumber.' + ); + const [errorHandlingEnabled, setErrorHandlingEnabled] = useState(true); + const [retryCount, setRetryCount] = useState('8'); + + return ( + } + nodeLabel="Send email" + nodeCategory="Gmail · DAP layout" + onClose={onClose} + contentInset="0.875rem" + className="h-full" + > + + + + + + + + + + Variables + + + + +
+ + + Resolve 2 issues before running this node + + Fix the highlighted fields in Parameters and Error handling before you run or + publish this workflow. + + + +
+ + +
+ + + +
+

Message

+ + +
+ +