Enhance Process Builder Drag-and-Drop Functionality

- Improved drag-and-drop support in the Process Builder by setting drag data to 'text/plain' for better compatibility, especially on Mac.
- Added visual feedback during drag operations by applying a 'dragging' class to the event target.
- Updated the drop event handler to parse the dragged data correctly and prevent event propagation.
- Introduced a computed property for gateway available variables to enhance node configuration options.
- Adjusted the layout of the properties panel to accommodate a new VariableManager component for better variable management.
This commit is contained in:
Afiq 2025-05-15 11:53:59 +08:00
parent 668e08884e
commit b3ca62b548
9 changed files with 773 additions and 176 deletions

View File

@ -142,9 +142,13 @@ const onDragStart = (event, component) => {
data: component.defaultProps.data
};
// Set the drag data
// Set the drag data with text/plain format for better Mac compatibility
event.dataTransfer.effectAllowed = 'copy';
event.dataTransfer.setData('application/json', JSON.stringify(componentData));
event.dataTransfer.dropEffect = 'copy';
event.dataTransfer.setData('text/plain', JSON.stringify(componentData));
// Add visual feedback
event.target.classList.add('dragging');
};
// Add a component directly via click

View File

@ -269,10 +269,11 @@ const onDeleteKeyPress = () => {
// Handle drop event
const onDrop = (event) => {
event.preventDefault();
event.stopPropagation();
try {
// Get the dragged component data
const componentData = JSON.parse(event.dataTransfer.getData('application/json'));
const componentData = JSON.parse(event.dataTransfer.getData('text/plain'));
if (!componentData) return;
// Get the Vue Flow wrapper element
@ -296,7 +297,6 @@ const onDrop = (event) => {
}
};
// console.log('Adding new node:', newNode);
addNodes([newNode]);
} catch (error) {
console.error('Error handling drop:', error);
@ -306,7 +306,8 @@ const onDrop = (event) => {
// Handle drag over
const onDragOver = (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
event.stopPropagation();
event.dataTransfer.dropEffect = 'copy';
};
</script>

View File

@ -0,0 +1,280 @@
<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>
<h3 class="text-lg font-medium text-gray-900">Process Variables</h3>
<p class="mt-1 text-sm text-gray-500">Manage variables for your process flow</p>
</div>
<RsButton
@click="() => {
resetForm();
showAddVariable = true;
}"
variant="primary"
size="sm"
>
<Icon name="material-symbols:add" class="mr-1" />
Add Variable
</RsButton>
</div>
</div>
<!-- Variable List -->
<div class="p-4">
<!-- Empty State -->
<div v-if="!variables.length" class="text-center py-8">
<Icon name="material-symbols:data-object" class="w-12 h-12 mx-auto mb-3 text-gray-400" />
<h4 class="text-sm font-medium text-gray-900 mb-1">No Variables Added</h4>
<p class="text-sm text-gray-500 mb-4">Add variables to store and manage data in your process</p>
<RsButton
@click="() => {
resetForm();
showAddVariable = true;
}"
variant="secondary"
size="sm"
>
<Icon name="material-symbols:add" class="mr-1" />
Add Your First Variable
</RsButton>
</div>
<!-- Variable List -->
<div v-else class="space-y-2">
<div v-for="variable in variables" :key="variable.name" class="variable-item">
<div class="flex items-center justify-between p-3 bg-white rounded-lg border border-gray-200 hover:border-blue-200 hover:shadow-sm transition-all duration-200">
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="font-medium text-gray-900">{{ variable.name }}</span>
<RsBadge
:variant="variable.scope === 'global' ? 'primary' : 'secondary'"
size="sm"
>
{{ variable.scope }}
</RsBadge>
<RsBadge
variant="outline"
size="sm"
class="text-gray-500"
>
{{ variable.type }}
</RsBadge>
</div>
<p v-if="variable.description" class="mt-1 text-sm text-gray-500">
{{ variable.description }}
</p>
</div>
<div class="flex items-center gap-2 ml-4">
<button
@click="editVariable(variable)"
class="p-1.5 text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded-md transition-colors"
title="Edit variable"
>
<Icon name="material-symbols:edit" class="w-4 h-4" />
</button>
<button
@click="deleteVariable(variable)"
class="p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-md transition-colors"
title="Delete variable"
>
<Icon name="material-symbols:delete" class="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Add/Edit Variable Modal -->
<RsModal
v-model="showAddVariable"
:title="editingVariable ? 'Edit Variable' : 'Add Variable'"
size="md"
:hideFooter="true"
:overlayClose="false"
>
<FormKit
type="form"
@submit="saveVariable"
:actions="false"
class="space-y-4"
>
<FormKit
v-model="variableForm.name"
type="text"
label="Name"
placeholder="Enter variable name"
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'
}"
/>
<FormKit
v-model="variableForm.type"
type="select"
label="Type"
:options="[
{ label: 'String', value: 'string' },
{ label: 'Number', value: 'number' },
{ label: 'Boolean', value: 'boolean' },
{ label: 'Object', value: 'object' },
{ label: 'Array', value: 'array' },
{ label: 'Date', value: 'date' },
{ label: 'File', value: 'file' }
]"
validation="required"
:validation-messages="{
required: 'Variable type is required'
}"
/>
<FormKit
v-model="variableForm.scope"
type="select"
label="Scope"
:options="[
{ label: 'Process', value: 'process' },
{ label: 'Global', value: 'global' }
]"
validation="required"
:validation-messages="{
required: 'Variable scope is required'
}"
/>
<FormKit
v-model="variableForm.description"
type="textarea"
label="Description"
placeholder="Enter variable description"
:rows="2"
/>
<FormKit
v-model="variableForm.isRequired"
type="checkbox"
label="Required"
help="Mark this variable as required"
/>
<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' : 'Add' }}
</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 variableForm = ref({
name: '',
type: 'string',
scope: 'process',
description: '',
isRequired: false
});
// Computed
const variables = computed(() => {
return variableStore.getAllVariables.process;
});
// 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 ${variable.name}?`)) {
variableStore.deleteVariable(variable.name, variable.scope);
}
};
const resetForm = () => {
variableForm.value = {
name: '',
type: 'string',
scope: 'process',
description: '',
isRequired: false
};
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: formData.scope,
description: formData.description,
isRequired: formData.isRequired
};
if (editingVariable.value) {
// Update existing variable
variableStore.updateVariable(
editingVariable.value.name,
newVariable,
newVariable.scope
);
} 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
}
};
</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-1;
}
</style>

