- Updated VariableManager.vue to change variable type labels for clarity, replacing 'Int' and 'Decimal' with 'Number'. - Modified manager-approval-customScript.js to correct field references and enhance conditional logic for displaying the custom approved amount. - Enhanced manager-approval-form.json by adding 'readonly' attributes to several fields, ensuring they are non-editable during the approval process. - Revised travel-workflow-process.json to improve node connections and labels for better workflow clarity. - Updated travel-workflow-variables.json to refine variable descriptions and ensure consistency across the workflow. - Adjusted [id].vue to improve form data handling and loading states, enhancing user experience during workflow execution.
1761 lines
64 KiB
Vue
1761 lines
64 KiB
Vue
<template>
|
|
<div class="variable-manager">
|
|
<!-- Header with Add Button -->
|
|
<div class="bg-gray-50 border-b border-gray-200 p-3">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-3">
|
|
<div class="text-blue-500 flex-shrink-0">
|
|
<Icon name="material-symbols:data-object" class="text-xl" />
|
|
</div>
|
|
<div>
|
|
<h3 class="text-base font-medium text-gray-900">Variables</h3>
|
|
<p class="text-xs text-gray-500">
|
|
Define and manage variables to store and pass data within your process
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<RsButton
|
|
@click="
|
|
() => {
|
|
resetForm();
|
|
showAddVariable = true;
|
|
}
|
|
"
|
|
variant="primary"
|
|
size="sm"
|
|
>
|
|
<Icon name="material-symbols:add" class="mr-1" />
|
|
Add Variable
|
|
</RsButton>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search Bar -->
|
|
<div class="px-3 pt-2 pb-2">
|
|
<div class="relative">
|
|
<div class="absolute inset-y-0 left-0 pl-6 flex items-center pointer-events-none">
|
|
<Icon name="material-symbols:search" class="h-4 w-4 text-gray-400" />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
v-model="searchQuery"
|
|
class="block w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-sm"
|
|
placeholder="Search variables..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Variable List -->
|
|
<div class="p-3 overflow-auto flex-grow">
|
|
<!-- Empty State -->
|
|
<div v-if="!variables.length" class="text-center py-10 px-4 rounded-lg border-2 border-dashed border-gray-300 bg-gray-50">
|
|
<Icon
|
|
name="material-symbols:data-object"
|
|
class="w-14 h-14 mx-auto mb-3 text-gray-400"
|
|
/>
|
|
<h4 class="text-base font-medium text-gray-900 mb-1">
|
|
No Variables Added Yet
|
|
</h4>
|
|
<p class="text-sm text-gray-500 mb-4 max-w-md mx-auto">
|
|
Variables allow you to store and manage data in your process flow. Add your first variable to get started.
|
|
</p>
|
|
<RsButton
|
|
@click="
|
|
() => {
|
|
resetForm();
|
|
showAddVariable = true;
|
|
}
|
|
"
|
|
variant="primary"
|
|
size="md"
|
|
>
|
|
<Icon name="material-symbols:add" class="mr-1" />
|
|
Add First Variable
|
|
</RsButton>
|
|
</div>
|
|
|
|
<!-- Variables by Type -->
|
|
<div v-else-if="Object.keys(variablesByType).length" class="space-y-3">
|
|
<div
|
|
v-for="(typeData, type) in variablesByType"
|
|
:key="type"
|
|
class="variable-type-section"
|
|
>
|
|
<!-- Type Header -->
|
|
<div
|
|
class="flex items-center justify-between p-2.5 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-2.5">
|
|
<div
|
|
class="w-6 h-6 rounded-md flex items-center justify-center text-xs font-semibold"
|
|
:class="typeData.colorClass"
|
|
>
|
|
<Icon :name="typeData.icon" class="w-3.5 h-3.5" />
|
|
</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-4 h-4 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">
|
|
<div class="flex items-start justify-between gap-2">
|
|
<div class="min-w-0 flex-1">
|
|
<!-- Variable Name with title tooltip for full name -->
|
|
<h4 class="text-sm font-medium text-gray-900 truncate" :title="variable.name">
|
|
{{ variable.name }}
|
|
</h4>
|
|
|
|
<!-- Description on new line for better readability -->
|
|
<p v-if="variable.description" class="text-xs text-gray-500 mt-0.5 line-clamp-2" :title="variable.description">
|
|
{{ variable.description }}
|
|
</p>
|
|
|
|
<!-- Status indicators row -->
|
|
<div class="flex items-center gap-2 mt-1.5">
|
|
<!-- Usage indicator -->
|
|
<span v-if="checkVariableUsage(variable.name).isUsed"
|
|
class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800"
|
|
title="Used in process"
|
|
>
|
|
<Icon name="material-symbols:check-circle" class="w-3 h-3 mr-1" />
|
|
Used in process
|
|
</span>
|
|
|
|
<!-- Current Value Preview -->
|
|
<span v-if="variable.value !== undefined && variable.value !== ''"
|
|
class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 truncate max-w-24"
|
|
:title="getValuePreview(variable.value, variable.type)"
|
|
>
|
|
{{ getValuePreview(variable.value, variable.type) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Compact Actions -->
|
|
<div class="flex flex-col gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
|
|
<!-- 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 h-3" />
|
|
</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 h-3" />
|
|
</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 h-3" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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>
|
|
</div>
|
|
|
|
<!-- 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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- No search results -->
|
|
<div v-else class="text-center py-8">
|
|
<Icon
|
|
name="material-symbols:search-off"
|
|
class="w-12 h-12 mx-auto mb-3 text-gray-400"
|
|
/>
|
|
<h4 class="text-sm font-medium text-gray-900 mb-1">
|
|
No matching variables found
|
|
</h4>
|
|
<p class="text-sm text-gray-500 mb-4">
|
|
Try using different keywords or <a href="#" @click.prevent="searchQuery = ''" class="text-blue-500">clear your search</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Variable Usage Modal -->
|
|
<RsModal
|
|
v-model="showUsageModal"
|
|
:title="`Variable Usage: ${selectedVariable?.name || ''}`"
|
|
size="lg"
|
|
:hideFooter="true"
|
|
:overlayClose="true"
|
|
>
|
|
<div v-if="selectedVariable && variableUsageDetails" class="space-y-4">
|
|
<!-- Variable Info Header -->
|
|
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
|
<div class="flex items-start gap-3">
|
|
<div class="flex-shrink-0">
|
|
<div
|
|
class="w-8 h-8 rounded-lg flex items-center justify-center text-white"
|
|
:class="getTypeColorClass(selectedVariable.type)"
|
|
>
|
|
<Icon :name="getVariableIcon(selectedVariable.type)" class="w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<h3 class="text-lg font-semibold text-gray-900">{{ selectedVariable.name }}</h3>
|
|
<div class="flex items-center gap-2 mt-1">
|
|
<RsBadge :variant="getTypeColor(selectedVariable.type)" size="sm">
|
|
{{ selectedVariable.type }}
|
|
</RsBadge>
|
|
<span class="text-sm text-gray-500">•</span>
|
|
<span class="text-sm text-gray-500">{{ selectedVariable.scope || 'global' }} scope</span>
|
|
</div>
|
|
<p v-if="selectedVariable.description" class="text-sm text-gray-600 mt-2">
|
|
{{ selectedVariable.description }}
|
|
</p>
|
|
<div v-if="selectedVariable.value !== undefined && selectedVariable.value !== ''" class="mt-2">
|
|
<span class="text-xs font-medium text-gray-700">Current Value:</span>
|
|
<span class="ml-2 px-2 py-1 bg-amber-100 text-amber-800 rounded text-xs font-mono">
|
|
{{ getValuePreview(selectedVariable.value, selectedVariable.type) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Usage Summary -->
|
|
<div class="bg-blue-50 rounded-lg p-4 border border-blue-200">
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<Icon name="material-symbols:analytics" class="w-5 h-5 text-blue-600" />
|
|
<h4 class="text-sm font-semibold text-blue-900">Usage Summary</h4>
|
|
</div>
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-blue-600">{{ variableUsageDetails.totalUsages }}</div>
|
|
<div class="text-xs text-blue-700">Total References</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-blue-600">{{ variableUsageDetails.nodeCount }}</div>
|
|
<div class="text-xs text-blue-700">Nodes Using Variable</div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-blue-600">{{ variableUsageDetails.nodeTypes.size }}</div>
|
|
<div class="text-xs text-blue-700">Different Node Types</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Usage Details by Node -->
|
|
<div class="space-y-3">
|
|
<h4 class="text-sm font-semibold text-gray-900 flex items-center gap-2">
|
|
<Icon name="material-symbols:device-hub" class="w-4 h-4" />
|
|
Usage Details
|
|
</h4>
|
|
|
|
<div class="space-y-2 max-h-80 overflow-y-auto">
|
|
<div
|
|
v-for="usage in variableUsageDetails.usages"
|
|
:key="`${usage.nodeId}-${usage.location}`"
|
|
class="bg-white border border-gray-200 rounded-lg p-3 hover:border-blue-300 hover:shadow-sm transition-all cursor-pointer"
|
|
@click="navigateToNode(usage.nodeId)"
|
|
>
|
|
<div class="flex items-start gap-3">
|
|
<!-- Node Type Icon -->
|
|
<div class="flex-shrink-0 mt-0.5">
|
|
<div
|
|
class="w-6 h-6 rounded-md flex items-center justify-center text-white text-xs"
|
|
:class="getNodeTypeColor(usage.nodeType)"
|
|
>
|
|
<Icon :name="getNodeTypeIcon(usage.nodeType)" class="w-3.5 h-3.5" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Usage Details -->
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<h5 class="text-sm font-medium text-gray-900 truncate">
|
|
{{ usage.nodeLabel || `${usage.nodeType} Node` }}
|
|
</h5>
|
|
<RsBadge variant="secondary" size="xs">
|
|
{{ usage.nodeType }}
|
|
</RsBadge>
|
|
</div>
|
|
|
|
<div class="text-xs text-gray-600 space-y-1">
|
|
<div class="flex items-center gap-1">
|
|
<Icon name="material-symbols:location-on" class="w-3 h-3 text-gray-400" />
|
|
<span class="font-medium">Location:</span>
|
|
<span>{{ usage.location }}</span>
|
|
</div>
|
|
|
|
<div v-if="usage.context" class="flex items-start gap-1">
|
|
<Icon name="material-symbols:info" class="w-3 h-3 text-gray-400 mt-0.5" />
|
|
<span class="font-medium">Context:</span>
|
|
<span class="break-all">{{ usage.context }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Navigate Icon -->
|
|
<div class="flex-shrink-0">
|
|
<Icon name="material-symbols:arrow-outward" class="w-4 h-4 text-gray-400" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="flex justify-between pt-4 border-t border-gray-200">
|
|
<div class="flex gap-2">
|
|
<RsButton
|
|
@click="editVariableFromUsage"
|
|
variant="secondary"
|
|
size="sm"
|
|
>
|
|
<Icon name="material-symbols:edit" class="mr-1" />
|
|
Edit Variable
|
|
</RsButton>
|
|
</div>
|
|
<RsButton @click="showUsageModal = false" variant="tertiary">
|
|
Close
|
|
</RsButton>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- No Usage State -->
|
|
<div v-else-if="selectedVariable && !variableUsageDetails?.totalUsages" class="text-center py-8">
|
|
<Icon name="material-symbols:link-off" class="w-12 h-12 mx-auto mb-3 text-gray-400" />
|
|
<h4 class="text-base font-medium text-gray-900 mb-2">
|
|
Variable Not Used
|
|
</h4>
|
|
<p class="text-sm text-gray-500 mb-4">
|
|
The variable "{{ selectedVariable.name }}" is not currently being used in any nodes in your process.
|
|
</p>
|
|
<div class="flex justify-center gap-2">
|
|
<RsButton @click="editVariableFromUsage" variant="secondary" size="sm">
|
|
<Icon name="material-symbols:edit" class="mr-1" />
|
|
Edit Variable
|
|
</RsButton>
|
|
<RsButton @click="showUsageModal = false" variant="tertiary" size="sm">
|
|
Close
|
|
</RsButton>
|
|
</div>
|
|
</div>
|
|
</RsModal>
|
|
|
|
<!-- Add/Edit Variable Modal -->
|
|
<RsModal
|
|
v-model="showAddVariable"
|
|
:title="editingVariable ? 'Edit Variable' : 'Add Variable'"
|
|
size="md"
|
|
:hideFooter="true"
|
|
:overlayClose="false"
|
|
>
|
|
<div class="mb-4 flex items-start" v-if="!editingVariable">
|
|
<div class="mr-3 text-blue-500 flex-shrink-0 mt-1">
|
|
<Icon name="material-symbols:data-object" class="text-xl" />
|
|
</div>
|
|
<p class="text-sm text-gray-600">
|
|
Variables store data that can be used throughout your process flow. They can be updated by tasks, used in conditions,
|
|
or displayed in forms.
|
|
</p>
|
|
</div>
|
|
|
|
<FormKit
|
|
type="form"
|
|
@submit="saveVariable"
|
|
:actions="false"
|
|
class="space-y-4"
|
|
>
|
|
<FormKit
|
|
name="name"
|
|
v-model="variableForm.name"
|
|
type="text"
|
|
label="Variable Name"
|
|
placeholder="Enter variable name (e.g. customerName)"
|
|
validation="required|alpha_numeric|length:3,50"
|
|
:validation-messages="{
|
|
required: 'Variable name is required',
|
|
alpha_numeric:
|
|
'Variable name can only contain letters, numbers, and underscores',
|
|
length: 'Variable name must be between 3 and 50 characters',
|
|
}"
|
|
help="Use a descriptive name without spaces. Example: totalAmount, customerName, orderStatus"
|
|
/>
|
|
|
|
<FormKit
|
|
name="type"
|
|
v-model="variableForm.type"
|
|
type="select"
|
|
label="Data Type"
|
|
:options="variableTypes"
|
|
validation="required"
|
|
:validation-messages="{
|
|
required: 'Variable type is required',
|
|
}"
|
|
help="Select the type of data this variable will store"
|
|
/>
|
|
|
|
<FormKit
|
|
name="defaultValue"
|
|
v-model="variableForm.defaultValue"
|
|
:type="getInputTypeForVariableType(variableForm.type)"
|
|
:label="`Default Value${variableForm.type === 'boolean' ? '' : ' (Optional)'}`"
|
|
:placeholder="getPlaceholderForType(variableForm.type)"
|
|
:options="variableForm.type === 'boolean' ? [
|
|
{ label: 'True', value: true },
|
|
{ label: 'False', value: false }
|
|
] : undefined"
|
|
:help="getHelpTextForType(variableForm.type)"
|
|
/>
|
|
|
|
<FormKit
|
|
name="description"
|
|
v-model="variableForm.description"
|
|
type="textarea"
|
|
label="Description"
|
|
placeholder="Enter a description to help others understand what this variable is used for"
|
|
:rows="2"
|
|
help="A clear description helps others understand the purpose of this variable"
|
|
/>
|
|
|
|
<div class="flex justify-end space-x-2 pt-4 border-t border-gray-200">
|
|
<RsButton type="button" @click="closeModal" variant="tertiary">
|
|
Cancel
|
|
</RsButton>
|
|
<FormKit type="submit" input-class="rs-button rs-button-primary">
|
|
{{ editingVariable ? "Update Variable" : "Add Variable" }}
|
|
</FormKit>
|
|
</div>
|
|
</FormKit>
|
|
</RsModal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from "vue";
|
|
import { useProcessBuilderStore } from "~/stores/processBuilder";
|
|
|
|
const processStore = useProcessBuilderStore();
|
|
|
|
// State
|
|
const showAddVariable = ref(false);
|
|
const editingVariable = ref(null);
|
|
const searchQuery = ref("");
|
|
const collapsedSections = ref({});
|
|
const expandedValues = ref({});
|
|
const showUsageModal = ref(false);
|
|
const selectedVariable = ref(null);
|
|
const variableUsageDetails = ref(null);
|
|
const variableForm = ref({
|
|
name: "",
|
|
type: "string",
|
|
scope: "global",
|
|
description: "",
|
|
defaultValue: ""
|
|
});
|
|
|
|
// Variable type options with descriptions
|
|
const variableTypes = [
|
|
{ label: 'String - Text values', value: 'string' },
|
|
{ label: 'Number - Decimal numbers', value: 'number' },
|
|
{ label: 'Object - Complex data structure', value: 'object' },
|
|
{ label: 'DateTime - Date and time values', value: 'datetime' },
|
|
{ label: 'Date - Date values only', value: 'date' },
|
|
{ label: 'Boolean - True/False values', value: 'boolean' }
|
|
];
|
|
|
|
// Helper functions for default value input
|
|
const getInputTypeForVariableType = (type) => {
|
|
switch (type) {
|
|
case 'int':
|
|
case 'decimal':
|
|
return 'number';
|
|
case 'boolean':
|
|
return 'select';
|
|
case 'date':
|
|
return 'date';
|
|
case 'datetime':
|
|
return 'datetime-local';
|
|
case 'object':
|
|
return 'textarea';
|
|
default:
|
|
return 'text';
|
|
}
|
|
};
|
|
|
|
const getPlaceholderForType = (type) => {
|
|
switch (type) {
|
|
case 'string':
|
|
return 'Enter default text value';
|
|
case 'int':
|
|
return 'Enter default number (e.g. 0, 100)';
|
|
case 'decimal':
|
|
return 'Enter default decimal (e.g. 0.0, 99.99)';
|
|
case 'object':
|
|
return 'Enter default JSON object (e.g. {"key": "value"})';
|
|
case 'date':
|
|
return 'Select default date';
|
|
case 'datetime':
|
|
return 'Select default date and time';
|
|
case 'boolean':
|
|
return 'Select default boolean value';
|
|
default:
|
|
return 'Enter default value';
|
|
}
|
|
};
|
|
|
|
const getHelpTextForType = (type) => {
|
|
switch (type) {
|
|
case 'string':
|
|
return 'Default text that will be used when the variable is first created';
|
|
case 'int':
|
|
return 'Default whole number (no decimals)';
|
|
case 'decimal':
|
|
return 'Default decimal number (with decimal places)';
|
|
case 'object':
|
|
return 'Default JSON object - must be valid JSON format';
|
|
case 'date':
|
|
return 'Default date value for this variable';
|
|
case 'datetime':
|
|
return 'Default date and time value for this variable';
|
|
case 'boolean':
|
|
return 'Default true/false value for this variable';
|
|
default:
|
|
return 'Default value that will be used when the variable is first created';
|
|
}
|
|
};
|
|
|
|
// Computed
|
|
const variables = computed(() => {
|
|
// Get variables from the current process
|
|
return processStore.getProcessVariables();
|
|
});
|
|
|
|
// Filtered variables based on search query
|
|
const filteredVariables = computed(() => {
|
|
if (!searchQuery.value) return variables.value;
|
|
|
|
const query = searchQuery.value.toLowerCase();
|
|
return variables.value.filter(variable =>
|
|
variable.name.toLowerCase().includes(query) ||
|
|
(variable.description && variable.description.toLowerCase().includes(query)) ||
|
|
variable.type.toLowerCase().includes(query)
|
|
);
|
|
});
|
|
|
|
// 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 = {
|
|
...variable,
|
|
defaultValue: variable.value || ""
|
|
};
|
|
showAddVariable.value = true;
|
|
};
|
|
|
|
const deleteVariable = (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 variable usage in text/JSON content
|
|
const checkTextForVariable = (text, variableName) => {
|
|
if (!text || typeof text !== 'string') return false;
|
|
|
|
// Check for various variable reference patterns
|
|
const patterns = [
|
|
`{${variableName}}`, // Template placeholder: {variableName}
|
|
`{{${variableName}}}`, // Handlebars style: {{variableName}}
|
|
`\${${variableName}}`, // JavaScript template literal: ${variableName}
|
|
`processVariables.${variableName}`, // Direct object access
|
|
`processVariables["${variableName}"]`, // Bracket notation with quotes
|
|
`processVariables['${variableName}']`, // Bracket notation with single quotes
|
|
`"${variableName}"`, // JSON string value
|
|
`'${variableName}'`, // Single quoted string
|
|
new RegExp(`\\b${variableName}\\b`, 'g') // Word boundary match
|
|
];
|
|
|
|
return patterns.some(pattern => {
|
|
if (pattern instanceof RegExp) {
|
|
return pattern.test(text);
|
|
}
|
|
return text.includes(pattern);
|
|
});
|
|
};
|
|
|
|
// 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.label || `${node.type} node`;
|
|
|
|
switch (node.type) {
|
|
case 'api':
|
|
// Check output and error variables
|
|
if (node.data.outputVariable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: API Response Output Variable`);
|
|
isUsed = true;
|
|
}
|
|
if (node.data.errorVariable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: API Error Output Variable`);
|
|
isUsed = true;
|
|
}
|
|
|
|
// Check API URL for variable references
|
|
if (node.data.apiUrl && checkTextForVariable(node.data.apiUrl, variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: API URL contains variable reference`);
|
|
isUsed = true;
|
|
}
|
|
|
|
// Check request body for variable references
|
|
if (node.data.requestBody && checkTextForVariable(node.data.requestBody, variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: API Request Body contains variable reference`);
|
|
isUsed = true;
|
|
}
|
|
|
|
// Check headers for variable references
|
|
if (node.data.headers && checkTextForVariable(node.data.headers, variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: API Headers contain variable reference`);
|
|
isUsed = true;
|
|
}
|
|
|
|
// Check query parameters
|
|
if (node.data.queryParams && checkTextForVariable(node.data.queryParams, variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: API Query Parameters contain variable reference`);
|
|
isUsed = true;
|
|
}
|
|
break;
|
|
|
|
case 'script':
|
|
// Check error variable
|
|
if (node.data.errorVariable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Script Error Output Variable`);
|
|
isUsed = true;
|
|
}
|
|
|
|
// Check output variables
|
|
if (node.data.outputVariables && Array.isArray(node.data.outputVariables)) {
|
|
node.data.outputVariables.forEach(output => {
|
|
if (output.name === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Script Output Variable "${output.name}"`);
|
|
isUsed = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check input variables (can be array of strings or objects)
|
|
if (node.data.inputVariables && Array.isArray(node.data.inputVariables)) {
|
|
node.data.inputVariables.forEach(input => {
|
|
// Handle string array format
|
|
if (typeof input === 'string' && input === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Script Input Variable "${input}"`);
|
|
isUsed = true;
|
|
}
|
|
// Handle object format
|
|
else if (typeof input === 'object' && (input.name === variableName || input.variable === variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: Script Input Variable "${input.name || input.variable}"`);
|
|
isUsed = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check script code for variable references
|
|
if (node.data.scriptCode && checkTextForVariable(node.data.scriptCode, variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: Script Code contains variable reference`);
|
|
isUsed = true;
|
|
}
|
|
break;
|
|
|
|
case 'form':
|
|
// Check output mappings (form fields mapped to process variables)
|
|
if (node.data.outputMappings && Array.isArray(node.data.outputMappings)) {
|
|
node.data.outputMappings.forEach(mapping => {
|
|
if (mapping.processVariable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Form Field "${mapping.formField}" outputs to variable`);
|
|
isUsed = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check input mappings (process variables mapped to form fields)
|
|
if (node.data.inputMappings && Array.isArray(node.data.inputMappings)) {
|
|
node.data.inputMappings.forEach(mapping => {
|
|
if (mapping.processVariable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Variable inputs to Form Field "${mapping.formField}"`);
|
|
isUsed = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check legacy field mappings (for backward compatibility)
|
|
if (node.data.fieldMappings && Array.isArray(node.data.fieldMappings)) {
|
|
node.data.fieldMappings.forEach(mapping => {
|
|
if (mapping.processVariable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Form Field "${mapping.fieldName}" mapped to variable`);
|
|
isUsed = true;
|
|
}
|
|
if (mapping.defaultValue && checkTextForVariable(mapping.defaultValue, variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: Form Field "${mapping.fieldName}" default value references variable`);
|
|
isUsed = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check field conditions
|
|
if (node.data.fieldConditions && Array.isArray(node.data.fieldConditions)) {
|
|
node.data.fieldConditions.forEach((condition, index) => {
|
|
if (condition.variable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Form Field Condition ${index + 1}`);
|
|
isUsed = true;
|
|
}
|
|
if (condition.value && checkTextForVariable(String(condition.value), variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: Form Field Condition ${index + 1} value references variable`);
|
|
isUsed = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check form assignment conditions
|
|
if (node.data.assignmentConditions && Array.isArray(node.data.assignmentConditions)) {
|
|
node.data.assignmentConditions.forEach((condition, index) => {
|
|
if (condition.variable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Form Assignment Condition ${index + 1}`);
|
|
isUsed = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check assignment variable
|
|
if (node.data.assignmentVariable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Form Assignment Variable`);
|
|
isUsed = true;
|
|
}
|
|
|
|
// Check assignee variable (legacy)
|
|
if (node.data.assigneeVariable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Form Assignee Variable`);
|
|
isUsed = true;
|
|
}
|
|
break;
|
|
|
|
case 'business-rule':
|
|
case 'businessRule':
|
|
// Check rule groups and conditions
|
|
if (node.data.ruleGroups && Array.isArray(node.data.ruleGroups)) {
|
|
node.data.ruleGroups.forEach((group, groupIndex) => {
|
|
if (group.conditions && Array.isArray(group.conditions)) {
|
|
group.conditions.forEach((condition, condIndex) => {
|
|
if (condition.variable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Business Rule Group ${groupIndex + 1}, Condition ${condIndex + 1}`);
|
|
isUsed = true;
|
|
}
|
|
if (condition.value && checkTextForVariable(String(condition.value), variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: Business Rule Group ${groupIndex + 1}, Condition ${condIndex + 1} value references variable`);
|
|
isUsed = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check actions
|
|
if (group.actions && Array.isArray(group.actions)) {
|
|
group.actions.forEach((action, actionIndex) => {
|
|
if (action.variable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Business Rule Group ${groupIndex + 1}, Action ${actionIndex + 1} target variable`);
|
|
isUsed = true;
|
|
}
|
|
if (action.value && checkTextForVariable(String(action.value), variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: Business Rule Group ${groupIndex + 1}, Action ${actionIndex + 1} value references variable`);
|
|
isUsed = true;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check legacy conditions structure
|
|
if (node.data.conditions && Array.isArray(node.data.conditions)) {
|
|
node.data.conditions.forEach((conditionGroup, groupIndex) => {
|
|
if (conditionGroup.conditions && Array.isArray(conditionGroup.conditions)) {
|
|
conditionGroup.conditions.forEach((condition, condIndex) => {
|
|
if (condition.variable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Business Rule Condition Group ${groupIndex + 1}, Condition ${condIndex + 1}`);
|
|
isUsed = true;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
break;
|
|
|
|
case 'notification':
|
|
// Check recipient variables
|
|
if (node.data.recipientVariable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Notification Recipient Variable`);
|
|
isUsed = true;
|
|
}
|
|
if (node.data.recipientUser === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Notification Recipient User Variable`);
|
|
isUsed = true;
|
|
}
|
|
if (node.data.recipientEmail === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Notification Recipient Email Variable`);
|
|
isUsed = true;
|
|
}
|
|
|
|
// Check subject and message content for variable placeholders
|
|
if (node.data.subject && checkTextForVariable(node.data.subject, variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: Notification Subject contains variable reference`);
|
|
isUsed = true;
|
|
}
|
|
if (node.data.message && checkTextForVariable(node.data.message, variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: Notification Message contains variable reference`);
|
|
isUsed = true;
|
|
}
|
|
if (node.data.content && checkTextForVariable(node.data.content, variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: Notification Content contains variable reference`);
|
|
isUsed = true;
|
|
}
|
|
|
|
// Check dynamic content fields
|
|
if (node.data.dynamicSubject && checkTextForVariable(node.data.dynamicSubject, variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: Notification Dynamic Subject contains variable reference`);
|
|
isUsed = true;
|
|
}
|
|
if (node.data.dynamicMessage && checkTextForVariable(node.data.dynamicMessage, variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: Notification Dynamic Message contains variable reference`);
|
|
isUsed = true;
|
|
}
|
|
break;
|
|
|
|
case 'gateway':
|
|
case 'decision':
|
|
// Check gateway conditions
|
|
if (node.data.conditions && Array.isArray(node.data.conditions)) {
|
|
node.data.conditions.forEach((conditionGroup, groupIndex) => {
|
|
if (conditionGroup.conditions && Array.isArray(conditionGroup.conditions)) {
|
|
conditionGroup.conditions.forEach((condition, condIndex) => {
|
|
if (condition.variable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Decision Path "${conditionGroup.output || 'Path ' + (groupIndex + 1)}", Condition ${condIndex + 1}`);
|
|
isUsed = true;
|
|
}
|
|
if (condition.value && checkTextForVariable(String(condition.value), variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: Decision Path "${conditionGroup.output || 'Path ' + (groupIndex + 1)}", Condition ${condIndex + 1} value references variable`);
|
|
isUsed = true;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// For any custom node types, check common data fields
|
|
if (node.data.outputVariable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Output Variable`);
|
|
isUsed = true;
|
|
}
|
|
if (node.data.inputVariable === variableName) {
|
|
usageDetails.push(`- ${nodeLabel}: Input Variable`);
|
|
isUsed = true;
|
|
}
|
|
|
|
// Check any text fields for variable references
|
|
Object.keys(node.data).forEach(key => {
|
|
const value = node.data[key];
|
|
if (typeof value === 'string' && checkTextForVariable(value, variableName)) {
|
|
usageDetails.push(`- ${nodeLabel}: ${key} field contains variable reference`);
|
|
isUsed = true;
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
});
|
|
|
|
return { isUsed, usageDetails };
|
|
};
|
|
|
|
// Enhanced function to get detailed variable usage information
|
|
const getDetailedVariableUsage = (variableName) => {
|
|
// Use the same node source as the working checkVariableUsage function
|
|
const nodes = processStore.currentProcess?.nodes || [];
|
|
const usages = [];
|
|
const nodeTypes = new Set();
|
|
let totalUsages = 0;
|
|
|
|
const checkTextForVariable = (text, variable) => {
|
|
if (!text || typeof text !== 'string') return false;
|
|
|
|
// Use the same patterns as the working function
|
|
const patterns = [
|
|
`{${variable}}`, // Template placeholder: {variableName}
|
|
`{{${variable}}}`, // Handlebars style: {{variableName}}
|
|
`\${${variable}}`, // JavaScript template literal: ${variableName}
|
|
`processVariables.${variable}`, // Direct object access
|
|
`processVariables["${variable}"]`, // Bracket notation with quotes
|
|
`processVariables['${variable}']`, // Bracket notation with single quotes
|
|
`"${variable}"`, // JSON string value
|
|
`'${variable}'`, // Single quoted string
|
|
new RegExp(`\\b${variable}\\b`, 'g') // Word boundary match
|
|
];
|
|
|
|
return patterns.some(pattern => {
|
|
if (pattern instanceof RegExp) {
|
|
return pattern.test(text);
|
|
}
|
|
return text.includes(pattern);
|
|
});
|
|
};
|
|
|
|
nodes.forEach(node => {
|
|
if (!node.data) return;
|
|
|
|
const nodeUsages = [];
|
|
const nodeLabel = node.data.label || node.label || `${node.type} node`;
|
|
|
|
// Form nodes
|
|
if (node.type === 'form') {
|
|
const data = node.data || {};
|
|
|
|
// Check output mappings (form fields mapped to process variables)
|
|
if (data.outputMappings && Array.isArray(data.outputMappings)) {
|
|
data.outputMappings.forEach((mapping, index) => {
|
|
if (mapping.processVariable === variableName) {
|
|
nodeUsages.push({
|
|
location: `Output Mapping #${index + 1}`,
|
|
context: `Field "${mapping.formField}" → Variable "${mapping.processVariable}"`
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check input mappings (process variables mapped to form fields)
|
|
if (data.inputMappings && Array.isArray(data.inputMappings)) {
|
|
data.inputMappings.forEach((mapping, index) => {
|
|
if (mapping.processVariable === variableName) {
|
|
nodeUsages.push({
|
|
location: `Input Mapping #${index + 1}`,
|
|
context: `Variable "${mapping.processVariable}" → Field "${mapping.formField}"`
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check legacy field mappings (for backward compatibility)
|
|
if (data.fieldMappings && Array.isArray(data.fieldMappings)) {
|
|
data.fieldMappings.forEach((mapping, index) => {
|
|
if (mapping.processVariable === variableName) {
|
|
nodeUsages.push({
|
|
location: `Field Mapping #${index + 1}`,
|
|
context: `Form Field "${mapping.fieldName}" mapped to variable`
|
|
});
|
|
}
|
|
if (mapping.defaultValue && checkTextForVariable(mapping.defaultValue, variableName)) {
|
|
nodeUsages.push({
|
|
location: `Field Mapping #${index + 1} Default`,
|
|
context: `Form Field "${mapping.fieldName}" default value references variable`
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check assignment variable
|
|
if (data.assignmentVariable === variableName) {
|
|
nodeUsages.push({
|
|
location: 'Assignment Variable',
|
|
context: `Form result assigned to "${data.assignmentVariable}"`
|
|
});
|
|
}
|
|
|
|
// Check assignee variable (legacy)
|
|
if (data.assigneeVariable === variableName) {
|
|
nodeUsages.push({
|
|
location: 'Assignee Variable',
|
|
context: `Form assignee set to "${data.assigneeVariable}"`
|
|
});
|
|
}
|
|
|
|
// Check field conditions
|
|
if (data.fieldConditions && Array.isArray(data.fieldConditions)) {
|
|
data.fieldConditions.forEach((condition, index) => {
|
|
if (condition.variable === variableName) {
|
|
nodeUsages.push({
|
|
location: `Field Condition #${index + 1}`,
|
|
context: `Condition: ${condition.variable} ${condition.operator} ${condition.value}`
|
|
});
|
|
}
|
|
if (condition.value && checkTextForVariable(String(condition.value), variableName)) {
|
|
nodeUsages.push({
|
|
location: `Field Condition #${index + 1} Value`,
|
|
context: `Condition value references variable`
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check form assignment conditions
|
|
if (data.assignmentConditions && Array.isArray(data.assignmentConditions)) {
|
|
data.assignmentConditions.forEach((condition, index) => {
|
|
if (condition.variable === variableName) {
|
|
nodeUsages.push({
|
|
location: `Assignment Condition #${index + 1}`,
|
|
context: `Assignment condition on variable`
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// API nodes
|
|
else if (node.type === 'api') {
|
|
const data = node.data || {};
|
|
|
|
// Check output and error variables
|
|
if (data.outputVariable === variableName) {
|
|
nodeUsages.push({
|
|
location: 'API Output Variable',
|
|
context: `Response stored in "${data.outputVariable}"`
|
|
});
|
|
}
|
|
|
|
if (data.errorVariable === variableName) {
|
|
nodeUsages.push({
|
|
location: 'API Error Variable',
|
|
context: `Error stored in "${data.errorVariable}"`
|
|
});
|
|
}
|
|
|
|
// Check API URL for variable references
|
|
if (data.apiUrl && checkTextForVariable(data.apiUrl, variableName)) {
|
|
nodeUsages.push({
|
|
location: 'API URL',
|
|
context: `API URL contains variable reference`
|
|
});
|
|
}
|
|
|
|
// Check request body for variable references
|
|
if (data.requestBody && checkTextForVariable(data.requestBody, variableName)) {
|
|
nodeUsages.push({
|
|
location: 'Request Body',
|
|
context: `API Request Body contains variable reference`
|
|
});
|
|
}
|
|
|
|
// Check headers for variable references
|
|
if (data.headers && checkTextForVariable(data.headers, variableName)) {
|
|
nodeUsages.push({
|
|
location: 'Headers',
|
|
context: `API Headers contain variable reference`
|
|
});
|
|
}
|
|
|
|
// Check query parameters
|
|
if (data.queryParams && checkTextForVariable(data.queryParams, variableName)) {
|
|
nodeUsages.push({
|
|
location: 'Query Parameters',
|
|
context: `API Query Parameters contain variable reference`
|
|
});
|
|
}
|
|
}
|
|
|
|
// Script nodes
|
|
else if (node.type === 'script') {
|
|
const data = node.data || {};
|
|
|
|
// Check error variable
|
|
if (data.errorVariable === variableName) {
|
|
nodeUsages.push({
|
|
location: 'Script Error Variable',
|
|
context: `Error stored in "${data.errorVariable}"`
|
|
});
|
|
}
|
|
|
|
// Check output variables
|
|
if (data.outputVariables && Array.isArray(data.outputVariables)) {
|
|
data.outputVariables.forEach(output => {
|
|
if (output.name === variableName) {
|
|
nodeUsages.push({
|
|
location: 'Script Output Variables',
|
|
context: `Output variable: "${output.name}"`
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check input variables (can be array of strings or objects)
|
|
if (data.inputVariables && Array.isArray(data.inputVariables)) {
|
|
data.inputVariables.forEach(input => {
|
|
// Handle string array format
|
|
if (typeof input === 'string' && input === variableName) {
|
|
nodeUsages.push({
|
|
location: 'Script Input Variables',
|
|
context: `Input variable: "${input}"`
|
|
});
|
|
}
|
|
// Handle object format
|
|
else if (typeof input === 'object' && (input.name === variableName || input.variable === variableName)) {
|
|
nodeUsages.push({
|
|
location: 'Script Input Variables',
|
|
context: `Input variable: "${input.name || input.variable}"`
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check script code for variable references
|
|
if (data.scriptCode && checkTextForVariable(data.scriptCode, variableName)) {
|
|
nodeUsages.push({
|
|
location: 'Script Code',
|
|
context: 'Script code contains variable reference'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Decision nodes
|
|
else if (node.type === 'gateway' || node.type === 'decision') {
|
|
const data = node.data || {};
|
|
|
|
// Check gateway conditions
|
|
if (data.conditions && Array.isArray(data.conditions)) {
|
|
data.conditions.forEach((conditionGroup, groupIndex) => {
|
|
if (conditionGroup.conditions && Array.isArray(conditionGroup.conditions)) {
|
|
conditionGroup.conditions.forEach((condition, condIndex) => {
|
|
if (condition.variable === variableName) {
|
|
nodeUsages.push({
|
|
location: `Decision Path "${conditionGroup.output || 'Path ' + (groupIndex + 1)}", Condition ${condIndex + 1}`,
|
|
context: `${condition.variable} ${condition.operator} ${condition.value}`
|
|
});
|
|
}
|
|
if (condition.value && checkTextForVariable(String(condition.value), variableName)) {
|
|
nodeUsages.push({
|
|
location: `Decision Path "${conditionGroup.output || 'Path ' + (groupIndex + 1)}", Condition ${condIndex + 1} Value`,
|
|
context: `Condition value references variable`
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Business rule nodes
|
|
else if (node.type === 'business-rule' || node.type === 'businessRule') {
|
|
const data = node.data || {};
|
|
|
|
// Check rule groups and conditions
|
|
if (data.ruleGroups && Array.isArray(data.ruleGroups)) {
|
|
data.ruleGroups.forEach((group, groupIndex) => {
|
|
if (group.conditions && Array.isArray(group.conditions)) {
|
|
group.conditions.forEach((condition, condIndex) => {
|
|
if (condition.variable === variableName) {
|
|
nodeUsages.push({
|
|
location: `Business Rule Group ${groupIndex + 1}, Condition ${condIndex + 1}`,
|
|
context: `${condition.variable} ${condition.operator} ${condition.value}`
|
|
});
|
|
}
|
|
if (condition.value && checkTextForVariable(String(condition.value), variableName)) {
|
|
nodeUsages.push({
|
|
location: `Business Rule Group ${groupIndex + 1}, Condition ${condIndex + 1} Value`,
|
|
context: `Condition value references variable`
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check actions
|
|
if (group.actions && Array.isArray(group.actions)) {
|
|
group.actions.forEach((action, actionIndex) => {
|
|
if (action.variable === variableName) {
|
|
nodeUsages.push({
|
|
location: `Business Rule Group ${groupIndex + 1}, Action ${actionIndex + 1}`,
|
|
context: `Set ${action.variable} = ${action.value}`
|
|
});
|
|
}
|
|
if (action.value && checkTextForVariable(String(action.value), variableName)) {
|
|
nodeUsages.push({
|
|
location: `Business Rule Group ${groupIndex + 1}, Action ${actionIndex + 1} Value`,
|
|
context: `Action value references variable`
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check legacy conditions structure
|
|
if (data.conditions && Array.isArray(data.conditions)) {
|
|
data.conditions.forEach((conditionGroup, groupIndex) => {
|
|
if (conditionGroup.conditions && Array.isArray(conditionGroup.conditions)) {
|
|
conditionGroup.conditions.forEach((condition, condIndex) => {
|
|
if (condition.variable === variableName) {
|
|
nodeUsages.push({
|
|
location: `Business Rule Condition Group ${groupIndex + 1}, Condition ${condIndex + 1}`,
|
|
context: `${condition.variable} ${condition.operator} ${condition.value}`
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Notification nodes
|
|
else if (node.type === 'notification') {
|
|
const data = node.data || {};
|
|
|
|
// Check recipient variables
|
|
if (data.recipientVariable === variableName) {
|
|
nodeUsages.push({
|
|
location: 'Notification Recipient Variable',
|
|
context: `Recipient variable: "${data.recipientVariable}"`
|
|
});
|
|
}
|
|
if (data.recipientUser === variableName) {
|
|
nodeUsages.push({
|
|
location: 'Notification Recipient User Variable',
|
|
context: `Recipient user variable: "${data.recipientUser}"`
|
|
});
|
|
}
|
|
if (data.recipientEmail === variableName) {
|
|
nodeUsages.push({
|
|
location: 'Notification Recipient Email Variable',
|
|
context: `Recipient email variable: "${data.recipientEmail}"`
|
|
});
|
|
}
|
|
|
|
// Check subject and message content for variable placeholders
|
|
if (data.subject && checkTextForVariable(data.subject, variableName)) {
|
|
nodeUsages.push({
|
|
location: 'Notification Subject',
|
|
context: `Subject contains variable reference`
|
|
});
|
|
}
|
|
if (data.message && checkTextForVariable(data.message, variableName)) {
|
|
nodeUsages.push({
|
|
location: 'Notification Message',
|
|
context: `Message contains variable reference`
|
|
});
|
|
}
|
|
if (data.content && checkTextForVariable(data.content, variableName)) {
|
|
nodeUsages.push({
|
|
location: 'Notification Content',
|
|
context: `Content contains variable reference`
|
|
});
|
|
}
|
|
|
|
// Check dynamic content fields
|
|
if (data.dynamicSubject && checkTextForVariable(data.dynamicSubject, variableName)) {
|
|
nodeUsages.push({
|
|
location: 'Notification Dynamic Subject',
|
|
context: `Dynamic subject contains variable reference`
|
|
});
|
|
}
|
|
if (data.dynamicMessage && checkTextForVariable(data.dynamicMessage, variableName)) {
|
|
nodeUsages.push({
|
|
location: 'Notification Dynamic Message',
|
|
context: `Dynamic message contains variable reference`
|
|
});
|
|
}
|
|
}
|
|
|
|
// Default case - for any custom node types, check common data fields
|
|
else {
|
|
const data = node.data || {};
|
|
|
|
if (data.outputVariable === variableName) {
|
|
nodeUsages.push({
|
|
location: 'Output Variable',
|
|
context: `Output variable: "${data.outputVariable}"`
|
|
});
|
|
}
|
|
if (data.inputVariable === variableName) {
|
|
nodeUsages.push({
|
|
location: 'Input Variable',
|
|
context: `Input variable: "${data.inputVariable}"`
|
|
});
|
|
}
|
|
|
|
// Check any text fields for variable references
|
|
Object.keys(data).forEach(key => {
|
|
const value = data[key];
|
|
if (typeof value === 'string' && checkTextForVariable(value, variableName)) {
|
|
nodeUsages.push({
|
|
location: `${key} field`,
|
|
context: `${key} field contains variable reference`
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add usages to the main list if any found
|
|
if (nodeUsages.length > 0) {
|
|
nodeTypes.add(node.type);
|
|
nodeUsages.forEach(usage => {
|
|
usages.push({
|
|
nodeId: node.id,
|
|
nodeType: node.type,
|
|
nodeLabel: nodeLabel,
|
|
...usage
|
|
});
|
|
totalUsages++;
|
|
});
|
|
}
|
|
});
|
|
|
|
return {
|
|
usages,
|
|
totalUsages,
|
|
nodeCount: usages.reduce((acc, usage) => {
|
|
return acc.add(usage.nodeId);
|
|
}, new Set()).size,
|
|
nodeTypes
|
|
};
|
|
};
|
|
|
|
// Show variable usage details in modal
|
|
const showVariableUsage = (variable) => {
|
|
selectedVariable.value = variable;
|
|
variableUsageDetails.value = getDetailedVariableUsage(variable.name);
|
|
showUsageModal.value = true;
|
|
};
|
|
|
|
const resetForm = () => {
|
|
variableForm.value = {
|
|
name: "",
|
|
type: "string",
|
|
scope: "global",
|
|
description: "",
|
|
defaultValue: ""
|
|
};
|
|
editingVariable.value = null;
|
|
};
|
|
|
|
const closeModal = () => {
|
|
showAddVariable.value = false;
|
|
resetForm();
|
|
};
|
|
|
|
const saveVariable = async (formData) => {
|
|
try {
|
|
// Process default value based on type
|
|
let processedDefaultValue = formData.defaultValue;
|
|
|
|
if (formData.type === 'int' && processedDefaultValue !== '') {
|
|
processedDefaultValue = parseInt(processedDefaultValue);
|
|
} else if (formData.type === 'decimal' && processedDefaultValue !== '') {
|
|
processedDefaultValue = parseFloat(processedDefaultValue);
|
|
} else if (formData.type === 'object' && processedDefaultValue !== '') {
|
|
try {
|
|
processedDefaultValue = JSON.parse(processedDefaultValue);
|
|
} catch (e) {
|
|
alert('Invalid JSON format for object type. Please enter valid JSON.');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Create a new variable object
|
|
const newVariable = {
|
|
name: formData.name,
|
|
type: formData.type,
|
|
scope: "global",
|
|
description: formData.description,
|
|
value: processedDefaultValue
|
|
};
|
|
|
|
if (editingVariable.value) {
|
|
// 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);
|
|
}
|
|
|
|
// Close modal and reset form
|
|
closeModal();
|
|
} catch (error) {
|
|
console.error("Error saving variable:", error);
|
|
alert('Error saving variable. Please check your input and try again.');
|
|
}
|
|
};
|
|
|
|
// Get badge color based on variable type
|
|
const getTypeColor = (type) => {
|
|
switch (type) {
|
|
case 'string': return 'info';
|
|
case 'int':
|
|
case 'decimal': return 'primary';
|
|
case 'object': return 'success';
|
|
case 'datetime':
|
|
case 'date': return 'warning';
|
|
case 'boolean': return 'secondary';
|
|
default: return 'secondary';
|
|
}
|
|
};
|
|
|
|
// Format variable value for display
|
|
const formatValue = (value, type) => {
|
|
if (value === undefined || value === null) return 'null';
|
|
|
|
switch (type) {
|
|
case 'object':
|
|
try {
|
|
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
} catch (e) {
|
|
return String(value);
|
|
}
|
|
case 'boolean':
|
|
return value ? 'true' : 'false';
|
|
default:
|
|
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';
|
|
}
|
|
};
|
|
|
|
// Get color class for variable type icon
|
|
const getTypeColorClass = (type) => {
|
|
switch (type) {
|
|
case 'string': return 'bg-blue-500';
|
|
case 'int':
|
|
case 'decimal': return 'bg-purple-500';
|
|
case 'object': return 'bg-green-500';
|
|
case 'datetime':
|
|
case 'date': return 'bg-amber-500';
|
|
case 'boolean': return 'bg-gray-500';
|
|
default: return 'bg-gray-500';
|
|
}
|
|
};
|
|
|
|
// Get node type icon
|
|
const getNodeTypeIcon = (nodeType) => {
|
|
switch (nodeType) {
|
|
case 'form': return 'material-symbols:description';
|
|
case 'api': return 'material-symbols:api';
|
|
case 'script': return 'material-symbols:code';
|
|
case 'decision': return 'material-symbols:fork-right';
|
|
case 'businessRule': return 'material-symbols:rule';
|
|
case 'notification': return 'material-symbols:notifications';
|
|
case 'start': return 'material-symbols:play-circle';
|
|
case 'end': return 'material-symbols:stop-circle';
|
|
default: return 'material-symbols:device-hub';
|
|
}
|
|
};
|
|
|
|
// Get node type color class
|
|
const getNodeTypeColor = (nodeType) => {
|
|
switch (nodeType) {
|
|
case 'form': return 'bg-blue-500';
|
|
case 'api': return 'bg-green-500';
|
|
case 'script': return 'bg-purple-500';
|
|
case 'decision': return 'bg-amber-500';
|
|
case 'businessRule': return 'bg-red-500';
|
|
case 'notification': return 'bg-orange-500';
|
|
case 'start': return 'bg-green-600';
|
|
case 'end': return 'bg-red-600';
|
|
default: return 'bg-gray-500';
|
|
}
|
|
};
|
|
|
|
// Navigate to a specific node in the canvas
|
|
const navigateToNode = (nodeId) => {
|
|
console.log('VariableManager: navigateToNode called with:', nodeId);
|
|
|
|
// Close the modal first
|
|
showUsageModal.value = false;
|
|
|
|
// Find the node and center the view on it
|
|
const nodes = processStore.currentProcess?.nodes || [];
|
|
const node = nodes.find(n => n.id === nodeId);
|
|
|
|
console.log('VariableManager: Found node:', node);
|
|
console.log('VariableManager: Available nodes:', nodes.map(n => ({ id: n.id, type: n.type, label: n.data?.label })));
|
|
|
|
if (node) {
|
|
// Select the node in the process store
|
|
processStore.selectNode(nodeId);
|
|
|
|
// Emit a custom event that the parent can listen to for highlighting and navigation
|
|
const event = new CustomEvent('highlightNode', {
|
|
detail: { nodeId, node }
|
|
});
|
|
|
|
console.log('VariableManager: Dispatching event:', event.detail);
|
|
window.dispatchEvent(event);
|
|
|
|
console.log(`Navigating to node: ${nodeId}`, node);
|
|
} else {
|
|
console.warn(`Node with ID ${nodeId} not found in current process`);
|
|
console.log('Available nodes:', nodes);
|
|
}
|
|
};
|
|
|
|
// Edit variable from usage modal
|
|
const editVariableFromUsage = () => {
|
|
if (selectedVariable.value) {
|
|
showUsageModal.value = false;
|
|
editVariable(selectedVariable.value);
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.variable-manager {
|
|
@apply h-full flex flex-col;
|
|
}
|
|
|
|
.variable-item {
|
|
@apply transition-all duration-200;
|
|
}
|
|
|
|
.variable-item:hover {
|
|
@apply transform -translate-y-0.5;
|
|
}
|
|
|
|
.line-clamp-1 {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 1;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Light styling for FormKit form */
|
|
: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;
|
|
}
|
|
</style>
|