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:
parent
72c61184ae
commit
fa90c234c5
1073
components/ProcessTemplatesModal.vue
Normal file
1073
components/ProcessTemplatesModal.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
return 'Enter 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
|
||||
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-if="condition.valueType !== 'boolean'"
|
||||
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>
|
||||
|
@ -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: {
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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 */
|
||||
|
532
components/process-flow/ProcessSettingsModal.vue
Normal file
532
components/process-flow/ProcessSettingsModal.vue
Normal 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>
|
@ -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>
|
@ -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,44 +75,89 @@
|
||||
</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">
|
||||
{{ variable.type }}
|
||||
</RsBadge>
|
||||
<!-- 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>
|
||||
</div>
|
||||
</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) }}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center gap-1 opacity-60 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
@click="editVariable(variable)"
|
||||
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.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>
|
||||
<div class="flex items-center gap-2 ml-4">
|
||||
<button
|
||||
@click="editVariable(variable)"
|
||||
class="p-2 text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded-md 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"
|
||||
title="Delete variable"
|
||||
>
|
||||
<Icon name="material-symbols:delete" class="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<!-- 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>
|
||||
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
@ -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
|
||||
|
||||
|
@ -547,5 +547,11 @@ export default defineNuxtConfig({
|
||||
requestSizeLimiter: false,
|
||||
},
|
||||
},
|
||||
"/api/forms/**": {
|
||||
security: {
|
||||
xssValidator: false,
|
||||
requestSizeLimiter: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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,28 +848,65 @@ 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">
|
||||
<Icon name="material-symbols:save" class="mr-1" />
|
||||
Save Process
|
||||
</RsButton>
|
||||
<RsButton @click="confirmNavigation('/process-builder/manage')" variant="tertiary" size="sm">
|
||||
<Icon name="material-symbols:arrow-back" class="mr-1" />
|
||||
Back to Processes
|
||||
</RsButton>
|
||||
<!-- 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
|
||||
</RsButton>
|
||||
</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>
|
||||
|
||||
@ -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) {
|
||||
|
91
server/api/forms/[formId]/fields.get.js
Normal file
91
server/api/forms/[formId]/fields.get.js
Normal 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();
|
||||
}
|
||||
});
|
@ -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 {
|
||||
|
91
server/api/forms/[id]/fields.get.js
Normal file
91
server/api/forms/[id]/fields.get.js
Normal 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();
|
||||
}
|
||||
});
|
@ -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
|
||||
*/
|
||||
|
@ -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 = [];
|
||||
}
|
||||
}
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user