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 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.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 // Add a component directly via click

View File

@ -269,10 +269,11 @@ const onDeleteKeyPress = () => {
// Handle drop event // Handle drop event
const onDrop = (event) => { const onDrop = (event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation();
try { try {
// Get the dragged component data // 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; if (!componentData) return;
// Get the Vue Flow wrapper element // Get the Vue Flow wrapper element
@ -296,7 +297,6 @@ const onDrop = (event) => {
} }
}; };
// console.log('Adding new node:', newNode);
addNodes([newNode]); addNodes([newNode]);
} catch (error) { } catch (error) {
console.error('Error handling drop:', error); console.error('Error handling drop:', error);
@ -306,7 +306,8 @@ const onDrop = (event) => {
// Handle drag over // Handle drag over
const onDragOver = (event) => { const onDragOver = (event) => {
event.preventDefault(); event.preventDefault();
event.dataTransfer.dropEffect = 'move'; event.stopPropagation();
event.dataTransfer.dropEffect = 'copy';
}; };
</script> </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> <script setup>
import { ref, onMounted, computed, shallowRef, onUnmounted } from 'vue'; import { ref, onMounted, computed, shallowRef, onUnmounted } from 'vue';
import { useProcessBuilderStore } from '~/stores/processBuilder'; import { useProcessBuilderStore } from '~/stores/processBuilder';
import { useVariableStore } from '~/stores/variableStore';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import ProcessFlowCanvas from '~/components/process-flow/ProcessFlowCanvas.vue'; import ProcessFlowCanvas from '~/components/process-flow/ProcessFlowCanvas.vue';
import ProcessBuilderComponents from '~/components/process-flow/ProcessBuilderComponents.vue'; import ProcessBuilderComponents from '~/components/process-flow/ProcessBuilderComponents.vue';
import FormSelector from '~/components/process-flow/FormSelector.vue'; import FormSelector from '~/components/process-flow/FormSelector.vue';
import GatewayConditionManager from '~/components/process-flow/GatewayConditionManager.vue'; import GatewayConditionManager from '~/components/process-flow/GatewayConditionManager.vue';
import VariableManager from '~/components/process-flow/VariableManager.vue';
import { onBeforeRouteLeave } from 'vue-router'; import { onBeforeRouteLeave } from 'vue-router';
// Define page meta // Define page meta
@ -20,6 +22,7 @@ definePageMeta({
// Initialize the store and router // Initialize the store and router
const processStore = useProcessBuilderStore(); const processStore = useProcessBuilderStore();
const router = useRouter(); const router = useRouter();
const variableStore = useVariableStore();
// Track selected node local state (syncs with store) // Track selected node local state (syncs with store)
// Using shallowRef to avoid making Vue components reactive // 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 // Handle node selection
const onNodeSelected = (node) => { const onNodeSelected = (node) => {
selectedNodeData.value = JSON.parse(JSON.stringify(node)); selectedNodeData.value = JSON.parse(JSON.stringify(node));
@ -560,136 +572,59 @@ const onConditionsUpdated = (conditions) => {
</div> </div>
<!-- Right Panel - Properties --> <!-- 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"> <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> <h2 class="text-sm font-medium text-gray-700">Properties</h2>
</div> </div>
<div class="flex-1 overflow-y-auto p-4 bg-white"> <div class="flex-1 overflow-y-auto">
<!-- No selection state --> <!-- Show variable manager when no node is selected -->
<div v-if="!selectedNodeData && !selectedEdgeData" class="text-gray-500 text-center py-8"> <VariableManager v-if="!selectedNodeData" />
<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> <!-- Show node properties when a node is selected -->
</div> <div v-else class="p-4 space-y-4">
<!-- Node Label -->
<!-- Node properties --> <div>
<div v-else-if="selectedNodeData" class="space-y-4"> <label class="block text-sm font-medium text-gray-700 mb-1">Label</label>
<h3 class="text-sm font-medium text-gray-700 mb-2">{{ selectedNodeData.type.charAt(0).toUpperCase() + selectedNodeData.type.slice(1) }} Node Properties</h3> <input
<!-- Common properties for all nodes -->
<div class="space-y-3">
<FormKit
v-model="nodeLabel" v-model="nodeLabel"
type="text" type="text"
label="Label" 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"
placeholder="Node label"
/>
<FormKit
v-model="nodeDescription"
type="textarea"
label="Description"
placeholder="Enter description"
:rows="3"
/> />
</div> </div>
<!-- Task specific properties --> <!-- Node Description -->
<div v-if="selectedNodeData.type === 'task'" class="space-y-3"> <div>
<FormKit <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" v-model="nodeAssignee"
type="text" type="text"
label="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"
placeholder="Enter assignee"
/> />
</div> </div>
<!-- Form specific properties --> <!-- Form Selection for Form Nodes -->
<div v-if="selectedNodeData.type === 'form'" class="space-y-3"> <div v-if="selectedNodeData.type === 'form'">
<FormSelector <FormSelector @select="onFormSelected" />
v-model="selectedNodeData.data.formId"
@select="onFormSelected"
/>
</div> </div>
<!-- Script specific properties --> <!-- Gateway Conditions -->
<div v-if="selectedNodeData.type === 'script'" class="space-y-3"> <div v-if="selectedNodeData.type === 'gateway'">
<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"
/>
<GatewayConditionManager <GatewayConditionManager
v-model="nodeConditions" :conditions="selectedNodeData.data.conditions"
:gateway-id="selectedNodeData.id" @update="onConditionsUpdated"
@update:modelValue="handleConditionUpdate" :available-variables="gatewayAvailableVariables"
/> />
</div> </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> </div>
</div> </div>

View File

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

View File

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

View File

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