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>
|
||||
|
||||
<!-- 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>
|
||||
</template>
|
||||
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -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]);
|
||||
}
|
||||
});
|
||||
|
@ -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: `
|
||||
<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 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;
|
||||
}
|
||||
`;
|
@ -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) => {
|
||||
|
||||
<!-- Show node properties when a node is selected -->
|
||||
<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="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"
|
||||
:style="{ backgroundColor: nodeBorderColor }">
|
||||
<Icon :name="`material-symbols:${selectedNodeData.type === 'form' ? 'description' :
|
||||
selectedNodeData.type === 'api' ? 'api' :
|
||||
selectedNodeData.type === 'gateway' ? 'call_split' :
|
||||
selectedNodeData.type === 'script' ? 'code' :
|
||||
selectedNodeData.type === 'business-rule' ? 'rule' :
|
||||
selectedNodeData.type === 'notification' ? 'notifications' : 'account_tree'}`"
|
||||
<Icon :name="`material-symbols:${getNodeIcon(selectedNodeData.type)}`"
|
||||
class="w-4 h-4" />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@ -1801,8 +1882,8 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Node Shape Selector (only for non-start/end nodes) -->
|
||||
<div v-if="selectedNodeData.type !== 'start' && selectedNodeData.type !== 'end'">
|
||||
<!-- Node Shape Selector (only for non-start/end nodes and non-shapes) -->
|
||||
<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>
|
||||
<select
|
||||
v-model="nodeShape"
|
||||
@ -1817,6 +1898,34 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
|
||||
{{ shapeOptions.find(s => s.value === nodeShape)?.label || 'Rectangle' }}
|
||||
</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>
|
||||
|
||||
@ -1905,8 +2014,8 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Section -->
|
||||
<div v-if="selectedNodeData.type !== 'start' && selectedNodeData.type !== 'end'" class="p-4 border-t border-gray-200">
|
||||
<!-- Configuration Section (only for process nodes, not shapes) -->
|
||||
<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">
|
||||
<Icon name="material-symbols:settings" class="w-4 h-4 mr-2" />
|
||||
Configuration
|
||||
@ -1966,6 +2075,26 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
|
||||
</RsButton>
|
||||
</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>
|
||||
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user