Enhance Process Builder with HTML and Subprocess Node Features

- Introduced new HTML and Subprocess nodes in ProcessBuilderComponents.vue, allowing users to add custom HTML content and execute subprocesses within the process flow.
- Updated ProcessFlowNodes.js to include HtmlNode and SubprocessNode components with appropriate properties and rendering logic.
- Enhanced ProcessFlowCanvas.vue to manage the new node types effectively, ensuring proper integration with existing flow functionalities.
- Improved index.vue to support configuration modals for HTML and Subprocess nodes, enhancing user interaction and customization options.
- Refactored process management logic to accommodate new node types, ensuring seamless integration and consistent user experience across the process builder.
This commit is contained in:
Md Afiq Iskandar 2025-07-10 11:08:16 +08:00
parent 5faefb8900
commit b4eb3265c2
13 changed files with 2585 additions and 2072 deletions

View File

@ -0,0 +1,700 @@
<template>
<div class="html-node-config">
<div class="config-content">
<!-- Header -->
<div class="config-header">
<h3 class="text-lg font-semibold text-gray-800">HTML Node Configuration</h3>
<p class="text-sm text-gray-600">Configure custom HTML content for your process</p>
</div>
<!-- Basic Info Section -->
<div class="config-section">
<h4 class="section-title">Basic Information</h4>
<div class="section-content">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormKit
type="text"
label="HTML Node Name"
name="label"
v-model="localNodeData.label"
help="Display name for this HTML node"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., Custom Form, Information Display"
validation="required"
/>
<FormKit
type="textarea"
label="Description"
name="description"
v-model="localNodeData.description"
help="Describe what this HTML content does"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., Displays a custom form for user input"
rows="2"
/>
</div>
</div>
</div>
<!-- HTML Content Section -->
<div class="config-section">
<h4 class="section-title">HTML Content</h4>
<div class="section-content">
<!-- Code Editor Tabs -->
<div class="code-editor-tabs">
<div class="tabs-header border-b border-gray-200">
<div class="flex">
<button
@click="activeTab = 'html'"
class="px-4 py-2 text-sm font-medium"
:class="activeTab === 'html' ? 'text-blue-600 border-b-2 border-blue-500' : 'text-gray-600 hover:text-gray-800'"
>
<span class="flex items-center">
<Icon name="material-symbols:code" class="w-4 h-4 mr-2" />
HTML
</span>
</button>
<button
@click="activeTab = 'css'"
class="px-4 py-2 text-sm font-medium"
:class="activeTab === 'css' ? 'text-blue-600 border-b-2 border-blue-500' : 'text-gray-600 hover:text-gray-800'"
>
<span class="flex items-center">
<Icon name="material-symbols:format-color-fill" class="w-4 h-4 mr-2" />
CSS
</span>
</button>
<button
@click="activeTab = 'js'"
class="px-4 py-2 text-sm font-medium"
:class="activeTab === 'js' ? 'text-blue-600 border-b-2 border-blue-500' : 'text-gray-600 hover:text-gray-800'"
>
<span class="flex items-center">
<Icon name="material-symbols:code-blocks" class="w-4 h-4 mr-2" />
JavaScript
</span>
</button>
</div>
</div>
<!-- HTML Tab -->
<div v-show="activeTab === 'html'" class="tab-content py-4">
<div class="html-editor-container">
<RsCodeMirror
v-model="localNodeData.htmlCode"
:options="{
mode: 'htmlmixed',
theme: 'default',
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
autoCloseTags: true,
matchBrackets: true,
matchTags: true,
indentUnit: 2,
tabSize: 2
}"
class="html-editor"
placeholder="<!-- Enter your HTML code here -->
<div class='custom-html-content'>
<h2>Custom HTML Content</h2>
<p>This is a custom HTML node that can display rich content.</p>
<form>
<div class='form-group'>
<label>Name:</label>
<input type='text' class='form-control' />
</div>
<button type='button' class='btn'>Submit</button>
</form>
</div>"
/>
</div>
<p class="text-xs text-gray-500 mt-2">
💡 You can use variables in your HTML with double curly braces: <code>{{ variableSyntaxExample }}</code>
</p>
</div>
<!-- CSS Tab -->
<div v-show="activeTab === 'css'" class="tab-content py-4">
<div class="css-editor-container">
<RsCodeMirror
v-model="localNodeData.cssCode"
:options="{
mode: 'css',
theme: 'default',
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets: true,
indentUnit: 2,
tabSize: 2
}"
class="css-editor"
placeholder="/* Add your custom CSS styles here */
.custom-html-content {
padding: 15px;
border-radius: 5px;
background-color: #f5f5f5;
}
.form-group {
margin-bottom: 15px;
}
.form-control {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.btn {
padding: 8px 16px;
background-color: #4a6cf7;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}"
/>
</div>
<p class="text-xs text-gray-500 mt-2">
💡 Add CSS to style your HTML content. These styles will be scoped to this HTML node only.
</p>
</div>
<!-- JavaScript Tab -->
<div v-show="activeTab === 'js'" class="tab-content py-4">
<div class="js-editor-container">
<RsCodeMirror
v-model="localNodeData.jsCode"
:options="{
mode: 'javascript',
theme: 'default',
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets: true,
indentUnit: 2,
tabSize: 2
}"
class="js-editor"
placeholder="// Add your custom JavaScript here
// This will be executed when the HTML is rendered
document.addEventListener('DOMContentLoaded', function() {
// Access elements in your custom HTML
const button = document.querySelector('.btn');
if (button) {
button.addEventListener('click', function() {
// Handle button click
console.log('Button clicked!');
// You can access process variables like this:
// const value = processVariables.someVariable;
// And update them like this:
// processVariables.outputValue = 'New value';
});
}
});"
/>
</div>
<p class="text-xs text-gray-500 mt-2">
💡 Use <code>processVariables</code> to access and modify process data.
Available variables: {{ availableVariableNames.join(', ') || 'None' }}
</p>
</div>
</div>
<!-- Variable Integration -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
<FormKit
type="checkbox"
label="Allow Variable Access"
name="allowVariableAccess"
v-model="localNodeData.allowVariableAccess"
help="If enabled, HTML can access and modify process variables"
:classes="{ outer: 'field-wrapper' }"
/>
<FormKit
type="checkbox"
label="Auto-refresh on Variable Change"
name="autoRefresh"
v-model="localNodeData.autoRefresh"
help="If enabled, HTML content will refresh when variables change"
:classes="{ outer: 'field-wrapper' }"
/>
</div>
</div>
</div>
<!-- Variables Section -->
<div class="config-section">
<h4 class="section-title">Variable Management</h4>
<div class="section-content">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Input Variables -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Input Variables
</label>
<div class="variable-list">
<div
v-for="variable in availableVariables"
:key="variable.name"
class="variable-item"
:class="{ 'selected': isInputVariable(variable.name) }"
@click="toggleInputVariable(variable.name)"
>
<div class="variable-info">
<span class="variable-name">{{ variable.name }}</span>
<span class="variable-type">{{ variable.type }}</span>
</div>
<span class="variable-description">{{ variable.description }}</span>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">
Click to select variables this HTML node will read from
</p>
</div>
<!-- Output Variables -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Output Variables
</label>
<div class="space-y-2">
<div
v-for="(output, index) in localNodeData.outputVariables"
:key="index"
class="flex items-center space-x-2"
>
<FormKit
type="text"
v-model="output.name"
placeholder="Variable name"
:classes="{ outer: 'flex-1' }"
/>
<FormKit
type="select"
v-model="output.type"
:options="[
{ label: 'String', value: 'string' },
{ label: 'Number', value: 'number' },
{ label: 'Boolean', value: 'boolean' },
{ label: 'Object', value: 'object' },
{ label: 'Array', value: 'array' }
]"
:classes="{ outer: 'flex-1' }"
/>
<button
@click="removeOutputVariable(index)"
class="p-2 text-red-600 hover:bg-red-50 rounded"
>
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
</button>
</div>
<button
@click="addOutputVariable"
class="flex items-center space-x-2 px-3 py-2 text-blue-600 hover:bg-blue-50 rounded border border-dashed border-blue-300"
>
<Icon name="material-symbols:add" class="w-4 h-4" />
<span class="text-sm">Add Output Variable</span>
</button>
</div>
<p class="text-xs text-gray-500 mt-2">
Define variables this HTML node will create or modify
</p>
</div>
</div>
</div>
</div>
<!-- HTML Preview Section -->
<div class="config-section">
<h4 class="section-title">Preview</h4>
<div class="section-content">
<div class="preview-container p-4 bg-gray-50 rounded-lg">
<div class="flex items-center justify-between mb-3">
<h5 class="font-medium text-gray-700">HTML Preview</h5>
<div class="flex items-center space-x-2">
<span class="text-xs text-gray-500">Auto-refresh</span>
<label class="switch">
<input type="checkbox" v-model="autoPreview">
<span class="slider round"></span>
</label>
<button
@click="previewHtml"
:disabled="!localNodeData.htmlCode.trim()"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-sm"
>
<Icon name="material-symbols:refresh" class="w-4 h-4 mr-1" />
Refresh Preview
</button>
</div>
</div>
<div class="preview-frame border border-gray-200 rounded bg-white p-4 min-h-[250px]">
<div v-if="!previewContent" class="flex items-center justify-center h-full text-gray-400">
<div class="text-center">
<Icon name="material-symbols:preview" class="w-8 h-8 mb-2 mx-auto" />
<p>Click "Refresh Preview" to see your HTML content</p>
</div>
</div>
<div v-else v-html="previewContent" class="html-preview"></div>
</div>
<div v-if="previewError" class="error-details mt-3 p-3 bg-red-50 border border-red-200 rounded text-sm text-red-700">
<div class="flex items-start">
<Icon name="material-symbols:error" class="w-5 h-5 mr-2 flex-shrink-0 text-red-500" />
<div>{{ previewError }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
// Props and emits
const props = defineProps({
nodeData: {
type: Object,
default: () => ({})
},
availableVariables: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['update'])
// Local state
const localNodeData = ref({
label: '',
description: '',
htmlCode: '<!-- Enter your HTML code here -->\n<div class="custom-html-content">\n <h2>Custom HTML Content</h2>\n <p>This is a custom HTML node that can display rich content.</p>\n</div>',
cssCode: '',
jsCode: '',
inputVariables: [],
outputVariables: [],
allowVariableAccess: true,
autoRefresh: false
})
// Tab state
const activeTab = ref('html')
// Preview state
const autoPreview = ref(true)
const previewContent = ref(null)
const previewError = ref(null)
// Computed property to avoid template syntax issues
const variableSyntaxExample = computed(() => '{{ variableName }}')
// Computed properties
const availableVariableNames = computed(() => {
return props.availableVariables.map(v => v.name)
})
// Methods
const isInputVariable = (variableName) => {
return localNodeData.value.inputVariables.includes(variableName)
}
const toggleInputVariable = (variableName) => {
const index = localNodeData.value.inputVariables.indexOf(variableName)
if (index > -1) {
localNodeData.value.inputVariables.splice(index, 1)
} else {
localNodeData.value.inputVariables.push(variableName)
}
emitUpdate()
}
const addOutputVariable = () => {
localNodeData.value.outputVariables.push({
name: '',
type: 'string',
description: ''
})
emitUpdate()
}
const removeOutputVariable = (index) => {
localNodeData.value.outputVariables.splice(index, 1)
emitUpdate()
}
const previewHtml = () => {
try {
previewError.value = null
// Combine HTML, CSS and JS for preview
let htmlContent = localNodeData.value.htmlCode || ''
const cssCode = localNodeData.value.cssCode || ''
const jsCode = localNodeData.value.jsCode || ''
// Add CSS if provided
if (cssCode) {
htmlContent = `<style>${cssCode}</style>${htmlContent}`
}
// Add JS if provided (in a safe way)
if (jsCode) {
// We don't actually execute JS in the preview for security reasons
// Just show a placeholder that JS will be included
htmlContent += `<div class="js-indicator text-xs text-gray-500 mt-4 p-2 bg-gray-100 rounded">
<strong>Note:</strong> JavaScript functionality will be available when deployed.
</div>`
}
// Replace variable placeholders with mock values
if (localNodeData.value.allowVariableAccess) {
const variableRegex = /\{\{\s*([a-zA-Z0-9_\.]+)\s*\}\}/g
htmlContent = htmlContent.replace(variableRegex, (match, varName) => {
const variable = props.availableVariables.find(v => v.name === varName)
if (variable) {
return getMockValueForType(variable.type)
}
return `[${varName}]`
})
}
previewContent.value = htmlContent
} catch (error) {
previewError.value = `Preview error: ${error.message}`
previewContent.value = null
}
}
const getMockValueForType = (type) => {
switch (type) {
case 'string': return 'Sample Text'
case 'number': return '42'
case 'boolean': return 'true'
case 'object': return '{object}'
case 'array': return '[array]'
default: return 'value'
}
}
const emitUpdate = () => {
emit('update', localNodeData.value)
}
// Watch for prop changes
watch(
() => props.nodeData,
(newData) => {
if (newData && Object.keys(newData).length > 0) {
localNodeData.value = {
label: newData.label || 'HTML Node',
description: newData.description || '',
htmlCode: newData.htmlCode || '<!-- Enter your HTML code here -->\n<div class="custom-html-content">\n <h2>Custom HTML Content</h2>\n <p>This is a custom HTML node that can display rich content.</p>\n</div>',
cssCode: newData.cssCode || '',
jsCode: newData.jsCode || '',
inputVariables: newData.inputVariables || [],
outputVariables: newData.outputVariables || [],
allowVariableAccess: newData.allowVariableAccess !== undefined ? newData.allowVariableAccess : true,
autoRefresh: newData.autoRefresh || false
}
}
},
{ immediate: true, deep: true }
)
// Watch for changes in localNodeData and emit updates
watch(
localNodeData,
() => {
emitUpdate()
},
{ deep: true }
)
// Watch for HTML, CSS, or JS changes to auto-refresh preview
watch(
[
() => localNodeData.value.htmlCode,
() => localNodeData.value.cssCode,
() => localNodeData.value.jsCode
],
() => {
if (autoPreview.value) {
previewHtml()
}
}
)
// Initialize preview on component mount
onMounted(() => {
if (autoPreview.value && localNodeData.value.htmlCode) {
previewHtml()
}
})
</script>
<style scoped>
.html-node-config {
@apply max-w-6xl mx-auto bg-white;
}
.config-content {
@apply p-6 space-y-8;
}
.config-header {
@apply border-b border-gray-200 pb-4;
}
.config-section {
@apply space-y-4;
}
.section-title {
@apply text-base font-semibold text-gray-800 mb-3;
}
.section-content {
@apply space-y-4;
}
.code-editor-tabs {
@apply border border-gray-200 rounded-lg overflow-hidden bg-white;
}
.tabs-header {
@apply bg-gray-50;
}
.tab-content {
@apply p-0;
}
.html-editor-container,
.css-editor-container,
.js-editor-container {
@apply border-0;
}
.html-editor,
.css-editor,
.js-editor {
min-height: 250px;
}
/* Toggle Switch */
.switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background-color: white;
transition: .4s;
}
input:checked + .slider {
background-color: #3b82f6;
}
input:focus + .slider {
box-shadow: 0 0 1px #3b82f6;
}
input:checked + .slider:before {
transform: translateX(16px);
}
.slider.round {
border-radius: 20px;
}
.slider.round:before {
border-radius: 50%;
}
.variable-list {
@apply space-y-1 max-h-48 overflow-y-auto border border-gray-200 rounded-lg p-2;
}
.variable-item {
@apply p-2 rounded cursor-pointer hover:bg-gray-50 border border-transparent;
}
.variable-item.selected {
@apply bg-blue-50 border-blue-200;
}
.variable-info {
@apply flex items-center justify-between;
}
.variable-name {
@apply font-medium text-gray-800;
}
.variable-type {
@apply text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded;
}
.variable-description {
@apply text-xs text-gray-600 mt-1;
}
.preview-container {
@apply border border-gray-200;
}
.preview-frame {
min-height: 200px;
}
.field-wrapper {
@apply mb-0;
}
code {
@apply bg-gray-100 px-1 py-0.5 rounded text-xs font-mono;
}
.html-preview :deep(*) {
max-width: 100%;
}
</style>

View File

@ -0,0 +1,116 @@
<template>
<RsModal
v-model="showModal"
title="HTML Node Configuration"
size="xl"
position="center"
:okCallback="saveAndClose"
okTitle="Save"
:cancelCallback="closeModal"
>
<template #body>
<div class="mb-6">
<div class="flex items-start">
<div class="mr-4 text-blue-500 flex-shrink-0 mt-1">
<Icon name="material-symbols:code" class="text-2xl" />
</div>
<div>
<h3 class="text-lg font-semibold mb-1">Configure HTML Node</h3>
<p class="text-sm text-gray-600">
Create custom HTML content to display in your process flow.
HTML nodes are useful for creating custom forms, displaying information, and providing interactive elements.
</p>
</div>
</div>
</div>
<!-- Main configuration area -->
<HtmlNodeConfiguration
:nodeData="localNodeData"
:availableVariables="availableVariables"
@update="handleUpdate"
/>
<!-- Quick Reference Guide -->
<div class="mt-6 bg-blue-50 p-4 rounded-md border border-blue-100">
<h4 class="font-medium text-blue-700 mb-2 flex items-center">
<Icon name="material-symbols:info-outline" class="mr-1" />
Quick Reference Guide
</h4>
<div class="text-sm text-blue-700">
<ul class="list-disc list-inside space-y-1">
<li>Use <code class="bg-blue-100 px-1">{{ variableSyntaxExample }}</code> syntax to display process variables in your HTML</li>
<li>Add CSS styles to customize the appearance of your HTML content</li>
<li>Include JavaScript for interactive functionality</li>
<li>Select input variables that your HTML node will read from</li>
<li>Define output variables that your HTML node will modify</li>
</ul>
</div>
</div>
</template>
</RsModal>
</template>
<script setup>
import { ref, watch, computed } from 'vue';
import HtmlNodeConfiguration from './HtmlNodeConfiguration.vue';
import { Icon } from '#components';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
nodeData: {
type: Object,
required: true
},
availableVariables: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['update:modelValue', 'update']);
const showModal = ref(props.modelValue);
const localNodeData = ref({ ...props.nodeData });
// Computed property to avoid template syntax issues
const variableSyntaxExample = computed(() => '{{ variableName }}');
// Watch for changes to modelValue prop to sync modal visibility
watch(() => props.modelValue, (value) => {
showModal.value = value;
});
// Watch for changes to showModal to emit update:modelValue
watch(() => showModal.value, (value) => {
emit('update:modelValue', value);
});
// Watch for changes to nodeData prop
watch(() => props.nodeData, (value) => {
localNodeData.value = { ...value };
}, { deep: true });
function handleUpdate(updatedData) {
localNodeData.value = { ...updatedData };
}
function saveAndClose() {
emit('update', localNodeData.value);
showModal.value = false;
}
function closeModal() {
showModal.value = false;
}
</script>
<style scoped>
code {
font-family: monospace;
border-radius: 0.25rem;
}
</style>

