Refactor Variable Management and Enhance UI in Process Builder

- Updated BusinessRuleNodeConfiguration.vue to utilize props for available variables, improving data handling and consistency.
- Enhanced VariableManager.vue to group variables by type, providing a clearer organization and improved user experience.
- Introduced dynamic UI elements for variable usage indication and current value previews, enhancing interactivity.
- Implemented functionality to check variable usage across nodes, ensuring better management and awareness of variable dependencies.
- Added computed properties to force reactivity on variable updates, ensuring configuration modals reflect the latest data.
- Improved overall code clarity and maintainability through refactoring and enhanced comments.
This commit is contained in:
Md Afiq Iskandar 2025-07-07 17:11:09 +08:00
parent 5501c00c7c
commit d03eda54c8
4 changed files with 614 additions and 112 deletions

View File

@ -176,26 +176,26 @@
>
<option value="" disabled>Select variable</option>
<option
v-for="variable in availableVariables"
:key="variable.name"
v-for="(variable, index) in props.availableVariables"
:key="variable.name || index"
:value="variable.name"
>
{{ variable.label }}
{{ variable.label || variable.name || `Variable ${index}` }}
</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)
'bg-purple-100 text-purple-800': ['int', 'decimal'].includes(props.availableVariables.find(v => v.name === condition.variable)?.type),
'bg-blue-100 text-blue-800': props.availableVariables.find(v => v.name === condition.variable)?.type === 'string',
'bg-indigo-100 text-indigo-800': props.availableVariables.find(v => v.name === condition.variable)?.type === 'boolean',
'bg-amber-100 text-amber-800': ['date', 'datetime'].includes(props.availableVariables.find(v => v.name === condition.variable)?.type),
'bg-emerald-100 text-emerald-800': props.availableVariables.find(v => v.name === condition.variable)?.type === 'object',
'bg-gray-100 text-gray-800': !['int', 'decimal', 'string', 'boolean', 'date', 'datetime', 'object'].includes(props.availableVariables.find(v => v.name === condition.variable)?.type)
}"
>
{{ availableVariables.find(v => v.name === condition.variable)?.type }} type
{{ props.availableVariables.find(v => v.name === condition.variable)?.type }} type
</span>
</div>
</td>
@ -208,7 +208,7 @@
<option value="" disabled>Select operator</option>
<option
v-for="op in getOperatorsForType(
availableVariables.find(v => v.name === condition.variable)?.type
props.availableVariables.find(v => v.name === condition.variable)?.type
)"
:key="op.value"
:value="op.value"
@ -219,7 +219,7 @@
</td>
<td class="px-3 py-2">
<template v-if="getInputType(
availableVariables.find(v => v.name === condition.variable)?.type,
props.availableVariables.find(v => v.name === condition.variable)?.type,
condition.operator
) === 'none'">
<div class="flex items-center text-gray-400 text-sm italic">
@ -230,23 +230,23 @@
<!-- Range inputs for between operators -->
<template v-else-if="getInputType(
availableVariables.find(v => v.name === condition.variable)?.type,
props.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'"
:type="props.availableVariables.find(v => v.name === condition.variable)?.type === 'date' ? 'date' :
props.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'"
:type="props.availableVariables.find(v => v.name === condition.variable)?.type === 'date' ? 'date' :
props.availableVariables.find(v => v.name === condition.variable)?.type === 'datetime' ? 'datetime-local' : 'number'"
class="form-control text-xs"
placeholder="Max"
@blur="saveChanges"
@ -258,7 +258,7 @@
<!-- Weekday selector -->
<template v-else-if="getInputType(
availableVariables.find(v => v.name === condition.variable)?.type,
props.availableVariables.find(v => v.name === condition.variable)?.type,
condition.operator
) === 'weekday'">
<select v-model="condition.value" class="form-select" @change="saveChanges">
@ -274,7 +274,7 @@
<!-- Month selector -->
<template v-else-if="getInputType(
availableVariables.find(v => v.name === condition.variable)?.type,
props.availableVariables.find(v => v.name === condition.variable)?.type,
condition.operator
) === 'month'">
<select v-model="condition.value" class="form-select" @change="saveChanges">
@ -298,12 +298,12 @@
<input
v-model="condition.value"
:type="getInputType(
availableVariables.find(v => v.name === condition.variable)?.type,
props.availableVariables.find(v => v.name === condition.variable)?.type,
condition.operator
)"
class="form-control"
:placeholder="getValuePlaceholder(condition)"
:step="availableVariables.find(v => v.name === condition.variable)?.type === 'decimal' ? '0.01' : undefined"
:step="props.availableVariables.find(v => v.name === condition.variable)?.type === 'decimal' ? '0.01' : undefined"
@blur="saveChanges"
/>
</template>
@ -420,7 +420,7 @@
>
<option value="" disabled>Target variable</option>
<option
v-for="variable in availableVariables"
v-for="variable in props.availableVariables"
:key="variable.name"
:value="variable.name"
>
@ -525,20 +525,8 @@ const localNodeData = ref({
...props.nodeData
});
// Get available variables for conditions and actions
const availableVariables = computed(() => {
const processVariables = processStore.getProcessVariables();
if (!processVariables || typeof processVariables !== 'object') {
return [];
}
return Object.entries(processVariables).map(([name, variable]) => ({
name: name,
label: `${name} (${variable.type || 'string'})`,
type: variable.type || 'string',
scope: variable.scope || 'process'
}));
});
// Use the availableVariables prop instead of creating a computed property
// The prop is already properly formatted from the parent component
// Initialize with default values if needed
onMounted(() => {
@ -863,7 +851,7 @@ 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 varType = props.availableVariables.find(v => v.name === condition.variable)?.type?.toLowerCase();
const operator = condition.operator;
// Handle operators that don't need values
@ -928,7 +916,7 @@ const getValuePlaceholder = (condition) => {
// 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;
const varType = props.availableVariables.find(v => v.name === condition.variable)?.type;
// Reset values when operator changes
if (getInputType(varType, condition.operator) === 'none') {
@ -951,7 +939,7 @@ const updateConditionOperator = (groupIndex, condIndex) => {
// 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);
const selectedVar = props.availableVariables.find(v => v.name === condition.variable);
if (selectedVar) {
// Reset operator to a valid one for this type

View File

@ -74,82 +74,145 @@
</RsButton>
</div>
<!-- Variable List -->
<div v-else-if="filteredVariables.length" class="space-y-2">
<!-- Variables by Type -->
<div v-else-if="Object.keys(variablesByType).length" class="space-y-3">
<div
v-for="variable in filteredVariables"
:key="variable.name"
class="variable-item bg-white border border-gray-200 rounded-lg hover:border-blue-300 hover:shadow-sm transition-all duration-150 group"
v-for="(typeData, type) in variablesByType"
:key="type"
class="variable-type-section"
>
<!-- Variable Header -->
<div class="px-4 py-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3 min-w-0 flex-1">
<!-- Variable Icon -->
<div class="flex-shrink-0">
<div
class="w-8 h-8 rounded-lg flex items-center justify-center text-sm font-semibold"
:class="{
'bg-blue-100 text-blue-700': variable.type === 'string',
'bg-purple-100 text-purple-700': ['int', 'decimal'].includes(variable.type),
'bg-indigo-100 text-indigo-700': variable.type === 'boolean',
'bg-amber-100 text-amber-700': ['date', 'datetime'].includes(variable.type),
'bg-emerald-100 text-emerald-700': variable.type === 'object',
'bg-gray-100 text-gray-700': !['string', 'int', 'decimal', 'boolean', 'date', 'datetime', 'object'].includes(variable.type)
}"
>
<Icon :name="getVariableIcon(variable.type)" class="w-4 h-4" />
<!-- Type Header -->
<div
class="flex items-center justify-between p-3 bg-gray-50 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-100 transition-colors"
@click="toggleTypeSection(type)"
>
<div class="flex items-center gap-3">
<div
class="w-8 h-8 rounded-lg flex items-center justify-center text-sm font-semibold"
:class="typeData.colorClass"
>
<Icon :name="typeData.icon" class="w-4 h-4" />
</div>
<div>
<h3 class="text-sm font-semibold text-gray-900">
{{ typeData.label }}
</h3>
<p class="text-xs text-gray-500">
{{ typeData.variables.length }} variable{{ typeData.variables.length !== 1 ? 's' : '' }}
</p>
</div>
</div>
<div class="flex items-center gap-2">
<RsBadge
:variant="getTypeColor(type)"
size="sm"
>
{{ typeData.variables.length }}
</RsBadge>
<Icon
:name="collapsedSections[type] ? 'material-symbols:keyboard-arrow-right' : 'material-symbols:keyboard-arrow-down'"
class="w-5 h-5 text-gray-400 transition-transform duration-200"
/>
</div>
</div>
<!-- Variables in this type -->
<div
v-if="!collapsedSections[type]"
class="ml-4 space-y-1 border-l-2 border-gray-100 pl-4"
>
<div
v-for="variable in typeData.variables"
:key="variable.name"
class="variable-item bg-white border border-gray-200 rounded-lg hover:border-blue-300 hover:shadow-sm transition-all duration-150 group"
>
<div class="px-3 py-2.5">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 min-w-0 flex-1">
<!-- Variable Name -->
<h4 class="text-sm font-medium text-gray-900 truncate">{{ variable.name }}</h4>
<!-- Show usage indicator -->
<div v-if="checkVariableUsage(variable.name).isUsed" class="mt-1">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" clip-rule="evenodd" />
</svg>
Used in process
</span>
</div>
<!-- Current Value Preview -->
<div v-if="variable.value !== undefined && variable.value !== ''" class="flex-shrink-0">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800">
{{ getValuePreview(variable.value, variable.type) }}
</span>
</div>
</div>
<!-- Actions -->
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<!-- View Usage Button (only show if variable is used) -->
<button
v-if="checkVariableUsage(variable.name).isUsed"
@click="showVariableUsage(variable)"
class="p-1 text-gray-400 hover:text-green-600 hover:bg-green-50 rounded transition-colors"
title="View where this variable is used"
>
<Icon name="material-symbols:link" class="w-3.5 h-3.5" />
</button>
<button
@click="editVariable(variable)"
class="p-1 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
title="Edit variable"
>
<Icon name="material-symbols:edit" class="w-3.5 h-3.5" />
</button>
<button
@click="deleteVariable(variable)"
class="p-1 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
title="Delete variable"
>
<Icon name="material-symbols:delete" class="w-3.5 h-3.5" />
</button>
</div>
</div>
<!-- Variable Info -->
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 mb-1">
<h4 class="text-sm font-medium text-gray-900 truncate">{{ variable.name }}</h4>
<RsBadge
:variant="getTypeColor(variable.type)"
size="sm"
class="flex-shrink-0"
>
{{ variable.type }}
</RsBadge>
<!-- Description -->
<p v-if="variable.description" class="text-xs text-gray-500 mt-1 line-clamp-1">
{{ variable.description }}
</p>
<!-- Full Current Value Display (expandable) -->
<div v-if="variable.value !== undefined && variable.value !== '' && expandedValues[variable.name]" class="mt-2">
<div class="bg-amber-50 rounded-md p-2 border border-amber-100">
<div class="flex items-center justify-between mb-1">
<div class="flex items-center gap-1.5">
<Icon name="material-symbols:settings" class="w-3 h-3 text-amber-600" />
<span class="text-xs font-medium text-amber-700 uppercase tracking-wide">Current Value</span>
</div>
<button
@click="expandedValues[variable.name] = false"
class="text-amber-600 hover:text-amber-800 text-xs"
>
<Icon name="material-symbols:close" class="w-3 h-3" />
</button>
</div>
<div class="font-mono text-xs text-amber-800 break-all">
{{ formatValue(variable.value, variable.type) }}
</div>
</div>
<!-- Description -->
<p v-if="variable.description" class="text-xs text-gray-500 line-clamp-1">
{{ variable.description }}
</p>
</div>
</div>
<!-- Actions -->
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
@click="editVariable(variable)"
class="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
title="Edit variable"
>
<Icon name="material-symbols:edit" class="w-4 h-4" />
</button>
<button
@click="deleteVariable(variable)"
class="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
title="Delete variable"
>
<Icon name="material-symbols:delete" class="w-4 h-4" />
</button>
</div>
</div>
<!-- Current Value Display -->
<div v-if="variable.value !== undefined && variable.value !== ''" class="mt-3">
<div class="bg-amber-50 rounded-md p-2 border border-amber-100">
<div class="flex items-center gap-1.5 mb-1">
<Icon name="material-symbols:settings" class="w-3.5 h-3.5 text-amber-600" />
<span class="text-xs font-medium text-amber-700 uppercase tracking-wide">Current Value</span>
</div>
<div class="font-mono text-xs text-amber-800 break-all">
{{ formatValue(variable.value, variable.type) }}
<!-- Show full value button -->
<div v-else-if="variable.value !== undefined && variable.value !== '' && needsExpansion(variable.value, variable.type)" class="mt-1">
<button
@click="expandedValues[variable.name] = true"
class="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1"
>
<Icon name="material-symbols:visibility" class="w-3 h-3" />
Show full value
</button>
</div>
</div>
</div>
@ -271,6 +334,8 @@ const processStore = useProcessBuilderStore();
const showAddVariable = ref(false);
const editingVariable = ref(null);
const searchQuery = ref("");
const collapsedSections = ref({});
const expandedValues = ref({});
const variableForm = ref({
name: "",
type: "string",
@ -369,7 +434,97 @@ const filteredVariables = computed(() => {
);
});
// Group variables by type
const variablesByType = computed(() => {
const varsToUse = searchQuery.value ? filteredVariables.value : variables.value;
const grouped = {};
// Define type metadata
const typeMetadata = {
string: {
label: 'Text Variables',
icon: 'material-symbols:text-fields',
colorClass: 'bg-blue-100 text-blue-700'
},
int: {
label: 'Integer Numbers',
icon: 'material-symbols:pin',
colorClass: 'bg-purple-100 text-purple-700'
},
decimal: {
label: 'Decimal Numbers',
icon: 'material-symbols:pin',
colorClass: 'bg-purple-100 text-purple-700'
},
boolean: {
label: 'Boolean Values',
icon: 'material-symbols:toggle-on',
colorClass: 'bg-indigo-100 text-indigo-700'
},
date: {
label: 'Dates',
icon: 'material-symbols:calendar-today',
colorClass: 'bg-amber-100 text-amber-700'
},
datetime: {
label: 'Date & Time',
icon: 'material-symbols:schedule',
colorClass: 'bg-amber-100 text-amber-700'
},
object: {
label: 'Objects & Arrays',
icon: 'material-symbols:data-object',
colorClass: 'bg-emerald-100 text-emerald-700'
}
};
// Group variables by type
varsToUse.forEach(variable => {
const type = variable.type || 'string';
if (!grouped[type]) {
grouped[type] = {
variables: [],
...typeMetadata[type] || {
label: type.charAt(0).toUpperCase() + type.slice(1),
icon: 'material-symbols:data-object',
colorClass: 'bg-gray-100 text-gray-700'
}
};
// Set all sections to collapsed by default
if (collapsedSections.value[type] === undefined) {
collapsedSections.value[type] = true;
}
}
grouped[type].variables.push(variable);
});
// Sort variables within each type
Object.values(grouped).forEach(typeData => {
typeData.variables.sort((a, b) => a.name.localeCompare(b.name));
});
return grouped;
});
// Methods
const toggleTypeSection = (type) => {
collapsedSections.value[type] = !collapsedSections.value[type];
};
const getValuePreview = (value, type) => {
const formatted = formatValue(value, type);
if (formatted.length > 15) {
return formatted.substring(0, 15) + '...';
}
return formatted;
};
const needsExpansion = (value, type) => {
const formatted = formatValue(value, type);
return formatted.length > 15;
};
const editVariable = (variable) => {
editingVariable.value = variable;
variableForm.value = {
@ -380,8 +535,133 @@ const editVariable = (variable) => {
};
const deleteVariable = (variable) => {
if (confirm(`Are you sure you want to delete the variable "${variable.name}"? This might affect parts of your process that use this variable.`)) {
// Check if the variable is being used in any nodes
const usageInfo = checkVariableUsage(variable.name);
let confirmMessage = `Are you sure you want to delete the variable "${variable.name}"?`;
if (usageInfo.isUsed) {
confirmMessage += `\n\nWARNING: This variable is currently being used in:\n${usageInfo.usageDetails.join('\n')}`;
confirmMessage += '\n\nDeleting this variable may cause issues in your process. Continue?';
}
if (confirm(confirmMessage)) {
processStore.deleteProcessVariable(variable.name);
if (usageInfo.isUsed) {
console.warn(`Deleted variable "${variable.name}" that was being used in:`, usageInfo.usageDetails);
}
}
};
// Helper function to check where a variable is being used
const checkVariableUsage = (variableName) => {
const usageDetails = [];
let isUsed = false;
if (!processStore.currentProcess?.nodes) {
return { isUsed: false, usageDetails: [] };
}
processStore.currentProcess.nodes.forEach(node => {
if (!node.data) return;
const nodeLabel = node.data.label || `${node.type} node`;
switch (node.type) {
case 'api':
if (node.data.outputVariable === variableName) {
usageDetails.push(`- ${nodeLabel}: API Output Variable`);
isUsed = true;
}
if (node.data.errorVariable === variableName) {
usageDetails.push(`- ${nodeLabel}: API Error Variable`);
isUsed = true;
}
break;
case 'script':
if (node.data.errorVariable === variableName) {
usageDetails.push(`- ${nodeLabel}: Script Error Variable`);
isUsed = true;
}
if (node.data.outputVariables && Array.isArray(node.data.outputVariables)) {
node.data.outputVariables.forEach(output => {
if (output.name === variableName) {
usageDetails.push(`- ${nodeLabel}: Script Output Variable`);
isUsed = true;
}
});
}
break;
case 'form':
if (node.data.fieldMappings && Array.isArray(node.data.fieldMappings)) {
node.data.fieldMappings.forEach(mapping => {
if (mapping.processVariable === variableName) {
usageDetails.push(`- ${nodeLabel}: Form Field Mapping`);
isUsed = true;
}
});
}
break;
case 'businessRule':
if (node.data.conditions && Array.isArray(node.data.conditions)) {
node.data.conditions.forEach(conditionGroup => {
if (conditionGroup.conditions && Array.isArray(conditionGroup.conditions)) {
conditionGroup.conditions.forEach(condition => {
if (condition.variable === variableName) {
usageDetails.push(`- ${nodeLabel}: Business Rule Condition`);
isUsed = true;
}
});
}
});
}
break;
case 'notification':
const placeholderPattern = `{${variableName}}`;
if (node.data.subject && node.data.subject.includes(placeholderPattern)) {
usageDetails.push(`- ${nodeLabel}: Notification Subject`);
isUsed = true;
}
if (node.data.content && node.data.content.includes(placeholderPattern)) {
usageDetails.push(`- ${nodeLabel}: Notification Content`);
isUsed = true;
}
break;
case 'gateway':
if (node.data.conditions && Array.isArray(node.data.conditions)) {
node.data.conditions.forEach(conditionGroup => {
if (conditionGroup.conditions && Array.isArray(conditionGroup.conditions)) {
conditionGroup.conditions.forEach(condition => {
if (condition.variable === variableName) {
usageDetails.push(`- ${nodeLabel}: Gateway Condition`);
isUsed = true;
}
});
}
});
}
break;
}
});
return { isUsed, usageDetails };
};
// Show variable usage details in an alert
const showVariableUsage = (variable) => {
const usageInfo = checkVariableUsage(variable.name);
if (usageInfo.isUsed) {
const message = `Variable "${variable.name}" is currently being used in:\n\n${usageInfo.usageDetails.join('\n')}`;
alert(message);
} else {
alert(`Variable "${variable.name}" is not currently being used in any nodes.`);
}
};
@ -429,8 +709,32 @@ const saveVariable = async (formData) => {
};
if (editingVariable.value) {
// Update existing variable
processStore.updateProcessVariable(editingVariable.value.name, newVariable);
// Check if the variable name has changed
const oldName = editingVariable.value.name;
const newName = formData.name;
if (oldName !== newName) {
// Variable name has changed - use rename functionality to update all references
const success = processStore.renameProcessVariable(oldName, newName);
if (!success) {
alert(`Failed to rename variable. A variable named "${newName}" already exists or there was an error.`);
return;
}
// Additional update for other properties (type, description, value)
// Since rename only handles the name change and basic data copy
processStore.updateProcessVariable(newName, {
type: formData.type,
description: formData.description,
value: processedDefaultValue
});
console.log(`Variable "${oldName}" renamed to "${newName}" and all references updated`);
} else {
// Only properties changed, not the name
processStore.updateProcessVariable(editingVariable.value.name, newVariable);
}
} else {
// Add new variable
processStore.addProcessVariable(newVariable);

View File

@ -258,6 +258,20 @@ const gatewayAvailableVariables = computed(() => {
return allVars;
});
// Computed key that changes when variables are updated to force re-render of configuration modals
const variablesUpdateKey = computed(() => {
if (!processStore.currentProcess || !processStore.currentProcess.variables) {
return 'no-variables';
}
// Create a hash of variable names to detect changes
const variableNames = Object.keys(processStore.currentProcess.variables).sort().join(',');
return `vars-${variableNames.length}-${variableNames.split('').reduce((a, b) => {
a = ((a << 5) - a) + b.charCodeAt(0);
return a & a;
}, 0)}`;
});
// Handle node selection
const onNodeSelected = (node) => {
// console.log('Node selected:', node);
@ -1208,7 +1222,7 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
</div>
<div class="flex-1 overflow-y-auto">
<!-- Show variable manager when no node is selected -->
<VariableManager v-if="!selectedNodeData" />
<VariableManager v-if="!selectedNodeData" :key="`variables-${variablesUpdateKey}`" />
<!-- Show node properties when a node is selected -->
<div v-else class="p-4 space-y-4">
@ -1347,6 +1361,7 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
<FormNodeConfigurationModal
v-if="selectedNodeData && selectedNodeData.type === 'form'"
v-model="showFormConfigModal"
:key="`form-${selectedNodeData.id}-${variablesUpdateKey}`"
:nodeData="selectedNodeData.data"
:availableVariables="gatewayAvailableVariables"
@update="handleFormNodeUpdate"
@ -1356,6 +1371,7 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
<ApiNodeConfigurationModal
v-if="selectedNodeData && selectedNodeData.type === 'api'"
v-model="showApiConfigModal"
:key="`api-${selectedNodeData.id}-${variablesUpdateKey}`"
:nodeData="selectedNodeData.data"
:availableVariables="gatewayAvailableVariables"
@update="handleApiNodeUpdate"
@ -1365,6 +1381,7 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
<GatewayConditionManagerModal
v-if="selectedNodeData && selectedNodeData.type === 'gateway'"
v-model="showGatewayConfigModal"
:key="`gateway-${selectedNodeData.id}-${variablesUpdateKey}`"
:conditions="selectedNodeData.data.conditions || []"
:availableVariables="gatewayAvailableVariables"
:defaultPath="selectedNodeData.data.defaultPath || 'Default'"
@ -1376,6 +1393,7 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
<BusinessRuleNodeConfigurationModal
v-if="selectedNodeData && selectedNodeData.type === 'business-rule'"
v-model="showBusinessRuleConfigModal"
:key="`business-rule-${selectedNodeData.id}-${variablesUpdateKey}`"
:nodeId="selectedNodeData.id"
:nodeData="selectedNodeData.data"
:availableVariables="gatewayAvailableVariables"
@ -1386,6 +1404,7 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
<NotificationNodeConfigurationModal
v-if="selectedNodeData && selectedNodeData.type === 'notification'"
v-model="showNotificationConfigModal"
:key="`notification-${selectedNodeData.id}-${variablesUpdateKey}`"
:nodeData="selectedNodeData.data"
:availableVariables="gatewayAvailableVariables"
@update="handleNotificationNodeUpdate"
@ -1395,6 +1414,7 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
<ScriptNodeConfigurationModal
v-if="selectedNodeData && selectedNodeData.type === 'script'"
v-model="showScriptConfigModal"
:key="`script-${selectedNodeData.id}-${variablesUpdateKey}`"
:nodeData="selectedNodeData.data"
:availableVariables="gatewayAvailableVariables"
@update="handleScriptNodeUpdate"

View File

@ -1023,6 +1023,196 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
}
return this.currentProcess.variables[name] || null;
},
/**
* Update all references to a variable when its name changes
* This ensures data integrity across all nodes that reference the variable
*/
updateVariableReferences(oldVariableName, newVariableName) {
if (!this.currentProcess || !this.currentProcess.nodes || oldVariableName === newVariableName) {
return;
}
console.log(`Updating variable references from "${oldVariableName}" to "${newVariableName}"`);
this.currentProcess.nodes.forEach(node => {
if (!node.data) return;
switch (node.type) {
case 'api':
// Update API node output and error variables
if (node.data.outputVariable === oldVariableName) {
node.data.outputVariable = newVariableName;
console.log(`Updated API node ${node.id} outputVariable`);
}
if (node.data.errorVariable === oldVariableName) {
node.data.errorVariable = newVariableName;
console.log(`Updated API node ${node.id} errorVariable`);
}
break;
case 'script':
// Update script node output variables and error variable
if (node.data.errorVariable === oldVariableName) {
node.data.errorVariable = newVariableName;
console.log(`Updated Script node ${node.id} errorVariable`);
}
if (node.data.outputVariables && Array.isArray(node.data.outputVariables)) {
node.data.outputVariables.forEach(output => {
if (output.name === oldVariableName) {
output.name = newVariableName;
console.log(`Updated Script node ${node.id} outputVariable`);
}
});
}
break;
case 'form':
// Update form node field mappings (both input and output mappings)
if (node.data.inputMappings && Array.isArray(node.data.inputMappings)) {
node.data.inputMappings.forEach(mapping => {
if (mapping.processVariable === oldVariableName) {
mapping.processVariable = newVariableName;
console.log(`Updated Form node ${node.id} input mapping`);
}
});
}
if (node.data.outputMappings && Array.isArray(node.data.outputMappings)) {
node.data.outputMappings.forEach(mapping => {
if (mapping.processVariable === oldVariableName) {
mapping.processVariable = newVariableName;
console.log(`Updated Form node ${node.id} output mapping`);
}
});
}
if (node.data.fieldMappings && Array.isArray(node.data.fieldMappings)) {
node.data.fieldMappings.forEach(mapping => {
if (mapping.processVariable === oldVariableName) {
mapping.processVariable = newVariableName;
console.log(`Updated Form node ${node.id} field mapping`);
}
});
}
break;
case 'businessRule':
// Update business rule conditions (handle both ruleGroups and conditions structures)
if (node.data.ruleGroups && Array.isArray(node.data.ruleGroups)) {
node.data.ruleGroups.forEach(ruleGroup => {
if (ruleGroup.conditions && Array.isArray(ruleGroup.conditions)) {
ruleGroup.conditions.forEach(condition => {
if (condition.variable === oldVariableName) {
condition.variable = newVariableName;
console.log(`Updated Business Rule node ${node.id} ruleGroup condition variable`);
}
});
}
});
}
if (node.data.conditions && Array.isArray(node.data.conditions)) {
node.data.conditions.forEach(conditionGroup => {
if (conditionGroup.conditions && Array.isArray(conditionGroup.conditions)) {
conditionGroup.conditions.forEach(condition => {
if (condition.variable === oldVariableName) {
condition.variable = newVariableName;
console.log(`Updated Business Rule node ${node.id} condition variable`);
}
});
}
});
}
break;
case 'notification':
// Update notification content that may contain variable placeholders
if (node.data.subject) {
const oldPlaceholder = `{${oldVariableName}}`;
const newPlaceholder = `{${newVariableName}}`;
if (node.data.subject.includes(oldPlaceholder)) {
node.data.subject = node.data.subject.replace(new RegExp(`\\{${oldVariableName}\\}`, 'g'), newPlaceholder);
console.log(`Updated Notification node ${node.id} subject`);
}
}
if (node.data.content) {
const oldPlaceholder = `{${oldVariableName}}`;
const newPlaceholder = `{${newVariableName}}`;
if (node.data.content.includes(oldPlaceholder)) {
node.data.content = node.data.content.replace(new RegExp(`\\{${oldVariableName}\\}`, 'g'), newPlaceholder);
console.log(`Updated Notification node ${node.id} content`);
}
}
break;
case 'gateway':
// Update gateway conditions
if (node.data.conditions && Array.isArray(node.data.conditions)) {
node.data.conditions.forEach(conditionGroup => {
if (conditionGroup.conditions && Array.isArray(conditionGroup.conditions)) {
conditionGroup.conditions.forEach(condition => {
if (condition.variable === oldVariableName) {
condition.variable = newVariableName;
console.log(`Updated Gateway node ${node.id} condition variable`);
}
});
}
});
}
break;
default:
// For any other node types, check if there are variable references in the data
const nodeDataStr = JSON.stringify(node.data);
if (nodeDataStr.includes(oldVariableName)) {
console.log(`Warning: Node ${node.id} (type: ${node.type}) may contain references to variable "${oldVariableName}" that need manual review`);
}
break;
}
});
this.unsavedChanges = true;
},
/**
* Rename a variable and update all its references
*/
renameProcessVariable(oldName, newName) {
if (!this.currentProcess || !this.currentProcess.variables || oldName === newName) {
return false;
}
// Check if new name already exists
if (this.currentProcess.variables[newName]) {
console.error(`Variable "${newName}" already exists`);
return false;
}
// Get the variable data
const variableData = this.currentProcess.variables[oldName];
if (!variableData) {
console.error(`Variable "${oldName}" not found`);
return false;
}
// Update all references in nodes
this.updateVariableReferences(oldName, newName);
// Update the variable in the variables object
delete this.currentProcess.variables[oldName];
this.currentProcess.variables[newName] = {
...variableData,
name: newName
};
// Force reactivity update by creating a new object reference
this.currentProcess.variables = { ...this.currentProcess.variables };
// Force update of the entire process to trigger reactivity in all components
this.currentProcess = { ...this.currentProcess };
console.log(`Successfully renamed variable from "${oldName}" to "${newName}"`);
this.unsavedChanges = true;
return true;
}
}
});