From 5faefb89001af8958e74390871d2055f14537fb4 Mon Sep 17 00:00:00 2001 From: Afiq Date: Wed, 9 Jul 2025 13:44:53 +0800 Subject: [PATCH] Add Design Elements and Custom Shapes to Process Builder - Introduced a new section for Design Elements in ProcessBuilderComponents.vue, allowing users to drag and drop various shapes into the process flow. - Added multiple custom shapes including Horizontal Swimlane, Vertical Swimlane, Rectangle, Text Annotation, Process Group, Hexagon, and Trapezoid to enhance visual organization within the process builder. - Updated ProcessFlowCanvas.vue to ensure that shape nodes are treated correctly, preventing them from being connectable and ensuring proper rendering. - Enhanced ProcessFlowNodes.js to include new shape components with dynamic styling and properties. - Improved the user interface in index.vue to support shape size adjustments and provide clear information about shape elements. - Ensured that all new shapes maintain consistent styling and behavior with existing process nodes, enhancing overall user experience and functionality. --- .../process-flow/ProcessBuilderComponents.vue | 161 +++++++ components/process-flow/ProcessFlowCanvas.vue | 57 ++- components/process-flow/ProcessFlowNodes.js | 424 +++++++++++++++++- pages/process-builder/index.vue | 403 +++++++++++++++-- 4 files changed, 970 insertions(+), 75 deletions(-) diff --git a/components/process-flow/ProcessBuilderComponents.vue b/components/process-flow/ProcessBuilderComponents.vue index da27bb7..29ef021 100644 --- a/components/process-flow/ProcessBuilderComponents.vue +++ b/components/process-flow/ProcessBuilderComponents.vue @@ -35,6 +35,25 @@ + +
+

Design Elements

