corrad-bp/components/process-flow/VariableManager.vue
Afiq fa90c234c5 Enhance Process Builder with New Features and Configuration Options
- Added a new ProcessTemplatesModal component for selecting process flow templates, improving user experience in template management.
- Introduced a ProcessSettingsModal component for comprehensive process configuration, including process info, execution settings, and permissions management.
- Updated BusinessRuleNodeConfiguration and FormNodeConfiguration components to enhance user interaction and streamline configuration processes.
- Implemented new API endpoints for managing form fields and settings, allowing for better integration and data handling.
- Enhanced existing components with improved styling and functionality, including dynamic field conditions and bidirectional data mapping.
- Updated nuxt.config.js to include security settings for API routes, ensuring better protection against XSS and request size limitations.
- Removed the deprecated TaskNodeConfiguration component to streamline the process builder interface.
- Improved documentation to reflect recent changes and enhancements in the process builder features.
2025-05-27 17:00:24 +08:00

453 lines
14 KiB
Vue

<template>
<div class="variable-manager">
<!-- Header with Add Button -->
<div class="bg-gray-50 border-b border-gray-200 p-4">
<div class="flex items-center justify-between">
<div class="flex items-start">
<div class="mr-4 text-blue-500 flex-shrink-0 mt-1">
<Icon name="material-symbols:data-object" class="text-2xl" />
</div>
<div>
<h3 class="text-lg font-medium text-gray-900">Variables</h3>
<p class="mt-1 text-sm 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-4 pt-3 pb-2">
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Icon name="material-symbols:search" class="h-5 w-5 text-gray-400" />
</div>
<input
type="text"
v-model="searchQuery"
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
placeholder="Search variables..."
/>
</div>
</div>
<!-- Variable List -->
<div class="p-4 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>
<!-- Variable List -->
<div v-else-if="filteredVariables.length" class="space-y-4">
<div
v-for="variable in filteredVariables"
:key="variable.name"
class="variable-item group"
>
<div
class="bg-white rounded-xl border border-gray-200 hover:border-blue-300 hover:shadow-md transition-all duration-300 overflow-hidden"
>
<!-- Header with Name and Actions -->
<div class="flex items-center justify-between p-5 pb-3">
<div class="flex items-center gap-3 flex-1 min-w-0">
<!-- Variable Icon -->
<div class="flex-shrink-0 w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center">
<Icon
:name="getVariableIcon(variable.type)"
class="w-5 h-5 text-blue-600"
/>
</div>
<!-- Variable Name -->
<div class="flex-1 min-w-0">
<h4 class="text-lg font-semibold text-gray-900 truncate">
{{ variable.name }}
</h4>
<div class="flex items-center gap-2 mt-1">
<RsBadge :variant="getTypeColor(variable.type)" size="sm" class="font-medium">
{{ variable.type }}
</RsBadge>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex items-center gap-1 opacity-60 group-hover:opacity-100 transition-opacity">
<button
@click="editVariable(variable)"
class="p-2.5 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Edit variable"
>
<Icon name="material-symbols:edit" class="w-4 h-4" />
</button>
<button
@click="deleteVariable(variable)"
class="p-2.5 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Delete variable"
>
<Icon name="material-symbols:delete" class="w-4 h-4" />
</button>
</div>
</div>
<!-- Description -->
<div v-if="variable.description" class="px-5 pb-3">
<p class="text-sm text-gray-600 leading-relaxed">
{{ variable.description }}
</p>
</div>
<!-- Current Value (if set) -->
<div v-if="variable.value !== undefined && variable.value !== ''" class="px-5 pb-4">
<div class="bg-gray-50 rounded-lg p-3 border border-gray-100">
<div class="flex items-center gap-2 mb-2">
<Icon name="material-symbols:code" class="w-4 h-4 text-gray-500" />
<span class="text-xs font-medium text-gray-500 uppercase tracking-wide">Current Value</span>
</div>
<div class="font-mono text-sm text-gray-800 break-all">
{{ formatValue(variable.value, variable.type) }}
</div>
</div>
</div>
<!-- Default Value (if no current value but has default) -->
<div v-else-if="variable.defaultValue !== undefined && variable.defaultValue !== ''" class="px-5 pb-4">
<div class="bg-amber-50 rounded-lg p-3 border border-amber-100">
<div class="flex items-center gap-2 mb-2">
<Icon name="material-symbols:settings" class="w-4 h-4 text-amber-600" />
<span class="text-xs font-medium text-amber-600 uppercase tracking-wide">Default Value</span>
</div>
<div class="font-mono text-sm text-amber-800 break-all">
{{ formatValue(variable.defaultValue, variable.type) }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 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>
<!-- 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="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 { useVariableStore } from "~/stores/variableStore";
const variableStore = useVariableStore();
// State
const showAddVariable = ref(false);
const editingVariable = ref(null);
const searchQuery = ref("");
const variableForm = ref({
name: "",
type: "string",
scope: "global",
description: "",
});
// Variable type options with descriptions
const variableTypes = [
{ label: 'String - Text values', value: 'string' },
{ label: 'Int - Whole numbers', value: 'int' },
{ label: 'Decimal - Decimal numbers', value: 'decimal' },
{ 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' }
];
// Computed
const variables = computed(() => {
// Return all variables (treating everything as global)
const allVars = variableStore.getAllVariables;
return [...allVars.global, ...allVars.process];
});
// 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)
);
});
// Methods
const editVariable = (variable) => {
editingVariable.value = variable;
variableForm.value = { ...variable };
showAddVariable.value = true;
};
const deleteVariable = (variable) => {
if (confirm(`Are you sure you want to delete the variable "${variable.name}"? This might affect parts of your process that use this variable.`)) {
variableStore.deleteVariable(variable.name, 'global');
}
};
const resetForm = () => {
variableForm.value = {
name: "",
type: "string",
scope: "global",
description: ""
};
editingVariable.value = null;
};
const closeModal = () => {
showAddVariable.value = false;
resetForm();
};
const saveVariable = async (formData) => {
try {
// Create a new variable object
const newVariable = {
name: formData.name,
type: formData.type,
scope: "global",
description: formData.description
};
if (editingVariable.value) {
// Update existing variable
variableStore.updateVariable(
editingVariable.value.name,
newVariable,
'global'
);
} else {
// Add new variable
variableStore.addVariable(newVariable);
}
// Close modal and reset form
closeModal();
} catch (error) {
console.error("Error saving variable:", error);
// You might want to show an error message to the user here
}
};
// 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';
}
};
</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;
}
/* 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>