Enhance Process Builder with Script Task Configuration

- Introduced a new ScriptNodeConfiguration component for configuring JavaScript tasks within the process builder.
- Added ScriptNodeConfigurationModal for user-friendly script task setup, including input and output variable management.
- Updated process management logic to handle script variables directly within the process store, improving variable management and accessibility.
- Enhanced existing components to support the new script task feature, ensuring seamless integration with the process flow.
- Improved overall user experience with intuitive UI elements and clear documentation for the new functionality.
This commit is contained in:
Md Afiq Iskandar 2025-07-07 16:45:00 +08:00
parent edaae77744
commit 5501c00c7c
22 changed files with 1727 additions and 632 deletions

View File

@ -3,7 +3,7 @@
}
.formkit-fieldset-box {
@apply max-w-md border border-[rgb(var(--fk-border-color))] rounded-lg px-4 py-2;
@apply border-0 rounded-lg py-2;
}
.formkit-legend-box {

View File

@ -24,6 +24,20 @@ const emit = defineEmits(['script-generated'])
const generatedScript = ref('')
const isInitialized = ref(false)
// Helper function to map operators to valid JavaScript
const getJavaScriptOperator = (operator) => {
switch (operator) {
case 'and':
case 'AND':
return '&&'
case 'or':
case 'OR':
return '||'
default:
return '&&' // Default to AND if unknown operator
}
}
// Generate conditional logic script from form components
const generateConditionalLogicScript = () => {
const scriptsArray = []
@ -65,7 +79,7 @@ const generateConditionalLogicScript = () => {
default:
return `getField('${field}') === '${value}'`
}
}).join(` ${operator} `)
}).join(` ${getJavaScriptOperator(operator)} `)
// Generate action functions
const actionCode = action === 'show' ? `showField('${fieldName}')` :

View File

