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.
This commit is contained in:
parent
b4eb3265c2
commit
8919ac819c
140
components/process-flow/ArrowEdge.vue
Normal file
140
components/process-flow/ArrowEdge.vue
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<script setup>
|
||||||
|
import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath } from '@vue-flow/core'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
sourceX: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
sourceY: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
targetX: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
targetY: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
sourcePosition: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
targetPosition: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
animated: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
markerEnd: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const path = computed(() => {
|
||||||
|
const [edgePath, labelX, labelY] = getSmoothStepPath({
|
||||||
|
sourceX: props.sourceX,
|
||||||
|
sourceY: props.sourceY,
|
||||||
|
sourcePosition: props.sourcePosition,
|
||||||
|
targetX: props.targetX,
|
||||||
|
targetY: props.targetY,
|
||||||
|
targetPosition: props.targetPosition,
|
||||||
|
borderRadius: 8,
|
||||||
|
offset: 20
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: edgePath,
|
||||||
|
labelX,
|
||||||
|
labelY
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const edgeStyle = computed(() => ({
|
||||||
|
stroke: props.selected ? '#ff6b6b' : '#555',
|
||||||
|
strokeWidth: props.selected ? 3 : 2,
|
||||||
|
strokeDasharray: props.animated ? '5,5' : 'none',
|
||||||
|
...props.style
|
||||||
|
}))
|
||||||
|
|
||||||
|
const markerEndUrl = computed(() => {
|
||||||
|
return props.selected ? 'url(#arrow-selected)' : 'url(#arrow)'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
inheritAttrs: false,
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Main edge path -->
|
||||||
|
<BaseEdge
|
||||||
|
:path="path.path"
|
||||||
|
:style="edgeStyle"
|
||||||
|
:marker-end="markerEndUrl"
|
||||||
|
:class="{
|
||||||
|
'animated': animated,
|
||||||
|
'selected': selected
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Edge label if provided -->
|
||||||
|
<EdgeLabelRenderer v-if="label">
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
transform: `translate(-50%, -50%) translate(${path.labelX}px, ${path.labelY}px)`,
|
||||||
|
pointerEvents: 'all',
|
||||||
|
fontSize: '11px',
|
||||||
|
fontWeight: '600',
|
||||||
|
background: 'white',
|
||||||
|
padding: '3px 8px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
border: '1.5px solid #3b82f6',
|
||||||
|
boxShadow: '0 2px 8px rgba(59, 130, 246, 0.2)',
|
||||||
|
color: '#1e40af',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
zIndex: 10
|
||||||
|
}"
|
||||||
|
class="edge-label nodrag nopan"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</div>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.animated {
|
||||||
|
animation: dash 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dash {
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: -10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
285
components/process-flow/CustomEdge.vue
Normal file
285
components/process-flow/CustomEdge.vue
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
<template>
|
||||||
|
<g>
|
||||||
|
|
||||||
|
<!-- Main edge path -->
|
||||||
|
<BaseEdge
|
||||||
|
:id="id"
|
||||||
|
:path="edgePath"
|
||||||
|
:style="{
|
||||||
|
stroke: selected ? '#ff6b6b' : edgeStyle.stroke || '#555',
|
||||||
|
strokeWidth: selected ? 3 : edgeStyle.strokeWidth || 2,
|
||||||
|
...edgeStyle
|
||||||
|
}"
|
||||||
|
:marker-end="selected ? 'url(#arrow-selected)' : 'url(#arrow)'"
|
||||||
|
:class="{
|
||||||
|
'vue-flow__edge-path': true,
|
||||||
|
'custom-edge': true,
|
||||||
|
'selected': selected,
|
||||||
|
'animated': animated
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Interactive control points for path adjustment -->
|
||||||
|
<g v-if="selected && showControlPoints" class="edge-control-points">
|
||||||
|
<!-- Mid-point control for path bending -->
|
||||||
|
<circle
|
||||||
|
:cx="labelX"
|
||||||
|
:cy="labelY"
|
||||||
|
r="4"
|
||||||
|
fill="#3b82f6"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="2"
|
||||||
|
class="edge-control-point"
|
||||||
|
style="cursor: move;"
|
||||||
|
@mousedown="onControlPointMouseDown"
|
||||||
|
@touchstart="onControlPointMouseDown"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Visual indicator for draggable control -->
|
||||||
|
<circle
|
||||||
|
:cx="labelX"
|
||||||
|
:cy="labelY"
|
||||||
|
r="8"
|
||||||
|
fill="rgba(59, 130, 246, 0.2)"
|
||||||
|
class="edge-control-indicator"
|
||||||
|
style="pointer-events: none;"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Edge label using EdgeLabelRenderer for HTML content -->
|
||||||
|
<EdgeLabelRenderer v-if="label">
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
||||||
|
pointerEvents: 'all',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: '500'
|
||||||
|
}"
|
||||||
|
class="edge-label-container nodrag nopan"
|
||||||
|
>
|
||||||
|
<div class="edge-label">
|
||||||
|
{{ label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
</g>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, inject } from 'vue'
|
||||||
|
import { BaseEdge, EdgeLabelRenderer, getBezierPath, getSmoothStepPath, getStraightPath } from '@vue-flow/core'
|
||||||
|
import { useEdge } from '@vue-flow/core'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: String,
|
||||||
|
sourceX: Number,
|
||||||
|
sourceY: Number,
|
||||||
|
targetX: Number,
|
||||||
|
targetY: Number,
|
||||||
|
sourcePosition: String,
|
||||||
|
targetPosition: String,
|
||||||
|
selected: Boolean,
|
||||||
|
animated: Boolean,
|
||||||
|
label: String,
|
||||||
|
style: Object,
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'smoothstep'
|
||||||
|
},
|
||||||
|
data: Object,
|
||||||
|
markerEnd: String,
|
||||||
|
markerStart: String
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get edge instance for updating
|
||||||
|
const { edge } = useEdge()
|
||||||
|
|
||||||
|
// Control state
|
||||||
|
const showControlPoints = ref(true)
|
||||||
|
const isDragging = ref(false)
|
||||||
|
const pathOffset = ref({ x: 0, y: 0 })
|
||||||
|
|
||||||
|
// Computed edge style
|
||||||
|
const edgeStyle = computed(() => ({
|
||||||
|
stroke: '#555',
|
||||||
|
strokeWidth: 2,
|
||||||
|
...props.style
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Calculate edge path based on type with optional offset for custom positioning
|
||||||
|
const edgePath = computed(() => {
|
||||||
|
try {
|
||||||
|
const offsetX = pathOffset.value.x || 0
|
||||||
|
const offsetY = pathOffset.value.y || 0
|
||||||
|
|
||||||
|
// Apply offset to create custom path curvature
|
||||||
|
const centerX = (props.sourceX + props.targetX) / 2 + offsetX
|
||||||
|
const centerY = (props.sourceY + props.targetY) / 2 + offsetY
|
||||||
|
|
||||||
|
switch (props.type) {
|
||||||
|
case 'straight':
|
||||||
|
return getStraightPath(props.sourceX, props.sourceY, props.targetX, props.targetY)[0]
|
||||||
|
case 'smoothstep':
|
||||||
|
case 'custom':
|
||||||
|
return getSmoothStepPath(
|
||||||
|
props.sourceX,
|
||||||
|
props.sourceY,
|
||||||
|
props.sourcePosition,
|
||||||
|
props.targetX,
|
||||||
|
props.targetY,
|
||||||
|
props.targetPosition,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
20, // offset
|
||||||
|
8 // border radius
|
||||||
|
)[0]
|
||||||
|
case 'bezier':
|
||||||
|
default:
|
||||||
|
return getBezierPath({
|
||||||
|
sourceX: props.sourceX,
|
||||||
|
sourceY: props.sourceY,
|
||||||
|
sourcePosition: props.sourcePosition,
|
||||||
|
targetX: props.targetX,
|
||||||
|
targetY: props.targetY,
|
||||||
|
targetPosition: props.targetPosition,
|
||||||
|
curvature: 0.25
|
||||||
|
})[0]
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error calculating edge path:', error)
|
||||||
|
// Fallback to a simple straight line
|
||||||
|
return `M ${props.sourceX} ${props.sourceY} L ${props.targetX} ${props.targetY}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Calculate label position
|
||||||
|
const labelX = computed(() => {
|
||||||
|
return (props.sourceX + props.targetX) / 2 + pathOffset.value.x
|
||||||
|
})
|
||||||
|
|
||||||
|
const labelY = computed(() => {
|
||||||
|
return (props.sourceY + props.targetY) / 2 + pathOffset.value.y
|
||||||
|
})
|
||||||
|
|
||||||
|
// Control point drag handling
|
||||||
|
const onControlPointMouseDown = (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
isDragging.value = true
|
||||||
|
const startX = event.clientX || event.touches[0].clientX
|
||||||
|
const startY = event.clientY || event.touches[0].clientY
|
||||||
|
const startOffsetX = pathOffset.value.x
|
||||||
|
const startOffsetY = pathOffset.value.y
|
||||||
|
|
||||||
|
const onMouseMove = (moveEvent) => {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
|
||||||
|
const currentX = moveEvent.clientX || moveEvent.touches[0].clientX
|
||||||
|
const currentY = moveEvent.clientY || moveEvent.touches[0].clientY
|
||||||
|
|
||||||
|
const deltaX = currentX - startX
|
||||||
|
const deltaY = currentY - startY
|
||||||
|
|
||||||
|
// Update path offset for custom positioning
|
||||||
|
pathOffset.value = {
|
||||||
|
x: startOffsetX + deltaX,
|
||||||
|
y: startOffsetY + deltaY
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the custom path in edge data for persistence
|
||||||
|
if (edge.value) {
|
||||||
|
edge.value.data = {
|
||||||
|
...edge.value.data,
|
||||||
|
customPath: pathOffset.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
isDragging.value = false
|
||||||
|
document.removeEventListener('mousemove', onMouseMove)
|
||||||
|
document.removeEventListener('mouseup', onMouseUp)
|
||||||
|
document.removeEventListener('touchmove', onMouseMove)
|
||||||
|
document.removeEventListener('touchend', onMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove)
|
||||||
|
document.addEventListener('mouseup', onMouseUp)
|
||||||
|
document.addEventListener('touchmove', onMouseMove)
|
||||||
|
document.addEventListener('touchend', onMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize path offset from saved data
|
||||||
|
try {
|
||||||
|
if (edge.value && edge.value.data && edge.value.data.customPath) {
|
||||||
|
pathOffset.value = edge.value.data.customPath
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Edge data not available yet, using default path')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'CustomEdge',
|
||||||
|
inheritAttrs: false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.edge-label-container {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-label {
|
||||||
|
background: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-control-point {
|
||||||
|
cursor: move;
|
||||||
|
transition: r 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-control-point:hover {
|
||||||
|
r: 5;
|
||||||
|
fill: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-edge {
|
||||||
|
transition: stroke-width 0.2s ease, stroke 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-edge.animated {
|
||||||
|
stroke-dasharray: 5;
|
||||||
|
animation: dash 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-edge.selected {
|
||||||
|
filter: drop-shadow(0 0 4px rgba(255, 107, 107, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dash {
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: -10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide control points when not selected */
|
||||||
|
.edge-control-points {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected .edge-control-points {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
386
components/process-flow/EdgeConfiguration.vue
Normal file
386
components/process-flow/EdgeConfiguration.vue
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
<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>
|
268
components/process-flow/InteractiveArrowEdge.vue
Normal file
268
components/process-flow/InteractiveArrowEdge.vue
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
<script setup>
|
||||||
|
import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath, useVueFlow } from '@vue-flow/core'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: String,
|
||||||
|
sourceX: Number,
|
||||||
|
sourceY: Number,
|
||||||
|
targetX: Number,
|
||||||
|
targetY: Number,
|
||||||
|
sourcePosition: String,
|
||||||
|
targetPosition: String,
|
||||||
|
selected: Boolean,
|
||||||
|
animated: Boolean,
|
||||||
|
label: String,
|
||||||
|
style: Object,
|
||||||
|
markerEnd: String,
|
||||||
|
data: Object
|
||||||
|
})
|
||||||
|
|
||||||
|
const { updateEdgeData } = useVueFlow()
|
||||||
|
|
||||||
|
// Custom path offset for manual positioning
|
||||||
|
const pathOffset = ref({ x: 0, y: 0 })
|
||||||
|
|
||||||
|
// Initialize from saved data
|
||||||
|
if (props.data?.pathOffset) {
|
||||||
|
pathOffset.value = props.data.pathOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate edge path with custom offset
|
||||||
|
const path = computed(() => {
|
||||||
|
try {
|
||||||
|
// Apply the offset to create a custom control point
|
||||||
|
const centerX = (props.sourceX + props.targetX) / 2 + pathOffset.value.x
|
||||||
|
const centerY = (props.sourceY + props.targetY) / 2 + pathOffset.value.y
|
||||||
|
|
||||||
|
const [edgePath, labelX, labelY] = getSmoothStepPath({
|
||||||
|
sourceX: props.sourceX,
|
||||||
|
sourceY: props.sourceY,
|
||||||
|
sourcePosition: props.sourcePosition,
|
||||||
|
targetX: props.targetX,
|
||||||
|
targetY: props.targetY,
|
||||||
|
targetPosition: props.targetPosition,
|
||||||
|
borderRadius: 8,
|
||||||
|
offset: 20,
|
||||||
|
// Override center point with our custom position
|
||||||
|
centerX,
|
||||||
|
centerY
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: edgePath,
|
||||||
|
labelX: labelX || centerX,
|
||||||
|
labelY: labelY || centerY,
|
||||||
|
controlX: centerX,
|
||||||
|
controlY: centerY
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback to simple calculation
|
||||||
|
const centerX = (props.sourceX + props.targetX) / 2 + pathOffset.value.x
|
||||||
|
const centerY = (props.sourceY + props.targetY) / 2 + pathOffset.value.y
|
||||||
|
return {
|
||||||
|
path: `M ${props.sourceX} ${props.sourceY} Q ${centerX} ${centerY} ${props.targetX} ${props.targetY}`,
|
||||||
|
labelX: centerX,
|
||||||
|
labelY: centerY,
|
||||||
|
controlX: centerX,
|
||||||
|
controlY: centerY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const edgeStyle = computed(() => ({
|
||||||
|
stroke: props.selected ? '#ff6b6b' : '#555',
|
||||||
|
strokeWidth: props.selected ? 3 : 2,
|
||||||
|
strokeDasharray: props.animated ? '5,5' : 'none',
|
||||||
|
...props.style
|
||||||
|
}))
|
||||||
|
|
||||||
|
const markerEndUrl = computed(() => {
|
||||||
|
return props.selected ? 'url(#arrow-selected)' : 'url(#arrow)'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Drag handling for control point
|
||||||
|
const isDragging = ref(false)
|
||||||
|
|
||||||
|
const onControlPointMouseDown = (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
isDragging.value = true
|
||||||
|
const startX = event.clientX
|
||||||
|
const startY = event.clientY
|
||||||
|
const startOffsetX = pathOffset.value.x
|
||||||
|
const startOffsetY = pathOffset.value.y
|
||||||
|
|
||||||
|
const onMouseMove = (moveEvent) => {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
|
||||||
|
const deltaX = moveEvent.clientX - startX
|
||||||
|
const deltaY = moveEvent.clientY - startY
|
||||||
|
|
||||||
|
// Apply zoom factor (assuming 1.0 for now, can be enhanced)
|
||||||
|
pathOffset.value = {
|
||||||
|
x: startOffsetX + deltaX,
|
||||||
|
y: startOffsetY + deltaY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
isDragging.value = false
|
||||||
|
|
||||||
|
// Save the new path offset to edge data
|
||||||
|
if (props.id && updateEdgeData) {
|
||||||
|
updateEdgeData(props.id, {
|
||||||
|
...props.data,
|
||||||
|
pathOffset: pathOffset.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
document.removeEventListener('mousemove', onMouseMove)
|
||||||
|
document.removeEventListener('mouseup', onMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove)
|
||||||
|
document.addEventListener('mouseup', onMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset path offset
|
||||||
|
const resetPath = () => {
|
||||||
|
pathOffset.value = { x: 0, y: 0 }
|
||||||
|
if (props.id && updateEdgeData) {
|
||||||
|
updateEdgeData(props.id, {
|
||||||
|
...props.data,
|
||||||
|
pathOffset: undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
inheritAttrs: false,
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Main edge path -->
|
||||||
|
<BaseEdge
|
||||||
|
:path="path.path"
|
||||||
|
:style="edgeStyle"
|
||||||
|
:marker-end="markerEndUrl"
|
||||||
|
:class="{
|
||||||
|
'animated': animated,
|
||||||
|
'selected': selected
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Control point for edge repositioning (only when selected) -->
|
||||||
|
<g v-if="selected" class="edge-controls">
|
||||||
|
<!-- Control point circle -->
|
||||||
|
<circle
|
||||||
|
:cx="path.controlX"
|
||||||
|
:cy="path.controlY"
|
||||||
|
r="4"
|
||||||
|
fill="#3b82f6"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="2"
|
||||||
|
class="edge-control-point"
|
||||||
|
style="cursor: move;"
|
||||||
|
@mousedown="onControlPointMouseDown"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Larger invisible area for easier clicking -->
|
||||||
|
<circle
|
||||||
|
:cx="path.controlX"
|
||||||
|
:cy="path.controlY"
|
||||||
|
r="12"
|
||||||
|
fill="transparent"
|
||||||
|
class="edge-control-area"
|
||||||
|
style="cursor: move;"
|
||||||
|
@mousedown="onControlPointMouseDown"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Visual indicator for draggable control -->
|
||||||
|
<circle
|
||||||
|
:cx="path.controlX"
|
||||||
|
:cy="path.controlY"
|
||||||
|
r="8"
|
||||||
|
fill="rgba(59, 130, 246, 0.2)"
|
||||||
|
class="edge-control-indicator"
|
||||||
|
style="pointer-events: none;"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Edge label if provided -->
|
||||||
|
<EdgeLabelRenderer v-if="label">
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
transform: `translate(-50%, -50%) translate(${path.labelX}px, ${path.labelY}px)`,
|
||||||
|
pointerEvents: 'all',
|
||||||
|
fontSize: '11px',
|
||||||
|
fontWeight: '600',
|
||||||
|
background: 'white',
|
||||||
|
padding: '3px 8px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
border: '1.5px solid #3b82f6',
|
||||||
|
boxShadow: '0 2px 8px rgba(59, 130, 246, 0.2)',
|
||||||
|
color: '#1e40af',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
zIndex: 10
|
||||||
|
}"
|
||||||
|
class="edge-label nodrag nopan"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</div>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
|
||||||
|
<!-- Reset button for custom positioning -->
|
||||||
|
<EdgeLabelRenderer v-if="selected && (pathOffset.x !== 0 || pathOffset.y !== 0)">
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
transform: `translate(-50%, -50%) translate(${path.controlX + 20}px, ${path.controlY - 20}px)`,
|
||||||
|
pointerEvents: 'all'
|
||||||
|
}"
|
||||||
|
class="edge-reset-button nodrag nopan"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
@click="resetPath"
|
||||||
|
style="
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||||
|
"
|
||||||
|
title="Reset edge path"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.edge-control-point {
|
||||||
|
transition: r 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-control-point:hover {
|
||||||
|
r: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animated {
|
||||||
|
animation: dash 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dash {
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: -10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,10 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed, shallowRef, watch, defineExpose, nextTick } from 'vue';
|
import { ref, onMounted, computed, shallowRef, watch, defineExpose, nextTick, markRaw } from 'vue';
|
||||||
import { VueFlow, useVueFlow, Panel } from '@vue-flow/core';
|
import { VueFlow, useVueFlow, Panel } from '@vue-flow/core';
|
||||||
import { Background } from '@vue-flow/background';
|
import { Background } from '@vue-flow/background';
|
||||||
import { Controls } from '@vue-flow/controls';
|
import { Controls } from '@vue-flow/controls';
|
||||||
import { MiniMap } from '@vue-flow/minimap';
|
import { MiniMap } from '@vue-flow/minimap';
|
||||||
import { nodeTypes as customNodeTypes, nodeStyles } from './ProcessFlowNodes';
|
import { nodeTypes as customNodeTypes, nodeStyles } from './ProcessFlowNodes';
|
||||||
|
import InteractiveArrowEdge from './InteractiveArrowEdge.vue';
|
||||||
import '@vue-flow/core/dist/style.css';
|
import '@vue-flow/core/dist/style.css';
|
||||||
import '@vue-flow/core/dist/theme-default.css';
|
import '@vue-flow/core/dist/theme-default.css';
|
||||||
import '@vue-flow/controls/dist/style.css';
|
import '@vue-flow/controls/dist/style.css';
|
||||||
@ -55,17 +56,30 @@ const {
|
|||||||
} = useVueFlow({
|
} = useVueFlow({
|
||||||
defaultEdgeOptions: {
|
defaultEdgeOptions: {
|
||||||
animated: true,
|
animated: true,
|
||||||
type: 'smoothstep'
|
type: 'custom',
|
||||||
|
style: {
|
||||||
|
stroke: '#555',
|
||||||
|
strokeWidth: 2
|
||||||
|
},
|
||||||
|
markerEnd: 'url(#arrow)'
|
||||||
},
|
},
|
||||||
deleteKeyCode: 'Delete',
|
deleteKeyCode: 'Delete',
|
||||||
selectionKeyCode: 'Shift',
|
selectionKeyCode: 'Shift',
|
||||||
multiSelectionKeyCode: 'Control',
|
multiSelectionKeyCode: 'Control',
|
||||||
connectionMode: 'strict',
|
connectionMode: 'strict',
|
||||||
|
edgeUpdaterRadius: 12,
|
||||||
|
edgesUpdatable: true,
|
||||||
|
defaultMarkerColor: '#555',
|
||||||
isValidConnection: (connection) => {
|
isValidConnection: (connection) => {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Define custom edge types - use markRaw to prevent reactivity issues
|
||||||
|
const edgeTypes = {
|
||||||
|
custom: markRaw(InteractiveArrowEdge)
|
||||||
|
};
|
||||||
|
|
||||||
// Default nodes if empty
|
// Default nodes if empty
|
||||||
const defaultNodes = [];
|
const defaultNodes = [];
|
||||||
|
|
||||||
@ -586,9 +600,13 @@ const handleConnect = (connection) => {
|
|||||||
target: connection.target,
|
target: connection.target,
|
||||||
sourceHandle: connection.sourceHandle,
|
sourceHandle: connection.sourceHandle,
|
||||||
targetHandle: connection.targetHandle,
|
targetHandle: connection.targetHandle,
|
||||||
type: 'smoothstep',
|
type: 'custom',
|
||||||
animated: true,
|
animated: true,
|
||||||
style: { stroke: '#555' },
|
style: {
|
||||||
|
stroke: '#555',
|
||||||
|
strokeWidth: 2
|
||||||
|
},
|
||||||
|
markerEnd: 'url(#arrow)',
|
||||||
label: label
|
label: label
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -733,6 +751,25 @@ function updateNode(nodeId, newData) {
|
|||||||
nodeToUpdate.position = { ...newData.position };
|
nodeToUpdate.position = { ...newData.position };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update style if provided
|
||||||
|
if (newData.style !== undefined) {
|
||||||
|
nodeToUpdate.style = {
|
||||||
|
...(nodeToUpdate.style || {}),
|
||||||
|
...newData.style
|
||||||
|
};
|
||||||
|
|
||||||
|
// Also apply style directly to DOM element for immediate visual feedback
|
||||||
|
nextTick(() => {
|
||||||
|
const nodeElement = document.querySelector(`[data-id="${nodeId}"]`);
|
||||||
|
if (nodeElement) {
|
||||||
|
// Apply styles directly to the DOM element
|
||||||
|
Object.entries(newData.style).forEach(([key, value]) => {
|
||||||
|
nodeElement.style[key] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Force re-render by updating node internals
|
// Force re-render by updating node internals
|
||||||
updateNodeInternals([nodeId]);
|
updateNodeInternals([nodeId]);
|
||||||
|
|
||||||
@ -1053,7 +1090,7 @@ function fromObject(flowObject) {
|
|||||||
sourceHandle: edge.sourceHandle,
|
sourceHandle: edge.sourceHandle,
|
||||||
targetHandle: edge.targetHandle,
|
targetHandle: edge.targetHandle,
|
||||||
label: edge.label || '',
|
label: edge.label || '',
|
||||||
type: edge.type || 'smoothstep',
|
type: edge.type || 'custom',
|
||||||
animated: edge.animated !== undefined ? edge.animated : true,
|
animated: edge.animated !== undefined ? edge.animated : true,
|
||||||
data: edge.data || {}
|
data: edge.data || {}
|
||||||
// Only include essential properties
|
// Only include essential properties
|
||||||
@ -1102,6 +1139,7 @@ function fromObject(flowObject) {
|
|||||||
v-bind="flowOptions"
|
v-bind="flowOptions"
|
||||||
class="bg-slate-50 process-flow"
|
class="bg-slate-50 process-flow"
|
||||||
:nodeTypes="customNodeTypes"
|
:nodeTypes="customNodeTypes"
|
||||||
|
:edgeTypes="edgeTypes"
|
||||||
@node-click="onNodeClick"
|
@node-click="onNodeClick"
|
||||||
@edge-click="onEdgeClick"
|
@edge-click="onEdgeClick"
|
||||||
@pane-click="onPaneClick"
|
@pane-click="onPaneClick"
|
||||||
@ -1110,21 +1148,72 @@ function fromObject(flowObject) {
|
|||||||
@edgeDoubleClick="onEdgeDelete"
|
@edgeDoubleClick="onEdgeDelete"
|
||||||
@keyup.delete="onDeleteKeyPress"
|
@keyup.delete="onDeleteKeyPress"
|
||||||
>
|
>
|
||||||
|
<!-- Global SVG definitions for arrows -->
|
||||||
|
<svg style="position: absolute; top: 0; left: 0; width: 0; height: 0">
|
||||||
|
<defs>
|
||||||
|
<marker
|
||||||
|
id="arrow"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
refX="9"
|
||||||
|
refY="3"
|
||||||
|
markerWidth="6"
|
||||||
|
markerHeight="6"
|
||||||
|
orient="auto"
|
||||||
|
markerUnits="strokeWidth"
|
||||||
|
>
|
||||||
|
<path d="M0,0 L0,6 L9,3 z" fill="#555" />
|
||||||
|
</marker>
|
||||||
|
<marker
|
||||||
|
id="arrow-selected"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
refX="9"
|
||||||
|
refY="3"
|
||||||
|
markerWidth="6"
|
||||||
|
markerHeight="6"
|
||||||
|
orient="auto"
|
||||||
|
markerUnits="strokeWidth"
|
||||||
|
>
|
||||||
|
<path d="M0,0 L0,6 L9,3 z" fill="#ff6b6b" />
|
||||||
|
</marker>
|
||||||
|
<marker
|
||||||
|
id="arrow-animated"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
refX="9"
|
||||||
|
refY="3"
|
||||||
|
markerWidth="6"
|
||||||
|
markerHeight="6"
|
||||||
|
orient="auto"
|
||||||
|
markerUnits="strokeWidth"
|
||||||
|
>
|
||||||
|
<path d="M0,0 L0,6 L9,3 z" fill="#3b82f6" />
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
<Background pattern-color="#aaa" gap="20" />
|
<Background pattern-color="#aaa" gap="20" />
|
||||||
<Controls />
|
<Controls />
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
|
|
||||||
|
<!-- Custom edge template -->
|
||||||
|
<template #edge-custom="edgeProps">
|
||||||
|
<InteractiveArrowEdge v-bind="edgeProps" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #edge-label="{ label }">
|
<template #edge-label="{ label }">
|
||||||
<div class="edge-label">{{ label }}</div>
|
<div class="edge-label">{{ label }}</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Panel position="top-right" class="node-controls">
|
<Panel position="top-right" class="node-controls">
|
||||||
<div class="p-2 bg-white rounded shadow-sm text-sm">
|
<div class="p-2 bg-white rounded shadow-sm text-sm">
|
||||||
<div class="mb-1">Controls:</div>
|
<div class="mb-1 font-medium">Controls:</div>
|
||||||
<div>• Delete: Remove selected</div>
|
<div>• Delete: Remove selected</div>
|
||||||
<div>• Shift: Select nodes</div>
|
<div>• Shift: Select nodes</div>
|
||||||
<div>• Drag between nodes to connect</div>
|
<div>• Drag between nodes to connect</div>
|
||||||
<div>• Double-click to remove</div>
|
<div>• Double-click to remove</div>
|
||||||
|
<div class="mt-2 mb-1 font-medium">Edge Features:</div>
|
||||||
|
<div>• Arrows show flow direction</div>
|
||||||
|
<div>• Select edge for controls</div>
|
||||||
|
<div>• Blue dot = drag to reposition</div>
|
||||||
|
<div>• Reset button restores path</div>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
</VueFlow>
|
</VueFlow>
|
||||||
@ -1181,21 +1270,28 @@ function fromObject(flowObject) {
|
|||||||
|
|
||||||
.edge-label {
|
.edge-label {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding: 2px 4px;
|
padding: 3px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 12px;
|
||||||
font-size: 10px;
|
font-size: 11px;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
font-weight: 600;
|
||||||
|
border: 1.5px solid #3b82f6;
|
||||||
|
color: #1e40af;
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2);
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Vue Flow custom styles */
|
/* Vue Flow custom styles */
|
||||||
:deep(.vue-flow__edge-path) {
|
:deep(.vue-flow__edge-path) {
|
||||||
stroke: #555;
|
stroke: #555;
|
||||||
stroke-width: 2px;
|
stroke-width: 2px;
|
||||||
|
transition: stroke-width 0.2s ease, stroke 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.vue-flow__edge.selected .vue-flow__edge-path) {
|
:deep(.vue-flow__edge.selected .vue-flow__edge-path) {
|
||||||
stroke: #ff6b6b;
|
stroke: #ff6b6b;
|
||||||
stroke-width: 3px;
|
stroke-width: 3px;
|
||||||
|
filter: drop-shadow(0 0 4px rgba(255, 107, 107, 0.3));
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.vue-flow__edge.animated .vue-flow__edge-path) {
|
:deep(.vue-flow__edge.animated .vue-flow__edge-path) {
|
||||||
@ -1203,6 +1299,49 @@ function fromObject(flowObject) {
|
|||||||
animation: flowEdgeAnimation 2s linear infinite;
|
animation: flowEdgeAnimation 2s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.vue-flow__edge:hover .vue-flow__edge-path) {
|
||||||
|
stroke-width: 3px;
|
||||||
|
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom edge styles */
|
||||||
|
:deep(.custom-edge) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.custom-edge.selected) {
|
||||||
|
filter: drop-shadow(0 0 4px rgba(255, 107, 107, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edge updater styles */
|
||||||
|
:deep(.vue-flow__edge-updater) {
|
||||||
|
stroke: #3b82f6;
|
||||||
|
fill: #3b82f6;
|
||||||
|
stroke-width: 2;
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.vue-flow__edge-updater:hover) {
|
||||||
|
stroke: #2563eb;
|
||||||
|
fill: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edge control point styles for better visibility */
|
||||||
|
:deep(.edge-control-point) {
|
||||||
|
cursor: move !important;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease, r 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.vue-flow__edge.selected .edge-control-point) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.edge-control-point:hover) {
|
||||||
|
r: 6 !important;
|
||||||
|
fill: #2563eb !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Add custom node icon styles */
|
/* Add custom node icon styles */
|
||||||
:deep(.custom-node) {
|
:deep(.custom-node) {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
@ -1346,14 +1346,20 @@ export const nodeStyles = `
|
|||||||
|
|
||||||
.node-gateway .custom-node-content {
|
.node-gateway .custom-node-content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
transform: rotate(-45deg);
|
transform: rotate(-45deg);
|
||||||
width: 120%;
|
width: 100%;
|
||||||
height: 120%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 8px;
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-start .custom-node-content,
|
.node-start .custom-node-content,
|
||||||
@ -1427,12 +1433,12 @@ export const nodeStyles = `
|
|||||||
}
|
}
|
||||||
|
|
||||||
.node-gateway .custom-node-title {
|
.node-gateway .custom-node-title {
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
color: #333;
|
color: #c2410c;
|
||||||
margin: 0;
|
margin: 0 0 4px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 90%;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@ -1501,9 +1507,13 @@ export const nodeStyles = `
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gateway-details {
|
.gateway-details {
|
||||||
width: 100%;
|
width: 80%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 4px;
|
margin-top: 2px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1584,9 +1594,11 @@ export const nodeStyles = `
|
|||||||
|
|
||||||
.node-gateway .node-description {
|
.node-gateway .node-description {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 2px;
|
||||||
font-size: 10px;
|
font-size: 8px;
|
||||||
|
line-height: 1.1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
max-width: 80px;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
@ -1595,12 +1607,12 @@ export const nodeStyles = `
|
|||||||
|
|
||||||
.node-gateway .node-rule-detail {
|
.node-gateway .node-rule-detail {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 10px;
|
font-size: 8px;
|
||||||
color: #666;
|
color: #666;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 80%;
|
||||||
margin-bottom: 3px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-gateway .node-rule-detail-label {
|
.node-gateway .node-rule-detail-label {
|
||||||
@ -1612,9 +1624,11 @@ export const nodeStyles = `
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
font-size: 10px;
|
font-size: 8px;
|
||||||
max-width: 60px;
|
max-width: 50px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #c2410c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-gateway .material-icons {
|
.node-gateway .material-icons {
|
||||||
@ -1714,13 +1728,13 @@ export const nodeStyles = `
|
|||||||
background: white;
|
background: white;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
z-index: -10 !important; /* Lower z-index so shapes stay in background */
|
/* z-index is now controlled dynamically through node data */
|
||||||
}
|
}
|
||||||
|
|
||||||
.shape-node.selected {
|
.shape-node.selected {
|
||||||
border-color: #3b82f6;
|
border-color: #3b82f6;
|
||||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
z-index: -8 !important; /* Still behind process nodes when selected */
|
/* z-index is now controlled dynamically through node data */
|
||||||
}
|
}
|
||||||
|
|
||||||
.shape-node:hover {
|
.shape-node:hover {
|
||||||
|
@ -118,13 +118,34 @@
|
|||||||
v-model="newVariableType"
|
v-model="newVariableType"
|
||||||
class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:border-purple-500 focus:ring focus:ring-purple-200"
|
class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:border-purple-500 focus:ring focus:ring-purple-200"
|
||||||
>
|
>
|
||||||
<option value="string">String (Text)</option>
|
<optgroup label="Common Types">
|
||||||
<option value="int">Integer (Whole Number)</option>
|
<option value="string">String (Text)</option>
|
||||||
<option value="decimal">Decimal (Number with decimals)</option>
|
<option value="int">Integer (Whole Number)</option>
|
||||||
<option value="boolean">Boolean (True/False)</option>
|
<option value="decimal">Decimal (Number with decimals)</option>
|
||||||
<option value="date">Date</option>
|
<option value="boolean">Boolean (True/False)</option>
|
||||||
<option value="datetime">DateTime</option>
|
</optgroup>
|
||||||
<option value="object">Object (JSON)</option>
|
<optgroup label="Date & Time Types">
|
||||||
|
<option value="date">Date</option>
|
||||||
|
<option value="datetime">DateTime</option>
|
||||||
|
<option value="time">Time</option>
|
||||||
|
<option value="timestamp">Timestamp</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Complex Types">
|
||||||
|
<option value="object">Object (JSON)</option>
|
||||||
|
<option value="array">Array (List)</option>
|
||||||
|
<option value="map">Map (Key-Value Pairs)</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Special Types">
|
||||||
|
<option value="currency">Currency</option>
|
||||||
|
<option value="percentage">Percentage</option>
|
||||||
|
<option value="email">Email</option>
|
||||||
|
<option value="url">URL</option>
|
||||||
|
<option value="phone">Phone Number</option>
|
||||||
|
<option value="file">File</option>
|
||||||
|
<option value="image">Image</option>
|
||||||
|
<option value="binary">Binary Data</option>
|
||||||
|
<option value="uuid">UUID</option>
|
||||||
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -240,6 +261,7 @@ const errorMessage = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const groupedVariables = computed(() => {
|
const groupedVariables = computed(() => {
|
||||||
|
// Initialize with common types but allow for dynamic additions
|
||||||
const types = {
|
const types = {
|
||||||
string: { label: 'Text Variables', variables: [] },
|
string: { label: 'Text Variables', variables: [] },
|
||||||
int: { label: 'Integer Variables', variables: [] },
|
int: { label: 'Integer Variables', variables: [] },
|
||||||
@ -248,6 +270,7 @@ const groupedVariables = computed(() => {
|
|||||||
date: { label: 'Date Variables', variables: [] },
|
date: { label: 'Date Variables', variables: [] },
|
||||||
datetime: { label: 'DateTime Variables', variables: [] },
|
datetime: { label: 'DateTime Variables', variables: [] },
|
||||||
object: { label: 'Object Variables', variables: [] }
|
object: { label: 'Object Variables', variables: [] }
|
||||||
|
// Other types will be added dynamically
|
||||||
};
|
};
|
||||||
|
|
||||||
let variables = props.availableVariables;
|
let variables = props.availableVariables;
|
||||||
@ -259,9 +282,16 @@ const groupedVariables = computed(() => {
|
|||||||
|
|
||||||
variables.forEach(variable => {
|
variables.forEach(variable => {
|
||||||
const type = variable.type || 'string';
|
const type = variable.type || 'string';
|
||||||
if (types[type]) {
|
|
||||||
types[type].variables.push(variable);
|
// If type doesn't exist in our types object, add it dynamically
|
||||||
|
if (!types[type]) {
|
||||||
|
// Create a friendly label from the type name
|
||||||
|
const label = type.charAt(0).toUpperCase() + type.slice(1) + ' Variables';
|
||||||
|
types[type] = { label, variables: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the variable to its type group
|
||||||
|
types[type].variables.push(variable);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Object.values(types).filter(group => group.variables.length > 0);
|
return Object.values(types).filter(group => group.variables.length > 0);
|
||||||
@ -344,10 +374,26 @@ const getTypeColorClass = (type) => {
|
|||||||
string: 'bg-blue-100 text-blue-800',
|
string: 'bg-blue-100 text-blue-800',
|
||||||
int: 'bg-purple-100 text-purple-800',
|
int: 'bg-purple-100 text-purple-800',
|
||||||
decimal: 'bg-purple-100 text-purple-800',
|
decimal: 'bg-purple-100 text-purple-800',
|
||||||
|
number: 'bg-purple-100 text-purple-800',
|
||||||
boolean: 'bg-indigo-100 text-indigo-800',
|
boolean: 'bg-indigo-100 text-indigo-800',
|
||||||
date: 'bg-amber-100 text-amber-800',
|
date: 'bg-amber-100 text-amber-800',
|
||||||
datetime: 'bg-amber-100 text-amber-800',
|
datetime: 'bg-amber-100 text-amber-800',
|
||||||
object: 'bg-emerald-100 text-emerald-800'
|
object: 'bg-emerald-100 text-emerald-800',
|
||||||
|
// Add colors for any additional types you know about
|
||||||
|
array: 'bg-emerald-100 text-emerald-800',
|
||||||
|
map: 'bg-teal-100 text-teal-800',
|
||||||
|
set: 'bg-teal-100 text-teal-800',
|
||||||
|
binary: 'bg-slate-100 text-slate-800',
|
||||||
|
time: 'bg-amber-100 text-amber-800',
|
||||||
|
timestamp: 'bg-amber-100 text-amber-800',
|
||||||
|
uuid: 'bg-violet-100 text-violet-800',
|
||||||
|
email: 'bg-blue-100 text-blue-800',
|
||||||
|
url: 'bg-blue-100 text-blue-800',
|
||||||
|
phone: 'bg-blue-100 text-blue-800',
|
||||||
|
currency: 'bg-green-100 text-green-800',
|
||||||
|
percentage: 'bg-purple-100 text-purple-800',
|
||||||
|
file: 'bg-orange-100 text-orange-800',
|
||||||
|
image: 'bg-orange-100 text-orange-800'
|
||||||
};
|
};
|
||||||
return classes[type] || 'bg-gray-100 text-gray-800';
|
return classes[type] || 'bg-gray-100 text-gray-800';
|
||||||
};
|
};
|
||||||
@ -361,13 +407,47 @@ const formatPreviewValue = (value, type) => {
|
|||||||
case 'boolean':
|
case 'boolean':
|
||||||
return value ? 'true' : 'false';
|
return value ? 'true' : 'false';
|
||||||
case 'object':
|
case 'object':
|
||||||
|
case 'array':
|
||||||
|
case 'map':
|
||||||
|
case 'set':
|
||||||
try {
|
try {
|
||||||
const str = typeof value === 'string' ? value : JSON.stringify(value);
|
const str = typeof value === 'string' ? value : JSON.stringify(value);
|
||||||
return str.length > 30 ? `${str.substring(0, 30)}...` : str;
|
return str.length > 30 ? `${str.substring(0, 30)}...` : str;
|
||||||
} catch {
|
} catch {
|
||||||
return 'Invalid JSON';
|
return 'Invalid JSON';
|
||||||
}
|
}
|
||||||
|
case 'date':
|
||||||
|
case 'datetime':
|
||||||
|
case 'time':
|
||||||
|
case 'timestamp':
|
||||||
|
// Try to format dates nicely if possible
|
||||||
|
try {
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return value.toLocaleString();
|
||||||
|
} else if (typeof value === 'string' && (value.includes('T') || value.includes('-'))) {
|
||||||
|
return new Date(value).toLocaleString();
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return String(value);
|
||||||
|
case 'int':
|
||||||
|
case 'decimal':
|
||||||
|
case 'percentage':
|
||||||
|
case 'currency':
|
||||||
|
// For numeric types
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
case 'file':
|
||||||
|
case 'image':
|
||||||
|
case 'binary':
|
||||||
|
// For file-like types
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.length > 20 ? `${value.substring(0, 20)}...` : value;
|
||||||
|
}
|
||||||
|
return '[Binary Data]';
|
||||||
default:
|
default:
|
||||||
|
// For any other type, just convert to string
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
111
docs/BR/business-rules.md
Normal file
111
docs/BR/business-rules.md
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
Definisi Aktiviti BF-NAS-PRF-AS-QS-03 Analisa Data (Had Kifayah)
|
||||||
|
DEFINISI AKTIVITI FUNGSI BISNES
|
||||||
|
Rujukan Fungsi BF-NAS-PRF-AS_QS
|
||||||
|
Nama Fungsi Pendaftaran Pantas Perseorangan
|
||||||
|
Rujukan Aktiviti BF-NAS-PRF-AS-QS-03
|
||||||
|
Nama Aktiviti Analisa Data (Had Kifayah)
|
||||||
|
Keterangan Aktiviti Proses pengiraan had kifayah asnaf menggunakan maklumat konfigurasi had kifayah dan juga maklumat yang telah diisi oleh pemohon/pendaftar
|
||||||
|
Aktor NAS
|
||||||
|
|
||||||
|
Aktiviti Sebelum -
|
||||||
|
|
||||||
|
Aktiviti Selepas/ Aktiviti Lain BF-NAS-PRF-AS-QS-04
|
||||||
|
Syor Status Keluarga Asnaf
|
||||||
|
|
||||||
|
Kaedah/Operasi (Bagaimana):
|
||||||
|
1. Sebagai contoh, proses pengiraan had kifayah menggunakan maklumat-maklumat didalam konfigurasi had kifayah. Untuk kes pendaftaran pantas perseorangan, hanya minimum maklumat yang dimasukkan. Oleh itu proses pengiraan had kifayah akan menggunakan maklumat-maklumat yang dimasukkan oleh pemohon.
|
||||||
|
• No Pengenalan
|
||||||
|
• Pendapatan
|
||||||
|
• Bilangan tanggungan
|
||||||
|
2. Sistem akan membuat pengiraan had kifayah menggunakan maklumat yang dimasukkan satu persatu mengikut formula had kifayah yang telah dikonfigurasi.
|
||||||
|
3. Pengesyoran profil akan menggunakan maklumat %had kifayah, contohnya:
|
||||||
|
• 0%-49.9% - Fakir
|
||||||
|
• 50 – 100% - Miskin
|
||||||
|
• >100% - Non-FM
|
||||||
|
4. Pengiraan had kifayah ini adalah dinamik. Sistem akan membuat pengiraan mengikut formula terkini.
|
||||||
|
5. Data had kifayah keluarga akan disimpan ke dalam pangkalan data
|
||||||
|
6. Sistem akan menjana ID rekod dan masa tindakan.
|
||||||
|
7. Rekod Log
|
||||||
|
o Tindakan Admin direkodkan ke dalam Audit Trail, termasuk:
|
||||||
|
- Nama pengguna
|
||||||
|
- Tarikh dan Masa
|
||||||
|
- Jenis perubahan (Tambah/Kemaskini)
|
||||||
|
- Butiran data
|
||||||
|
8. Pemberitahuan Sistem (Automatik) – ‘Pengiraan had kifayah selesai’
|
||||||
|
Penggunaan Maklumat:
|
||||||
|
1. No Pengenalan Ketua Keluarga(CRU)
|
||||||
|
2. No Pengenalan Tanggungan (CRU)
|
||||||
|
3. Jumlah had kifayah mengikut formula (CRU)
|
||||||
|
4. Kira Jumlah Had Kifayah Keluarga (CRU)
|
||||||
|
5. Kira %Had Kifayah (CRU)
|
||||||
|
6. Kategori Keluarga (CRU)
|
||||||
|
Polisi dan Dasar Berkaitan
|
||||||
|
1. Dasar Keselamatan ICT LZS
|
||||||
|
• Polisi Pengurusan Data ICT Kuatkuasa 1 Jan 2020 (3)
|
||||||
|
• Polisi Perlindungan Data Peribadi LZS
|
||||||
|
2. Garis Panduan dan Syarat Kelayakan Agihan Zakat
|
||||||
|
• Menetapkan prinsip asas mengenai pemberian zakat termasuk had keperluan asas dan mekanisme kelayakan penerima.
|
||||||
|
• Kelulusan kadar kifayah mesti selari dengan keutamaan dan strategi agihan semasa.
|
||||||
|
Kaedah Alternatif
|
||||||
|
Tiada
|
||||||
|
Ciri-ciri Kualiti (Keperluan Bukan Fungsian)
|
||||||
|
1. Audit Trail
|
||||||
|
Catatan Tambahan:
|
||||||
|
Aktiviti akan dipanggil setiap kali membuat pendaftaran pantas asnaf.
|
||||||
|
|
||||||
|
Jadual 28: Definisi Aktiviti BF-NAS-PRF-AS-QS-03 Analisa Data (Had Kifayah)
|
||||||
|
|
||||||
|
Definisi Aktiviti BF-NAS-PRF-AS-QS-04 Syor Status Keluarga Asnaf
|
||||||
|
DEFINISI AKTIVITI FUNGSI BISNES
|
||||||
|
Rujukan Fungsi BF-NAS-PRF-AS-QS
|
||||||
|
Nama Fungsi Pendaftaran Pantas Perseorangan
|
||||||
|
Rujukan Aktiviti BF-NAS-PRF-AS-QS-04
|
||||||
|
Nama Aktiviti Syor Status Keluarga Asnaf
|
||||||
|
Keterangan Aktiviti Sistem akan mengesyorkan status keluarga asnaf hasil dari pengiraan had kifayah.
|
||||||
|
Aktor NAS
|
||||||
|
|
||||||
|
Aktiviti Sebelum -
|
||||||
|
|
||||||
|
Aktiviti Selepas/ Aktiviti Lain BF-NAS-PRF-AS-QS-05
|
||||||
|
Pengesahan
|
||||||
|
|
||||||
|
Kaedah/Operasi (Bagaimana):
|
||||||
|
1. Pengesyoran profil akan menggunakan maklumat %had kifayah, contohnya:
|
||||||
|
a. 0%-49.9% - Fakir
|
||||||
|
b. 50 – 100% - Miskin
|
||||||
|
c. >100% - Non-FM
|
||||||
|
2. Pengiraan had kifayah ini adalah dinamik. Sistem akan membuat pengiraan mengikut formula terkini.
|
||||||
|
3. Notifikasi berkenaan maklumat ID ketua keluarga akan dihantar kepada pegawai yang akan membuat pengesahan.
|
||||||
|
4. Pengesahan akan dilakukan oleh pegawai yang telah dikonfigurasi untuk mengesahkan profil ini.
|
||||||
|
5. Jika tarikh masuk islam diisi, status individu tersebut akan menjadi ‘Muallaf’ dengan syarat tempoh tarikh masuk islam dengan tarikh semasa kurang dari 5 tahun.
|
||||||
|
6. Status keluarga akan ditentukan oleh proses pengiraan had kifayah.
|
||||||
|
7. Status individu tanggungan akan menggunakan tetapan status keluarga tersebut. (Fakir, Miskin, Muallaf, Non-FM)
|
||||||
|
8. Simpan maklumat
|
||||||
|
a. %Had kifayah kepada %had kifayah syor
|
||||||
|
b. status keluarga kepada kategori keluarga asnaf syor
|
||||||
|
c. status asnaf = status keluarga kepada kategori asnaf syor
|
||||||
|
9. Ringkasan maklumat asnaf yang digenerate oleh AI perlu disimpan di dalam pangkalan data (syor pengesahan) berserta tarikh janaan laporan (tarikh pengesyoran) dan akan dipaparkan kemudian pada proses pengesahan.
|
||||||
|
Penggunaan Maklumat:
|
||||||
|
Maklumat Had Kifayah
|
||||||
|
1. No Pengenalan Ketua Keluarga(R)
|
||||||
|
2. Nama (R)
|
||||||
|
3. Alamat (R)
|
||||||
|
4. %Had Kifayah syor (C)
|
||||||
|
5. Kategori Keluarga Asnaf syor (C)
|
||||||
|
6. Kategori Asnaf syor (C)
|
||||||
|
7. Kategori Tanggungan syor (C)
|
||||||
|
8. Syor Pengesahan (C)
|
||||||
|
9. Tarikh Pengesyoran (C)
|
||||||
|
Polisi dan Dasar Berkaitan
|
||||||
|
1. Dasar Keselamatan ICT LZS
|
||||||
|
• Polisi Pengurusan Data ICT Kuatkuasa 1 Jan 2020 (3)
|
||||||
|
• Polisi Perlindungan Data Peribadi LZS
|
||||||
|
2. Garis Panduan dan Syarat Kelayakan Agihan Zakat
|
||||||
|
• Menetapkan prinsip asas mengenai pemberian zakat termasuk had keperluan asas dan mekanisme kelayakan penerima.
|
||||||
|
• Kelulusan kadar kifayah mesti selari dengan keutamaan dan strategi agihan semasa.
|
||||||
|
Kaedah Alternatif
|
||||||
|
Tiada
|
||||||
|
Ciri-ciri Kualiti (Keperluan Bukan Fungsian)
|
||||||
|
Tiada
|
||||||
|
Catatan Tambahan:
|
||||||
|
Aktiviti akan dipanggil setiap kali membuat pendaftaran pantas asnaf.
|
@ -1,41 +1,889 @@
|
|||||||
// Conditional logic for showing 'Nyatakan keperluan lain' field
|
this.hideField("form_jeniskp_1");
|
||||||
onFieldChange("keperluan_mendesak", (value) => {
|
this.hideField("form_jeniskp_2");
|
||||||
if (Array.isArray(value) && value.includes("lain_lain")) {
|
this.hideField("form_jeniskp_3");
|
||||||
showField("keperluan_lain_nyatakan");
|
|
||||||
} else {
|
this.onFieldChange("select_1", (value) => {
|
||||||
hideField("keperluan_lain_nyatakan");
|
this.hideField("form_jeniskp_1");
|
||||||
setField("keperluan_lain_nyatakan", "");
|
this.hideField("form_jeniskp_2");
|
||||||
|
this.hideField("form_jeniskp_3");
|
||||||
|
if (value && value.trim()) {
|
||||||
|
if (value == "jeniskp_1") this.showField("form_jeniskp_1");
|
||||||
|
if (value == "jeniskp_2") this.showField("form_jeniskp_2");
|
||||||
|
if (value == "jeniskp_3") this.showField("form_jeniskp_3");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show success message on form load
|
|
||||||
showInfo(
|
|
||||||
"Sila lengkapkan semua maklumat yang diperlukan untuk penilaian awal."
|
|
||||||
);
|
|
||||||
// Conditional Logic Script
|
// Conditional Logic Script
|
||||||
|
|
||||||
// Conditional logic for field: keperluan_lain_nyatakan
|
// Conditional Logic Script
|
||||||
onFieldChange("keperluan_mendesak", function () {
|
|
||||||
if (
|
// Conditional Logic Script
|
||||||
String(getField("keperluan_mendesak") || "")
|
|
||||||
.toLowerCase()
|
// Conditional Logic Script
|
||||||
.includes("lain_lain".toLowerCase())
|
|
||||||
) {
|
// Conditional Logic Script
|
||||||
showField("keperluan_lain_nyatakan");
|
|
||||||
|
// Conditional Logic Script
|
||||||
|
|
||||||
|
// Conditional logic for field: nyatakan_lain2
|
||||||
|
onFieldChange("radio_bangsa", function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
} else {
|
} else {
|
||||||
hideField("keperluan_lain_nyatakan");
|
showField("nyatakan_lain2");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initial evaluation for field: keperluan_lain_nyatakan
|
// Initial evaluation for field: nyatakan_lain2
|
||||||
(function () {
|
(function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// Conditional Logic Script
|
||||||
|
|
||||||
|
// Conditional logic for field: nyatakan_lain2
|
||||||
|
onFieldChange("radio_bangsa", function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: nyatakan_lain2
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// Conditional Logic Script
|
||||||
|
|
||||||
|
// Conditional logic for field: nyatakan_lain2
|
||||||
|
onFieldChange("radio_bangsa", function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: nyatakan_lain2
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: text_14
|
||||||
|
onFieldChange("radio_pendidikan", function () {
|
||||||
|
if (getField("radio_pendidikan") !== "lain") {
|
||||||
|
hideField("text_14");
|
||||||
|
} else {
|
||||||
|
showField("text_14");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: text_14
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_pendidikan") !== "lain") {
|
||||||
|
hideField("text_14");
|
||||||
|
} else {
|
||||||
|
showField("text_14");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// Conditional Logic Script
|
||||||
|
|
||||||
|
// Conditional logic for field: nyatakan_lain2
|
||||||
|
onFieldChange("radio_bangsa", function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: nyatakan_lain2
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: text_14
|
||||||
|
onFieldChange("radio_pendidikan", function () {
|
||||||
|
if (getField("radio_pendidikan") !== "lain") {
|
||||||
|
hideField("text_14");
|
||||||
|
} else {
|
||||||
|
showField("text_14");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: text_14
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_pendidikan") !== "lain") {
|
||||||
|
hideField("text_14");
|
||||||
|
} else {
|
||||||
|
showField("text_14");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// Conditional Logic Script
|
||||||
|
|
||||||
|
// Conditional logic for field: nyatakan_lain2
|
||||||
|
onFieldChange("radio_bangsa", function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: nyatakan_lain2
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: text_14
|
||||||
|
onFieldChange("radio_pendidikan", function () {
|
||||||
|
if (getField("radio_pendidikan") !== "lain") {
|
||||||
|
hideField("text_14");
|
||||||
|
} else {
|
||||||
|
showField("text_14");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: text_14
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_pendidikan") !== "lain") {
|
||||||
|
hideField("text_14");
|
||||||
|
} else {
|
||||||
|
showField("text_14");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// Conditional Logic Script
|
||||||
|
|
||||||
|
// Conditional logic for field: nyatakan_lain2
|
||||||
|
onFieldChange("radio_bangsa", function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: nyatakan_lain2
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: text_14
|
||||||
|
onFieldChange("radio_pendidikan", function () {
|
||||||
|
if (getField("radio_pendidikan") !== "lain") {
|
||||||
|
hideField("text_14");
|
||||||
|
} else {
|
||||||
|
showField("text_14");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: text_14
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_pendidikan") !== "lain") {
|
||||||
|
hideField("text_14");
|
||||||
|
} else {
|
||||||
|
showField("text_14");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// Conditional Logic Script
|
||||||
|
|
||||||
|
// Conditional logic for field: nyatakan_lain2
|
||||||
|
onFieldChange("radio_bangsa", function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: nyatakan_lain2
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: text_14
|
||||||
|
onFieldChange("radio_pendidikan", function () {
|
||||||
|
if (getField("radio_pendidikan") !== "lain") {
|
||||||
|
hideField("text_14");
|
||||||
|
} else {
|
||||||
|
showField("text_14");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: text_14
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_pendidikan") !== "lain") {
|
||||||
|
hideField("text_14");
|
||||||
|
} else {
|
||||||
|
showField("text_14");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Hide "Nyatakan Hubungan Lain-lain" initially
|
||||||
|
this.hideField("hubungan_lain_nyatakan");
|
||||||
|
|
||||||
|
// Show/hide relationship specification field
|
||||||
|
this.onFieldChange("hubungan_keluarga", (value) => {
|
||||||
|
if (value && value.includes("lain_lain")) {
|
||||||
|
this.showField("hubungan_lain_nyatakan");
|
||||||
|
} else {
|
||||||
|
this.hideField("hubungan_lain_nyatakan");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide "Sebab Pembayaran Tunai" initially
|
||||||
|
this.hideField("sebab_tunai");
|
||||||
|
|
||||||
|
// Show/hide cash payment reason field
|
||||||
|
this.onFieldChange("cara_pembayaran", (value) => {
|
||||||
|
if (value && value.includes("tunai")) {
|
||||||
|
this.showField("sebab_tunai");
|
||||||
|
} else {
|
||||||
|
this.hideField("sebab_tunai");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide education specification field initially
|
||||||
|
this.hideField("pendidikan_lain_tanggungan");
|
||||||
|
|
||||||
|
// Show/hide education specification field
|
||||||
|
this.onFieldChange("pendidikan_tertinggi_tanggungan", (value) => {
|
||||||
|
if (value && value.includes("lain_lain")) {
|
||||||
|
this.showField("pendidikan_lain_tanggungan");
|
||||||
|
} else {
|
||||||
|
this.hideField("pendidikan_lain_tanggungan");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide school information initially
|
||||||
|
this.hideField("maklumat_sekolah");
|
||||||
|
|
||||||
|
// Show/hide school information based on schooling status
|
||||||
|
this.onFieldChange("bersekolah_tanggungan", (value) => {
|
||||||
|
if (value === "ya") {
|
||||||
|
this.showField("maklumat_sekolah");
|
||||||
|
} else {
|
||||||
|
this.hideField("maklumat_sekolah");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle repeating group conditional logic for each dependent
|
||||||
|
this.onFieldChange("tanggungan_maklumat", (value) => {
|
||||||
|
if (value && Array.isArray(value)) {
|
||||||
|
value.forEach((item, index) => {
|
||||||
|
// Handle race specification for each dependent
|
||||||
|
if (item.bangsa_tanggungan !== "lain_lain") {
|
||||||
|
// Hide the specification field for this item
|
||||||
|
const fieldName = `tanggungan_maklumat[${index}].bangsa_lain_tanggungan`;
|
||||||
|
// Note: Repeating group field hiding requires specific handling
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Conditional Logic Script
|
||||||
|
|
||||||
|
// Conditional logic for field: nyatakan_lain2
|
||||||
|
onFieldChange("radio_bangsa", function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: nyatakan_lain2
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: text_14
|
||||||
|
onFieldChange("radio_pendidikan", function () {
|
||||||
|
if (getField("radio_pendidikan") !== "lain") {
|
||||||
|
hideField("text_14");
|
||||||
|
} else {
|
||||||
|
showField("text_14");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: text_14
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_pendidikan") !== "lain") {
|
||||||
|
hideField("text_14");
|
||||||
|
} else {
|
||||||
|
showField("text_14");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: hubungan_lain_nyatakan
|
||||||
|
onFieldChange("hubungan_keluarga", function () {
|
||||||
if (
|
if (
|
||||||
String(getField("keperluan_mendesak") || "")
|
!String(getField("hubungan_keluarga") || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes("lain_lain".toLowerCase())
|
.includes("lain_lain".toLowerCase())
|
||||||
) {
|
) {
|
||||||
showField("keperluan_lain_nyatakan");
|
hideField("hubungan_lain_nyatakan");
|
||||||
} else {
|
} else {
|
||||||
hideField("keperluan_lain_nyatakan");
|
showField("hubungan_lain_nyatakan");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: hubungan_lain_nyatakan
|
||||||
|
(function () {
|
||||||
|
if (
|
||||||
|
!String(getField("hubungan_keluarga") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("lain_lain".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("hubungan_lain_nyatakan");
|
||||||
|
} else {
|
||||||
|
showField("hubungan_lain_nyatakan");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: sebab_tunai
|
||||||
|
onFieldChange("cara_pembayaran", function () {
|
||||||
|
if (
|
||||||
|
!String(getField("cara_pembayaran") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("tunai".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("sebab_tunai");
|
||||||
|
} else {
|
||||||
|
showField("sebab_tunai");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: sebab_tunai
|
||||||
|
(function () {
|
||||||
|
if (
|
||||||
|
!String(getField("cara_pembayaran") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("tunai".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("sebab_tunai");
|
||||||
|
} else {
|
||||||
|
showField("sebab_tunai");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: pendidikan_lain_tanggungan
|
||||||
|
onFieldChange("pendidikan_tertinggi_tanggungan", function () {
|
||||||
|
if (
|
||||||
|
!String(getField("pendidikan_tertinggi_tanggungan") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("lain_lain".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("pendidikan_lain_tanggungan");
|
||||||
|
} else {
|
||||||
|
showField("pendidikan_lain_tanggungan");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: pendidikan_lain_tanggungan
|
||||||
|
(function () {
|
||||||
|
if (
|
||||||
|
!String(getField("pendidikan_tertinggi_tanggungan") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("lain_lain".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("pendidikan_lain_tanggungan");
|
||||||
|
} else {
|
||||||
|
showField("pendidikan_lain_tanggungan");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: maklumat_sekolah
|
||||||
|
onFieldChange("bersekolah_tanggungan", function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("maklumat_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("maklumat_sekolah");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: maklumat_sekolah
|
||||||
|
(function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("maklumat_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("maklumat_sekolah");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// Conditional Logic Script
|
||||||
|
|
||||||
|
// Conditional logic for field: nyatakan_lain2
|
||||||
|
onFieldChange("radio_bangsa", function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: nyatakan_lain2
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: text_14
|
||||||
|
onFieldChange("radio_pendidikan", function () {
|
||||||
|
if (getField("radio_pendidikan") !== "lain") {
|
||||||
|
hideField("text_14");
|
||||||
|
} else {
|
||||||
|
showField("text_14");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: text_14
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_pendidikan") !== "lain") {
|
||||||
|
hideField("text_14");
|
||||||
|
} else {
|
||||||
|
showField("text_14");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: hubungan_lain_nyatakan
|
||||||
|
onFieldChange("hubungan_keluarga", function () {
|
||||||
|
if (
|
||||||
|
!String(getField("hubungan_keluarga") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("lain_lain".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("hubungan_lain_nyatakan");
|
||||||
|
} else {
|
||||||
|
showField("hubungan_lain_nyatakan");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: hubungan_lain_nyatakan
|
||||||
|
(function () {
|
||||||
|
if (
|
||||||
|
!String(getField("hubungan_keluarga") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("lain_lain".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("hubungan_lain_nyatakan");
|
||||||
|
} else {
|
||||||
|
showField("hubungan_lain_nyatakan");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: bangsa_lain_tanggungan
|
||||||
|
onFieldChange("bangsa_tanggungan", function () {
|
||||||
|
if (getField("bangsa_tanggungan") !== "lain_lain") {
|
||||||
|
hideField("bangsa_lain_tanggungan");
|
||||||
|
} else {
|
||||||
|
showField("bangsa_lain_tanggungan");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: bangsa_lain_tanggungan
|
||||||
|
(function () {
|
||||||
|
if (getField("bangsa_tanggungan") !== "lain_lain") {
|
||||||
|
hideField("bangsa_lain_tanggungan");
|
||||||
|
} else {
|
||||||
|
showField("bangsa_lain_tanggungan");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: sebab_tunai
|
||||||
|
onFieldChange("cara_pembayaran", function () {
|
||||||
|
if (
|
||||||
|
!String(getField("cara_pembayaran") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("tunai".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("sebab_tunai");
|
||||||
|
} else {
|
||||||
|
showField("sebab_tunai");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: sebab_tunai
|
||||||
|
(function () {
|
||||||
|
if (
|
||||||
|
!String(getField("cara_pembayaran") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("tunai".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("sebab_tunai");
|
||||||
|
} else {
|
||||||
|
showField("sebab_tunai");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: pendidikan_lain_tanggungan
|
||||||
|
onFieldChange("pendidikan_tertinggi_tanggungan", function () {
|
||||||
|
if (
|
||||||
|
!String(getField("pendidikan_tertinggi_tanggungan") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("lain_lain".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("pendidikan_lain_tanggungan");
|
||||||
|
} else {
|
||||||
|
showField("pendidikan_lain_tanggungan");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: pendidikan_lain_tanggungan
|
||||||
|
(function () {
|
||||||
|
if (
|
||||||
|
!String(getField("pendidikan_tertinggi_tanggungan") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("lain_lain".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("pendidikan_lain_tanggungan");
|
||||||
|
} else {
|
||||||
|
showField("pendidikan_lain_tanggungan");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: nama_sekolah
|
||||||
|
onFieldChange("bersekolah_tanggungan", function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("nama_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("nama_sekolah");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: nama_sekolah
|
||||||
|
(function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("nama_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("nama_sekolah");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: alamat_sekolah
|
||||||
|
onFieldChange("bersekolah_tanggungan", function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("alamat_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("alamat_sekolah");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: alamat_sekolah
|
||||||
|
(function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("alamat_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("alamat_sekolah");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: daerah_sekolah
|
||||||
|
onFieldChange("bersekolah_tanggungan", function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("daerah_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("daerah_sekolah");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: daerah_sekolah
|
||||||
|
(function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("daerah_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("daerah_sekolah");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: negeri_sekolah
|
||||||
|
onFieldChange("bersekolah_tanggungan", function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("negeri_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("negeri_sekolah");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: negeri_sekolah
|
||||||
|
(function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("negeri_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("negeri_sekolah");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: poskod_sekolah
|
||||||
|
onFieldChange("bersekolah_tanggungan", function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("poskod_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("poskod_sekolah");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: poskod_sekolah
|
||||||
|
(function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("poskod_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("poskod_sekolah");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// Conditional Logic Script
|
||||||
|
|
||||||
|
// Conditional logic for field: nyatakan_lain2
|
||||||
|
onFieldChange("radio_bangsa", function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: nyatakan_lain2
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_bangsa") !== "lain") {
|
||||||
|
hideField("nyatakan_lain2");
|
||||||
|
} else {
|
||||||
|
showField("nyatakan_lain2");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: text_14
|
||||||
|
onFieldChange("radio_pendidikan", function () {
|
||||||
|
if (getField("radio_pendidikan") !== "lain") {
|
||||||
|
hideField("text_14");
|
||||||
|
} else {
|
||||||
|
showField("text_14");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: text_14
|
||||||
|
(function () {
|
||||||
|
if (getField("radio_pendidikan") !== "lain") {
|
||||||
|
hideField("text_14");
|
||||||
|
} else {
|
||||||
|
showField("text_14");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: hubungan_lain_nyatakan
|
||||||
|
onFieldChange("hubungan_keluarga", function () {
|
||||||
|
if (
|
||||||
|
!String(getField("hubungan_keluarga") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("lain_lain".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("hubungan_lain_nyatakan");
|
||||||
|
} else {
|
||||||
|
showField("hubungan_lain_nyatakan");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: hubungan_lain_nyatakan
|
||||||
|
(function () {
|
||||||
|
if (
|
||||||
|
!String(getField("hubungan_keluarga") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("lain_lain".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("hubungan_lain_nyatakan");
|
||||||
|
} else {
|
||||||
|
showField("hubungan_lain_nyatakan");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: bangsa_lain_tanggungan
|
||||||
|
onFieldChange("bangsa_tanggungan", function () {
|
||||||
|
if (getField("bangsa_tanggungan") !== "lain_lain") {
|
||||||
|
hideField("bangsa_lain_tanggungan");
|
||||||
|
} else {
|
||||||
|
showField("bangsa_lain_tanggungan");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: bangsa_lain_tanggungan
|
||||||
|
(function () {
|
||||||
|
if (getField("bangsa_tanggungan") !== "lain_lain") {
|
||||||
|
hideField("bangsa_lain_tanggungan");
|
||||||
|
} else {
|
||||||
|
showField("bangsa_lain_tanggungan");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: sebab_tunai
|
||||||
|
onFieldChange("cara_pembayaran", function () {
|
||||||
|
if (
|
||||||
|
!String(getField("cara_pembayaran") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("tunai".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("sebab_tunai");
|
||||||
|
} else {
|
||||||
|
showField("sebab_tunai");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: sebab_tunai
|
||||||
|
(function () {
|
||||||
|
if (
|
||||||
|
!String(getField("cara_pembayaran") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("tunai".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("sebab_tunai");
|
||||||
|
} else {
|
||||||
|
showField("sebab_tunai");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: pendidikan_lain_tanggungan
|
||||||
|
onFieldChange("pendidikan_tertinggi_tanggungan", function () {
|
||||||
|
if (
|
||||||
|
!String(getField("pendidikan_tertinggi_tanggungan") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("lain_lain".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("pendidikan_lain_tanggungan");
|
||||||
|
} else {
|
||||||
|
showField("pendidikan_lain_tanggungan");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: pendidikan_lain_tanggungan
|
||||||
|
(function () {
|
||||||
|
if (
|
||||||
|
!String(getField("pendidikan_tertinggi_tanggungan") || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes("lain_lain".toLowerCase())
|
||||||
|
) {
|
||||||
|
hideField("pendidikan_lain_tanggungan");
|
||||||
|
} else {
|
||||||
|
showField("pendidikan_lain_tanggungan");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: nama_sekolah
|
||||||
|
onFieldChange("bersekolah_tanggungan", function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("nama_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("nama_sekolah");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: nama_sekolah
|
||||||
|
(function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("nama_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("nama_sekolah");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: alamat_sekolah
|
||||||
|
onFieldChange("bersekolah_tanggungan", function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("alamat_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("alamat_sekolah");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: alamat_sekolah
|
||||||
|
(function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("alamat_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("alamat_sekolah");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: daerah_sekolah
|
||||||
|
onFieldChange("bersekolah_tanggungan", function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("daerah_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("daerah_sekolah");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: daerah_sekolah
|
||||||
|
(function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("daerah_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("daerah_sekolah");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: negeri_sekolah
|
||||||
|
onFieldChange("bersekolah_tanggungan", function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("negeri_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("negeri_sekolah");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: negeri_sekolah
|
||||||
|
(function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("negeri_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("negeri_sekolah");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Conditional logic for field: poskod_sekolah
|
||||||
|
onFieldChange("bersekolah_tanggungan", function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("poskod_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("poskod_sekolah");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial evaluation for field: poskod_sekolah
|
||||||
|
(function () {
|
||||||
|
if (getField("bersekolah_tanggungan") !== "ya") {
|
||||||
|
hideField("poskod_sekolah");
|
||||||
|
} else {
|
||||||
|
showField("poskod_sekolah");
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
File diff suppressed because it is too large
Load Diff
2195
docs/json/process-builder/currentDefiniton.json
Normal file
2195
docs/json/process-builder/currentDefiniton.json
Normal file
File diff suppressed because it is too large
Load Diff
1462
docs/json/process-builder/fullProcessSettings.json
Normal file
1462
docs/json/process-builder/fullProcessSettings.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -66,28 +66,6 @@
|
|||||||
"sourceHandle": "api-1751871528249-right",
|
"sourceHandle": "api-1751871528249-right",
|
||||||
"targetHandle": "script-1751871635000-left"
|
"targetHandle": "script-1751871635000-left"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "script-1751871635000-api-1752114771983-1752114797295",
|
|
||||||
"data": {},
|
|
||||||
"type": "smoothstep",
|
|
||||||
"label": "",
|
|
||||||
"source": "script-1751871635000",
|
|
||||||
"target": "api-1752114771983",
|
|
||||||
"animated": true,
|
|
||||||
"sourceHandle": "script-1751871635000-right",
|
|
||||||
"targetHandle": "api-1752114771983-left"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "api-1752114771983-html-1752109761532-1752114821740",
|
|
||||||
"data": {},
|
|
||||||
"type": "smoothstep",
|
|
||||||
"label": "",
|
|
||||||
"source": "api-1752114771983",
|
|
||||||
"target": "html-1752109761532",
|
|
||||||
"animated": true,
|
|
||||||
"sourceHandle": "api-1752114771983-right",
|
|
||||||
"targetHandle": "html-1752109761532-left"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "start-1751870920411-form-1753000000000-1752115210580",
|
"id": "start-1751870920411-form-1753000000000-1752115210580",
|
||||||
"data": {},
|
"data": {},
|
||||||
@ -120,15 +98,299 @@
|
|||||||
"animated": true,
|
"animated": true,
|
||||||
"sourceHandle": "api-1753000000001-right",
|
"sourceHandle": "api-1753000000001-right",
|
||||||
"targetHandle": "script-1753000000002-left"
|
"targetHandle": "script-1753000000002-left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "script-1753000000002-gateway-1753100000000-1752202680793",
|
||||||
|
"data": {},
|
||||||
|
"type": "smoothstep",
|
||||||
|
"label": "",
|
||||||
|
"source": "script-1753000000002",
|
||||||
|
"target": "gateway-1753100000000",
|
||||||
|
"animated": true,
|
||||||
|
"sourceHandle": "script-1753000000002-right",
|
||||||
|
"targetHandle": "gateway-1753100000000-left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gateway-1753100000000-form-1751870928350-1752202820616",
|
||||||
|
"data": {},
|
||||||
|
"type": "smoothstep",
|
||||||
|
"label": "Tidak",
|
||||||
|
"source": "gateway-1753100000000",
|
||||||
|
"target": "form-1751870928350",
|
||||||
|
"animated": true,
|
||||||
|
"sourceHandle": "gateway-1753100000000-right",
|
||||||
|
"targetHandle": "form-1751870928350-left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "form-1753100000001-gateway-1753100000002-1752202841767",
|
||||||
|
"data": {},
|
||||||
|
"type": "smoothstep",
|
||||||
|
"label": "",
|
||||||
|
"source": "form-1753100000001",
|
||||||
|
"target": "gateway-1753100000002",
|
||||||
|
"animated": true,
|
||||||
|
"sourceHandle": "form-1753100000001-right",
|
||||||
|
"targetHandle": "gateway-1753100000002-left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gateway-1753100000002-subprocess-1753100000003-1752202973206",
|
||||||
|
"data": {},
|
||||||
|
"type": "smoothstep",
|
||||||
|
"label": "Ya",
|
||||||
|
"source": "gateway-1753100000002",
|
||||||
|
"target": "subprocess-1753100000003",
|
||||||
|
"animated": true,
|
||||||
|
"sourceHandle": "gateway-1753100000002-right",
|
||||||
|
"targetHandle": "subprocess-1753100000003-left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gateway-1753100000000-form-1753100000001-1752203198190",
|
||||||
|
"data": {},
|
||||||
|
"type": "smoothstep",
|
||||||
|
"label": "Ya",
|
||||||
|
"source": "gateway-1753100000000",
|
||||||
|
"target": "form-1753100000001",
|
||||||
|
"animated": true,
|
||||||
|
"sourceHandle": "gateway-1753100000000-right",
|
||||||
|
"targetHandle": "form-1753100000001-top"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "script-1751871635000-form-family-tree-choice-1752204006162",
|
||||||
|
"data": {},
|
||||||
|
"type": "smoothstep",
|
||||||
|
"label": "",
|
||||||
|
"source": "script-1751871635000",
|
||||||
|
"target": "form-family-tree-choice",
|
||||||
|
"animated": true,
|
||||||
|
"sourceHandle": "script-1751871635000-right",
|
||||||
|
"targetHandle": "form-family-tree-choice-left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "form-family-tree-choice-gateway-family-tree-choice-1752204014461",
|
||||||
|
"data": {},
|
||||||
|
"type": "smoothstep",
|
||||||
|
"label": "",
|
||||||
|
"source": "form-family-tree-choice",
|
||||||
|
"target": "gateway-family-tree-choice",
|
||||||
|
"animated": true,
|
||||||
|
"sourceHandle": "form-family-tree-choice-bottom",
|
||||||
|
"targetHandle": "gateway-family-tree-choice-left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gateway-family-tree-choice-api-1752114771983-1752204050925",
|
||||||
|
"data": {},
|
||||||
|
"type": "smoothstep",
|
||||||
|
"label": "Ya",
|
||||||
|
"source": "gateway-family-tree-choice",
|
||||||
|
"target": "api-1752114771983",
|
||||||
|
"animated": true,
|
||||||
|
"sourceHandle": "gateway-family-tree-choice-right",
|
||||||
|
"targetHandle": "api-1752114771983-left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api-1752114771983-html-1752109761532-1752204167660",
|
||||||
|
"data": {},
|
||||||
|
"type": "smoothstep",
|
||||||
|
"label": "",
|
||||||
|
"source": "api-1752114771983",
|
||||||
|
"target": "html-1752109761532",
|
||||||
|
"animated": true,
|
||||||
|
"sourceHandle": "api-1752114771983-bottom",
|
||||||
|
"targetHandle": "html-1752109761532-left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gateway-family-tree-choice-form-kategori-asnaf-1752204312974",
|
||||||
|
"data": {},
|
||||||
|
"type": "smoothstep",
|
||||||
|
"label": "Tidak",
|
||||||
|
"source": "gateway-family-tree-choice",
|
||||||
|
"target": "form-kategori-asnaf",
|
||||||
|
"animated": true,
|
||||||
|
"sourceHandle": "gateway-family-tree-choice-right",
|
||||||
|
"targetHandle": "form-kategori-asnaf-top"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "form-kategori-asnaf-gateway-fakir-miskin-choice-1752204332412",
|
||||||
|
"data": {},
|
||||||
|
"type": "smoothstep",
|
||||||
|
"label": "",
|
||||||
|
"source": "form-kategori-asnaf",
|
||||||
|
"target": "gateway-fakir-miskin-choice",
|
||||||
|
"animated": true,
|
||||||
|
"sourceHandle": "form-kategori-asnaf-bottom",
|
||||||
|
"targetHandle": "gateway-fakir-miskin-choice-left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gateway-1753100000002-subprocess-1752202751327-1752204374213",
|
||||||
|
"data": {},
|
||||||
|
"type": "smoothstep",
|
||||||
|
"label": "Tidak",
|
||||||
|
"source": "gateway-1753100000002",
|
||||||
|
"target": "subprocess-1752202751327",
|
||||||
|
"animated": true,
|
||||||
|
"sourceHandle": "gateway-1753100000002-right",
|
||||||
|
"targetHandle": "subprocess-1752202751327-left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gateway-fakir-miskin-choice-business-rule-1751871900000-1752204422657",
|
||||||
|
"data": {},
|
||||||
|
"type": "smoothstep",
|
||||||
|
"label": "Ya",
|
||||||
|
"source": "gateway-fakir-miskin-choice",
|
||||||
|
"target": "business-rule-1751871900000",
|
||||||
|
"animated": true,
|
||||||
|
"sourceHandle": "gateway-fakir-miskin-choice-right",
|
||||||
|
"targetHandle": "business-rule-1751871900000-left"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"nodes": [
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "gateway-family-tree-choice",
|
||||||
|
"data": {
|
||||||
|
"label": "Lihat Family Tree?",
|
||||||
|
"shape": "diamond",
|
||||||
|
"textColor": "#374151",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "condition-group-ya-path",
|
||||||
|
"output": "Ya",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "condition-1",
|
||||||
|
"value": "ya",
|
||||||
|
"operator": "eq",
|
||||||
|
"variable": "lihatFamilyTree",
|
||||||
|
"valueType": "string",
|
||||||
|
"logicalOperator": "and"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "condition-group-tidak-path",
|
||||||
|
"output": "Tidak",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "condition-2",
|
||||||
|
"value": "tidak",
|
||||||
|
"operator": "eq",
|
||||||
|
"variable": "lihatFamilyTree",
|
||||||
|
"valueType": "string",
|
||||||
|
"logicalOperator": "and"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"borderColor": "#6b7280",
|
||||||
|
"defaultPath": "gateway-fakir-miskin-choice",
|
||||||
|
"description": "Choose whether to view family tree",
|
||||||
|
"gatewayType": "exclusive",
|
||||||
|
"backgroundColor": "#f9fafb"
|
||||||
|
},
|
||||||
|
"type": "gateway",
|
||||||
|
"label": "Lihat Family Tree?",
|
||||||
|
"position": { "x": 2070, "y": -345 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "form-family-tree-choice",
|
||||||
|
"data": {
|
||||||
|
"label": "Pilihan Family Tree",
|
||||||
|
"formId": 6,
|
||||||
|
"formName": "Pilihan Family Tree",
|
||||||
|
"formUuid": "6e07fc8f-a160-478a-85fd-fa3364401547",
|
||||||
|
"description": "Form untuk memilih sama ada mahu lihat family tree atau tidak",
|
||||||
|
"assignedRoles": [],
|
||||||
|
"assignedUsers": [],
|
||||||
|
"inputMappings": [
|
||||||
|
{ "formField": "nama_pemohon", "processVariable": "applicantName" }
|
||||||
|
],
|
||||||
|
"assignmentType": "public",
|
||||||
|
"outputMappings": [
|
||||||
|
{
|
||||||
|
"formField": "lihat_family_tree",
|
||||||
|
"processVariable": "lihatFamilyTree"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConditions": []
|
||||||
|
},
|
||||||
|
"type": "form",
|
||||||
|
"label": "Pilihan Family Tree",
|
||||||
|
"position": { "x": 2040, "y": -540 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gateway-fakir-miskin-choice",
|
||||||
|
"data": {
|
||||||
|
"label": "Fakir Miskin?",
|
||||||
|
"shape": "diamond",
|
||||||
|
"textColor": "#333333",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "condition-group-ya-path",
|
||||||
|
"output": "Ya",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "condition-1",
|
||||||
|
"value": "fakir_miskin",
|
||||||
|
"operator": "eq",
|
||||||
|
"variable": "kategoriAsnaf",
|
||||||
|
"valueType": "string",
|
||||||
|
"logicalOperator": "and"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "condition-group-tidak-path",
|
||||||
|
"output": "Tidak",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "condition-2",
|
||||||
|
"value": "bukan_fakir_miskin",
|
||||||
|
"operator": "eq",
|
||||||
|
"variable": "kategoriAsnaf",
|
||||||
|
"valueType": "string",
|
||||||
|
"logicalOperator": "and"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"borderColor": "#6b7280",
|
||||||
|
"defaultPath": "business-rule-1751871900000",
|
||||||
|
"description": "Determine if applicant is Fakir Miskin",
|
||||||
|
"gatewayType": "exclusive",
|
||||||
|
"backgroundColor": "#f9fafb"
|
||||||
|
},
|
||||||
|
"type": "gateway",
|
||||||
|
"label": "Fakir Miskin?",
|
||||||
|
"position": { "x": 2085, "y": 210 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "form-kategori-asnaf",
|
||||||
|
"data": {
|
||||||
|
"label": "Pilihan Kategori Asnaf",
|
||||||
|
"formId": 7,
|
||||||
|
"formName": "Pilihan Kategori Asnaf",
|
||||||
|
"formUuid": "7e07fc8f-a160-478a-85fd-fa3364401548",
|
||||||
|
"description": "Form untuk memilih kategori asnaf",
|
||||||
|
"assignedRoles": [],
|
||||||
|
"assignedUsers": [],
|
||||||
|
"inputMappings": [
|
||||||
|
{ "formField": "nama_pemohon", "processVariable": "applicantName" }
|
||||||
|
],
|
||||||
|
"assignmentType": "public",
|
||||||
|
"outputMappings": [
|
||||||
|
{ "formField": "kategori_asnaf", "processVariable": "kategoriAsnaf" }
|
||||||
|
],
|
||||||
|
"fieldConditions": []
|
||||||
|
},
|
||||||
|
"type": "form",
|
||||||
|
"label": "Pilihan Kategori Asnaf",
|
||||||
|
"position": { "x": 2055, "y": -30 }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "start-1751870920411",
|
"id": "start-1751870920411",
|
||||||
"data": { "label": "Start", "description": "Process start point" },
|
"data": { "label": "Start", "description": "Process start point" },
|
||||||
"type": "start",
|
"type": "start",
|
||||||
"label": "Start",
|
"label": "Start",
|
||||||
"position": { "x": 120, "y": -495 }
|
"position": { "x": -75, "y": -510 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "form-1752471000000",
|
"id": "form-1752471000000",
|
||||||
@ -170,7 +432,7 @@
|
|||||||
},
|
},
|
||||||
"type": "form",
|
"type": "form",
|
||||||
"label": "Penilaian Awal",
|
"label": "Penilaian Awal",
|
||||||
"position": { "x": 450, "y": -525 }
|
"position": { "x": 435, "y": -540 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api-1752471000010",
|
"id": "api-1752471000010",
|
||||||
@ -187,7 +449,7 @@
|
|||||||
},
|
},
|
||||||
"type": "api",
|
"type": "api",
|
||||||
"label": "Submit Penilaian Awal API",
|
"label": "Submit Penilaian Awal API",
|
||||||
"position": { "x": 450, "y": -345 }
|
"position": { "x": 435, "y": -345 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "script-1752471000020",
|
"id": "script-1752471000020",
|
||||||
@ -434,7 +696,7 @@
|
|||||||
},
|
},
|
||||||
"type": "form",
|
"type": "form",
|
||||||
"label": "Borang Maklumat Peribadi",
|
"label": "Borang Maklumat Peribadi",
|
||||||
"position": { "x": 1275, "y": -525 }
|
"position": { "x": 1260, "y": -540 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api-1751871528249",
|
"id": "api-1751871528249",
|
||||||
@ -451,7 +713,7 @@
|
|||||||
},
|
},
|
||||||
"type": "api",
|
"type": "api",
|
||||||
"label": "Submit Profile API",
|
"label": "Submit Profile API",
|
||||||
"position": { "x": 1275, "y": -345 }
|
"position": { "x": 1260, "y": -345 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "script-1751871635000",
|
"id": "script-1751871635000",
|
||||||
@ -572,7 +834,7 @@
|
|||||||
},
|
},
|
||||||
"type": "form",
|
"type": "form",
|
||||||
"label": "Borang Semak Dokumen",
|
"label": "Borang Semak Dokumen",
|
||||||
"position": { "x": 885, "y": 675 }
|
"position": { "x": 825, "y": 945 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "api-1751871750000",
|
"id": "api-1751871750000",
|
||||||
@ -589,7 +851,7 @@
|
|||||||
},
|
},
|
||||||
"type": "api",
|
"type": "api",
|
||||||
"label": "Submit Document Verification API",
|
"label": "Submit Document Verification API",
|
||||||
"position": { "x": 1050, "y": 510 }
|
"position": { "x": 1170, "y": 810 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "script-1751871770000",
|
"id": "script-1751871770000",
|
||||||
@ -667,12 +929,14 @@
|
|||||||
},
|
},
|
||||||
"type": "script",
|
"type": "script",
|
||||||
"label": "Process Verification Response",
|
"label": "Process Verification Response",
|
||||||
"position": { "x": 630, "y": 525 }
|
"position": { "x": 540, "y": 780 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gateway-1751871800000",
|
"id": "gateway-1751871800000",
|
||||||
"data": {
|
"data": {
|
||||||
"label": "Lengkap?",
|
"label": "Lengkap?",
|
||||||
|
"shape": "diamond",
|
||||||
|
"textColor": "#333333",
|
||||||
"conditions": [
|
"conditions": [
|
||||||
{
|
{
|
||||||
"id": "condition-group-ya-path",
|
"id": "condition-group-ya-path",
|
||||||
@ -703,18 +967,22 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"borderColor": "#6b7280",
|
||||||
"defaultPath": "notification-1751872000000",
|
"defaultPath": "notification-1751872000000",
|
||||||
"description": "Check if documents are complete",
|
"description": "Check if documents are complete",
|
||||||
"gatewayType": "exclusive"
|
"gatewayType": "exclusive",
|
||||||
|
"backgroundColor": "#f9fafb"
|
||||||
},
|
},
|
||||||
"type": "gateway",
|
"type": "gateway",
|
||||||
"label": "Lengkap?",
|
"label": "Lengkap?",
|
||||||
"position": { "x": 1350, "y": 315 }
|
"position": { "x": 1935, "y": 510 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "business-rule-1751871900000",
|
"id": "business-rule-1751871900000",
|
||||||
"data": {
|
"data": {
|
||||||
"label": "Analisis Had Kifayah",
|
"label": "Analisis Had Kifayah",
|
||||||
|
"shape": "rectangle",
|
||||||
|
"textColor": "#1e40af",
|
||||||
"ruleGroups": [
|
"ruleGroups": [
|
||||||
{
|
{
|
||||||
"id": "group-1",
|
"id": "group-1",
|
||||||
@ -828,13 +1096,15 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"borderColor": "#3b82f6",
|
||||||
"description": "Calculate Had Kifayah based on household composition and income",
|
"description": "Calculate Had Kifayah based on household composition and income",
|
||||||
"errorVariable": "kifayahCalculationError",
|
"errorVariable": "kifayahCalculationError",
|
||||||
"outputVariable": "kifayahCalculationResult"
|
"outputVariable": "kifayahCalculationResult",
|
||||||
|
"backgroundColor": "#eff6ff"
|
||||||
},
|
},
|
||||||
"type": "business-rule",
|
"type": "business-rule",
|
||||||
"label": "Analisis Had Kifayah",
|
"label": "Analisis Had Kifayah",
|
||||||
"position": { "x": 1665, "y": 120 }
|
"position": { "x": 2445, "y": -45 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "notification-1751871950000",
|
"id": "notification-1751871950000",
|
||||||
@ -851,7 +1121,7 @@
|
|||||||
},
|
},
|
||||||
"type": "notification",
|
"type": "notification",
|
||||||
"label": "Hantar Keputusan Had Kifayah",
|
"label": "Hantar Keputusan Had Kifayah",
|
||||||
"position": { "x": 1890, "y": 30 }
|
"position": { "x": 2730, "y": 690 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "notification-1751872000000",
|
"id": "notification-1751872000000",
|
||||||
@ -868,7 +1138,7 @@
|
|||||||
},
|
},
|
||||||
"type": "notification",
|
"type": "notification",
|
||||||
"label": "Terima Notifikasi",
|
"label": "Terima Notifikasi",
|
||||||
"position": { "x": 1485, "y": 360 }
|
"position": { "x": 1455, "y": 915 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "end-1751872100000",
|
"id": "end-1751872100000",
|
||||||
@ -878,7 +1148,7 @@
|
|||||||
},
|
},
|
||||||
"type": "end",
|
"type": "end",
|
||||||
"label": "End",
|
"label": "End",
|
||||||
"position": { "x": 1935, "y": 390 }
|
"position": { "x": 1710, "y": 930 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "html-1752109761532",
|
"id": "html-1752109761532",
|
||||||
@ -899,7 +1169,7 @@
|
|||||||
},
|
},
|
||||||
"type": "html",
|
"type": "html",
|
||||||
"label": "Family Tree",
|
"label": "Family Tree",
|
||||||
"position": { "x": 2385, "y": -360 }
|
"position": { "x": 2535, "y": -435 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "rectangle-shape-1752110224921",
|
"id": "rectangle-shape-1752110224921",
|
||||||
@ -978,7 +1248,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"label": "",
|
"label": "",
|
||||||
"shape": "rectangle",
|
"shape": "rectangle",
|
||||||
"width": 650,
|
"width": 750,
|
||||||
"height": 400,
|
"height": 400,
|
||||||
"isShape": true,
|
"isShape": true,
|
||||||
"shapeType": "rectangle",
|
"shapeType": "rectangle",
|
||||||
@ -1010,7 +1280,7 @@
|
|||||||
},
|
},
|
||||||
"type": "api",
|
"type": "api",
|
||||||
"label": "Called Family Tree",
|
"label": "Called Family Tree",
|
||||||
"position": { "x": 2070, "y": -345 }
|
"position": { "x": 2295, "y": -525 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "text-annotation-1752114833800",
|
"id": "text-annotation-1752114833800",
|
||||||
@ -1028,7 +1298,7 @@
|
|||||||
},
|
},
|
||||||
"type": "text-annotation",
|
"type": "text-annotation",
|
||||||
"label": "BF-NAS-PRF-AS-FM",
|
"label": "BF-NAS-PRF-AS-FM",
|
||||||
"position": { "x": 2430, "y": -555 }
|
"position": { "x": 2520, "y": -555 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "form-1753000000000",
|
"id": "form-1753000000000",
|
||||||
@ -1148,11 +1418,200 @@
|
|||||||
"type": "text-annotation",
|
"type": "text-annotation",
|
||||||
"label": "BF-NAS-PRF-AS-QS-01",
|
"label": "BF-NAS-PRF-AS-QS-01",
|
||||||
"position": { "x": 810, "y": -30 }
|
"position": { "x": 810, "y": -30 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gateway-1753100000000",
|
||||||
|
"data": {
|
||||||
|
"label": "Profil Wujud?",
|
||||||
|
"shape": "diamond",
|
||||||
|
"textColor": "#374151",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "condition-group-ya-path",
|
||||||
|
"output": "Ya",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "condition-1",
|
||||||
|
"value": true,
|
||||||
|
"operator": "eq",
|
||||||
|
"variable": "profileFound",
|
||||||
|
"valueType": "boolean",
|
||||||
|
"logicalOperator": "and"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "condition-group-tidak-path",
|
||||||
|
"output": "Tidak",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "condition-2",
|
||||||
|
"value": false,
|
||||||
|
"operator": "eq",
|
||||||
|
"variable": "profileFound",
|
||||||
|
"valueType": "boolean",
|
||||||
|
"logicalOperator": "and"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"borderColor": "#6b7280",
|
||||||
|
"defaultPath": "form-1752471000000",
|
||||||
|
"description": "Check if profile exists in system",
|
||||||
|
"gatewayType": "exclusive",
|
||||||
|
"backgroundColor": "#f9fafb"
|
||||||
|
},
|
||||||
|
"type": "gateway",
|
||||||
|
"label": "Profil Wujud?",
|
||||||
|
"position": { "x": 1230, "y": 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "form-1753100000001",
|
||||||
|
"data": {
|
||||||
|
"label": "Pengesahan Kemaskini Profil",
|
||||||
|
"formId": 5,
|
||||||
|
"formName": "Pengesahan Kemaskini Profil",
|
||||||
|
"formUuid": "5e07fc8f-a160-478a-85fd-fa3364401546",
|
||||||
|
"description": "Borang pengesahan untuk kemaskini profil sedia ada",
|
||||||
|
"assignedRoles": [],
|
||||||
|
"assignedUsers": [],
|
||||||
|
"inputMappings": [
|
||||||
|
{
|
||||||
|
"formField": "nama_pemohon",
|
||||||
|
"processVariable": "profileData.nama"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"formField": "no_kad_pengenalan",
|
||||||
|
"processVariable": "profileData.noKadPengenalan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"formField": "tarikh_lahir",
|
||||||
|
"processVariable": "profileData.tarikhLahir"
|
||||||
|
},
|
||||||
|
{ "formField": "alamat", "processVariable": "profileData.alamat" }
|
||||||
|
],
|
||||||
|
"assignmentType": "public",
|
||||||
|
"outputMappings": [
|
||||||
|
{
|
||||||
|
"formField": "kemaskini_profil",
|
||||||
|
"processVariable": "kemaskiniProfil"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"formField": "sebab_kemaskini",
|
||||||
|
"processVariable": "sebabKemaskini"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConditions": [],
|
||||||
|
"assignmentVariable": "",
|
||||||
|
"assignmentVariableType": "user_id"
|
||||||
|
},
|
||||||
|
"type": "form",
|
||||||
|
"label": "Pengesahan Kemaskini Profil",
|
||||||
|
"position": { "x": 1200, "y": 195 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gateway-1753100000002",
|
||||||
|
"data": {
|
||||||
|
"label": "Perubahan Profil?",
|
||||||
|
"shape": "diamond",
|
||||||
|
"textColor": "#374151",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "condition-group-ya-path",
|
||||||
|
"output": "Ya",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "condition-1",
|
||||||
|
"value": "ya",
|
||||||
|
"operator": "eq",
|
||||||
|
"variable": "kemaskiniProfil",
|
||||||
|
"valueType": "string",
|
||||||
|
"logicalOperator": "and"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "condition-group-tidak-path",
|
||||||
|
"output": "Tidak",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "condition-2",
|
||||||
|
"value": "tidak",
|
||||||
|
"operator": "eq",
|
||||||
|
"variable": "kemaskiniProfil",
|
||||||
|
"valueType": "string",
|
||||||
|
"logicalOperator": "and"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"borderColor": "#6b7280",
|
||||||
|
"defaultPath": "form-1752471000000",
|
||||||
|
"description": "Check if user wants to update profile",
|
||||||
|
"gatewayType": "exclusive",
|
||||||
|
"backgroundColor": "#f9fafb"
|
||||||
|
},
|
||||||
|
"type": "gateway",
|
||||||
|
"label": "Perubahan Profil?",
|
||||||
|
"position": { "x": 1440, "y": 60 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "subprocess-1753100000003",
|
||||||
|
"data": {
|
||||||
|
"label": "BF-NAS-PRF-AS-UP-02",
|
||||||
|
"shape": "rectangle",
|
||||||
|
"processId": "kemaskini-profil-process",
|
||||||
|
"textColor": "#134e4a",
|
||||||
|
"borderColor": "#14b8a6",
|
||||||
|
"description": "Sub Process: Kemaskini Profil",
|
||||||
|
"subprocessId": 3,
|
||||||
|
"inputMappings": [
|
||||||
|
{
|
||||||
|
"processVariable": "profileData",
|
||||||
|
"subprocessVariable": "profileData"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"processVariable": "sebabKemaskini",
|
||||||
|
"subprocessVariable": "sebabKemaskini"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputMappings": [
|
||||||
|
{
|
||||||
|
"processVariable": "updatedProfileData",
|
||||||
|
"subprocessVariable": "profileData"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"processVariable": "profileUpdateStatus",
|
||||||
|
"subprocessVariable": "updateStatus"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subprocessName": "BF-NAS-PRF-AS-UP-02",
|
||||||
|
"backgroundColor": "#f0fdfa"
|
||||||
|
},
|
||||||
|
"type": "subprocess",
|
||||||
|
"label": "BF-NAS-PRF-AS-UP-02",
|
||||||
|
"position": { "x": 1635, "y": -30 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "subprocess-1752202751327",
|
||||||
|
"data": {
|
||||||
|
"label": "BF-NAS-BTN-PB",
|
||||||
|
"shape": "rectangle",
|
||||||
|
"textColor": "#134e4a",
|
||||||
|
"borderColor": "#14b8a6",
|
||||||
|
"description": "Sub Process: Bantuan",
|
||||||
|
"subprocessId": 4,
|
||||||
|
"subprocessName": "BF-NAS-BTN-PB",
|
||||||
|
"backgroundColor": "#f0fdfa"
|
||||||
|
},
|
||||||
|
"type": "subprocess",
|
||||||
|
"label": "BF-NAS-BTN-PB",
|
||||||
|
"position": { "x": 1635, "y": 180 }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"x": -104.1414298310864,
|
"x": -818.4067095426772,
|
||||||
"y": 273.7689874210555,
|
"y": 210.4283305655609,
|
||||||
"zoom": 0.402665859661672
|
"zoom": 0.5930425475009586
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -319,12 +319,18 @@ const hasCurrentProcess = computed(() => {
|
|||||||
const canvasNodes = computed(() => {
|
const canvasNodes = computed(() => {
|
||||||
if (!processStore.currentProcess) return [];
|
if (!processStore.currentProcess) return [];
|
||||||
// Return a deep copy to ensure proper reactivity for nested objects like data
|
// Return a deep copy to ensure proper reactivity for nested objects like data
|
||||||
return processStore.currentProcess.nodes.map(node => ({
|
return processStore.currentProcess.nodes.map(node => {
|
||||||
...node,
|
const isShape = node.data?.isShape || false;
|
||||||
data: { ...node.data },
|
const style = isShape ? { zIndex: node.data.zIndex ?? 0 } : {};
|
||||||
// Ensure labels are synchronized both ways
|
|
||||||
label: node.label || node.data?.label || node.type
|
return {
|
||||||
}));
|
...node,
|
||||||
|
style: { ...(node.style || {}), ...style },
|
||||||
|
data: { ...node.data },
|
||||||
|
// Ensure labels are synchronized both ways
|
||||||
|
label: node.label || node.data?.label || node.type
|
||||||
|
};
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const canvasEdges = computed(() => {
|
const canvasEdges = computed(() => {
|
||||||
@ -716,9 +722,14 @@ const updateNodeInStore = async () => {
|
|||||||
|
|
||||||
// Force canvas update directly for immediate feedback
|
// Force canvas update directly for immediate feedback
|
||||||
if (processFlowCanvas.value && processFlowCanvas.value.updateNode) {
|
if (processFlowCanvas.value && processFlowCanvas.value.updateNode) {
|
||||||
|
// Include z-index in style if this is a shape
|
||||||
|
const isShape = newData?.isShape || false;
|
||||||
|
const style = isShape ? { zIndex: newData.zIndex ?? 0 } : {};
|
||||||
|
|
||||||
processFlowCanvas.value.updateNode(nodeId, {
|
processFlowCanvas.value.updateNode(nodeId, {
|
||||||
label: newLabel,
|
label: newLabel,
|
||||||
data: newData
|
data: newData,
|
||||||
|
style: style
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -750,6 +761,9 @@ const refreshSelectedNodeFromStore = () => {
|
|||||||
// Special handler for shape size updates to force reactivity
|
// Special handler for shape size updates to force reactivity
|
||||||
const updateShapeSize = async () => {
|
const updateShapeSize = async () => {
|
||||||
if (selectedNodeData.value && selectedNodeData.value.data?.isShape) {
|
if (selectedNodeData.value && selectedNodeData.value.data?.isShape) {
|
||||||
|
// Get the z-index before updating
|
||||||
|
const zIndex = selectedNodeData.value.data.zIndex || 0;
|
||||||
|
|
||||||
// Force update the node in store with new size
|
// Force update the node in store with new size
|
||||||
updateNodeInStore();
|
updateNodeInStore();
|
||||||
|
|
||||||
@ -757,8 +771,21 @@ const updateShapeSize = async () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
if (processFlowCanvas.value && processFlowCanvas.value.updateNode) {
|
if (processFlowCanvas.value && processFlowCanvas.value.updateNode) {
|
||||||
processFlowCanvas.value.updateNode(selectedNodeData.value.id, {
|
processFlowCanvas.value.updateNode(selectedNodeData.value.id, {
|
||||||
data: { ...selectedNodeData.value.data }
|
data: { ...selectedNodeData.value.data },
|
||||||
|
style: { zIndex }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Also directly manipulate DOM for immediate feedback
|
||||||
|
const nodeElement = document.querySelector(`[data-id="${selectedNodeData.value.id}"]`);
|
||||||
|
if (nodeElement) {
|
||||||
|
nodeElement.style.zIndex = zIndex;
|
||||||
|
|
||||||
|
// Also update custom-node if it exists
|
||||||
|
const customNode = nodeElement.querySelector('.custom-node');
|
||||||
|
if (customNode) {
|
||||||
|
customNode.style.zIndex = zIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -927,7 +954,7 @@ const onEdgesChange = (changes, currentEdges) => {
|
|||||||
source: edge.source,
|
source: edge.source,
|
||||||
target: edge.target,
|
target: edge.target,
|
||||||
label: edge.label || '',
|
label: edge.label || '',
|
||||||
type: edge.type || 'smoothstep',
|
type: edge.type || 'custom',
|
||||||
animated: edge.animated !== undefined ? edge.animated : true,
|
animated: edge.animated !== undefined ? edge.animated : true,
|
||||||
data: edge.data || {},
|
data: edge.data || {},
|
||||||
sourceHandle: edge.sourceHandle,
|
sourceHandle: edge.sourceHandle,
|
||||||
@ -1292,6 +1319,42 @@ const onAddComponent = async (component) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If this is a shape node, set an initial z-index
|
||||||
|
if (component.data?.isShape) {
|
||||||
|
// Find the highest z-index among existing shapes
|
||||||
|
let highestZ = 0;
|
||||||
|
if (processStore.currentProcess && processStore.currentProcess.nodes) {
|
||||||
|
processStore.currentProcess.nodes.forEach(node => {
|
||||||
|
if (node.data?.isShape) {
|
||||||
|
const nodeZ = parseInt(node.data.zIndex || 0, 10);
|
||||||
|
highestZ = Math.max(highestZ, nodeZ);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new shape's z-index to be on top of existing shapes
|
||||||
|
// Add 10 to ensure it's clearly above other shapes
|
||||||
|
const newZ = highestZ + 10;
|
||||||
|
newNode.data.zIndex = newZ;
|
||||||
|
|
||||||
|
// Add style with z-index
|
||||||
|
newNode.style = { zIndex: newZ };
|
||||||
|
|
||||||
|
// After the node is added, directly manipulate the DOM to ensure z-index is applied
|
||||||
|
setTimeout(() => {
|
||||||
|
const nodeElement = document.querySelector(`[data-id="${newNode.id}"]`);
|
||||||
|
if (nodeElement) {
|
||||||
|
nodeElement.style.zIndex = newZ;
|
||||||
|
|
||||||
|
// Also update custom-node if it exists
|
||||||
|
const customNode = nodeElement.querySelector('.custom-node');
|
||||||
|
if (customNode) {
|
||||||
|
customNode.style.zIndex = newZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
// Add the node to the process store
|
// Add the node to the process store
|
||||||
await processStore.addNode(newNode);
|
await processStore.addNode(newNode);
|
||||||
|
|
||||||
@ -1430,7 +1493,7 @@ const applyProcessTemplate = async (template) => {
|
|||||||
const newEdge = {
|
const newEdge = {
|
||||||
...edge,
|
...edge,
|
||||||
id: edge.id, // Keep original ID
|
id: edge.id, // Keep original ID
|
||||||
type: edge.type || 'smoothstep',
|
type: edge.type || 'custom',
|
||||||
animated: edge.animated !== undefined ? edge.animated : true
|
animated: edge.animated !== undefined ? edge.animated : true
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1825,6 +1888,199 @@ const canShowNodeActions = computed(() => {
|
|||||||
selectedNodeData.value.type !== 'end'
|
selectedNodeData.value.type !== 'end'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nodeZIndex = computed({
|
||||||
|
get: () => {
|
||||||
|
return selectedNodeData.value?.data?.isShape ? selectedNodeData.value.data.zIndex || 0 : 0;
|
||||||
|
},
|
||||||
|
set: (value) => {
|
||||||
|
if (selectedNodeData.value && selectedNodeData.value.data?.isShape) {
|
||||||
|
const z = parseInt(value, 10);
|
||||||
|
const newZ = isNaN(z) ? 0 : z;
|
||||||
|
selectedNodeData.value.data.zIndex = newZ;
|
||||||
|
updateNodeInStore();
|
||||||
|
|
||||||
|
// Force update the node style in the canvas
|
||||||
|
if (processFlowCanvas.value && processFlowCanvas.value.updateNode) {
|
||||||
|
processFlowCanvas.value.updateNode(selectedNodeData.value.id, {
|
||||||
|
style: { zIndex: newZ }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also directly manipulate DOM for immediate feedback
|
||||||
|
nextTick(() => {
|
||||||
|
const nodeElement = document.querySelector(`[data-id="${selectedNodeData.value.id}"]`);
|
||||||
|
if (nodeElement) {
|
||||||
|
nodeElement.style.zIndex = newZ;
|
||||||
|
|
||||||
|
// Also update custom-node if it exists
|
||||||
|
const customNode = nodeElement.querySelector('.custom-node');
|
||||||
|
if (customNode) {
|
||||||
|
customNode.style.zIndex = newZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add functions to bring forward and send backward shapes
|
||||||
|
const bringForward = () => {
|
||||||
|
if (selectedNodeData.value && selectedNodeData.value.data?.isShape) {
|
||||||
|
// Increment z-index by 1
|
||||||
|
const currentZ = parseInt(selectedNodeData.value.data.zIndex || 0, 10);
|
||||||
|
const newZ = currentZ + 1;
|
||||||
|
selectedNodeData.value.data.zIndex = newZ;
|
||||||
|
|
||||||
|
// Update in store
|
||||||
|
updateNodeInStore();
|
||||||
|
|
||||||
|
// Force update the node style in the canvas
|
||||||
|
if (processFlowCanvas.value && processFlowCanvas.value.updateNode) {
|
||||||
|
processFlowCanvas.value.updateNode(selectedNodeData.value.id, {
|
||||||
|
style: { zIndex: newZ }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also directly manipulate DOM for immediate feedback
|
||||||
|
nextTick(() => {
|
||||||
|
const nodeElement = document.querySelector(`[data-id="${selectedNodeData.value.id}"]`);
|
||||||
|
if (nodeElement) {
|
||||||
|
nodeElement.style.zIndex = newZ;
|
||||||
|
|
||||||
|
// Also update custom-node if it exists
|
||||||
|
const customNode = nodeElement.querySelector('.custom-node');
|
||||||
|
if (customNode) {
|
||||||
|
customNode.style.zIndex = newZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendBackward = () => {
|
||||||
|
if (selectedNodeData.value && selectedNodeData.value.data?.isShape) {
|
||||||
|
// Decrement z-index by 1, but not below 0
|
||||||
|
const currentZ = parseInt(selectedNodeData.value.data.zIndex || 0, 10);
|
||||||
|
const newZ = Math.max(0, currentZ - 1);
|
||||||
|
selectedNodeData.value.data.zIndex = newZ;
|
||||||
|
|
||||||
|
// Update in store
|
||||||
|
updateNodeInStore();
|
||||||
|
|
||||||
|
// Force update the node style in the canvas
|
||||||
|
if (processFlowCanvas.value && processFlowCanvas.value.updateNode) {
|
||||||
|
processFlowCanvas.value.updateNode(selectedNodeData.value.id, {
|
||||||
|
style: { zIndex: newZ }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also directly manipulate DOM for immediate feedback
|
||||||
|
nextTick(() => {
|
||||||
|
const nodeElement = document.querySelector(`[data-id="${selectedNodeData.value.id}"]`);
|
||||||
|
if (nodeElement) {
|
||||||
|
nodeElement.style.zIndex = newZ;
|
||||||
|
|
||||||
|
// Also update custom-node if it exists
|
||||||
|
const customNode = nodeElement.querySelector('.custom-node');
|
||||||
|
if (customNode) {
|
||||||
|
customNode.style.zIndex = newZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bringToFront = () => {
|
||||||
|
if (selectedNodeData.value && selectedNodeData.value.data?.isShape) {
|
||||||
|
// Find the highest z-index in the current shapes
|
||||||
|
let highestZ = 0;
|
||||||
|
if (processStore.currentProcess && processStore.currentProcess.nodes) {
|
||||||
|
processStore.currentProcess.nodes.forEach(node => {
|
||||||
|
if (node.data?.isShape) {
|
||||||
|
const nodeZ = parseInt(node.data.zIndex || 0, 10);
|
||||||
|
highestZ = Math.max(highestZ, nodeZ);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set to higher than the highest found (add 10 to ensure it's clearly on top)
|
||||||
|
const newZ = highestZ + 10;
|
||||||
|
selectedNodeData.value.data.zIndex = newZ;
|
||||||
|
|
||||||
|
// Update in store
|
||||||
|
updateNodeInStore();
|
||||||
|
|
||||||
|
// Force update the node style in the canvas
|
||||||
|
if (processFlowCanvas.value && processFlowCanvas.value.updateNode) {
|
||||||
|
processFlowCanvas.value.updateNode(selectedNodeData.value.id, {
|
||||||
|
style: { zIndex: newZ }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also directly manipulate DOM for immediate feedback
|
||||||
|
nextTick(() => {
|
||||||
|
const nodeElement = document.querySelector(`[data-id="${selectedNodeData.value.id}"]`);
|
||||||
|
if (nodeElement) {
|
||||||
|
nodeElement.style.zIndex = newZ;
|
||||||
|
|
||||||
|
// Also update custom-node if it exists
|
||||||
|
const customNode = nodeElement.querySelector('.custom-node');
|
||||||
|
if (customNode) {
|
||||||
|
customNode.style.zIndex = newZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendToBack = () => {
|
||||||
|
if (selectedNodeData.value && selectedNodeData.value.data?.isShape) {
|
||||||
|
// Find the lowest z-index in the current shapes
|
||||||
|
let lowestZ = Number.MAX_SAFE_INTEGER;
|
||||||
|
if (processStore.currentProcess && processStore.currentProcess.nodes) {
|
||||||
|
processStore.currentProcess.nodes.forEach(node => {
|
||||||
|
if (node.data?.isShape && node.id !== selectedNodeData.value.id) {
|
||||||
|
const nodeZ = parseInt(node.data.zIndex || 0, 10);
|
||||||
|
lowestZ = Math.min(lowestZ, nodeZ);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no other shapes were found, set to 0
|
||||||
|
if (lowestZ === Number.MAX_SAFE_INTEGER) {
|
||||||
|
lowestZ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set to lower than the lowest found (subtract 10 to ensure it's clearly below)
|
||||||
|
const newZ = Math.max(0, lowestZ - 10);
|
||||||
|
selectedNodeData.value.data.zIndex = newZ;
|
||||||
|
|
||||||
|
// Update in store
|
||||||
|
updateNodeInStore();
|
||||||
|
|
||||||
|
// Force update the node style in the canvas
|
||||||
|
if (processFlowCanvas.value && processFlowCanvas.value.updateNode) {
|
||||||
|
processFlowCanvas.value.updateNode(selectedNodeData.value.id, {
|
||||||
|
style: { zIndex: newZ }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also directly manipulate DOM for immediate feedback
|
||||||
|
nextTick(() => {
|
||||||
|
const nodeElement = document.querySelector(`[data-id="${selectedNodeData.value.id}"]`);
|
||||||
|
if (nodeElement) {
|
||||||
|
nodeElement.style.zIndex = newZ;
|
||||||
|
|
||||||
|
// Also update custom-node if it exists
|
||||||
|
const customNode = nodeElement.querySelector('.custom-node');
|
||||||
|
if (customNode) {
|
||||||
|
customNode.style.zIndex = newZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -2130,33 +2386,92 @@ const canShowNodeActions = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Size Controls for Shapes -->
|
<!-- Size Controls for Shapes -->
|
||||||
<div v-if="selectedNodeData.data?.isShape" class="space-y-3">
|
<div v-if="selectedNodeData.data?.isShape" class="space-y-4">
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<h4 class="text-sm font-medium text-gray-700 mb-2 flex items-center">
|
||||||
<div>
|
<Icon name="material-symbols:straighten" class="w-4 h-4 mr-2" />
|
||||||
<label class="block text-xs font-medium text-gray-600 mb-1">Width</label>
|
Size
|
||||||
<input
|
</h4>
|
||||||
v-model.number="selectedNodeData.data.width"
|
|
||||||
type="number"
|
<div class="grid grid-cols-2 gap-3">
|
||||||
min="50"
|
<div>
|
||||||
max="2000"
|
<label class="block text-xs font-medium text-gray-600 mb-1">Width</label>
|
||||||
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
<input
|
||||||
@input="updateShapeSize"
|
v-model.number="selectedNodeData.data.width"
|
||||||
/>
|
type="number"
|
||||||
|
min="50"
|
||||||
|
max="2000"
|
||||||
|
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
@input="updateShapeSize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-600 mb-1">Height</label>
|
||||||
|
<input
|
||||||
|
v-model.number="selectedNodeData.data.height"
|
||||||
|
type="number"
|
||||||
|
min="50"
|
||||||
|
max="2000"
|
||||||
|
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
@input="updateShapeSize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-medium text-gray-600 mb-1">Height</label>
|
<!-- Z-Index control with heading -->
|
||||||
<input
|
<div class="mt-6 pt-4 border-t border-gray-100">
|
||||||
v-model.number="selectedNodeData.data.height"
|
<h4 class="text-sm font-medium text-gray-700 mb-2 flex items-center">
|
||||||
type="number"
|
<Icon name="material-symbols:layers" class="w-4 h-4 mr-2" />
|
||||||
min="50"
|
Layer Position
|
||||||
max="2000"
|
</h4>
|
||||||
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
@input="updateShapeSize"
|
<div class="mb-3">
|
||||||
/>
|
<label class="block text-xs font-medium text-gray-600 mb-1">Stack Order (Z-Index)</label>
|
||||||
|
<input
|
||||||
|
v-model.number="nodeZIndex"
|
||||||
|
type="number"
|
||||||
|
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
<div class="flex justify-center space-x-2 mt-2">
|
||||||
|
<button
|
||||||
|
@click="sendToBack"
|
||||||
|
title="Send to Back"
|
||||||
|
class="p-2 bg-gray-100 hover:bg-gray-200 rounded border border-gray-300"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:vertical-align-bottom" class="w-4 h-4 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="sendBackward"
|
||||||
|
title="Send Backward"
|
||||||
|
class="p-2 bg-gray-100 hover:bg-gray-200 rounded border border-gray-300"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:arrow-downward" class="w-4 h-4 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="bringForward"
|
||||||
|
title="Bring Forward"
|
||||||
|
class="p-2 bg-gray-100 hover:bg-gray-200 rounded border border-gray-300"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:arrow-upward" class="w-4 h-4 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="bringToFront"
|
||||||
|
title="Bring to Front"
|
||||||
|
class="p-2 bg-gray-100 hover:bg-gray-200 rounded border border-gray-300"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:vertical-align-top" class="w-4 h-4 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-blue-50 p-2 rounded-md">
|
||||||
|
<p class="text-xs text-blue-700">
|
||||||
|
<strong>Tip:</strong> Use these controls to arrange the stacking order of shapes. Higher values appear on top of other shapes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -2274,7 +2589,7 @@ const canShowNodeActions = computed(() => {
|
|||||||
<div v-if="selectedNodeData.type === 'gateway'">
|
<div v-if="selectedNodeData.type === 'gateway'">
|
||||||
<p class="text-xs text-gray-600 mb-3">Define decision conditions and workflow paths.</p>
|
<p class="text-xs text-gray-600 mb-3">Define decision conditions and workflow paths.</p>
|
||||||
<RsButton @click="showGatewayConfigModal = true" variant="primary" class="w-full">
|
<RsButton @click="showGatewayConfigModal = true" variant="primary" class="w-full">
|
||||||
<Icon name="material-symbols:call_split" class="w-4 h-4 mr-2" />
|
<Icon name="material-symbols:call-split" class="w-4 h-4 mr-2" />
|
||||||
Configure Decision Paths
|
Configure Decision Paths
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
@ -3258,12 +3573,10 @@ const canShowNodeActions = computed(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
z-index: -10 !important; /* Always behind process nodes */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.shape-node.selected) {
|
:deep(.shape-node.selected) {
|
||||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.6);
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.6);
|
||||||
z-index: -8 !important; /* Higher z-index when selected, but still behind process nodes */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure process nodes always stay above shapes, even during drag operations */
|
/* Ensure process nodes always stay above shapes, even during drag operations */
|
||||||
@ -3320,11 +3633,9 @@ const canShowNodeActions = computed(() => {
|
|||||||
/* Ensure custom nodes (process nodes) maintain proper layering */
|
/* Ensure custom nodes (process nodes) maintain proper layering */
|
||||||
:deep(.custom-node:not(.shape-node)) {
|
:deep(.custom-node:not(.shape-node)) {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.custom-node:not(.shape-node).selected) {
|
:deep(.custom-node:not(.shape-node).selected) {
|
||||||
z-index: 15 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure edges/connection lines stay above shapes */
|
/* Ensure edges/connection lines stay above shapes */
|
||||||
|
Loading…
x
Reference in New Issue
Block a user