corrad-bp/components/process-flow/EdgeConfiguration.vue
Md Afiq Iskandar 8919ac819c Add Interactive Edges and Edge Configuration to Process Flow
- Introduced new components: ArrowEdge, CustomEdge, InteractiveArrowEdge, and EdgeConfiguration for enhanced edge management in the process flow.
- Implemented dynamic edge paths with customizable styles, labels, and animations, improving visual representation and user interaction.
- Enhanced ProcessFlowCanvas to support new edge types and configurations, allowing for more flexible process designs.
- Updated ProcessFlowNodes to include new edge components, ensuring seamless integration with existing node functionalities.
- Improved user experience by providing configuration options for edges, including animation and style settings, directly within the process builder.
2025-07-11 14:13:42 +08:00

386 lines
11 KiB
Vue

<template>
<div v-if="selectedEdge" class="edge-configuration">
<RsCard>
<template #header>
<div class="flex items-center gap-2">
<svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
</svg>
<span class="font-medium">Edge Configuration</span>
</div>
</template>
<template #body>
<div class="space-y-4">
<!-- Edge Information -->
<div class="bg-gray-50 p-3 rounded-lg">
<div class="text-sm text-gray-600 mb-2">Connection:</div>
<div class="text-sm font-mono">
{{ selectedEdge.sourceNode?.data?.label || selectedEdge.source }}
{{ selectedEdge.targetNode?.data?.label || selectedEdge.target }}
</div>
</div>
<!-- Edge Label -->
<FormKit
type="text"
name="label"
label="Label"
v-model="edgeConfig.label"
placeholder="Enter edge label"
help="Text displayed on the edge"
@input="updateEdge"
/>
<!-- Edge Type -->
<FormKit
type="select"
name="type"
label="Edge Type"
v-model="edgeConfig.type"
:options="edgeTypeOptions"
help="Visual style of the edge path"
@input="updateEdge"
/>
<!-- Animation -->
<FormKit
type="checkbox"
name="animated"
label="Animated"
v-model="edgeConfig.animated"
help="Show flowing animation on the edge"
@input="updateEdge"
/>
<!-- Style Options -->
<div class="space-y-3">
<div class="text-sm font-medium text-gray-700">Style</div>
<!-- Stroke Color -->
<FormKit
type="color"
name="strokeColor"
label="Color"
v-model="edgeConfig.style.stroke"
@input="updateEdge"
/>
<!-- Stroke Width -->
<FormKit
type="range"
name="strokeWidth"
label="Width"
v-model="edgeConfig.style.strokeWidth"
:min="1"
:max="8"
:step="0.5"
help="Line thickness"
@input="updateEdge"
/>
<!-- Dash Pattern -->
<FormKit
type="select"
name="dashPattern"
label="Line Style"
v-model="edgeConfig.dashPattern"
:options="dashPatternOptions"
help="Solid or dashed line"
@input="updateEdge"
/>
</div>
<!-- Arrow Configuration -->
<div class="space-y-3">
<div class="text-sm font-medium text-gray-700">Arrows</div>
<FormKit
type="checkbox"
name="showStartArrow"
label="Start Arrow"
v-model="edgeConfig.showStartArrow"
help="Show arrow at the start of the edge"
@input="updateEdge"
/>
<FormKit
type="checkbox"
name="showEndArrow"
label="End Arrow"
v-model="edgeConfig.showEndArrow"
help="Show arrow at the end of the edge"
@input="updateEdge"
/>
</div>
<!-- Path Customization -->
<div class="space-y-3">
<div class="text-sm font-medium text-gray-700">Path Adjustment</div>
<div class="text-sm text-gray-600 mb-2">
Select the edge to see control points for custom positioning
</div>
<RsButton
v-if="edgeConfig.customPath"
@click="resetPath"
variant="secondary"
size="sm"
class="w-full"
>
Reset to Auto Path
</RsButton>
</div>
<!-- Advanced Options -->
<RsCollapse>
<RsCollapseItem title="Advanced Options">
<!-- Z-Index -->
<FormKit
type="range"
name="zIndex"
label="Layer (Z-Index)"
v-model="edgeConfig.zIndex"
:min="0"
:max="1000"
:step="1"
help="Higher values appear on top"
@input="updateEdge"
/>
<!-- Interaction Width -->
<FormKit
type="range"
name="interactionWidth"
label="Click Area Width"
v-model="edgeConfig.interactionWidth"
:min="5"
:max="30"
:step="1"
help="Invisible area around edge for easier clicking"
@input="updateEdge"
/>
<!-- Updatable -->
<FormKit
type="checkbox"
name="updatable"
label="Allow Reconnection"
v-model="edgeConfig.updatable"
help="Allow dragging edge endpoints to reconnect"
@input="updateEdge"
/>
</RsCollapseItem>
</RsCollapse>
</div>
</template>
<template #footer>
<div class="flex justify-between gap-2">
<RsButton @click="deleteEdge" variant="danger" size="sm">
Delete Edge
</RsButton>
<RsButton @click="closeConfiguration" variant="secondary" size="sm">
Close
</RsButton>
</div>
</template>
</RsCard>
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
import RsCard from '~/components/RsCard.vue'
import RsButton from '~/components/RsButton.vue'
import RsCollapse from '~/components/RsCollapse.vue'
import RsCollapseItem from '~/components/RsCollapseItem.vue'
const props = defineProps({
selectedEdge: {
type: Object,
default: null
}
})
const emit = defineEmits(['updateEdge', 'deleteEdge', 'close'])
// Edge configuration object
const edgeConfig = ref({
label: '',
type: 'custom',
animated: true,
style: {
stroke: '#555',
strokeWidth: 2,
strokeDasharray: ''
},
showStartArrow: false,
showEndArrow: true,
dashPattern: 'solid',
customPath: null,
zIndex: 0,
interactionWidth: 10,
updatable: true
})
// Edge type options
const edgeTypeOptions = [
{ label: 'Custom (Smooth Step)', value: 'custom' },
{ label: 'Bezier Curve', value: 'bezier' },
{ label: 'Straight Line', value: 'straight' },
{ label: 'Step', value: 'step' },
{ label: 'Smooth Step', value: 'smoothstep' }
]
// Dash pattern options
const dashPatternOptions = [
{ label: 'Solid', value: 'solid' },
{ label: 'Dashed', value: 'dashed' },
{ label: 'Dotted', value: 'dotted' },
{ label: 'Dash-Dot', value: 'dashdot' }
]
// Watch for selected edge changes
watch(() => props.selectedEdge, (newEdge) => {
if (newEdge) {
// Load edge configuration
edgeConfig.value = {
label: newEdge.label || '',
type: newEdge.type || 'custom',
animated: newEdge.animated !== undefined ? newEdge.animated : true,
style: {
stroke: newEdge.style?.stroke || '#555',
strokeWidth: newEdge.style?.strokeWidth || 2,
strokeDasharray: newEdge.style?.strokeDasharray || ''
},
showStartArrow: !!newEdge.markerStart,
showEndArrow: !!newEdge.markerEnd,
dashPattern: getDashPatternFromStyle(newEdge.style?.strokeDasharray),
customPath: newEdge.data?.customPath || null,
zIndex: newEdge.zIndex || 0,
interactionWidth: newEdge.interactionWidth || 10,
updatable: newEdge.updatable !== undefined ? newEdge.updatable : true
}
}
}, { immediate: true })
// Helper function to determine dash pattern from style
function getDashPatternFromStyle(dasharray) {
if (!dasharray) return 'solid'
if (dasharray === '5 5') return 'dashed'
if (dasharray === '2 2') return 'dotted'
if (dasharray === '5 2 2 2') return 'dashdot'
return 'solid'
}
// Helper function to get dash array from pattern
function getDashArrayFromPattern(pattern) {
switch (pattern) {
case 'dashed': return '5 5'
case 'dotted': return '2 2'
case 'dashdot': return '5 2 2 2'
default: return ''
}
}
// Update edge function
function updateEdge() {
if (!props.selectedEdge) return
const updates = {
label: edgeConfig.value.label,
type: edgeConfig.value.type,
animated: edgeConfig.value.animated,
style: {
...edgeConfig.value.style,
strokeDasharray: getDashArrayFromPattern(edgeConfig.value.dashPattern)
},
markerStart: edgeConfig.value.showStartArrow ? 'url(#arrow)' : undefined,
markerEnd: edgeConfig.value.showEndArrow ? 'url(#arrow)' : undefined,
zIndex: edgeConfig.value.zIndex,
interactionWidth: edgeConfig.value.interactionWidth,
updatable: edgeConfig.value.updatable
}
emit('updateEdge', props.selectedEdge.id, updates)
}
// Reset path to auto-generated
function resetPath() {
if (!props.selectedEdge) return
edgeConfig.value.customPath = null
emit('updateEdge', props.selectedEdge.id, {
data: {
...props.selectedEdge.data,
customPath: null
}
})
}
// Delete edge
function deleteEdge() {
if (!props.selectedEdge) return
emit('deleteEdge', props.selectedEdge.id)
}
// Close configuration
function closeConfiguration() {
emit('close')
}
</script>
<style scoped>
.edge-configuration {
width: 300px;
max-height: 80vh;
overflow-y: auto;
}
:deep(.formkit-outer) {
margin-bottom: 1rem;
}
:deep(.formkit-label) {
font-size: 0.875rem;
font-weight: 500;
color: rgb(55 65 81);
}
:deep(.formkit-help) {
font-size: 0.75rem;
color: rgb(107 114 128);
}
:deep(.formkit-input[type="color"]) {
width: 100%;
height: 2.5rem;
border-radius: 0.375rem;
border: 1px solid rgb(209 213 219);
}
:deep(.formkit-input[type="range"]) {
width: 100%;
}
/* Custom scrollbar */
.edge-configuration::-webkit-scrollbar {
width: 6px;
}
.edge-configuration::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.edge-configuration::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.edge-configuration::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>