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.
This commit is contained in:
parent
0a01f1116a
commit
5faefb8900
@ -35,6 +35,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Design Elements -->
|
||||||
|
<div class="component-category mb-6">
|
||||||
|
<h3 class="text-gray-700 text-sm font-medium px-3 mb-2">Design Elements</h3>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 px-2">
|
||||||
|
<div
|
||||||
|
v-for="shape in getComponentsByCategory('Shape')"
|
||||||
|
:key="shape.type"
|
||||||
|
class="component-item rounded p-3 flex flex-col items-center justify-center cursor-grab hover:bg-blue-50 transition-colors border border-blue-200 touch-manipulation"
|
||||||
|
:class="{ 'hidden': !matchesSearch(shape) }"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="onDragStart($event, shape)"
|
||||||
|
@click="addComponent(shape)"
|
||||||
|
>
|
||||||
|
<Icon :name="shape.icon" class="mb-2 w-6 h-6 text-blue-500" />
|
||||||
|
<span class="text-xs text-blue-600 text-center leading-tight">{{ shape.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -188,6 +207,148 @@ const availableComponents = [
|
|||||||
errorVariable: 'scriptError'
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -356,22 +356,17 @@ watch(() => props.initialNodes, async (newNodes, oldNodes) => {
|
|||||||
newNodes.forEach(newNode => {
|
newNodes.forEach(newNode => {
|
||||||
const existingNode = nodes.value.find(n => n.id === newNode.id);
|
const existingNode = nodes.value.find(n => n.id === newNode.id);
|
||||||
if (existingNode) {
|
if (existingNode) {
|
||||||
// Check if the node has actually changed before updating
|
// Always update the existing node to ensure latest data is reflected
|
||||||
const hasChanges = (
|
// This is critical for property changes to be visible in the canvas
|
||||||
existingNode.label !== newNode.label ||
|
Object.assign(existingNode, {
|
||||||
JSON.stringify(existingNode.data) !== JSON.stringify(newNode.data) ||
|
label: newNode.label,
|
||||||
Math.abs(existingNode.position.x - newNode.position.x) > 1 ||
|
data: { ...newNode.data },
|
||||||
Math.abs(existingNode.position.y - newNode.position.y) > 1
|
position: { ...newNode.position }
|
||||||
);
|
});
|
||||||
|
|
||||||
if (hasChanges) {
|
// Always call updateNodeInternals to force re-render
|
||||||
Object.assign(existingNode, {
|
// This ensures property changes are reflected immediately
|
||||||
label: newNode.label,
|
updateNodeInternals([newNode.id]);
|
||||||
data: { ...newNode.data },
|
|
||||||
position: { ...newNode.position }
|
|
||||||
});
|
|
||||||
updateNodeInternals([newNode.id]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -630,7 +625,7 @@ const onDrop = (event) => {
|
|||||||
y: event.clientY - bounds.top
|
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 = {
|
const newNode = {
|
||||||
id: `${componentData.type}-${Date.now()}`,
|
id: `${componentData.type}-${Date.now()}`,
|
||||||
type: componentData.type,
|
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
|
// Add to Vue Flow for immediate visual feedback
|
||||||
addNodes([newNode]);
|
addNodes([newNode]);
|
||||||
|
|
||||||
@ -678,22 +679,30 @@ defineExpose({
|
|||||||
// Update an existing node
|
// Update an existing node
|
||||||
function updateNode(nodeId, newData) {
|
function updateNode(nodeId, newData) {
|
||||||
const nodeToUpdate = nodes.value.find(node => node.id === nodeId);
|
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
|
// Update the node properties - handle all provided data
|
||||||
if (newData.label) {
|
if (newData.label !== undefined) {
|
||||||
nodeToUpdate.label = newData.label;
|
nodeToUpdate.label = newData.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the node data
|
// Update the node data - merge with existing data
|
||||||
if (newData.data) {
|
if (newData.data !== undefined) {
|
||||||
nodeToUpdate.data = {
|
nodeToUpdate.data = {
|
||||||
...nodeToUpdate.data,
|
...nodeToUpdate.data,
|
||||||
...newData.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]);
|
updateNodeInternals([nodeId]);
|
||||||
|
|
||||||
return nodeToUpdate;
|
return nodeToUpdate;
|
||||||
@ -741,15 +750,17 @@ function syncCanvas(newNodes, newEdges) {
|
|||||||
addNodes([...nodesToAdd]);
|
addNodes([...nodesToAdd]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update existing nodes
|
// Update existing nodes - force update all properties
|
||||||
newNodes.forEach(newNode => {
|
newNodes.forEach(newNode => {
|
||||||
const existingNode = nodes.value.find(n => n.id === newNode.id);
|
const existingNode = nodes.value.find(n => n.id === newNode.id);
|
||||||
if (existingNode) {
|
if (existingNode) {
|
||||||
|
// Always update to ensure property changes are reflected
|
||||||
Object.assign(existingNode, {
|
Object.assign(existingNode, {
|
||||||
label: newNode.label,
|
label: newNode.label,
|
||||||
data: { ...newNode.data },
|
data: { ...newNode.data },
|
||||||
position: { ...newNode.position }
|
position: { ...newNode.position }
|
||||||
});
|
});
|
||||||
|
// Always force re-render for property changes
|
||||||
updateNodeInternals([newNode.id]);
|
updateNodeInternals([newNode.id]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -151,14 +151,25 @@ const CustomNode = markRaw({
|
|||||||
const borderColor = this.data?.borderColor || defaultBorder;
|
const borderColor = this.data?.borderColor || defaultBorder;
|
||||||
const textColor = this.data?.textColor || defaultText;
|
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-bg-color': backgroundColor,
|
||||||
'--node-border-color': borderColor,
|
'--node-border-color': borderColor,
|
||||||
'--node-text-color': textColor,
|
'--node-text-color': textColor,
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
borderColor: borderColor,
|
|
||||||
color: textColor
|
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: {
|
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: `
|
||||||
|
<div
|
||||||
|
:class="['shape-node', 'swimlane-horizontal', { 'selected': selected }]"
|
||||||
|
:style="shapeStyle"
|
||||||
|
>
|
||||||
|
<div class="shape-content" v-if="displayLabel || displayDescription">
|
||||||
|
<div class="shape-label" v-if="displayLabel">{{ displayLabel }}</div>
|
||||||
|
<div class="shape-description" v-if="displayDescription">{{ displayDescription }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
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: `
|
||||||
|
<div
|
||||||
|
:class="['shape-node', 'swimlane-vertical', { 'selected': selected }]"
|
||||||
|
:style="shapeStyle"
|
||||||
|
>
|
||||||
|
<div class="shape-content" v-if="displayLabel || displayDescription">
|
||||||
|
<div class="shape-label" v-if="displayLabel">{{ displayLabel }}</div>
|
||||||
|
<div class="shape-description" v-if="displayDescription">{{ displayDescription }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
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: `
|
||||||
|
<div
|
||||||
|
:class="['shape-node', 'rectangle-shape', { 'selected': selected }]"
|
||||||
|
:style="shapeStyle"
|
||||||
|
>
|
||||||
|
<div class="shape-content" v-if="displayLabel || displayDescription">
|
||||||
|
<div class="shape-label" v-if="displayLabel">{{ displayLabel }}</div>
|
||||||
|
<div class="shape-description" v-if="displayDescription">{{ displayDescription }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
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: `
|
||||||
|
<div
|
||||||
|
:class="['shape-node', 'text-annotation', { 'selected': selected }]"
|
||||||
|
:style="shapeStyle"
|
||||||
|
>
|
||||||
|
<div class="shape-content" v-if="displayLabel || displayDescription">
|
||||||
|
<div class="shape-label" v-if="displayLabel">{{ displayLabel }}</div>
|
||||||
|
<div class="shape-description" v-if="displayDescription">{{ displayDescription }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
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: `
|
||||||
|
<div
|
||||||
|
:class="['shape-node', 'process-group', { 'selected': selected }]"
|
||||||
|
:style="shapeStyle"
|
||||||
|
>
|
||||||
|
<div class="shape-content" v-if="displayLabel || displayDescription">
|
||||||
|
<div class="shape-label" v-if="displayLabel">{{ displayLabel }}</div>
|
||||||
|
<div class="shape-description" v-if="displayDescription">{{ displayDescription }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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: `
|
||||||
|
<div
|
||||||
|
:class="['custom-node', 'shape-node', 'shape-hexagon', { 'selected': selected }]"
|
||||||
|
:style="shapeStyle"
|
||||||
|
>
|
||||||
|
<div class="custom-node-content" v-if="displayLabel || displayDescription">
|
||||||
|
<div class="custom-node-label" v-if="displayLabel">{{ displayLabel }}</div>
|
||||||
|
<div class="shape-description" v-if="displayDescription">{{ displayDescription }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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: `
|
||||||
|
<div
|
||||||
|
:class="['custom-node', 'shape-node', 'shape-trapezoid', { 'selected': selected }]"
|
||||||
|
:style="shapeStyle"
|
||||||
|
>
|
||||||
|
<div class="custom-node-content" v-if="displayLabel || displayDescription">
|
||||||
|
<div class="custom-node-label" v-if="displayLabel">{{ displayLabel }}</div>
|
||||||
|
<div class="shape-description" v-if="displayDescription">{{ displayDescription }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
// Export the node types object to use with Vue Flow
|
// Export the node types object to use with Vue Flow
|
||||||
export const nodeTypes = markRaw({
|
export const nodeTypes = {
|
||||||
start: StartNode,
|
start: StartNode,
|
||||||
end: EndNode,
|
end: EndNode,
|
||||||
gateway: GatewayNode,
|
gateway: GatewayNode,
|
||||||
@ -648,8 +956,16 @@ export const nodeTypes = markRaw({
|
|||||||
script: ScriptNode,
|
script: ScriptNode,
|
||||||
'business-rule': BusinessRuleNode,
|
'business-rule': BusinessRuleNode,
|
||||||
api: ApiCallNode,
|
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
|
// Default CSS for the nodes to be imported where needed
|
||||||
export const nodeStyles = `
|
export const nodeStyles = `
|
||||||
@ -1269,4 +1585,100 @@ export const nodeStyles = `
|
|||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-left: 4px solid #6b7280; /* Gray border to match icon color */
|
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;
|
||||||
|
}
|
||||||
`;
|
`;
|
@ -286,7 +286,9 @@ const canvasNodes = computed(() => {
|
|||||||
// Return a deep copy to ensure proper reactivity for nested objects like data
|
// Return a deep copy to ensure proper reactivity for nested objects like data
|
||||||
return processStore.currentProcess.nodes.map(node => ({
|
return processStore.currentProcess.nodes.map(node => ({
|
||||||
...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' }
|
{ 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
|
// Helper function to validate and normalize hex color
|
||||||
const normalizeHexColor = (value, fallback = '#ffffff') => {
|
const normalizeHexColor = (value, fallback = '#ffffff') => {
|
||||||
if (!value || typeof value !== 'string') return fallback;
|
if (!value || typeof value !== 'string') return fallback;
|
||||||
@ -638,21 +660,68 @@ const onPaneClick = () => {
|
|||||||
onPaneClickMobile();
|
onPaneClickMobile();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update node in store
|
// Enhanced updateNodeInStore with automatic refresh and canvas sync
|
||||||
const updateNodeInStore = () => {
|
const updateNodeInStore = async () => {
|
||||||
if (selectedNodeData.value) {
|
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 nodeId = selectedNodeData.value.id;
|
||||||
const newLabel = selectedNodeData.value.label;
|
const newLabel = selectedNodeData.value.label;
|
||||||
const newData = { ...selectedNodeData.value.data };
|
const newData = { ...selectedNodeData.value.data };
|
||||||
|
|
||||||
// Update in store first - this will trigger canvasNodes computed to update
|
// Update in store first
|
||||||
processStore.updateNode(nodeId, {
|
processStore.updateNode(nodeId, {
|
||||||
label: newLabel,
|
label: newLabel,
|
||||||
data: newData
|
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
|
// Handle condition update
|
||||||
const handleConditionUpdate = (conditions) => {
|
const handleConditionUpdate = async (conditions) => {
|
||||||
if (selectedNodeData.value && selectedNodeData.value.type === 'gateway') {
|
if (selectedNodeData.value && selectedNodeData.value.type === 'gateway') {
|
||||||
// Update conditions in the node data
|
// Update conditions in the node data
|
||||||
selectedNodeData.value.data = {
|
selectedNodeData.value.data = {
|
||||||
@ -722,8 +791,8 @@ const handleConditionUpdate = (conditions) => {
|
|||||||
processStore.currentProcess.edges = updatedEdges;
|
processStore.currentProcess.edges = updatedEdges;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the node in store
|
// Update the node in store and refresh
|
||||||
updateNodeInStore();
|
await updateNodeInStore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -745,7 +814,6 @@ const onNodesChange = (changes, currentNodes) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (Object.keys(positionChanges).length > 0) {
|
if (Object.keys(positionChanges).length > 0) {
|
||||||
|
|
||||||
processStore.updateNodePositions(positionChanges);
|
processStore.updateNodePositions(positionChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -755,7 +823,6 @@ const onNodesChange = (changes, currentNodes) => {
|
|||||||
.map(change => change.id);
|
.map(change => change.id);
|
||||||
|
|
||||||
if (removedNodes.length > 0) {
|
if (removedNodes.length > 0) {
|
||||||
|
|
||||||
removedNodes.forEach(nodeId => {
|
removedNodes.forEach(nodeId => {
|
||||||
processStore.deleteNode(nodeId);
|
processStore.deleteNode(nodeId);
|
||||||
});
|
});
|
||||||
@ -766,13 +833,9 @@ const onNodesChange = (changes, currentNodes) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update node selection if needed
|
// REMOVED: Don't overwrite selectedNodeData from canvas changes to preserve local edits
|
||||||
if (selectedNodeData.value) {
|
// This was causing property panel changes to be lost when the canvas updated
|
||||||
const updatedNode = currentNodes.find(node => node.id === selectedNodeData.value.id);
|
// The canvasNodes computed and its watcher will handle synchronization properly
|
||||||
if (updatedNode) {
|
|
||||||
selectedNodeData.value = { ...updatedNode };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle updates from the canvas when edges change
|
// Handle updates from the canvas when edges change
|
||||||
@ -1334,7 +1397,7 @@ const onConditionsUpdated = (conditions) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle API node update
|
// Handle API node update
|
||||||
const handleApiNodeUpdate = (updatedData) => {
|
const handleApiNodeUpdate = async (updatedData) => {
|
||||||
if (selectedNodeData.value && selectedNodeData.value.type === 'api') {
|
if (selectedNodeData.value && selectedNodeData.value.type === 'api') {
|
||||||
// Make sure to update the label both in data and at the root level
|
// Make sure to update the label both in data and at the root level
|
||||||
const newLabel = updatedData.label || 'API Call';
|
const newLabel = updatedData.label || 'API Call';
|
||||||
@ -1348,13 +1411,13 @@ const handleApiNodeUpdate = (updatedData) => {
|
|||||||
// Also update the root label
|
// Also update the root label
|
||||||
selectedNodeData.value.label = newLabel;
|
selectedNodeData.value.label = newLabel;
|
||||||
|
|
||||||
// Update the node in store
|
// Update the node in store and refresh
|
||||||
updateNodeInStore();
|
await updateNodeInStore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add this function to handle form node updates
|
// Add this function to handle form node updates
|
||||||
const handleFormNodeUpdate = (updatedData) => {
|
const handleFormNodeUpdate = async (updatedData) => {
|
||||||
if (selectedNodeData.value && selectedNodeData.value.type === 'form') {
|
if (selectedNodeData.value && selectedNodeData.value.type === 'form') {
|
||||||
// Make sure to update the label both in data and at the root level
|
// Make sure to update the label both in data and at the root level
|
||||||
const newLabel = updatedData.label || 'Form Task';
|
const newLabel = updatedData.label || 'Form Task';
|
||||||
@ -1368,40 +1431,40 @@ const handleFormNodeUpdate = (updatedData) => {
|
|||||||
// Also update the root label
|
// Also update the root label
|
||||||
selectedNodeData.value.label = newLabel;
|
selectedNodeData.value.label = newLabel;
|
||||||
|
|
||||||
// Update the node in store
|
// Update the node in store and refresh
|
||||||
updateNodeInStore();
|
await updateNodeInStore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update handler for business rule node
|
// Update handler for business rule node
|
||||||
const handleBusinessRuleUpdate = (data) => {
|
const handleBusinessRuleUpdate = async (data) => {
|
||||||
if (selectedNodeData.value) {
|
if (selectedNodeData.value) {
|
||||||
selectedNodeData.value.data = {
|
selectedNodeData.value.data = {
|
||||||
...selectedNodeData.value.data,
|
...selectedNodeData.value.data,
|
||||||
...data
|
...data
|
||||||
};
|
};
|
||||||
updateNodeInStore();
|
await updateNodeInStore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add a method to handle default path updates
|
// Add a method to handle default path updates
|
||||||
const handleDefaultPathUpdate = (path) => {
|
const handleDefaultPathUpdate = async (path) => {
|
||||||
if (selectedNodeData.value && selectedNodeData.value.type === 'gateway') {
|
if (selectedNodeData.value && selectedNodeData.value.type === 'gateway') {
|
||||||
selectedNodeData.value.data.defaultPath = path;
|
selectedNodeData.value.data.defaultPath = path;
|
||||||
updateNodeInStore();
|
await updateNodeInStore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle notification node update
|
// Handle notification node update
|
||||||
const handleNotificationNodeUpdate = (updatedData) => {
|
const handleNotificationNodeUpdate = async (updatedData) => {
|
||||||
if (selectedNodeData.value) {
|
if (selectedNodeData.value) {
|
||||||
selectedNodeData.value.data = { ...updatedData };
|
selectedNodeData.value.data = { ...updatedData };
|
||||||
updateNodeInStore();
|
await updateNodeInStore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle script node update
|
// Handle script node update
|
||||||
const handleScriptNodeUpdate = (updatedData) => {
|
const handleScriptNodeUpdate = async (updatedData) => {
|
||||||
if (selectedNodeData.value && selectedNodeData.value.type === 'script') {
|
if (selectedNodeData.value && selectedNodeData.value.type === 'script') {
|
||||||
// Make sure to update the label both in data and at the root level
|
// Make sure to update the label both in data and at the root level
|
||||||
const newLabel = updatedData.label || 'Script Task';
|
const newLabel = updatedData.label || 'Script Task';
|
||||||
@ -1430,8 +1493,8 @@ const handleScriptNodeUpdate = (updatedData) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the node in store
|
// Update the node in store and refresh
|
||||||
updateNodeInStore();
|
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 for process changes to restore Vue Flow state
|
||||||
watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
|
watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
|
||||||
if (!newProcess) return;
|
if (!newProcess) return;
|
||||||
@ -1749,21 +1833,18 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
|
|||||||
|
|
||||||
<!-- Show node properties when a node is selected -->
|
<!-- Show node properties when a node is selected -->
|
||||||
<div v-else class="flex flex-col h-full">
|
<div v-else class="flex flex-col h-full">
|
||||||
<!-- Node Header -->
|
<!-- Node/Shape Header -->
|
||||||
<div class="p-4 border-b border-gray-200 bg-gray-50">
|
<div class="p-4 border-b border-gray-200 bg-gray-50">
|
||||||
<div class="flex items-center space-x-2 mb-2">
|
<div class="flex items-center space-x-2 mb-2">
|
||||||
<div class="w-8 h-8 rounded-lg flex items-center justify-center text-white text-sm font-medium"
|
<div class="w-8 h-8 rounded-lg flex items-center justify-center text-white text-sm font-medium"
|
||||||
:style="{ backgroundColor: nodeBorderColor }">
|
:style="{ backgroundColor: nodeBorderColor }">
|
||||||
<Icon :name="`material-symbols:${selectedNodeData.type === 'form' ? 'description' :
|
<Icon :name="`material-symbols:${getNodeIcon(selectedNodeData.type)}`"
|
||||||
selectedNodeData.type === 'api' ? 'api' :
|
|
||||||
selectedNodeData.type === 'gateway' ? 'call_split' :
|
|
||||||
selectedNodeData.type === 'script' ? 'code' :
|
|
||||||
selectedNodeData.type === 'business-rule' ? 'rule' :
|
|
||||||
selectedNodeData.type === 'notification' ? 'notifications' : 'account_tree'}`"
|
|
||||||
class="w-4 h-4" />
|
class="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold text-gray-900 capitalize">{{ selectedNodeData.type.replace('-', ' ') }} Node</h3>
|
<h3 class="text-sm font-semibold text-gray-900 capitalize">
|
||||||
|
{{ selectedNodeData.data?.isShape ? 'Shape' : 'Node' }}: {{ selectedNodeData.type.replace('-', ' ') }}
|
||||||
|
</h3>
|
||||||
<p class="text-xs text-gray-500">{{ selectedNodeData.id }}</p>
|
<p class="text-xs text-gray-500">{{ selectedNodeData.id }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1801,8 +1882,8 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Node Shape Selector (only for non-start/end nodes) -->
|
<!-- Node Shape Selector (only for non-start/end nodes and non-shapes) -->
|
||||||
<div v-if="selectedNodeData.type !== 'start' && selectedNodeData.type !== 'end'">
|
<div v-if="selectedNodeData.type !== 'start' && selectedNodeData.type !== 'end' && !selectedNodeData.data?.isShape">
|
||||||
<label class="block text-xs font-medium text-gray-600 mb-1">Shape</label>
|
<label class="block text-xs font-medium text-gray-600 mb-1">Shape</label>
|
||||||
<select
|
<select
|
||||||
v-model="nodeShape"
|
v-model="nodeShape"
|
||||||
@ -1817,6 +1898,34 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
|
|||||||
{{ shapeOptions.find(s => s.value === nodeShape)?.label || 'Rectangle' }}
|
{{ shapeOptions.find(s => s.value === nodeShape)?.label || 'Rectangle' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Size Controls for Shapes -->
|
||||||
|
<div v-if="selectedNodeData.data?.isShape" class="space-y-3">
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-600 mb-1">Width</label>
|
||||||
|
<input
|
||||||
|
v-model.number="selectedNodeData.data.width"
|
||||||
|
type="number"
|
||||||
|
min="50"
|
||||||
|
max="2000"
|
||||||
|
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
@input="updateShapeSize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-600 mb-1">Height</label>
|
||||||
|
<input
|
||||||
|
v-model.number="selectedNodeData.data.height"
|
||||||
|
type="number"
|
||||||
|
min="50"
|
||||||
|
max="2000"
|
||||||
|
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
@input="updateShapeSize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1905,8 +2014,8 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Configuration Section -->
|
<!-- Configuration Section (only for process nodes, not shapes) -->
|
||||||
<div v-if="selectedNodeData.type !== 'start' && selectedNodeData.type !== 'end'" class="p-4 border-t border-gray-200">
|
<div v-if="selectedNodeData.type !== 'start' && selectedNodeData.type !== 'end' && !selectedNodeData.data?.isShape" class="p-4 border-t border-gray-200">
|
||||||
<h4 class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
<h4 class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
||||||
<Icon name="material-symbols:settings" class="w-4 h-4 mr-2" />
|
<Icon name="material-symbols:settings" class="w-4 h-4 mr-2" />
|
||||||
Configuration
|
Configuration
|
||||||
@ -1966,6 +2075,26 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
|
|||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Shape Information -->
|
||||||
|
<div v-if="selectedNodeData.data?.isShape" class="p-4 border-t border-gray-200">
|
||||||
|
<h4 class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
||||||
|
<Icon name="material-symbols:info" class="w-4 h-4 mr-2" />
|
||||||
|
Shape Information
|
||||||
|
</h4>
|
||||||
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||||
|
<p class="text-sm text-blue-700">
|
||||||
|
<strong>Design Element:</strong> 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.
|
||||||
|
</p>
|
||||||
|
<div class="mt-2 text-xs text-blue-600">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon name="material-symbols:palette" class="w-3 h-3 mr-1" />
|
||||||
|
Use the appearance section above to customize colors and styling
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -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 */
|
/* Touch-friendly adjustments */
|
||||||
@media (hover: none) and (pointer: coarse) {
|
@media (hover: none) and (pointer: coarse) {
|
||||||
.component-item {
|
.component-item {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user