Implement Node Validation System for Process Flow
- Introduced a comprehensive node validation system in the process flow builder, enhancing the user experience by providing real-time feedback on node configurations and process integrity. - Added `useNodeValidation` composable to manage validation logic, including checks for required nodes, configuration completeness, and flow logic. - Integrated validation indicators in node components (ApiNode, FormNode, GatewayNode, ScriptNode) to visually represent validation issues. - Created `ValidationIndicator` and `ValidationTooltip` components for displaying validation statuses and detailed messages. - Updated `ProcessFlowCanvas.vue` to trigger validation on node and edge changes, ensuring immediate feedback during process design. - Enhanced `processBuilder` store to manage validation results and summary statistics, allowing for a centralized validation state. - Documented the validation system implementation plan to guide future enhancements and user training.
This commit is contained in:
parent
4668abd062
commit
ed00664882
@ -1,8 +1,11 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(mkdir:*)"
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(yarn dev:*)",
|
||||
"Bash(yarn lint:*)",
|
||||
"Bash(yarn build:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import { Background } from "@vue-flow/background";
|
||||
import { Controls } from "@vue-flow/controls";
|
||||
import { MiniMap } from "@vue-flow/minimap";
|
||||
import InteractiveArrowEdge from "./InteractiveArrowEdge.vue";
|
||||
import { useNodeValidation } from '~/composables/useNodeValidation';
|
||||
// Import all file-based custom node components
|
||||
import StartNode from "~/components/process-flow/custom/StartNode.vue";
|
||||
import EndNode from "~/components/process-flow/custom/EndNode.vue";
|
||||
@ -96,6 +97,16 @@ const emit = defineEmits([
|
||||
// Get the flow instance
|
||||
const { flowInstance } = useVueFlow();
|
||||
|
||||
// Initialize validation system
|
||||
const {
|
||||
validateProcess,
|
||||
validationResults,
|
||||
isValidating,
|
||||
overallValidationStatus,
|
||||
validationSummary,
|
||||
clearValidation
|
||||
} = useNodeValidation();
|
||||
|
||||
// Initialize Vue Flow
|
||||
const {
|
||||
nodes,
|
||||
@ -734,6 +745,27 @@ onEdgesChange((changes) => {
|
||||
emit("edgesChange", changes, edges.value);
|
||||
});
|
||||
|
||||
// Watch for changes to nodes and edges to trigger validation
|
||||
watch(
|
||||
() => [nodes.value, edges.value],
|
||||
([currentNodes, currentEdges]) => {
|
||||
if (currentNodes && currentNodes.length > 0) {
|
||||
// Debounce validation to avoid excessive re-computation
|
||||
clearTimeout(validationTimeout.value);
|
||||
validationTimeout.value = setTimeout(() => {
|
||||
validateProcess(currentNodes, currentEdges || []);
|
||||
}, 300);
|
||||
} else {
|
||||
// Clear validation if no nodes
|
||||
clearValidation();
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Timeout for debouncing validation
|
||||
const validationTimeout = ref(null);
|
||||
|
||||
// Handle new connections
|
||||
const handleConnect = (connection) => {
|
||||
if (!connection.source || !connection.target) return;
|
||||
@ -1928,6 +1960,55 @@ function fromObject(flowObject) {
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<!-- Validation Panel -->
|
||||
<Panel position="top-left" class="validation-panel">
|
||||
<div class="validation-panel-content">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="validation-title">Process Validation</h4>
|
||||
<div class="validation-status" :class="overallValidationStatus">
|
||||
<Icon
|
||||
:name="overallValidationStatus === 'error' ? 'material-symbols:error' :
|
||||
overallValidationStatus === 'warning' ? 'material-symbols:warning' :
|
||||
'material-symbols:check-circle'"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<span class="status-text">
|
||||
{{ overallValidationStatus === 'error' ? 'Errors' :
|
||||
overallValidationStatus === 'warning' ? 'Warnings' : 'Valid' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="validationSummary.totalIssues > 0" class="validation-summary">
|
||||
<div class="summary-stats">
|
||||
<span v-if="validationSummary.errors > 0" class="stat-item error">
|
||||
{{ validationSummary.errors }} error{{ validationSummary.errors > 1 ? 's' : '' }}
|
||||
</span>
|
||||
<span v-if="validationSummary.warnings > 0" class="stat-item warning">
|
||||
{{ validationSummary.warnings }} warning{{ validationSummary.warnings > 1 ? 's' : '' }}
|
||||
</span>
|
||||
<span v-if="validationSummary.infos > 0" class="stat-item info">
|
||||
{{ validationSummary.infos }} info{{ validationSummary.infos > 1 ? 's' : '' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="nodes.length > 0" class="validation-success">
|
||||
<Icon name="material-symbols:check-circle" class="w-4 h-4 text-green-500" />
|
||||
<span class="text-green-600 text-sm">Process validation passed</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="validation-empty">
|
||||
<span class="text-gray-500 text-sm">Add nodes to validate process</span>
|
||||
</div>
|
||||
|
||||
<div v-if="isValidating" class="validation-loading">
|
||||
<Icon name="material-symbols:refresh" class="w-4 h-4 animate-spin" />
|
||||
<span class="text-sm text-gray-600">Validating...</span>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</VueFlow>
|
||||
</div>
|
||||
</template>
|
||||
@ -2313,4 +2394,104 @@ function fromObject(flowObject) {
|
||||
.help-list li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Validation Panel Styles */
|
||||
.validation-panel {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.validation-panel-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
padding: 12px;
|
||||
min-width: 220px;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.validation-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.validation-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.validation-status.error {
|
||||
color: #dc2626;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.validation-status.warning {
|
||||
color: #d97706;
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.validation-status.valid {
|
||||
color: #059669;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.validation-summary {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.summary-stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-item.error {
|
||||
color: #dc2626;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.stat-item.warning {
|
||||
color: #d97706;
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.stat-item.info {
|
||||
color: #2563eb;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.validation-success {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.validation-empty {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.validation-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 10px;
|
||||
}
|
||||
</style>
|
||||
|
175
components/process-flow/ValidationIndicator.vue
Normal file
175
components/process-flow/ValidationIndicator.vue
Normal file
@ -0,0 +1,175 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import ValidationTooltip from './ValidationTooltip.vue'
|
||||
|
||||
const props = defineProps({
|
||||
nodeId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
validationIssues: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
showTooltip: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
// Computed properties for validation display
|
||||
const hasIssues = computed(() => props.validationIssues.length > 0)
|
||||
|
||||
const validationSeverity = computed(() => {
|
||||
if (!hasIssues.value) return null
|
||||
|
||||
const hasError = props.validationIssues.some(issue => issue.severity === 'error')
|
||||
const hasWarning = props.validationIssues.some(issue => issue.severity === 'warning')
|
||||
|
||||
if (hasError) return 'error'
|
||||
if (hasWarning) return 'warning'
|
||||
return 'info'
|
||||
})
|
||||
|
||||
const validationIcon = computed(() => {
|
||||
switch (validationSeverity.value) {
|
||||
case 'error': return 'material-symbols:error'
|
||||
case 'warning': return 'material-symbols:warning'
|
||||
case 'info': return 'material-symbols:info'
|
||||
default: return null
|
||||
}
|
||||
})
|
||||
|
||||
const validationTooltip = computed(() => {
|
||||
if (!hasIssues.value) return null
|
||||
|
||||
const issuesByType = {
|
||||
error: props.validationIssues.filter(i => i.severity === 'error'),
|
||||
warning: props.validationIssues.filter(i => i.severity === 'warning'),
|
||||
info: props.validationIssues.filter(i => i.severity === 'info')
|
||||
}
|
||||
|
||||
const lines = []
|
||||
|
||||
if (issuesByType.error.length > 0) {
|
||||
lines.push('ERRORS:')
|
||||
issuesByType.error.forEach(issue => lines.push(`• ${issue.message}`))
|
||||
}
|
||||
|
||||
if (issuesByType.warning.length > 0) {
|
||||
if (lines.length > 0) lines.push('')
|
||||
lines.push('WARNINGS:')
|
||||
issuesByType.warning.forEach(issue => lines.push(`• ${issue.message}`))
|
||||
}
|
||||
|
||||
if (issuesByType.info.length > 0) {
|
||||
if (lines.length > 0) lines.push('')
|
||||
lines.push('INFO:')
|
||||
issuesByType.info.forEach(issue => lines.push(`• ${issue.message}`))
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
})
|
||||
|
||||
const severityClass = computed(() => {
|
||||
return `validation-${validationSeverity.value}`
|
||||
})
|
||||
|
||||
const severityColorClass = computed(() => {
|
||||
switch (validationSeverity.value) {
|
||||
case 'error': return 'text-red-500'
|
||||
case 'warning': return 'text-yellow-500'
|
||||
case 'info': return 'text-blue-500'
|
||||
default: return ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ValidationTooltip
|
||||
v-if="hasIssues && showTooltip"
|
||||
:content="validationTooltip"
|
||||
position="top"
|
||||
>
|
||||
<div
|
||||
class="validation-indicator"
|
||||
:class="severityClass"
|
||||
>
|
||||
<i
|
||||
:class="[validationIcon, severityColorClass]"
|
||||
class="validation-icon"
|
||||
></i>
|
||||
|
||||
<!-- Badge with issue count -->
|
||||
<span
|
||||
v-if="validationIssues.length > 1"
|
||||
class="validation-badge"
|
||||
:class="severityClass"
|
||||
>
|
||||
{{ validationIssues.length }}
|
||||
</span>
|
||||
</div>
|
||||
</ValidationTooltip>
|
||||
|
||||
<!-- Fallback without tooltip -->
|
||||
<div
|
||||
v-else-if="hasIssues"
|
||||
class="validation-indicator"
|
||||
:class="severityClass"
|
||||
>
|
||||
<i
|
||||
:class="[validationIcon, severityColorClass]"
|
||||
class="validation-icon"
|
||||
></i>
|
||||
|
||||
<!-- Badge with issue count -->
|
||||
<span
|
||||
v-if="validationIssues.length > 1"
|
||||
class="validation-badge"
|
||||
:class="severityClass"
|
||||
>
|
||||
{{ validationIssues.length }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.validation-indicator {
|
||||
@apply absolute -top-2 -right-2 z-50 flex items-center justify-center;
|
||||
@apply w-6 h-6 rounded-full bg-white border-2 shadow-sm;
|
||||
pointer-events: auto;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.validation-indicator.validation-error {
|
||||
@apply border-red-500 bg-red-50;
|
||||
}
|
||||
|
||||
.validation-indicator.validation-warning {
|
||||
@apply border-yellow-500 bg-yellow-50;
|
||||
}
|
||||
|
||||
.validation-indicator.validation-info {
|
||||
@apply border-blue-500 bg-blue-50;
|
||||
}
|
||||
|
||||
.validation-icon {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
.validation-badge {
|
||||
@apply absolute -top-1 -right-1 w-4 h-4 rounded-full text-xs;
|
||||
@apply flex items-center justify-center text-white font-bold;
|
||||
@apply bg-red-500;
|
||||
}
|
||||
|
||||
.validation-badge.validation-warning {
|
||||
@apply bg-yellow-500;
|
||||
}
|
||||
|
||||
.validation-badge.validation-info {
|
||||
@apply bg-blue-500;
|
||||
}
|
||||
|
||||
/* Remove old tooltip styles - now using ValidationTooltip component */
|
||||
</style>
|
160
components/process-flow/ValidationTooltip.vue
Normal file
160
components/process-flow/ValidationTooltip.vue
Normal file
@ -0,0 +1,160 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
position: {
|
||||
type: String,
|
||||
default: 'top' // top, bottom, left, right
|
||||
}
|
||||
})
|
||||
|
||||
const showTooltip = ref(false)
|
||||
const tooltipRef = ref(null)
|
||||
|
||||
const positionClass = computed(() => {
|
||||
return `tooltip-${props.position}`
|
||||
})
|
||||
|
||||
const onMouseEnter = () => {
|
||||
showTooltip.value = true
|
||||
}
|
||||
|
||||
const onMouseLeave = () => {
|
||||
showTooltip.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="tooltip-container"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<div
|
||||
v-if="showTooltip && content"
|
||||
ref="tooltipRef"
|
||||
class="tooltip"
|
||||
:class="positionClass"
|
||||
>
|
||||
<div class="tooltip-content">
|
||||
<pre class="tooltip-text">{{ content }}</pre>
|
||||
</div>
|
||||
<div class="tooltip-arrow"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tooltip-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
background: #1f2937;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
max-width: 300px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tooltip-text {
|
||||
margin: 0;
|
||||
white-space: pre-line;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Position variants */
|
||||
.tooltip-top {
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tooltip-bottom {
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tooltip-left {
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.tooltip-right {
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Arrow styles */
|
||||
.tooltip-arrow {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.tooltip-top .tooltip-arrow {
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 6px solid #1f2937;
|
||||
}
|
||||
|
||||
.tooltip-bottom .tooltip-arrow {
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-bottom: 6px solid #1f2937;
|
||||
}
|
||||
|
||||
.tooltip-left .tooltip-arrow {
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-top: 6px solid transparent;
|
||||
border-bottom: 6px solid transparent;
|
||||
border-left: 6px solid #1f2937;
|
||||
}
|
||||
|
||||
.tooltip-right .tooltip-arrow {
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-top: 6px solid transparent;
|
||||
border-bottom: 6px solid transparent;
|
||||
border-right: 6px solid #1f2937;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.tooltip {
|
||||
max-width: 250px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,5 +1,7 @@
|
||||
<script setup>
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import { useProcessBuilderStore } from '~/stores/processBuilder'
|
||||
import ValidationIndicator from '../ValidationIndicator.vue'
|
||||
|
||||
// Define props that Vue Flow passes to custom nodes
|
||||
const props = defineProps([
|
||||
@ -10,6 +12,14 @@ const props = defineProps([
|
||||
'data' // Custom data object
|
||||
])
|
||||
|
||||
// Validation functionality - get from store
|
||||
const processStore = useProcessBuilderStore()
|
||||
|
||||
// Get validation issues for this node
|
||||
const validationIssues = computed(() => {
|
||||
return processStore.getNodeValidation(props.id)
|
||||
})
|
||||
|
||||
// Computed properties for node display
|
||||
const nodeLabel = computed(() => {
|
||||
return props.label || (props.data && props.data.label) || 'API Call'
|
||||
@ -104,6 +114,12 @@ const onClick = () => {
|
||||
:isValidConnection="() => true"
|
||||
/>
|
||||
|
||||
<!-- Validation Indicator -->
|
||||
<ValidationIndicator
|
||||
:node-id="id"
|
||||
:validation-issues="validationIssues"
|
||||
/>
|
||||
|
||||
<div class="custom-node-content">
|
||||
<div class="flex items-center mb-1">
|
||||
<div class="custom-node-icon">
|
||||
|
@ -1,5 +1,7 @@
|
||||
<script setup>
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import { useProcessBuilderStore } from '~/stores/processBuilder'
|
||||
import ValidationIndicator from '../ValidationIndicator.vue'
|
||||
|
||||
// Define props that Vue Flow passes to custom nodes
|
||||
const props = defineProps([
|
||||
@ -10,6 +12,14 @@ const props = defineProps([
|
||||
'data' // Custom data object
|
||||
])
|
||||
|
||||
// Validation functionality - get from store
|
||||
const processStore = useProcessBuilderStore()
|
||||
|
||||
// Get validation issues for this node
|
||||
const validationIssues = computed(() => {
|
||||
return processStore.getNodeValidation(props.id)
|
||||
})
|
||||
|
||||
// Computed properties for node display
|
||||
const nodeLabel = computed(() => {
|
||||
return props.label || (props.data && props.data.label) || 'Form Task'
|
||||
@ -100,6 +110,12 @@ const onClick = () => {
|
||||
:isValidConnection="() => true"
|
||||
/>
|
||||
|
||||
<!-- Validation Indicator -->
|
||||
<ValidationIndicator
|
||||
:node-id="id"
|
||||
:validation-issues="validationIssues"
|
||||
/>
|
||||
|
||||
<div class="custom-node-content">
|
||||
<div class="flex items-center mb-1">
|
||||
<div class="custom-node-icon">
|
||||
|
@ -1,5 +1,7 @@
|
||||
<script setup>
|
||||
import { Handle, Position } from '@vue-flow/core'
|
||||
import { useProcessBuilderStore } from '~/stores/processBuilder'
|
||||
import ValidationIndicator from '../ValidationIndicator.vue'
|
||||
|
||||
// Define props that Vue Flow passes to custom nodes
|
||||
const props = defineProps([
|
||||
@ -10,6 +12,14 @@ const props = defineProps([
|
||||
'data' // Custom data object
|
||||
])
|
||||
|
||||
// Validation functionality
|
||||
const processStore = useProcessBuilderStore()
|
||||
|
||||
// Get validation issues for this node
|
||||
const validationIssues = computed(() => {
|
||||
return processStore.getNodeValidation(props.id)
|
||||
})
|
||||
|
||||
// Computed properties for node display
|
||||
const nodeLabel = computed(() => {
|
||||
return props.label || (props.data && props.data.label) || 'Decision Point'
|
||||
@ -119,6 +129,12 @@ const onClick = () => {
|
||||
:isValidConnection="() => true"
|
||||
/>
|
||||
|
||||
<!-- Validation Indicator -->
|
||||
<ValidationIndicator
|
||||
:node-id="id"
|
||||
:validation-issues="validationIssues"
|
||||
/>
|
||||
|
||||
<div class="custom-node-content">
|
||||
<div class="material-icons">call_split</div>
|
||||
<div class="custom-node-title" :title="nodeLabel">{{ nodeLabel }}</div>
|
||||
|
@ -1,5 +1,7 @@
|
||||
<script setup>
|
||||
import { Handle, Position } from "@vue-flow/core";
|
||||
import { useProcessBuilderStore } from '~/stores/processBuilder'
|
||||
import ValidationIndicator from '../ValidationIndicator.vue'
|
||||
|
||||
// Define props that Vue Flow passes to custom nodes
|
||||
const props = defineProps([
|
||||
@ -10,6 +12,14 @@ const props = defineProps([
|
||||
"data", // Custom data object
|
||||
]);
|
||||
|
||||
// Validation functionality
|
||||
const processStore = useProcessBuilderStore()
|
||||
|
||||
// Get validation issues for this node
|
||||
const validationIssues = computed(() => {
|
||||
return processStore.getNodeValidation(props.id)
|
||||
})
|
||||
|
||||
// Computed properties for node display
|
||||
const nodeLabel = computed(() => {
|
||||
return props.label || (props.data && props.data.label) || "Script";
|
||||
@ -100,6 +110,12 @@ const onClick = () => {
|
||||
:isValidConnection="() => true"
|
||||
/>
|
||||
|
||||
<!-- Validation Indicator -->
|
||||
<ValidationIndicator
|
||||
:node-id="id"
|
||||
:validation-issues="validationIssues"
|
||||
/>
|
||||
|
||||
<div class="custom-node-content">
|
||||
<div class="flex items-center mb-1">
|
||||
<div class="custom-node-icon">
|
||||
|
589
composables/useNodeValidation.js
Normal file
589
composables/useNodeValidation.js
Normal file
@ -0,0 +1,589 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
/**
|
||||
* Node Validation Composable
|
||||
* Provides comprehensive validation for process flow nodes and overall process structure
|
||||
*/
|
||||
export function useNodeValidation() {
|
||||
// Validation state
|
||||
const validationResults = ref(new Map())
|
||||
const isValidating = ref(false)
|
||||
const lastValidationTime = ref(null)
|
||||
|
||||
// Validation configuration
|
||||
const validationConfig = ref({
|
||||
processStructure: {
|
||||
requireStartNode: true,
|
||||
requireEndNode: true,
|
||||
allowMultipleStarts: false
|
||||
},
|
||||
nodeConfiguration: {
|
||||
validateFormSelection: true,
|
||||
validateApiUrls: true,
|
||||
validateGatewayPaths: true,
|
||||
validateScriptContent: true,
|
||||
validateNotificationContent: true
|
||||
},
|
||||
flowLogic: {
|
||||
checkReachability: true,
|
||||
checkDeadEnds: true
|
||||
}
|
||||
})
|
||||
|
||||
// Validation severity levels
|
||||
const SEVERITY = {
|
||||
ERROR: 'error',
|
||||
WARNING: 'warning',
|
||||
INFO: 'info'
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate individual node configuration
|
||||
*/
|
||||
const validateNode = (node) => {
|
||||
const issues = []
|
||||
|
||||
if (!node || !node.type) {
|
||||
issues.push({
|
||||
type: 'configuration',
|
||||
severity: SEVERITY.ERROR,
|
||||
message: 'Node is missing or has no type',
|
||||
nodeId: node?.id || 'unknown'
|
||||
})
|
||||
return issues
|
||||
}
|
||||
|
||||
// Validate based on node type
|
||||
switch (node.type) {
|
||||
case 'form':
|
||||
validateFormNode(node, issues)
|
||||
break
|
||||
case 'api':
|
||||
validateApiNode(node, issues)
|
||||
break
|
||||
case 'gateway':
|
||||
validateGatewayNode(node, issues)
|
||||
break
|
||||
case 'script':
|
||||
validateScriptNode(node, issues)
|
||||
break
|
||||
case 'notification':
|
||||
validateNotificationNode(node, issues)
|
||||
break
|
||||
case 'businessRule':
|
||||
validateBusinessRuleNode(node, issues)
|
||||
break
|
||||
case 'html':
|
||||
validateHtmlNode(node, issues)
|
||||
break
|
||||
case 'subprocess':
|
||||
validateSubprocessNode(node, issues)
|
||||
break
|
||||
case 'start':
|
||||
case 'end':
|
||||
// Start and end nodes have minimal validation requirements
|
||||
validateBasicNode(node, issues)
|
||||
break
|
||||
}
|
||||
|
||||
return issues
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate form node configuration
|
||||
*/
|
||||
const validateFormNode = (node, issues) => {
|
||||
if (validationConfig.value.nodeConfiguration.validateFormSelection) {
|
||||
if (!node.data?.formId) {
|
||||
issues.push({
|
||||
type: 'configuration',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'Form node has no form selected',
|
||||
nodeId: node.id,
|
||||
recommendation: 'Select a form for this node to collect user input'
|
||||
})
|
||||
}
|
||||
|
||||
if (!node.data?.formName) {
|
||||
issues.push({
|
||||
type: 'configuration',
|
||||
severity: SEVERITY.INFO,
|
||||
message: 'Form node has no display name',
|
||||
nodeId: node.id,
|
||||
recommendation: 'Add a display name for better process readability'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate API node configuration
|
||||
*/
|
||||
const validateApiNode = (node, issues) => {
|
||||
if (validationConfig.value.nodeConfiguration.validateApiUrls) {
|
||||
if (!node.data?.apiUrl || node.data.apiUrl.trim() === '') {
|
||||
issues.push({
|
||||
type: 'configuration',
|
||||
severity: SEVERITY.ERROR,
|
||||
message: 'API node has no URL configured',
|
||||
nodeId: node.id,
|
||||
recommendation: 'Configure the API endpoint URL'
|
||||
})
|
||||
}
|
||||
|
||||
if (!node.data?.apiMethod) {
|
||||
issues.push({
|
||||
type: 'configuration',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'API node has no HTTP method specified',
|
||||
nodeId: node.id,
|
||||
recommendation: 'Specify the HTTP method (GET, POST, etc.)'
|
||||
})
|
||||
}
|
||||
|
||||
// Validate URL format if present
|
||||
if (node.data?.apiUrl) {
|
||||
try {
|
||||
new URL(node.data.apiUrl)
|
||||
} catch {
|
||||
issues.push({
|
||||
type: 'configuration',
|
||||
severity: SEVERITY.ERROR,
|
||||
message: 'API node has invalid URL format',
|
||||
nodeId: node.id,
|
||||
recommendation: 'Ensure the URL is properly formatted (e.g., https://example.com/api)'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate gateway node configuration
|
||||
*/
|
||||
const validateGatewayNode = (node, issues) => {
|
||||
if (validationConfig.value.nodeConfiguration.validateGatewayPaths) {
|
||||
const conditions = node.data?.conditions || []
|
||||
|
||||
if (conditions.length === 0) {
|
||||
issues.push({
|
||||
type: 'configuration',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'Gateway node has no conditions defined',
|
||||
nodeId: node.id,
|
||||
recommendation: 'Add conditions to control process flow branching'
|
||||
})
|
||||
}
|
||||
|
||||
// Check for incomplete conditions
|
||||
conditions.forEach((condition, index) => {
|
||||
if (!condition.variable || !condition.operator || condition.value === undefined) {
|
||||
issues.push({
|
||||
type: 'configuration',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: `Gateway condition ${index + 1} is incomplete`,
|
||||
nodeId: node.id,
|
||||
recommendation: 'Complete all condition fields (variable, operator, value)'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate script node configuration
|
||||
*/
|
||||
const validateScriptNode = (node, issues) => {
|
||||
if (validationConfig.value.nodeConfiguration.validateScriptContent) {
|
||||
if (!node.data?.code || node.data.code.trim() === '') {
|
||||
issues.push({
|
||||
type: 'configuration',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'Script node has no code defined',
|
||||
nodeId: node.id,
|
||||
recommendation: 'Add JavaScript code to execute in this node'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate notification node configuration
|
||||
*/
|
||||
const validateNotificationNode = (node, issues) => {
|
||||
if (validationConfig.value.nodeConfiguration.validateNotificationContent) {
|
||||
if (!node.data?.content || node.data.content.trim() === '') {
|
||||
issues.push({
|
||||
type: 'configuration',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'Notification node has no content',
|
||||
nodeId: node.id,
|
||||
recommendation: 'Add message content for the notification'
|
||||
})
|
||||
}
|
||||
|
||||
if (!node.data?.notificationType) {
|
||||
issues.push({
|
||||
type: 'configuration',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'Notification node has no type specified',
|
||||
nodeId: node.id,
|
||||
recommendation: 'Specify notification type (email, SMS, in-app)'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate business rule node configuration
|
||||
*/
|
||||
const validateBusinessRuleNode = (node, issues) => {
|
||||
const rules = node.data?.rules || []
|
||||
|
||||
if (rules.length === 0) {
|
||||
issues.push({
|
||||
type: 'configuration',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'Business rule node has no rules defined',
|
||||
nodeId: node.id,
|
||||
recommendation: 'Add business rules to evaluate conditions'
|
||||
})
|
||||
}
|
||||
|
||||
rules.forEach((rule, index) => {
|
||||
if (!rule.variable || !rule.operator || rule.value === undefined) {
|
||||
issues.push({
|
||||
type: 'configuration',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: `Business rule ${index + 1} is incomplete`,
|
||||
nodeId: node.id,
|
||||
recommendation: 'Complete all rule fields (variable, operator, value)'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate HTML node configuration
|
||||
*/
|
||||
const validateHtmlNode = (node, issues) => {
|
||||
if (!node.data?.htmlContent || node.data.htmlContent.trim() === '') {
|
||||
issues.push({
|
||||
type: 'configuration',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'HTML node has no content',
|
||||
nodeId: node.id,
|
||||
recommendation: 'Add HTML content to display in this node'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate subprocess node configuration
|
||||
*/
|
||||
const validateSubprocessNode = (node, issues) => {
|
||||
if (!node.data?.subprocessId) {
|
||||
issues.push({
|
||||
type: 'configuration',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'Subprocess node has no subprocess selected',
|
||||
nodeId: node.id,
|
||||
recommendation: 'Select a subprocess to execute'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate basic node properties
|
||||
*/
|
||||
const validateBasicNode = (node, issues) => {
|
||||
if (!node.label || node.label.trim() === '') {
|
||||
issues.push({
|
||||
type: 'configuration',
|
||||
severity: SEVERITY.INFO,
|
||||
message: 'Node has no label',
|
||||
nodeId: node.id,
|
||||
recommendation: 'Add a descriptive label for better readability'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate overall process structure
|
||||
*/
|
||||
const validateProcessStructure = (nodes, edges) => {
|
||||
const issues = []
|
||||
|
||||
// Check for start nodes
|
||||
const startNodes = nodes.filter(node => node.type === 'start')
|
||||
if (validationConfig.value.processStructure.requireStartNode) {
|
||||
if (startNodes.length === 0) {
|
||||
issues.push({
|
||||
type: 'structure',
|
||||
severity: SEVERITY.ERROR,
|
||||
message: 'Process has no start node',
|
||||
nodeId: null,
|
||||
recommendation: 'Add a start node to define where the process begins'
|
||||
})
|
||||
} else if (startNodes.length > 1 && !validationConfig.value.processStructure.allowMultipleStarts) {
|
||||
issues.push({
|
||||
type: 'structure',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: `Process has ${startNodes.length} start nodes`,
|
||||
nodeId: null,
|
||||
recommendation: 'Consider having only one start node for clearer process flow'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check for end nodes
|
||||
const endNodes = nodes.filter(node => node.type === 'end')
|
||||
if (validationConfig.value.processStructure.requireEndNode) {
|
||||
if (endNodes.length === 0) {
|
||||
issues.push({
|
||||
type: 'structure',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'Process has no end node',
|
||||
nodeId: null,
|
||||
recommendation: 'Add an end node to properly conclude the process'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return issues
|
||||
}
|
||||
|
||||
/**
|
||||
* Find nodes that are unreachable from start nodes
|
||||
*/
|
||||
const findUnreachableNodes = (nodes, edges) => {
|
||||
if (!validationConfig.value.flowLogic.checkReachability) return []
|
||||
|
||||
const startNodes = nodes.filter(n => n.type === 'start')
|
||||
if (startNodes.length === 0) return []
|
||||
|
||||
const reachable = new Set()
|
||||
const queue = [...startNodes.map(n => n.id)]
|
||||
|
||||
while (queue.length > 0) {
|
||||
const nodeId = queue.shift()
|
||||
if (reachable.has(nodeId)) continue
|
||||
|
||||
reachable.add(nodeId)
|
||||
const outgoingEdges = edges.filter(e => e.source === nodeId)
|
||||
queue.push(...outgoingEdges.map(e => e.target))
|
||||
}
|
||||
|
||||
const unreachableNodes = nodes.filter(n => !reachable.has(n.id))
|
||||
return unreachableNodes.map(node => ({
|
||||
type: 'flow',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'Node is unreachable from start',
|
||||
nodeId: node.id,
|
||||
recommendation: 'Connect this node to the main process flow or remove it'
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Find nodes that cannot reach any end node (dead ends)
|
||||
*/
|
||||
const findDeadEnds = (nodes, edges) => {
|
||||
if (!validationConfig.value.flowLogic.checkDeadEnds) return []
|
||||
|
||||
const endNodes = nodes.filter(n => n.type === 'end')
|
||||
if (endNodes.length === 0) return []
|
||||
|
||||
const canReachEnd = new Set()
|
||||
const queue = [...endNodes.map(n => n.id)]
|
||||
|
||||
while (queue.length > 0) {
|
||||
const nodeId = queue.shift()
|
||||
if (canReachEnd.has(nodeId)) continue
|
||||
|
||||
canReachEnd.add(nodeId)
|
||||
const incomingEdges = edges.filter(e => e.target === nodeId)
|
||||
queue.push(...incomingEdges.map(e => e.source))
|
||||
}
|
||||
|
||||
const deadEndNodes = nodes.filter(n => n.type !== 'end' && !canReachEnd.has(n.id))
|
||||
return deadEndNodes.map(node => ({
|
||||
type: 'flow',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'Node cannot reach any end node',
|
||||
nodeId: node.id,
|
||||
recommendation: 'Ensure this node has a path to an end node'
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate entire process (nodes + edges + structure)
|
||||
*/
|
||||
const validateProcess = (nodes, edges) => {
|
||||
isValidating.value = true
|
||||
const allIssues = []
|
||||
|
||||
try {
|
||||
// Clear previous results
|
||||
validationResults.value.clear()
|
||||
|
||||
// Validate process structure
|
||||
const structureIssues = validateProcessStructure(nodes, edges)
|
||||
allIssues.push(...structureIssues)
|
||||
|
||||
// Validate individual nodes
|
||||
nodes.forEach(node => {
|
||||
const nodeIssues = validateNode(node)
|
||||
allIssues.push(...nodeIssues)
|
||||
})
|
||||
|
||||
// Validate flow logic
|
||||
const unreachableIssues = findUnreachableNodes(nodes, edges)
|
||||
const deadEndIssues = findDeadEnds(nodes, edges)
|
||||
allIssues.push(...unreachableIssues)
|
||||
allIssues.push(...deadEndIssues)
|
||||
|
||||
// Group issues by node
|
||||
allIssues.forEach(issue => {
|
||||
const nodeId = issue.nodeId || 'process'
|
||||
if (!validationResults.value.has(nodeId)) {
|
||||
validationResults.value.set(nodeId, [])
|
||||
}
|
||||
validationResults.value.get(nodeId).push(issue)
|
||||
})
|
||||
|
||||
lastValidationTime.value = new Date()
|
||||
} finally {
|
||||
isValidating.value = false
|
||||
}
|
||||
|
||||
return allIssues
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation results for a specific node
|
||||
*/
|
||||
const getNodeValidation = (nodeId) => {
|
||||
return validationResults.value.get(nodeId) || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation icon for a node
|
||||
*/
|
||||
const getValidationIcon = (nodeId) => {
|
||||
const issues = getNodeValidation(nodeId)
|
||||
if (issues.length === 0) return null
|
||||
|
||||
const hasError = issues.some(issue => issue.severity === SEVERITY.ERROR)
|
||||
const hasWarning = issues.some(issue => issue.severity === SEVERITY.WARNING)
|
||||
|
||||
if (hasError) return 'material-symbols:error'
|
||||
if (hasWarning) return 'material-symbols:warning'
|
||||
return 'material-symbols:info'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation tooltip content for a node
|
||||
*/
|
||||
const getValidationTooltip = (nodeId) => {
|
||||
const issues = getNodeValidation(nodeId)
|
||||
if (issues.length === 0) return null
|
||||
|
||||
const lines = issues.map(issue => `• ${issue.message}`)
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node has validation issues
|
||||
*/
|
||||
const hasValidationIssues = (nodeId) => {
|
||||
return getNodeValidation(nodeId).length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Get overall validation status
|
||||
*/
|
||||
const overallValidationStatus = computed(() => {
|
||||
let hasErrors = false
|
||||
let hasWarnings = false
|
||||
|
||||
for (const issues of validationResults.value.values()) {
|
||||
for (const issue of issues) {
|
||||
if (issue.severity === SEVERITY.ERROR) hasErrors = true
|
||||
if (issue.severity === SEVERITY.WARNING) hasWarnings = true
|
||||
}
|
||||
}
|
||||
|
||||
if (hasErrors) return 'error'
|
||||
if (hasWarnings) return 'warning'
|
||||
return 'valid'
|
||||
})
|
||||
|
||||
/**
|
||||
* Get validation summary statistics
|
||||
*/
|
||||
const validationSummary = computed(() => {
|
||||
let totalIssues = 0
|
||||
let errors = 0
|
||||
let warnings = 0
|
||||
let infos = 0
|
||||
|
||||
for (const issues of validationResults.value.values()) {
|
||||
totalIssues += issues.length
|
||||
for (const issue of issues) {
|
||||
switch (issue.severity) {
|
||||
case SEVERITY.ERROR: errors++; break
|
||||
case SEVERITY.WARNING: warnings++; break
|
||||
case SEVERITY.INFO: infos++; break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalIssues,
|
||||
errors,
|
||||
warnings,
|
||||
infos,
|
||||
isValid: totalIssues === 0
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Clear validation results
|
||||
*/
|
||||
const clearValidation = () => {
|
||||
validationResults.value.clear()
|
||||
lastValidationTime.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Update validation configuration
|
||||
*/
|
||||
const updateValidationConfig = (newConfig) => {
|
||||
validationConfig.value = { ...validationConfig.value, ...newConfig }
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
validationResults,
|
||||
isValidating,
|
||||
lastValidationTime,
|
||||
validationConfig,
|
||||
|
||||
// Methods
|
||||
validateNode,
|
||||
validateProcess,
|
||||
getNodeValidation,
|
||||
getValidationIcon,
|
||||
getValidationTooltip,
|
||||
hasValidationIssues,
|
||||
clearValidation,
|
||||
updateValidationConfig,
|
||||
|
||||
// Computed
|
||||
overallValidationStatus,
|
||||
validationSummary,
|
||||
|
||||
// Constants
|
||||
SEVERITY
|
||||
}
|
||||
}
|
463
docs/node-validation-system-plan.md
Normal file
463
docs/node-validation-system-plan.md
Normal file
@ -0,0 +1,463 @@
|
||||
# Node Validation System Implementation Plan
|
||||
|
||||
## Overview
|
||||
This document outlines the implementation plan for a comprehensive node validation system in the Vue Flow process builder. The system will provide visual feedback, quality-of-life improvements, and help users create robust business processes by validating process integrity and configuration completeness.
|
||||
|
||||
## Problem Statement
|
||||
Users need guidance when building business processes to ensure:
|
||||
- Process flows are logically complete (have start/end nodes)
|
||||
- Nodes are properly configured (forms selected, APIs configured, etc.)
|
||||
- Variables are used correctly (no unused/undefined variables)
|
||||
- Gateway decision paths are complete
|
||||
- Process flows are reachable and don't have dead ends
|
||||
|
||||
## System Architecture
|
||||
|
||||
### Core Components
|
||||
1. **Node Validation Composable** (`composables/useNodeValidation.js`)
|
||||
2. **Validation Visual Indicators** (warning icons, tooltips)
|
||||
3. **Validation Store Integration** (with processBuilder store)
|
||||
4. **Real-time Validation Updates** (reactive validation on changes)
|
||||
|
||||
### Integration Points
|
||||
- **ProcessFlowCanvas.vue**: Add validation visual layer
|
||||
- **Node Components**: Add validation state display
|
||||
- **Process Builder Store**: Integrate validation state
|
||||
- **Variable Store**: Hook variable usage validation
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Foundation & Easy Wins 🟢 START HERE
|
||||
|
||||
#### 1.1 Core Validation Infrastructure
|
||||
**Files to Create:**
|
||||
- `composables/useNodeValidation.js`
|
||||
- `types/validation.js` (TypeScript definitions if needed)
|
||||
|
||||
**Key Features:**
|
||||
```javascript
|
||||
// Basic validation composable structure
|
||||
export function useNodeValidation() {
|
||||
const validationResults = ref(new Map())
|
||||
const isValidating = ref(false)
|
||||
|
||||
// Core validation functions
|
||||
const validateNode = (node) => { /* ... */ }
|
||||
const validateProcess = (nodes, edges) => { /* ... */ }
|
||||
const getValidationIcon = (nodeId) => { /* ... */ }
|
||||
const getValidationTooltip = (nodeId) => { /* ... */ }
|
||||
|
||||
return {
|
||||
validationResults,
|
||||
validateNode,
|
||||
validateProcess,
|
||||
// ... other methods
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 Basic Validation Rules (Quick Implementations)
|
||||
|
||||
**1.2.1 Process Structure Validation**
|
||||
- ✅ **Missing Start Node**: Process must have exactly one start node
|
||||
- ✅ **Missing End Node**: Process must have at least one end node
|
||||
- ✅ **Multiple Start Nodes**: Warn if more than one start node exists
|
||||
|
||||
**1.2.2 Node Configuration Validation**
|
||||
- ✅ **Form Node - No Form Selected**: `data.formId` is missing
|
||||
- ✅ **API Node - No URL**: `data.apiUrl` is empty/missing
|
||||
- ✅ **Gateway Node - Insufficient Paths**: Less than 2 outgoing edges
|
||||
- ✅ **Script Node - No Code**: `data.code` is empty
|
||||
- ✅ **Notification Node - No Content**: `data.content` is missing
|
||||
|
||||
#### 1.3 Visual Indicator System
|
||||
|
||||
**1.3.1 Node Warning Overlay**
|
||||
```vue
|
||||
<!-- Add to each node component -->
|
||||
<div v-if="hasValidationIssues" class="validation-warning">
|
||||
<Icon name="material-symbols:warning" class="warning-icon" />
|
||||
</div>
|
||||
```
|
||||
|
||||
**1.3.2 Hover Tooltips**
|
||||
```vue
|
||||
<!-- Tooltip component for validation messages -->
|
||||
<Tooltip v-if="validationMessage" :content="validationMessage">
|
||||
<Icon name="material-symbols:info" />
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
**1.3.3 Canvas Integration**
|
||||
- Add validation overlay to `ProcessFlowCanvas.vue`
|
||||
- Show process-level validation summary
|
||||
- Add validation toggle in canvas controls
|
||||
|
||||
### Phase 2: Advanced Validations 🟡 MEDIUM PRIORITY
|
||||
|
||||
#### 2.1 Variable Usage Analysis
|
||||
|
||||
**2.1.1 Unused Variables Detection**
|
||||
```javascript
|
||||
// Check variables defined but never referenced
|
||||
const findUnusedVariables = (processVariables, nodes) => {
|
||||
const usedVariables = new Set()
|
||||
|
||||
// Scan all nodes for variable references
|
||||
nodes.forEach(node => {
|
||||
scanNodeForVariableUsage(node, usedVariables)
|
||||
})
|
||||
|
||||
// Find variables not in usedVariables set
|
||||
return Object.keys(processVariables).filter(
|
||||
varName => !usedVariables.has(varName)
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**2.1.2 Undefined Variables Detection**
|
||||
```javascript
|
||||
// Check nodes referencing non-existent variables
|
||||
const findUndefinedVariables = (processVariables, nodes) => {
|
||||
const undefinedRefs = []
|
||||
|
||||
nodes.forEach(node => {
|
||||
const referencedVars = extractVariableReferences(node)
|
||||
referencedVars.forEach(varName => {
|
||||
if (!processVariables[varName]) {
|
||||
undefinedRefs.push({ nodeId: node.id, variable: varName })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return undefinedRefs
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 Flow Logic Validation
|
||||
|
||||
**2.2.1 Reachability Analysis**
|
||||
```javascript
|
||||
// Find nodes unreachable from start
|
||||
const findUnreachableNodes = (nodes, edges) => {
|
||||
const startNodes = nodes.filter(n => n.type === 'start')
|
||||
if (startNodes.length === 0) return nodes.map(n => n.id)
|
||||
|
||||
const reachable = new Set()
|
||||
const queue = [...startNodes.map(n => n.id)]
|
||||
|
||||
while (queue.length > 0) {
|
||||
const nodeId = queue.shift()
|
||||
if (reachable.has(nodeId)) continue
|
||||
|
||||
reachable.add(nodeId)
|
||||
const outgoingEdges = edges.filter(e => e.source === nodeId)
|
||||
queue.push(...outgoingEdges.map(e => e.target))
|
||||
}
|
||||
|
||||
return nodes.filter(n => !reachable.has(n.id)).map(n => n.id)
|
||||
}
|
||||
```
|
||||
|
||||
**2.2.2 Dead End Detection**
|
||||
```javascript
|
||||
// Find nodes with no path to end (except end nodes)
|
||||
const findDeadEnds = (nodes, edges) => {
|
||||
const endNodes = nodes.filter(n => n.type === 'end')
|
||||
if (endNodes.length === 0) return []
|
||||
|
||||
// Work backwards from end nodes
|
||||
const reachable = new Set()
|
||||
const queue = [...endNodes.map(n => n.id)]
|
||||
|
||||
while (queue.length > 0) {
|
||||
const nodeId = queue.shift()
|
||||
if (reachable.has(nodeId)) continue
|
||||
|
||||
reachable.add(nodeId)
|
||||
const incomingEdges = edges.filter(e => e.target === nodeId)
|
||||
queue.push(...incomingEdges.map(e => e.source))
|
||||
}
|
||||
|
||||
return nodes
|
||||
.filter(n => n.type !== 'end' && !reachable.has(n.id))
|
||||
.map(n => n.id)
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 Gateway Completeness Validation
|
||||
|
||||
**2.3.1 Condition Coverage**
|
||||
```javascript
|
||||
// Validate gateway has conditions for all paths
|
||||
const validateGatewayConditions = (gatewayNode, edges) => {
|
||||
const outgoingEdges = edges.filter(e => e.source === gatewayNode.id)
|
||||
const conditions = gatewayNode.data?.conditions || []
|
||||
|
||||
const issues = []
|
||||
|
||||
// Check if each outgoing edge has a corresponding condition
|
||||
outgoingEdges.forEach(edge => {
|
||||
const hasCondition = conditions.some(cond =>
|
||||
cond.output === edge.label || edge.label === gatewayNode.data?.defaultPath
|
||||
)
|
||||
|
||||
if (!hasCondition) {
|
||||
issues.push(`Edge to ${edge.target} has no condition defined`)
|
||||
}
|
||||
})
|
||||
|
||||
return issues
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Real-time Integration 🔵 ADVANCED
|
||||
|
||||
#### 3.1 Reactive Validation System
|
||||
|
||||
**3.1.1 Process Store Integration**
|
||||
```javascript
|
||||
// In processBuilder store
|
||||
watch(
|
||||
() => [currentProcess.value?.nodes, currentProcess.value?.edges],
|
||||
([nodes, edges]) => {
|
||||
if (nodes && edges) {
|
||||
validateProcess(nodes, edges)
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
```
|
||||
|
||||
**3.1.2 Variable Store Integration**
|
||||
```javascript
|
||||
// In variableStore
|
||||
watch(
|
||||
() => processVariables.value,
|
||||
(variables) => {
|
||||
validateVariableUsage(variables)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
```
|
||||
|
||||
#### 3.2 Canvas UI Integration
|
||||
|
||||
**3.2.1 Validation Summary Panel**
|
||||
```vue
|
||||
<Panel position="top-left" class="validation-panel">
|
||||
<div class="validation-summary">
|
||||
<h4>Process Validation</h4>
|
||||
<div class="validation-status" :class="overallStatus">
|
||||
<Icon :name="statusIcon" />
|
||||
<span>{{ statusText }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="issues.length > 0" class="validation-issues">
|
||||
<h5>Issues Found ({{ issues.length }})</h5>
|
||||
<ul>
|
||||
<li v-for="issue in issues" :key="issue.id" @click="focusIssue(issue)">
|
||||
<Icon name="material-symbols:warning" />
|
||||
{{ issue.message }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
```
|
||||
|
||||
**3.2.2 Node Validation Indicators**
|
||||
```vue
|
||||
<!-- In each node component -->
|
||||
<template>
|
||||
<div class="custom-node" :class="nodeClasses">
|
||||
<!-- Existing node content -->
|
||||
|
||||
<!-- Validation overlay -->
|
||||
<div v-if="validationIssues.length > 0" class="validation-overlay">
|
||||
<Tooltip :content="validationTooltip">
|
||||
<Icon
|
||||
name="material-symbols:warning"
|
||||
class="validation-warning-icon"
|
||||
:class="validationSeverity"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Phase 4: User Experience Enhancements 🟣 POLISH
|
||||
|
||||
#### 4.1 Validation Settings & Configuration
|
||||
|
||||
**4.1.1 Validation Rules Configuration**
|
||||
```javascript
|
||||
// User configurable validation rules
|
||||
const validationConfig = ref({
|
||||
processStructure: {
|
||||
requireStartNode: true,
|
||||
requireEndNode: true,
|
||||
allowMultipleStarts: false
|
||||
},
|
||||
nodeConfiguration: {
|
||||
validateFormSelection: true,
|
||||
validateApiUrls: true,
|
||||
validateGatewayPaths: true
|
||||
},
|
||||
variableUsage: {
|
||||
warnUnusedVariables: true,
|
||||
errorUndefinedVariables: true
|
||||
},
|
||||
flowLogic: {
|
||||
checkReachability: true,
|
||||
checkDeadEnds: true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### 4.2 Validation Reporting & Export
|
||||
|
||||
**4.2.1 Validation Report Generation**
|
||||
```javascript
|
||||
const generateValidationReport = () => {
|
||||
return {
|
||||
processId: currentProcess.value.id,
|
||||
processName: currentProcess.value.name,
|
||||
timestamp: new Date().toISOString(),
|
||||
overallStatus: getOverallValidationStatus(),
|
||||
summary: {
|
||||
totalNodes: nodes.length,
|
||||
validNodes: validNodes.length,
|
||||
invalidNodes: invalidNodes.length,
|
||||
warnings: warnings.length,
|
||||
errors: errors.length
|
||||
},
|
||||
issues: validationResults.value,
|
||||
recommendations: generateRecommendations()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Validation Rule Categories
|
||||
|
||||
### 1. Critical Errors (🔴 Red - Blocks Process Execution)
|
||||
- No start node
|
||||
- Unreachable nodes
|
||||
- Undefined variable references
|
||||
- Invalid API configurations
|
||||
|
||||
### 2. Warnings (🟡 Yellow - Should Fix)
|
||||
- No end node
|
||||
- Unused variables
|
||||
- Missing form selections
|
||||
- Incomplete gateway conditions
|
||||
|
||||
### 3. Suggestions (🔵 Blue - Best Practices)
|
||||
- Process naming conventions
|
||||
- Variable naming standards
|
||||
- Node organization recommendations
|
||||
- Performance optimizations
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Phase 1 Tasks
|
||||
- [ ] Create `useNodeValidation` composable
|
||||
- [ ] Implement basic process structure validation
|
||||
- [ ] Add node configuration validation
|
||||
- [ ] Create validation visual indicators
|
||||
- [ ] Add warning icons to nodes
|
||||
- [ ] Implement hover tooltips
|
||||
- [ ] Integrate with ProcessFlowCanvas
|
||||
|
||||
### Phase 2 Tasks
|
||||
- [ ] Variable usage analysis
|
||||
- [ ] Flow reachability validation
|
||||
- [ ] Dead end detection
|
||||
- [ ] Gateway condition validation
|
||||
- [ ] Advanced validation algorithms
|
||||
|
||||
### Phase 3 Tasks
|
||||
- [ ] Real-time validation updates
|
||||
- [ ] Process store integration
|
||||
- [ ] Variable store integration
|
||||
- [ ] Canvas UI enhancements
|
||||
|
||||
### Phase 4 Tasks
|
||||
- [ ] Validation settings configuration
|
||||
- [ ] Validation reporting system
|
||||
- [ ] Export functionality
|
||||
- [ ] Performance optimization
|
||||
|
||||
## Technical Considerations
|
||||
|
||||
### Performance
|
||||
- Use debounced validation to avoid excessive re-computation
|
||||
- Implement validation caching for unchanged nodes
|
||||
- Consider web workers for complex validation algorithms
|
||||
|
||||
### User Experience
|
||||
- Non-blocking validation (warnings, not errors)
|
||||
- Progressive disclosure of validation details
|
||||
- Quick-fix suggestions where possible
|
||||
- Keyboard shortcuts for validation navigation
|
||||
|
||||
### Extensibility
|
||||
- Plugin-based validation rule system
|
||||
- Custom validation rules API
|
||||
- Integration with external validation services
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- Individual validation rule functions
|
||||
- Validation composable methods
|
||||
- Edge case handling
|
||||
|
||||
### Integration Tests
|
||||
- Canvas integration validation
|
||||
- Store integration testing
|
||||
- Real-time update validation
|
||||
|
||||
### User Testing
|
||||
- Validation feedback effectiveness
|
||||
- Performance impact assessment
|
||||
- User workflow integration
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Quantitative
|
||||
- Reduction in invalid process deployments
|
||||
- Decrease in process runtime errors
|
||||
- User validation engagement rate
|
||||
- Performance impact measurement
|
||||
|
||||
### Qualitative
|
||||
- User feedback on validation helpfulness
|
||||
- Process builder confidence improvement
|
||||
- Reduced support requests for process issues
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Advanced Features
|
||||
- AI-powered process optimization suggestions
|
||||
- Best practice recommendations
|
||||
- Validation rule learning from user patterns
|
||||
- Integration with process analytics
|
||||
|
||||
### Enterprise Features
|
||||
- Organization-wide validation policies
|
||||
- Compliance validation rules
|
||||
- Audit trail for validation fixes
|
||||
- Team validation collaboration tools
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Start with Phase 1** - Basic validation infrastructure
|
||||
2. **Test thoroughly** - Ensure foundation is solid
|
||||
3. **Iterate quickly** - Get user feedback early
|
||||
4. **Build incrementally** - Add complexity gradually
|
||||
|
||||
This document serves as the master plan for implementing a comprehensive node validation system that will significantly improve the user experience and process quality in the Vue Flow process builder.
|
@ -12,7 +12,19 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
||||
historyIndex: -1,
|
||||
unsavedChanges: false,
|
||||
lastChangeDescription: '',
|
||||
isModifyingNodes: false // Flag to prevent interference during node operations
|
||||
isModifyingNodes: false, // Flag to prevent interference during node operations
|
||||
// Validation state
|
||||
validationResults: new Map(),
|
||||
validationSummary: {
|
||||
totalIssues: 0,
|
||||
errors: 0,
|
||||
warnings: 0,
|
||||
infos: 0,
|
||||
isValid: true
|
||||
},
|
||||
overallValidationStatus: 'valid', // 'valid', 'warning', 'error'
|
||||
lastValidationTime: null,
|
||||
validationEnabled: true
|
||||
}),
|
||||
|
||||
getters: {
|
||||
@ -58,6 +70,47 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
||||
*/
|
||||
canRedo: (state) => {
|
||||
return state.historyIndex < state.history.length - 1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get validation results for a specific node
|
||||
*/
|
||||
getNodeValidation: (state) => (nodeId) => {
|
||||
return state.validationResults.get(nodeId) || [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a node has validation issues
|
||||
*/
|
||||
hasValidationIssues: (state) => (nodeId) => {
|
||||
const issues = state.validationResults.get(nodeId);
|
||||
return issues && issues.length > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get validation icon for a node
|
||||
*/
|
||||
getValidationIcon: (state) => (nodeId) => {
|
||||
const issues = state.validationResults.get(nodeId);
|
||||
if (!issues || issues.length === 0) return null;
|
||||
|
||||
const hasError = issues.some(issue => issue.severity === 'error');
|
||||
const hasWarning = issues.some(issue => issue.severity === 'warning');
|
||||
|
||||
if (hasError) return 'material-symbols:error';
|
||||
if (hasWarning) return 'material-symbols:warning';
|
||||
return 'material-symbols:info';
|
||||
},
|
||||
|
||||
/**
|
||||
* Get validation tooltip for a node
|
||||
*/
|
||||
getValidationTooltip: (state) => (nodeId) => {
|
||||
const issues = state.validationResults.get(nodeId);
|
||||
if (!issues || issues.length === 0) return null;
|
||||
|
||||
const lines = issues.map(issue => `• ${issue.message}`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
},
|
||||
|
||||
@ -1350,6 +1403,52 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
||||
console.log(`Successfully renamed variable from "${oldName}" to "${newName}"`);
|
||||
this.unsavedChanges = true;
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update validation results
|
||||
*/
|
||||
updateValidationResults(validationResults, validationSummary, overallStatus) {
|
||||
this.validationResults = new Map(validationResults);
|
||||
this.validationSummary = { ...validationSummary };
|
||||
this.overallValidationStatus = overallStatus;
|
||||
this.lastValidationTime = new Date();
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all validation results
|
||||
*/
|
||||
clearValidationResults() {
|
||||
this.validationResults.clear();
|
||||
this.validationSummary = {
|
||||
totalIssues: 0,
|
||||
errors: 0,
|
||||
warnings: 0,
|
||||
infos: 0,
|
||||
isValid: true
|
||||
};
|
||||
this.overallValidationStatus = 'valid';
|
||||
this.lastValidationTime = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle validation on/off
|
||||
*/
|
||||
toggleValidation() {
|
||||
this.validationEnabled = !this.validationEnabled;
|
||||
if (!this.validationEnabled) {
|
||||
this.clearValidationResults();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set validation enabled state
|
||||
*/
|
||||
setValidationEnabled(enabled) {
|
||||
this.validationEnabled = enabled;
|
||||
if (!enabled) {
|
||||
this.clearValidationResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user