Enhance Custom Node Components with Shape Class and Style Updates
- Added a computed property for dynamic shape classes in custom node components (ApiNode, BusinessRuleNode, FormNode, GatewayNode, HtmlNode, NotificationNode, ScriptNode, SubprocessNode) to support various shapes. - Updated the class bindings in the template to include the new shape classes, improving visual representation of nodes based on their defined shapes. - Adjusted styles in `nodeStyles.js` to accommodate new shape types (hexagon, trapezoid) and ensure proper rendering without conflicting base styles. - Enhanced the process builder interface by refining node styles and ensuring consistent behavior across different node types.
This commit is contained in:
parent
b8431c1a65
commit
9f53c75716
@ -43,6 +43,12 @@ const nodeStyle = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Computed for shape class
|
||||
const shapeClass = computed(() => {
|
||||
const shape = props.data?.shape || 'rectangle'
|
||||
return `shape-${shape}`
|
||||
})
|
||||
|
||||
// Handle node click event
|
||||
const emit = defineEmits(['node-click'])
|
||||
const onClick = () => {
|
||||
@ -52,7 +58,7 @@ const onClick = () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="['custom-node', 'node-api', { 'selected': selected }]"
|
||||
:class="['custom-node', 'node-api', shapeClass, { 'selected': selected }]"
|
||||
:style="nodeStyle"
|
||||
@click="onClick"
|
||||
>
|
||||
|
@ -75,6 +75,12 @@ const nodeStyle = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Computed for shape class
|
||||
const shapeClass = computed(() => {
|
||||
const shape = props.data?.shape || 'rectangle'
|
||||
return `shape-${shape}`
|
||||
})
|
||||
|
||||
// Handle node click event
|
||||
const emit = defineEmits(['node-click'])
|
||||
const onClick = () => {
|
||||
@ -84,7 +90,7 @@ const onClick = () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="['custom-node', 'node-business-rule', { 'selected': selected }]"
|
||||
:class="['custom-node', 'node-business-rule', shapeClass, { 'selected': selected }]"
|
||||
:style="nodeStyle"
|
||||
@click="onClick"
|
||||
>
|
||||
|
@ -39,6 +39,12 @@ const nodeStyle = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Computed for shape class
|
||||
const shapeClass = computed(() => {
|
||||
const shape = props.data?.shape || 'rectangle'
|
||||
return `shape-${shape}`
|
||||
})
|
||||
|
||||
// Handle node click event
|
||||
const emit = defineEmits(['node-click'])
|
||||
const onClick = () => {
|
||||
@ -48,7 +54,7 @@ const onClick = () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="['custom-node', 'node-form', { 'selected': selected }]"
|
||||
:class="['custom-node', 'node-form', shapeClass, { 'selected': selected }]"
|
||||
:style="nodeStyle"
|
||||
@click="onClick"
|
||||
>
|
||||
|
@ -58,6 +58,12 @@ const nodeStyle = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Computed for shape class
|
||||
const shapeClass = computed(() => {
|
||||
const shape = props.data?.shape || 'diamond'
|
||||
return `shape-${shape}`
|
||||
})
|
||||
|
||||
// Handle node click event
|
||||
const emit = defineEmits(['node-click'])
|
||||
const onClick = () => {
|
||||
@ -67,7 +73,7 @@ const onClick = () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="['custom-node', 'node-gateway', { 'selected': selected }]"
|
||||
:class="['custom-node', 'node-gateway', shapeClass, { 'selected': selected }]"
|
||||
:style="nodeStyle"
|
||||
@click="onClick"
|
||||
>
|
||||
@ -125,12 +131,6 @@ const onClick = () => {
|
||||
{{ totalPaths === 0 ? 'None' : totalPaths }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="node-rule-detail flex items-center justify-between text-xs mt-1">
|
||||
<span class="node-rule-detail-label">Default:</span>
|
||||
<span class="node-rule-detail-value ml-1 font-medium text-orange-600">
|
||||
{{ defaultPath }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -176,32 +176,38 @@ const onClick = () => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
color: #f97316;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.custom-node-title {
|
||||
font-size: 11px;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
color: #c2410c;
|
||||
margin: 0 0 4px 0;
|
||||
text-align: center;
|
||||
width: 90%;
|
||||
white-space: nowrap;
|
||||
width: 70px;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2;
|
||||
line-height: 1.0;
|
||||
max-height: 18px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.gateway-details {
|
||||
width: 80%;
|
||||
width: 70px;
|
||||
text-align: center;
|
||||
margin-top: 2px;
|
||||
display: flex;
|
||||
|
@ -52,6 +52,12 @@ const nodeStyle = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Computed for shape class
|
||||
const shapeClass = computed(() => {
|
||||
const shape = props.data?.shape || 'rectangle'
|
||||
return `shape-${shape}`
|
||||
})
|
||||
|
||||
// Handle node click event
|
||||
const emit = defineEmits(['node-click'])
|
||||
const onClick = () => {
|
||||
@ -61,7 +67,7 @@ const onClick = () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="['custom-node', 'node-html', { 'selected': selected }]"
|
||||
:class="['custom-node', 'node-html', shapeClass, { 'selected': selected }]"
|
||||
:style="nodeStyle"
|
||||
@click="onClick"
|
||||
>
|
||||
|
@ -74,6 +74,12 @@ const nodeStyle = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Computed for shape class
|
||||
const shapeClass = computed(() => {
|
||||
const shape = props.data?.shape || 'rectangle'
|
||||
return `shape-${shape}`
|
||||
})
|
||||
|
||||
// Handle node click event
|
||||
const emit = defineEmits(['node-click'])
|
||||
const onClick = () => {
|
||||
@ -83,7 +89,7 @@ const onClick = () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="['custom-node', 'node-notification', { 'selected': selected }]"
|
||||
:class="['custom-node', 'node-notification', shapeClass, { 'selected': selected }]"
|
||||
:style="nodeStyle"
|
||||
@click="onClick"
|
||||
>
|
||||
|
@ -1,54 +1,60 @@
|
||||
<script setup>
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import { Handle, Position } from "@vue-flow/core";
|
||||
|
||||
// Define props that Vue Flow passes to custom nodes
|
||||
const props = defineProps([
|
||||
'id', // Node ID
|
||||
'type', // Node type
|
||||
'label', // Node label
|
||||
'selected', // Selection state
|
||||
'data' // Custom data object
|
||||
])
|
||||
"id", // Node ID
|
||||
"type", // Node type
|
||||
"label", // Node label
|
||||
"selected", // Selection state
|
||||
"data", // Custom data object
|
||||
]);
|
||||
|
||||
// Computed properties for node display
|
||||
const nodeLabel = computed(() => {
|
||||
return props.label || (props.data && props.data.label) || 'Script'
|
||||
})
|
||||
return props.label || (props.data && props.data.label) || "Script";
|
||||
});
|
||||
|
||||
const scriptLanguage = computed(() => {
|
||||
return props.data?.language || 'Not specified'
|
||||
})
|
||||
return props.data?.language || "Not specified";
|
||||
});
|
||||
|
||||
const hasScript = computed(() => {
|
||||
return !!props.data?.script
|
||||
})
|
||||
return !!props.data?.script;
|
||||
});
|
||||
|
||||
// Computed for node styling based on colors
|
||||
const nodeStyle = computed(() => {
|
||||
const backgroundColor = props.data?.backgroundColor || '#f9fafb'
|
||||
const borderColor = props.data?.borderColor || '#6b7280'
|
||||
const textColor = props.data?.textColor || '#374151'
|
||||
|
||||
const backgroundColor = props.data?.backgroundColor || "#f9fafb";
|
||||
const borderColor = props.data?.borderColor || "#6b7280";
|
||||
const textColor = props.data?.textColor || "#374151";
|
||||
|
||||
return {
|
||||
'--node-bg-color': backgroundColor,
|
||||
'--node-border-color': borderColor,
|
||||
'--node-text-color': textColor,
|
||||
"--node-bg-color": backgroundColor,
|
||||
"--node-border-color": borderColor,
|
||||
"--node-text-color": textColor,
|
||||
backgroundColor: backgroundColor,
|
||||
borderColor: borderColor,
|
||||
color: textColor
|
||||
}
|
||||
})
|
||||
color: textColor,
|
||||
};
|
||||
});
|
||||
|
||||
// Computed for shape class
|
||||
const shapeClass = computed(() => {
|
||||
const shape = props.data?.shape || "rectangle";
|
||||
return `shape-${shape}`;
|
||||
});
|
||||
|
||||
// Handle node click event
|
||||
const emit = defineEmits(['node-click'])
|
||||
const emit = defineEmits(["node-click"]);
|
||||
const onClick = () => {
|
||||
emit('node-click', props.id)
|
||||
}
|
||||
emit("node-click", props.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="['custom-node', 'node-script', { 'selected': selected }]"
|
||||
<div
|
||||
:class="['custom-node', 'node-script', shapeClass, { selected: selected }]"
|
||||
:style="nodeStyle"
|
||||
@click="onClick"
|
||||
>
|
||||
@ -62,7 +68,7 @@ const onClick = () => {
|
||||
:isConnectable="true"
|
||||
:isValidConnection="() => true"
|
||||
/>
|
||||
|
||||
|
||||
<Handle
|
||||
type="target"
|
||||
:position="Position.Left"
|
||||
@ -72,7 +78,7 @@ const onClick = () => {
|
||||
:isConnectable="true"
|
||||
:isValidConnection="() => true"
|
||||
/>
|
||||
|
||||
|
||||
<!-- Output handles -->
|
||||
<Handle
|
||||
type="source"
|
||||
@ -83,7 +89,7 @@ const onClick = () => {
|
||||
:isConnectable="true"
|
||||
:isValidConnection="() => true"
|
||||
/>
|
||||
|
||||
|
||||
<Handle
|
||||
type="source"
|
||||
:position="Position.Bottom"
|
||||
@ -93,7 +99,7 @@ const onClick = () => {
|
||||
:isConnectable="true"
|
||||
:isValidConnection="() => true"
|
||||
/>
|
||||
|
||||
|
||||
<div class="custom-node-content">
|
||||
<div class="flex items-center mb-1">
|
||||
<div class="custom-node-icon">
|
||||
@ -101,19 +107,17 @@ const onClick = () => {
|
||||
</div>
|
||||
<div class="custom-node-label" :title="nodeLabel">{{ nodeLabel }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="node-details">
|
||||
<p class="node-description">{{ data?.description || 'Script execution' }}</p>
|
||||
<div class="node-rule-detail flex items-center justify-between text-xs mt-1">
|
||||
<p class="node-description">
|
||||
{{ data?.description || "Script execution" }}
|
||||
</p>
|
||||
<div
|
||||
class="node-rule-detail flex items-center justify-between text-xs mt-1"
|
||||
>
|
||||
<span class="node-rule-detail-label">Language:</span>
|
||||
<span class="node-rule-detail-value ml-1 font-medium text-gray-600">
|
||||
{{ scriptLanguage }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="node-rule-detail flex items-center justify-between text-xs mt-1">
|
||||
<span class="node-rule-detail-label">Script:</span>
|
||||
<span class="node-rule-detail-value ml-1 font-medium text-gray-600">
|
||||
{{ hasScript ? 'Defined' : 'Not defined' }}
|
||||
JavaScript
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -205,7 +209,10 @@ const onClick = () => {
|
||||
}
|
||||
|
||||
/* Handle styles */
|
||||
.handle-top, .handle-bottom, .handle-left, .handle-right {
|
||||
.handle-top,
|
||||
.handle-bottom,
|
||||
.handle-left,
|
||||
.handle-right {
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
border-radius: 50% !important;
|
||||
@ -222,7 +229,7 @@ const onClick = () => {
|
||||
top: -6px !important;
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
border-color: #2196F3 !important;
|
||||
border-color: #2196f3 !important;
|
||||
background: #e3f2fd !important;
|
||||
}
|
||||
|
||||
@ -230,7 +237,7 @@ const onClick = () => {
|
||||
bottom: -6px !important;
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
border-color: #4CAF50 !important;
|
||||
border-color: #4caf50 !important;
|
||||
background: #e8f5e9 !important;
|
||||
}
|
||||
|
||||
@ -238,7 +245,7 @@ const onClick = () => {
|
||||
left: -6px !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
border-color: #2196F3 !important;
|
||||
border-color: #2196f3 !important;
|
||||
background: #e3f2fd !important;
|
||||
}
|
||||
|
||||
@ -246,7 +253,7 @@ const onClick = () => {
|
||||
right: -6px !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
border-color: #4CAF50 !important;
|
||||
border-color: #4caf50 !important;
|
||||
background: #e8f5e9 !important;
|
||||
}
|
||||
|
||||
@ -261,25 +268,25 @@ const onClick = () => {
|
||||
/* Handle hover effects */
|
||||
.handle-top:hover {
|
||||
transform: translateX(-50%) scale(1.1);
|
||||
background: #2196F3 !important;
|
||||
border-color: #1565C0 !important;
|
||||
background: #2196f3 !important;
|
||||
border-color: #1565c0 !important;
|
||||
}
|
||||
|
||||
.handle-bottom:hover {
|
||||
transform: translateX(-50%) scale(1.1);
|
||||
background: #4CAF50 !important;
|
||||
border-color: #2E7D32 !important;
|
||||
background: #4caf50 !important;
|
||||
border-color: #2e7d32 !important;
|
||||
}
|
||||
|
||||
.handle-left:hover {
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
background: #2196F3 !important;
|
||||
border-color: #1565C0 !important;
|
||||
background: #2196f3 !important;
|
||||
border-color: #1565c0 !important;
|
||||
}
|
||||
|
||||
.handle-right:hover {
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
background: #4CAF50 !important;
|
||||
border-color: #2E7D32 !important;
|
||||
background: #4caf50 !important;
|
||||
border-color: #2e7d32 !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -39,6 +39,12 @@ const nodeStyle = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Computed for shape class
|
||||
const shapeClass = computed(() => {
|
||||
const shape = props.data?.shape || 'rectangle'
|
||||
return `shape-${shape}`
|
||||
})
|
||||
|
||||
// Handle node click event
|
||||
const emit = defineEmits(['node-click'])
|
||||
const onClick = () => {
|
||||
@ -48,7 +54,7 @@ const onClick = () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="['custom-node', 'node-subprocess', { 'selected': selected }]"
|
||||
:class="['custom-node', 'node-subprocess', shapeClass, { 'selected': selected }]"
|
||||
:style="nodeStyle"
|
||||
@click="onClick"
|
||||
>
|
||||
|
@ -222,6 +222,33 @@ export const nodeStyles = `
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
/* Override base styles for shaped nodes */
|
||||
.node-form.shape-hexagon,
|
||||
.node-script.shape-hexagon,
|
||||
.node-api.shape-hexagon,
|
||||
.node-business-rule.shape-hexagon,
|
||||
.node-notification.shape-hexagon,
|
||||
.node-html.shape-hexagon,
|
||||
.node-subprocess.shape-hexagon {
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.node-form.shape-trapezoid,
|
||||
.node-script.shape-trapezoid,
|
||||
.node-api.shape-trapezoid,
|
||||
.node-business-rule.shape-trapezoid,
|
||||
.node-notification.shape-trapezoid,
|
||||
.node-html.shape-trapezoid,
|
||||
.node-subprocess.shape-trapezoid {
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.node-gateway {
|
||||
width: 120px !important;
|
||||
height: 120px !important;
|
||||
@ -332,16 +359,20 @@ export const nodeStyles = `
|
||||
}
|
||||
|
||||
.node-gateway .custom-node-label {
|
||||
font-size: 11px;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
color: #c2410c;
|
||||
margin: 0 0 4px 0;
|
||||
text-align: center;
|
||||
width: 90%;
|
||||
white-space: nowrap;
|
||||
width: 70px;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2;
|
||||
line-height: 1.0;
|
||||
max-height: 18px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.node-details {
|
||||
@ -484,9 +515,9 @@ export const nodeStyles = `
|
||||
}
|
||||
|
||||
.node-gateway .material-icons {
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
color: #f97316;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* Update node-specific styles to be more consistent */
|
||||
@ -503,6 +534,14 @@ export const nodeStyles = `
|
||||
border-left: 4px solid #9333ea; /* Purple border to match icon color */
|
||||
}
|
||||
|
||||
.node-form.shape-hexagon,
|
||||
.node-form.shape-trapezoid {
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.node-api {
|
||||
width: 180px;
|
||||
background: white;
|
||||
@ -516,6 +555,14 @@ export const nodeStyles = `
|
||||
border-left: 4px solid #6366f1; /* Indigo border to match icon color */
|
||||
}
|
||||
|
||||
.node-api.shape-hexagon,
|
||||
.node-api.shape-trapezoid {
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Script node styling */
|
||||
.node-script {
|
||||
width: 180px;
|
||||
@ -587,6 +634,15 @@ export const nodeStyles = `
|
||||
border-left: 4px solid #14b8a6; /* Teal border to match icon color */
|
||||
}
|
||||
|
||||
/* Global overrides for all shaped nodes to remove conflicting base styles */
|
||||
[class*="node-"].shape-hexagon,
|
||||
[class*="node-"].shape-trapezoid {
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Shape node styles */
|
||||
.shape-node {
|
||||
position: relative;
|
||||
@ -658,19 +714,305 @@ export const nodeStyles = `
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Hexagon and Trapezoid shape styles using clip-path */
|
||||
.shape-hexagon {
|
||||
background: var(--node-bg-color, #f8fafc);
|
||||
border: 2px solid var(--node-border-color, #e2e8f0);
|
||||
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
|
||||
color: var(--node-text-color, #475569);
|
||||
/* Shape classes for flowchart nodes */
|
||||
.custom-node.shape-rectangle {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.shape-trapezoid {
|
||||
background: var(--node-bg-color, #f8fafc);
|
||||
border: 2px solid var(--node-border-color, #e2e8f0);
|
||||
.custom-node.shape-rounded-rectangle {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.custom-node.shape-circle {
|
||||
border-radius: 50%;
|
||||
min-width: 120px;
|
||||
min-height: 120px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.custom-node.shape-circle .custom-node-content {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.custom-node.shape-diamond {
|
||||
transform: rotate(45deg);
|
||||
border-radius: 8px;
|
||||
min-width: 120px;
|
||||
min-height: 120px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.custom-node.shape-diamond .custom-node-content {
|
||||
transform: rotate(-45deg);
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.custom-node.shape-hexagon {
|
||||
position: relative;
|
||||
border-radius: 0 !important;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
min-width: 140px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.custom-node.shape-hexagon::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--node-bg-color, white);
|
||||
border: 2px solid var(--node-border-color, #ddd);
|
||||
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.custom-node.shape-hexagon .custom-node-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.custom-node.shape-parallelogram {
|
||||
transform: skew(-20deg);
|
||||
border-radius: 4px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.custom-node.shape-parallelogram .custom-node-content {
|
||||
transform: skew(20deg);
|
||||
}
|
||||
|
||||
.custom-node.shape-trapezoid {
|
||||
position: relative;
|
||||
border-radius: 0 !important;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
min-width: 140px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.custom-node.shape-trapezoid::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--node-bg-color, white);
|
||||
border: 2px solid var(--node-border-color, #ddd);
|
||||
clip-path: polygon(20% 0%, 80% 0%, 100% 100%, 0% 100%);
|
||||
color: var(--node-text-color, #475569);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.custom-node.shape-trapezoid .custom-node-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Special backgrounds for business-rule, notification, and api hexagon shapes */
|
||||
.node-business-rule.shape-hexagon::before {
|
||||
border-left: 4px solid var(--node-border-color, #9333ea);
|
||||
}
|
||||
|
||||
.node-notification.shape-hexagon::before {
|
||||
border-left: 4px solid var(--node-border-color, #3b82f6);
|
||||
}
|
||||
|
||||
.node-api.shape-hexagon::before {
|
||||
border-left: 4px solid var(--node-border-color, #10b981);
|
||||
}
|
||||
|
||||
.node-form.shape-hexagon::before {
|
||||
border-left: 4px solid var(--node-border-color, #9333ea);
|
||||
}
|
||||
|
||||
.node-script.shape-hexagon::before {
|
||||
border-left: 4px solid var(--node-border-color, #6b7280);
|
||||
}
|
||||
|
||||
.node-html.shape-hexagon::before {
|
||||
border-left: 4px solid var(--node-border-color, #0ea5e9);
|
||||
}
|
||||
|
||||
.node-subprocess.shape-hexagon::before {
|
||||
border-left: 4px solid var(--node-border-color, #14b8a6);
|
||||
}
|
||||
|
||||
.node-gateway.shape-hexagon::before {
|
||||
border: 2px solid var(--node-border-color, #FF9800);
|
||||
}
|
||||
|
||||
/* Special backgrounds for business-rule, notification, and api trapezoid shapes */
|
||||
.node-business-rule.shape-trapezoid::before {
|
||||
border-left: 4px solid var(--node-border-color, #9333ea);
|
||||
}
|
||||
|
||||
.node-notification.shape-trapezoid::before {
|
||||
border-left: 4px solid var(--node-border-color, #3b82f6);
|
||||
}
|
||||
|
||||
.node-api.shape-trapezoid::before {
|
||||
border-left: 4px solid var(--node-border-color, #10b981);
|
||||
}
|
||||
|
||||
.node-form.shape-trapezoid::before {
|
||||
border-left: 4px solid var(--node-border-color, #9333ea);
|
||||
}
|
||||
|
||||
.node-script.shape-trapezoid::before {
|
||||
border-left: 4px solid var(--node-border-color, #6b7280);
|
||||
}
|
||||
|
||||
.node-html.shape-trapezoid::before {
|
||||
border-left: 4px solid var(--node-border-color, #0ea5e9);
|
||||
}
|
||||
|
||||
.node-subprocess.shape-trapezoid::before {
|
||||
border-left: 4px solid var(--node-border-color, #14b8a6);
|
||||
}
|
||||
|
||||
.node-gateway.shape-trapezoid::before {
|
||||
border: 2px solid var(--node-border-color, #FF9800);
|
||||
}
|
||||
|
||||
/* Handle positioning adjustments for different shapes */
|
||||
.custom-node.shape-circle .vue-flow__handle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.custom-node.shape-circle .handle-top {
|
||||
top: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.custom-node.shape-circle .handle-bottom {
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.custom-node.shape-circle .handle-left {
|
||||
left: -5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.custom-node.shape-circle .handle-right {
|
||||
right: -5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.custom-node.shape-diamond .vue-flow__handle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.custom-node.shape-diamond .handle-top {
|
||||
top: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.custom-node.shape-diamond .handle-bottom {
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.custom-node.shape-diamond .handle-left {
|
||||
left: -5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.custom-node.shape-diamond .handle-right {
|
||||
right: -5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.custom-node.shape-hexagon .vue-flow__handle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.custom-node.shape-hexagon .handle-top {
|
||||
top: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.custom-node.shape-hexagon .handle-bottom {
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.custom-node.shape-hexagon .handle-left {
|
||||
left: 5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.custom-node.shape-hexagon .handle-right {
|
||||
right: 5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.custom-node.shape-trapezoid .vue-flow__handle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.custom-node.shape-trapezoid .handle-top {
|
||||
top: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.custom-node.shape-trapezoid .handle-bottom {
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.custom-node.shape-trapezoid .handle-left {
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.custom-node.shape-trapezoid .handle-right {
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.custom-node.shape-parallelogram .vue-flow__handle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
/* Ensure shapes don't interfere with node connections */
|
||||
|
703
docs/vue-flow-process-builder-system-guide.md
Normal file
703
docs/vue-flow-process-builder-system-guide.md
Normal file
@ -0,0 +1,703 @@
|
||||
# Vue Flow Process Builder System Guide
|
||||
|
||||
## 📋 System Overview
|
||||
|
||||
This is a **Vue 3 + Nuxt 3** application that provides a visual process builder using **Vue Flow** library. The system allows users to create, edit, and manage business processes through a drag-and-drop interface with custom nodes and edges.
|
||||
|
||||
### 🏗️ **Core Architecture**
|
||||
- **Frontend**: Vue 3 with Composition API + Nuxt 3
|
||||
- **Flow Engine**: Vue Flow (Vue 3 compatible fork of React Flow)
|
||||
- **Styling**: Tailwind CSS + Custom SCSS
|
||||
- **State Management**: Pinia stores
|
||||
- **Database**: Prisma ORM
|
||||
- **UI Components**: Custom component library + FormKit
|
||||
|
||||
---
|
||||
|
||||
## 📁 File Structure & Key Locations
|
||||
|
||||
### **🎯 Main Process Builder**
|
||||
```
|
||||
pages/process-builder/
|
||||
├── index.vue # Main process builder page
|
||||
└── manage.vue # Process management page
|
||||
```
|
||||
|
||||
### **🔧 Core Vue Flow Components**
|
||||
```
|
||||
components/process-flow/
|
||||
├── ProcessFlowCanvas.vue # Main Vue Flow canvas component
|
||||
├── ArrowEdge.vue # Custom edge component
|
||||
├── custom/ # Custom node components
|
||||
│ ├── StartNode.vue # Process start node
|
||||
│ ├── EndNode.vue # Process end node
|
||||
│ ├── FormNode.vue # Form task node
|
||||
│ ├── ApiNode.vue # API call node
|
||||
│ ├── ScriptNode.vue # Script execution node
|
||||
│ ├── BusinessRuleNode.vue # Business rule node
|
||||
│ ├── NotificationNode.vue # Notification node
|
||||
│ ├── HtmlNode.vue # HTML content node
|
||||
│ ├── SubprocessNode.vue # Subprocess node
|
||||
│ ├── GatewayNode.vue # Decision/gateway node
|
||||
│ ├── TextAnnotation.vue # Text annotation node
|
||||
│ └── ProcessGroup.vue # Process grouping node
|
||||
├── notification/ # Notification system components
|
||||
│ ├── NotificationManager.vue
|
||||
│ ├── NotificationQueue.vue
|
||||
│ └── NotificationLogs.vue
|
||||
└── [25+ other process flow files]
|
||||
```
|
||||
|
||||
### **📊 Configuration & Data**
|
||||
```
|
||||
composables/
|
||||
├── processFlowNodes.js # Node type definitions & configurations
|
||||
├── nodeStyles.js # Global CSS styles for all nodes
|
||||
├── codemirrorThemes.js # Code editor themes
|
||||
└── themeList.js # UI theme configurations
|
||||
|
||||
docs/json/process-builder/
|
||||
├── processDefinition.json # Process structure schema
|
||||
└── processVariables.json # Process variable definitions
|
||||
|
||||
stores/
|
||||
├── processBuilder.js # Pinia store for process builder state
|
||||
├── formBuilder.js # Form builder state
|
||||
└── layout.js # Layout configurations
|
||||
```
|
||||
|
||||
### **🎨 Styling System**
|
||||
```
|
||||
assets/style/
|
||||
├── css/ # Compiled CSS
|
||||
│ ├── tailwind.css
|
||||
│ └── component/ # Component-specific styles
|
||||
├── scss/
|
||||
│ ├── main.scss # Main SCSS entry point
|
||||
│ └── custom/ # Custom styling
|
||||
│ ├── library/ # Third-party library styles
|
||||
│ │ ├── _dropdown.scss
|
||||
│ │ ├── _formkit.scss
|
||||
│ │ └── _floatingvue.scss
|
||||
│ └── transition/ # Animation styles
|
||||
│ ├── fade.scss
|
||||
│ └── page.scss
|
||||
|
||||
plugins/
|
||||
├── process-flow-styles.client.js # Vue Flow styling injection
|
||||
└── vue-codemirror.js # Code editor plugin
|
||||
```
|
||||
|
||||
### **⚙️ Server API**
|
||||
```
|
||||
server/api/
|
||||
├── process/
|
||||
│ ├── [id].get.js # Get process definition
|
||||
│ ├── [id].put.js # Update process
|
||||
│ ├── [id].delete.js # Delete process
|
||||
│ ├── [id]/
|
||||
│ │ ├── publish.post.js # Publish process
|
||||
│ │ ├── duplicate.post.js # Duplicate process
|
||||
│ │ └── history.get.js # Process history
|
||||
│ └── dashboard/
|
||||
│ └── summary.get.js # Dashboard summary
|
||||
└── forms/ # Form-related APIs
|
||||
├── [id].get.js
|
||||
├── [id].put.js
|
||||
└── [id]/history.get.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 How the System Works
|
||||
|
||||
### **1. Vue Flow Integration**
|
||||
|
||||
#### **Main Canvas Component** (`ProcessFlowCanvas.vue`)
|
||||
```vue
|
||||
<template>
|
||||
<VueFlow
|
||||
v-model:nodes="nodes"
|
||||
v-model:edges="edges"
|
||||
:node-types="customNodeTypes"
|
||||
:edge-types="customEdgeTypes"
|
||||
@error="handleVueFlowError"
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
<MiniMap />
|
||||
</VueFlow>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { markRaw } from 'vue'
|
||||
// Import all custom node components
|
||||
import StartNode from './custom/StartNode.vue'
|
||||
import FormNode from './custom/FormNode.vue'
|
||||
// ... other imports
|
||||
|
||||
// Register node types with Vue Flow
|
||||
const customNodeTypes = {
|
||||
'start': markRaw(StartNode),
|
||||
'form': markRaw(FormNode),
|
||||
'api': markRaw(ApiNode),
|
||||
// ... other node types
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
#### **Custom Node Structure**
|
||||
Every custom node follows this pattern:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
// Props from Vue Flow
|
||||
const props = defineProps([
|
||||
'id', // Node ID
|
||||
'type', // Node type
|
||||
'label', // Node label
|
||||
'selected', // Selection state
|
||||
'data' // Custom data object
|
||||
])
|
||||
|
||||
// Shape class for dynamic styling
|
||||
const shapeClass = computed(() => {
|
||||
const shape = props.data?.shape || 'rectangle'
|
||||
return `shape-${shape}`
|
||||
})
|
||||
|
||||
// Node styling
|
||||
const nodeStyle = computed(() => ({
|
||||
'--node-bg-color': props.data?.backgroundColor || 'white',
|
||||
'--node-border-color': props.data?.borderColor || '#ddd',
|
||||
'--node-text-color': props.data?.textColor || 'black'
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="['custom-node', 'node-form', shapeClass, { 'selected': selected }]"
|
||||
:style="nodeStyle"
|
||||
>
|
||||
<!-- Input handles -->
|
||||
<Handle type="target" :position="Position.Left" />
|
||||
|
||||
<!-- Node content -->
|
||||
<div class="custom-node-content">
|
||||
<div class="custom-node-header">
|
||||
<span class="material-icons">description</span>
|
||||
<span class="custom-node-title">{{ data?.label || 'Form' }}</span>
|
||||
</div>
|
||||
<!-- Additional content -->
|
||||
</div>
|
||||
|
||||
<!-- Output handles -->
|
||||
<Handle type="source" :position="Position.Right" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### **2. Node Shape System**
|
||||
|
||||
#### **Shape Configuration**
|
||||
Nodes support multiple shapes through CSS `clip-path`:
|
||||
|
||||
```javascript
|
||||
// Available shapes
|
||||
const shapes = [
|
||||
'rectangle', // Default rectangle
|
||||
'rounded-rectangle', // Rounded corners
|
||||
'circle', // Circle shape
|
||||
'diamond', // Diamond (gateway nodes)
|
||||
'hexagon', // Hexagon shape
|
||||
'trapezoid', // Trapezoid shape
|
||||
'parallelogram' // Parallelogram shape
|
||||
]
|
||||
```
|
||||
|
||||
#### **Shape CSS Implementation** (`nodeStyles.js`)
|
||||
```css
|
||||
/* Rectangle (default) */
|
||||
.custom-node.shape-rectangle {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Hexagon shape */
|
||||
.custom-node.shape-hexagon {
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.custom-node.shape-hexagon::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: var(--node-bg-color, white);
|
||||
border: 2px solid var(--node-border-color, #ddd);
|
||||
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Node-type specific colors for shapes */
|
||||
.node-form.shape-hexagon::before {
|
||||
border-left: 4px solid #9333ea; /* Purple */
|
||||
}
|
||||
```
|
||||
|
||||
### **3. Node Types & Configurations**
|
||||
|
||||
#### **Node Type Registry** (`processFlowNodes.js`)
|
||||
```javascript
|
||||
export const nodeTypes = {
|
||||
'start': {
|
||||
label: 'Start',
|
||||
icon: 'play_circle',
|
||||
color: '#10b981',
|
||||
allowedShapes: ['circle', 'rectangle'],
|
||||
defaultData: {
|
||||
label: 'Start',
|
||||
backgroundColor: '#dcfce7',
|
||||
borderColor: '#10b981'
|
||||
}
|
||||
},
|
||||
'form': {
|
||||
label: 'Form',
|
||||
icon: 'description',
|
||||
color: '#9333ea',
|
||||
allowedShapes: ['rectangle', 'rounded-rectangle', 'hexagon', 'trapezoid'],
|
||||
defaultData: {
|
||||
label: 'Form Task',
|
||||
formId: null,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
// ... other node types
|
||||
}
|
||||
```
|
||||
|
||||
### **4. Process Builder State Management**
|
||||
|
||||
#### **Pinia Store** (`stores/processBuilder.js`)
|
||||
```javascript
|
||||
export const useProcessBuilderStore = defineStore('processBuilder', () => {
|
||||
// State
|
||||
const nodes = ref([])
|
||||
const edges = ref([])
|
||||
const selectedNode = ref(null)
|
||||
const processDefinition = ref({})
|
||||
|
||||
// Actions
|
||||
const addNode = (nodeData) => {
|
||||
const newNode = {
|
||||
id: generateId(),
|
||||
type: nodeData.type,
|
||||
position: nodeData.position,
|
||||
data: { ...nodeTypes[nodeData.type].defaultData, ...nodeData.data }
|
||||
}
|
||||
nodes.value.push(newNode)
|
||||
}
|
||||
|
||||
const updateNode = (nodeId, updates) => {
|
||||
const nodeIndex = nodes.value.findIndex(n => n.id === nodeId)
|
||||
if (nodeIndex > -1) {
|
||||
nodes.value[nodeIndex] = { ...nodes.value[nodeIndex], ...updates }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes, edges, selectedNode, processDefinition,
|
||||
addNode, updateNode
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Implementation Patterns
|
||||
|
||||
### **1. Adding a New Node Type**
|
||||
|
||||
#### **Step 1: Create Node Component**
|
||||
```bash
|
||||
# Create new component file
|
||||
touch components/process-flow/custom/YourNewNode.vue
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- YourNewNode.vue -->
|
||||
<script setup>
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps(['id', 'type', 'label', 'selected', 'data'])
|
||||
|
||||
const shapeClass = computed(() => {
|
||||
const shape = props.data?.shape || 'rectangle'
|
||||
return `shape-${shape}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['custom-node', 'node-yournew', shapeClass, { 'selected': selected }]">
|
||||
<Handle type="target" :position="Position.Left" />
|
||||
<div class="custom-node-content">
|
||||
<span class="material-icons">your_icon</span>
|
||||
<span>{{ data?.label || 'Your Node' }}</span>
|
||||
</div>
|
||||
<Handle type="source" :position="Position.Right" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
#### **Step 2: Register in ProcessFlowCanvas.vue**
|
||||
```javascript
|
||||
// Import the component
|
||||
import YourNewNode from './custom/YourNewNode.vue'
|
||||
|
||||
// Add to customNodeTypes
|
||||
const customNodeTypes = {
|
||||
// ... existing types
|
||||
'yournew': markRaw(YourNewNode),
|
||||
}
|
||||
```
|
||||
|
||||
#### **Step 3: Add Node Configuration**
|
||||
```javascript
|
||||
// In processFlowNodes.js
|
||||
export const nodeTypes = {
|
||||
// ... existing types
|
||||
'yournew': {
|
||||
label: 'Your New Node',
|
||||
icon: 'your_icon',
|
||||
color: '#your-color',
|
||||
allowedShapes: ['rectangle', 'circle'],
|
||||
defaultData: {
|
||||
label: 'Your New Node',
|
||||
yourProperty: 'default-value'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **Step 4: Add Styling**
|
||||
```css
|
||||
/* In nodeStyles.js */
|
||||
.node-yournew {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-left: 4px solid #your-color;
|
||||
}
|
||||
```
|
||||
|
||||
### **2. Adding Shape Support**
|
||||
|
||||
#### **Step 1: Define Shape CSS**
|
||||
```css
|
||||
/* In nodeStyles.js */
|
||||
.custom-node.shape-yourshape {
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.custom-node.shape-yourshape::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: var(--node-bg-color, white);
|
||||
border: 2px solid var(--node-border-color, #ddd);
|
||||
clip-path: polygon(/* your shape points */);
|
||||
z-index: 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### **Step 2: Add to Shape Lists**
|
||||
```javascript
|
||||
// In processFlowNodes.js or relevant config
|
||||
const availableShapes = [
|
||||
// ... existing shapes
|
||||
'yourshape'
|
||||
]
|
||||
|
||||
// Add to node type configurations
|
||||
'form': {
|
||||
allowedShapes: ['rectangle', 'hexagon', 'yourshape']
|
||||
}
|
||||
```
|
||||
|
||||
### **3. Customizing Node Properties**
|
||||
|
||||
#### **Properties Panel Integration**
|
||||
```vue
|
||||
<!-- In node component -->
|
||||
<script setup>
|
||||
const props = defineProps(['id', 'type', 'label', 'selected', 'data'])
|
||||
|
||||
// Computed properties for dynamic behavior
|
||||
const isRequired = computed(() => props.data?.required || false)
|
||||
const formId = computed(() => props.data?.formId)
|
||||
const customProperties = computed(() => props.data?.customProperties || {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="custom-node">
|
||||
<!-- Display based on properties -->
|
||||
<div v-if="isRequired" class="required-indicator">*</div>
|
||||
<div class="node-content">
|
||||
{{ data?.label || 'Default Label' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Styling System
|
||||
|
||||
### **CSS Architecture**
|
||||
1. **Global Styles** (`nodeStyles.js`) - Injected globally via plugin
|
||||
2. **Component Styles** - Scoped styles in individual `.vue` files
|
||||
3. **Tailwind Classes** - Utility classes for layout and spacing
|
||||
4. **CSS Variables** - Dynamic theming support
|
||||
|
||||
### **CSS Variable System**
|
||||
```css
|
||||
/* Node supports dynamic styling via CSS variables */
|
||||
.custom-node {
|
||||
background: var(--node-bg-color, white);
|
||||
border-color: var(--node-border-color, #ddd);
|
||||
color: var(--node-text-color, black);
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Set in component
|
||||
const nodeStyle = computed(() => ({
|
||||
'--node-bg-color': props.data?.backgroundColor || 'white',
|
||||
'--node-border-color': props.data?.borderColor || '#ddd',
|
||||
'--node-text-color': props.data?.textColor || 'black'
|
||||
}))
|
||||
```
|
||||
|
||||
### **Shape Override Pattern**
|
||||
```css
|
||||
/* Base node styles */
|
||||
.node-form {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Shape overrides */
|
||||
.node-form.shape-hexagon {
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Plugin System
|
||||
|
||||
### **Vue Flow Styles Plugin** (`process-flow-styles.client.js`)
|
||||
```javascript
|
||||
export default defineNuxtPlugin(() => {
|
||||
// Inject global styles for Vue Flow nodes
|
||||
if (process.client) {
|
||||
const { injectGlobalStyles } = useNodeStyles()
|
||||
injectGlobalStyles()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### **CodeMirror Integration** (`vue-codemirror.js`)
|
||||
```javascript
|
||||
export default defineNuxtPlugin(() => {
|
||||
// Register CodeMirror for script editing
|
||||
// Used in ScriptNode and ApiNode components
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Data Flow
|
||||
|
||||
### **Process Definition Structure**
|
||||
```json
|
||||
{
|
||||
"id": "process-uuid",
|
||||
"name": "Process Name",
|
||||
"version": "1.0.0",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-1",
|
||||
"type": "start",
|
||||
"position": { "x": 100, "y": 100 },
|
||||
"data": {
|
||||
"label": "Start Process",
|
||||
"backgroundColor": "#dcfce7",
|
||||
"shape": "circle"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "edge-1",
|
||||
"source": "node-1",
|
||||
"target": "node-2",
|
||||
"type": "default"
|
||||
}
|
||||
],
|
||||
"variables": {},
|
||||
"settings": {}
|
||||
}
|
||||
```
|
||||
|
||||
### **Node Data Structure**
|
||||
```javascript
|
||||
// Standard node data properties
|
||||
{
|
||||
id: 'unique-node-id',
|
||||
type: 'form|api|script|gateway|etc',
|
||||
position: { x: number, y: number },
|
||||
data: {
|
||||
// Universal properties
|
||||
label: 'Node Label',
|
||||
shape: 'rectangle|circle|hexagon|etc',
|
||||
backgroundColor: '#ffffff',
|
||||
borderColor: '#dddddd',
|
||||
textColor: '#000000',
|
||||
|
||||
// Type-specific properties
|
||||
formId: 'form-uuid', // For form nodes
|
||||
apiEndpoint: 'url', // For API nodes
|
||||
scriptCode: 'javascript', // For script nodes
|
||||
conditions: [], // For gateway nodes
|
||||
|
||||
// Custom properties
|
||||
customProperties: {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Common Issues & Solutions
|
||||
|
||||
### **1. Nodes Not Displaying in Production**
|
||||
**Problem**: Custom nodes show as empty boxes in production build.
|
||||
**Solution**: Use file-based components with `markRaw()` wrapper.
|
||||
|
||||
```javascript
|
||||
// ❌ Wrong - breaks in production
|
||||
const nodeTypes = {
|
||||
'form': {
|
||||
template: '<div>Form Node</div>'
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Correct - works in production
|
||||
import FormNode from './custom/FormNode.vue'
|
||||
const nodeTypes = {
|
||||
'form': markRaw(FormNode)
|
||||
}
|
||||
```
|
||||
|
||||
### **2. Shapes Not Displaying**
|
||||
**Problem**: Node shapes remain rectangular despite shape selection.
|
||||
**Solution**: Ensure shape CSS overrides base styles with `!important`.
|
||||
|
||||
```css
|
||||
/* ❌ Wrong - base styles override */
|
||||
.custom-node.shape-hexagon {
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* ✅ Correct - overrides base styles */
|
||||
.custom-node.shape-hexagon {
|
||||
border-radius: 0 !important;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
```
|
||||
|
||||
### **3. Handle Connection Issues**
|
||||
**Problem**: Nodes can't connect to each other.
|
||||
**Solution**: Ensure handles have proper `type` and `position`.
|
||||
|
||||
```vue
|
||||
<!-- ✅ Correct handle setup -->
|
||||
<Handle
|
||||
type="target"
|
||||
:position="Position.Left"
|
||||
:isConnectable="true"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
:position="Position.Right"
|
||||
:isConnectable="true"
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Deployment Checklist
|
||||
|
||||
### **Production Readiness**
|
||||
- [ ] All custom nodes are file-based components (not inline)
|
||||
- [ ] Node types registered with `markRaw()` wrapper
|
||||
- [ ] Global styles injected via plugin system
|
||||
- [ ] CSS variables used for dynamic theming
|
||||
- [ ] Shape overrides use `!important` declarations
|
||||
- [ ] Handle connections properly configured
|
||||
- [ ] Error boundaries implemented for Vue Flow errors
|
||||
- [ ] Process validation before save/publish
|
||||
|
||||
### **Performance Optimization**
|
||||
- [ ] Large node sets use virtual scrolling
|
||||
- [ ] Debounced auto-save functionality
|
||||
- [ ] Optimized re-renders with `computed` properties
|
||||
- [ ] Proper Vue 3 reactivity patterns
|
||||
- [ ] Memory leak prevention with proper cleanup
|
||||
|
||||
---
|
||||
|
||||
## 📚 Key Dependencies
|
||||
|
||||
```json
|
||||
{
|
||||
"@vue-flow/core": "^1.x.x",
|
||||
"@vue-flow/background": "^1.x.x",
|
||||
"@vue-flow/controls": "^1.x.x",
|
||||
"@vue-flow/minimap": "^1.x.x",
|
||||
"vue": "^3.x.x",
|
||||
"nuxt": "^3.x.x",
|
||||
"@pinia/nuxt": "^0.x.x",
|
||||
"@formkit/nuxt": "^1.x.x",
|
||||
"tailwindcss": "^3.x.x"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 Reference Links
|
||||
|
||||
- **Vue Flow Documentation**: https://vueflow.dev/
|
||||
- **Vue 3 Composition API**: https://vuejs.org/guide/
|
||||
- **Nuxt 3**: https://nuxt.com/docs
|
||||
- **Pinia Store**: https://pinia.vuejs.org/
|
||||
- **Tailwind CSS**: https://tailwindcss.com/docs
|
||||
|
||||
---
|
||||
|
||||
**📝 Last Updated**: Current as of the Vue Flow custom nodes migration and shape system implementation.
|
||||
|
||||
**🔄 Version**: 1.0 - Comprehensive system documentation
|
||||
|
||||
**👥 For New Team Members**: This document contains everything needed to understand and work with the Vue Flow Process Builder system. Start with the "File Structure" section and follow the implementation patterns for any new features.
|
@ -275,7 +275,7 @@ const components = [
|
||||
{
|
||||
type: 'gateway',
|
||||
label: 'Decision Point',
|
||||
icon: 'call_split',
|
||||
icon: 'call-split',
|
||||
iconColor: 'text-orange-500',
|
||||
data: { description: 'Decision point for branching the workflow', conditions: [], shape: 'diamond', backgroundColor: '#fff7ed', borderColor: '#f97316', textColor: '#c2410c' }
|
||||
},
|
||||
@ -454,7 +454,7 @@ const getNodeIcon = (nodeType) => {
|
||||
const iconMap = {
|
||||
'form': 'description',
|
||||
'api': 'api',
|
||||
'gateway': 'call_split',
|
||||
'gateway': 'call-split',
|
||||
'script': 'code',
|
||||
'business-rule': 'rule',
|
||||
'notification': 'notifications',
|
||||
|
@ -192,7 +192,7 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
||||
} else if (nodeId.includes('gateway-')) {
|
||||
nodeType = 'gateway';
|
||||
label = 'Decision Point';
|
||||
icon = 'call_split';
|
||||
icon = 'call-split';
|
||||
} else if (nodeId.includes('script-')) {
|
||||
nodeType = 'script';
|
||||
label = 'Script';
|
||||
|
Loading…
x
Reference in New Issue
Block a user