Enhance Process Builder with New Features and Configuration Options

- Added a new ProcessTemplatesModal component for selecting process flow templates, improving user experience in template management.
- Introduced a ProcessSettingsModal component for comprehensive process configuration, including process info, execution settings, and permissions management.
- Updated BusinessRuleNodeConfiguration and FormNodeConfiguration components to enhance user interaction and streamline configuration processes.
- Implemented new API endpoints for managing form fields and settings, allowing for better integration and data handling.
- Enhanced existing components with improved styling and functionality, including dynamic field conditions and bidirectional data mapping.
- Updated nuxt.config.js to include security settings for API routes, ensuring better protection against XSS and request size limitations.
- Removed the deprecated TaskNodeConfiguration component to streamline the process builder interface.
- Improved documentation to reflect recent changes and enhancements in the process builder features.
This commit is contained in:
Afiq 2025-05-27 17:00:24 +08:00
parent 72c61184ae
commit fa90c234c5
23 changed files with 5009 additions and 872 deletions

File diff suppressed because it is too large Load Diff

View File

@ -172,7 +172,7 @@
<select
v-model="condition.variable"
class="form-select"
@change="saveChanges"
@change="updateConditionVariable(groupIndex, condIndex)"
>
<option value="" disabled>Select variable</option>
<option
@ -183,6 +183,21 @@
{{ variable.label }}
</option>
</select>
<div v-if="condition.variable" class="mt-1">
<span
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"
:class="{
'bg-purple-100 text-purple-800': ['int', 'decimal'].includes(availableVariables.find(v => v.name === condition.variable)?.type),
'bg-blue-100 text-blue-800': availableVariables.find(v => v.name === condition.variable)?.type === 'string',
'bg-indigo-100 text-indigo-800': availableVariables.find(v => v.name === condition.variable)?.type === 'boolean',
'bg-amber-100 text-amber-800': ['date', 'datetime'].includes(availableVariables.find(v => v.name === condition.variable)?.type),
'bg-emerald-100 text-emerald-800': availableVariables.find(v => v.name === condition.variable)?.type === 'object',
'bg-gray-100 text-gray-800': !['int', 'decimal', 'string', 'boolean', 'date', 'datetime', 'object'].includes(availableVariables.find(v => v.name === condition.variable)?.type)
}"
>
{{ availableVariables.find(v => v.name === condition.variable)?.type }} type
</span>
</div>
</td>
<td class="px-3 py-2">
<select
@ -206,7 +221,80 @@
<template v-if="getInputType(
availableVariables.find(v => v.name === condition.variable)?.type,
condition.operator
) !== 'none'">
) === 'none'">
<div class="flex items-center text-gray-400 text-sm italic">
<Icon name="material-symbols:info" class="w-4 h-4 mr-1" />
No value required
</div>
</template>
<!-- Range inputs for between operators -->
<template v-else-if="getInputType(
availableVariables.find(v => v.name === condition.variable)?.type,
condition.operator
) === 'range'">
<div class="space-y-1">
<div class="grid grid-cols-2 gap-1">
<input
v-model="condition.minValue"
:type="availableVariables.find(v => v.name === condition.variable)?.type === 'date' ? 'date' :
availableVariables.find(v => v.name === condition.variable)?.type === 'datetime' ? 'datetime-local' : 'number'"
class="form-control text-xs"
placeholder="Min"
@blur="saveChanges"
/>
<input
v-model="condition.maxValue"
:type="availableVariables.find(v => v.name === condition.variable)?.type === 'date' ? 'date' :
availableVariables.find(v => v.name === condition.variable)?.type === 'datetime' ? 'datetime-local' : 'number'"
class="form-control text-xs"
placeholder="Max"
@blur="saveChanges"
/>
</div>
<p class="text-xs text-gray-500">Range values</p>
</div>
</template>
<!-- Weekday selector -->
<template v-else-if="getInputType(
availableVariables.find(v => v.name === condition.variable)?.type,
condition.operator
) === 'weekday'">
<select v-model="condition.value" class="form-select" @change="saveChanges">
<option value="1">Monday</option>
<option value="2">Tuesday</option>
<option value="3">Wednesday</option>
<option value="4">Thursday</option>
<option value="5">Friday</option>
<option value="6">Saturday</option>
<option value="7">Sunday</option>
</select>
</template>
<!-- Month selector -->
<template v-else-if="getInputType(
availableVariables.find(v => v.name === condition.variable)?.type,
condition.operator
) === 'month'">
<select v-model="condition.value" class="form-select" @change="saveChanges">
<option value="1">January</option>
<option value="2">February</option>
<option value="3">March</option>
<option value="4">April</option>
<option value="5">May</option>
<option value="6">June</option>
<option value="7">July</option>
<option value="8">August</option>
<option value="9">September</option>
<option value="10">October</option>
<option value="11">November</option>
<option value="12">December</option>
</select>
</template>
<!-- Regular input -->
<template v-else>
<input
v-model="condition.value"
:type="getInputType(
@ -214,11 +302,11 @@
condition.operator
)"
class="form-control"
:placeholder="condition.operator?.includes('n_days') ? 'Number of days' : 'Value'"
:placeholder="getValuePlaceholder(condition)"
:step="availableVariables.find(v => v.name === condition.variable)?.type === 'decimal' ? '0.01' : undefined"
@blur="saveChanges"
/>
</template>
<span v-else class="text-gray-400 text-sm italic">N/A</span>
</td>
<td class="px-3 py-2 text-center">
<button
@ -588,16 +676,23 @@ const updateActionType = (groupIndex, actionIndex) => {
// Get operators based on variable type
const getOperatorsForType = (type) => {
switch (type?.toLowerCase()) {
case 'number':
case 'int':
case 'decimal':
case 'number':
return [
{ value: 'eq', label: '= (Equal to)' },
{ value: 'neq', label: '≠ (Not equal to)' },
{ value: 'gt', label: '> (Greater than)' },
{ value: 'gte', label: '≥ (Greater than or equal)' },
{ value: 'gte', label: '≥ (Greater than or equal to)' },
{ value: 'lt', label: '< (Less than)' },
{ value: 'lte', label: '≤ (Less than or equal)' }
{ value: 'lte', label: '≤ (Less than or equal to)' },
{ value: 'between', label: 'Between (inclusive)' },
{ value: 'not_between', label: 'Not between' },
{ value: 'is_even', label: 'Is even number' },
{ value: 'is_odd', label: 'Is odd number' },
{ value: 'is_positive', label: 'Is positive' },
{ value: 'is_negative', label: 'Is negative' },
{ value: 'is_zero', label: 'Is zero' }
];
case 'string':
return [
@ -608,10 +703,20 @@ const getOperatorsForType = (type) => {
{ value: 'starts_with', label: 'Starts with' },
{ value: 'ends_with', label: 'Ends with' },
{ value: 'is_empty', label: 'Is empty' },
{ value: 'is_not_empty', label: 'Is not empty' }
{ value: 'is_not_empty', label: 'Is not empty' },
{ value: 'regex', label: 'Matches pattern (regex)' },
{ value: 'length_eq', label: 'Length equals' },
{ value: 'length_gt', label: 'Length greater than' },
{ value: 'length_lt', label: 'Length less than' }
];
case 'boolean':
return [
{ value: 'eq', label: '= (Equal to)' },
{ value: 'is_true', label: 'Is True' },
{ value: 'is_false', label: 'Is False' }
];
case 'datetime':
case 'date':
case 'datetime':
return [
{ value: 'eq', label: '= (Equal to)' },
{ value: 'neq', label: '≠ (Not equal to)' },
@ -619,25 +724,30 @@ const getOperatorsForType = (type) => {
{ value: 'gte', label: '≥ (On or after)' },
{ value: 'lt', label: '< (Before)' },
{ value: 'lte', label: '≤ (On or before)' },
{ value: 'between', label: 'Between dates' },
{ value: 'is_today', label: 'Is today' },
{ value: 'is_yesterday', label: 'Is yesterday' },
{ value: 'is_tomorrow', label: 'Is tomorrow' },
{ value: 'is_this_week', label: 'Is this week' },
{ value: 'is_this_month', label: 'Is this month' },
{ value: 'is_this_year', label: 'Is this year' },
{ value: 'is_future', label: 'Is in the future' },
{ value: 'is_past', label: 'Is in the past' },
{ value: 'last_n_days', label: 'In the last N days' },
{ value: 'next_n_days', label: 'In the next N days' }
];
case 'boolean':
return [
{ value: 'is_true', label: 'Is true' },
{ value: 'is_false', label: 'Is false' }
{ value: 'next_n_days', label: 'In the next N days' },
{ value: 'weekday_eq', label: 'Day of week equals' },
{ value: 'month_eq', label: 'Month equals' }
];
case 'object':
return [
{ value: 'eq', label: '= (Equal to)' },
{ value: 'neq', label: '≠ (Not equal to)' },
{ value: 'contains_key', label: 'Contains key' },
{ value: 'not_contains_key', label: 'Does not contain key' },
{ value: 'is_empty', label: 'Is empty' },
{ value: 'is_not_empty', label: 'Is not empty' }
{ value: 'has_property', label: 'Has property' },
{ value: 'property_equals', label: 'Property equals' },
{ value: 'property_contains', label: 'Property contains' },
{ value: 'is_empty', label: 'Is empty/null' },
{ value: 'is_not_empty', label: 'Is not empty/null' },
{ value: 'property_count', label: 'Property count equals' }
];
default:
return [
@ -654,17 +764,36 @@ const getInputType = (varType, operator) => {
// Special operators that don't need value input
const noValueOperators = [
'is_empty', 'is_not_empty', 'is_true', 'is_false',
'is_today', 'is_future', 'is_past'
'is_today', 'is_yesterday', 'is_tomorrow', 'is_this_week', 'is_this_month', 'is_this_year',
'is_future', 'is_past', 'is_even', 'is_odd', 'is_positive', 'is_negative', 'is_zero'
];
if (noValueOperators.includes(operator)) {
return 'none';
}
// Special operators that need specific input types
if (operator === 'between' || operator === 'not_between') {
return 'range';
}
// Operators that need number input regardless of base type
if (['last_n_days', 'next_n_days', 'length_eq', 'length_gt', 'length_lt', 'property_count'].includes(operator)) {
return 'number';
}
// Day/month selection
if (operator === 'weekday_eq') {
return 'weekday';
}
if (operator === 'month_eq') {
return 'month';
}
switch (varType?.toLowerCase()) {
case 'number':
case 'int':
case 'decimal':
case 'number':
return 'number';
case 'datetime':
return 'datetime-local';
@ -740,14 +869,107 @@ const parseValue = (value, type) => {
}
};
// Get placeholder text based on variable type and operator
const getValuePlaceholder = (condition) => {
const varType = availableVariables.value.find(v => v.name === condition.variable)?.type?.toLowerCase();
const operator = condition.operator;
// Handle operators that don't need values
if (getInputType(varType, operator) === 'none') {
return 'No value needed';
}
// Handle special operators
if (operator === 'between' || operator === 'not_between') {
if (varType === 'int' || varType === 'decimal' || varType === 'number') {
return 'Enter range: min,max (e.g., 10,50)';
}
if (varType === 'date' || varType === 'datetime') {
return 'Enter date range: start,end';
}
}
if (operator === 'last_n_days' || operator === 'next_n_days') {
return 'Enter number of days (e.g., 7)';
}
if (operator === 'length_eq' || operator === 'length_gt' || operator === 'length_lt') {
return 'Enter length value (e.g., 5)';
}
if (operator === 'property_count') {
return 'Enter expected count (e.g., 3)';
}
if (operator === 'has_property' || operator === 'property_equals' || operator === 'property_contains') {
return 'Enter property path (e.g., user.email)';
}
if (operator === 'regex') {
return 'Enter regex pattern (e.g., ^[A-Z]+$)';
}
// Type-specific placeholders
switch (varType) {
case 'int':
return 'Enter a whole number (e.g., 42)';
case 'decimal':
case 'number':
return 'Enter a number (e.g., 3.14)';
case 'date':
return 'Select a date';
case 'datetime':
return 'Select date and time';
case 'string':
return operator === 'contains' || operator === 'not_contains'
? 'Enter text to search for'
: 'Enter text value';
case 'boolean':
return 'true or false';
case 'object':
return 'Enter JSON object or property path';
default:
return 'Enter value';
}
};
// Add new methods for handling condition updates
const updateConditionOperator = (groupIndex, condIndex) => {
const condition = localNodeData.value.ruleGroups[groupIndex].conditions[condIndex];
const varType = availableVariables.value.find(v => v.name === condition.variable)?.type;
// Reset value if operator doesn't need it
// Reset values when operator changes
if (getInputType(varType, condition.operator) === 'none') {
condition.value = null;
condition.minValue = null;
condition.maxValue = null;
} else if (getInputType(varType, condition.operator) === 'range') {
condition.value = null;
condition.minValue = condition.minValue || '';
condition.maxValue = condition.maxValue || '';
} else {
condition.minValue = null;
condition.maxValue = null;
condition.value = condition.value || '';
}
saveChanges();
};
// Update condition variable
const updateConditionVariable = (groupIndex, condIndex) => {
const condition = localNodeData.value.ruleGroups[groupIndex].conditions[condIndex];
const selectedVar = availableVariables.value.find(v => v.name === condition.variable);
if (selectedVar) {
// Reset operator to a valid one for this type
const operators = getOperatorsForType(selectedVar.type);
condition.operator = operators.length > 0 ? operators[0].value : 'eq';
// Reset values
condition.value = '';
condition.minValue = '';
condition.maxValue = '';
}
saveChanges();

View File

@ -123,36 +123,30 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Process Variable</label>
<select
<FormKit
type="select"
v-model="mapping.processVariable"
class="form-select"
@change="saveChanges"
>
<option value="" disabled>Select a process variable</option>
<option
v-for="variable in availableVariables"
:key="variable.name"
:value="variable.name"
>
{{ variable.label }}
</option>
</select>
:options="processVariableOptions"
placeholder="Select a process variable"
:classes="{ outer: 'mb-0' }"
/>
<p class="mt-1 text-xs text-gray-500">
The source variable containing the data
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Form Field Name</label>
<input
<label class="block text-sm font-medium text-gray-700 mb-1">Form Field</label>
<FormKit
type="select"
v-model="mapping.formField"
type="text"
class="form-control"
placeholder="e.g., first_name"
@blur="saveChanges"
:options="formFieldOptions"
placeholder="Select a form field"
:disabled="formFields.length === 0"
:classes="{ outer: 'mb-0' }"
/>
<p class="mt-1 text-xs text-gray-500">
The target field ID in the form
{{ formFields.length === 0 ? 'No form fields available' : 'The target field in the form' }}
</p>
</div>
</div>
@ -217,41 +211,35 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Form Field Name</label>
<input
<label class="block text-sm font-medium text-gray-700 mb-1">Form Field</label>
<FormKit
type="select"
v-model="mapping.formField"
type="text"
class="form-control"
placeholder="e.g., first_name"
@blur="saveChanges"
:options="formFieldOptions"
placeholder="Select a form field"
:disabled="formFields.length === 0"
:classes="{ outer: 'mb-0' }"
/>
<p class="mt-1 text-xs text-gray-500">
The source field ID in the form
{{ formFields.length === 0 ? 'No form fields available' : 'The source field in the form' }}
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Process Variable</label>
<div class="flex items-center gap-2">
<select
<FormKit
type="select"
v-model="mapping.processVariable"
class="form-select"
@change="saveChanges"
>
<option value="" disabled>Select a variable</option>
<option :value="'create_new_' + getVariableNameFromFormField(mapping.formField)">
Create new variable
</option>
<option
v-for="variable in availableVariables"
:key="variable.name"
:value="variable.name"
>
{{ variable.label }}
</option>
</select>
:options="[
{ label: 'Create new variable', value: 'create_new_' + getVariableNameFromFormField(mapping.formField) },
...processVariableOptions
]"
placeholder="Select a variable"
:classes="{ outer: 'mb-0', input: 'flex-1' }"
/>
<RsButton
v-if="mapping.processVariable && mapping.processVariable.startsWith('create_new_')"
v-if="getStringValue(mapping.processVariable) && getStringValue(mapping.processVariable).startsWith('create_new_')"
@click="createVariableFromMapping(mapping)"
variant="primary"
size="sm"
@ -270,6 +258,178 @@
</div>
</div>
</div>
<!-- Step 3: Field Conditions (only if form is selected) -->
<div v-if="localNodeData.formId" class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200">
<div class="flex items-center mb-3">
<div class="w-6 h-6 rounded-full bg-emerald-100 flex items-center justify-center mr-2">
<span class="text-xs font-semibold text-emerald-600">3</span>
</div>
<h4 class="font-medium">Field Conditions</h4>
</div>
<p class="text-sm text-gray-600 mb-4">
Configure conditional field behavior based on process variables (e.g., make fields readonly, hidden, or required).
</p>
<div class="mb-5">
<div class="flex justify-between items-center mb-3">
<div>
<h5 class="text-sm font-medium flex items-center">
<span class="w-5 h-5 rounded-full bg-amber-100 text-amber-700 flex items-center justify-center mr-2 text-xs">
<Icon name="material-symbols:rule" />
</span>
Field Conditions
</h5>
<p class="text-xs text-gray-500 ml-7">Control field visibility, readonly state, and requirements dynamically</p>
</div>
<RsButton
@click="addFieldCondition()"
variant="secondary"
size="sm"
class="btn-sm-emerald"
>
<Icon name="material-symbols:add" class="mr-1" /> Add Condition
</RsButton>
</div>
<!-- No field conditions placeholder -->
<div v-if="!localNodeData.fieldConditions || localNodeData.fieldConditions.length === 0"
class="py-4 text-center text-gray-500 text-sm border border-dashed rounded-md bg-white">
<p class="mb-2">
No field conditions defined. All fields will use their default behavior.
</p>
<RsButton
@click="addFieldCondition()"
variant="secondary"
size="sm"
class="btn-sm-emerald"
>
<Icon name="material-symbols:add" class="mr-1" /> Add First Condition
</RsButton>
</div>
<div v-else class="space-y-3">
<div v-for="(condition, index) in localNodeData.fieldConditions" :key="'condition-' + index"
class="p-4 border rounded-md bg-amber-50">
<div class="flex justify-between mb-3">
<h5 class="text-sm font-medium flex items-center">
<Icon name="material-symbols:rule" class="text-amber-600 mr-1" />
Field Condition #{{ index + 1 }}
</h5>
<button
@click="removeFieldCondition(index)"
class="text-red-500 hover:text-red-700 p-1 rounded hover:bg-red-50"
title="Remove condition"
>
<Icon name="material-symbols:delete-outline" />
</button>
</div>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4">
<!-- Process Variable -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Process Variable</label>
<FormKit
type="select"
v-model="condition.processVariable"
:options="processVariableOptions"
placeholder="Select variable"
:classes="{ outer: 'mb-0' }"
/>
<p class="mt-1 text-xs text-gray-500">Variable to check</p>
</div>
<!-- Operator -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Operator</label>
<FormKit
type="select"
v-model="condition.operator"
:options="[
{ label: 'Equals', value: 'equals' },
{ label: 'Not Equals', value: 'not_equals' },
{ label: 'Is True', value: 'is_true' },
{ label: 'Is False', value: 'is_false' },
{ label: 'Is Empty', value: 'is_empty' },
{ label: 'Is Not Empty', value: 'is_not_empty' },
{ label: 'Contains', value: 'contains' },
{ label: 'Greater Than', value: 'greater_than' },
{ label: 'Less Than', value: 'less_than' }
]"
placeholder="Select operator"
:classes="{ outer: 'mb-0' }"
/>
<p class="mt-1 text-xs text-gray-500">Comparison type</p>
</div>
<!-- Value -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Value</label>
<FormKit
v-if="!['is_true', 'is_false', 'is_empty', 'is_not_empty'].includes(condition.operator)"
type="text"
v-model="condition.value"
placeholder="Comparison value"
:classes="{ outer: 'mb-0' }"
/>
<div v-else class="text-xs text-gray-400 italic mt-2">
No value needed for this operator
</div>
<p class="mt-1 text-xs text-gray-500">Value to compare</p>
</div>
<!-- Target Form Field -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Target Field</label>
<FormKit
type="select"
v-model="condition.targetField"
:options="formFieldOptions"
placeholder="Select field"
:disabled="formFields.length === 0"
:classes="{ outer: 'mb-0' }"
/>
<p class="mt-1 text-xs text-gray-500">Field to control</p>
</div>
</div>
<!-- Action Row -->
<div class="mt-4 grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Action</label>
<FormKit
type="select"
v-model="condition.action"
:options="[
{ label: 'Make Readonly', value: 'readonly' },
{ label: 'Hide Field', value: 'hide' },
{ label: 'Make Required', value: 'required' },
{ label: 'Make Optional', value: 'optional' },
{ label: 'Show Field', value: 'show' },
{ label: 'Enable Field', value: 'enable' }
]"
placeholder="Select action"
:classes="{ outer: 'mb-0' }"
/>
<p class="mt-1 text-xs text-gray-500">What to do when condition is met</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description (Optional)</label>
<FormKit
type="text"
v-model="condition.description"
placeholder="e.g., Hide email when user is resubmitting"
:classes="{ outer: 'mb-0' }"
/>
<p class="mt-1 text-xs text-gray-500">For documentation purposes</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
@ -295,6 +455,10 @@ const emit = defineEmits(['update']);
// Get the variable store for creating variables
const variableStore = useVariableStore();
// State for form fields
const formFields = ref([]);
const isLoadingFields = ref(false);
// Local state for node data - create a deep copy to avoid mutation issues
const localNodeData = ref({
label: 'Form Task',
@ -303,11 +467,12 @@ const localNodeData = ref({
formName: '',
formUuid: null,
inputMappings: [],
outputMappings: []
outputMappings: [],
fieldConditions: []
});
// Watch for changes from parent props
watch(() => props.nodeData, (newNodeData) => {
watch(() => props.nodeData, async (newNodeData) => {
if (newNodeData) {
// Create a deep copy to break reactivity chains with parent
localNodeData.value = {
@ -317,17 +482,25 @@ watch(() => props.nodeData, (newNodeData) => {
formName: newNodeData.formName || '',
formUuid: newNodeData.formUuid || null,
inputMappings: Array.isArray(newNodeData.inputMappings)
? [...newNodeData.inputMappings]
? newNodeData.inputMappings.map(mapping => ({ ...mapping }))
: [],
outputMappings: Array.isArray(newNodeData.outputMappings)
? [...newNodeData.outputMappings]
? newNodeData.outputMappings.map(mapping => ({ ...mapping }))
: [],
fieldConditions: Array.isArray(newNodeData.fieldConditions)
? newNodeData.fieldConditions.map(condition => ({ ...condition }))
: []
};
// Load form fields if form is already selected
if (newNodeData.formId || newNodeData.formUuid) {
await loadFormFields(newNodeData.formId || newNodeData.formUuid);
}
}
}, { immediate: true, deep: true });
// Function to handle form selection
function handleFormSelection(form) {
async function handleFormSelection(form) {
if (!form) return;
localNodeData.value = {
@ -336,9 +509,13 @@ function handleFormSelection(form) {
formName: form.formName,
formUuid: form.formUUID,
label: form.formName || 'Form Task',
description: `Form: ${form.formName}`
description: `Form: ${form.formName}`,
fieldConditions: []
};
// Load form fields for this form
await loadFormFields(form.formID || form.formUUID);
saveChanges();
}
@ -352,9 +529,13 @@ function clearFormSelection() {
label: 'Form Task',
description: 'Form submission task',
inputMappings: [],
outputMappings: []
outputMappings: [],
fieldConditions: []
};
// Clear form fields
formFields.value = [];
saveChanges();
}
@ -400,34 +581,60 @@ function removeOutputMapping(index) {
// Generate a variable name from a form field
function getVariableNameFromFormField(formField) {
// Handle cases where formField is not a string
if (!formField) return 'form_data';
// Convert to string if it's not already (in case it's an object)
let fieldName = '';
if (typeof formField === 'string') {
fieldName = formField;
} else if (typeof formField === 'object' && formField.name) {
fieldName = formField.name;
} else if (typeof formField === 'object' && formField.value) {
fieldName = formField.value;
} else {
return 'form_data';
}
// Ensure we have a valid string
if (!fieldName || typeof fieldName !== 'string') {
return 'form_data';
}
// Convert the form field to a valid variable name
// Remove spaces, special characters, and convert to camelCase
return formField.replace(/[^a-zA-Z0-9_]/g, '_')
const result = fieldName.replace(/[^a-zA-Z0-9_]/g, '_')
.replace(/^([A-Z])/, (match) => match.toLowerCase())
.replace(/[\s_]+(\w)/g, (_, c) => c.toUpperCase());
return result;
}
// Create a variable from a mapping
function createVariableFromMapping(mapping) {
if (!mapping.processVariable || !mapping.processVariable.startsWith('create_new_')) return;
const processVariableValue = getStringValue(mapping.processVariable);
const variableName = mapping.processVariable.replace('create_new_', '');
if (!variableName) return;
if (!processVariableValue || !processVariableValue.startsWith('create_new_')) {
return;
}
const variableName = processVariableValue.replace('create_new_', '');
if (!variableName) {
return;
}
// Create the variable
const newVariable = {
name: variableName,
type: 'string',
scope: 'global',
description: `Form data from ${mapping.formField} in ${localNodeData.value.formName || 'form'}`
description: `Form data from ${getStringValue(mapping.formField)} in ${localNodeData.value.formName || 'form'}`
};
// Add the variable
variableStore.addVariable(newVariable);
// Update the mapping to use the new variable
// Update the mapping to use the new variable name (string value, not object)
mapping.processVariable = variableName;
// Save changes
@ -436,14 +643,115 @@ function createVariableFromMapping(mapping) {
});
}
// Load form fields for the selected form
async function loadFormFields(formId) {
if (!formId) {
formFields.value = [];
return;
}
isLoadingFields.value = true;
try {
const response = await fetch(`/api/forms/${formId}/fields`);
const result = await response.json();
if (result.success && result.fields) {
formFields.value = result.fields;
} else {
console.error('Failed to load form fields:', result.error);
formFields.value = [];
}
} catch (error) {
console.error('Error loading form fields:', error);
formFields.value = [];
} finally {
isLoadingFields.value = false;
}
}
// Computed property for form field options (for FormKit select)
const formFieldOptions = computed(() => {
return formFields.value.map(field => ({
label: `${field.label} (${field.name})`,
value: field.name,
description: field.description || `${field.type} field`
}));
});
// 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'})`,
value: variable.name,
description: variable.description || `Type: ${variable.type || 'unknown'}`
}));
});
// Save changes by emitting them to parent
function saveChanges() {
// Create a clean copy of the data to avoid reactivity issues
const nodeDataCopy = JSON.parse(JSON.stringify(localNodeData.value));
const nodeDataCopy = {
...localNodeData.value,
inputMappings: localNodeData.value.inputMappings ?
localNodeData.value.inputMappings.map(mapping => ({ ...mapping })) : [],
outputMappings: localNodeData.value.outputMappings ?
localNodeData.value.outputMappings.map(mapping => ({ ...mapping })) : [],
fieldConditions: localNodeData.value.fieldConditions ?
localNodeData.value.fieldConditions.map(condition => ({ ...condition })) : []
};
// Emit the updated data to parent
emit('update', nodeDataCopy);
}
// Explicit save function for when the Save button is clicked
function saveAllChanges() {
saveChanges();
}
// Expose saveAllChanges so parent components can call it
defineExpose({
saveAllChanges
});
// Add a field condition
function addFieldCondition() {
if (!localNodeData.value.fieldConditions) {
localNodeData.value.fieldConditions = [];
}
localNodeData.value.fieldConditions.push({
processVariable: '',
operator: '',
value: '',
targetField: '',
action: '',
description: ''
});
saveChanges();
}
// Remove a field condition
function removeFieldCondition(index) {
localNodeData.value.fieldConditions.splice(index, 1);
saveChanges();
}
// Helper function to safely extract string values from FormKit select options
function getStringValue(value) {
if (typeof value === 'string') {
return value;
} else if (typeof value === 'object' && value.label) {
return value.label;
} else if (typeof value === 'object' && value.value) {
return value.value;
} else {
console.warn('Unexpected value type:', value);
return '';
}
}
</script>
<style scoped>

View File

@ -26,6 +26,7 @@
<!-- Main configuration area -->
<FormNodeConfiguration
ref="formNodeConfigRef"
:nodeData="localNodeData"
:availableVariables="availableVariables"
@update="handleUpdate"
@ -75,6 +76,7 @@ const emit = defineEmits(['update:modelValue', 'update']);
const showModal = ref(props.modelValue);
const localNodeData = ref({ ...props.nodeData });
const formNodeConfigRef = ref(null);
// Watch for changes to modelValue prop to sync modal visibility
watch(() => props.modelValue, (value) => {
@ -88,14 +90,33 @@ watch(() => showModal.value, (value) => {
// Watch for changes to nodeData prop
watch(() => props.nodeData, (value) => {
localNodeData.value = { ...value };
// Create deep copy to prevent reactivity issues
localNodeData.value = {
...value,
inputMappings: Array.isArray(value.inputMappings)
? value.inputMappings.map(mapping => ({ ...mapping }))
: [],
outputMappings: Array.isArray(value.outputMappings)
? value.outputMappings.map(mapping => ({ ...mapping }))
: [],
fieldConditions: Array.isArray(value.fieldConditions)
? value.fieldConditions.map(condition => ({ ...condition }))
: []
};
}, { deep: true });
function handleUpdate(updatedData) {
localNodeData.value = { ...updatedData };
// Store the changes but don't emit until explicit save
}
function saveAndClose() {
// Explicitly save all changes including field conditions
if (formNodeConfigRef.value && formNodeConfigRef.value.saveAllChanges) {
formNodeConfigRef.value.saveAllChanges();
}
// Also emit the current data to ensure all changes are saved
emit('update', localNodeData.value);
showModal.value = false;
}

View File

@ -96,7 +96,9 @@ watch(() => props.conditions, (newConditions) => {
// Operators based on variable type
const getOperatorsForType = (type) => {
switch (type) {
switch (type?.toLowerCase()) {
case 'int':
case 'decimal':
case 'number':
return [
{ value: 'eq', label: '= (Equal to)' },
@ -104,45 +106,77 @@ const getOperatorsForType = (type) => {
{ value: 'gt', label: '> (Greater than)' },
{ value: 'gte', label: '≥ (Greater than or equal to)' },
{ value: 'lt', label: '< (Less than)' },
{ value: 'lte', label: '≤ (Less than or equal to)' }
{ value: 'lte', label: '≤ (Less than or equal to)' },
{ value: 'between', label: 'Between (inclusive)' },
{ value: 'not_between', label: 'Not between' }
];
case 'string':
return [
{ value: 'eq', label: '= (Equal to)' },
{ value: 'neq', label: '≠ (Not equal to)' },
{ value: 'contains', label: 'Contains' },
{ value: 'startsWith', label: 'Starts with' },
{ value: 'endsWith', label: 'Ends with' }
{ value: 'not_contains', label: 'Does not contain' },
{ value: 'starts_with', label: 'Starts with' },
{ value: 'ends_with', label: 'Ends with' },
{ value: 'empty', label: 'Is empty' },
{ value: 'not_empty', label: 'Is not empty' },
{ value: 'regex', label: 'Matches pattern (regex)' }
];
case 'boolean':
return [
{ value: 'eq', label: '= (Equal to)' },
{ value: 'neq', label: '≠ (Not equal to)' }
{ value: 'is_true', label: 'Is True' },
{ value: 'is_false', label: 'Is False' }
];
case 'date':
case 'datetime':
return [
{ value: 'eq', label: '= (Equal to)' },
{ value: 'neq', label: '≠ (Not equal to)' },
{ value: 'gt', label: '> (After)' },
{ value: 'gte', label: '≥ (On or after)' },
{ value: 'lt', label: '< (Before)' },
{ value: 'lte', label: '≤ (On or before)' }
{ value: 'lte', label: '≤ (On or before)' },
{ value: 'between', label: 'Between dates' },
{ value: 'today', label: 'Is today' },
{ value: 'this_week', label: 'Is this week' },
{ value: 'this_month', label: 'Is this month' },
{ value: 'this_year', label: 'Is this year' }
];
case 'object':
return [
{ value: 'eq', label: '= (Equal to)' },
{ value: 'neq', label: '≠ (Not equal to)' },
{ value: 'has_property', label: 'Has property' },
{ value: 'property_equals', label: 'Property equals' },
{ value: 'empty', label: 'Is empty/null' },
{ value: 'not_empty', label: 'Is not empty/null' }
];
default:
return [
{ value: 'eq', label: '= (Equal to)' },
{ value: 'neq', label: '≠ (Not equal to)' }
{ value: 'neq', label: '≠ (Not equal to)' },
{ value: 'empty', label: 'Is empty' },
{ value: 'not_empty', label: 'Is not empty' }
];
}
};
// Get value input type based on variable type
const getInputTypeForVarType = (type) => {
switch (type) {
case 'number': return 'number';
case 'date': return 'date';
case 'boolean': return 'checkbox';
default: return 'text';
switch (type?.toLowerCase()) {
case 'int':
case 'decimal':
case 'number':
return 'number';
case 'date':
return 'date';
case 'datetime':
return 'datetime-local';
case 'boolean':
return 'checkbox';
default:
return 'text';
}
};
@ -169,6 +203,8 @@ const addConditionGroup = () => {
variable: defaultVar.name,
operator: getOperatorsForType(defaultVar.type || 'string')[0].value,
value: '',
minValue: '',
maxValue: '',
valueType: defaultVar.type || 'string',
logicalOperator: 'and' // This won't be used for the first condition but included for consistency
}
@ -211,6 +247,8 @@ const addConditionToGroup = (groupIndex) => {
variable: defaultVar.name,
operator: getOperatorsForType(defaultVar.type || 'string')[0].value,
value: '',
minValue: '',
maxValue: '',
valueType: defaultVar.type || 'string',
logicalOperator: 'and' // Default operator for this condition
};
@ -240,8 +278,16 @@ const updateCondition = (groupIndex, conditionIndex, field, value) => {
localConditions.value[groupIndex].conditions[conditionIndex].valueType = selectedVar.type;
// Reset operator to a valid one for this type
localConditions.value[groupIndex].conditions[conditionIndex].operator = getOperatorsForType(selectedVar.type)[0].value;
// Reset value
// Reset values
localConditions.value[groupIndex].conditions[conditionIndex].value = '';
localConditions.value[groupIndex].conditions[conditionIndex].minValue = '';
localConditions.value[groupIndex].conditions[conditionIndex].maxValue = '';
} else if (field === 'operator') {
localConditions.value[groupIndex].conditions[conditionIndex].operator = value;
// Reset values when operator changes
localConditions.value[groupIndex].conditions[conditionIndex].value = '';
localConditions.value[groupIndex].conditions[conditionIndex].minValue = '';
localConditions.value[groupIndex].conditions[conditionIndex].maxValue = '';
} else {
localConditions.value[groupIndex].conditions[conditionIndex][field] = value;
}
@ -265,7 +311,21 @@ const conditionText = (condition) => {
const variableName = variable?.label || variable?.name || condition.variable || 'Unknown variable';
const operatorText = operator?.label?.split(' ')[0] || condition.operator || '=';
return `${variableName} ${operatorText} ${condition.value}`;
// Handle operators that don't need values
if (['empty', 'not_empty', 'is_true', 'is_false', 'today', 'this_week', 'this_month', 'this_year'].includes(condition.operator)) {
return `${variableName} ${operatorText}`;
}
// Handle between operators
if (['between', 'not_between'].includes(condition.operator)) {
const minVal = condition.minValue || '';
const maxVal = condition.maxValue || '';
return `${variableName} ${operatorText} ${minVal} and ${maxVal}`;
}
// Handle regular operators with values
const value = condition.value || '';
return `${variableName} ${operatorText} ${value}`;
};
// Default path value
@ -307,13 +367,61 @@ const groupedConditionText = (group) => {
}).join(' ');
};
// Add the following helper method to the existing script
// Get placeholder text based on variable type and operator
const getValuePlaceholder = (condition) => {
const varType = condition.valueType?.toLowerCase();
if (varType === 'number') return 'Enter a number';
if (varType === 'date') return 'Select a date';
if (varType === 'string') return 'Enter text value';
const operator = condition.operator;
// Handle operators that don't need values
if (['empty', 'not_empty', 'is_true', 'is_false', 'today', 'this_week', 'this_month', 'this_year'].includes(operator)) {
return 'No value needed';
}
// Handle between operators
if (operator === 'between' || operator === 'not_between') {
if (varType === 'int' || varType === 'decimal' || varType === 'number') {
return 'Enter two numbers: min,max (e.g., 10,50)';
}
if (varType === 'date' || varType === 'datetime') {
return 'Enter date range: start,end';
}
}
// Handle property-based operators for objects
if (operator === 'has_property') {
return 'Enter property name (e.g., email)';
}
if (operator === 'property_equals') {
return 'Enter property:value (e.g., status:active)';
}
// Handle regex
if (operator === 'regex') {
return 'Enter regex pattern (e.g., ^[A-Z]+$)';
}
// Type-specific placeholders
switch (varType) {
case 'int':
return 'Enter a whole number (e.g., 42)';
case 'decimal':
case 'number':
return 'Enter a number (e.g., 3.14)';
case 'date':
return 'Select a date';
case 'datetime':
return 'Select date and time';
case 'string':
return operator === 'contains' || operator === 'not_contains'
? 'Enter text to search for'
: 'Enter text value';
case 'boolean':
return 'true or false';
case 'object':
return 'Enter JSON object or property path';
default:
return 'Enter value';
}
};
// Add a method to get friendly path summary
@ -504,9 +612,24 @@ const getPathSummary = (group) => {
:key="variable.name"
:value="variable.name"
>
{{ variable.label || variable.name }}
{{ variable.label || variable.name }} ({{ variable.type }})
</option>
</select>
<div v-if="condition.variable" class="mt-1 flex items-center">
<span
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"
:class="{
'bg-purple-100 text-purple-800': ['int', 'decimal'].includes(condition.valueType),
'bg-blue-100 text-blue-800': condition.valueType === 'string',
'bg-indigo-100 text-indigo-800': condition.valueType === 'boolean',
'bg-amber-100 text-amber-800': ['date', 'datetime'].includes(condition.valueType),
'bg-emerald-100 text-emerald-800': condition.valueType === 'object',
'bg-gray-100 text-gray-800': !['int', 'decimal', 'string', 'boolean', 'date', 'datetime', 'object'].includes(condition.valueType)
}"
>
{{ condition.valueType }} type
</span>
</div>
</div>
<!-- Operator -->
@ -532,22 +655,64 @@ const getPathSummary = (group) => {
<!-- Value -->
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Value</label>
<!-- No value needed for certain operators -->
<div
v-if="['empty', 'not_empty', 'is_true', 'is_false', 'today', 'this_week', 'this_month', 'this_year'].includes(condition.operator)"
class="flex items-center h-[38px] p-2 border rounded-md shadow-sm bg-gray-50 text-gray-500"
>
<Icon name="material-symbols:info" class="w-4 h-4 mr-2" />
<span class="text-sm">No value required</span>
</div>
<!-- Boolean value selection -->
<div
v-else-if="condition.valueType === 'boolean' && !['is_true', 'is_false'].includes(condition.operator)"
class="flex items-center h-[38px] p-2 border rounded-md shadow-sm bg-white"
>
<input
v-if="condition.valueType !== 'boolean'"
type="checkbox"
v-model="condition.value"
@change="saveChanges"
class="form-checkbox text-orange-500 focus:ring-orange-500"
/>
<span class="ml-2 text-sm">{{ condition.value ? 'True' : 'False' }}</span>
</div>
<!-- Special handling for between operators -->
<div
v-else-if="['between', 'not_between'].includes(condition.operator)"
class="space-y-2"
>
<div class="grid grid-cols-2 gap-2">
<input
:type="getInputTypeForVarType(condition.valueType)"
v-model="condition.minValue"
@blur="saveChanges"
class="w-full p-2 border rounded-md shadow-sm focus:border-orange-500 focus:ring focus:ring-orange-200 text-sm"
:placeholder="condition.valueType === 'date' || condition.valueType === 'datetime' ? 'Start date' : 'Min value'"
/>
<input
:type="getInputTypeForVarType(condition.valueType)"
v-model="condition.maxValue"
@blur="saveChanges"
class="w-full p-2 border rounded-md shadow-sm focus:border-orange-500 focus:ring focus:ring-orange-200 text-sm"
:placeholder="condition.valueType === 'date' || condition.valueType === 'datetime' ? 'End date' : 'Max value'"
/>
</div>
<p class="text-xs text-gray-500">Enter the range for the condition</p>
</div>
<!-- Regular value input -->
<input
v-else
:type="getInputTypeForVarType(condition.valueType)"
v-model="condition.value"
@blur="saveChanges"
class="w-full p-2 border rounded-md shadow-sm focus:border-orange-500 focus:ring focus:ring-orange-200 text-sm"
:placeholder="getValuePlaceholder(condition)"
:step="condition.valueType === 'decimal' ? '0.01' : undefined"
/>
<div v-else class="flex items-center h-[38px] p-2 border rounded-md shadow-sm bg-white">
<input
type="checkbox"
v-model="condition.value"
class="form-checkbox text-orange-500 focus:ring-orange-500"
/>
<span class="ml-2 text-sm">{{ condition.value ? 'Yes/True' : 'No/False' }}</span>
</div>
</div>
</div>
</div>

View File

@ -34,6 +34,8 @@
</div>
</div>
</div>
</div>
</template>
@ -70,20 +72,7 @@ const availableComponents = [
}
}
},
{
type: 'task',
name: 'Task',
category: 'Core',
icon: 'material-symbols:assignment-outline',
description: 'Manual or automated task',
defaultProps: {
label: 'Task',
data: {
description: 'A general task',
assignee: ''
}
}
},
{
type: 'form',
name: 'Form Task',
@ -166,26 +155,10 @@ const availableComponents = [
}
}
},
// Advanced components
{
type: 'script',
name: 'Script Task',
category: 'Advanced',
icon: 'material-symbols:code',
description: 'Execute custom code',
defaultProps: {
label: 'Script',
data: {
description: 'Script execution',
language: 'JavaScript',
script: ''
}
}
},
{
type: 'business-rule',
name: 'Business Rule',
category: 'Advanced',
category: 'Core',
icon: 'material-symbols:rule',
description: 'Apply business rules to data',
defaultProps: {

View File

@ -54,7 +54,11 @@ const {
deleteKeyCode: 'Delete',
selectionKeyCode: 'Shift',
multiSelectionKeyCode: 'Control',
connectionMode: 'loose'
connectionMode: 'loose',
isValidConnection: (connection) => {
// console.log('Validating connection:', connection);
return true;
}
});
// Default nodes if empty
@ -73,6 +77,7 @@ const flowOptions = ref({
snapGrid: [15, 15],
edgeUpdaterRadius: 10,
connectionMode: 'loose',
connectionRadius: 25,
elevateEdgesOnSelect: true,
nodesDraggable: true,
nodesConnectable: true,
@ -82,7 +87,8 @@ const flowOptions = ref({
panOnScroll: false,
zoomOnScroll: true,
zoomOnPinch: true,
zoomOnDoubleClick: false
zoomOnDoubleClick: false,
connectOnClick: false
});
// Use shallowRef for selected node to avoid unnecessary reactivity
@ -170,6 +176,100 @@ onMounted(() => {
}, 100);
});
// Watch for changes to initialNodes prop and update the canvas
watch(() => props.initialNodes, async (newNodes, oldNodes) => {
if (newNodes && Array.isArray(newNodes)) {
// console.log('ProcessFlowCanvas: Updating nodes, count:', newNodes.length);
// Clear existing nodes and add new ones
nodes.value = [];
if (newNodes.length > 0) {
addNodes([...newNodes]); // Create a copy to avoid reactivity issues
// Fit view to show all nodes after both nodes and edges are processed
await nextTick();
setTimeout(() => {
fitView();
}, 100);
}
}
}, { deep: true });
// Watch for changes to initialEdges prop and update the canvas
// This watcher depends on nodes being already present
watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCount]) => {
if (newEdges && Array.isArray(newEdges) && nodeCount > 0) {
// console.log('ProcessFlowCanvas: Updating edges, count:', newEdges.length, 'nodeCount:', nodeCount);
// Clear existing edges
edges.value = [];
if (newEdges.length > 0) {
// Verify all nodes exist before adding edges
const validEdges = newEdges.filter(edge => {
const sourceExists = nodes.value.some(node => node.id === edge.source);
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}`);
return false;
}
return true;
});
if (validEdges.length > 0) {
// Ensure all edges have proper handle specifications
const edgesWithHandles = validEdges.map(edge => {
// If edge already has sourceHandle and targetHandle, use them
if (edge.sourceHandle && edge.targetHandle) {
return edge;
}
// Otherwise, generate default handles based on node types
const sourceNode = nodes.value.find(n => n.id === edge.source);
const targetNode = nodes.value.find(n => n.id === edge.target);
let sourceHandle = edge.sourceHandle;
let targetHandle = edge.targetHandle;
// Generate default source handle if missing
if (!sourceHandle && sourceNode) {
if (sourceNode.type === 'start') {
sourceHandle = `${edge.source}-bottom`; // Start nodes prefer bottom output
} else if (sourceNode.type === 'gateway') {
sourceHandle = `${edge.source}-right`; // Gateway nodes prefer right output for first connection
} else {
sourceHandle = `${edge.source}-bottom`; // Most nodes prefer bottom output
}
}
// Generate default target handle if missing
if (!targetHandle && targetNode) {
if (targetNode.type === 'end') {
targetHandle = `${edge.target}-top`; // End nodes prefer top input
} else {
targetHandle = `${edge.target}-top`; // Most nodes prefer top input
}
}
return {
...edge,
sourceHandle,
targetHandle
};
});
addEdges([...edgesWithHandles]); // Create a copy to avoid reactivity issues
// console.log('ProcessFlowCanvas: Successfully added edges with handles:', edgesWithHandles.length);
}
}
} else if (newEdges && Array.isArray(newEdges) && newEdges.length > 0 && nodeCount === 0) {
// console.log('ProcessFlowCanvas: Edges provided but no nodes yet, waiting...');
}
}, { deep: true });
// Remove the deep watch as it's causing recursive updates
// Handle node changes
@ -186,6 +286,8 @@ onEdgesChange((changes) => {
const handleConnect = (connection) => {
if (!connection.source || !connection.target) return;
console.log('Connection created:', connection);
// Try to determine if this is coming from a gateway
const sourceNode = nodes.value.find(node => node.id === connection.source);
let label = '';
@ -219,15 +321,24 @@ const handleConnect = (connection) => {
}
const newEdge = {
id: `${connection.source}-${connection.target}`,
id: `${connection.source}-${connection.target}-${Date.now()}`,
source: connection.source,
target: connection.target,
sourceHandle: connection.sourceHandle,
targetHandle: connection.targetHandle,
type: 'smoothstep',
animated: true,
style: { stroke: '#555' },
label: label
};
console.log('Creating edge with handles:', {
sourceHandle: connection.sourceHandle,
targetHandle: connection.targetHandle,
source: connection.source,
target: connection.target
});
addEdges([newEdge]);
emit('edgesChange', edges.value);
};

View File

@ -3,20 +3,54 @@ import { Handle, Position } from '@vue-flow/core';
// Custom node renderer
const CustomNode = markRaw({
setup() {
return {
Position
};
},
template: `
<div
:class="['custom-node', 'node-' + type, selected ? 'selected' : '']"
@click="onClick"
>
<!-- Top handle -->
<Handle
v-if="type !== 'start'"
type="target"
position="top"
:class="'handle-' + type + '-input'"
:position="Position.Top"
:class="'handle-' + type + '-input handle-top'"
:id="id + '-top'"
:style="{ zIndex: 1000 }"
:isConnectable="true"
:isValidConnection="() => true"
/>
<!-- Left handle -->
<Handle
v-if="type !== 'start'"
type="target"
:position="Position.Left"
:class="'handle-' + type + '-input handle-left'"
:id="id + '-left'"
:style="{ zIndex: 1000 }"
:isConnectable="true"
:isValidConnection="() => true"
/>
<!-- Right handle -->
<Handle
v-if="type !== 'end'"
type="source"
:position="Position.Right"
:class="'handle-' + type + '-output handle-right'"
:id="id + '-right'"
:style="{ zIndex: 1000 }"
:isConnectable="true"
:isValidConnection="() => true"
/>
<div class="custom-node-content">
<template v-if="type === 'task' || type === 'form' || type === 'script'">
<template v-if="type === 'form' || type === 'script' || type === 'api' || type === 'business-rule' || type === 'notification'">
<div class="flex items-center mb-1">
<div class="custom-node-icon">
<slot name="icon"></slot>
@ -38,11 +72,16 @@ const CustomNode = markRaw({
</template>
</div>
<!-- Bottom handle -->
<Handle
v-if="type !== 'end'"
type="source"
position="bottom"
:class="'handle-' + type + '-output'"
:position="Position.Bottom"
:class="'handle-' + type + '-output handle-bottom'"
:id="id + '-bottom'"
:style="{ zIndex: 1000 }"
:isConnectable="true"
:isValidConnection="() => true"
/>
</div>
`,
@ -66,146 +105,115 @@ const CustomNode = markRaw({
}
});
// Task node
export const TaskNode = 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) || 'Task';
},
// Helper method to get assignment display text
assignmentText() {
if (!this.data) return 'Unassigned';
const { assignmentType, assignedUsers, assignedRoles, assigneeVariable } = this.data;
if (assignmentType === 'user' && Array.isArray(assignedUsers) && assignedUsers.length > 0) {
return `${assignedUsers.length} User${assignedUsers.length > 1 ? 's' : ''}`;
}
if (assignmentType === 'role' && Array.isArray(assignedRoles) && assignedRoles.length > 0) {
return `${assignedRoles.length} Role${assignedRoles.length > 1 ? 's' : ''}`;
}
if (assignmentType === 'variable' && assigneeVariable) {
return `Variable: ${assigneeVariable}`;
}
return 'Unassigned';
},
// Helper to determine priority class
priorityClass() {
if (!this.data || !this.data.priority) return '';
const priorityColors = {
low: 'text-green-500',
medium: 'text-blue-500',
high: 'text-orange-500',
urgent: 'text-red-500'
};
return priorityColors[this.data.priority] || '';
},
// Helper to get priority label
priorityLabel() {
if (!this.data || !this.data.priority) return 'None';
return this.data.priority.charAt(0).toUpperCase() + this.data.priority.slice(1);
},
// Helper for due date
dueLabel() {
if (!this.data || !this.data.dueDateType || this.data.dueDateType === 'none') {
return 'Not set';
}
if (this.data.dueDateType === 'fixed') {
return `${this.data.dueDateDays || 0} days`;
}
return `Variable: ${this.data.dueDateVariable || 'none'}`;
}
},
render() {
return h(CustomNode, {
id: this.id,
type: 'task',
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' }, 'assignment'),
default: () => h('div', { class: 'node-details' }, [
h('p', { class: 'node-description' }, this.data?.description || 'A general task'),
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
h('span', { class: 'node-rule-detail-label' }, 'Assigned:'),
h('span', { class: 'node-rule-detail-value ml-1 font-medium text-blue-600' }, this.assignmentText)
]),
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
h('span', { class: 'node-rule-detail-label' }, 'Priority:'),
h('span', {
class: `node-rule-detail-value ml-1 font-medium ${this.priorityClass}`
}, this.priorityLabel)
]),
this.data?.dueDateType !== 'none' && this.data?.dueDateType ?
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
h('span', { class: 'node-rule-detail-label' }, 'Due:'),
h('span', { class: 'node-rule-detail-value ml-1 font-medium text-blue-600' }, this.dueLabel)
]) : null
])
});
}
});
// Start node
export const StartNode = markRaw({
props: ['id', 'type', 'label', 'selected', 'data'],
setup() {
return {
Position
};
},
computed: {
nodeLabel() {
// Get label from either prop or data, with fallback
return this.label || (this.data && this.data.label) || 'Start';
}
},
render() {
return h(CustomNode, {
id: this.id,
type: 'start',
label: this.nodeLabel,
selected: this.selected,
data: this.data,
onClick: () => this.$emit('node-click', this.id)
}, {
icon: () => h('i', { class: 'material-icons text-green-600' }, 'play_arrow'),
default: () => null
});
template: `
<div
:class="['custom-node', 'node-start', selected ? 'selected' : '']"
@click="$emit('node-click', id)"
>
<!-- Start node only has output handles -->
<!-- Right handle -->
<Handle
type="source"
:position="Position.Right"
class="handle-start-output handle-right"
:id="id + '-right'"
:style="{ zIndex: 1000 }"
:isConnectable="true"
:isValidConnection="() => true"
/>
<!-- Bottom handle -->
<Handle
type="source"
:position="Position.Bottom"
class="handle-start-output handle-bottom"
:id="id + '-bottom'"
:style="{ zIndex: 1000 }"
:isConnectable="true"
:isValidConnection="() => true"
/>
<div class="custom-node-content">
<div class="custom-node-icon">
<i class="material-icons text-green-600">play_arrow</i>
</div>
<div class="custom-node-title">{{ nodeLabel }}</div>
</div>
</div>
`,
components: {
Handle
}
});
// End node
export const EndNode = markRaw({
props: ['id', 'type', 'label', 'selected', 'data'],
setup() {
return {
Position
};
},
computed: {
nodeLabel() {
// Get label from either prop or data, with fallback
return this.label || (this.data && this.data.label) || 'End';
}
},
render() {
return h(CustomNode, {
id: this.id,
type: 'end',
label: this.nodeLabel,
selected: this.selected,
data: this.data,
onClick: () => this.$emit('node-click', this.id)
}, {
icon: () => h('i', { class: 'material-icons text-red-600' }, 'stop'),
default: () => null
});
template: `
<div
:class="['custom-node', 'node-end', selected ? 'selected' : '']"
@click="$emit('node-click', id)"
>
<!-- End node only has input handles -->
<!-- Top handle -->
<Handle
type="target"
:position="Position.Top"
class="handle-end-input handle-top"
:id="id + '-top'"
:style="{ zIndex: 1000 }"
:isConnectable="true"
:isValidConnection="() => true"
/>
<!-- Left handle -->
<Handle
type="target"
:position="Position.Left"
class="handle-end-input handle-left"
:id="id + '-left'"
:style="{ zIndex: 1000 }"
:isConnectable="true"
:isValidConnection="() => true"
/>
<div class="custom-node-content">
<div class="custom-node-icon">
<i class="material-icons text-red-600">stop</i>
</div>
<div class="custom-node-title">{{ nodeLabel }}</div>
</div>
</div>
`,
components: {
Handle
}
});
@ -567,7 +575,6 @@ export const NotificationNode = markRaw({
// Export the node types object to use with Vue Flow
export const nodeTypes = markRaw({
task: TaskNode,
start: StartNode,
end: EndNode,
gateway: GatewayNode,
@ -593,8 +600,204 @@ export const nodeStyles = `
box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.2);
}
/* Handle positioning and styling */
.handle-top {
top: -6px !important;
left: 50% !important;
transform: translateX(-50%) !important;
width: 12px !important;
height: 12px !important;
border-radius: 50% !important;
background: #fff !important;
border: 2px solid #666 !important;
opacity: 0;
transition: all 0.2s ease;
cursor: crosshair;
z-index: 100 !important;
position: absolute !important;
}
.handle-bottom {
bottom: -6px !important;
left: 50% !important;
transform: translateX(-50%) !important;
width: 12px !important;
height: 12px !important;
border-radius: 50% !important;
background: #fff !important;
border: 2px solid #666 !important;
opacity: 0;
transition: all 0.2s ease;
cursor: crosshair;
z-index: 100 !important;
position: absolute !important;
}
.handle-left {
left: -6px !important;
top: 50% !important;
transform: translateY(-50%) !important;
width: 12px !important;
height: 12px !important;
border-radius: 50% !important;
background: #fff !important;
border: 2px solid #666 !important;
opacity: 0;
transition: all 0.2s ease;
cursor: crosshair;
z-index: 100 !important;
position: absolute !important;
}
.handle-right {
right: -6px !important;
top: 50% !important;
transform: translateY(-50%) !important;
width: 12px !important;
height: 12px !important;
border-radius: 50% !important;
background: #fff !important;
border: 2px solid #666 !important;
opacity: 0;
transition: all 0.2s ease;
cursor: crosshair;
z-index: 100 !important;
position: absolute !important;
}
/* Show handles on hover and during connection */
.custom-node:hover .handle-top,
.custom-node:hover .handle-bottom,
.custom-node:hover .handle-left,
.custom-node:hover .handle-right,
.vue-flow__node.connecting .handle-top,
.vue-flow__node.connecting .handle-bottom,
.vue-flow__node.connecting .handle-left,
.vue-flow__node.connecting .handle-right {
opacity: 1;
}
/* Show all handles when any node is being connected */
.vue-flow.connecting .handle-top,
.vue-flow.connecting .handle-bottom,
.vue-flow.connecting .handle-left,
.vue-flow.connecting .handle-right {
opacity: 1 !important;
}
/* Active handle styles for connection mode */
.vue-flow__handle.connecting,
.vue-flow__handle.valid {
opacity: 1 !important;
transform: scale(1.3) !important;
border-width: 3px !important;
box-shadow: 0 0 10px rgba(37, 99, 235, 0.5);
}
/* Ensure handles are clickable and properly sized */
.vue-flow__handle {
pointer-events: all !important;
min-width: 12px !important;
min-height: 12px !important;
position: absolute !important;
}
/* Force handle visibility during connection */
.vue-flow.connecting .vue-flow__handle {
opacity: 1 !important;
pointer-events: all !important;
z-index: 1000 !important;
}
/* Connection line styles */
.vue-flow__connection-line {
stroke: #2563eb;
stroke-width: 3;
stroke-dasharray: 5,5;
z-index: 1000;
}
/* Target handle highlighting during connection */
.vue-flow__handle.target:hover,
.vue-flow__handle.valid {
background: #2563eb !important;
border-color: #1d4ed8 !important;
transform: scale(1.3) !important;
}
/* Handle hover effects */
.handle-top:hover,
.handle-bottom:hover,
.handle-left:hover,
.handle-right:hover {
transform: scale(1.1);
border-width: 3px;
}
.handle-top:hover {
transform: translateX(-50%) scale(1.1);
}
.handle-bottom:hover {
transform: translateX(-50%) scale(1.1);
}
.handle-left:hover {
transform: translateY(-50%) scale(1.1);
}
.handle-right:hover {
transform: translateY(-50%) scale(1.1);
}
/* Source handles (output) */
.handle-start-output,
.handle-gateway-output,
.handle-form-output,
.handle-script-output,
.handle-api-output,
.handle-business-rule-output,
.handle-notification-output {
border-color: #4CAF50;
background: #e8f5e9;
}
.handle-start-output:hover,
.handle-gateway-output:hover,
.handle-form-output:hover,
.handle-script-output:hover,
.handle-api-output:hover,
.handle-business-rule-output:hover,
.handle-notification-output:hover {
background: #4CAF50;
border-color: #2E7D32;
}
/* Target handles (input) */
.handle-end-input,
.handle-gateway-input,
.handle-form-input,
.handle-script-input,
.handle-api-input,
.handle-business-rule-input,
.handle-notification-input {
border-color: #2196F3;
background: #e3f2fd;
}
.handle-end-input:hover,
.handle-gateway-input:hover,
.handle-form-input:hover,
.handle-script-input:hover,
.handle-api-input:hover,
.handle-business-rule-input:hover,
.handle-notification-input:hover {
background: #2196F3;
border-color: #1565C0;
}
/* Base styles for different node types */
.node-task, .node-form, .node-script, .node-api {
.node-form, .node-script, .node-api, .node-business-rule, .node-notification {
width: 180px;
background: white;
border-radius: 4px;
@ -689,10 +892,11 @@ export const nodeStyles = `
font-size: 14px;
}
.node-task .custom-node-title,
.node-form .custom-node-title,
.node-script .custom-node-title,
.node-api .custom-node-title {
.node-api .custom-node-title,
.node-business-rule .custom-node-title,
.node-notification .custom-node-title {
font-weight: 500;
font-size: 11px;
display: flex;
@ -808,57 +1012,66 @@ export const nodeStyles = `
margin-top: 4px;
}
.handle-task-input,
.handle-form-input,
.handle-script-input,
.handle-api-input,
.handle-gateway-input {
top: -5px !important;
width: 8px !important;
height: 8px !important;
border-radius: 50% !important;
}
.handle-task-output,
.handle-form-output,
.handle-script-output,
.handle-api-output,
.handle-gateway-output {
bottom: -5px !important;
width: 8px !important;
height: 8px !important;
border-radius: 50% !important;
}
.handle-start-output {
bottom: -5px !important;
width: 8px !important;
height: 8px !important;
border-radius: 50% !important;
}
.handle-end-input {
top: -5px !important;
width: 8px !important;
height: 8px !important;
border-radius: 50% !important;
}
/* Position handles correctly for gateway node */
.handle-gateway-input {
transform: translateY(-42px) !important;
background-color: #f97316 !important;
border: 2px solid white !important;
width: 12px !important;
height: 12px !important;
.node-gateway .handle-top {
top: -6px !important;
left: 50% !important;
transform: translateX(-50%) rotate(-45deg) !important;
border-color: #f97316 !important;
background: #fff !important;
}
.handle-gateway-output {
transform: translateY(42px) !important;
background-color: #f97316 !important;
border: 2px solid white !important;
width: 12px !important;
height: 12px !important;
.node-gateway .handle-bottom {
bottom: -6px !important;
left: 50% !important;
transform: translateX(-50%) rotate(-45deg) !important;
border-color: #f97316 !important;
background: #fff !important;
}
.node-gateway .handle-left {
left: -6px !important;
top: 50% !important;
transform: translateY(-50%) rotate(-45deg) !important;
border-color: #f97316 !important;
background: #fff !important;
}
.node-gateway .handle-right {
right: -6px !important;
top: 50% !important;
transform: translateY(-50%) rotate(-45deg) !important;
border-color: #f97316 !important;
background: #fff !important;
}
.node-gateway:hover .handle-top,
.node-gateway:hover .handle-bottom,
.node-gateway:hover .handle-left,
.node-gateway:hover .handle-right {
opacity: 1 !important;
}
.node-gateway .handle-top:hover {
transform: translateX(-50%) rotate(-45deg) scale(1.1) !important;
background: #f97316 !important;
}
.node-gateway .handle-bottom:hover {
transform: translateX(-50%) rotate(-45deg) scale(1.1) !important;
background: #f97316 !important;
}
.node-gateway .handle-left:hover {
transform: translateY(-50%) rotate(-45deg) scale(1.1) !important;
background: #f97316 !important;
}
.node-gateway .handle-right:hover {
transform: translateY(-50%) rotate(-45deg) scale(1.1) !important;
background: #f97316 !important;
}
/* Badge style */

View File

@ -0,0 +1,532 @@
<template>
<RsModal v-model="showModal" title="Process Settings" size="xl" position="center">
<div>
<RsTab :tabs="settingsTabs" v-model="activeTab">
<!-- Process Info Tab -->
<template #info>
<div class="p-4 space-y-4">
<FormKit
type="text"
label="Process Name"
v-model="localProcess.name"
help="Name of your process"
validation="required"
/>
<FormKit
type="textarea"
label="Process Description"
v-model="localProcess.description"
help="Brief description of what this process does"
rows="3"
/>
<FormKit
type="select"
label="Priority Level"
v-model="localProcess.priority"
:options="[
{ label: 'Low Priority', value: 'low' },
{ label: 'Normal Priority', value: 'normal' },
{ label: 'High Priority', value: 'high' },
{ label: 'Critical Priority', value: 'critical' }
]"
help="Set the priority level for this process"
/>
<FormKit
type="text"
label="Process Category"
v-model="localProcess.category"
help="Category or department this process belongs to"
placeholder="e.g., HR, Finance, Operations"
/>
<FormKit
type="text"
label="Owner/Manager"
v-model="localProcess.owner"
help="Person responsible for this process"
placeholder="e.g., John Doe, HR Manager"
/>
</div>
</template>
<!-- Execution Settings Tab -->
<template #execution>
<div class="p-4 space-y-4">
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
<div class="flex items-center mb-2">
<Icon name="material-symbols:info" class="w-5 h-5 text-blue-600 mr-2" />
<h4 class="font-medium text-blue-900">Execution Settings</h4>
</div>
<p class="text-sm text-blue-700">
Configure how this process should execute and handle different scenarios.
</p>
</div>
<FormKit
type="select"
label="Process Type"
v-model="localProcess.processType"
:options="[
{ label: 'Standard Process', value: 'standard' },
{ label: 'Approval Workflow', value: 'approval' },
{ label: 'Data Collection', value: 'data_collection' },
{ label: 'Automated Task', value: 'automation' },
{ label: 'Review Process', value: 'review' }
]"
help="Type of process workflow"
/>
<div class="grid grid-cols-2 gap-4">
<FormKit
type="number"
label="Max Execution Time (minutes)"
v-model="localProcess.maxExecutionTime"
help="Maximum time allowed for process completion"
min="1"
max="10080"
/>
<FormKit
type="number"
label="Auto-Timeout (hours)"
v-model="localProcess.autoTimeout"
help="Automatically timeout after specified hours"
min="1"
max="168"
/>
</div>
<FormKit
type="checkbox"
label="Allow Parallel Execution"
v-model="localProcess.allowParallel"
help="Allow multiple instances of this process to run simultaneously"
/>
<FormKit
type="checkbox"
label="Enable Error Recovery"
v-model="localProcess.enableErrorRecovery"
help="Automatically retry failed steps before stopping"
/>
<FormKit
type="checkbox"
label="Send Completion Notifications"
v-model="localProcess.sendNotifications"
help="Send notifications when process completes"
/>
</div>
</template>
<!-- Variables & Data Tab -->
<template #variables>
<div class="p-4 space-y-4">
<div class="bg-purple-50 border border-purple-200 rounded-lg p-4 mb-4">
<div class="flex items-center mb-2">
<Icon name="material-symbols:data-object" class="w-5 h-5 text-purple-600 mr-2" />
<h4 class="font-medium text-purple-900">Variable Configuration</h4>
</div>
<p class="text-sm text-purple-700">
Configure data handling and variable persistence settings.
</p>
</div>
<FormKit
type="select"
label="Data Persistence"
v-model="localProcess.dataPersistence"
:options="[
{ label: 'Session Only', value: 'session' },
{ label: 'Temporary (24 hours)', value: 'temporary' },
{ label: 'Short Term (7 days)', value: 'short_term' },
{ label: 'Long Term (30 days)', value: 'long_term' },
{ label: 'Permanent', value: 'permanent' }
]"
help="How long should process data be stored"
/>
<FormKit
type="checkbox"
label="Log Variable Changes"
v-model="localProcess.logVariableChanges"
help="Keep a log of all variable modifications during execution"
/>
<FormKit
type="checkbox"
label="Encrypt Sensitive Data"
v-model="localProcess.encryptSensitiveData"
help="Automatically encrypt variables marked as sensitive"
/>
<FormKit
type="textarea"
label="Data Retention Policy"
v-model="localProcess.dataRetentionPolicy"
help="Describe how data should be handled after process completion"
rows="3"
placeholder="e.g., Delete personal data after 30 days, archive business data for 7 years"
/>
</div>
</template>
<!-- Permissions Tab -->
<template #permissions>
<div class="p-4 space-y-4">
<div class="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
<div class="flex items-center mb-2">
<Icon name="material-symbols:security" class="w-5 h-5 text-green-600 mr-2" />
<h4 class="font-medium text-green-900">Access Control</h4>
</div>
<p class="text-sm text-green-700">
Define who can execute, modify, and view this process.
</p>
</div>
<FormKit
type="select"
label="Execution Permission"
v-model="localProcess.executionPermission"
:options="[
{ label: 'Anyone', value: 'public' },
{ label: 'Authenticated Users', value: 'authenticated' },
{ label: 'Specific Roles', value: 'roles' },
{ label: 'Process Managers Only', value: 'managers' },
{ label: 'Administrators Only', value: 'admin' }
]"
help="Who can start and execute this process"
/>
<FormKit
type="text"
label="Allowed Roles"
v-model="localProcess.allowedRoles"
help="Comma-separated list of roles that can execute this process"
placeholder="e.g., manager, hr_admin, finance_user"
v-if="localProcess.executionPermission === 'roles'"
/>
<FormKit
type="select"
label="Modification Permission"
v-model="localProcess.modificationPermission"
:options="[
{ label: 'Process Owner Only', value: 'owner' },
{ label: 'Process Managers', value: 'managers' },
{ label: 'Administrators', value: 'admin' },
{ label: 'Anyone with Edit Rights', value: 'editors' }
]"
help="Who can modify this process"
/>
<FormKit
type="checkbox"
label="Require Approval for Changes"
v-model="localProcess.requireApproval"
help="Changes to this process must be approved before taking effect"
/>
<FormKit
type="checkbox"
label="Enable Audit Trail"
v-model="localProcess.enableAuditTrail"
help="Keep detailed logs of who accessed and modified this process"
/>
</div>
</template>
<!-- JSON Export Tab -->
<template #json>
<div class="p-4">
<div class="mb-4">
<h3 class="text-lg font-medium mb-2">Process Configuration</h3>
<p class="text-sm text-gray-600 mb-4">
This section displays the complete process configuration as JSON for developers and system integration.
</p>
<!-- Process metadata -->
<div class="bg-gray-50 p-3 rounded border mb-4 text-sm">
<div class="grid grid-cols-2 gap-2">
<div>
<span class="font-medium">Node Count:</span> {{ nodeCount }}
</div>
<div>
<span class="font-medium">Edge Count:</span> {{ edgeCount }}
</div>
<div>
<span class="font-medium">Process ID:</span> {{ localProcess.id || 'Not saved yet' }}
</div>
<div>
<span class="font-medium">Variable Count:</span> {{ variableCount }}
</div>
</div>
</div>
<!-- Action buttons -->
<div class="flex gap-2 mb-4">
<RsButton @click="copyToClipboard" variant="secondary" size="sm">
<Icon name="material-symbols:content-copy" class="mr-1" />
Copy JSON
</RsButton>
<RsButton @click="downloadJson" variant="secondary" size="sm">
<Icon name="material-symbols:download" class="mr-1" />
Download
</RsButton>
</div>
</div>
<!-- JSON Display -->
<div class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-auto" style="max-height: 400px;">
<pre class="text-sm">{{ formattedJson }}</pre>
</div>
</div>
</template>
</RsTab>
</div>
<!-- Footer Actions -->
<template #footer>
<div class="flex justify-end gap-2">
<RsButton @click="closeModal" variant="tertiary">
Cancel
</RsButton>
<RsButton @click="saveSettings" variant="primary">
Save Settings
</RsButton>
</div>
</template>
</RsModal>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useProcessBuilderStore } from '~/stores/processBuilder'
import { useVariableStore } from '~/stores/variableStore'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue'])
const processStore = useProcessBuilderStore()
const variableStore = useVariableStore()
// Modal visibility
const showModal = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
// Settings tabs configuration
const settingsTabs = [
{ key: 'info', label: 'Process Info', icon: 'material-symbols:info-outline' },
{ key: 'execution', label: 'Execution', icon: 'material-symbols:play-circle-outline' },
{ key: 'variables', label: 'Variables & Data', icon: 'material-symbols:data-object' },
{ key: 'permissions', label: 'Permissions', icon: 'material-symbols:security' },
{ key: 'json', label: 'JSON Export', icon: 'material-symbols:code' }
]
const activeTab = ref('info')
// Local process data (copy of current process)
const localProcess = ref({
name: '',
description: '',
priority: 'normal',
category: '',
owner: '',
processType: 'standard',
maxExecutionTime: 60,
autoTimeout: 24,
allowParallel: false,
enableErrorRecovery: true,
sendNotifications: true,
dataPersistence: 'short_term',
logVariableChanges: true,
encryptSensitiveData: false,
dataRetentionPolicy: '',
executionPermission: 'authenticated',
allowedRoles: '',
modificationPermission: 'managers',
requireApproval: false,
enableAuditTrail: true
})
// Computed properties for metadata
const nodeCount = computed(() => {
return processStore.currentProcess?.nodes?.length || 0
})
const edgeCount = computed(() => {
return processStore.currentProcess?.edges?.length || 0
})
const variableCount = computed(() => {
const allVars = variableStore.getAllVariables
return (allVars.global?.length || 0) + (allVars.process?.length || 0)
})
// JSON export functionality
const formattedJson = computed(() => {
const exportData = {
processInfo: {
id: localProcess.value.id,
name: localProcess.value.name,
description: localProcess.value.description,
priority: localProcess.value.priority,
category: localProcess.value.category,
owner: localProcess.value.owner
},
settings: {
processType: localProcess.value.processType,
maxExecutionTime: localProcess.value.maxExecutionTime,
autoTimeout: localProcess.value.autoTimeout,
allowParallel: localProcess.value.allowParallel,
enableErrorRecovery: localProcess.value.enableErrorRecovery,
sendNotifications: localProcess.value.sendNotifications
},
dataSettings: {
dataPersistence: localProcess.value.dataPersistence,
logVariableChanges: localProcess.value.logVariableChanges,
encryptSensitiveData: localProcess.value.encryptSensitiveData,
dataRetentionPolicy: localProcess.value.dataRetentionPolicy
},
permissions: {
executionPermission: localProcess.value.executionPermission,
allowedRoles: localProcess.value.allowedRoles,
modificationPermission: localProcess.value.modificationPermission,
requireApproval: localProcess.value.requireApproval,
enableAuditTrail: localProcess.value.enableAuditTrail
},
workflow: {
nodes: processStore.currentProcess?.nodes || [],
edges: processStore.currentProcess?.edges || []
},
variables: variableStore.getAllVariables,
metadata: {
nodeCount: nodeCount.value,
edgeCount: edgeCount.value,
variableCount: variableCount.value,
exportedAt: new Date().toISOString()
}
}
return JSON.stringify(exportData, null, 2)
})
// Watch for changes to current process and sync with local data
watch(() => processStore.currentProcess, (newProcess) => {
if (newProcess) {
localProcess.value = {
...localProcess.value,
id: newProcess.id,
name: newProcess.name || '',
description: newProcess.description || '',
...newProcess.settings
}
}
}, { immediate: true, deep: true })
// Methods
const closeModal = () => {
showModal.value = false
}
const saveSettings = () => {
// Update the process in the store
if (processStore.currentProcess) {
const updatedProcess = {
...processStore.currentProcess,
name: localProcess.value.name,
description: localProcess.value.description,
settings: {
priority: localProcess.value.priority,
category: localProcess.value.category,
owner: localProcess.value.owner,
processType: localProcess.value.processType,
maxExecutionTime: localProcess.value.maxExecutionTime,
autoTimeout: localProcess.value.autoTimeout,
allowParallel: localProcess.value.allowParallel,
enableErrorRecovery: localProcess.value.enableErrorRecovery,
sendNotifications: localProcess.value.sendNotifications,
dataPersistence: localProcess.value.dataPersistence,
logVariableChanges: localProcess.value.logVariableChanges,
encryptSensitiveData: localProcess.value.encryptSensitiveData,
dataRetentionPolicy: localProcess.value.dataRetentionPolicy,
executionPermission: localProcess.value.executionPermission,
allowedRoles: localProcess.value.allowedRoles,
modificationPermission: localProcess.value.modificationPermission,
requireApproval: localProcess.value.requireApproval,
enableAuditTrail: localProcess.value.enableAuditTrail
}
}
// Update the store
processStore.updateCurrentProcess(updatedProcess)
}
closeModal()
}
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(formattedJson.value)
// You might want to show a toast notification here
console.log('JSON copied to clipboard')
} catch (err) {
console.error('Failed to copy JSON:', err)
}
}
const downloadJson = () => {
const blob = new Blob([formattedJson.value], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${localProcess.value.name || 'process'}_settings.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
</script>
<style scoped>
/* Custom styling for the settings modal */
:deep(.formkit-outer) {
margin-bottom: 1rem;
}
:deep(.formkit-label) {
font-weight: 500;
margin-bottom: 0.25rem;
font-size: 0.875rem;
color: #374151;
}
:deep(.formkit-help) {
font-size: 0.75rem;
color: #6b7280;
margin-top: 0.25rem;
}
:deep(.formkit-messages) {
font-size: 0.75rem;
color: #ef4444;
margin-top: 0.25rem;
}
pre {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
line-height: 1.5;
}
</style>

View File

@ -1,391 +0,0 @@
<template>
<div class="task-node-configuration">
<h3 class="text-lg font-semibold mb-4">Task Configuration</h3>
<!-- <div class="form-group mb-4">
<label for="nodeLabel" class="form-label">Task Name</label>
<input
id="nodeLabel"
v-model="localNodeData.label"
type="text"
class="form-control"
placeholder="Task Name"
@blur="saveChanges"
/>
</div> -->
<!-- <div class="form-group mb-4">
<label for="nodeDescription" class="form-label">Description</label>
<textarea
id="nodeDescription"
v-model="localNodeData.description"
class="form-control"
placeholder="Task description"
rows="2"
@blur="saveChanges"
></textarea>
</div> -->
<!-- Assignment Type Selector -->
<div class="form-group mb-4">
<label class="form-label">Assignment Type</label>
<div class="flex flex-wrap gap-2">
<label class="inline-flex items-center">
<input
type="radio"
v-model="localNodeData.assignmentType"
value="user"
class="form-radio"
@change="saveChanges"
/>
<span class="ml-2">User</span>
</label>
<label class="inline-flex items-center">
<input
type="radio"
v-model="localNodeData.assignmentType"
value="role"
class="form-radio"
@change="saveChanges"
/>
<span class="ml-2">Role</span>
</label>
<label class="inline-flex items-center">
<input
type="radio"
v-model="localNodeData.assignmentType"
value="variable"
class="form-radio"
@change="saveChanges"
/>
<span class="ml-2">Process Variable</span>
</label>
</div>
</div>
<!-- User Assignment -->
<div v-if="localNodeData.assignmentType === 'user'" class="form-group mb-4">
<label class="form-label">Assign to Users</label>
<div v-if="loading.users" class="py-2 text-gray-500 text-sm">
Loading users...
</div>
<div v-else-if="users.length === 0" class="py-2 text-gray-500 text-sm">
No users available
</div>
<div v-else class="max-h-60 overflow-y-auto border rounded-md divide-y">
<div
v-for="user in users"
:key="user.userID"
class="p-2 hover:bg-gray-50 flex items-center justify-between"
>
<div class="flex items-center">
<input
type="checkbox"
:id="`user-${user.userID}`"
:value="user.userID"
v-model="localNodeData.assignedUsers"
class="form-checkbox"
@change="saveChanges"
/>
<label :for="`user-${user.userID}`" class="ml-2 cursor-pointer flex-1">
<div class="font-medium text-sm">{{ user.userFullName || user.userUsername }}</div>
<div class="text-xs text-gray-500">{{ user.userEmail }}</div>
</label>
</div>
</div>
</div>
</div>
<!-- Role Assignment -->
<div v-if="localNodeData.assignmentType === 'role'" class="form-group mb-4">
<label class="form-label">Assign to Roles</label>
<div v-if="loading.roles" class="py-2 text-gray-500 text-sm">
Loading roles...
</div>
<div v-else-if="roles.length === 0" class="py-2 text-gray-500 text-sm">
No roles available
</div>
<div v-else class="max-h-60 overflow-y-auto border rounded-md divide-y">
<div
v-for="role in roles"
:key="role.roleID"
class="p-2 hover:bg-gray-50 flex items-center justify-between"
>
<div class="flex items-center">
<input
type="checkbox"
:id="`role-${role.roleID}`"
:value="role.roleID"
v-model="localNodeData.assignedRoles"
class="form-checkbox"
@change="saveChanges"
/>
<label :for="`role-${role.roleID}`" class="ml-2 cursor-pointer flex-1">
<div class="font-medium text-sm">{{ role.roleName }}</div>
<div class="text-xs text-gray-500">{{ role.roleDescription }}</div>
</label>
</div>
</div>
</div>
</div>
<!-- Variable Assignment -->
<div v-if="localNodeData.assignmentType === 'variable'" class="form-group mb-4">
<label class="form-label">Assignee Variable</label>
<select
v-model="localNodeData.assigneeVariable"
class="form-control"
@change="saveChanges"
>
<option value="" disabled>Select a variable</option>
<option
v-for="variable in availableVariables"
:key="variable.name"
:value="variable.name"
>
{{ variable.label }}
</option>
</select>
<small class="text-gray-500 text-xs mt-1 block">
The selected variable should contain a user ID or a role ID.
</small>
</div>
<!-- Additional Task Settings -->
<div class="border-t border-gray-200 my-4 pt-4">
<h4 class="text-base font-medium mb-2">Additional Settings</h4>
<!-- Due Date -->
<div class="form-group mb-4">
<label class="form-label">Due Date</label>
<div class="grid grid-cols-2 gap-3">
<select
v-model="localNodeData.dueDateType"
class="form-control"
@change="saveChanges"
>
<option value="none">No due date</option>
<option value="fixed">Fixed duration</option>
<option value="variable">From variable</option>
</select>
<div v-if="localNodeData.dueDateType === 'fixed'">
<input
type="number"
v-model="localNodeData.dueDateDays"
class="form-control"
placeholder="Days"
min="0"
@blur="saveChanges"
/>
</div>
<div v-if="localNodeData.dueDateType === 'variable'">
<select
v-model="localNodeData.dueDateVariable"
class="form-control"
@change="saveChanges"
>
<option value="" disabled>Select a variable</option>
<option
v-for="variable in availableVariables"
:key="variable.name"
:value="variable.name"
>
{{ variable.label }}
</option>
</select>
</div>
</div>
</div>
<!-- Priority -->
<div class="form-group mb-4">
<label class="form-label">Priority</label>
<select
v-model="localNodeData.priority"
class="form-control"
@change="saveChanges"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
<!-- Instructions -->
<div class="form-group mb-4">
<label class="form-label">Task Instructions</label>
<textarea
v-model="localNodeData.instructions"
class="form-control"
rows="3"
placeholder="Instructions for the assignee"
@blur="saveChanges"
></textarea>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { useVariableStore } from '@/stores/variableStore';
const props = defineProps({
nodeData: {
type: Object,
required: true
},
availableVariables: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['update']);
// Local state
const users = ref([]);
const roles = ref([]);
const loading = ref({
users: false,
roles: false
});
// Local state for node data
const localNodeData = ref({
label: 'Task',
description: '',
assignmentType: 'user', // 'user', 'role', or 'variable'
assignedUsers: [],
assignedRoles: [],
assigneeVariable: '',
dueDateType: 'none', // 'none', 'fixed', or 'variable'
dueDateDays: 3,
dueDateVariable: '',
priority: 'medium',
instructions: ''
});
// Get the variable store for variables
const variableStore = useVariableStore();
// Watch for changes from parent props
watch(() => props.nodeData, (newNodeData) => {
if (newNodeData) {
// Create a deep copy to break reactivity chains with parent
localNodeData.value = {
label: newNodeData.label || 'Task',
description: newNodeData.description || '',
assignmentType: newNodeData.assignmentType || 'user',
assignedUsers: Array.isArray(newNodeData.assignedUsers) ? [...newNodeData.assignedUsers] : [],
assignedRoles: Array.isArray(newNodeData.assignedRoles) ? [...newNodeData.assignedRoles] : [],
assigneeVariable: newNodeData.assigneeVariable || '',
dueDateType: newNodeData.dueDateType || 'none',
dueDateDays: newNodeData.dueDateDays || 3,
dueDateVariable: newNodeData.dueDateVariable || '',
priority: newNodeData.priority || 'medium',
instructions: newNodeData.instructions || ''
};
}
}, { immediate: true, deep: true });
// Fetch users and roles when component is mounted
onMounted(async () => {
await fetchUsers();
await fetchRoles();
});
// Fetch users from API
async function fetchUsers() {
loading.value.users = true;
try {
const response = await fetch('/api/users');
const result = await response.json();
if (result.success && Array.isArray(result.users)) {
users.value = result.users;
} else {
console.error('Error fetching users:', result.error || 'Unknown error');
users.value = [];
}
} catch (error) {
console.error('Error fetching users:', error);
users.value = [];
} finally {
loading.value.users = false;
}
}
// Fetch roles from API
async function fetchRoles() {
loading.value.roles = true;
try {
const response = await fetch('/api/roles');
const result = await response.json();
if (result.success && Array.isArray(result.roles)) {
roles.value = result.roles;
} else {
console.error('Error fetching roles:', result.error || 'Unknown error');
roles.value = [];
}
} catch (error) {
console.error('Error fetching roles:', error);
roles.value = [];
} finally {
loading.value.roles = false;
}
}
// Save changes by emitting them to parent
function saveChanges() {
// Create a clean copy of the data to avoid reactivity issues
const nodeDataCopy = JSON.parse(JSON.stringify(localNodeData.value));
// Emit the updated data to parent
emit('update', nodeDataCopy);
}
</script>
<style scoped>
/* .task-node-configuration {
padding: 1rem;
background-color: #f8f8f8;
} */
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-control {
width: 100%;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
line-height: 1.5;
border: 1px solid #ced4da;
border-radius: 0.25rem;
}
.form-checkbox {
width: 1rem;
height: 1rem;
cursor: pointer;
}
.form-radio {
width: 1rem;
height: 1rem;
cursor: pointer;
}
</style>

View File

@ -8,9 +8,9 @@
<Icon name="material-symbols:data-object" class="text-2xl" />
</div>
<div>
<h3 class="text-lg font-medium text-gray-900">Process Variables</h3>
<h3 class="text-lg font-medium text-gray-900">Variables</h3>
<p class="mt-1 text-sm text-gray-500">
Define and manage global variables to store and pass data within your process
Define and manage variables to store and pass data within your process
</p>
</div>
</div>
@ -75,46 +75,91 @@
</div>
<!-- Variable List -->
<div v-else-if="filteredVariables.length" class="space-y-3">
<div v-else-if="filteredVariables.length" class="space-y-4">
<div
v-for="variable in filteredVariables"
:key="variable.name"
class="variable-item"
class="variable-item group"
>
<div
class="flex items-center justify-between p-4 bg-white rounded-lg border border-gray-200 hover:border-blue-200 hover:shadow-sm transition-all duration-200"
class="bg-white rounded-xl border border-gray-200 hover:border-blue-300 hover:shadow-md transition-all duration-300 overflow-hidden"
>
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="font-medium text-gray-900">{{ variable.name }}</span>
<RsBadge :variant="getTypeColor(variable.type)" size="sm">
<!-- Header with Name and Actions -->
<div class="flex items-center justify-between p-5 pb-3">
<div class="flex items-center gap-3 flex-1 min-w-0">
<!-- Variable Icon -->
<div class="flex-shrink-0 w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center">
<Icon
:name="getVariableIcon(variable.type)"
class="w-5 h-5 text-blue-600"
/>
</div>
<!-- Variable Name -->
<div class="flex-1 min-w-0">
<h4 class="text-lg font-semibold text-gray-900 truncate">
{{ variable.name }}
</h4>
<div class="flex items-center gap-2 mt-1">
<RsBadge :variant="getTypeColor(variable.type)" size="sm" class="font-medium">
{{ variable.type }}
</RsBadge>
</div>
<p v-if="variable.description" class="mt-1 text-sm text-gray-500">
{{ variable.description }}
</p>
<div v-if="variable.value !== undefined" class="mt-2 text-xs font-mono bg-gray-50 p-2 rounded border border-gray-200 max-w-md truncate">
{{ formatValue(variable.value, variable.type) }}
</div>
</div>
<div class="flex items-center gap-2 ml-4">
<!-- Action Buttons -->
<div class="flex items-center gap-1 opacity-60 group-hover:opacity-100 transition-opacity">
<button
@click="editVariable(variable)"
class="p-2 text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded-md transition-colors"
class="p-2.5 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Edit variable"
>
<Icon name="material-symbols:edit" class="w-4 h-4" />
</button>
<button
@click="deleteVariable(variable)"
class="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-md transition-colors"
class="p-2.5 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Delete variable"
>
<Icon name="material-symbols:delete" class="w-4 h-4" />
</button>
</div>
</div>
<!-- Description -->
<div v-if="variable.description" class="px-5 pb-3">
<p class="text-sm text-gray-600 leading-relaxed">
{{ variable.description }}
</p>
</div>
<!-- Current Value (if set) -->
<div v-if="variable.value !== undefined && variable.value !== ''" class="px-5 pb-4">
<div class="bg-gray-50 rounded-lg p-3 border border-gray-100">
<div class="flex items-center gap-2 mb-2">
<Icon name="material-symbols:code" class="w-4 h-4 text-gray-500" />
<span class="text-xs font-medium text-gray-500 uppercase tracking-wide">Current Value</span>
</div>
<div class="font-mono text-sm text-gray-800 break-all">
{{ formatValue(variable.value, variable.type) }}
</div>
</div>
</div>
<!-- Default Value (if no current value but has default) -->
<div v-else-if="variable.defaultValue !== undefined && variable.defaultValue !== ''" class="px-5 pb-4">
<div class="bg-amber-50 rounded-lg p-3 border border-amber-100">
<div class="flex items-center gap-2 mb-2">
<Icon name="material-symbols:settings" class="w-4 h-4 text-amber-600" />
<span class="text-xs font-medium text-amber-600 uppercase tracking-wide">Default Value</span>
</div>
<div class="font-mono text-sm text-amber-800 break-all">
{{ formatValue(variable.defaultValue, variable.type) }}
</div>
</div>
</div>
</div>
</div>
</div>
@ -239,8 +284,9 @@ const variableTypes = [
// Computed
const variables = computed(() => {
// Only return global variables
return variableStore.getAllVariables.global;
// Return all variables (treating everything as global)
const allVars = variableStore.getAllVariables;
return [...allVars.global, ...allVars.process];
});
// Filtered variables based on search query
@ -298,7 +344,7 @@ const saveVariable = async (formData) => {
variableStore.updateVariable(
editingVariable.value.name,
newVariable,
"global"
'global'
);
} else {
// Add new variable
@ -316,14 +362,14 @@ const saveVariable = async (formData) => {
// Get badge color based on variable type
const getTypeColor = (type) => {
switch (type) {
case 'string': return 'blue';
case 'string': return 'info';
case 'int':
case 'decimal': return 'purple';
case 'object': return 'emerald';
case 'decimal': return 'primary';
case 'object': return 'success';
case 'datetime':
case 'date': return 'amber';
case 'boolean': return 'indigo';
default: return 'gray';
case 'date': return 'warning';
case 'boolean': return 'secondary';
default: return 'secondary';
}
};
@ -344,6 +390,27 @@ const formatValue = (value, type) => {
return String(value);
}
};
// Get icon based on variable type
const getVariableIcon = (type) => {
switch (type) {
case 'string':
return 'material-symbols:text-fields';
case 'int':
case 'decimal':
return 'material-symbols:pin';
case 'boolean':
return 'material-symbols:toggle-on';
case 'date':
return 'material-symbols:calendar-today';
case 'datetime':
return 'material-symbols:schedule';
case 'object':
return 'material-symbols:data-object';
default:
return 'material-symbols:data-object';
}
};
</script>
<style scoped>

View File

@ -54,6 +54,24 @@ Last updated: December 2024
## Recent Updates
### December 2024 - Major Process Builder Enhancements
- **Enhanced Form Node Configuration**: Complete redesign of form task configuration with step-by-step workflow
- **Input/Output Mappings**: Bidirectional data flow between process variables and form fields
- **Field Conditions**: Dynamic field behavior based on process variables (readonly, hidden, required states)
- **4-Point Connection System**: All nodes now have 4 connection points (top, bottom, left, right) to prevent edge overlaps
- **Improved Visual Design**: Better handle styling with hover effects and connection state feedback
- **Auto-Save Mechanism**: Reliable data persistence for form configurations and mappings
- **Variable Integration**: Seamless integration with process variables for form pre-filling and data capture
- **Conditional Logic**: Support for complex field conditions with multiple operators and actions
- **Process Settings Management**: Comprehensive process configuration with 5 organized tabs:
- **Process Info**: Name, description, priority, category, ownership management
- **Execution Settings**: Process type, timeouts, parallel execution, error recovery
- **Variables & Data**: Data persistence policies, logging, encryption, retention controls
- **Permissions**: Role-based access control, execution permissions, approval workflows
- **JSON Export**: Complete configuration export with metadata for API integration
- **Advanced Configuration Options**: Professional-grade settings comparable to enterprise BPM platforms
- **Enhanced State Management**: Improved process store with settings persistence and history tracking
### December 2024 - Major Form Builder Enhancements
- **JavaScript Execution Engine**: Added FormScriptEngine component for real-time calculations
- **Dynamic Field Updates**: Implemented onFieldChange and onLoad event handlers

View File

@ -3,6 +3,58 @@
## Overview
This document outlines the planned improvements for the Process Builder core components. The improvements are designed to be manageable and maintainable while adding essential functionality.
## Recently Completed Features ✅
### December 2024 - Process Settings Management
**Status: Completed** ✅
Implemented comprehensive process configuration management with a professional-grade settings interface:
- **Process Info Tab**: Complete process metadata management including name, description, priority levels, categorization, and ownership assignment
- **Execution Settings Tab**: Advanced process execution controls including process types (standard, approval, data collection, automation, review), timeout management, parallel execution, error recovery, and notification settings
- **Variables & Data Tab**: Data persistence policies with multiple retention options (session, temporary, short-term, long-term, permanent), variable change logging, sensitive data encryption, and compliance-ready retention policies
- **Permissions Tab**: Enterprise-grade access control with execution permissions, role-based access, modification permissions, approval workflows, and comprehensive audit trails
- **JSON Export Tab**: Complete configuration export functionality with metadata, copy-to-clipboard, and download capabilities for API integration and backup purposes
**Technical Implementation:**
- Fully integrated with existing process builder store
- Reactive state management with local change tracking
- Professional UI with tabbed interface and validation
- Comprehensive TypeScript interfaces for settings structure
- Future-ready API integration patterns documented
### December 2024 - Enhanced Form Node Configuration
**Status: Completed** ✅
Completely redesigned form task configuration with step-by-step workflow:
- **3-Step Configuration Process**: Form selection, data mapping, and field conditions
- **Bidirectional Data Mapping**: Input and output variable mapping between process and forms
- **Dynamic Field Conditions**: Complex conditional logic for field behavior (readonly, hidden, required, optional)
- **Professional UI Design**: Consistent with enterprise BPM standards
- **Auto-Save Mechanisms**: Reliable data persistence throughout configuration
### December 2024 - 4-Point Connection System
**Status: Completed** ✅
Enhanced node connection system to prevent edge overlaps and improve visual clarity:
- **Universal 4-Point System**: All nodes support top, bottom, left, right connection points
- **Intelligent Handle Display**: Handles appear on hover with smooth transitions
- **Color-Coded Connections**: Green for outputs, blue for inputs
- **Smart Edge Routing**: Automatic routing to prevent visual overlaps
- **Enhanced User Experience**: Improved connection feedback and visual indicators
### December 2024 - Variable System Enhancements
**Status: Completed** ✅
Improved variable management with simplified scope handling:
- **Unified Variable Management**: Streamlined global variable system
- **Enhanced Variable Manager UI**: Professional interface with search, filtering, and type indicators
- **Template Integration**: Variables properly loaded from process templates
- **Reactive Updates**: Real-time synchronization between variable manager and node configurations
## Variable System
### 1. Global Variables

File diff suppressed because it is too large Load Diff

View File

@ -61,11 +61,18 @@ Activities represent work performed in a process:
- **Form Task** (Emerald Icon)
- A task that requires form data
- Has both input and output handles
- Step-by-step configuration workflow
- Properties: Form selection, data mapping
- Has 4 connection points: top, bottom, left, right handles
- **Enhanced Configuration**: Complete 3-step configuration workflow
- **Step 1 - Form Selection**: Choose existing forms or create new ones
- **Step 2 - Data Mapping**: Configure bidirectional data flow
- **Input Mappings**: Map process variables to pre-fill form fields
- **Output Mappings**: Capture form submission data in process variables
- **Step 3 - Field Conditions**: Dynamic field behavior based on process state
- Make fields readonly, hidden, required, or optional
- Support for complex conditional logic with multiple operators
- Real-time form adaptations based on process variables
- Connects to forms created in the Form Builder
- Visualizes bidirectional data flow
- Visualizes bidirectional data flow with color-coded mapping sections
- **API Task** (Indigo Icon)
- Automated task that makes API calls
@ -160,13 +167,29 @@ The Variable Manager provides a central location to define and manage global pro
- **Control**: Multi-select nodes
### Connection Points
- **Input Handle**: Located at the top of nodes (except Start)
- **Output Handle**: Located at the bottom of nodes (except End)
- **Creating Connections**:
1. Hover over a node to see handles
2. Click and drag from a handle
3. Drop on another node's handle
4. Connection automatically styles based on type
All nodes now feature **4 connection points** for maximum flexibility:
- **Top Handle**: Primary input connection point
- **Right Handle**: Secondary output connection point
- **Bottom Handle**: Primary output connection point
- **Left Handle**: Secondary input connection point
**Special Cases**:
- **Start Nodes**: Only have output handles (right + bottom)
- **End Nodes**: Only have input handles (top + left)
**Enhanced Visibility**:
- Handles are invisible by default for clean appearance
- Handles become visible when hovering over nodes
- Handles are highlighted during connection mode
- Color-coded: Green for outputs, Blue for inputs
- Smooth transitions and hover effects for better user experience
**Creating Connections**:
1. Hover over a node to reveal handles
2. Click and drag from any handle
3. Drop on another node's compatible handle
4. Connection automatically routes to prevent overlaps
5. Use specific handles to control connection positioning
## Process Properties
@ -186,23 +209,397 @@ The Variable Manager provides a central location to define and manage global pro
- Role-based assignments
- Group assignments
## Form Integration
## Process Settings
The Process Builder integrates with the Form Builder to allow forms to be attached to process tasks:
The Process Settings feature provides comprehensive configuration options for managing all aspects of your business process. Access Process Settings through the dropdown menu in the header by clicking the three-dot menu and selecting "Process Settings".
### Accessing Process Settings
1. **From the Header Menu**
- Click the three-dot menu (⋮) in the top-right of the Process Builder
- Select "Process Settings" from the dropdown menu
- The settings modal opens with tabbed configuration options
2. **When to Use Process Settings**
- Configure process metadata and ownership
- Set execution parameters and timeouts
- Define data persistence and security policies
- Manage permissions and access control
- Export process configuration for integration
### Settings Tabs Overview
The Process Settings modal is organized into five comprehensive tabs:
#### 1. Process Info Tab 📋
Configure basic process information and ownership:
**Process Details**:
- **Process Name**: The display name for your process
- **Process Description**: Detailed description of what the process accomplishes
- **Priority Level**: Set process priority (Low, Normal, High, Critical)
- **Process Category**: Department or functional area (e.g., HR, Finance, Operations)
- **Owner/Manager**: Person responsible for the process
**Best Practices**:
- Use descriptive names that clearly indicate the process purpose
- Include business context in descriptions
- Set appropriate priority levels to help with resource allocation
- Assign clear ownership for accountability
#### 2. Execution Settings Tab ⚙️
Control how your process executes and handles different scenarios:
**Process Type Configuration**:
- **Standard Process**: Regular business workflow
- **Approval Workflow**: Multi-step approval process
- **Data Collection**: Information gathering process
- **Automated Task**: System-driven automation
- **Review Process**: Quality assurance or compliance review
**Timing and Performance**:
- **Max Execution Time**: Maximum time allowed for process completion (1-10,080 minutes)
- **Auto-Timeout**: Automatically timeout after specified hours (1-168 hours)
- **Allow Parallel Execution**: Enable multiple instances to run simultaneously
- **Enable Error Recovery**: Automatically retry failed steps before stopping
**Notifications**:
- **Send Completion Notifications**: Notify stakeholders when process completes
**Example Configuration**:
```
Process Type: Approval Workflow
Max Execution Time: 480 minutes (8 hours)
Auto-Timeout: 72 hours (3 days)
Parallel Execution: Enabled
Error Recovery: Enabled
Notifications: Enabled
```
#### 3. Variables & Data Tab 🗄️
Configure data handling and variable persistence:
**Data Persistence Options**:
- **Session Only**: Data deleted when session ends
- **Temporary (24 hours)**: Data retained for 24 hours
- **Short Term (7 days)**: Data retained for one week
- **Long Term (30 days)**: Data retained for one month
- **Permanent**: Data retained indefinitely
**Security and Compliance**:
- **Log Variable Changes**: Track all variable modifications during execution
- **Encrypt Sensitive Data**: Automatically encrypt variables marked as sensitive
- **Data Retention Policy**: Custom policy description for compliance requirements
**Best Practices**:
- Choose appropriate persistence based on business and legal requirements
- Enable variable change logging for audit trails
- Document retention policies clearly for compliance
- Consider data privacy regulations (GDPR, CCPA) when setting retention
#### 4. Permissions Tab 🔒
Define access control and security settings:
**Execution Permissions**:
- **Anyone**: Public access (use with caution)
- **Authenticated Users**: Any logged-in user
- **Specific Roles**: Define custom role-based access
- **Process Managers Only**: Restrict to process managers
- **Administrators Only**: Admin-only access
**Modification Permissions**:
- **Process Owner Only**: Original creator only
- **Process Managers**: Designated process managers
- **Administrators**: System administrators
- **Anyone with Edit Rights**: Users with edit permissions
**Advanced Security**:
- **Require Approval for Changes**: Changes must be approved before taking effect
- **Enable Audit Trail**: Detailed logs of all access and modifications
**Role-Based Configuration Example**:
```
Execution Permission: Specific Roles
Allowed Roles: hr_manager, hr_admin, department_head
Modification Permission: Process Managers
Require Approval: Enabled
Audit Trail: Enabled
```
#### 5. JSON Export Tab 📊
Export complete process configuration for developers and system integration:
**Export Features**:
- **Process Metadata**: Complete process information and settings
- **Workflow Structure**: All nodes and edges with their configurations
- **Variables**: All process variables and their definitions
- **Permissions**: Access control and security settings
- **Timestamps**: Creation and modification dates
**Export Options**:
- **Copy to Clipboard**: Quick copy for immediate use
- **Download JSON**: Save complete configuration file
- **Developer Integration**: Use exported JSON for API integration or backup
**Export Data Structure**:
```json
{
"processInfo": {
"id": "process-uuid",
"name": "Customer Onboarding",
"description": "Complete customer onboarding workflow",
"priority": "high",
"category": "Sales",
"owner": "Sales Manager"
},
"settings": {
"processType": "approval",
"maxExecutionTime": 480,
"allowParallel": true,
"enableErrorRecovery": true
},
"workflow": {
"nodes": [...],
"edges": [...]
},
"variables": {...},
"permissions": {...},
"metadata": {
"exportedAt": "2024-12-20T10:30:00Z"
}
}
```
### Common Process Settings Scenarios
#### Scenario 1: HR Onboarding Process
```
Process Type: Standard Process
Priority: High
Max Execution Time: 1440 minutes (24 hours)
Data Persistence: Long Term (30 days)
Execution Permission: HR Managers
Audit Trail: Enabled
```
#### Scenario 2: Financial Approval Workflow
```
Process Type: Approval Workflow
Priority: Critical
Auto-Timeout: 48 hours
Data Persistence: Permanent
Execution Permission: Finance Team
Require Approval: Enabled for changes
```
#### Scenario 3: Customer Support Automation
```
Process Type: Automated Task
Priority: Normal
Parallel Execution: Enabled
Data Persistence: Short Term (7 days)
Execution Permission: Support Staff
Error Recovery: Enabled
```
### Process Settings Best Practices
#### Planning and Design
1. **Define Clear Ownership**
- Assign process owners for accountability
- Document roles and responsibilities
- Regular review and updates
2. **Set Appropriate Permissions**
- Follow principle of least privilege
- Use role-based access control
- Regular permission audits
3. **Configure Realistic Timeouts**
- Consider business requirements
- Account for user availability
- Set appropriate escalation procedures
#### Data Management
1. **Choose Appropriate Persistence**
- Balance performance with compliance needs
- Consider storage costs
- Document retention policies
2. **Implement Security Measures**
- Enable audit trails for sensitive processes
- Use encryption for PII data
- Regular security reviews
#### Monitoring and Maintenance
1. **Regular Settings Review**
- Quarterly configuration reviews
- Update settings as requirements change
- Monitor performance and adjust timeouts
2. **Documentation Updates**
- Keep process descriptions current
- Document any changes made
- Maintain version history
### Troubleshooting Process Settings
#### Common Issues
1. **Settings Not Saving**
- Verify all required fields are completed
- Check permission levels
- Ensure network connectivity
2. **Permission Errors**
- Verify user roles and permissions
- Check with administrator if needed
- Review audit trail for access attempts
3. **Export/Import Issues**
- Validate JSON format
- Check file size limits
- Verify browser clipboard permissions
#### Getting Help
- Review error messages for specific guidance
- Check the audit trail for permission issues
- Contact administrators for role-related problems
- Use the JSON export for debugging configuration issues
## Advanced Form Integration
The Process Builder features sophisticated integration with the Form Builder, enabling complex data flows and dynamic form behavior.
### Adding and Configuring Form Tasks
1. **Adding a Form Task**
- Drag a Form Task component onto the canvas
- Select the task to open its properties
- Drag a Form Task component (emerald icon) onto the canvas
- Connect it to your process flow using the 4-point connection system
- Click "Configure Form Task" to open the configuration modal
2. **Selecting a Form**
- In the properties panel, use the Form Selector to choose a form
- Forms are listed from those created in the Form Builder
- Selected forms will be displayed to users when they reach this task
2. **Step 1: Form Selection**
- **Choose Existing Form**: Browse and select from available forms
- **Form Preview**: See form structure and field details
- **Create New Form**: Direct integration with Form Builder
- The selected form determines available fields for mapping
3. **Form Data in Process**
- Form submissions become available as process variables
- Data can be referenced in gateway conditions
- Form fields can be pre-populated with process data
3. **Step 2: Form Data Mapping**
Configure bidirectional data flow between your process and the form:
**Input Variables (Process → Form)**
- Map process variables to pre-fill form fields
- Users see pre-populated data when the form loads
- Useful for displaying existing customer data, calculated values, or previous submissions
- Example: Map `customer.email` process variable to "Email Address" form field
**Output Variables (Form → Process)**
- Capture form submission data in process variables
- Create new variables automatically from form fields
- Use captured data in subsequent process steps
- Example: Map "Order Total" form field to `order.total` process variable
4. **Step 3: Field Conditions**
Create dynamic form behavior based on process state:
**Condition Components**:
- **Process Variable**: The variable to evaluate
- **Operator**: Comparison type (equals, not equals, greater than, etc.)
- **Value**: Comparison value (when applicable)
- **Target Field**: The form field to control
- **Action**: What to do when condition is met
**Available Actions**:
- **Make Readonly**: Field displays value but cannot be edited
- **Hide Field**: Field is completely hidden from user
- **Make Required**: Field becomes mandatory for submission
- **Make Optional**: Field becomes optional
- **Show Field**: Field becomes visible (opposite of hide)
- **Enable Field**: Field becomes editable (opposite of readonly)
**Operators**:
- **Equals/Not Equals**: Exact value comparison
- **Is True/Is False**: Boolean value checks
- **Is Empty/Is Not Empty**: Check for presence of data
- **Contains**: Text substring matching
- **Greater Than/Less Than**: Numeric comparisons
### Example Use Cases
**Customer Onboarding Process**:
```
Input Mapping:
- customer.type → "Customer Type" field (pre-select existing customers)
Output Mapping:
- "Company Name" field → customer.company
- "Industry" field → customer.industry
Field Conditions:
- IF customer.type = "Existing" THEN "Company Name" = Readonly
- IF customer.type = "Enterprise" THEN "Dedicated Rep" = Required
```
**Expense Approval Process**:
```
Input Mapping:
- employee.name → "Employee Name" field
- expense.amount → "Amount" field
Output Mapping:
- "Justification" field → expense.justification
- "Manager Approval" field → expense.approved
Field Conditions:
- IF expense.amount > 1000 THEN "Manager Approval" = Required
- IF employee.level = "Executive" THEN "Amount" = Readonly
```
**Support Ticket Process**:
```
Input Mapping:
- ticket.priority → "Priority" field
- customer.tier → "Customer Tier" field
Output Mapping:
- "Resolution" field → ticket.resolution
- "Satisfaction Rating" field → ticket.rating
Field Conditions:
- IF customer.tier = "Premium" THEN "Priority" = Readonly AND "Escalation" = Show
- IF ticket.priority = "Critical" THEN "Manager Review" = Required
```
### Best Practices for Form Integration
1. **Data Flow Planning**
- Map the complete data journey from process start to end
- Identify which data needs to be captured vs. displayed
- Plan for data validation and transformation needs
2. **Variable Naming**
- Use consistent, descriptive variable names
- Follow a naming convention (e.g., `customer.email`, `order.total`)
- Create variables in the Variable Manager before mapping
3. **Field Condition Design**
- Start with simple conditions and build complexity gradually
- Test conditions with various scenarios
- Document complex logic for future maintenance
4. **User Experience**
- Pre-fill forms whenever possible to reduce user effort
- Use conditional logic to show only relevant fields
- Provide clear feedback when fields become readonly or required
5. **Performance Considerations**
- Avoid excessive field conditions that could slow form rendering
- Test forms with realistic data volumes
- Consider caching for frequently accessed reference data
## Best Practices
@ -258,7 +655,7 @@ The Process Builder integrates with the Form Builder to allow forms to be attach
For technical details about implementation and integration, please refer to the [Process Builder Technical Documentation](TECHNICAL_GUIDE.md).
Last updated: July 10, 2024
Last updated: December 2024
## Working with Business Rules

View File

@ -547,5 +547,11 @@ export default defineNuxtConfig({
requestSizeLimiter: false,
},
},
"/api/forms/**": {
security: {
xssValidator: false,
requestSizeLimiter: false,
},
},
},
});

View File

@ -1901,8 +1901,9 @@ const applyFormTemplate = (template) => {
formStore.setFormName(template.name);
}
// Reset the form components
// Reset the form components and form ID (to ensure this becomes a new form)
formStore.formComponents = [];
formStore.currentFormId = null; // Important: This ensures we create a new form instead of updating existing
// Add the template components
if (template.components && template.components.length > 0) {

View File

@ -14,10 +14,12 @@ import VariableManager from '~/components/process-flow/VariableManager.vue';
import { onBeforeRouteLeave } from 'vue-router';
import FormNodeConfiguration from '~/components/process-flow/FormNodeConfiguration.vue';
import FormNodeConfigurationModal from '~/components/process-flow/FormNodeConfigurationModal.vue';
import TaskNodeConfiguration from '~/components/process-flow/TaskNodeConfiguration.vue';
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 ProcessTemplatesModal from '~/components/ProcessTemplatesModal.vue';
import ProcessSettingsModal from '~/components/process-flow/ProcessSettingsModal.vue';
// Define page meta
definePageMeta({
@ -66,6 +68,9 @@ const showApiConfigModal = ref(false);
const showGatewayConfigModal = ref(false);
const showBusinessRuleConfigModal = ref(false);
const showNotificationConfigModal = ref(false);
const showTemplatesModal = ref(false);
const showProcessSettings = ref(false);
const showDropdown = ref(false);
// Component definitions
const components = [
@ -76,13 +81,6 @@ const components = [
iconColor: 'text-green-500',
data: { description: 'Process starts here' }
},
{
type: 'task',
label: 'Task',
icon: 'assignment',
iconColor: 'text-blue-500',
data: { description: 'Task node', assignee: '' }
},
{
type: 'form',
label: 'Form',
@ -494,11 +492,22 @@ onMounted(() => {
// Add the beforeunload event listener
window.addEventListener('beforeunload', handleBeforeUnload);
// Add click outside listener for dropdown
document.addEventListener('click', handleClickOutside);
});
// Handle click outside dropdown
const handleClickOutside = (event) => {
if (!event.target.closest('.dropdown')) {
showDropdown.value = false;
}
};
onUnmounted(() => {
// Remove event listeners
window.removeEventListener('beforeunload', handleBeforeUnload);
document.removeEventListener('click', handleClickOutside);
});
// Show warning if there are unsaved changes
@ -625,6 +634,111 @@ const onAddComponent = (component) => {
onNodeSelected(newNode);
};
// Handle template application
const applyProcessTemplate = async (template) => {
try {
console.log('Applying process template:', template.name);
console.log('Template nodes:', template.nodes ? template.nodes.length : 0);
console.log('Template edges:', template.edges ? template.edges.length : 0);
// Create a new process if one doesn't exist
if (!processStore.currentProcess) {
console.log('No current process, creating new one...');
processStore.createProcess(template.name, template.description || 'Process created from template');
} else {
// Confirm if there's already content in the existing process
if (processStore.currentProcess.nodes.length > 0 || processStore.currentProcess.edges.length > 0) {
if (!confirm("This will replace your current process content. Continue?")) {
return;
}
}
// Update process name if user allows
if (processStore.currentProcess.name === 'New Process' || confirm("Update the process name to match the template?")) {
processStore.currentProcess.name = template.name;
processStore.currentProcess.description = template.description;
}
}
// Clear current process nodes and edges
if (processStore.currentProcess) {
console.log('Clearing existing nodes and edges...');
processStore.currentProcess.nodes = [];
processStore.currentProcess.edges = [];
}
// Add nodes and edges together - let the canvas watchers handle the sequencing
const templateNodes = template.nodes || [];
const templateEdges = template.edges || [];
console.log('Adding template nodes:', templateNodes.length);
console.log('Adding template edges:', templateEdges.length);
// Process nodes first
templateNodes.forEach((node) => {
const newNode = {
...node,
id: node.id, // Keep original ID for edge references
label: node.data?.label || node.label || `${node.type} node`, // Set label at root level
position: node.position || { x: 100, y: 100 },
data: {
...node.data,
label: node.data?.label || node.label || `${node.type} node`
}
};
processStore.addNode(newNode);
});
// Process edges after nodes
templateEdges.forEach((edge) => {
const newEdge = {
...edge,
id: edge.id, // Keep original ID
type: edge.type || 'smoothstep',
animated: edge.animated !== undefined ? edge.animated : true
};
processStore.addEdge(newEdge);
});
// Add template variables to the variable store
if (template.variables && template.variables.length > 0) {
console.log('Adding template variables:', template.variables.length);
// Clear existing process variables first
variableStore.clearAllProcessVariables();
template.variables.forEach((variable) => {
console.log(`Adding variable: ${variable.name} (${variable.type}) with scope: ${variable.scope}`);
variableStore.addVariable({
...variable,
id: crypto.randomUUID() // Generate unique ID for the variable
});
});
}
console.log('Template application completed - nodes:', processStore.currentProcess.nodes.length, 'edges:', processStore.currentProcess.edges.length);
// Mark the process as having unsaved changes
processStore.unsavedChanges = true;
// Fit the view to show all nodes
if (processFlowCanvas.value && processFlowCanvas.value.fitView) {
nextTick(() => {
processFlowCanvas.value.fitView();
});
}
// Show success message
console.log(`Template "${template.name}" applied successfully`);
} catch (error) {
console.error('Error applying process template:', error);
alert('Failed to apply template: ' + error.message);
}
};
// Fix references to functions
const onFormSelected = (formData) => {
if (selectedNodeData.value && selectedNodeData.value.type === 'form') {
@ -681,26 +795,6 @@ const handleFormNodeUpdate = (updatedData) => {
}
};
// Add this function to handle task node updates
const handleTaskNodeUpdate = (updatedData) => {
if (selectedNodeData.value && selectedNodeData.value.type === 'task') {
// Make sure to update the label both in data and at the root level
const newLabel = updatedData.label || 'Task';
// 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
updateNodeInStore();
}
};
// Update handler for business rule node
const handleBusinessRuleUpdate = (data) => {
if (selectedNodeData.value) {
@ -727,6 +821,11 @@ const handleNotificationNodeUpdate = (updatedData) => {
updateNodeInStore();
}
};
// Navigate to variables page
const navigateToVariables = () => {
confirmNavigation('/variables');
};
</script>
<template>
@ -735,7 +834,13 @@ const handleNotificationNodeUpdate = (updatedData) => {
<header
class="bg-gray-800 px-4 py-4 flex items-center justify-between text-white shadow-md"
>
<!-- Left section - Logo and navigation -->
<div class="flex items-center gap-3">
<Icon
@click="confirmNavigation('/process-builder/manage')"
name="ph:arrow-circle-left-duotone"
class="cursor-pointer w-6 h-6"
/>
<img
src="@/assets/img/logo/logo-word-white.svg"
alt="Corrad Logo"
@ -743,29 +848,66 @@ const handleNotificationNodeUpdate = (updatedData) => {
/>
</div>
<div class="flex items-center gap-3">
<!-- Middle section - Process name -->
<div class="flex-1 flex justify-center items-center mx-4">
<FormKit
v-if="hasCurrentProcess"
v-model="processStore.currentProcess.name"
type="text"
placeholder="Enter process name"
placeholder="Process Name"
validation="required"
validation-visibility="live"
:validation-messages="{ required: 'Please enter a process name' }"
class="process-name-input max-w-md"
:classes="{
outer: 'w-64 mb-0',
input: 'w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500',
outer: 'mb-0 w-full',
}"
/>
<div v-else class="text-lg font-medium text-gray-400">No Process Selected</div>
</div>
<div class="flex items-center gap-3">
<RsButton @click="saveProcess" variant="primary" size="sm" :disabled="!hasCurrentProcess">
<!-- Right section - Actions -->
<div class="flex items-center">
<!-- Primary actions -->
<div class="flex items-center mr-2 border-r border-gray-600 pr-2">
<RsButton @click="saveProcess" variant="primary" size="sm" class="mr-2" :disabled="!hasCurrentProcess">
<Icon name="material-symbols:save" class="mr-1" />
Save Process
Save
</RsButton>
<RsButton @click="confirmNavigation('/process-builder/manage')" variant="tertiary" size="sm">
<Icon name="material-symbols:arrow-back" class="mr-1" />
Back to Processes
</div>
<!-- Templates button -->
<div class="mr-2 border-r border-gray-600 pr-2">
<RsButton @click="showTemplatesModal = true" variant="secondary" size="sm" :disabled="!hasCurrentProcess">
<Icon name="material-symbols:description-outline" class="mr-1" />
Templates
</RsButton>
</div>
<!-- Secondary actions -->
<div class="flex items-center">
<div class="dropdown relative">
<RsButton @click="showDropdown = !showDropdown" variant="tertiary" size="sm" class="flex items-center">
<Icon name="material-symbols:more-vert" class="w-5 h-5" />
</RsButton>
<div v-if="showDropdown" class="dropdown-menu absolute right-0 mt-2 bg-white rounded shadow-lg py-1 z-10 w-48 text-gray-800">
<button @click="showProcessSettings = true; showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
<Icon name="material-symbols:tune" class="mr-2 w-4 h-4" />
<span>Process Settings</span>
</button>
<button @click="navigateToVariables(); showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
<Icon name="material-symbols:data-object" class="mr-2 w-4 h-4" />
<span>Variables</span>
</button>
<button @click="confirmNavigation('/process-builder/manage'); showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
<Icon name="material-symbols:settings" class="mr-2 w-4 h-4" />
<span>Manage Processes</span>
</button>
</div>
</div>
</div>
</div>
</header>
<!-- Main Content Area -->
@ -826,13 +968,6 @@ const handleNotificationNodeUpdate = (updatedData) => {
</div>
<!-- Node Type Specific Properties -->
<div v-if="selectedNodeData.type === 'task'">
<TaskNodeConfiguration
:nodeData="selectedNodeData.data"
:availableVariables="variableStore.getAllVariables.global"
@update="handleTaskNodeUpdate"
/>
</div>
<!-- Form Selection for Form Nodes -->
<div v-if="selectedNodeData.type === 'form'">
@ -983,6 +1118,17 @@ const handleNotificationNodeUpdate = (updatedData) => {
:availableVariables="gatewayAvailableVariables"
@update="handleNotificationNodeUpdate"
/>
<!-- Process Templates Modal -->
<ProcessTemplatesModal
v-model="showTemplatesModal"
@select-template="applyProcessTemplate"
/>
<!-- Process Settings Modal -->
<ProcessSettingsModal
v-model="showProcessSettings"
/>
</div>
</template>
@ -1072,11 +1218,15 @@ const handleNotificationNodeUpdate = (updatedData) => {
background-color: #f8fafc;
}
.process-name-input {
width: 100%;
max-width: 400px;
}
.process-name-input :deep(.formkit-inner) {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
color: white;
min-width: 200px;
}
.process-name-input :deep(.formkit-inner:focus-within) {

View File

@ -0,0 +1,91 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default defineEventHandler(async (event) => {
try {
const formId = event.context.params.formId;
if (!formId) {
throw createError({
statusCode: 400,
statusMessage: 'Form ID is required'
});
}
// Fetch the form from database
const form = await prisma.form.findFirst({
where: {
OR: [
{ formID: parseInt(formId) || -1 },
{ formUUID: formId }
]
},
select: {
formID: true,
formUUID: true,
formName: true,
formComponents: true
}
});
if (!form) {
throw createError({
statusCode: 404,
statusMessage: 'Form not found'
});
}
// Extract form fields from the form components
const formFields = [];
if (form.formComponents && Array.isArray(form.formComponents)) {
form.formComponents.forEach(component => {
// Only include components that have names (actual form fields)
if (component.props && component.props.name) {
// Filter out non-input components
const inputTypes = [
'text', 'textarea', 'number', 'email', 'password', 'url', 'tel',
'select', 'checkbox', 'radio', 'switch', 'date', 'time',
'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone',
'mask', 'hidden'
];
if (inputTypes.includes(component.type)) {
formFields.push({
name: component.props.name,
label: component.props.label || component.props.name,
type: component.type,
required: component.props.validation && component.props.validation.includes('required'),
description: component.props.help || ''
});
}
}
});
}
return {
success: true,
form: {
id: form.formID,
uuid: form.formUUID,
name: form.formName
},
fields: formFields
};
} catch (error) {
console.error('Error fetching form fields:', error);
if (error.statusCode) {
throw error;
}
throw createError({
statusCode: 500,
statusMessage: 'Failed to fetch form fields'
});
} finally {
await prisma.$disconnect();
}
});

View File

@ -35,6 +35,23 @@ export default defineEventHandler(async (event) => {
updateData.formStatus = body.status;
}
// Add the missing custom script and styling fields
if (body.customScript !== undefined) {
updateData.customScript = body.customScript;
}
if (body.customCSS !== undefined) {
updateData.customCSS = body.customCSS;
}
if (body.formEvents !== undefined) {
updateData.formEvents = body.formEvents;
}
if (body.scriptMode !== undefined) {
updateData.scriptMode = body.scriptMode;
}
// Try to update by UUID first
let form;
try {

View File

@ -0,0 +1,91 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default defineEventHandler(async (event) => {
try {
const formId = event.context.params.id;
if (!formId) {
throw createError({
statusCode: 400,
statusMessage: 'Form ID is required'
});
}
// Fetch the form from database
const form = await prisma.form.findFirst({
where: {
OR: [
{ formID: parseInt(formId) || -1 },
{ formUUID: formId }
]
},
select: {
formID: true,
formUUID: true,
formName: true,
formComponents: true
}
});
if (!form) {
throw createError({
statusCode: 404,
statusMessage: 'Form not found'
});
}
// Extract form fields from the form components
const formFields = [];
if (form.formComponents && Array.isArray(form.formComponents)) {
form.formComponents.forEach(component => {
// Only include components that have names (actual form fields)
if (component.props && component.props.name) {
// Filter out non-input components
const inputTypes = [
'text', 'textarea', 'number', 'email', 'password', 'url', 'tel',
'select', 'checkbox', 'radio', 'switch', 'date', 'time',
'datetime-local', 'range', 'color', 'file', 'otp', 'dropzone',
'mask', 'hidden'
];
if (inputTypes.includes(component.type)) {
formFields.push({
name: component.props.name,
label: component.props.label || component.props.name,
type: component.type,
required: component.props.validation && component.props.validation.includes('required'),
description: component.props.help || ''
});
}
}
});
}
return {
success: true,
form: {
id: form.formID,
uuid: form.formUUID,
name: form.formName
},
fields: formFields
};
} catch (error) {
console.error('Error fetching form fields:', error);
if (error.statusCode) {
throw error;
}
throw createError({
statusCode: 500,
statusMessage: 'Failed to fetch form fields'
});
} finally {
await prisma.$disconnect();
}
});

View File

@ -127,6 +127,22 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
}
},
/**
* Update the current process with new data
*/
updateCurrentProcess(processUpdates) {
if (!this.currentProcess) return;
this.currentProcess = {
...this.currentProcess,
...processUpdates,
updatedAt: new Date().toISOString()
};
this.unsavedChanges = true;
this.saveToHistory('Update process settings');
},
/**
* Save the current process
*/

View File

@ -37,14 +37,14 @@ export const useVariableStore = defineStore('variables', {
actions: {
// Add a new variable
addVariable(variable) {
// Set scope to global by default
// Respect the scope passed in, default to global if not specified
const scope = variable.scope || 'global';
const updatedVariable = {
...variable,
scope: 'global'
scope: scope
};
// For backward compatibility, still respect the scope if it's explicitly set
if (variable.scope === 'process') {
if (scope === 'process') {
this.processVariables.push(updatedVariable);
} else {
this.globalVariables.push(updatedVariable);
@ -109,6 +109,11 @@ export const useVariableStore = defineStore('variables', {
input: [],
output: []
};
},
// Clear all process variables specifically (for template application)
clearAllProcessVariables() {
this.processVariables = [];
}
}
});