View File

@ -0,0 +1,255 @@
# Process Builder Improvements
## Overview
This document outlines the planned improvements for the Process Builder core components. The improvements are designed to be manageable and maintainable while adding essential functionality.
## Variable System
### 1. Global Variables
```javascript
{
name: 'string',
type: 'string|number|boolean|object|array',
defaultValue: any,
description: 'string',
scope: 'global',
isRequired: boolean,
isReadOnly: boolean
}
```
### 2. Process Variables
```javascript
{
name: 'string',
type: 'string|number|boolean|object|array',
defaultValue: any,
description: 'string',
scope: 'process',
isRequired: boolean,
isReadOnly: boolean,
direction: 'in|out|inout' // for process arguments
}
```
### 3. Task/Form Arguments
```javascript
{
name: 'string',
type: 'string|number|boolean|object|array',
defaultValue: any,
description: 'string',
direction: 'in|out|inout',
isRequired: boolean,
validation: {
rules: [],
customValidation: 'string' // custom validation script
}
}
```
## Core Components Improvements
### 1. Start Event
```javascript
{
type: 'start',
data: {
description: 'Process start point',
triggerType: 'manual', // manual, scheduled
schedule: null, // for scheduled triggers
variables: {
input: [], // process input arguments
output: [] // process output arguments
},
globalVariables: [] // global variables used in this process
}
}
```
### 2. End Event
```javascript
{
type: 'end',
data: {
description: 'Process end point',
resultType: 'success', // success, error
variables: {
input: [], // variables required for end event
output: [] // variables to be returned
},
returnValues: [] // values to return to calling process
}
}
```
### 3. Task
```javascript
{
type: 'task',
data: {
description: 'A general task',
assignee: '',
taskType: 'manual', // manual, automated
priority: 'medium', // low, medium, high
dueDate: null,
variables: {
input: [], // task input arguments
output: [] // task output arguments
},
notifications: {
onAssign: true,
onComplete: true
}
}
}
```
### 4. Form Task
```javascript
{
type: 'form',
data: {
description: 'Form submission task',
formId: null,
formName: null,
formSettings: {
allowDraft: true,
autoSave: true
},
variables: {
input: [], // form input arguments
output: [] // form output arguments
},
dataMapping: {
input: [], // map process variables to form
output: [] // map form to process variables
}
}
}
```
### 5. Gateway
```javascript
{
type: 'gateway',
data: {
description: 'Decision gateway',
conditions: [],
defaultPath: 'Default',
gatewayType: 'exclusive', // exclusive, parallel
variables: {
input: [], // variables needed for conditions
output: [] // variables to pass to next node
},
timeout: {
enabled: false,
duration: 0
}
}
}
```
## New Core Components
### 1. Script Task
```javascript
{
type: 'script',
data: {
description: 'Execute custom script',
scriptType: 'javascript',
script: '',
variables: {
input: [], // script input arguments
output: [] // script output arguments
},
timeout: 30 // seconds
}
}
```
## Implementation Priority
### Phase 1 - Essential Improvements
1. Implement basic variable system
- Global variables
- Process variables
- Task/Form arguments
2. Add basic trigger types to Start Event
3. Add result types to End Event
4. Add task priorities and due dates
5. Add form settings for drafts and auto-save
### Phase 2 - Enhanced Features
1. Add variable validation system
2. Add data mapping for forms
3. Add script task component
4. Add timeout handling
5. Add notifications system
### Phase 3 - Advanced Features
1. Add subprocess component
2. Add advanced gateway conditions
3. Add process templates
4. Add process versioning
5. Add process analytics
## Variable System Features
### 1. Variable Types
- String
- Number
- Boolean
- Object
- Array
- Date
- File
- Custom types
### 2. Variable Scopes
- Global (accessible across all processes)
- Process (accessible within a process)
- Task/Form (accessible within a task/form)
- Local (accessible within a script)
### 3. Variable Operations
- Create/Delete
- Read/Write
- Copy/Move
- Transform
- Validate
- Persist
### 4. Variable Passing
- Process to Process
- Task to Task
- Form to Process
- Script to Process
- Gateway Conditions
## Notes
- Keep improvements focused on essential functionality
- Maintain backward compatibility
- Ensure easy maintenance
- Document all new features
- Add proper validation
- Include error handling
- Implement proper variable scoping
- Add variable type checking
- Include variable persistence
## Future Considerations
- Process templates
- Process versioning
- Process analytics
- Advanced notifications
- Custom validations
- Process documentation
- Process testing
- Process deployment
- Variable encryption
- Variable versioning
- Variable dependencies
Last updated: June 10, 2024