View File

@ -198,13 +198,46 @@ const availableComponents = [
defaultProps: {
label: 'Script Task',
data: {
description: 'Data transformation script',
scriptCode: '// Transform API response or process variables\n// Example:\n// processVariables.newVariable = processVariables.apiResponse.data.field;\n',
description: 'Execute JavaScript code',
scriptCode: '',
scriptLanguage: 'javascript',
inputVariables: [],
outputVariables: []
}
}
},
{
type: 'html',
name: 'HTML Content',
category: 'Core',
icon: 'material-symbols:code',
description: 'Display custom HTML content',
defaultProps: {
label: 'HTML Content',
data: {
description: 'Custom HTML content',
htmlCode: '<!-- Enter your HTML code here -->\n<div class="custom-html-content">\n <h2>Custom HTML Content</h2>\n <p>This is a custom HTML node that can display rich content.</p>\n</div>',
cssCode: '',
jsCode: '',
inputVariables: [],
outputVariables: [],
continueOnError: false,
errorVariable: 'scriptError'
allowVariableAccess: true,
autoRefresh: false
}
}
},
{
type: 'subprocess',
name: 'Sub Process',
category: 'Core',
icon: 'material-symbols:hub-outline',
description: 'Execute another process as a sub-process',
defaultProps: {
label: 'Sub Process',
data: {
description: 'Executes another process',
subprocessId: null,
subprocessName: ''
}
}
},
@ -289,66 +322,6 @@ const availableComponents = [
shapeType: 'text-annotation'
}
}
},
{
type: 'process-group',
name: 'Process Group',
category: 'Shape',
icon: 'material-symbols:group-work',
description: 'Group container for organizing related processes',
defaultProps: {
label: '',
data: {
description: '',
width: 400,
height: 300,
backgroundColor: '#f0f9ff',
borderColor: '#0284c7',
textColor: '#0369a1',
isShape: true,
shapeType: 'process-group'
}
}
},
{
type: 'hexagon-shape',
name: 'Hexagon',
category: 'Shape',
icon: 'material-symbols:hexagon',
description: 'Hexagon shape for special processes or decision points',
defaultProps: {
label: '',
data: {
description: '',
width: 200,
height: 150,
backgroundColor: '#f8fafc',
borderColor: '#e2e8f0',
textColor: '#475569',
isShape: true,
shapeType: 'hexagon'
}
}
},
{
type: 'trapezoid-shape',
name: 'Trapezoid',
category: 'Shape',
icon: 'material-symbols:change-history',
description: 'Trapezoid shape for data processing or manual tasks',
defaultProps: {
label: '',
data: {
description: '',
width: 220,
height: 120,
backgroundColor: '#f8fafc',
borderColor: '#e2e8f0',
textColor: '#475569',
isShape: true,
shapeType: 'trapezoid'
}
}
}
];
@ -389,6 +362,7 @@ const onDragStart = (event, component) => {
// Add a component directly via click
const addComponent = (component) => {
return;
// Use same format as drag operation for consistency
const componentData = {
type: component.type,

View File

@ -334,7 +334,6 @@ watch(() => props.initialNodes, async (newNodes, oldNodes) => {
isUpdatingNodes.value = true;
try {
// console.log('ProcessFlowCanvas: Updating nodes, count:', newNodes.length);
// Instead of clearing all nodes, sync them intelligently
const currentNodeIds = new Set(nodes.value.map(n => n.id));
@ -393,15 +392,37 @@ watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCoun
isUpdatingEdges.value = true;
try {
// console.log('ProcessFlowCanvas: Updating edges, count:', newEdges.length, 'nodeCount:', nodeCount);
// Instead of clearing all edges, sync them intelligently
const currentEdgeIds = new Set(edges.value.map(e => e.id));
const newEdgeIds = new Set(newEdges.map(e => e.id));
// Remove edges that are no longer in the new list
const edgesToRemove = edges.value.filter(edge => !newEdgeIds.has(edge.id));
// CRITICAL: Be more conservative about edge removal
// Only remove edges that are definitely not in the new list AND whose nodes don't exist
const edgesToRemove = edges.value.filter(edge => {
const isInNewList = newEdgeIds.has(edge.id);
if (isInNewList) {
return false; // Don't remove if it's in the new list
}
// Double-check that both source and target nodes still exist
const sourceExists = nodes.value.some(node => node.id === edge.source);
const targetExists = nodes.value.some(node => node.id === edge.target);
// Only remove if nodes don't exist (orphaned edges)
const shouldRemove = !sourceExists || !targetExists;
if (shouldRemove) {
console.log(`🗑️ Removing orphaned edge ${edge.id}: source ${edge.source} exists: ${sourceExists}, target ${edge.target} exists: ${targetExists}`);
}
return shouldRemove;
});
if (edgesToRemove.length > 0) {
console.log('🗑️ Removing edges:', edgesToRemove.map(e => `${e.source}->${e.target} (${e.id})`));
removeEdges(edgesToRemove);
}
@ -412,7 +433,7 @@ watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCoun
const targetExists = nodes.value.some(node => node.id === edge.target);
if (!sourceExists || !targetExists) {
console.warn(`Skipping edge ${edge.id}: source ${edge.source} exists: ${sourceExists}, target ${edge.target} exists: ${targetExists}`);
console.warn(`⚠️ Skipping edge ${edge.id}: source ${edge.source} exists: ${sourceExists}, target ${edge.target} exists: ${targetExists}`);
return false;
}
@ -423,10 +444,13 @@ watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCoun
const edgesToAdd = validEdges.filter(edge => !currentEdgeIds.has(edge.id));
if (edgesToAdd.length > 0) {
console.log(' Adding new edges:', edgesToAdd.map(e => `${e.source}->${e.target} (${e.id})`));
// Ensure all edges have proper handle specifications
const edgesWithHandles = edgesToAdd.map(edge => {
// IMPORTANT: If edge already has sourceHandle and targetHandle, preserve them exactly as they are
if (edge.sourceHandle && edge.targetHandle) {
console.log(`🔗 Edge ${edge.id} already has handles: ${edge.sourceHandle} -> ${edge.targetHandle}`);
return edge;
}
@ -457,6 +481,7 @@ watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCoun
}
}
console.log(`🔗 Generated handles for edge ${edge.id}: ${sourceHandle} -> ${targetHandle}`);
return {
...edge,
sourceHandle,
@ -465,10 +490,10 @@ watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCoun
});
addEdges([...edgesWithHandles]); // Create a copy to avoid reactivity issues
// console.log('ProcessFlowCanvas: Successfully added edges with handles:', edgesWithHandles.length);
}
// Update existing edges that have changed - IMPORTANT: preserve handle positions
let updatedEdgeCount = 0;
newEdges.forEach(newEdge => {
const existingEdge = edges.value.find(e => e.id === newEdge.id);
if (existingEdge) {
@ -481,15 +506,21 @@ watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCoun
if (hasChanges) {
Object.assign(existingEdge, {
label: newEdge.label,
// Preserve existing handles if they exist
// CRITICAL: Preserve existing handles if they exist
sourceHandle: existingEdge.sourceHandle || newEdge.sourceHandle,
targetHandle: existingEdge.targetHandle || newEdge.targetHandle,
style: newEdge.style ? { ...newEdge.style } : undefined
});
updatedEdgeCount++;
}
}
});
if (updatedEdgeCount > 0) {
console.log('🔄 Updated existing edges:', updatedEdgeCount);
}
}
} finally {
// Use a small delay to prevent immediate re-triggering
setTimeout(() => {
@ -497,7 +528,7 @@ watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCoun
}, 50);
}
} else if (newEdges && Array.isArray(newEdges) && newEdges.length > 0 && nodeCount === 0) {
// console.log('ProcessFlowCanvas: Edges provided but no nodes yet, waiting...');
console.log('⚠️ ProcessFlowCanvas: Edges provided but no nodes yet, waiting...');
}
}, { deep: true });
@ -723,30 +754,31 @@ function removeNode(nodeId) {
return nodeToRemove;
}
// Add explicit sync method to manually update canvas
// Manual sync function for explicit canvas updates
function syncCanvas(newNodes, newEdges) {
// Force clear the updating flags first to ensure we can process
isUpdatingNodes.value = false;
isUpdatingEdges.value = false;
console.log('🔄 Manual canvas sync requested - nodes:', newNodes?.length || 0, 'edges:', newEdges?.length || 0);
// Wait a moment for any ongoing operations to complete
setTimeout(() => {
// Use a small delay to ensure any pending Vue Flow operations complete
setTimeout(async () => {
try {
// Sync nodes first
if (newNodes && Array.isArray(newNodes)) {
const currentNodeIds = new Set(nodes.value.map(n => n.id));
const newNodeIds = new Set(newNodes.map(n => n.id));
console.log('📊 Current canvas nodes:', currentNodeIds.size, 'New nodes:', newNodeIds.size);
// Remove nodes that are no longer in the new list
const nodesToRemove = nodes.value.filter(node => !newNodeIds.has(node.id));
if (nodesToRemove.length > 0) {
console.log('🗑️ Removing nodes:', nodesToRemove.map(n => n.id));
removeNodes(nodesToRemove);
}
// Add new nodes that aren't already present
const nodesToAdd = newNodes.filter(node => !currentNodeIds.has(node.id));
if (nodesToAdd.length > 0) {
console.log(' Adding new nodes:', nodesToAdd.map(n => n.id));
addNodes([...nodesToAdd]);
}
@ -764,28 +796,45 @@ function syncCanvas(newNodes, newEdges) {
updateNodeInternals([newNode.id]);
}
});
// Wait for nodes to be fully processed before handling edges
await nextTick();
await new Promise(resolve => setTimeout(resolve, 100));
}
console.log('📊 Canvas state after node sync - nodes:', nodes.value.length, 'edges:', edges.value.length);
// Sync edges after nodes are updated
// Sync edges after nodes are updated - CRITICAL: Only if we have nodes
if (newEdges && Array.isArray(newEdges) && nodes.value.length > 0) {
const currentEdgeIds = new Set(edges.value.map(e => e.id));
const newEdgeIds = new Set(newEdges.map(e => e.id));
console.log('📊 Current canvas edges:', currentEdgeIds.size, 'New edges:', newEdgeIds.size);
// CRITICAL: Only remove edges that are definitely not in the new list
// Be more conservative about edge removal to prevent accidental deletions
const edgesToRemove = edges.value.filter(edge => {
const shouldRemove = !newEdgeIds.has(edge.id);
if (shouldRemove) {
// Double-check that both source and target nodes still exist
const sourceExists = nodes.value.some(node => node.id === edge.source);
const targetExists = nodes.value.some(node => node.id === edge.target);
// Only remove if the edge is truly not needed OR if nodes don't exist
return !sourceExists || !targetExists;
}
return false;
});
// Remove edges that are no longer in the new list
const edgesToRemove = edges.value.filter(edge => !newEdgeIds.has(edge.id));
if (edgesToRemove.length > 0) {
console.log('🗑️ Removing edges:', edgesToRemove.map(e => `${e.source}->${e.target} (${e.id})`));
removeEdges(edgesToRemove);
}
// Add new edges that aren't already present
const edgesToAdd = newEdges.filter(edge => !currentEdgeIds.has(edge.id));
if (edgesToAdd.length > 0) {
console.log(' Adding new edges:', edgesToAdd.map(e => `${e.source}->${e.target} (${e.id})`));
// Verify nodes exist and add handles
const validEdges = edgesToAdd.filter(edge => {
@ -793,19 +842,19 @@ function syncCanvas(newNodes, newEdges) {
const targetExists = nodes.value.some(node => node.id === edge.target);
if (!sourceExists || !targetExists) {
console.warn(`Skipping edge ${edge.id}: source ${edge.source} exists: ${sourceExists}, target ${edge.target} exists: ${targetExists}`);
console.warn(`⚠️ Skipping edge ${edge.id}: source ${edge.source} exists: ${sourceExists}, target ${edge.target} exists: ${targetExists}`);
return false;
}
return true;
});
console.log('✅ Valid edges to add:', validEdges.length);
const edgesWithHandles = validEdges.map(edge => {
// If edge already has sourceHandle and targetHandle, use them
if (edge.sourceHandle && edge.targetHandle) {
console.log(`🔗 Edge ${edge.id} already has handles: ${edge.sourceHandle} -> ${edge.targetHandle}`);
return edge;
}
@ -833,36 +882,51 @@ function syncCanvas(newNodes, newEdges) {
}
}
console.log(`🔗 Generated handles for edge ${edge.id}: ${sourceHandle} -> ${targetHandle}`);
return { ...edge, sourceHandle, targetHandle };
});
if (edgesWithHandles.length > 0) {
console.log('✅ Adding edges with handles:', edgesWithHandles.length);
addEdges([...edgesWithHandles]);
}
}
// Update existing edges
// Update existing edges - preserve handles and only update changed properties
let updatedEdgeCount = 0;
newEdges.forEach(newEdge => {
const existingEdge = edges.value.find(e => e.id === newEdge.id);
if (existingEdge) {
Object.assign(existingEdge, {
label: newEdge.label,
sourceHandle: newEdge.sourceHandle,
targetHandle: newEdge.targetHandle,
style: newEdge.style ? { ...newEdge.style } : undefined
});
// Check if update is needed
const needsUpdate = (
existingEdge.label !== newEdge.label ||
JSON.stringify(existingEdge.style) !== JSON.stringify(newEdge.style)
);
if (needsUpdate) {
Object.assign(existingEdge, {
label: newEdge.label,
// CRITICAL: Preserve existing handles if they exist
sourceHandle: existingEdge.sourceHandle || newEdge.sourceHandle,
targetHandle: existingEdge.targetHandle || newEdge.targetHandle,
style: newEdge.style ? { ...newEdge.style } : undefined
});
updatedEdgeCount++;
}
}
});
if (updatedEdgeCount > 0) {
console.log('🔄 Updated existing edges:', updatedEdgeCount);
}
} else if (newEdges && Array.isArray(newEdges) && newEdges.length > 0) {
console.warn('Cannot add edges: nodes not ready. Node count:', nodes.value.length);
console.warn('⚠️ Cannot add edges: nodes not ready. Node count:', nodes.value.length);
}
console.log('✅ Canvas sync completed - final state: nodes:', nodes.value.length, 'edges:', edges.value.length);
} catch (error) {
console.error('Error during canvas sync:', error);
console.error('Error during canvas sync:', error);
}
}, 50); // Small delay to allow any pending operations to complete
}
@ -1187,6 +1251,10 @@ function fromObject(flowObject) {
color: #607D8B;
}
:deep(.node-html .custom-node-icon .material-icons) {
color: #0ea5e9;
}
:deep(.custom-node-title) {
font-weight: 500;
flex-grow: 1;

View File

@ -144,6 +144,11 @@ const CustomNode = markRaw({
defaultBorder = '#0ea5e9';
defaultText = '#0284c7';
break;
case 'subprocess':
defaultBg = '#f0fdfa';
defaultBorder = '#14b8a6';
defaultText = '#134e4a';
break;
}
}
@ -444,6 +449,67 @@ export const ScriptNode = markRaw({
}
});
// HTML node
export const HtmlNode = markRaw({
props: ['id', 'type', 'label', 'selected', 'data'],
computed: {
nodeLabel() {
// Get label from either prop or data, with fallback
return this.label || (this.data && this.data.label) || 'HTML Content';
},
hasHtmlContent() {
return !!this.data?.htmlCode;
},
hasCssContent() {
return !!this.data?.cssCode;
},
hasJsContent() {
return !!this.data?.jsCode;
},
contentSummary() {
const parts = [];
if (this.hasHtmlContent) parts.push('HTML');
if (this.hasCssContent) parts.push('CSS');
if (this.hasJsContent) parts.push('JS');
return parts.length > 0 ? parts.join(' + ') : 'Empty';
}
},
render() {
return h(CustomNode, {
id: this.id,
type: 'html',
label: this.nodeLabel,
selected: this.selected,
data: this.data,
onClick: () => this.$emit('node-click', this.id)
}, {
icon: () => h('i', { class: 'material-icons text-blue-500' }, 'code'),
default: () => h('div', { class: 'node-details' }, [
h('p', { class: 'node-description' }, this.data?.description || 'Custom HTML content'),
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
h('span', { class: 'node-rule-detail-label' }, 'Content:'),
h('span', {
class: 'node-rule-detail-value ml-1 font-medium text-blue-600'
}, 'HTML')
]),
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
h('span', { class: 'node-rule-detail-label' }, 'Status:'),
h('span', {
class: 'node-rule-detail-value ml-1 font-medium text-gray-600'
}, this.hasHtmlContent ? 'Configured' : 'Not configured')
]),
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
h('span', { class: 'node-rule-detail-label' }, 'Variables:'),
h('span', {
class: 'node-rule-detail-value ml-1 font-medium text-blue-600'
}, this.data?.allowVariableAccess ? 'Enabled' : 'Disabled')
])
])
});
}
});
// API Call node
export const ApiCallNode = markRaw({
props: ['id', 'type', 'label', 'selected', 'data'],
@ -650,8 +716,52 @@ export const NotificationNode = markRaw({
}
});
// Subprocess node
export const SubprocessNode = markRaw({
props: ['id', 'type', 'label', 'selected', 'data'],
computed: {
nodeLabel() {
return this.label || (this.data && this.data.label) || 'Sub Process';
},
subprocessName() {
return this.data?.subprocessName || 'None selected';
},
isConfigured() {
return !!this.data?.subprocessId;
}
},
render() {
return h(CustomNode, {
id: this.id,
type: 'subprocess',
label: this.nodeLabel,
selected: this.selected,
data: this.data,
onClick: () => this.$emit('node-click', this.id)
}, {
icon: () => h('i', { class: 'material-icons text-teal-500' }, 'hub'),
default: () => h('div', { class: 'node-details' }, [
h('p', { class: 'node-description' }, this.data?.description || 'Executes another process'),
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
h('span', { class: 'node-rule-detail-label' }, 'Process:'),
h('span', {
class: this.isConfigured ? 'node-rule-detail-value ml-1 font-medium text-teal-600' : 'node-rule-detail-value ml-1 italic text-gray-400'
}, this.subprocessName)
]),
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
h('span', { class: 'node-rule-detail-label' }, 'Status:'),
h('span', {
class: 'node-rule-detail-value ml-1 font-medium',
'class': this.isConfigured ? 'text-green-600' : 'text-red-600'
}, this.isConfigured ? 'Configured' : 'Not configured')
])
])
});
}
});
// Shape Components (Design Elements)
const HorizontalSwimlaneShape = {
const HorizontalSwimlaneShape = markRaw({
props: ['id', 'data', 'selected', 'label'],
computed: {
shapeStyle() {
@ -693,9 +803,9 @@ const HorizontalSwimlaneShape = {
</div>
</div>
`
};
});
const VerticalSwimlaneShape = {
const VerticalSwimlaneShape = markRaw({
props: ['id', 'data', 'selected', 'label'],
computed: {
shapeStyle() {
@ -739,9 +849,9 @@ const VerticalSwimlaneShape = {
</div>
</div>
`
};
});
const RectangleShape = {
const RectangleShape = markRaw({
props: ['id', 'data', 'selected', 'label'],
computed: {
shapeStyle() {
@ -783,9 +893,9 @@ const RectangleShape = {
</div>
</div>
`
};
});
const TextAnnotationShape = {
const TextAnnotationShape = markRaw({
props: ['id', 'data', 'selected', 'label'],
computed: {
shapeStyle() {
@ -828,9 +938,9 @@ const TextAnnotationShape = {
</div>
</div>
`
};
});
const ProcessGroupShape = {
const ProcessGroupShape = markRaw({
props: ['id', 'data', 'selected', 'label'],
computed: {
shapeStyle() {
@ -873,10 +983,10 @@ const ProcessGroupShape = {
</div>
</div>
`
};
});
// Hexagon Shape Component
const HexagonShape = {
const HexagonShape = markRaw({
props: ['id', 'data', 'selected', 'label'],
computed: {
shapeStyle() {
@ -909,10 +1019,10 @@ const HexagonShape = {
</div>
</div>
`
};
});
// Trapezoid Shape Component
const TrapezoidShape = {
const TrapezoidShape = markRaw({
props: ['id', 'data', 'selected', 'label'],
computed: {
shapeStyle() {
@ -945,7 +1055,7 @@ const TrapezoidShape = {
</div>
</div>
`
};
});
// Export the node types object to use with Vue Flow
export const nodeTypes = {
@ -957,6 +1067,8 @@ export const nodeTypes = {
'business-rule': BusinessRuleNode,
api: ApiCallNode,
notification: NotificationNode,
html: HtmlNode, // Add the new HtmlNode to the nodeTypes object
subprocess: SubprocessNode,
// Shape nodes
'swimlane-horizontal': HorizontalSwimlaneShape,
'swimlane-vertical': VerticalSwimlaneShape,
@ -1586,6 +1698,14 @@ export const nodeStyles = `
border-left: 4px solid #6b7280; /* Gray border to match icon color */
}
/* HTML node styling */
.node-html {
border-left: 4px solid #0ea5e9; /* Blue border to match icon color */
background-color: #f0f9ff;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
/* Shape node styles */
.shape-node {
position: relative;

View File

@ -0,0 +1,204 @@
<script setup>
import { ref, watch, computed } from 'vue';
import { useProcessBuilderStore } from '~/stores/processBuilder';
const props = defineProps({
nodeData: {
type: Object,
required: true
}
});
const emit = defineEmits(['update']);
const processStore = useProcessBuilderStore();
const localNodeData = ref({});
const selectedProcessDetails = ref(null);
const isLoadingDetails = ref(false);
const searchQuery = ref('');
// Watch for changes to nodeData prop
watch(() => props.nodeData, (value) => {
localNodeData.value = JSON.parse(JSON.stringify(value));
if (localNodeData.value.subprocessId) {
fetchProcessDetails(localNodeData.value.subprocessId);
}
}, { deep: true, immediate: true });
// Fetch the list of all processes for the selection list
const { data: processes, pending, error } = useFetch('/api/process', {
lazy: true,
server: false,
transform: (response) => {
if (!response || !response.data || !Array.isArray(response.data.processes)) return [];
const currentProcessId = processStore.currentProcess?.id;
return response.data.processes
.filter(p => p.processID !== currentProcessId)
.map(p => ({
label: p.processName,
value: p.processID,
description: p.processDescription,
version: p.processVersion,
status: p.processStatus,
modifiedDate: p.processModifiedDate
}));
}
});
const filteredProcesses = computed(() => {
if (!processes.value) return [];
if (!searchQuery.value) return processes.value;
return processes.value.filter(p =>
p.label.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
(p.description && p.description.toLowerCase().includes(searchQuery.value.toLowerCase()))
);
});
// Fetch detailed information for a single selected process
const fetchProcessDetails = async (processId) => {
if (!processId) {
selectedProcessDetails.value = null;
return;
}
isLoadingDetails.value = true;
try {
const { data } = await useFetch(`/api/process/${processId}`);
if (data.value && data.value.success) {
selectedProcessDetails.value = data.value.process;
} else {
selectedProcessDetails.value = null;
}
} catch (e) {
selectedProcessDetails.value = null;
} finally {
isLoadingDetails.value = false;
}
};
const onSelectionChange = (processId) => {
localNodeData.value.subprocessId = processId;
const selected = processes.value.find(p => p.value === processId);
if (selected) {
localNodeData.value.subprocessName = selected.label;
localNodeData.value.label = selected.label;
localNodeData.value.description = `Sub-process: ${selected.label}`;
fetchProcessDetails(processId);
} else {
// This case may not be hit if selection is cleared differently, but good for safety
localNodeData.value.subprocessId = null;
localNodeData.value.subprocessName = '';
localNodeData.value.label = 'Sub Process';
localNodeData.value.description = 'Executes another process';
selectedProcessDetails.value = null;
}
emit('update', localNodeData.value);
};
// Computed property for content summary
const contentSummary = computed(() => {
if (!selectedProcessDetails.value) return null;
const { processDefinition, processVariables } = selectedProcessDetails.value;
const nodeCount = processDefinition?.nodes?.length || 0;
const edgeCount = processDefinition?.edges?.length || 0;
const variableCount = processVariables ? Object.keys(processVariables).length : 0;
return { nodes: nodeCount, edges: edgeCount, variables: variableCount };
});
// Helper functions
const formatDate = (dateString) => dateString ? new Date(dateString).toLocaleString() : 'N/A';
const getNodeIcon = (type) => ({
'start': 'heroicons:play-circle', 'end': 'heroicons:stop-circle', 'task': 'heroicons:rectangle-stack',
'form': 'heroicons:document-text', 'gateway': 'heroicons:arrows-pointing-out', 'script': 'heroicons:code-bracket',
'api': 'heroicons:cloud-arrow-down', 'notification': 'heroicons:bell', 'business-rule': 'heroicons:document-check',
'subprocess': 'heroicons:flow-chart'
}[type] || 'heroicons:rectangle-stack');
</script>
<template>
<div class="flex h-[65vh]">
<!-- Left Panel: Process List -->
<div class="w-1/3 border-r border-gray-200 bg-gray-50 flex flex-col">
<div class="p-4 border-b border-gray-200">
<h3 class="font-medium text-gray-900">Available Processes</h3>
<div class="relative mt-2">
<input
type="text"
v-model="searchQuery"
placeholder="Search processes..."
class="w-full px-3 py-2 pl-9 bg-white border border-gray-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-teal-500"
/>
<Icon name="heroicons:magnifying-glass" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
</div>
</div>
<div v-if="pending" class="flex-1 flex items-center justify-center text-gray-500">
<Icon name="material-symbols:progress-activity" class="w-5 h-5 animate-spin mr-2" />
<span>Loading...</span>
</div>
<div v-else-if="error" class="p-4 text-red-600">Error loading processes.</div>
<div v-else class="flex-1 overflow-y-auto">
<div
v-for="process in filteredProcesses"
:key="process.value"
@click="onSelectionChange(process.value)"
class="p-4 border-b border-gray-200 cursor-pointer hover:bg-gray-100"
:class="{ 'bg-teal-50 border-l-4 border-teal-500': localNodeData.subprocessId === process.value }"
>
<div class="font-medium text-gray-900">{{ process.label }}</div>
<p class="text-xs text-gray-600 mt-1 truncate">{{ process.description || 'No description' }}</p>
</div>
</div>
</div>
<!-- Right Panel: Details Preview -->
<div class="w-2/3 bg-white flex-1 overflow-y-auto">
<div class="p-6">
<div v-if="!localNodeData.subprocessId" class="flex items-center justify-center h-full text-gray-500">
<div class="text-center">
<Icon name="heroicons:document-text" class="h-12 w-12 mx-auto mb-4 text-gray-300" />
<p>Select a process from the list to see its details.</p>
</div>
</div>
<div v-else-if="isLoadingDetails" class="flex items-center justify-center h-full text-gray-500">
<Icon name="material-symbols:progress-activity" class="w-6 h-6 animate-spin mr-2" />
<span>Loading details...</span>
</div>
<div v-else-if="selectedProcessDetails">
<!-- Process Info -->
<div class="mb-6 p-4 bg-gray-50 rounded-lg">
<h4 class="font-medium text-gray-900 mb-2">Process Information</h4>
<div class="grid grid-cols-2 gap-4 text-sm">
<div><span class="text-gray-600">Name:</span><span class="ml-2 font-medium">{{ selectedProcessDetails.processName }}</span></div>
<div><span class="text-gray-600">Status:</span><RsBadge :color="selectedProcessDetails.processStatus === 'published' ? 'success' : 'warning'" class="ml-2 capitalize">{{ selectedProcessDetails.processStatus }}</RsBadge></div>
<div class="col-span-2"><span class="text-gray-600">Description:</span><span class="ml-2">{{ selectedProcessDetails.processDescription || 'N/A' }}</span></div>
<div v-if="contentSummary" class="col-span-2 mt-2 pt-4 border-t border-gray-200 grid grid-cols-3 gap-4 text-center">
<div><div class="text-lg font-bold text-teal-600">{{ contentSummary.nodes }}</div><div class="text-xs text-gray-500">Nodes</div></div>
<div><div class="text-lg font-bold text-teal-600">{{ contentSummary.edges }}</div><div class="text-xs text-gray-500">Connections</div></div>
<div><div class="text-lg font-bold text-teal-600">{{ contentSummary.variables }}</div><div class="text-xs text-gray-500">Variables</div></div>
</div>
</div>
</div>
<!-- Process Nodes Preview -->
<div class="space-y-4">
<h4 class="font-medium text-gray-900">Process Nodes</h4>
<div class="border rounded-lg p-4 bg-white max-h-64 overflow-y-auto">
<div v-if="selectedProcessDetails.processDefinition?.nodes?.length" class="space-y-3">
<div v-for="node in selectedProcessDetails.processDefinition.nodes" :key="node.id" class="flex items-center justify-between p-3 border rounded-lg bg-gray-50">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center"><Icon :name="getNodeIcon(node.type)" class="h-4 w-4 text-blue-600" /></div>
<div>
<div class="font-medium text-gray-900">{{ node.data?.label || node.label || node.type }}</div>
<div class="text-sm text-gray-600">{{ node.id }}</div>
</div>
</div>
<div class="text-sm text-gray-500 capitalize">{{ (node.type || 'node').replace(/-/g, ' ') }}</div>
</div>
</div>
<div v-else class="text-center py-8 text-gray-500">No nodes in this process.</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,101 @@
<script setup>
import { ref, watch } from 'vue';
import SubprocessNodeConfiguration from './SubprocessNodeConfiguration.vue';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
nodeData: {
type: Object,
required: true
},
});
const emit = defineEmits(['update:modelValue', 'update']);
const showModal = ref(props.modelValue);
const localNodeData = ref({});
// Sync modal visibility with the modelValue prop
watch(() => props.modelValue, (value) => {
showModal.value = value;
});
// Emit updates when the modal's visibility changes
watch(() => showModal.value, (value) => {
emit('update:modelValue', value);
});
// Keep a local copy of the node data to avoid direct prop mutation
watch(() => props.nodeData, (value) => {
localNodeData.value = JSON.parse(JSON.stringify(value));
}, { deep: true, immediate: true });
// Handle updates from the configuration component
function handleUpdate(updatedData) {
localNodeData.value = updatedData;
}
// Save changes and close the modal
function saveAndClose() {
emit('update', localNodeData.value);
closeModal();
}
// Close the modal without saving
function closeModal() {
showModal.value = false;
}
</script>
<template>
<RsModal
v-model="showModal"
title="Sub-process Configuration"
size="5xl"
:okCallback="saveAndClose"
okTitle="Save Changes"
:cancelCallback="closeModal"
cancelTitle="Cancel"
>
<template #body>
<div class="mb-6">
<div class="flex items-start">
<div class="mr-4 text-teal-500 flex-shrink-0 mt-1">
<Icon name="material-symbols:hub" class="text-2xl" />
</div>
<div>
<h3 class="text-lg font-semibold mb-1">Configure Sub-process</h3>
<p class="text-sm text-gray-600">
Select a process to execute as a sub-process. The main process will pause and wait for the selected sub-process to complete before continuing.
</p>
</div>
</div>
</div>
<!-- Main configuration area -->
<SubprocessNodeConfiguration
:nodeData="localNodeData"
@update="handleUpdate"
/>
<!-- Quick Reference Guide -->
<div class="mt-6 bg-teal-50 p-4 rounded-md border border-teal-100">
<h4 class="font-medium text-teal-700 mb-2 flex items-center">
<Icon name="material-symbols:info-outline" class="mr-1" />
How Sub-processes Work
</h4>
<div class="text-sm text-teal-700">
<ul class="list-disc list-inside space-y-1">
<li>Sub-processes run as independent instances.</li>
<li>You can map input variables from the parent process to the sub-process.</li>
<li>Output variables from the sub-process can be mapped back to the parent.</li>
<li>The parent process will wait until the sub-process reaches an end state.</li>
</ul>
</div>
</div>
</template>
</RsModal>
</template>

View File

@ -1,878 +1,41 @@
this.hideField("form_jeniskp_1");
this.hideField("form_jeniskp_2");
this.hideField("form_jeniskp_3");
this.onFieldChange("select_1", (value) => {
this.hideField("form_jeniskp_1");
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");
// Conditional logic for showing 'Nyatakan keperluan lain' field
onFieldChange("keperluan_mendesak", (value) => {
if (Array.isArray(value) && value.includes("lain_lain")) {
showField("keperluan_lain_nyatakan");
} else {
hideField("keperluan_lain_nyatakan");
setField("keperluan_lain_nyatakan", "");
}
});
// 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");
}
})();
// Show success message on form load
showInfo(
"Sila lengkapkan semua maklumat yang diperlukan untuk penilaian awal."
);
// 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 () {
// Conditional logic for field: keperluan_lain_nyatakan
onFieldChange("keperluan_mendesak", function () {
if (
!String(getField("hubungan_keluarga") || "")
String(getField("keperluan_mendesak") || "")
.toLowerCase()
.includes("lain_lain".toLowerCase())
) {
hideField("hubungan_lain_nyatakan");
showField("keperluan_lain_nyatakan");
} else {
showField("hubungan_lain_nyatakan");
hideField("keperluan_lain_nyatakan");
}
});
// Initial evaluation for field: hubungan_lain_nyatakan
// Initial evaluation for field: keperluan_lain_nyatakan
(function () {
if (
!String(getField("hubungan_keluarga") || "")
String(getField("keperluan_mendesak") || "")
.toLowerCase()
.includes("lain_lain".toLowerCase())
) {
hideField("hubungan_lain_nyatakan");
showField("keperluan_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");
hideField("keperluan_lain_nyatakan");
}
})();

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,51 @@
{
"edges": [
{
"id": "start-1751870920411-form-1751870928350-1751954902366",
"id": "start-1751870920411-form-1752471000000-1752110219601",
"data": {},
"type": "smoothstep",
"label": "",
"source": "start-1751870920411",
"target": "form-1751870928350",
"target": "form-1752471000000",
"animated": true,
"sourceHandle": "start-1751870920411-right",
"targetHandle": "form-1752471000000-left"
},
{
"id": "form-1752471000000-api-1752471000010-1752110221444",
"data": {},
"type": "smoothstep",
"label": "",
"source": "form-1752471000000",
"target": "api-1752471000010",
"animated": true,
"sourceHandle": "form-1752471000000-bottom",
"targetHandle": "api-1752471000010-top"
},
{
"id": "api-1752471000010-script-1752471000020-1752110222889",
"data": {},
"type": "smoothstep",
"label": "",
"source": "api-1752471000010",
"target": "script-1752471000020",
"animated": true,
"sourceHandle": "api-1752471000010-right",
"targetHandle": "script-1752471000020-left"
},
{
"id": "script-1752471000020-form-1751870928350-1752110513125",
"data": {},
"type": "smoothstep",
"label": "",
"source": "script-1752471000020",
"target": "form-1751870928350",
"animated": true,
"sourceHandle": "script-1752471000020-right",
"targetHandle": "form-1751870928350-left"
},
{
"id": "form-1751870928350-api-1751871528249-1751954924255",
"id": "form-1751870928350-api-1751871528249-1752110547688",
"data": {},
"type": "smoothstep",
"label": "",
@ -23,7 +56,7 @@
"targetHandle": "api-1751871528249-top"
},
{
"id": "api-1751871528249-script-1751871635000-1751954926618",
"id": "api-1751871528249-script-1751871635000-1752110661170",
"data": {},
"type": "smoothstep",
"label": "",
@ -34,103 +67,59 @@
"targetHandle": "script-1751871635000-left"
},
{
"id": "form-1751871700000-script-1751871635000-1751954928000",
"id": "script-1751871635000-api-1752114771983-1752114797295",
"data": {},
"type": "smoothstep",
"label": "",
"source": "form-1751871700000",
"target": "script-1751871635000",
"source": "script-1751871635000",
"target": "api-1752114771983",
"animated": true,
"sourceHandle": "form-1751871700000-bottom",
"targetHandle": "script-1751871635000-top"
"sourceHandle": "script-1751871635000-right",
"targetHandle": "api-1752114771983-left"
},
{
"id": "api-1751871750000-form-1751871700000-1751954936240",
"id": "api-1752114771983-html-1752109761532-1752114821740",
"data": {},
"type": "smoothstep",
"label": "",
"source": "api-1751871750000",
"target": "form-1751871700000",
"source": "api-1752114771983",
"target": "html-1752109761532",
"animated": true,
"sourceHandle": "api-1751871750000-bottom",
"targetHandle": "form-1751871700000-top"
"sourceHandle": "api-1752114771983-right",
"targetHandle": "html-1752109761532-left"
},
{
"id": "api-1751871750000-script-1751871770000-1751954938889",
"id": "start-1751870920411-form-1753000000000-1752115210580",
"data": {},
"type": "smoothstep",
"label": "",
"source": "api-1751871750000",
"target": "script-1751871770000",
"source": "start-1751870920411",
"target": "form-1753000000000",
"animated": true,
"sourceHandle": "api-1751871750000-right",
"targetHandle": "script-1751871770000-left"
"sourceHandle": "start-1751870920411-right",
"targetHandle": "form-1753000000000-left"
},
{
"id": "script-1751871770000-gateway-1751871800000-1751954943222",
"id": "form-1753000000000-api-1753000000001-1752115217959",
"data": {},
"type": "smoothstep",
"label": "",
"source": "script-1751871770000",
"target": "gateway-1751871800000",
"source": "form-1753000000000",
"target": "api-1753000000001",
"animated": true,
"sourceHandle": "script-1751871770000-bottom",
"targetHandle": "gateway-1751871800000-left"
"sourceHandle": "form-1753000000000-bottom",
"targetHandle": "api-1753000000001-top"
},
{
"id": "gateway-1751871800000-business-rule-1751871900000-1751954958263",
"data": {},
"type": "smoothstep",
"label": "Ya",
"source": "gateway-1751871800000",
"target": "business-rule-1751871900000",
"animated": true,
"sourceHandle": "gateway-1751871800000-right",
"targetHandle": "business-rule-1751871900000-left"
},
{
"id": "gateway-1751871800000-notification-1751872000000-1751954960514",
"data": {},
"type": "smoothstep",
"label": "Tidak",
"source": "gateway-1751871800000",
"target": "notification-1751872000000",
"animated": true,
"sourceHandle": "gateway-1751871800000-right",
"targetHandle": "notification-1751872000000-left"
},
{
"id": "business-rule-1751871900000-notification-1751871950000-1751954963756",
"id": "api-1753000000001-script-1753000000002-1752115222952",
"data": {},
"type": "smoothstep",
"label": "",
"source": "business-rule-1751871900000",
"target": "notification-1751871950000",
"source": "api-1753000000001",
"target": "script-1753000000002",
"animated": true,
"sourceHandle": "business-rule-1751871900000-right",
"targetHandle": "notification-1751871950000-left"
},
{
"id": "notification-1751871950000-end-1751872100000-1751954966017",
"data": {},
"type": "smoothstep",
"label": "",
"source": "notification-1751871950000",
"target": "end-1751872100000",
"animated": true,
"sourceHandle": "notification-1751871950000-bottom",
"targetHandle": "end-1751872100000-top"
},
{
"id": "notification-1751872000000-end-1751872100000-1751954967691",
"data": {},
"type": "smoothstep",
"label": "",
"source": "notification-1751872000000",
"target": "end-1751872100000",
"animated": true,
"sourceHandle": "notification-1751872000000-right",
"targetHandle": "end-1751872100000-left"
"sourceHandle": "api-1753000000001-right",
"targetHandle": "script-1753000000002-left"
}
],
"nodes": [
@ -139,7 +128,165 @@
"data": { "label": "Start", "description": "Process start point" },
"type": "start",
"label": "Start",
"position": { "x": 210, "y": 180 }
"position": { "x": 120, "y": -495 }
},
{
"id": "form-1752471000000",
"data": {
"label": "Penilaian Awal",
"formId": 3,
"formName": "Penilaian Awal",
"formUuid": "8e07fc8f-a160-478a-85fd-fa3364401544",
"description": "Form: Penilaian Awal untuk permohonan bantuan",
"assignedRoles": [],
"assignedUsers": [],
"inputMappings": [],
"assignmentType": "public",
"outputMappings": [
{
"formField": "komitmen_pembiayaan",
"processVariable": "komitmenKosTinggi"
},
{
"formField": "keperluan_mendesak",
"processVariable": "keperluanMendesak"
},
{
"formField": "keperluan_lain_nyatakan",
"processVariable": "keperluanLainNyatakan"
},
{
"formField": "dokumen_berkaitan",
"processVariable": "dokumenBerkaitan"
},
{
"formField": "catatan_tambahan",
"processVariable": "catatanTambahan"
}
],
"fieldConditions": [],
"assignmentVariable": "",
"assignmentVariableType": "user_id"
},
"type": "form",
"label": "Penilaian Awal",
"position": { "x": 450, "y": -525 }
},
{
"id": "api-1752471000010",
"data": {
"label": "Submit Penilaian Awal API",
"apiUrl": "https://jsonplaceholder.typicode.com/posts",
"headers": "{ \"Content-Type\": \"application/json\" }",
"apiMethod": "POST",
"description": "Submit penilaian awal data to external system",
"requestBody": "{\n \"komitmenKosTinggi\": \"{komitmenKosTinggi}\",\n \"keperluanMendesak\": \"{keperluanMendesak}\",\n \"keperluanLainNyatakan\": \"{keperluanLainNyatakan}\",\n \"dokumenBerkaitan\": \"{dokumenBerkaitan}\",\n \"catatanTambahan\": \"{catatanTambahan}\"\n}",
"errorVariable": "penilaianAwalApiError",
"outputVariable": "penilaianAwalApiResponse",
"continueOnError": false
},
"type": "api",
"label": "Submit Penilaian Awal API",
"position": { "x": 450, "y": -345 }
},
{
"id": "script-1752471000020",
"data": {
"label": "Process Penilaian Awal Response",
"scriptCode": "// Extract important data from Penilaian Awal API response\nconst apiData = processVariables.penilaianAwalApiResponse;\n\nif (apiData && apiData.data) {\n // Generate a reference number for the assessment\n processVariables.penilaianAwalId = apiData.data.id || 'PA-' + Date.now();\n \n // Process the high cost commitment answer\n processVariables.hasHighCostCommitment = processVariables.komitmenKosTinggi === 'ya';\n \n // Process urgent needs\n if (Array.isArray(processVariables.keperluanMendesak)) {\n // Set flags for specific urgent needs\n processVariables.hasUrgentMedicalNeed = processVariables.keperluanMendesak.includes('perubatan_kritikal');\n processVariables.hasDisasterNeed = processVariables.keperluanMendesak.includes('bencana');\n processVariables.hasDeathRelatedNeed = processVariables.keperluanMendesak.includes('kematian');\n processVariables.hasFamilyConflict = processVariables.keperluanMendesak.includes('konflik_keluarga');\n processVariables.hasHomelessness = processVariables.keperluanMendesak.includes('tiada_tempat_tinggal');\n processVariables.hasUtilityArrears = processVariables.keperluanMendesak.includes('tunggakan_utiliti');\n processVariables.hasOtherNeeds = processVariables.keperluanMendesak.includes('lain_lain');\n processVariables.hasNoUrgentNeeds = processVariables.keperluanMendesak.includes('tidak_mendesak');\n \n // Calculate urgency score based on selected needs\n let urgencyScore = 0;\n if (processVariables.hasUrgentMedicalNeed) urgencyScore += 5;\n if (processVariables.hasDisasterNeed) urgencyScore += 5;\n if (processVariables.hasDeathRelatedNeed) urgencyScore += 4;\n if (processVariables.hasFamilyConflict) urgencyScore += 3;\n if (processVariables.hasHomelessness) urgencyScore += 5;\n if (processVariables.hasUtilityArrears) urgencyScore += 2;\n if (processVariables.hasOtherNeeds) urgencyScore += 1;\n if (processVariables.hasNoUrgentNeeds) urgencyScore = 0;\n \n processVariables.urgencyScore = urgencyScore;\n processVariables.urgencyLevel = urgencyScore >= 5 ? 'high' : (urgencyScore >= 3 ? 'medium' : 'low');\n }\n \n // Check if documents were uploaded\n processVariables.hasUploadedDocuments = processVariables.dokumenBerkaitan && \n Array.isArray(processVariables.dokumenBerkaitan) && \n processVariables.dokumenBerkaitan.length > 0;\n \n // Set status for next step\n processVariables.penilaianAwalStatus = 'completed';\n processVariables.readyForPersonalInfo = true;\n \n console.log('Penilaian Awal processed successfully:', {\n penilaianAwalId: processVariables.penilaianAwalId,\n urgencyLevel: processVariables.urgencyLevel,\n urgencyScore: processVariables.urgencyScore,\n hasHighCostCommitment: processVariables.hasHighCostCommitment,\n hasUploadedDocuments: processVariables.hasUploadedDocuments\n });\n} else {\n // Handle API error case\n processVariables.penilaianAwalStatus = 'failed';\n processVariables.readyForPersonalInfo = false;\n processVariables.penilaianAwalError = 'Failed to submit penilaian awal';\n}",
"description": "Process the penilaian awal form data and API response",
"errorVariable": "penilaianAwalScriptError",
"inputVariables": [
"penilaianAwalApiResponse",
"komitmenKosTinggi",
"keperluanMendesak",
"keperluanLainNyatakan",
"dokumenBerkaitan",
"catatanTambahan"
],
"scriptLanguage": "javascript",
"continueOnError": false,
"outputVariables": [
{
"name": "penilaianAwalId",
"type": "string",
"description": "Generated ID for the initial assessment"
},
{
"name": "hasHighCostCommitment",
"type": "boolean",
"description": "Whether applicant has high cost commitments"
},
{
"name": "urgencyScore",
"type": "number",
"description": "Calculated urgency score based on needs"
},
{
"name": "urgencyLevel",
"type": "string",
"description": "Urgency level (high/medium/low)"
},
{
"name": "hasUrgentMedicalNeed",
"type": "boolean",
"description": "Whether applicant has urgent medical needs"
},
{
"name": "hasDisasterNeed",
"type": "boolean",
"description": "Whether applicant has disaster-related needs"
},
{
"name": "hasDeathRelatedNeed",
"type": "boolean",
"description": "Whether applicant has death-related needs"
},
{
"name": "hasFamilyConflict",
"type": "boolean",
"description": "Whether applicant has family conflict"
},
{
"name": "hasHomelessness",
"type": "boolean",
"description": "Whether applicant is homeless"
},
{
"name": "hasUtilityArrears",
"type": "boolean",
"description": "Whether applicant has utility arrears"
},
{
"name": "hasOtherNeeds",
"type": "boolean",
"description": "Whether applicant has other needs"
},
{
"name": "hasNoUrgentNeeds",
"type": "boolean",
"description": "Whether applicant has no urgent needs"
},
{
"name": "hasUploadedDocuments",
"type": "boolean",
"description": "Whether documents were uploaded"
},
{
"name": "penilaianAwalStatus",
"type": "string",
"description": "Status of initial assessment submission"
},
{
"name": "readyForPersonalInfo",
"type": "boolean",
"description": "Whether ready for personal info form"
}
]
},
"type": "script",
"label": "Process Penilaian Awal Response",
"position": { "x": 780, "y": -345 }
},
{
"id": "form-1751870928350",
@ -287,7 +434,7 @@
},
"type": "form",
"label": "Borang Maklumat Peribadi",
"position": { "x": 375, "y": 120 }
"position": { "x": 1275, "y": -525 }
},
{
"id": "api-1751871528249",
@ -304,7 +451,7 @@
},
"type": "api",
"label": "Submit Profile API",
"position": { "x": 375, "y": 360 }
"position": { "x": 1275, "y": -345 }
},
{
"id": "script-1751871635000",
@ -383,7 +530,7 @@
},
"type": "script",
"label": "Process API Response",
"position": { "x": 720, "y": 360 }
"position": { "x": 1590, "y": -345 }
},
{
"id": "form-1751871700000",
@ -425,7 +572,7 @@
},
"type": "form",
"label": "Borang Semak Dokumen",
"position": { "x": 720, "y": 120 }
"position": { "x": 885, "y": 675 }
},
{
"id": "api-1751871750000",
@ -442,7 +589,7 @@
},
"type": "api",
"label": "Submit Document Verification API",
"position": { "x": 720, "y": -105 }
"position": { "x": 1050, "y": 510 }
},
{
"id": "script-1751871770000",
@ -520,7 +667,7 @@
},
"type": "script",
"label": "Process Verification Response",
"position": { "x": 1020, "y": -105 }
"position": { "x": 630, "y": 525 }
},
{
"id": "gateway-1751871800000",
@ -562,7 +709,7 @@
},
"type": "gateway",
"label": "Lengkap?",
"position": { "x": 1125, "y": 195 }
"position": { "x": 1350, "y": 315 }
},
{
"id": "business-rule-1751871900000",
@ -687,7 +834,7 @@
},
"type": "business-rule",
"label": "Analisis Had Kifayah",
"position": { "x": 1485, "y": 45 }
"position": { "x": 1665, "y": 120 }
},
{
"id": "notification-1751871950000",
@ -731,12 +878,281 @@
},
"type": "end",
"label": "End",
"position": { "x": 1950, "y": 420 }
"position": { "x": 1935, "y": 390 }
},
{
"id": "html-1752109761532",
"data": {
"label": "Family Tree",
"shape": "rectangle",
"jsCode": "",
"cssCode": "",
"htmlCode": "<!-- Enter your HTML code here -->\n<div class=\"custom-html-content\">\n <h2>Custom HTML Content</h2>\n <p>This is a custom HTML node that can display rich content.</p>\n</div>",
"textColor": "#333333",
"autoRefresh": false,
"borderColor": "#dddddd",
"description": "Family Tree for Borang",
"inputVariables": [],
"backgroundColor": "#ffffff",
"outputVariables": [],
"allowVariableAccess": true
},
"type": "html",
"label": "Family Tree",
"position": { "x": 2385, "y": -360 }
},
{
"id": "rectangle-shape-1752110224921",
"data": {
"label": "",
"shape": "rectangle",
"width": 650,
"height": 400,
"isShape": true,
"shapeType": "rectangle",
"textColor": "#374151",
"borderColor": "#16a34a",
"description": "",
"backgroundColor": "#e8f5e9"
},
"type": "rectangle-shape",
"label": "",
"position": { "x": 375, "y": -570 }
},
{
"id": "text-annotation-1752110279700",
"data": {
"label": "NF-NAS-PRF-AS-PA",
"shape": "rectangle",
"width": 200,
"height": 80,
"isShape": true,
"shapeType": "text-annotation",
"textColor": "#92400e",
"borderColor": "#fbbf24",
"description": "Pernilaian Awal",
"backgroundColor": "#fffbeb"
},
"type": "text-annotation",
"label": "BF-NAS-PRF-AS-PA",
"position": { "x": 810, "y": -555 }
},
{
"id": "rectangle-shape-1752110492897",
"data": {
"label": "",
"shape": "rectangle",
"width": 650,
"height": 400,
"isShape": true,
"shapeType": "rectangle",
"textColor": "#374151",
"borderColor": "#16a34a",
"description": "",
"backgroundColor": "#e8f5e9"
},
"type": "rectangle-shape",
"label": "",
"position": { "x": 1185, "y": -570 }
},
{
"id": "text-annotation-1752110562983",
"data": {
"label": "BF-NAS-PRF-AS-QS-02",
"shape": "rectangle",
"width": 200,
"height": 80,
"isShape": true,
"shapeType": "text-annotation",
"textColor": "#92400e",
"borderColor": "#fbbf24",
"description": "Isi Borang Permohonan Online",
"backgroundColor": "#fffbeb"
},
"type": "text-annotation",
"label": "BF-NAS-PRF-AS-QS-02",
"position": { "x": 1620, "y": -555 }
},
{
"id": "rectangle-shape-1752114739551",
"data": {
"label": "",
"shape": "rectangle",
"width": 650,
"height": 400,
"isShape": true,
"shapeType": "rectangle",
"textColor": "#374151",
"borderColor": "#16a34a",
"description": "",
"backgroundColor": "#e8f5e9"
},
"type": "rectangle-shape",
"label": "",
"position": { "x": 1995, "y": -570 }
},
{
"id": "api-1752114771983",
"data": {
"label": "API Call",
"shape": "rectangle",
"apiUrl": "",
"headers": "{ \"Content-Type\": \"application/json\" }",
"apiMethod": "GET",
"textColor": "#1e40af",
"borderColor": "#3b82f6",
"description": "External API call",
"requestBody": "",
"errorVariable": "apiError",
"outputVariable": "apiResponse",
"backgroundColor": "#eff6ff",
"continueOnError": false
},
"type": "api",
"label": "Called Family Tree",
"position": { "x": 2070, "y": -345 }
},
{
"id": "text-annotation-1752114833800",
"data": {
"label": "",
"shape": "rectangle",
"width": 200,
"height": 80,
"isShape": true,
"shapeType": "text-annotation",
"textColor": "#92400e",
"borderColor": "#fbbf24",
"description": "Family Tree",
"backgroundColor": "#fffbeb"
},
"type": "text-annotation",
"label": "BF-NAS-PRF-AS-FM",
"position": { "x": 2430, "y": -555 }
},
{
"id": "form-1753000000000",
"data": {
"label": "Carian Profil",
"formId": 4,
"formName": "Carian Profil",
"formUuid": "4e07fc8f-a160-478a-85fd-fa3364401545",
"description": "Skrin carian asnaf atau login",
"assignedRoles": [],
"assignedUsers": [],
"inputMappings": [],
"assignmentType": "public",
"outputMappings": [
{ "formField": "search_type", "processVariable": "carianSearchType" },
{ "formField": "search_id", "processVariable": "carianSearchId" },
{ "formField": "login_id", "processVariable": "carianLoginId" },
{
"formField": "login_password",
"processVariable": "carianLoginPassword"
}
],
"fieldConditions": [],
"assignmentVariable": "",
"assignmentVariableType": "user_id"
},
"type": "form",
"label": "Carian Profil",
"position": { "x": 450, "y": -15 }
},
{
"id": "api-1753000000001",
"data": {
"label": "Submit Carian Profil API",
"apiUrl": "https://api.example.com/profiles/search",
"headers": "{ \"Content-Type\": \"application/json\" }",
"apiMethod": "POST",
"description": "Submit profile search or login credentials",
"requestBody": "{\n \"searchType\": \"{carianSearchType}\",\n \"searchId\": \"{carianSearchId}\",\n \"loginId\": \"{carianLoginId}\",\n \"password\": \"{carianLoginPassword}\"\n}",
"errorVariable": "carianProfilApiError",
"outputVariable": "carianProfilApiResponse",
"continueOnError": false
},
"type": "api",
"label": "Submit Carian Profil API",
"position": { "x": 450, "y": 180 }
},
{
"id": "script-1753000000002",
"data": {
"label": "Process Carian Profil Response",
"scriptCode": "// Process API response from profile search/login\nconst response = processVariables.carianProfilApiResponse;\n\nif (response && response.data) {\n if (response.data.loginSuccess) {\n processVariables.loginSuccess = true;\n processVariables.profileData = response.data.profile;\n processVariables.carianProfilStatus = 'login_successful';\n } else if (response.data.profileFound) {\n processVariables.profileFound = true;\n processVariables.profileData = response.data.profile;\n processVariables.carianProfilStatus = 'profile_found';\n } else {\n processVariables.profileFound = false;\n processVariables.loginSuccess = false;\n processVariables.carianProfilStatus = 'not_found';\n }\n} else {\n processVariables.carianProfilStatus = 'error';\n processVariables.carianProfilScriptError = 'Invalid or empty API response';\n}",
"description": "Process the response from the Carian Profil API",
"errorVariable": "carianProfilScriptError",
"inputVariables": ["carianProfilApiResponse"],
"scriptLanguage": "javascript",
"continueOnError": false,
"outputVariables": [
{
"name": "profileFound",
"type": "boolean",
"description": "Indicates if a profile was found via search"
},
{
"name": "loginSuccess",
"type": "boolean",
"description": "Indicates if the asnaf login was successful"
},
{
"name": "profileData",
"type": "object",
"description": "The retrieved profile data"
},
{
"name": "carianProfilStatus",
"type": "string",
"description": "The status of the profile search/login action"
}
]
},
"type": "script",
"label": "Process Carian Profil Response",
"position": { "x": 780, "y": 180 }
},
{
"id": "rectangle-shape-1752115136908",
"data": {
"label": "",
"shape": "rectangle",
"width": 650,
"height": 400,
"isShape": true,
"shapeType": "rectangle",
"textColor": "#374151",
"borderColor": "#16a34a",
"description": "",
"backgroundColor": "#e8f5e9"
},
"type": "rectangle-shape",
"label": "",
"position": { "x": 375, "y": -45 }
},
{
"id": "text-annotation-1752115184991",
"data": {
"label": "",
"shape": "rectangle",
"width": 200,
"height": 80,
"isShape": true,
"shapeType": "text-annotation",
"textColor": "#92400e",
"borderColor": "#fbbf24",
"description": "Carian Profil",
"backgroundColor": "#fffbeb"
},
"type": "text-annotation",
"label": "BF-NAS-PRF-AS-QS-01",
"position": { "x": 810, "y": -30 }
}
],
"viewport": {
"x": -117.3519061583577,
"y": 343.2355816226784,
"zoom": 0.7722385141739979
"x": -104.1414298310864,
"y": 273.7689874210555,
"zoom": 0.402665859661672
}
}

View File

@ -95,6 +95,13 @@
"value": "123456789012",
"description": "Bank account number from Section B"
},
"profileData": {
"name": "profileData",
"type": "object",
"scope": "global",
"value": null,
"description": "Data of the profile found"
},
"radioBangsa": {
"name": "radioBangsa",
"type": "string",
@ -137,6 +144,13 @@
"value": "",
"description": "Birth certificate number from Section A"
},
"loginSuccess": {
"name": "loginSuccess",
"type": "boolean",
"scope": "global",
"value": null,
"description": "Flag indicating successful login"
},
"okuAllowance": {
"name": "okuAllowance",
"type": "number",
@ -151,6 +165,13 @@
"value": true,
"description": "Whether payment processing is ready"
},
"profileFound": {
"name": "profileFound",
"type": "boolean",
"scope": "global",
"value": null,
"description": "Flag indicating if a profile was found"
},
"profileScore": {
"name": "profileScore",
"type": "number",
@ -165,6 +186,20 @@
"value": "lelaki",
"description": "Gender from Section A"
},
"urgencyLevel": {
"name": "urgencyLevel",
"type": "string",
"scope": "global",
"value": null,
"description": "Urgency level (high/medium/low)"
},
"urgencyScore": {
"name": "urgencyScore",
"type": "number",
"scope": "global",
"value": null,
"description": "Calculated urgency score based on needs"
},
"alamatSekolah": {
"name": "alamatSekolah",
"type": "string",
@ -186,6 +221,13 @@
"value": "APP-1751871528249",
"description": "Generated application ID from script processing"
},
"carianLoginId": {
"name": "carianLoginId",
"type": "string",
"scope": "global",
"value": null,
"description": "Asnaf ID for login"
},
"daerahSekolah": {
"name": "daerahSekolah",
"type": "string",
@ -214,6 +256,13 @@
"value": true,
"description": "Whether applicant has dependents"
},
"hasOtherNeeds": {
"name": "hasOtherNeeds",
"type": "boolean",
"scope": "global",
"value": null,
"description": "Whether applicant has other needs"
},
"householdType": {
"name": "householdType",
"type": "string",
@ -270,6 +319,13 @@
"value": ["akaun"],
"description": "Payment method from Section B"
},
"carianSearchId": {
"name": "carianSearchId",
"type": "string",
"scope": "global",
"value": null,
"description": "IC or Foreign ID used for profile search"
},
"dateMasukislam": {
"name": "dateMasukislam",
"type": "string",
@ -305,6 +361,34 @@
"value": null,
"description": "Risk assessment result from verification"
},
"catatanTambahan": {
"name": "catatanTambahan",
"type": "string",
"scope": "global",
"value": null,
"description": "Catatan tambahan berkaitan permohonan"
},
"hasDisasterNeed": {
"name": "hasDisasterNeed",
"type": "boolean",
"scope": "global",
"value": null,
"description": "Whether applicant has disaster-related needs"
},
"hasHomelessness": {
"name": "hasHomelessness",
"type": "boolean",
"scope": "global",
"value": null,
"description": "Whether applicant is homeless"
},
"penilaianAwalId": {
"name": "penilaianAwalId",
"type": "string",
"scope": "global",
"value": null,
"description": "Generated ID for the initial assessment"
},
"radioPendidikan": {
"name": "radioPendidikan",
"type": "string",
@ -340,6 +424,20 @@
"value": "melayu",
"description": "Dependent race from Section B"
},
"carianSearchType": {
"name": "carianSearchType",
"type": "string",
"scope": "global",
"value": null,
"description": "Type of search for Carian Profil (e.g., 'ic_search', 'login')"
},
"dokumenBerkaitan": {
"name": "dokumenBerkaitan",
"type": "array",
"scope": "global",
"value": null,
"description": "Upload dokumen yang berkaitan"
},
"eligibilityScore": {
"name": "eligibilityScore",
"type": "number",
@ -347,6 +445,13 @@
"value": null,
"description": "Eligibility score from verification API"
},
"hasNoUrgentNeeds": {
"name": "hasNoUrgentNeeds",
"type": "boolean",
"scope": "global",
"value": null,
"description": "Whether applicant has no urgent needs"
},
"hubunganKeluarga": {
"name": "hubunganKeluarga",
"type": "array",
@ -389,6 +494,20 @@
"value": true,
"description": "Whether documents verification is required"
},
"hasFamilyConflict": {
"name": "hasFamilyConflict",
"type": "boolean",
"scope": "global",
"value": null,
"description": "Whether applicant has family conflict"
},
"hasUtilityArrears": {
"name": "hasUtilityArrears",
"type": "boolean",
"scope": "global",
"value": null,
"description": "Whether applicant has utility arrears"
},
"jantinaTanggungan": {
"name": "jantinaTanggungan",
"type": "string",
@ -396,6 +515,20 @@
"value": "perempuan",
"description": "Dependent gender from Section B"
},
"keperluanMendesak": {
"name": "keperluanMendesak",
"type": "array",
"scope": "global",
"value": null,
"description": "Apakah keperluan tuan/puan mendesak sekarang ini?"
},
"komitmenKosTinggi": {
"name": "komitmenKosTinggi",
"type": "string",
"scope": "global",
"value": null,
"description": "Adakah tuan/puan mempunyai komitmen dan pembiayaan melibatkan kos yang tinggi?"
},
"namaPemegangAkaun": {
"name": "namaPemegangAkaun",
"type": "string",
@ -431,6 +564,13 @@
"value": null,
"description": "Notes from document verification process"
},
"carianProfilStatus": {
"name": "carianProfilStatus",
"type": "string",
"scope": "global",
"value": null,
"description": "Status of the Carian Profil step (e.g., 'found', 'not_found', 'login_success')"
},
"childcareAllowance": {
"name": "childcareAllowance",
"type": "number",
@ -459,6 +599,13 @@
"value": "mykad",
"description": "Dependent ID type from Section B"
},
"penilaianAwalError": {
"name": "penilaianAwalError",
"type": "string",
"scope": "global",
"value": null,
"description": "Error message from penilaian awal processing"
},
"verificationStatus": {
"name": "verificationStatus",
"type": "string",
@ -473,6 +620,20 @@
"value": null,
"description": "Whether process can proceed to kifayah analysis"
},
"carianLoginPassword": {
"name": "carianLoginPassword",
"type": "string",
"scope": "global",
"value": null,
"description": "Password for asnaf login"
},
"hasDeathRelatedNeed": {
"name": "hasDeathRelatedNeed",
"type": "boolean",
"scope": "global",
"value": null,
"description": "Whether applicant has death-related needs"
},
"noTelefonTanggungan": {
"name": "noTelefonTanggungan",
"type": "string",
@ -480,6 +641,13 @@
"value": "03-12345678",
"description": "Dependent phone number from Section B"
},
"penilaianAwalStatus": {
"name": "penilaianAwalStatus",
"type": "string",
"scope": "global",
"value": null,
"description": "Status of initial assessment submission"
},
"spouseKifayahAmount": {
"name": "spouseKifayahAmount",
"type": "decimal",
@ -501,6 +669,27 @@
"value": "ya",
"description": "Dependent currently studying from Section B"
},
"carianProfilApiError": {
"name": "carianProfilApiError",
"type": "object",
"scope": "global",
"value": null,
"description": "Error from Carian Profil API call"
},
"hasUploadedDocuments": {
"name": "hasUploadedDocuments",
"type": "boolean",
"scope": "global",
"value": null,
"description": "Whether documents were uploaded"
},
"hasUrgentMedicalNeed": {
"name": "hasUrgentMedicalNeed",
"type": "boolean",
"scope": "global",
"value": null,
"description": "Whether applicant has urgent medical needs"
},
"hubunganLainNyatakan": {
"name": "hubunganLainNyatakan",
"type": "string",
@ -515,6 +704,13 @@
"value": null,
"description": "Error from kifayah analysis API call"
},
"readyForPersonalInfo": {
"name": "readyForPersonalInfo",
"type": "boolean",
"scope": "global",
"value": null,
"description": "Whether ready for personal info form"
},
"verificationApiError": {
"name": "verificationApiError",
"type": "object",
@ -522,6 +718,20 @@
"value": null,
"description": "Error from document verification API call"
},
"hasHighCostCommitment": {
"name": "hasHighCostCommitment",
"type": "boolean",
"scope": "global",
"value": null,
"description": "Whether applicant has high cost commitments"
},
"keperluanLainNyatakan": {
"name": "keperluanLainNyatakan",
"type": "string",
"scope": "global",
"value": null,
"description": "Keperluan lain yang dinyatakan oleh pemohon"
},
"kifayahAnalysisResult": {
"name": "kifayahAnalysisResult",
"type": "object",
@ -529,6 +739,13 @@
"value": null,
"description": "Result from kifayah eligibility analysis API"
},
"penilaianAwalApiError": {
"name": "penilaianAwalApiError",
"type": "object",
"scope": "global",
"value": null,
"description": "API error from Submit Penilaian Awal API"
},
"tarikhLahirTanggungan": {
"name": "tarikhLahirTanggungan",
"type": "string",
@ -585,6 +802,20 @@
"value": "ya",
"description": "Dependent lives with family from Section B"
},
"carianProfilApiResponse": {
"name": "carianProfilApiResponse",
"type": "object",
"scope": "global",
"value": null,
"description": "API response from Carian Profil submission"
},
"carianProfilScriptError": {
"name": "carianProfilScriptError",
"type": "object",
"scope": "global",
"value": null,
"description": "Error from Carian Profil script execution"
},
"chronicIllnessAllowance": {
"name": "chronicIllnessAllowance",
"type": "number",
@ -662,6 +893,20 @@
"value": "",
"description": "Dependent other education specification from Section B"
},
"penilaianAwalApiResponse": {
"name": "penilaianAwalApiResponse",
"type": "object",
"scope": "global",
"value": null,
"description": "API response from Submit Penilaian Awal API"
},
"penilaianAwalScriptError": {
"name": "penilaianAwalScriptError",
"type": "object",
"scope": "global",
"value": null,
"description": "Error from Penilaian Awal script execution"
},
"tarikhMulaKfamTanggungan": {
"name": "tarikhMulaKfamTanggungan",
"type": "string",

View File

@ -18,6 +18,8 @@ import BusinessRuleNodeConfiguration from '~/components/process-flow/BusinessRul
import BusinessRuleNodeConfigurationModal from '~/components/process-flow/BusinessRuleNodeConfigurationModal.vue';
import NotificationNodeConfigurationModal from '~/components/process-flow/NotificationNodeConfigurationModal.vue';
import ScriptNodeConfigurationModal from '~/components/process-flow/ScriptNodeConfigurationModal.vue';
import HtmlNodeConfigurationModal from '~/components/process-flow/HtmlNodeConfigurationModal.vue';
import SubprocessNodeConfigurationModal from '~/components/process-flow/SubprocessNodeConfigurationModal.vue';
import ProcessTemplatesModal from '~/components/ProcessTemplatesModal.vue';
import ProcessSettingsModal from '~/components/process-flow/ProcessSettingsModal.vue';
import ProcessHistoryModal from '~/components/ProcessHistoryModal.vue';
@ -86,6 +88,8 @@ const showGatewayConfigModal = ref(false);
const showBusinessRuleConfigModal = ref(false);
const showNotificationConfigModal = ref(false);
const showScriptConfigModal = ref(false);
const showHtmlConfigModal = ref(false);
const showSubprocessConfigModal = ref(false);
const showTemplatesModal = ref(false);
const showProcessSettings = ref(false);
const showDropdown = ref(false);
@ -252,6 +256,22 @@ const components = [
iconColor: 'text-gray-500',
data: { description: 'Script execution', language: 'JavaScript', shape: 'rectangle', backgroundColor: '#f9fafb', borderColor: '#6b7280', textColor: '#374151' }
},
{
type: 'html',
label: 'HTML',
icon: 'html',
iconColor: 'text-blue-500',
data: {
description: 'Custom HTML content',
htmlCode: '<!-- Enter your HTML code here -->\n<div class="custom-html-content">\n <h2>Custom HTML Content</h2>\n <p>This is a custom HTML node that can display rich content.</p>\n</div>',
cssCode: '',
jsCode: '',
shape: 'rectangle',
backgroundColor: '#e0f2fe',
borderColor: '#0ea5e9',
textColor: '#0c4a6e'
}
},
{
type: 'business-rule',
label: 'Business Rule',
@ -266,6 +286,21 @@ const components = [
iconColor: 'text-blue-500',
data: { description: 'Send notifications', shape: 'rectangle', backgroundColor: '#f0f9ff', borderColor: '#0ea5e9', textColor: '#0284c7' }
},
{
type: 'subprocess',
label: 'Sub Process',
icon: 'hub',
iconColor: 'text-teal-500',
data: {
description: 'Executes another process',
subprocessId: null,
subprocessName: '',
shape: 'rectangle',
backgroundColor: '#f0fdfa',
borderColor: '#14b8a6',
textColor: '#134e4a'
}
},
{
type: 'end',
label: 'End Point',
@ -387,6 +422,7 @@ const getNodeIcon = (nodeType) => {
'script': 'code',
'business-rule': 'rule',
'notification': 'notifications',
'subprocess': 'hub',
'start': 'play_circle_filled',
'end': 'stop_circle',
'swimlane-horizontal': 'view-stream',
@ -530,7 +566,6 @@ const gatewayAvailableVariables = computed(() => {
description: v.description || ''
}));
// console.log('Gateway available variables:', allVars);
return allVars;
});
@ -607,6 +642,9 @@ const onNodeSelected = (node) => {
case 'notification':
defaultColors = { backgroundColor: '#f0f9ff', borderColor: '#0ea5e9', textColor: '#0284c7' };
break;
case 'subprocess':
defaultColors = { backgroundColor: '#f0fdfa', borderColor: '#14b8a6', textColor: '#134e4a' };
break;
}
if (!nodeCopy.data.backgroundColor) {
@ -751,6 +789,9 @@ const resetNodeColors = () => {
case 'notification':
defaultColors = { backgroundColor: '#f0f9ff', borderColor: '#0ea5e9', textColor: '#0284c7' };
break;
case 'subprocess':
defaultColors = { backgroundColor: '#f0fdfa', borderColor: '#14b8a6', textColor: '#134e4a' };
break;
}
selectedNodeData.value.data.backgroundColor = defaultColors.backgroundColor;
@ -802,16 +843,17 @@ const onNodesChange = (changes, currentNodes) => {
// Skip processing during component addition to avoid conflicts
if (isAddingComponent.value) {
return;
return;
}
// Handle position changes (only when dragging is complete)
const positionChanges = {};
const hasPositionChanges = changes
.filter(change => change.type === 'position' && change.position && !change.dragging)
.forEach(change => {
positionChanges[change.id] = change.position;
});
const positionChangesList = changes
.filter(change => change.type === 'position' && change.position && !change.dragging);
positionChangesList.forEach(change => {
positionChanges[change.id] = change.position;
});
if (Object.keys(positionChanges).length > 0) {
processStore.updateNodePositions(positionChanges);
@ -833,6 +875,16 @@ const onNodesChange = (changes, currentNodes) => {
}
}
// Handle node additions (this should be rare since we add nodes through the store first)
const addedNodes = changes
.filter(change => change.type === 'add')
.map(change => change.id);
if (addedNodes.length > 0) {
// These nodes are already in the canvas, so we don't need to add them to the store
// unless they're not already there
}
// REMOVED: Don't overwrite selectedNodeData from canvas changes to preserve local edits
// This was causing property panel changes to be lost when the canvas updated
// The canvasNodes computed and its watcher will handle synchronization properly
@ -842,6 +894,11 @@ const onNodesChange = (changes, currentNodes) => {
const onEdgesChange = (changes, currentEdges) => {
if (!changes || !currentEdges) return;
// Skip processing during component addition to avoid conflicts
if (isAddingComponent.value) {
return;
}
// Handle edge removals
const removedEdges = changes
.filter(change => change.type === 'remove')
@ -872,7 +929,9 @@ const onEdgesChange = (changes, currentEdges) => {
label: edge.label || '',
type: edge.type || 'smoothstep',
animated: edge.animated !== undefined ? edge.animated : true,
data: edge.data || {}
data: edge.data || {},
sourceHandle: edge.sourceHandle,
targetHandle: edge.targetHandle
});
}
});
@ -1226,7 +1285,7 @@ const onAddComponent = async (component) => {
...component.data,
// Ensure shape is set for new nodes
shape: component.data.shape || (component.type === 'gateway' ? 'diamond' : 'rectangle'),
// Ensure default colors are set for new nodes (use component defaults which are now type-specific)
// Ensure default colors are set for new nodes
backgroundColor: component.data.backgroundColor,
borderColor: component.data.borderColor,
textColor: component.data.textColor
@ -1234,30 +1293,78 @@ const onAddComponent = async (component) => {
};
// Add the node to the process store
processStore.addNode(newNode);
await processStore.addNode(newNode);
// Wait for the next tick to ensure the store update is complete
// Wait for store update and next render cycle
await nextTick();
await new Promise(resolve => setTimeout(resolve, 50));
// Explicitly sync the canvas with current store state
if (processFlowCanvas.value && processFlowCanvas.value.syncCanvas) {
processFlowCanvas.value.syncCanvas(
processStore.currentProcess.nodes,
processStore.currentProcess.edges
);
// CRITICAL FIX: Instead of calling syncCanvas (which can cause edge removal/re-addition),
// we'll add the node directly to the canvas and preserve existing edges
if (processFlowCanvas.value) {
try {
// Get the fresh node from store (with any store-side modifications)
const freshNode = processStore.currentProcess?.nodes.find(n => n.id === newNode.id);
if (freshNode && processFlowCanvas.value.addNode) {
// Add only the new node to the canvas directly
processFlowCanvas.value.addNode(freshNode);
// Wait for the node to be added to the canvas
await nextTick();
await new Promise(resolve => setTimeout(resolve, 50));
// Select the newly added node after it's stable
onNodeSelected(freshNode);
console.log('✅ Successfully added new node without affecting existing edges');
} else {
console.warn('⚠️ Fresh node not found in store, falling back to full sync');
// Fallback to full sync if something went wrong
const currentNodes = processStore.currentProcess?.nodes || [];
const currentEdges = processStore.currentProcess?.edges || [];
if (processFlowCanvas.value.syncCanvas) {
processFlowCanvas.value.syncCanvas(currentNodes, currentEdges);
await nextTick();
await new Promise(resolve => setTimeout(resolve, 50));
const addedNode = currentNodes.find(n => n.id === newNode.id);
if (addedNode) {
onNodeSelected(addedNode);
}
}
}
} catch (error) {
console.error('❌ Error adding node to canvas:', error);
// Fallback to full sync if direct addition fails
const currentNodes = processStore.currentProcess?.nodes || [];
const currentEdges = processStore.currentProcess?.edges || [];
if (processFlowCanvas.value.syncCanvas) {
console.log('🔄 Falling back to full canvas sync due to error');
processFlowCanvas.value.syncCanvas(currentNodes, currentEdges);
await nextTick();
await new Promise(resolve => setTimeout(resolve, 50));
const addedNode = currentNodes.find(n => n.id === newNode.id);
if (addedNode) {
onNodeSelected(addedNode);
}
}
}
}
// Select the newly added node
onNodeSelected(newNode);
} catch (error) {
console.error('Error adding component:', error);
toast.error('Failed to add component. Please try again.');
} finally {
// Reset the flag after a short delay to allow canvas to stabilize
// Reset the flag after a longer delay to ensure canvas is stable
setTimeout(() => {
isAddingComponent.value = false;
}, 100);
}, 200);
}
};
@ -1498,6 +1605,61 @@ const handleScriptNodeUpdate = async (updatedData) => {
}
};
// Handle HTML node update
const handleHtmlNodeUpdate = async (updatedData) => {
if (selectedNodeData.value && selectedNodeData.value.type === 'html') {
// Make sure to update the label both in data and at the root level
const newLabel = updatedData.label || 'HTML Content';
// Update the data
selectedNodeData.value.data = {
...updatedData,
label: newLabel // Ensure label is in data
};
// Also update the root label
selectedNodeData.value.label = newLabel;
// Add output variables to the process
if (updatedData.outputVariables && Array.isArray(updatedData.outputVariables)) {
updatedData.outputVariables.forEach(output => {
if (output.name && output.name.trim()) {
processStore.addProcessVariable({
name: output.name,
type: output.type || 'string',
scope: 'global',
value: null,
description: output.description || `Output from ${newLabel}`
});
}
});
}
// Update the node in store and refresh
await updateNodeInStore();
}
};
// Handle Sub-process node update
const handleSubprocessNodeUpdate = async (updatedData) => {
if (selectedNodeData.value && selectedNodeData.value.type === 'subprocess') {
// Make sure to update the label both in data and at the root level
const newLabel = updatedData.label || 'Sub Process';
// Update the data
selectedNodeData.value.data = {
...updatedData,
label: newLabel // Ensure label is in data
};
// Also update the root label
selectedNodeData.value.label = newLabel;
// Update the node in store and refresh
await updateNodeInStore();
}
};
// Handle process restoration from history
const handleProcessRestored = (restoredProcess) => {
// The process has been restored in the backend, so we need to reload it
@ -1618,6 +1780,51 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
}
}, 200); // Allow time for canvas to initialize
}, { immediate: false });
// Duplicate Node logic
const duplicateNode = async () => {
if (!selectedNodeData.value) return;
const node = selectedNodeData.value;
// Prevent duplication for start/end nodes and shapes
if (node.type === 'start' || node.type === 'end' || node.data?.isShape) return;
// Deep copy node
const newNode = JSON.parse(JSON.stringify(node));
newNode.id = `${node.type}_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
// Offset position
newNode.position = {
x: (node.position?.x || 100) + 40,
y: (node.position?.y || 100) + 40
};
// Update label
if (newNode.label) {
newNode.label = newNode.label.endsWith(' (Copy)') ? newNode.label : `${newNode.label} (Copy)`;
}
if (newNode.data && newNode.data.label) {
newNode.data.label = newNode.data.label.endsWith(' (Copy)') ? newNode.data.label : `${newNode.data.label} (Copy)`;
}
// Remove any edge/connection-specific data if present
delete newNode.selected;
delete newNode.dragging;
// Add to store only
await processStore.addNode(newNode);
await nextTick();
// Select the new node from the store
const freshNode = processStore.currentProcess?.nodes.find(n => n.id === newNode.id);
if (freshNode) {
onNodeSelected(freshNode);
}
};
// Add computed property for action visibility
const canShowNodeActions = computed(() => {
return (
selectedNodeData.value &&
selectedNodeData.value.type !== 'start' &&
selectedNodeData.value.type !== 'end'
);
});
</script>
<template>
@ -1835,17 +2042,41 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
<div v-else class="flex flex-col h-full">
<!-- Node/Shape Header -->
<div class="p-4 border-b border-gray-200 bg-gray-50">
<div class="flex items-center space-x-2 mb-2">
<div class="w-8 h-8 rounded-lg flex items-center justify-center text-white text-sm font-medium"
:style="{ backgroundColor: nodeBorderColor }">
<Icon :name="`material-symbols:${getNodeIcon(selectedNodeData.type)}`"
class="w-4 h-4" />
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-2">
<div class="w-8 h-8 rounded-lg flex items-center justify-center text-white text-sm font-medium"
:style="{ backgroundColor: nodeBorderColor }">
<Icon :name="`material-symbols:${getNodeIcon(selectedNodeData.type)}`"
class="w-4 h-4" />
</div>
<div>
<h3 class="text-sm font-semibold text-gray-900 capitalize">
{{ selectedNodeData.data?.isShape ? 'Shape' : 'Node' }}: {{ selectedNodeData.type.replace('-', ' ') }}
</h3>
<p class="text-xs text-gray-500">{{ selectedNodeData.id }}</p>
</div>
</div>
<div>
<h3 class="text-sm font-semibold text-gray-900 capitalize">
{{ selectedNodeData.data?.isShape ? 'Shape' : 'Node' }}: {{ selectedNodeData.type.replace('-', ' ') }}
</h3>
<p class="text-xs text-gray-500">{{ selectedNodeData.id }}</p>
<div v-if="canShowNodeActions" class="flex items-center gap-2">
<RsButton
@click="duplicateNode"
variant="secondary-text"
size="sm"
icon
:title="'Duplicate Node'"
class="modern-icon-btn"
>
<Icon name="material-symbols:content-copy" />
</RsButton>
<RsButton
@click="deleteNode"
variant="danger-text"
size="sm"
icon
:title="'Delete Node'"
class="modern-icon-btn"
>
<Icon name="material-symbols:delete" />
</RsButton>
</div>
</div>
</div>
@ -2074,6 +2305,24 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
Configure Script Task
</RsButton>
</div>
<!-- HTML Configuration -->
<div v-if="selectedNodeData.type === 'html'">
<p class="text-xs text-gray-600 mb-3">Configure HTML content and output variables.</p>
<RsButton @click="showHtmlConfigModal = true" variant="primary" class="w-full">
<Icon name="material-symbols:html" class="w-4 h-4 mr-2" />
Configure HTML Node
</RsButton>
</div>
<!-- Sub-process Configuration -->
<div v-if="selectedNodeData.type === 'subprocess'">
<p class="text-xs text-gray-600 mb-3">Select a process to execute as a sub-process.</p>
<RsButton @click="showSubprocessConfigModal = true" variant="primary" class="w-full">
<Icon name="material-symbols:hub" class="w-4 h-4 mr-2" />
Configure Sub-process
</RsButton>
</div>
</div>
<!-- Shape Information -->
@ -2233,6 +2482,25 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
:availableVariables="gatewayAvailableVariables"
@update="handleScriptNodeUpdate"
/>
<!-- HTML Configuration Modal -->
<HtmlNodeConfigurationModal
v-if="selectedNodeData && selectedNodeData.type === 'html'"
v-model="showHtmlConfigModal"
:key="`html-${selectedNodeData.id}-${variablesUpdateKey}`"
:nodeData="selectedNodeData.data"
:availableVariables="gatewayAvailableVariables"
@update="handleHtmlNodeUpdate"
/>
<!-- Sub-process Configuration Modal -->
<SubprocessNodeConfigurationModal
v-if="selectedNodeData && selectedNodeData.type === 'subprocess'"
v-model="showSubprocessConfigModal"
:key="`subprocess-${selectedNodeData.id}-${variablesUpdateKey}`"
:nodeData="selectedNodeData.data"
@update="handleSubprocessNodeUpdate"
/>
<!-- Process Templates Modal -->
<ProcessTemplatesModal
@ -2248,7 +2516,7 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
<!-- Process History Modal -->
<ProcessHistoryModal
:is-open="showProcessHistoryModal"
:process-id="processStore.currentProcess?.id"
:process-id="processStore.currentProcess?.id || ''"
@close="showProcessHistoryModal = false"
@restored="handleProcessRestored"
/>
@ -3222,4 +3490,22 @@ watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
stroke-width: 3px; /* Thicker edges for easier mobile selection */
}
}
/* Modern icon button style for node actions */
.modern-icon-btn {
padding: 0.375rem;
border-radius: 9999px;
min-width: 32px;
min-height: 32px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s;
}
.modern-icon-btn:hover {
background: #f3f4f6;
}
.modern-icon-btn[variant~='danger-text']:hover {
background: #fee2e2;
}
</style>

View File

@ -436,14 +436,7 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
y: flowData.viewport?.y || 0,
zoom: flowData.viewport?.zoom || 1
};
console.log('🧹 Cleaned flow data:', {
originalNodes: flowData.nodes?.length || 0,
cleanNodes: cleanNodes.length,
originalEdges: flowData.edges?.length || 0,
cleanEdges: cleanEdges.length
});
return {
nodes: cleanNodes,
edges: cleanEdges,
@ -660,20 +653,37 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
return existingNode;
}
// Create a new node with proper data structure
const newNode = {
id: node.id || uuidv4(),
type: node.type,
label: node.label || 'New Node',
position: node.position || { x: 0, y: 0 },
data: node.data || {}
label: node.label || node.data?.label || 'New Node',
position: node.position || { x: 100, y: 100 },
data: {
...node.data,
label: node.data?.label || node.label || 'New Node',
// Ensure shape is set for new nodes
shape: node.data?.shape || (node.type === 'gateway' ? 'diamond' : 'rectangle'),
// Ensure default colors are set for new nodes
backgroundColor: node.data?.backgroundColor || '#ffffff',
borderColor: node.data?.borderColor || '#000000'
}
};
this.currentProcess.nodes.push(newNode);
this.selectedNodeId = newNode.id;
// Create a deep copy to avoid reference issues
const nodeCopy = JSON.parse(JSON.stringify(newNode));
// Add to current process nodes array
this.currentProcess.nodes = [...this.currentProcess.nodes, nodeCopy];
// Update selection
this.selectedNodeId = nodeCopy.id;
// Save to history
this.saveToHistory('Add node');
this.unsavedChanges = true;
return newNode;
return nodeCopy;
},
/**