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:
parent
5faefb8900
commit
b4eb3265c2
700
components/process-flow/HtmlNodeConfiguration.vue
Normal file
700
components/process-flow/HtmlNodeConfiguration.vue
Normal 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>
|
116
components/process-flow/HtmlNodeConfigurationModal.vue
Normal file
116
components/process-flow/HtmlNodeConfigurationModal.vue
Normal 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>
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
204
components/process-flow/SubprocessNodeConfiguration.vue
Normal file
204
components/process-flow/SubprocessNodeConfiguration.vue
Normal 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>
|
101
components/process-flow/SubprocessNodeConfigurationModal.vue
Normal file
101
components/process-flow/SubprocessNodeConfigurationModal.vue
Normal 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>
|
@ -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
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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>
|
@ -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;
|
||||
},
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user