View File

@ -1,11 +1,13 @@
<script setup>
import { ref, onMounted, computed, shallowRef, onUnmounted } from 'vue';
import { useProcessBuilderStore } from '~/stores/processBuilder';
import { useVariableStore } from '~/stores/variableStore';
import { useRouter } from 'vue-router';
import ProcessFlowCanvas from '~/components/process-flow/ProcessFlowCanvas.vue';
import ProcessBuilderComponents from '~/components/process-flow/ProcessBuilderComponents.vue';
import FormSelector from '~/components/process-flow/FormSelector.vue';
import GatewayConditionManager from '~/components/process-flow/GatewayConditionManager.vue';
import VariableManager from '~/components/process-flow/VariableManager.vue';
import { onBeforeRouteLeave } from 'vue-router';
// Define page meta
@ -20,6 +22,7 @@ definePageMeta({
// Initialize the store and router
const processStore = useProcessBuilderStore();
const router = useRouter();
const variableStore = useVariableStore();
// Track selected node local state (syncs with store)
// Using shallowRef to avoid making Vue components reactive
@ -169,6 +172,15 @@ const nodeDefaultPath = computed({
}
});
// Computed for gateway available variables
const gatewayAvailableVariables = computed(() => {
return variableStore.getAllVariables.process.map(v => ({
name: v.name,
label: v.name, // or v.description || v.name
type: v.type
}));
});
// Handle node selection
const onNodeSelected = (node) => {
selectedNodeData.value = JSON.parse(JSON.stringify(node));
@ -560,136 +572,59 @@ const onConditionsUpdated = (conditions) => {
</div>
<!-- Right Panel - Properties -->
<div class="w-72 border-l border-gray-200 flex flex-col overflow-hidden">
<div class="w-80 border-l border-gray-200 flex flex-col overflow-hidden">
<div class="bg-gray-100 p-3 flex items-center justify-between border-b border-gray-200">
<h2 class="text-sm font-medium text-gray-700">Properties</h2>
</div>
<div class="flex-1 overflow-y-auto p-4 bg-white">
<!-- No selection state -->
<div v-if="!selectedNodeData && !selectedEdgeData" class="text-gray-500 text-center py-8">
<Icon name="material-symbols:touch-app" class="w-12 h-12 mx-auto mb-2" />
<p>Select a node or connection to edit its properties</p>
</div>
<!-- Node properties -->
<div v-else-if="selectedNodeData" class="space-y-4">
<h3 class="text-sm font-medium text-gray-700 mb-2">{{ selectedNodeData.type.charAt(0).toUpperCase() + selectedNodeData.type.slice(1) }} Node Properties</h3>
<!-- Common properties for all nodes -->
<div class="space-y-3">
<FormKit
<div class="flex-1 overflow-y-auto">
<!-- Show variable manager when no node is selected -->
<VariableManager v-if="!selectedNodeData" />
<!-- Show node properties when a node is selected -->
<div v-else class="p-4 space-y-4">
<!-- Node Label -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Label</label>
<input
v-model="nodeLabel"
type="text"
label="Label"
placeholder="Node label"
/>
<FormKit
v-model="nodeDescription"
type="textarea"
label="Description"
placeholder="Enter description"
:rows="3"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<!-- Task specific properties -->
<div v-if="selectedNodeData.type === 'task'" class="space-y-3">
<FormKit
<!-- Node Description -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea
v-model="nodeDescription"
rows="2"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<!-- Node Type Specific Properties -->
<div v-if="selectedNodeData.type === 'task'">
<label class="block text-sm font-medium text-gray-700 mb-1">Assignee</label>
<input
v-model="nodeAssignee"
type="text"
label="Assignee"
placeholder="Enter assignee"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<!-- Form specific properties -->
<div v-if="selectedNodeData.type === 'form'" class="space-y-3">
<FormSelector
v-model="selectedNodeData.data.formId"
@select="onFormSelected"
/>
<!-- Form Selection for Form Nodes -->
<div v-if="selectedNodeData.type === 'form'">
<FormSelector @select="onFormSelected" />
</div>
<!-- Script specific properties -->
<div v-if="selectedNodeData.type === 'script'" class="space-y-3">
<FormKit
v-model="nodeLanguage"
type="select"
label="Language"
:options="['JavaScript', 'Python', 'PHP']"
/>
<FormKit
v-if="selectedNodeData.data.script !== undefined"
v-model="selectedNodeData.data.script"
type="textarea"
label="Script"
placeholder="Enter script code"
:rows="5"
/>
</div>
<!-- Gateway specific properties -->
<div v-if="selectedNodeData.type === 'gateway'" class="space-y-3">
<FormKit
v-model="nodeDefaultPath"
type="text"
label="Default Path Label"
placeholder="Default"
/>
<!-- Gateway Conditions -->
<div v-if="selectedNodeData.type === 'gateway'">
<GatewayConditionManager
v-model="nodeConditions"
:gateway-id="selectedNodeData.id"
@update:modelValue="handleConditionUpdate"
:conditions="selectedNodeData.data.conditions"
@update="onConditionsUpdated"
:available-variables="gatewayAvailableVariables"
/>
</div>
<!-- Delete button -->
<div class="pt-4 border-t border-gray-200 mt-4">
<RsButton @click="deleteNode" variant="danger" size="sm" class="w-full">
<Icon name="material-symbols:delete" class="mr-1" />
Delete Node
</RsButton>
</div>
</div>
<!-- Edge properties -->
<div v-else-if="selectedEdgeData" class="space-y-4">
<h3 class="text-sm font-medium text-gray-700 mb-2">Connection Properties</h3>
<div class="space-y-3">
<FormKit
:model-value="selectedEdgeData.label"
@input="updateEdgeLabel"
type="text"
label="Label"
placeholder="Connection label"
/>
<div class="pt-4">
<div class="mb-2 text-sm text-gray-500">Connection Details</div>
<div class="p-3 bg-gray-50 rounded-md border border-gray-200 text-sm">
<div class="mb-1">
<span class="font-medium">From:</span>
{{ selectedEdgeData.sourceNode?.label || selectedEdgeData.source }}
</div>
<div>
<span class="font-medium">To:</span>
{{ selectedEdgeData.targetNode?.label || selectedEdgeData.target }}
</div>
</div>
</div>
<!-- Delete button -->
<div class="pt-4 border-t border-gray-200 mt-4">
<RsButton @click="deleteEdge" variant="danger" size="sm" class="w-full">
<Icon name="material-symbols:delete" class="mr-1" />
Delete Connection
</RsButton>
</div>
</div>
</div>
</div>
</div>

View File

@ -136,10 +136,10 @@
"$ref": "#/definitions/audit"
}
},
"userrole": {
"forms": {
"type": "array",
"items": {
"$ref": "#/definitions/userrole"
"$ref": "#/definitions/form"
}
},
"processes": {
@ -148,17 +148,17 @@
"$ref": "#/definitions/process"
}
},
"forms": {
"type": "array",
"items": {
"$ref": "#/definitions/form"
}
},
"assignedTasks": {
"type": "array",
"items": {
"$ref": "#/definitions/task"
}
},
"userrole": {
"type": "array",
"items": {
"$ref": "#/definitions/userrole"
}
}
}
},
@ -453,8 +453,15 @@
],
"format": "date-time"
},
"process": {
"$ref": "#/definitions/process"
"assignee": {
"anyOf": [
{
"$ref": "#/definitions/user"
},
{
"type": "null"
}
]
},
"form": {
"anyOf": [
@ -466,15 +473,8 @@
}
]
},
"assignee": {
"anyOf": [
{
"$ref": "#/definitions/user"
},
{
"type": "null"
}
]
"process": {
"$ref": "#/definitions/process"
}
}
}