+
+
+ + {{ shape.name }} +
+
+
+ @@ -188,6 +207,148 @@ const availableComponents = [ errorVariable: 'scriptError' } } + }, + + // Design Elements / Shapes + { + type: 'swimlane-horizontal', + name: 'Horizontal Swimlane', + category: 'Shape', + icon: 'material-symbols:view-stream', + description: 'Horizontal swimlane for organizing process sections', + defaultProps: { + label: '', + data: { + description: '', + width: 600, + height: 150, + backgroundColor: '#f8fafc', + borderColor: '#e2e8f0', + textColor: '#475569', + isShape: true, + shapeType: 'swimlane-horizontal' + } + } + }, + { + type: 'swimlane-vertical', + name: 'Vertical Swimlane', + category: 'Shape', + icon: 'material-symbols:view-column', + description: 'Vertical swimlane for organizing process sections', + defaultProps: { + label: '', + data: { + description: '', + width: 200, + height: 400, + backgroundColor: '#f8fafc', + borderColor: '#e2e8f0', + textColor: '#475569', + isShape: true, + shapeType: 'swimlane-vertical' + } + } + }, + { + type: 'rectangle-shape', + name: 'Rectangle', + category: 'Shape', + icon: 'material-symbols:rectangle', + description: 'Rectangle shape for grouping and annotation', + defaultProps: { + label: '', + data: { + description: '', + width: 300, + height: 200, + backgroundColor: '#fefefe', + borderColor: '#d1d5db', + textColor: '#374151', + isShape: true, + shapeType: 'rectangle' + } + } + }, + { + type: 'text-annotation', + name: 'Text Annotation', + category: 'Shape', + icon: 'material-symbols:text-fields', + description: 'Text annotation for adding notes and comments', + defaultProps: { + label: '', + data: { + description: '', + width: 200, + height: 80, + backgroundColor: '#fffbeb', + borderColor: '#fbbf24', + textColor: '#92400e', + isShape: true, + shapeType: 'text-annotation' + } + } + }, + { + type: 'process-group', + name: 'Process Group', + category: 'Shape', + icon: 'material-symbols:group-work', + description: 'Group container for organizing related processes', + defaultProps: { + label: '', + data: { + description: '', + width: 400, + height: 300, + backgroundColor: '#f0f9ff', + borderColor: '#0284c7', + textColor: '#0369a1', + isShape: true, + shapeType: 'process-group' + } + } + }, + { + type: 'hexagon-shape', + name: 'Hexagon', + category: 'Shape', + icon: 'material-symbols:hexagon', + description: 'Hexagon shape for special processes or decision points', + defaultProps: { + label: '', + data: { + description: '', + width: 200, + height: 150, + backgroundColor: '#f8fafc', + borderColor: '#e2e8f0', + textColor: '#475569', + isShape: true, + shapeType: 'hexagon' + } + } + }, + { + type: 'trapezoid-shape', + name: 'Trapezoid', + category: 'Shape', + icon: 'material-symbols:change-history', + description: 'Trapezoid shape for data processing or manual tasks', + defaultProps: { + label: '', + data: { + description: '', + width: 220, + height: 120, + backgroundColor: '#f8fafc', + borderColor: '#e2e8f0', + textColor: '#475569', + isShape: true, + shapeType: 'trapezoid' + } + } } ]; diff --git a/components/process-flow/ProcessFlowCanvas.vue b/components/process-flow/ProcessFlowCanvas.vue index 2f87548..90e56d2 100644 --- a/components/process-flow/ProcessFlowCanvas.vue +++ b/components/process-flow/ProcessFlowCanvas.vue @@ -356,22 +356,17 @@ watch(() => props.initialNodes, async (newNodes, oldNodes) => { newNodes.forEach(newNode => { const existingNode = nodes.value.find(n => n.id === newNode.id); if (existingNode) { - // Check if the node has actually changed before updating - const hasChanges = ( - existingNode.label !== newNode.label || - JSON.stringify(existingNode.data) !== JSON.stringify(newNode.data) || - Math.abs(existingNode.position.x - newNode.position.x) > 1 || - Math.abs(existingNode.position.y - newNode.position.y) > 1 - ); + // Always update the existing node to ensure latest data is reflected + // This is critical for property changes to be visible in the canvas + Object.assign(existingNode, { + label: newNode.label, + data: { ...newNode.data }, + position: { ...newNode.position } + }); - if (hasChanges) { - Object.assign(existingNode, { - label: newNode.label, - data: { ...newNode.data }, - position: { ...newNode.position } - }); - updateNodeInternals([newNode.id]); - } + // Always call updateNodeInternals to force re-render + // This ensures property changes are reflected immediately + updateNodeInternals([newNode.id]); } }); @@ -630,7 +625,7 @@ const onDrop = (event) => { y: event.clientY - bounds.top }); - // Create new node + // Create new node (both process nodes and shapes are treated as nodes in Vue Flow) const newNode = { id: `${componentData.type}-${Date.now()}`, type: componentData.type, @@ -642,6 +637,12 @@ const onDrop = (event) => { } }; + // For shape nodes, ensure they don't have connection handles + if (componentData.data?.isShape) { + // Shapes should not be connectable + newNode.connectable = false; + } + // Add to Vue Flow for immediate visual feedback addNodes([newNode]); @@ -678,22 +679,30 @@ defineExpose({ // Update an existing node function updateNode(nodeId, newData) { const nodeToUpdate = nodes.value.find(node => node.id === nodeId); - if (!nodeToUpdate) return; + if (!nodeToUpdate) { + console.warn(`Canvas updateNode: Node ${nodeId} not found`); + return; + } - // Update the node properties - if (newData.label) { + // Update the node properties - handle all provided data + if (newData.label !== undefined) { nodeToUpdate.label = newData.label; } - // Update the node data - if (newData.data) { + // Update the node data - merge with existing data + if (newData.data !== undefined) { nodeToUpdate.data = { ...nodeToUpdate.data, ...newData.data }; } - // Update node internals to trigger re-render + // Update position if provided + if (newData.position !== undefined) { + nodeToUpdate.position = { ...newData.position }; + } + + // Force re-render by updating node internals updateNodeInternals([nodeId]); return nodeToUpdate; @@ -741,15 +750,17 @@ function syncCanvas(newNodes, newEdges) { addNodes([...nodesToAdd]); } - // Update existing nodes + // Update existing nodes - force update all properties newNodes.forEach(newNode => { const existingNode = nodes.value.find(n => n.id === newNode.id); if (existingNode) { + // Always update to ensure property changes are reflected Object.assign(existingNode, { label: newNode.label, data: { ...newNode.data }, position: { ...newNode.position } }); + // Always force re-render for property changes updateNodeInternals([newNode.id]); } }); diff --git a/components/process-flow/ProcessFlowNodes.js b/components/process-flow/ProcessFlowNodes.js index 6f5ff69..cd82aea 100644 --- a/components/process-flow/ProcessFlowNodes.js +++ b/components/process-flow/ProcessFlowNodes.js @@ -151,14 +151,25 @@ const CustomNode = markRaw({ const borderColor = this.data?.borderColor || defaultBorder; const textColor = this.data?.textColor || defaultText; - return { + // Get the current shape to determine if we should apply direct styles + const shape = this.data?.shape || 'rectangle'; + const isShapedNode = shape !== 'rectangle' && shape !== 'rounded-rectangle' && shape !== 'circle'; + + const style = { '--node-bg-color': backgroundColor, '--node-border-color': borderColor, '--node-text-color': textColor, - backgroundColor: backgroundColor, - borderColor: borderColor, color: textColor }; + + // Only apply direct background/border styles for non-shaped nodes + // Shaped nodes (hexagon, trapezoid) use CSS ::before pseudo-elements with clip-path + if (!isShapedNode) { + style.backgroundColor = backgroundColor; + style.borderColor = borderColor; + } + + return style; } }, methods: { @@ -639,8 +650,305 @@ export const NotificationNode = markRaw({ } }); +// Shape Components (Design Elements) +const HorizontalSwimlaneShape = { + props: ['id', 'data', 'selected', 'label'], + computed: { + shapeStyle() { + return { + width: `${this.data?.width || 600}px`, + height: `${this.data?.height || 150}px`, + '--node-bg-color': this.data?.backgroundColor || '#f8fafc', + '--node-border-color': this.data?.borderColor || '#e2e8f0', + '--node-text-color': this.data?.textColor || '#475569', + backgroundColor: this.data?.backgroundColor || '#f8fafc', + border: `2px solid ${this.data?.borderColor || '#e2e8f0'}`, + borderRadius: '8px', + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: this.data?.textColor || '#475569', + fontSize: '14px', + fontWeight: '500', + cursor: 'move', + zIndex: -10 // Behind process nodes + }; + }, + displayLabel() { + return this.label || this.data?.label || ''; + }, + displayDescription() { + return this.data?.description || ''; + } + }, + template: ` +
+
+
{{ displayLabel }}
+
{{ displayDescription }}
+
+
+ ` +}; + +const VerticalSwimlaneShape = { + props: ['id', 'data', 'selected', 'label'], + computed: { + shapeStyle() { + return { + width: `${this.data?.width || 200}px`, + height: `${this.data?.height || 400}px`, + '--node-bg-color': this.data?.backgroundColor || '#f8fafc', + '--node-border-color': this.data?.borderColor || '#e2e8f0', + '--node-text-color': this.data?.textColor || '#475569', + backgroundColor: this.data?.backgroundColor || '#f8fafc', + border: `2px solid ${this.data?.borderColor || '#e2e8f0'}`, + borderRadius: '8px', + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: this.data?.textColor || '#475569', + fontSize: '14px', + fontWeight: '500', + cursor: 'move', + zIndex: -10, // Behind process nodes + writingMode: 'vertical-rl', + textOrientation: 'mixed' + }; + }, + displayLabel() { + return this.label || this.data?.label || ''; + }, + displayDescription() { + return this.data?.description || ''; + } + }, + template: ` +
+
+
{{ displayLabel }}
+
{{ displayDescription }}
+
+
+ ` +}; + +const RectangleShape = { + props: ['id', 'data', 'selected', 'label'], + computed: { + shapeStyle() { + return { + width: `${this.data?.width || 300}px`, + height: `${this.data?.height || 200}px`, + '--node-bg-color': this.data?.backgroundColor || '#fefefe', + '--node-border-color': this.data?.borderColor || '#d1d5db', + '--node-text-color': this.data?.textColor || '#374151', + backgroundColor: this.data?.backgroundColor || '#fefefe', + border: `2px solid ${this.data?.borderColor || '#d1d5db'}`, + borderRadius: '4px', + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: this.data?.textColor || '#374151', + fontSize: '14px', + fontWeight: '500', + cursor: 'move', + zIndex: -10 // Behind process nodes + }; + }, + displayLabel() { + return this.label || this.data?.label || ''; + }, + displayDescription() { + return this.data?.description || ''; + } + }, + template: ` +
+
+
{{ displayLabel }}
+
{{ displayDescription }}
+
+
+ ` +}; + +const TextAnnotationShape = { + props: ['id', 'data', 'selected', 'label'], + computed: { + shapeStyle() { + return { + width: `${this.data?.width || 200}px`, + height: `${this.data?.height || 80}px`, + '--node-bg-color': this.data?.backgroundColor || '#fffbeb', + '--node-border-color': this.data?.borderColor || '#fbbf24', + '--node-text-color': this.data?.textColor || '#92400e', + backgroundColor: this.data?.backgroundColor || '#fffbeb', + border: `2px dashed ${this.data?.borderColor || '#fbbf24'}`, + borderRadius: '4px', + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: this.data?.textColor || '#92400e', + fontSize: '12px', + fontWeight: '400', + cursor: 'move', + zIndex: -10, // Behind process nodes + fontStyle: 'italic' + }; + }, + displayLabel() { + return this.label || this.data?.label || ''; + }, + displayDescription() { + return this.data?.description || ''; + } + }, + template: ` +
+
+
{{ displayLabel }}
+
{{ displayDescription }}
+
+
+ ` +}; + +const ProcessGroupShape = { + props: ['id', 'data', 'selected', 'label'], + computed: { + shapeStyle() { + return { + width: `${this.data?.width || 400}px`, + height: `${this.data?.height || 300}px`, + '--node-bg-color': this.data?.backgroundColor || '#f0f9ff', + '--node-border-color': this.data?.borderColor || '#0284c7', + '--node-text-color': this.data?.textColor || '#0369a1', + backgroundColor: this.data?.backgroundColor || '#f0f9ff', + border: `3px solid ${this.data?.borderColor || '#0284c7'}`, + borderRadius: '12px', + position: 'relative', + display: 'flex', + alignItems: 'flex-start', + justifyContent: 'center', + color: this.data?.textColor || '#0369a1', + fontSize: '16px', + fontWeight: '600', + cursor: 'move', + zIndex: -10, // Behind process nodes + padding: '16px' + }; + }, + displayLabel() { + return this.label || this.data?.label || ''; + }, + displayDescription() { + return this.data?.description || ''; + } + }, + template: ` +
+
+
{{ displayLabel }}
+
{{ displayDescription }}
+
+
+ ` +}; + +// Hexagon Shape Component +const HexagonShape = { + props: ['id', 'data', 'selected', 'label'], + computed: { + shapeStyle() { + return { + width: `${this.data?.width || 200}px`, + height: `${this.data?.height || 150}px`, + '--node-bg-color': this.data?.backgroundColor || '#f8fafc', + '--node-border-color': this.data?.borderColor || '#e2e8f0', + '--node-text-color': this.data?.textColor || '#475569', + position: 'relative', + cursor: 'move', + zIndex: -10 // Behind process nodes + }; + }, + displayLabel() { + return this.label || this.data?.label || ''; + }, + displayDescription() { + return this.data?.description || ''; + } + }, + template: ` +
+
+
{{ displayLabel }}
+
{{ displayDescription }}
+
+
+ ` +}; + +// Trapezoid Shape Component +const TrapezoidShape = { + props: ['id', 'data', 'selected', 'label'], + computed: { + shapeStyle() { + return { + width: `${this.data?.width || 220}px`, + height: `${this.data?.height || 120}px`, + '--node-bg-color': this.data?.backgroundColor || '#f8fafc', + '--node-border-color': this.data?.borderColor || '#e2e8f0', + '--node-text-color': this.data?.textColor || '#475569', + position: 'relative', + cursor: 'move', + zIndex: -10 // Behind process nodes + }; + }, + displayLabel() { + return this.label || this.data?.label || ''; + }, + displayDescription() { + return this.data?.description || ''; + } + }, + template: ` +
+
+
{{ displayLabel }}
+
{{ displayDescription }}
+
+
+ ` +}; + // Export the node types object to use with Vue Flow -export const nodeTypes = markRaw({ +export const nodeTypes = { start: StartNode, end: EndNode, gateway: GatewayNode, @@ -648,8 +956,16 @@ export const nodeTypes = markRaw({ script: ScriptNode, 'business-rule': BusinessRuleNode, api: ApiCallNode, - notification: NotificationNode -}); + notification: NotificationNode, + // Shape nodes + 'swimlane-horizontal': HorizontalSwimlaneShape, + 'swimlane-vertical': VerticalSwimlaneShape, + 'rectangle-shape': RectangleShape, + 'text-annotation': TextAnnotationShape, + 'process-group': ProcessGroupShape, + 'hexagon-shape': HexagonShape, + 'trapezoid-shape': TrapezoidShape +}; // Default CSS for the nodes to be imported where needed export const nodeStyles = ` @@ -1269,4 +1585,100 @@ export const nodeStyles = ` border: 1px solid #ddd; border-left: 4px solid #6b7280; /* Gray border to match icon color */ } + +/* Shape node styles */ +.shape-node { + position: relative; + border: 2px solid #e5e7eb; + border-radius: 8px; + background: white; + cursor: move; + transition: all 0.2s ease; + z-index: -10 !important; /* Lower z-index so shapes stay in background */ +} + +.shape-node.selected { + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); + z-index: -8 !important; /* Still behind process nodes when selected */ +} + +.shape-node:hover { + border-color: #94a3b8; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.shape-content { + position: relative; + z-index: 1; + text-align: center; + overflow: hidden; +} + +.shape-label { + font-weight: 500; + margin-bottom: 4px; + user-select: none; + word-wrap: break-word; +} + +.shape-description { + font-size: 12px; + opacity: 0.7; + user-select: none; + word-wrap: break-word; +} + +/* Specific shape styles */ +.swimlane-horizontal { + border-style: solid; + border-width: 2px 0; + border-radius: 0; +} + +.swimlane-vertical { + border-style: solid; + border-width: 0 2px; + border-radius: 0; +} + +.text-annotation { + border-style: dashed; + background: rgba(255, 251, 235, 0.8); + font-style: italic; +} + +.process-group { + border-width: 3px; + border-style: solid; + border-radius: 12px; +} + +.rectangle-shape { + border-radius: 4px; +} + +/* Ensure shapes don't interfere with node connections */ +.shape-node * { + pointer-events: none; +} + +.shape-node { + pointer-events: all; +} + +/* Resize handles for shapes when selected */ +.shape-node.selected::after { + content: ''; + position: absolute; + bottom: -4px; + right: -4px; + width: 8px; + height: 8px; + background: #3b82f6; + border: 2px solid white; + border-radius: 50%; + cursor: nw-resize; + pointer-events: all; +} `; \ No newline at end of file diff --git a/pages/process-builder/index.vue b/pages/process-builder/index.vue index 57a88c9..979b558 100644 --- a/pages/process-builder/index.vue +++ b/pages/process-builder/index.vue @@ -286,7 +286,9 @@ const canvasNodes = computed(() => { // Return a deep copy to ensure proper reactivity for nested objects like data return processStore.currentProcess.nodes.map(node => ({ ...node, - data: { ...node.data } + data: { ...node.data }, + // Ensure labels are synchronized both ways + label: node.label || node.data?.label || node.type })); }); @@ -376,6 +378,26 @@ const shapeOptions = [ { value: 'trapezoid', label: 'Trapezoid', icon: 'architecture' } ]; +// Helper function to get icon for node type +const getNodeIcon = (nodeType) => { + const iconMap = { + 'form': 'description', + 'api': 'api', + 'gateway': 'call_split', + 'script': 'code', + 'business-rule': 'rule', + 'notification': 'notifications', + 'start': 'play_circle_filled', + 'end': 'stop_circle', + 'swimlane-horizontal': 'view-stream', + 'swimlane-vertical': 'view-column', + 'rectangle-shape': 'rectangle', + 'text-annotation': 'text-fields', + 'process-group': 'group-work' + }; + return iconMap[nodeType] || 'account_tree'; +}; + // Helper function to validate and normalize hex color const normalizeHexColor = (value, fallback = '#ffffff') => { if (!value || typeof value !== 'string') return fallback; @@ -638,21 +660,68 @@ const onPaneClick = () => { onPaneClickMobile(); }; -// Update node in store -const updateNodeInStore = () => { +// Enhanced updateNodeInStore with automatic refresh and canvas sync +const updateNodeInStore = async () => { if (selectedNodeData.value) { - // console.log('Updating node:', selectedNodeData.value.id, selectedNodeData.value.label); - - // Simplify the update to avoid recursive reactivity const nodeId = selectedNodeData.value.id; const newLabel = selectedNodeData.value.label; const newData = { ...selectedNodeData.value.data }; - // Update in store first - this will trigger canvasNodes computed to update + // Update in store first processStore.updateNode(nodeId, { label: newLabel, data: newData }); + + // Wait for store update to complete + await nextTick(); + + // Force canvas update directly for immediate feedback + if (processFlowCanvas.value && processFlowCanvas.value.updateNode) { + processFlowCanvas.value.updateNode(nodeId, { + label: newLabel, + data: newData + }); + } + + // Refresh selected node from store to maintain consistency + refreshSelectedNodeFromStore(); + } +}; + +// Enhanced refresh function with change detection to prevent loops +const refreshSelectedNodeFromStore = () => { + if (selectedNodeData.value && processStore.currentProcess) { + const storeNode = processStore.currentProcess.nodes.find(node => node.id === selectedNodeData.value.id); + if (storeNode) { + // Use a more sophisticated comparison to detect actual changes + const hasChanges = ( + selectedNodeData.value.label !== storeNode.label || + JSON.stringify(selectedNodeData.value.data) !== JSON.stringify(storeNode.data) || + selectedNodeData.value.position?.x !== storeNode.position?.x || + selectedNodeData.value.position?.y !== storeNode.position?.y + ); + + if (hasChanges) { + selectedNodeData.value = { ...storeNode, data: { ...storeNode.data } }; + } + } + } +}; + +// Special handler for shape size updates to force reactivity +const updateShapeSize = async () => { + if (selectedNodeData.value && selectedNodeData.value.data?.isShape) { + // Force update the node in store with new size + updateNodeInStore(); + + // Also force update the canvas to ensure immediate visual feedback + await nextTick(); + if (processFlowCanvas.value && processFlowCanvas.value.updateNode) { + processFlowCanvas.value.updateNode(selectedNodeData.value.id, { + data: { ...selectedNodeData.value.data } + }); + } } }; @@ -692,7 +761,7 @@ const resetNodeColors = () => { }; // Handle condition update -const handleConditionUpdate = (conditions) => { +const handleConditionUpdate = async (conditions) => { if (selectedNodeData.value && selectedNodeData.value.type === 'gateway') { // Update conditions in the node data selectedNodeData.value.data = { @@ -722,8 +791,8 @@ const handleConditionUpdate = (conditions) => { processStore.currentProcess.edges = updatedEdges; } - // Update the node in store - updateNodeInStore(); + // Update the node in store and refresh + await updateNodeInStore(); } }; @@ -745,7 +814,6 @@ const onNodesChange = (changes, currentNodes) => { }); if (Object.keys(positionChanges).length > 0) { - processStore.updateNodePositions(positionChanges); } @@ -755,7 +823,6 @@ const onNodesChange = (changes, currentNodes) => { .map(change => change.id); if (removedNodes.length > 0) { - removedNodes.forEach(nodeId => { processStore.deleteNode(nodeId); }); @@ -766,13 +833,9 @@ const onNodesChange = (changes, currentNodes) => { } } - // Update node selection if needed - if (selectedNodeData.value) { - const updatedNode = currentNodes.find(node => node.id === selectedNodeData.value.id); - if (updatedNode) { - selectedNodeData.value = { ...updatedNode }; - } - } + // REMOVED: Don't overwrite selectedNodeData from canvas changes to preserve local edits + // This was causing property panel changes to be lost when the canvas updated + // The canvasNodes computed and its watcher will handle synchronization properly }; // Handle updates from the canvas when edges change @@ -1334,7 +1397,7 @@ const onConditionsUpdated = (conditions) => { }; // Handle API node update -const handleApiNodeUpdate = (updatedData) => { +const handleApiNodeUpdate = async (updatedData) => { if (selectedNodeData.value && selectedNodeData.value.type === 'api') { // Make sure to update the label both in data and at the root level const newLabel = updatedData.label || 'API Call'; @@ -1348,13 +1411,13 @@ const handleApiNodeUpdate = (updatedData) => { // Also update the root label selectedNodeData.value.label = newLabel; - // Update the node in store - updateNodeInStore(); + // Update the node in store and refresh + await updateNodeInStore(); } }; // Add this function to handle form node updates -const handleFormNodeUpdate = (updatedData) => { +const handleFormNodeUpdate = async (updatedData) => { if (selectedNodeData.value && selectedNodeData.value.type === 'form') { // Make sure to update the label both in data and at the root level const newLabel = updatedData.label || 'Form Task'; @@ -1368,40 +1431,40 @@ const handleFormNodeUpdate = (updatedData) => { // Also update the root label selectedNodeData.value.label = newLabel; - // Update the node in store - updateNodeInStore(); + // Update the node in store and refresh + await updateNodeInStore(); } }; // Update handler for business rule node -const handleBusinessRuleUpdate = (data) => { +const handleBusinessRuleUpdate = async (data) => { if (selectedNodeData.value) { selectedNodeData.value.data = { ...selectedNodeData.value.data, ...data }; - updateNodeInStore(); + await updateNodeInStore(); } }; // Add a method to handle default path updates -const handleDefaultPathUpdate = (path) => { +const handleDefaultPathUpdate = async (path) => { if (selectedNodeData.value && selectedNodeData.value.type === 'gateway') { selectedNodeData.value.data.defaultPath = path; - updateNodeInStore(); + await updateNodeInStore(); } }; // Handle notification node update -const handleNotificationNodeUpdate = (updatedData) => { +const handleNotificationNodeUpdate = async (updatedData) => { if (selectedNodeData.value) { selectedNodeData.value.data = { ...updatedData }; - updateNodeInStore(); + await updateNodeInStore(); } }; // Handle script node update -const handleScriptNodeUpdate = (updatedData) => { +const handleScriptNodeUpdate = async (updatedData) => { if (selectedNodeData.value && selectedNodeData.value.type === 'script') { // Make sure to update the label both in data and at the root level const newLabel = updatedData.label || 'Script Task'; @@ -1430,8 +1493,8 @@ const handleScriptNodeUpdate = (updatedData) => { }); } - // Update the node in store - updateNodeInStore(); + // Update the node in store and refresh + await updateNodeInStore(); } }; @@ -1475,6 +1538,27 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => { } }); +// Watch for canvas nodes changes to ensure selected node stays in sync +watch(() => canvasNodes.value, (newNodes) => { + // If we have a selected node, make sure it's kept in sync with the canvas + if (selectedNodeData.value && newNodes.length > 0) { + const updatedNode = newNodes.find(node => node.id === selectedNodeData.value.id); + if (updatedNode) { + // Use the same change detection logic as refreshSelectedNodeFromStore + const hasChanges = ( + selectedNodeData.value.label !== updatedNode.label || + JSON.stringify(selectedNodeData.value.data) !== JSON.stringify(updatedNode.data) || + selectedNodeData.value.position?.x !== updatedNode.position?.x || + selectedNodeData.value.position?.y !== updatedNode.position?.y + ); + + if (hasChanges) { + selectedNodeData.value = { ...updatedNode, data: { ...updatedNode.data } }; + } + } + } +}, { deep: true }); + // Watch for process changes to restore Vue Flow state watch(() => processStore.currentProcess, async (newProcess, oldProcess) => { if (!newProcess) return; @@ -1749,21 +1833,18 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
- +
-
-

{{ selectedNodeData.type.replace('-', ' ') }} Node

+

+ {{ selectedNodeData.data?.isShape ? 'Shape' : 'Node' }}: {{ selectedNodeData.type.replace('-', ' ') }} +

{{ selectedNodeData.id }}

@@ -1801,8 +1882,8 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => { />
- -
+ +
+
+
+ + +
+
+
@@ -1905,8 +2014,8 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => { - -
+ +

Configuration @@ -1966,6 +2075,26 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {

+ + +
+

+ + Shape Information +

+
+

+ Design Element: This is a decorative shape used for visual organization and layout. + It doesn't participate in the process flow and cannot be connected to other nodes. +

+
+
+ + Use the appearance section above to customize colors and styling +
+
+
+
@@ -2851,6 +2980,188 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => { } } +/* Shape node styles */ +:deep(.shape-node) { + border-radius: 4px; + position: relative; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + user-select: none; + z-index: -10 !important; /* Always behind process nodes */ +} + +:deep(.shape-node.selected) { + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.6); + z-index: -8 !important; /* Higher z-index when selected, but still behind process nodes */ +} + +/* Ensure process nodes always stay above shapes, even during drag operations */ +:deep(.vue-flow__node:not([data-id*="swimlane"]):not([data-id*="rectangle-shape"]):not([data-id*="text-annotation"]):not([data-id*="process-group"]):not([data-id*="hexagon-shape"]):not([data-id*="trapezoid-shape"])) { + z-index: 10 !important; /* Process nodes always above shapes */ +} + +:deep(.vue-flow__node:not([data-id*="swimlane"]):not([data-id*="rectangle-shape"]):not([data-id*="text-annotation"]):not([data-id*="process-group"]):not([data-id*="hexagon-shape"]):not([data-id*="trapezoid-shape"]).selected) { + z-index: 15 !important; /* Selected process nodes even higher */ +} + +:deep(.vue-flow__node:not([data-id*="swimlane"]):not([data-id*="rectangle-shape"]):not([data-id*="text-annotation"]):not([data-id*="process-group"]):not([data-id*="hexagon-shape"]):not([data-id*="trapezoid-shape"]).dragging) { + z-index: 20 !important; /* Dragging process nodes on top */ +} + +/* Vue Flow state-specific z-index rules */ +:deep(.vue-flow__node[data-id*="start"]), +:deep(.vue-flow__node[data-id*="end"]), +:deep(.vue-flow__node[data-id*="form"]), +:deep(.vue-flow__node[data-id*="api"]), +:deep(.vue-flow__node[data-id*="gateway"]), +:deep(.vue-flow__node[data-id*="script"]), +:deep(.vue-flow__node[data-id*="business-rule"]), +:deep(.vue-flow__node[data-id*="notification"]) { + z-index: 10 !important; /* All process node types above shapes */ +} + +:deep(.vue-flow__node[data-id*="start"].selected), +:deep(.vue-flow__node[data-id*="end"].selected), +:deep(.vue-flow__node[data-id*="form"].selected), +:deep(.vue-flow__node[data-id*="api"].selected), +:deep(.vue-flow__node[data-id*="gateway"].selected), +:deep(.vue-flow__node[data-id*="script"].selected), +:deep(.vue-flow__node[data-id*="business-rule"].selected), +:deep(.vue-flow__node[data-id*="notification"].selected) { + z-index: 15 !important; /* Selected process nodes even higher */ +} + +/* Handle Vue Flow's internal dragging state classes */ +:deep(.vue-flow__node.dragging:not(.shape-node)) { + z-index: 25 !important; /* Dragging process nodes should be on top of everything */ +} + +/* Ensure edge connection during drag always works for process nodes */ +:deep(.vue-flow.connecting .vue-flow__node:not(.shape-node)) { + z-index: 30 !important; /* During connection mode, process nodes are highest */ +} + +/* Shape nodes should never interfere with edge connections */ +:deep(.shape-node .vue-flow__handle) { + display: none !important; /* Shapes should not have connection handles */ +} + +/* Ensure custom nodes (process nodes) maintain proper layering */ +:deep(.custom-node:not(.shape-node)) { + position: relative; + z-index: 10 !important; +} + +:deep(.custom-node:not(.shape-node).selected) { + z-index: 15 !important; +} + +/* Ensure edges/connection lines stay above shapes */ +:deep(.vue-flow__edges) { + z-index: 5 !important; /* Edges above shapes but below nodes */ +} + +:deep(.vue-flow__edge) { + z-index: 5 !important; /* Individual edges above shapes */ +} + +:deep(.vue-flow__edge.selected) { + z-index: 8 !important; /* Selected edges higher */ +} + +:deep(.vue-flow__connection) { + z-index: 12 !important; /* Active connection line above everything except dragging nodes */ +} + +:deep(.vue-flow__connection-path) { + z-index: 12 !important; /* Connection path above shapes and nodes */ +} + +/* Ensure edge paths and labels stay above shapes */ +:deep(.vue-flow__edge-path) { + z-index: 5 !important; /* Edge paths above shapes */ +} + +:deep(.vue-flow__edge-text) { + z-index: 6 !important; /* Edge labels above edge paths and shapes */ +} + +:deep(.vue-flow__edge-textbg) { + z-index: 5 !important; /* Edge text background above shapes */ +} + +/* Ensure SVG elements in edges maintain proper z-index */ +:deep(.vue-flow__edge svg) { + z-index: 5 !important; +} + +:deep(.shape-node .shape-content) { + padding: 8px; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +:deep(.shape-node .shape-label) { + font-weight: 500; + line-height: 1.2; + word-wrap: break-word; + max-width: 100%; +} + +:deep(.shape-node .shape-description) { + font-size: 0.75rem; + opacity: 0.8; + margin-top: 4px; + line-height: 1.2; + word-wrap: break-word; + max-width: 100%; +} + +/* Specific shape styling */ +:deep(.shape-node.swimlane-horizontal) { + min-width: 600px; + min-height: 150px; + border-radius: 8px; +} + +:deep(.shape-node.swimlane-vertical) { + min-width: 200px; + min-height: 400px; + border-radius: 8px; +} + +:deep(.shape-node.rectangle-shape) { + min-width: 300px; + min-height: 200px; + border-radius: 4px; +} + +:deep(.shape-node.text-annotation) { + min-width: 200px; + min-height: 80px; + border-radius: 4px; + font-style: italic; +} + +:deep(.shape-node.process-group) { + min-width: 400px; + min-height: 300px; + border-radius: 12px; + padding: 16px; +} + +:deep(.shape-node.process-group .shape-content) { + align-items: flex-start; + justify-content: flex-start; +} + /* Touch-friendly adjustments */ @media (hover: none) and (pointer: coarse) { .component-item {