@ -659,7 +659,9 @@ const availableComponents = [
description: 'Section heading text',
defaultProps: {
value: 'Section Heading',
level: 2
level: 2,
width: '100%',
gridColumn: 'span 12'
}
},
{
@ -669,7 +671,9 @@ const availableComponents = [
icon: 'material-symbols:text-snippet-outline',
description: 'Paragraph of text',
defaultProps: {
value: 'Enter some descriptive text here.'
value: 'Enter some descriptive text here.',
width: '100%',
gridColumn: 'span 12'
}
},
{

View File

@ -58,65 +58,166 @@
</div>
<div class="section-content">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Field Type Changer -->
<div v-if="availableTypeChanges.length > 0" class="type-changer-section">
<FormKit
v-if="showField('label') && component.type !== 'button'"
type="text"
label="Field Label"
name="label"
v-model="configModel.label"
help="What users will see above this field"
type="select"
label="Field Type"
name="fieldType"
:modelValue="component.type"
@input="handleTypeChange"
:options="[
{ label: getComponentTypeName(component.type), value: component.type, disabled: true },
...availableTypeChanges.map(type => ({
label: `Change to ${getComponentTypeName(type)}`,
value: type
}))
]"
help="Change this field to a similar compatible type"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., Full Name, Email Address"
/>
<!-- Button Text (instead of label for buttons) -->
<FormKit
v-if="component.type === 'button'"
type="text"
label="Button Text"
name="label"
v-model="configModel.label"
help="Text displayed on the button"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., Submit, Save, Continue"
/>
<FormKit
v-if="showField('name')"
type="text"
label="Field Name (Internal)"
name="name"
v-model="configModel.name"
help="Used internally to identify this field"
validation="required|alpha_numeric"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., full_name, email_address"
/>
<!-- Change Preview -->
<div v-if="showTypeChangePreview" class="type-change-preview">
<div class="preview-header">
<Icon name="heroicons:arrow-right" class="w-4 h-4 text-blue-600" />
<span class="text-sm font-medium text-gray-700">Preview Change</span>
</div>
<div class="preview-content">
<div class="preview-item">
<span class="preview-label">From:</span>
<span class="preview-value">{{ getComponentTypeName(component.type) }}</span>
</div>
<div class="preview-item">
<span class="preview-label">To:</span>
<span class="preview-value">{{ getComponentTypeName(pendingTypeChange) }}</span>
</div>
<div class="preview-item">
<span class="preview-label">Preserved:</span>
<span class="preview-value">{{ getPreservedProperties(component.type, pendingTypeChange).join(', ') }}</span>
</div>
</div>
<div class="preview-actions">
<RsButton @click="confirmTypeChange" variant="primary" size="sm">
<Icon name="heroicons:check" class="w-4 h-4 mr-1" />
Confirm Change
</RsButton>
<RsButton @click="cancelTypeChange" variant="secondary" size="sm">
<Icon name="heroicons:x-mark" class="w-4 h-4 mr-1" />
Cancel
</RsButton>
</div>
</div>
</div>
<FormKit
v-if="showField('placeholder')"
type="text"
label="Placeholder Text"
name="placeholder"
v-model="configModel.placeholder"
help="Hint text shown inside the empty field"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., Enter your email address..."
/>
<!-- Heading-specific Essential Settings -->
<template v-if="component.type === 'heading'">
<FormKit
type="text"
label="Heading Text"
name="value"
v-model="configModel.value"
help="The heading text that will be displayed"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., Personal Information, Contact Details"
validation="required"
/>
<FormKit
type="select"
label="Heading Level"
name="level"
v-model="configModel.level"
:options="[
{ label: 'Heading 1 (Largest)', value: 1 },
{ label: 'Heading 2 (Large)', value: 2 },
{ label: 'Heading 3 (Medium)', value: 3 },
{ label: 'Heading 4 (Small)', value: 4 },
{ label: 'Heading 5 (Smaller)', value: 5 },
{ label: 'Heading 6 (Smallest)', value: 6 }
]"
help="Choose the size and importance of this heading"
:classes="{ outer: 'field-wrapper' }"
/>
</template>
<FormKit
v-if="showField('help')"
type="textarea"
label="Help Text"
name="help"
v-model="configModel.help"
help="Additional instructions or guidance for users"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., We'll use this to send you updates about your order"
rows="2"
/>
<!-- Paragraph-specific Essential Settings -->
<template v-else-if="component.type === 'paragraph'">
<FormKit
type="textarea"
label="Paragraph Text"
name="value"
v-model="configModel.value"
help="The text content that will be displayed"
:classes="{ outer: 'field-wrapper' }"
placeholder="Enter your paragraph text here..."
validation="required"
rows="4"
/>
</template>
<!-- Standard form field Essential Settings -->
<template v-else>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormKit
v-if="showField('label') && component.type !== 'button'"
type="text"
label="Field Label"
name="label"
v-model="configModel.label"
help="What users will see above this field"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., Full Name, Email Address"
/>
<!-- Button Text (instead of label for buttons) -->
<FormKit
v-if="component.type === 'button'"
type="text"
label="Button Text"
name="label"
v-model="configModel.label"
help="Text displayed on the button"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., Submit, Save, Continue"
/>
<FormKit
v-if="showField('name')"
type="text"
label="Field Name (Internal)"
name="name"
v-model="configModel.name"
help="Used internally to identify this field"
validation="required|alpha_numeric"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., full_name, email_address"
/>
</div>
<FormKit
v-if="showField('placeholder')"
type="text"
label="Placeholder Text"
name="placeholder"
v-model="configModel.placeholder"
help="Hint text shown inside the empty field"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., Enter your email address..."
/>
<FormKit
v-if="showField('help')"
type="textarea"
label="Help Text"
name="help"
v-model="configModel.help"
help="Additional instructions or guidance for users"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., We'll use this to send you updates about your order"
rows="2"
/>
</template>
</div>
</div>
@ -350,23 +451,7 @@
</div>
</template>
<!-- Heading Level -->
<template v-if="component.type === 'heading'">
<FormKit
type="select"
label="Heading Size"
name="level"
v-model="configModel.level"
:options="[
{ label: '🔵 Heading 1 (Largest)', value: 1 },
{ label: '🟡 Heading 2 (Large)', value: 2 },
{ label: '🟠 Heading 3 (Medium)', value: 3 },
{ label: '🔴 Heading 4 (Small)', value: 4 }
]"
help="Choose the size and importance of this heading"
:classes="{ outer: 'field-wrapper' }"
/>
</template>
<!-- Button Configuration -->
<template v-if="component.type === 'button'">
@ -1338,6 +1423,10 @@ const configModel = ref({
}
})
// Type changing state
const pendingTypeChange = ref(null)
const showTypeChangePreview = ref(false)
// Component info helpers
const modalTitle = computed(() => {
if (!props.component) return 'Field Settings'
@ -1485,7 +1574,7 @@ const showField = (fieldName) => {
const hasOptions = computed(() => showField('options'))
const hasSpecificSettings = computed(() => {
if (!props.component) return false
const specificTypes = ['mask', 'otp', 'dropzone', 'range', 'heading', 'paragraph', 'button', 'form-section', 'dynamic-list', 'repeating-table']
const specificTypes = ['mask', 'otp', 'dropzone', 'range', 'button', 'form-section', 'dynamic-list', 'repeating-table']
return specificTypes.includes(props.component.type)
})
@ -1517,6 +1606,79 @@ const isTextBasedField = computed(() => {
return ['text', 'textarea', 'email', 'password', 'url', 'tel'].includes(props.component?.type)
})
// Type changing functionality
const compatibilityGroups = {
// Text-based inputs (can switch between each other)
textInputs: ['text', 'textarea', 'email', 'password', 'url', 'tel', 'mask'],
// Selection inputs (can switch between each other)
selectionInputs: ['select', 'radio', 'checkbox'],
// Date/time inputs (can switch between each other)
dateTimeInputs: ['date', 'time', 'datetime-local'],
// Number-based inputs
numberInputs: ['number', 'range'],
// File inputs
fileInputs: ['file', 'dropzone'],
// Layout elements
layoutElements: ['heading', 'paragraph'],
// Advanced inputs (each unique but can have some compatibility)
advancedInputs: ['switch', 'color', 'otp', 'hidden']
}
const availableTypeChanges = computed(() => {
if (!props.component) return []
const currentType = props.component.type
// Find which group the current type belongs to
for (const [groupName, types] of Object.entries(compatibilityGroups)) {
if (types.includes(currentType)) {
// Return other types in the same group
return types.filter(type => type !== currentType)
}
}
return []
})
// Get preserved properties when changing types
const getPreservedProperties = (fromType, toType) => {
const commonProperties = ['label', 'name', 'help', 'validation', 'width', 'gridColumn']
const preserved = [...commonProperties]
// Check for specific preserved properties based on compatibility groups
if (compatibilityGroups.textInputs.includes(fromType) && compatibilityGroups.textInputs.includes(toType)) {
preserved.push('placeholder')
}
if (compatibilityGroups.selectionInputs.includes(fromType) && compatibilityGroups.selectionInputs.includes(toType)) {
preserved.push('options')
}
if (compatibilityGroups.dateTimeInputs.includes(fromType) && compatibilityGroups.dateTimeInputs.includes(toType)) {
preserved.push('min', 'max')
}
if (compatibilityGroups.numberInputs.includes(fromType) && compatibilityGroups.numberInputs.includes(toType)) {
preserved.push('min', 'max', 'step')
}
if (compatibilityGroups.fileInputs.includes(fromType) && compatibilityGroups.fileInputs.includes(toType)) {
preserved.push('accept', 'multiple')
}
if (compatibilityGroups.layoutElements.includes(fromType) && compatibilityGroups.layoutElements.includes(toType)) {
preserved.push('value')
}
return preserved.filter((prop, index, self) => self.indexOf(prop) === index) // Remove duplicates
}
// File size helper
const configModelMaxSizeMB = computed({
get: () => configModel.value.maxSize ? Math.round(configModel.value.maxSize / (1024 * 1024)) : 5,
@ -1868,6 +2030,201 @@ const removeNestedComponent = (index) => {
configModel.value.children.splice(index, 1)
}
}
// Type changing methods
const handleTypeChange = (newType) => {
if (newType === props.component.type) return
pendingTypeChange.value = newType
showTypeChangePreview.value = true
}
const confirmTypeChange = () => {
if (!pendingTypeChange.value) return
const newType = pendingTypeChange.value
const preservedProps = getPreservedProperties(props.component.type, newType)
// Create new component configuration with preserved properties
const newProps = {}
// Preserve common properties
preservedProps.forEach(prop => {
if (configModel.value[prop] !== undefined) {
newProps[prop] = configModel.value[prop]
}
})
// Add default properties for the new type that aren't being preserved
const newTypeDefaults = getDefaultPropsForType(newType)
Object.keys(newTypeDefaults).forEach(prop => {
if (newProps[prop] === undefined) {
newProps[prop] = newTypeDefaults[prop]
}
})
// Update the component type and properties
const updatedComponent = {
...props.component,
type: newType,
props: newProps
}
// Emit the updated component
emit('update-component', updatedComponent)
// Reset state
pendingTypeChange.value = null
showTypeChangePreview.value = false
// Close modal after change
handleClose()
}
const cancelTypeChange = () => {
pendingTypeChange.value = null
showTypeChangePreview.value = false
}
// Get default properties for a specific component type
const getDefaultPropsForType = (type) => {
const defaults = {
text: {
type: 'text',
placeholder: 'Enter text...',
validation: ''
},
textarea: {
type: 'textarea',
placeholder: 'Enter text...',
validation: '',
rows: 3
},
email: {
type: 'email',
placeholder: 'email@example.com',
validation: 'email'
},
password: {
type: 'password',
placeholder: 'Enter password...',
validation: ''
},
url: {
type: 'url',
placeholder: 'https://example.com',
validation: 'url'
},
tel: {
type: 'tel',
placeholder: '+1 (555) 123-4567',
validation: ''
},
mask: {
type: 'mask',
placeholder: 'Enter value...',
mask: '###-###-####',
validation: ''
},
select: {
type: 'select',
placeholder: 'Select an option',
options: [
{ label: 'Option 1', value: 'option_1' },
{ label: 'Option 2', value: 'option_2' }
],
validation: ''
},
radio: {
type: 'radio',
options: [
{ label: 'Option 1', value: 'option_1' },
{ label: 'Option 2', value: 'option_2' }
],
validation: ''
},
checkbox: {
type: 'checkbox',
options: [
{ label: 'Option 1', value: 'option_1' },
{ label: 'Option 2', value: 'option_2' }
],
validation: ''
},
date: {
type: 'date',
validation: ''
},
time: {
type: 'time',
validation: ''
},
'datetime-local': {
type: 'datetime-local',
validation: ''
},
number: {
type: 'number',
placeholder: '0',
validation: ''
},
range: {
type: 'range',
min: 0,
max: 100,
step: 1,
value: 50,
validation: ''
},
file: {
type: 'file',
accept: '*/*',
validation: ''
},
dropzone: {
type: 'dropzone',
accept: 'image/*,.pdf,.doc,.docx',
multiple: true,
maxSize: 5242880,
maxFiles: 5,
validation: ''
},
switch: {
type: 'switch',
value: false,
validation: ''
},
color: {
type: 'color',
value: '#3b82f6',
validation: ''
},
otp: {
type: 'otp',
digits: 6,
validation: 'required'
},
hidden: {
type: 'hidden',
value: ''
},
heading: {
value: 'Section Heading',
level: 2
},
paragraph: {
value: 'Enter some descriptive text here.'
}
}
// Ensure width and gridColumn are included for all types
const baseDefaults = {
width: '100%',
gridColumn: 'span 12'
}
return { ...baseDefaults, ...(defaults[type] || {}) }
}
</script>
<style scoped>
@ -2145,6 +2502,39 @@ const removeNestedComponent = (index) => {
@apply mb-0;
}
/* Type Changer Styles */
.type-changer-section {
@apply mb-6 p-4 border-2 border-dashed border-blue-200 rounded-lg bg-blue-50;
}
.type-change-preview {
@apply mt-4 p-4 bg-white border border-blue-300 rounded-lg;
}
.preview-header {
@apply flex items-center space-x-2 mb-3 pb-2 border-b border-gray-200;
}
.preview-content {
@apply space-y-2 mb-4;
}
.preview-item {
@apply flex items-center justify-between text-sm;
}
.preview-label {
@apply font-medium text-gray-600;
}
.preview-value {
@apply text-gray-800 bg-gray-100 px-2 py-1 rounded text-xs;
}
.preview-actions {
@apply flex items-center space-x-2 pt-3 border-t border-gray-200;
}
/* Mask Examples */
.mask-examples {
@apply mt-3 p-3 bg-gray-50 rounded-lg;

View File

@ -3,6 +3,7 @@ import { useThemeStore } from "~/stores/theme";
import { vue } from "@codemirror/lang-vue";
import { javascript } from "@codemirror/lang-javascript";
import { css } from "@codemirror/lang-css";
import { oneDark } from "@codemirror/theme-one-dark";
import { amy, ayuLight, barf, clouds, cobalt, dracula } from "thememirror";
@ -24,9 +25,9 @@ const props = defineProps({
type: Object,
default: () => ({}),
},
mode: {
language: {
type: String,
default: "vue",
default: "javascript",
},
height: {
type: String,
@ -88,7 +89,7 @@ const dropdownThemes = ref([
const value = ref(props.modelValue);
const extensions = ref([]);
if (props.mode == "vue") {
if (props.language == "vue") {
extensions.value = [
vue(),
oneDark,
@ -96,7 +97,7 @@ if (props.mode == "vue") {
indentUnit.of(" "),
indentOnInput(),
];
} else {
} else if (props.language == "javascript") {
extensions.value = [
javascript(),
oneDark,
@ -104,6 +105,12 @@ if (props.mode == "vue") {
indentUnit.of(" "),
indentOnInput(),
];
} else if (props.language == "css") {
extensions.value = [
css(),
oneDark,
autocompletion(),
];
}
const totalLines = ref(0);
@ -135,7 +142,7 @@ watch(
? dracula
: clouds;
if (props.mode == "vue") {
if (props.language == "vue") {
extensions.value = [
vue(),
themeExtension,

View File

@ -55,7 +55,7 @@ const props = defineProps({
},
overlayClose: {
type: Boolean,
default: true,
default: false,
},
height: {
type: String,

View File

@ -325,7 +325,6 @@
<script setup>
import { ref, computed, watch, nextTick } from 'vue';
import { useProcessBuilderStore } from '@/stores/processBuilder';
import { useVariableStore } from '@/stores/variableStore';
const props = defineProps({
nodeData: {
@ -340,8 +339,8 @@ const props = defineProps({
const emit = defineEmits(['update']);
// Get the variable store for creating variables
const variableStore = useVariableStore();
// Get the process store for creating variables
const processStore = useProcessBuilderStore();
// Local state for node data - create a deep copy to avoid mutation issues
const localNodeData = ref({
@ -366,9 +365,7 @@ const availableVariables = computed(() => {
// Only use global variables, matching VariableManager implementation
const globalVars = props.availableVariables?.map(v => ({
name: v.name || 'unnamed',
label: v?.description
? `${v.name} (${v.description})`
: v.name,
label: `${v.name} (${v.type || 'string'})`,
type: v.type || 'string',
value: v.value
})) || [];
@ -406,7 +403,7 @@ function createGlobalVariable(name, description = '') {
};
// Add the variable using the store's addVariable method
variableStore.addVariable(newVariable);
processStore.addProcessVariable(newVariable);
// Force a refresh of the component
nextTick(() => {
@ -421,23 +418,29 @@ function saveChanges() {
// Ensure variables exist before saving
if (nodeDataCopy.outputVariable) {
variableStore.addVariableIfNotExists({
name: nodeDataCopy.outputVariable,
type: 'object',
scope: 'global',
value: null,
description: `API response from ${nodeDataCopy.label}`
});
const existingVar = processStore.getProcessVariable(nodeDataCopy.outputVariable);
if (!existingVar) {
processStore.addProcessVariable({
name: nodeDataCopy.outputVariable,
type: 'object',
scope: 'global',
value: null,
description: `API response from ${nodeDataCopy.label}`
});
}
}
if (nodeDataCopy.errorVariable) {
variableStore.addVariableIfNotExists({
name: nodeDataCopy.errorVariable,
type: 'object',
scope: 'global',
value: null,
description: `API error from ${nodeDataCopy.label}`
});
const existingVar = processStore.getProcessVariable(nodeDataCopy.errorVariable);
if (!existingVar) {
processStore.addProcessVariable({
name: nodeDataCopy.errorVariable,
type: 'object',
scope: 'global',
value: null,
description: `API error from ${nodeDataCopy.label}`
});
}
}
// Emit the updated data to parent
@ -500,16 +503,14 @@ async function testApiCall() {
// Update variables in the store
if (testResult.value.success && localNodeData.value.outputVariable) {
variableStore.updateVariable(
processStore.updateProcessVariable(
localNodeData.value.outputVariable,
{ value: result.data },
'global'
{ value: result.data }
);
} else if (!testResult.value.success && localNodeData.value.errorVariable) {
variableStore.updateVariable(
processStore.updateProcessVariable(
localNodeData.value.errorVariable,
{ value: testResult.value.error },
'global'
{ value: testResult.value.error }
);
}
} catch (error) {
@ -524,10 +525,9 @@ async function testApiCall() {
// Store error in variable
if (localNodeData.value.errorVariable) {
variableStore.updateVariable(
processStore.updateProcessVariable(
localNodeData.value.errorVariable,
{ value: testResult.value.error },
'global'
{ value: testResult.value.error }
);
}
} finally {
@ -586,9 +586,14 @@ function getPreviewWithValues(field) {
text = text.replace(variablePattern, (match, varName) => {
const variable = availableVariables.value.find(v => v.name === varName);
if (variable) {
return JSON.stringify(variable.value) || '(undefined)';
// Handle different value types
if (variable.value === null || variable.value === undefined) {
return `"<${varName}>"`; // Show variable name as placeholder
}
// Return the actual value or stringify complex objects
return typeof variable.value === 'string' ? `"${variable.value}"` : JSON.stringify(variable.value);
}
return '(undefined)';
return `"<${varName}>"`; // Show variable name as placeholder for missing variables
});
// Try to format as JSON if possible
@ -599,7 +604,8 @@ function getPreviewWithValues(field) {
return text;
}
} catch (error) {
return '(Invalid format)';
console.warn(`Preview error for ${field}:`, error);
return localNodeData.value[field] || '';
}
}
</script>

View File

@ -492,7 +492,7 @@
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import { useVariableStore } from '~/stores/variableStore';
import { useProcessBuilderStore } from '~/stores/processBuilder';
import { DateTime } from 'luxon';
import { Icon } from '#components';
@ -514,7 +514,7 @@ const props = defineProps({
const emit = defineEmits(['update', 'close']);
// Get the variable store for variables
const variableStore = useVariableStore();
const processStore = useProcessBuilderStore();
// Create a local copy of the node data for editing
const localNodeData = ref({
@ -527,25 +527,17 @@ const localNodeData = ref({
// Get available variables for conditions and actions
const availableVariables = computed(() => {
const processVars = variableStore.getAllVariables.process.map(v => ({
name: v.name || 'unnamed',
label: v?.description
? `${v.description} (${v.name || 'unnamed'}, process)`
: `${v.name || 'unnamed'} (process)`,
type: v.type || 'string',
scope: 'process'
const processVariables = processStore.getProcessVariables();
if (!processVariables || typeof processVariables !== 'object') {
return [];
}
return Object.entries(processVariables).map(([name, variable]) => ({
name: name,
label: `${name} (${variable.type || 'string'})`,
type: variable.type || 'string',
scope: variable.scope || 'process'
}));
const globalVars = variableStore.getAllVariables.global.map(v => ({
name: v.name || 'unnamed',
label: v?.description
? `${v.description} (${v.name || 'unnamed'}, global)`
: `${v.name || 'unnamed'} (global)`,
type: v.type || 'string',
scope: 'global'
}));
return [...processVars, ...globalVars];
});
// Initialize with default values if needed

View File

@ -607,7 +607,7 @@
<script setup>
import { ref, computed, watch, nextTick } from 'vue';
import { useVariableStore } from '@/stores/variableStore';
import { useProcessBuilderStore } from '@/stores/processBuilder';
import FormSelector from './FormSelector.vue';
import { Icon } from '#components';
@ -633,7 +633,7 @@ const props = defineProps({
const emit = defineEmits(['update']);
// Get the variable store for creating variables
const variableStore = useVariableStore();
const processStore = useProcessBuilderStore();
// State for form fields
const formFields = ref([]);
@ -836,7 +836,7 @@ function createVariableFromMapping(mapping) {
};
// Add the variable
variableStore.addVariable(newVariable);
processStore.addProcessVariable(newVariable);
// Update the mapping to use the new variable name (string value, not object)
mapping.processVariable = variableName;
@ -886,7 +886,7 @@ const formFieldOptions = computed(() => {
// Computed property for process variable options (for FormKit select)
const processVariableOptions = computed(() => {
return props.availableVariables.map(variable => ({
label: variable.label || `${variable.name} (${variable.scope || 'unknown'})`,
label: `${variable.name} (${variable.type || 'string'})`,
value: variable.name,
description: variable.description || `Type: ${variable.type || 'unknown'}`
}));

View File

@ -612,7 +612,7 @@ const getPathSummary = (group) => {
:key="variable.name"
:value="variable.name"
>
{{ variable.label || variable.name }} ({{ variable.type }})
{{ variable.name }} ({{ variable.type }})
</option>
</select>
<div v-if="condition.variable" class="mt-1 flex items-center">

View File

@ -458,7 +458,6 @@
<script setup>
import { ref, computed, watch } from 'vue';
import { useVariableStore } from '~/stores/variableStore';
const props = defineProps({
nodeData: {

View File

@ -169,6 +169,25 @@ const availableComponents = [
priority: 'medium'
}
}
},
{
type: 'script',
name: 'Script Task',
category: 'Core',
icon: 'material-symbols:code',
description: 'Execute JavaScript to transform data',
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',
scriptLanguage: 'javascript',
inputVariables: [],
outputVariables: [],
continueOnError: false,
errorVariable: 'scriptError'
}
}
}
];

View File

@ -305,7 +305,6 @@
<script setup>
import { ref, computed, watch } from 'vue'
import { useProcessBuilderStore } from '~/stores/processBuilder'
import { useVariableStore } from '~/stores/variableStore'
const props = defineProps({
modelValue: {
@ -317,7 +316,6 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
const processStore = useProcessBuilderStore()
const variableStore = useVariableStore()
// Modal visibility
const showModal = computed({
@ -370,8 +368,11 @@ const edgeCount = computed(() => {
})
const variableCount = computed(() => {
const allVars = variableStore.getAllVariables
return (allVars.global?.length || 0) + (allVars.process?.length || 0)
const processVariables = processStore.getProcessVariables()
if (!processVariables || typeof processVariables !== 'object') {
return 0
}
return Object.keys(processVariables).length
})
// JSON export functionality
@ -410,7 +411,7 @@ const formattedJson = computed(() => {
nodes: processStore.currentProcess?.nodes || [],
edges: processStore.currentProcess?.edges || []
},
variables: variableStore.getAllVariables,
variables: processStore.getProcessVariables(),
metadata: {
nodeCount: nodeCount.value,
edgeCount: edgeCount.value,

View File

@ -0,0 +1,453 @@
<template>
<div class="script-node-config">
<div class="config-content">
<!-- Header -->
<div class="config-header">
<h3 class="text-lg font-semibold text-gray-800">Script Task Configuration</h3>
<p class="text-sm text-gray-600">Configure JavaScript code to transform data and variables</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="Script Name"
name="label"
v-model="localNodeData.label"
help="Display name for this script task"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., Process API Response, Transform Data"
validation="required"
/>
<FormKit
type="textarea"
label="Description"
name="description"
v-model="localNodeData.description"
help="Describe what this script does"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., Transforms API response data into process variables"
rows="2"
/>
</div>
</div>
</div>
<!-- Script Code Section -->
<div class="config-section">
<h4 class="section-title">Script Code</h4>
<div class="section-content">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">
JavaScript Code
</label>
<div class="script-editor-container">
<RsCodeMirror
v-model="localNodeData.scriptCode"
:options="{
mode: 'javascript',
theme: 'default',
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets: true,
indentUnit: 2,
tabSize: 2
}"
class="script-editor"
placeholder="// Available objects:
// - processVariables: object containing all process variables
// - console: for debugging (console.log)
//
// Example - Transform API response:
// const apiData = processVariables.apiResponse;
// processVariables.applicantStatus = apiData.data.status;
// processVariables.approvalRequired = apiData.data.score < 70;
//
// Example - Extract specific fields:
// processVariables.firstName = processVariables.text3?.split(' ')[0];
// processVariables.documentCount = processVariables.apiResponse?.data?.documents?.length || 0;"
/>
</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>
<!-- Error Handling -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormKit
type="checkbox"
label="Continue on Error"
name="continueOnError"
v-model="localNodeData.continueOnError"
help="If enabled, process continues even if script fails"
:classes="{ outer: 'field-wrapper' }"
/>
<FormKit
v-if="!localNodeData.continueOnError"
type="text"
label="Error Variable"
name="errorVariable"
v-model="localNodeData.errorVariable"
help="Variable to store error information"
:classes="{ outer: 'field-wrapper' }"
placeholder="e.g., scriptError"
/>
</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 script 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 script will create or modify
</p>
</div>
</div>
</div>
</div>
<!-- Script Testing Section -->
<div class="config-section">
<h4 class="section-title">Script Testing</h4>
<div class="section-content">
<div class="test-container p-4 bg-gray-50 rounded-lg">
<div class="flex items-center justify-between mb-3">
<h5 class="font-medium text-gray-700">Test Script Execution</h5>
<button
@click="testScript"
:disabled="!localNodeData.scriptCode.trim()"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
Test Script
</button>
</div>
<div v-if="testResult" class="test-result">
<div class="flex items-center space-x-2 mb-2">
<Icon
:name="testResult.success ? 'material-symbols:check-circle' : 'material-symbols:error'"
:class="testResult.success ? 'text-green-600' : 'text-red-600'"
class="w-5 h-5"
/>
<span :class="testResult.success ? 'text-green-700' : 'text-red-700'" class="font-medium">
{{ testResult.success ? 'Script executed successfully' : 'Script execution failed' }}
</span>
</div>
<div v-if="testResult.error" class="error-details p-3 bg-red-50 border border-red-200 rounded text-sm text-red-700">
{{ testResult.error }}
</div>
<div v-if="testResult.result" class="result-details">
<h6 class="font-medium text-gray-700 mb-2">Modified Variables:</h6>
<pre class="text-xs bg-white p-3 border border-gray-200 rounded overflow-auto max-h-32">{{ JSON.stringify(testResult.result, null, 2) }}</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } 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: '',
scriptCode: '// Transform API response or process variables\n// Example:\n// processVariables.newVariable = processVariables.apiResponse.data.field;\n',
scriptLanguage: 'javascript',
inputVariables: [],
outputVariables: [],
continueOnError: false,
errorVariable: 'scriptError'
})
const testResult = ref(null)
// 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 testScript = () => {
try {
// Create a test environment with mock process variables
const mockProcessVariables = {}
// Add mock values for available variables
props.availableVariables.forEach(variable => {
mockProcessVariables[variable.name] = variable.value || getMockValueForType(variable.type)
})
// Create a safe execution context
const scriptFunction = new Function('processVariables', 'console', localNodeData.value.scriptCode)
// Mock console for testing
const mockConsole = {
log: (...args) => console.log('[Script Test]', ...args),
error: (...args) => console.error('[Script Test]', ...args),
warn: (...args) => console.warn('[Script Test]', ...args)
}
// Execute the script
scriptFunction(mockProcessVariables, mockConsole)
testResult.value = {
success: true,
result: mockProcessVariables,
error: null
}
} catch (error) {
testResult.value = {
success: false,
result: null,
error: error.message
}
}
}
const getMockValueForType = (type) => {
switch (type) {
case 'string': return 'mock_value'
case 'number': return 42
case 'boolean': return true
case 'object': return { data: { field: 'value' } }
case 'array': return ['item1', 'item2']
default: return 'mock_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 || 'Script Task',
description: newData.description || '',
scriptCode: newData.scriptCode || '// Transform API response or process variables\n// Example:\n// processVariables.newVariable = processVariables.apiResponse.data.field;\n',
scriptLanguage: newData.scriptLanguage || 'javascript',
inputVariables: newData.inputVariables || [],
outputVariables: newData.outputVariables || [],
continueOnError: newData.continueOnError || false,
errorVariable: newData.errorVariable || 'scriptError'
}
}
},
{ immediate: true, deep: true }
)
// Watch for changes in localNodeData and emit updates
watch(
localNodeData,
() => {
emitUpdate()
},
{ deep: true }
)
// Reset test result when script changes
watch(
() => localNodeData.value.scriptCode,
() => {
testResult.value = null
}
)
</script>
<style scoped>
.script-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;
}
.script-editor-container {
@apply border border-gray-300 rounded-lg overflow-hidden;
}
.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;
}
.test-container {
@apply border border-gray-200;
}
.field-wrapper {
@apply mb-0;
}
</style>

View File

@ -0,0 +1,113 @@
<template>
<RsModal
v-model="showModal"
title="Script Task 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-purple-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 Script Task</h3>
<p class="text-sm text-gray-600">
Execute JavaScript code to transform data, process variables, and apply business logic.
Script tasks are useful for data manipulation, calculations, and preparing data for subsequent steps.
</p>
</div>
</div>
</div>
<!-- Main configuration area -->
<ScriptNodeConfiguration
:nodeData="localNodeData"
:availableVariables="availableVariables"
@update="handleUpdate"
/>
<!-- Quick Reference Guide -->
<div class="mt-6 bg-purple-50 p-4 rounded-md border border-purple-100">
<h4 class="font-medium text-purple-700 mb-2 flex items-center">
<Icon name="material-symbols:info-outline" class="mr-1" />
Quick Reference Guide
</h4>
<div class="text-sm text-purple-700">
<ul class="list-disc list-inside space-y-1">
<li>Use <code class="bg-purple-100 px-1">processVariables</code> to access and modify process data</li>
<li>Available functions: <code class="bg-purple-100 px-1">console.log()</code> for debugging</li>
<li>Transform API responses into usable variables for subsequent steps</li>
<li>Apply business rules and calculations to your data</li>
<li>Test your script before saving to ensure it works correctly</li>
</ul>
</div>
</div>
</template>
</RsModal>
</template>
<script setup>
import { ref, watch } from 'vue';
import ScriptNodeConfiguration from './ScriptNodeConfiguration.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 });
// Watch for changes to modelValue prop to sync modal visibility
watch(() => props.modelValue, (value) => {
showModal.value = value;
});
// Watch for changes to showModal to emit update:modelValue
watch(() => showModal.value, (value) => {
emit('update:modelValue', value);
});
// Watch for changes to nodeData prop
watch(() => props.nodeData, (value) => {
localNodeData.value = { ...value };
}, { deep: true });
function handleUpdate(updatedData) {
localNodeData.value = { ...updatedData };
}
function saveAndClose() {
emit('update', localNodeData.value);
showModal.value = false;
}
function closeModal() {
showModal.value = false;
}
</script>
<style scoped>
code {
font-family: monospace;
border-radius: 0.25rem;
}
</style>

View File

@ -141,15 +141,15 @@
</div>
</div>
<!-- Default Value Display -->
<div v-if="variable.defaultValue !== undefined && variable.defaultValue !== ''" class="mt-3">
<!-- Current Value Display -->
<div v-if="variable.value !== undefined && variable.value !== ''" class="mt-3">
<div class="bg-amber-50 rounded-md p-2 border border-amber-100">
<div class="flex items-center gap-1.5 mb-1">
<Icon name="material-symbols:settings" class="w-3.5 h-3.5 text-amber-600" />
<span class="text-xs font-medium text-amber-700 uppercase tracking-wide">Default Value</span>
<span class="text-xs font-medium text-amber-700 uppercase tracking-wide">Current Value</span>
</div>
<div class="font-mono text-xs text-amber-800 break-all">
{{ formatValue(variable.defaultValue, variable.type) }}
{{ formatValue(variable.value, variable.type) }}
</div>
</div>
</div>
@ -263,9 +263,9 @@
<script setup>
import { ref, computed } from "vue";
import { useVariableStore } from "~/stores/variableStore";
import { useProcessBuilderStore } from "~/stores/processBuilder";
const variableStore = useVariableStore();
const processStore = useProcessBuilderStore();
// State
const showAddVariable = ref(false);
@ -353,9 +353,8 @@ const getHelpTextForType = (type) => {
// Computed
const variables = computed(() => {
// Return all variables (treating everything as global)
const allVars = variableStore.getAllVariables;
return [...allVars.global, ...allVars.process];
// Get variables from the current process
return processStore.getProcessVariables();
});
// Filtered variables based on search query
@ -375,15 +374,14 @@ const editVariable = (variable) => {
editingVariable.value = variable;
variableForm.value = {
...variable,
defaultValue: variable.defaultValue || ""
defaultValue: variable.value || ""
};
showAddVariable.value = true;
};
const deleteVariable = (variable) => {
if (confirm(`Are you sure you want to delete the variable "${variable.name}"? This might affect parts of your process that use this variable.`)) {
const scope = variable.scope || 'global';
variableStore.deleteVariable(variable.name, scope);
processStore.deleteProcessVariable(variable.name);
}
};
@ -427,19 +425,15 @@ const saveVariable = async (formData) => {
type: formData.type,
scope: "global",
description: formData.description,
defaultValue: processedDefaultValue
value: processedDefaultValue
};
if (editingVariable.value) {
// Update existing variable
variableStore.updateVariable(
editingVariable.value.name,
newVariable,
'global'
);
processStore.updateProcessVariable(editingVariable.value.name, newVariable);
} else {
// Add new variable
variableStore.addVariable(newVariable);
processStore.addProcessVariable(newVariable);
}
// Close modal and reset form

View File

@ -7,7 +7,7 @@ export const useToast = () => {
success: (message, options = {}) => {
toast.success(message, {
timeout: 3000,
position: 'top-right',
position: 'bottom-right',
...options
});
},
@ -15,7 +15,7 @@ export const useToast = () => {
error: (message, options = {}) => {
toast.error(message, {
timeout: 5000,
position: 'top-right',
position: 'bottom-right',
...options
});
},
@ -23,7 +23,7 @@ export const useToast = () => {
warning: (message, options = {}) => {
toast.warning(message, {
timeout: 4000,
position: 'top-right',
position: 'bottom-right',
...options
});
},
@ -31,7 +31,7 @@ export const useToast = () => {
info: (message, options = {}) => {
toast.info(message, {
timeout: 3000,
position: 'top-right',
position: 'bottom-right',
...options
});
}

View File

@ -16,8 +16,11 @@ const props = defineProps({
// console.log("props", props);
const isDynamicRouteError = computed(() => {
const path = window?.location?.pathname || '';
return path.includes('/[') || path.includes(']');
if(process.client) {
const path = window?.location?.pathname || '';
return path.includes('/[') || path.includes(']');
}
return false;
});
const handleError = () => {

View File

@ -1,479 +1,472 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useProcessBuilderStore } from '~/stores/processBuilder'
definePageMeta({
title: "Dashboard",
title: "Dashboard - Corrad BPM",
middleware: ["auth"],
requiresAuth: true,
});
const data1 = ref([]);
const data2 = ref([]);
const data3 = ref([]);
const data4 = ref([]);
var sparkline1Data = [47, 45, 54, 38, 56, 24, 65];
var sparkline2Data = [61, 35, 66, 41, 59, 25, 32];
var sparkline3Data = [25, 18, 36, 41, 43, 35, 14];
var sparkline4Data = [8, 16, 22, 41, 43, 35, 14];
const router = useRouter()
const processStore = useProcessBuilderStore()
const changeKey = ref(0);
// Dashboard stats
const stats = ref({
activeProcesses: 0,
totalForms: 0,
pendingTasks: 0,
completedInstances: 0
})
const customers = [
// Recent activity data
const recentProcesses = ref([])
const recentTasks = ref([])
// System features data
const systemFeatures = [
{
name: "Iqmal",
age: "25",
city: "Kuala Lumpur",
country: "Malaysia",
totalPurchase: 1524,
purchase: 23,
id: 'process-builder',
title: 'Process Builder',
description: 'Design business workflows with drag-and-drop visual editor',
icon: 'material-symbols:account-tree',
color: 'bg-blue-500',
colorLight: 'bg-blue-100',
textColor: 'text-blue-600',
route: '/process-builder',
features: ['Visual Workflow Designer', 'BPMN 2.0 Support', 'Real-time Collaboration']
},
{
name: "Adi",
age: "45",
city: "Pulau Pinang",
country: "Malaysia",
totalPurchase: 643,
purchase: 14,
id: 'form-builder',
title: 'Form Builder',
description: 'Create dynamic forms with advanced components and logic',
icon: 'material-symbols:description',
color: 'bg-purple-500',
colorLight: 'bg-purple-100',
textColor: 'text-purple-600',
route: '/form-builder',
features: ['Drag & Drop Form Builder', 'Conditional Logic', 'FormKit Integration']
},
{
name: "Raziq",
age: "21",
city: "Kelantan",
country: "Malaysia",
totalPurchase: 543,
purchase: 12,
id: 'execution-engine',
title: 'Execution Engine',
description: 'Run and manage process instances with monitoring',
icon: 'material-symbols:play-circle',
color: 'bg-green-500',
colorLight: 'bg-green-100',
textColor: 'text-green-600',
route: '/execution',
features: ['Process Execution', 'Task Management', 'Instance Monitoring']
},
{
name: "Haqeem",
age: "19",
city: "Negeri Sembilan",
country: "Malaysia",
totalPurchase: 258,
purchase: 6,
},
];
const randomizeArray = function (arg) {
var array = arg.slice();
var currentIndex = array.length,
temporaryValue,
randomIndex;
while (0 !== currentIndex) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
id: 'api-integrations',
title: 'API Integrations',
description: 'Connect with external systems and services',
icon: 'material-symbols:api',
color: 'bg-orange-500',
colorLight: 'bg-orange-100',
textColor: 'text-orange-600',
route: '/devtool/api-editor',
features: ['REST API Calls', 'Webhook Support', 'Data Transformation']
}
]
return array;
};
data1.value.push({
name: "Revenues",
data: randomizeArray(sparkline1Data),
});
data2.value.push({
name: "Users",
data: randomizeArray(sparkline2Data),
});
data3.value.push({
name: "Products",
data: randomizeArray(sparkline3Data),
});
data4.value.push({
name: "Viewers",
data: randomizeArray(sparkline4Data),
});
const chartOptions = computed(() => ({
chart: {
type: "area",
sparkline: {
enabled: true,
},
},
stroke: {
curve: "smooth",
},
fill: {
opacity: 1,
},
labels: [...Array(7).keys()].map((n) => `2022-06-0${n + 1}`),
xaxis: {
type: "datetime",
},
}));
// Radial Chart
const radialData = ref([44, 55, 67, 83]);
const chartOptionsRadial = computed(() => ({
chart: {
height: 350,
type: "radialBar",
},
plotOptions: {
radialBar: {
dataLabels: {
style: {
colors: "#9CA3AF",
},
name: {
offsetY: 30,
fontSize: "18px",
},
value: {
offsetY: -15,
fontSize: "30px",
},
total: {
show: true,
label: "Total",
formatter: function (w) {
// By default this function returns the average of all series. The below is just an example to show the use of custom formatter function
return 249;
},
},
},
},
},
labels: ["Product A", "Product B", "Product C", "Product D"],
stroke: {
lineCap: "round",
},
}));
// Transaction Graph
const transactionData = ref([
// Quick actions
const quickActions = [
{
name: "Bill A",
data: [...Array(12).keys()].map((n) => Math.round(Math.random() * 100)),
title: 'Create New Process',
description: 'Start building a new business process',
icon: 'material-symbols:add-circle',
color: 'bg-emerald-500',
action: () => router.push('/process-builder')
},
{
name: "Bill B",
data: [...Array(12).keys()].map((n) => Math.round(Math.random() * 100)),
title: 'Design Form',
description: 'Build interactive forms',
icon: 'material-symbols:edit-document',
color: 'bg-indigo-500',
action: () => router.push('/form-builder')
},
]);
{
title: 'View Inbox',
description: 'Check pending tasks',
icon: 'material-symbols:inbox',
color: 'bg-rose-500',
action: () => router.push('/execution/inbox')
},
{
title: 'Manage Processes',
description: 'View all processes',
icon: 'material-symbols:folder-managed',
color: 'bg-amber-500',
action: () => router.push('/process-builder/manage')
}
]
const chartOptionsTransaction = computed(() => ({
chart: {
height: 350,
type: "area",
toolbar: {
show: false,
},
// Development roadmap
const developmentRoadmap = [
{
phase: 'Phase 1 - Foundation',
status: 'completed',
items: [
'Process Builder with Visual Designer',
'Form Builder with Dynamic Components',
'Basic Process Execution Engine',
'User Authentication & Authorization'
]
},
dataLabels: {
enabled: false,
{
phase: 'Phase 2 - Integration',
status: 'in-progress',
items: [
'API Node Integration',
'Email & SMS Notifications',
'Business Rules Engine',
'Process Templates & Versioning'
]
},
stroke: {
curve: "smooth",
},
colors: ["#6366F1", "#F97316"],
yaxis: {
labels: {
style: {
colors: "#9CA3AF",
fontSize: "12px",
},
},
},
xaxis: {
type: "datetime",
categories: [
"2022-01-01",
"2022-02-01",
"2022-03-01",
"2022-04-01",
"2022-05-01",
"2022-06-01",
"2022-07-01",
"2022-08-01",
"2022-09-01",
"2022-10-01",
"2022-11-01",
"2022-12-01",
],
labels: {
style: {
colors: "#9CA3AF",
fontSize: "14px",
fontWeight: 400,
},
datetimeFormatter: {
month: "MMM",
},
},
},
legend: {
position: "top",
horizontalAlign: "left",
labels: {
colors: "#9CA3AF",
useSeriesColors: false,
},
},
tooltip: {
x: {
format: "MMMM",
},
},
}));
{
phase: 'Phase 3 - Advanced Features',
status: 'planned',
items: [
'Advanced Analytics & Reporting',
'Process Mining & Optimization',
'Mobile App Support',
'Enterprise Connectors'
]
}
]
// Load dashboard data
const loadDashboardData = async () => {
try {
// Load recent processes
await processStore.fetchProcesses({ limit: 5, sortBy: 'processModifiedDate', sortOrder: 'desc' })
recentProcesses.value = processStore.processes.slice(0, 5)
// Update stats
stats.value = {
activeProcesses: processStore.processes.filter(p => p.status === 'published').length,
totalForms: processStore.processes.length,
pendingTasks: Math.floor(Math.random() * 25) + 5, // Mock data
completedInstances: Math.floor(Math.random() * 150) + 50 // Mock data
}
// Mock recent tasks
recentTasks.value = [
{ id: 1, title: 'Review Profile Registration', process: 'Pendaftaran Profil', assignee: 'You', dueDate: '2024-01-15' },
{ id: 2, title: 'Approve Leave Request', process: 'Leave Management', assignee: 'HR Team', dueDate: '2024-01-16' },
{ id: 3, title: 'Verify Documents', process: 'Document Verification', assignee: 'Admin', dueDate: '2024-01-17' }
]
} catch (error) {
console.error('Error loading dashboard data:', error)
}
}
// Navigation helpers
const navigateToFeature = (route) => {
router.push(route)
}
const getStatusColor = (status) => {
switch (status) {
case 'completed': return 'text-green-600 bg-green-100'
case 'in-progress': return 'text-blue-600 bg-blue-100'
case 'planned': return 'text-gray-600 bg-gray-100'
default: return 'text-gray-600 bg-gray-100'
}
}
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
})
}
onMounted(() => {
setTimeout(() => {
changeKey.value++;
}, 500);
});
loadDashboardData()
})
</script>
<template>
<div>
<div class="space-y-6">
<LayoutsBreadcrumb />
<!-- First Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-x-6">
<!-- Summary Card #1 -->
<rs-card>
<div class="summary-1 pt-5 pb-3 px-5 flex items-center gap-4">
<div
class="p-5 flex justify-center items-center bg-primary/20 rounded-2xl"
>
<Icon class="text-primary" name="ic:outline-attach-money"></Icon>
<!-- Welcome Header -->
<div class="bg-gradient-to-r from-blue-600 to-purple-600 rounded-xl p-8 text-white">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold mb-2">Welcome to Corrad BPM</h1>
<p class="text-blue-100 text-lg">Business Process Management & Workflow Automation Platform</p>
</div>
<div class="hidden md:block">
<Icon name="material-symbols:float-landscape" class="w-20 h-20 text-blue-200" />
</div>
</div>
</div>
<!-- System Statistics -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
<rs-card class="hover:shadow-lg transition-shadow">
<div class="p-6 flex items-center">
<div class="p-4 bg-blue-100 rounded-xl mr-4">
<Icon name="material-symbols:account-tree" class="w-8 h-8 text-blue-600" />
</div>
<div class="flex-1 truncate">
<span class="block font-semibold text-xl leading-tight">
RM 100,000</span
>
<span class="text-base font-semibold text-gray-500"
>Total Revenues</span
>
<div>
<div class="text-2xl font-bold text-gray-900">{{ stats.activeProcesses }}</div>
<div class="text-sm text-gray-500">Active Processes</div>
</div>
</div>
<client-only>
<VueApexCharts
:key="changeKey"
width="100%"
height="53"
:options="{
...chartOptions,
colors: ['#F43F5E'],
yaxis: {
min: 0,
max: Math.max(...data1[0].data) + 10,
},
}"
:series="data1"
></VueApexCharts>
</client-only>
</rs-card>
<!-- Summary Card #2 -->
<rs-card>
<div class="summary-2 pt-5 pb-3 px-5 flex items-center gap-4">
<div
class="p-5 flex justify-center items-center bg-indigo-100 rounded-2xl"
>
<Icon
class="text-indigo-500"
name="ic:outline-account-circle"
></Icon>
<rs-card class="hover:shadow-lg transition-shadow">
<div class="p-6 flex items-center">
<div class="p-4 bg-purple-100 rounded-xl mr-4">
<Icon name="material-symbols:description" class="w-8 h-8 text-purple-600" />
</div>
<div class="flex-1 truncate">
<span class="block font-semibold text-xl leading-tight"> 512</span>
<span class="text-base font-semibold text-gray-500"
>Total Users</span
>
<div>
<div class="text-2xl font-bold text-gray-900">{{ stats.totalForms }}</div>
<div class="text-sm text-gray-500">Total Forms</div>
</div>
</div>
<client-only>
<VueApexCharts
:key="changeKey"
width="100%"
height="53"
:options="{
...chartOptions,
colors: ['#6366F1'],
yaxis: {
min: 0,
max: Math.max(...data2[0].data) + 10,
},
}"
:series="data2"
></VueApexCharts>
</client-only>
</rs-card>
<!-- Summary Card #3 -->
<rs-card>
<div class="summary-3 pt-5 pb-3 px-5 flex items-center gap-4">
<div
class="p-5 flex justify-center items-center bg-orange-100 rounded-2xl"
>
<Icon class="text-orange-500" name="ic:outline-shopping-bag"></Icon>
<rs-card class="hover:shadow-lg transition-shadow">
<div class="p-6 flex items-center">
<div class="p-4 bg-orange-100 rounded-xl mr-4">
<Icon name="material-symbols:pending-actions" class="w-8 h-8 text-orange-600" />
</div>
<div class="flex-1 truncate">
<span class="block font-semibold text-xl leading-tight"> 20</span>
<span class="text-base font-semibold text-gray-500"
>Total Products</span
>
<div>
<div class="text-2xl font-bold text-gray-900">{{ stats.pendingTasks }}</div>
<div class="text-sm text-gray-500">Pending Tasks</div>
</div>
</div>
<client-only>
<VueApexCharts
:key="changeKey"
width="100%"
height="53"
:options="{
...chartOptions,
colors: ['#F97316'],
yaxis: {
min: 0,
max: Math.max(...data3[0].data) + 10,
},
}"
:series="data3"
></VueApexCharts>
</client-only>
</rs-card>
<!-- Summary Card #4 -->
<rs-card>
<div class="summary-4 pt-5 pb-3 px-5 flex items-center gap-4">
<div
class="p-5 flex justify-center items-center bg-blue-100 rounded-2xl"
>
<Icon class="text-blue-500" name="ic:outline-remove-red-eye"></Icon>
<rs-card class="hover:shadow-lg transition-shadow">
<div class="p-6 flex items-center">
<div class="p-4 bg-green-100 rounded-xl mr-4">
<Icon name="material-symbols:task-alt" class="w-8 h-8 text-green-600" />
</div>
<div class="flex-1 truncate">
<span class="block font-semibold text-xl leading-tight">
2,452</span
>
<span class="text-base font-semibold text-gray-500"
>Total Viewers</span
>
<div>
<div class="text-2xl font-bold text-gray-900">{{ stats.completedInstances }}</div>
<div class="text-sm text-gray-500">Completed Instances</div>
</div>
</div>
<client-only>
<VueApexCharts
:key="changeKey"
width="100%"
height="53"
:options="{
...chartOptions,
colors: ['#3B82F6'],
yaxis: {
min: 0,
max: Math.max(...data4[0].data) + 10,
},
}"
:series="data4"
></VueApexCharts>
</client-only>
</rs-card>
</div>
<div class="flex flex-col md:flex-row gap-x-6">
<div class="w-12/2 md:w-8/12 flex flex-col">
<!-- Graph -->
<rs-card class="flex-1">
<template #header> Transaction </template>
<!-- Quick Actions -->
<rs-card>
<template #header>
<div class="flex items-center">
<Icon name="material-symbols:flash-on" class="w-5 h-5 mr-2 text-yellow-500" />
Quick Actions
</div>
</template>
<template #body>
<client-only>
<VueApexCharts
:key="changeKey"
width="100%"
height="300"
name="area"
:options="chartOptionsTransaction"
:series="transactionData"
></VueApexCharts
></client-only>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
<div
v-for="action in quickActions"
:key="action.title"
@click="action.action"
class="p-4 border border-gray-200 rounded-lg hover:border-gray-300 hover:shadow-md transition-all cursor-pointer group"
>
<div class="flex items-center mb-3">
<div :class="[action.color, 'p-2 rounded-lg mr-3 group-hover:scale-110 transition-transform']">
<Icon :name="action.icon" class="w-5 h-5 text-white" />
</div>
<h3 class="font-semibold text-gray-900 group-hover:text-blue-600 transition-colors">{{ action.title }}</h3>
</div>
<p class="text-sm text-gray-600">{{ action.description }}</p>
</div>
</div>
</template>
</rs-card>
<rs-card class="flex-1">
<template #header> Referral</template>
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
<!-- System Features -->
<rs-card class="xl:col-span-1">
<template #header>
<div class="flex items-center">
<Icon name="material-symbols:featured-play-list" class="w-5 h-5 mr-2 text-blue-500" />
System Features
</div>
</template>
<template #body>
<div
v-for="(val, index) in customers"
:key="index"
class="flex justify-between items-center rounded-lg bg-[rgb(var(--bg-1))] p-5 first:mt-0 mt-3"
<div class="space-y-6">
<div
v-for="feature in systemFeatures"
:key="feature.id"
class="border border-gray-200 rounded-lg p-4 hover:border-gray-300 hover:shadow-sm transition-all cursor-pointer group"
@click="navigateToFeature(feature.route)"
>
<div class="flex items-center gap-x-4">
<img
src="@/assets/img/avatar/user.webp"
class="h-10 w-10 rounded-lg"
/>
<div class="flex items-start">
<div :class="[feature.colorLight, 'p-3 rounded-lg mr-4 group-hover:scale-105 transition-transform']">
<Icon :name="feature.icon" :class="[feature.textColor, 'w-6 h-6']" />
</div>
<div class="flex-1">
<div class="flex flex-col">
<h3 class="font-semibold text-gray-900 mb-1 group-hover:text-blue-600 transition-colors">{{ feature.title }}</h3>
<p class="text-sm text-gray-600 mb-3">{{ feature.description }}</p>
<div class="flex flex-wrap gap-2">
<span
class="text-gray-900 dark:text-white font-semibold text-lg"
v-for="item in feature.features.slice(0, 2)"
:key="item"
class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full"
>
{{ val.name }}
{{ item }}
</span>
<span class="text-gray-600 dark:text-gray-50 text-sm">
RM{{ parseFloat(val.totalPurchase).toFixed(2) }} |
{{ val.purchase }} sold
<span
v-if="feature.features.length > 2"
class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full"
>
+{{ feature.features.length - 2 }} more
</span>
</div>
</div>
</div>
<div>
<button
class="flex items-center p-4 rounded-full bg-[rgb(var(--bg-2))] hover:bg-[rgb(var(--bg-2))]/10 shadow-md"
>
<Icon size="20px" name="ic:baseline-mail-outline"></Icon>
</button>
<Icon name="material-symbols:arrow-forward" class="w-5 h-5 text-gray-400 group-hover:text-blue-500 transition-colors" />
</div>
</div>
</template>
</rs-card>
</div>
<div class="w-12/2 md:w-4/12 flex flex-col">
<!-- Monthly Target Radial -->
<rs-card class="flex-1">
<template #header> Monthly Target </template>
<template #body>
<client-only>
<VueApexCharts
:key="changeKey"
width="100%"
height="300"
name="radialBar"
:options="chartOptionsRadial"
:series="radialData"
></VueApexCharts>
</client-only>
<hr class="my-4" />
<p class="text-xl py-5 font-medium">Products</p>
<div
class="flex item-center gap-x-4"
:class="{
'mt-0': index === 0,
'mt-3': index !== 0,
}"
v-for="(val, index) in ['A', 'B', 'C', 'D', 'E']"
:key="index"
>
<img
src="@/assets/img/default-thumbnail.jpg"
class="h-20 w-20 object-cover rounded-lg"
/>
<div class="flex-1 flex items-center">
</div>
</template>
</rs-card>
<!-- Recent Activity -->
<rs-card class="xl:col-span-1">
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon name="material-symbols:history" class="w-5 h-5 mr-2 text-gray-500" />
Recent Activity
</div>
<RsButton variant="secondary" size="sm" @click="router.push('/execution/history')">
View All
</RsButton>
</div>
</template>
<template #body>
<div class="space-y-4">
<!-- Recent Processes -->
<div>
<h4 class="font-medium text-gray-900 mb-3">Recent Processes</h4>
<div class="space-y-3">
<div
v-for="process in recentProcesses.slice(0, 3)"
:key="process.id"
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
@click="router.push(`/process-builder?id=${process.id}`)"
>
<div class="flex items-center">
<div class="w-2 h-2 bg-blue-500 rounded-full mr-3"></div>
<div>
<div class="font-medium text-sm text-gray-900">{{ process.name }}</div>
<div class="text-xs text-gray-500">{{ formatDate(process.updatedAt) }}</div>
</div>
</div>
<RsBadge :variant="process.status === 'published' ? 'success' : 'warning'" size="sm">
{{ process.status }}
</RsBadge>
</div>
</div>
</div>
<!-- Recent Tasks -->
<div>
<h4 class="font-medium text-gray-900 mb-3">Pending Tasks</h4>
<div class="space-y-3">
<div
v-for="task in recentTasks"
:key="task.id"
class="flex items-center justify-between p-3 bg-amber-50 rounded-lg hover:bg-amber-100 transition-colors cursor-pointer"
@click="router.push('/execution/inbox')"
>
<div class="flex items-center">
<div class="w-2 h-2 bg-amber-500 rounded-full mr-3"></div>
<div>
<span class="font-semibold text-lg leading-tight"
>Product {{ val }}</span
>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</p>
<div class="font-medium text-sm text-gray-900">{{ task.title }}</div>
<div class="text-xs text-gray-500">{{ task.process }} Due {{ task.dueDate }}</div>
</div>
</div>
<Icon name="material-symbols:keyboard-arrow-right" class="w-5 h-5 text-gray-400" />
</div>
</div>
</div>
</div>
</template>
</rs-card>
</div>
<!-- Development Roadmap -->
<rs-card>
<template #header>
<div class="flex items-center">
<Icon name="material-symbols:road" class="w-5 h-5 mr-2 text-purple-500" />
Development Roadmap & System Guide
</div>
</template>
<template #body>
<div class="space-y-6">
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 class="font-semibold text-blue-900 mb-2">🚀 Getting Started with Corrad BPM</h3>
<p class="text-blue-800 text-sm mb-3">
Follow these steps to start developing business processes in the system:
</p>
<ol class="list-decimal list-inside space-y-1 text-sm text-blue-800">
<li><strong>Design Forms:</strong> Use Form Builder to create data collection forms</li>
<li><strong>Create Processes:</strong> Use Process Builder to design workflow logic</li>
<li><strong>Configure Integrations:</strong> Set up API calls and business rules</li>
<li><strong>Test & Deploy:</strong> Test process execution and publish to production</li>
</ol>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div
v-for="phase in developmentRoadmap"
:key="phase.phase"
class="border border-gray-200 rounded-lg p-4"
>
<div class="flex items-center justify-between mb-3">
<h4 class="font-semibold text-gray-900">{{ phase.phase }}</h4>
<RsBadge :class="getStatusColor(phase.status)" size="sm">
{{ phase.status.replace('-', ' ') }}
</RsBadge>
</div>
<ul class="space-y-2">
<li
v-for="item in phase.items"
:key="item"
class="flex items-start text-sm text-gray-600"
>
<Icon
:name="phase.status === 'completed' ? 'material-symbols:check-circle' :
phase.status === 'in-progress' ? 'material-symbols:radio-button-partial' :
'material-symbols:radio-button-unchecked'"
:class="phase.status === 'completed' ? 'text-green-500' :
phase.status === 'in-progress' ? 'text-blue-500' :
'text-gray-400'"
class="w-4 h-4 mr-2 mt-0.5 flex-shrink-0"
/>
{{ item }}
</li>
</ul>
</div>
</div>
</div>
</template>
</rs-card>
</div>
</template>
<style scoped>
/* Custom hover effects and transitions */
.group:hover .group-hover\:scale-105 {
transform: scale(1.05);
}
.group:hover .group-hover\:scale-110 {
transform: scale(1.1);
}
</style>

View File

@ -1,6 +1,6 @@
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useVariableStore } from '~/stores/variableStore';
import { useProcessBuilderStore } from '~/stores/processBuilder';
import NotificationManager from '~/components/process-flow/notification/NotificationManager.vue';
// Define page meta
@ -13,28 +13,21 @@ definePageMeta({
});
// Initialize the store
const variableStore = useVariableStore();
const processStore = useProcessBuilderStore();
// Get available variables for usage in notifications
const availableVariables = computed(() => {
const processVars = variableStore.getAllVariables.process.map(v => ({
name: v.name || 'unnamed',
label: v?.description
? `${v.description} (${v.name || 'unnamed'}, process)`
: `${v.name || 'unnamed'} (process)` ,
type: v.type || 'string',
scope: 'process'
const processVariables = processStore.getProcessVariables();
if (!processVariables || typeof processVariables !== 'object') {
return [];
}
return Object.entries(processVariables).map(([name, variable]) => ({
name: name,
label: `${name} (${variable.type || 'string'})`,
type: variable.type || 'string',
scope: variable.scope || 'process'
}));
const globalVars = variableStore.getAllVariables.global.map(v => ({
name: v.name || 'unnamed',
label: v?.description
? `${v.description} (${v.name || 'unnamed'}, global)`
: `${v.name || 'unnamed'} (global)` ,
type: v.type || 'string',
scope: 'global'
}));
return [...processVars, ...globalVars];
});
// Current user ID

View File

@ -1,7 +1,6 @@
<script setup>
import { ref, onMounted, computed, shallowRef, onUnmounted, nextTick, watch } from 'vue';
import { useProcessBuilderStore } from '~/stores/processBuilder';
import { useVariableStore } from '~/stores/variableStore';
import { useRouter, useRoute } from 'vue-router';
import ProcessFlowCanvas from '~/components/process-flow/ProcessFlowCanvas.vue';
import ProcessBuilderComponents from '~/components/process-flow/ProcessBuilderComponents.vue';
@ -18,6 +17,7 @@ import FormNodeConfigurationModal from '~/components/process-flow/FormNodeConfig
import BusinessRuleNodeConfiguration from '~/components/process-flow/BusinessRuleNodeConfiguration.vue';
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 ProcessTemplatesModal from '~/components/ProcessTemplatesModal.vue';
import ProcessSettingsModal from '~/components/process-flow/ProcessSettingsModal.vue';
import ProcessHistoryModal from '~/components/ProcessHistoryModal.vue';
@ -35,7 +35,6 @@ definePageMeta({
const processStore = useProcessBuilderStore();
const router = useRouter();
const route = useRoute();
const variableStore = useVariableStore();
// Initialize toast with fallback
let toast;
@ -86,6 +85,7 @@ const showApiConfigModal = ref(false);
const showGatewayConfigModal = ref(false);
const showBusinessRuleConfigModal = ref(false);
const showNotificationConfigModal = ref(false);
const showScriptConfigModal = ref(false);
const showTemplatesModal = ref(false);
const showProcessSettings = ref(false);
const showDropdown = ref(false);
@ -239,23 +239,21 @@ const nodeDefaultPath = computed({
// Computed for gateway available variables
const gatewayAvailableVariables = computed(() => {
const processVars = variableStore.getAllVariables.process.map(v => ({
if (!processStore.currentProcess || !processStore.currentProcess.variables) {
return [];
}
// Get variables from the current process instead of variable store
const processVariables = Object.values(processStore.currentProcess.variables);
const allVars = processVariables.map(v => ({
name: v.name || 'unnamed',
label: v?.description
? `${v.description} (${v.name || 'unnamed'}, process)`
: `${v.name || 'unnamed'} (process)` ,
label: `${v.name || 'unnamed'} (${v.type || 'string'})`,
type: v.type || 'string',
scope: 'process'
scope: v.scope || 'global',
description: v.description || ''
}));
const globalVars = variableStore.getAllVariables.global.map(v => ({
name: v.name || 'unnamed',
label: v?.description
? `${v.description} (${v.name || 'unnamed'}, global)`
: `${v.name || 'unnamed'} (global)` ,
type: v.type || 'string',
scope: 'global'
}));
const allVars = [...processVars, ...globalVars];
// console.log('Gateway available variables:', allVars);
return allVars;
});
@ -897,12 +895,12 @@ const applyProcessTemplate = async (template) => {
if (template.variables && template.variables.length > 0) {
console.log('Adding template variables:', template.variables.length);
// Clear existing process variables first
variableStore.clearAllProcessVariables();
// Clear existing process variables first (they'll be loaded from the process)
// Process variables are now managed directly in the process store
template.variables.forEach((variable) => {
console.log(`Adding variable: ${variable.name} (${variable.type}) with scope: ${variable.scope}`);
variableStore.addVariable({
processStore.addProcessVariable({
...variable,
id: crypto.randomUUID() // Generate unique ID for the variable
});
@ -1014,6 +1012,41 @@ const handleNotificationNodeUpdate = (updatedData) => {
}
};
// Handle script node update
const handleScriptNodeUpdate = (updatedData) => {
if (selectedNodeData.value && selectedNodeData.value.type === 'script') {
// Make sure to update the label both in data and at the root level
const newLabel = updatedData.label || 'Script Task';
// 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
updateNodeInStore();
}
};
// Handle process restoration from history
const handleProcessRestored = (restoredProcess) => {
// The process has been restored in the backend, so we need to reload it
@ -1235,6 +1268,13 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
Configure Notification
</RsButton>
</div>
<!-- Script Configuration -->
<div v-if="selectedNodeData.type === 'script'">
<RsButton @click="showScriptConfigModal = true" variant="primary" class="w-full">
Configure Script Task
</RsButton>
</div>
</div>
</div>
</div>
@ -1308,7 +1348,7 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
v-if="selectedNodeData && selectedNodeData.type === 'form'"
v-model="showFormConfigModal"
:nodeData="selectedNodeData.data"
:availableVariables="variableStore.getAllVariables.global"
:availableVariables="gatewayAvailableVariables"
@update="handleFormNodeUpdate"
/>
@ -1351,6 +1391,15 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
@update="handleNotificationNodeUpdate"
/>
<!-- Script Configuration Modal -->
<ScriptNodeConfigurationModal
v-if="selectedNodeData && selectedNodeData.type === 'script'"
v-model="showScriptConfigModal"
:nodeData="selectedNodeData.data"
:availableVariables="gatewayAvailableVariables"
@update="handleScriptNodeUpdate"
/>
<!-- Process Templates Modal -->
<ProcessTemplatesModal
v-model="showTemplatesModal"

View File

@ -247,15 +247,11 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
updatedAt: apiProcess.processModifiedDate
};
this.currentProcess = process;
// Load variables into variable store
if (process.variables) {
const variableStore = useVariableStore();
Object.entries(process.variables).forEach(([name, variable]) => {
variableStore.addVariable(variable);
});
}
// Variables are already loaded in process.variables, no need to sync with variable store
this.unsavedChanges = false;
return { success: true, process };
@ -308,13 +304,16 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
if (!this.currentProcess) return false;
try {
// Get variables from the current process, not from variable store
const processVariables = this.currentProcess.variables || {};
const processData = {
processName: this.currentProcess.name,
processDescription: this.currentProcess.description,
nodes: this.currentProcess.nodes,
edges: this.currentProcess.edges,
viewport: this.currentProcess.viewport || { x: 0, y: 0, zoom: 1 },
variables: useVariableStore().getAllVariables.process || {},
variables: processVariables,
settings: this.currentProcess.settings || {},
permissions: this.currentProcess.permissions || {}
// Note: processStatus is intentionally NOT included here to preserve current status
@ -326,6 +325,7 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
processId: this.currentProcess.id,
nodeCount: processData.nodes.length,
edgeCount: processData.edges.length,
variableCount: Object.keys(processVariables).length,
nodes: processData.nodes.map(n => ({ id: n.id, type: n.type, label: n.label })),
edges: processData.edges.map(e => ({ id: e.id, source: e.source, target: e.target }))
});
@ -808,14 +808,7 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
*/
clearHistory() {
this.history = [];
if (this.currentProcess) {
this.history.push({
state: JSON.parse(JSON.stringify(this.currentProcess)),
action: 'Initial state',
timestamp: new Date().toISOString()
});
}
this.historyIndex = 0;
this.historyIndex = -1;
},
/**
@ -924,7 +917,7 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
nodes: this.currentProcess.nodes,
edges: this.currentProcess.edges,
viewport: this.currentProcess.viewport || { x: 0, y: 0, zoom: 1 },
variables: useVariableStore().getAllVariables.process || {},
variables: this.currentProcess.variables || {},
settings: this.currentProcess.settings || {},
permissions: this.currentProcess.permissions || {},
changeDescription: changeDescription || this.lastChangeDescription,
@ -958,6 +951,78 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
console.error('Error saving process:', error);
throw error;
}
},
// Variable management methods
/**
* Add or update a variable in the current process
*/
addProcessVariable(variable) {
if (!this.currentProcess) return;
if (!this.currentProcess.variables) {
this.currentProcess.variables = {};
}
this.currentProcess.variables[variable.name] = {
name: variable.name,
type: variable.type || 'string',
scope: variable.scope || 'global',
value: variable.value,
description: variable.description || ''
};
this.unsavedChanges = true;
},
/**
* Update an existing variable in the current process
*/
updateProcessVariable(name, updates) {
if (!this.currentProcess || !this.currentProcess.variables || !this.currentProcess.variables[name]) {
return;
}
this.currentProcess.variables[name] = {
...this.currentProcess.variables[name],
...updates
};
this.unsavedChanges = true;
},
/**
* Delete a variable from the current process
*/
deleteProcessVariable(name) {
if (!this.currentProcess || !this.currentProcess.variables) {
return;
}
delete this.currentProcess.variables[name];
this.unsavedChanges = true;
},
/**
* Get all variables from the current process
*/
getProcessVariables() {
if (!this.currentProcess || !this.currentProcess.variables) {
return [];
}
return Object.values(this.currentProcess.variables);
},
/**
* Get a specific variable from the current process
*/
getProcessVariable(name) {
if (!this.currentProcess || !this.currentProcess.variables) {
return null;
}
return this.currentProcess.variables[name] || null;
}
}
});