View File

@ -40,10 +40,10 @@ model user {
userCreatedDate DateTime? @db.DateTime(0)
userModifiedDate DateTime? @db.DateTime(0)
audit audit[]
userrole userrole[]
processes process[] @relation("ProcessCreator")
forms form[] @relation("FormCreator")
processes process[] @relation("ProcessCreator")
assignedTasks task[] @relation("TaskAssignee")
userrole userrole[]
}
model role {
@ -80,39 +80,33 @@ model userrole {
@@index([userRoleUserID], map: "FK_userrole_user")
}
// New models for Form Builder
model form {
formID Int @id @default(autoincrement())
formUUID String @unique @db.VarChar(36)
formName String @db.VarChar(255)
formDescription String? @db.Text
formComponents Json @db.Json
formComponents Json
formStatus String @default("active") @db.VarChar(50)
formCreatedBy Int?
formCreatedDate DateTime @default(now()) @db.DateTime(0)
formModifiedDate DateTime? @updatedAt @db.DateTime(0)
// Relations
creator user? @relation("FormCreator", fields: [formCreatedBy], references: [userID])
formTasks task[] @relation("FormTask")
@@index([formCreatedBy], map: "FK_form_creator")
}
// New models for Process Builder
model process {
processID Int @id @default(autoincrement())
processUUID String @unique @db.VarChar(36)
processName String @db.VarChar(255)
processDescription String? @db.Text
processDefinition Json @db.Json
processDefinition Json
processVersion Int @default(1)
processStatus String @default("draft") @db.VarChar(50)
processCreatedBy Int?
processCreatedDate DateTime @default(now()) @db.DateTime(0)
processModifiedDate DateTime? @updatedAt @db.DateTime(0)
// Relations
creator user? @relation("ProcessCreator", fields: [processCreatedBy], references: [userID])
tasks task[]
@ -125,18 +119,16 @@ model task {
taskNodeId String @db.VarChar(255)
taskName String @db.VarChar(255)
taskType String @db.VarChar(50)
taskData Json? @db.Json
taskData Json?
taskProcessId Int
taskFormId Int?
taskAssigneeId Int?
taskStatus String @default("pending") @db.VarChar(50)
taskCreatedDate DateTime @default(now()) @db.DateTime(0)
taskModifiedDate DateTime? @updatedAt @db.DateTime(0)
// Relations
process process @relation(fields: [taskProcessId], references: [processID])
form form? @relation("FormTask", fields: [taskFormId], references: [formID])
assignee user? @relation("TaskAssignee", fields: [taskAssigneeId], references: [userID])
form form? @relation("FormTask", fields: [taskFormId], references: [formID])
process process @relation(fields: [taskProcessId], references: [processID])
@@index([taskProcessId], map: "FK_task_process")
@@index([taskFormId], map: "FK_task_form")

View File

@ -1,5 +1,6 @@
import { defineStore } from 'pinia';
import { v4 as uuidv4 } from 'uuid';
import { useVariableStore } from './variableStore';
export const useProcessBuilderStore = defineStore('processBuilder', {
state: () => ({
@ -63,35 +64,52 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
* Create a new process
*/
createProcess(name, description = '') {
const newProcess = {
id: uuidv4(),
const process = {
id: crypto.randomUUID(),
name,
description,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
nodes: [],
edges: []
edges: [],
variables: {},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
this.processes.push(newProcess);
this.setCurrentProcess(newProcess.id);
this.saveToHistory('Create process');
this.processes.push(process);
this.currentProcess = process;
this.unsavedChanges = true;
return newProcess;
// Clear any existing variables
useVariableStore().clearProcessVariables();
return process;
},
/**
* Load a process
*/
loadProcess(processId) {
const process = this.processes.find(p => p.id === processId);
if (process) {
this.currentProcess = JSON.parse(JSON.stringify(process)); // Deep clone
this.selectedNodeId = null;
this.selectedEdgeId = null;
this.clearHistory();
this.unsavedChanges = false;
async loadProcess(processId) {
try {
// TODO: Implement API call to load process
// For now, just load from local state
const process = this.processes.find(p => p.id === processId);
if (process) {
this.currentProcess = process;
// Load variables into variable store
if (process.variables) {
const variableStore = useVariableStore();
Object.entries(process.variables).forEach(([name, variable]) => {
variableStore.addVariable(variable);
});
}
return true;
}
return false;
} catch (error) {
console.error('Error loading process:', error);
return false;
}
},
@ -112,14 +130,30 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
/**
* Save the current process
*/
saveProcess() {
async saveProcess() {
if (!this.currentProcess) return;
const index = this.processes.findIndex(p => p.id === this.currentProcess.id);
if (index !== -1) {
this.currentProcess.updatedAt = new Date().toISOString();
this.processes[index] = JSON.parse(JSON.stringify(this.currentProcess)); // Deep clone
try {
// Save process data
const processData = {
...this.currentProcess,
variables: useVariableStore().getAllVariables.process
};
// TODO: Implement API call to save process
// For now, just update local state
const index = this.processes.findIndex(p => p.id === this.currentProcess.id);
if (index !== -1) {
this.processes[index] = processData;
} else {
this.processes.push(processData);
}
this.unsavedChanges = false;
return true;
} catch (error) {
console.error('Error saving process:', error);
return false;
}
},

96
stores/variableStore.js Normal file
View File

@ -0,0 +1,96 @@
import { defineStore } from 'pinia';
export const useVariableStore = defineStore('variables', {
state: () => ({
// Global variables accessible across all processes
globalVariables: [],
// Current process variables
processVariables: [],
// Variables for the currently selected node
nodeVariables: {
input: [],
output: []
}
}),
getters: {
// Get all variables
getAllVariables: (state) => {
return {
global: state.globalVariables,
process: state.processVariables,
node: state.nodeVariables
};
},
// Get variable by name and scope
getVariable: (state) => (name, scope = 'process') => {
const variables = scope === 'global'
? state.globalVariables
: state.processVariables;
return variables.find(v => v.name === name);
}
},
actions: {
// Add a new variable
addVariable(variable) {
const { scope = 'process' } = variable;
if (scope === 'global') {
this.globalVariables.push({
...variable,
scope: 'global'
});
} else {
this.processVariables.push({
...variable,
scope: 'process'
});
}
},
// Update an existing variable
updateVariable(name, updates, scope = 'process') {
const variables = scope === 'global'
? this.globalVariables
: this.processVariables;
const index = variables.findIndex(v => v.name === name);
if (index !== -1) {
variables[index] = {
...variables[index],
...updates
};
}
},
// Delete a variable
deleteVariable(name, scope = 'process') {
const variables = scope === 'global'
? this.globalVariables
: this.processVariables;
const index = variables.findIndex(v => v.name === name);
if (index !== -1) {
variables.splice(index, 1);
}
},
// Set node variables
setNodeVariables(variables) {
this.nodeVariables = variables;
},
// Clear process variables (when switching processes)
clearProcessVariables() {
this.processVariables = [];
this.nodeVariables = {
input: [],
output: []
};
}
}
});