From bb5e4c0637672d1d5f3a07b2f4c8a81c4111b2f1 Mon Sep 17 00:00:00 2001 From: Md Afiq Iskandar Date: Thu, 15 May 2025 10:27:55 +0800 Subject: [PATCH] Add Form and Process Management Features - Introduced new components for form selection and gateway condition management within the process builder. - Implemented a `FormSelector` component for selecting and managing forms, including search functionality and loading states. - Developed a `GatewayConditionManager` component to manage conditions for gateways, allowing users to define and edit conditions visually. - Created a `ProcessBuilderComponents` component to facilitate the addition of core components in the process builder. - Enhanced the `ProcessFlowCanvas` to support new features, including edge selection and improved node management. - Updated the backend API to handle CRUD operations for forms and processes, including error handling for associated tasks. - Integrated new database models for forms and processes in Prisma, ensuring proper relationships and data integrity. - Improved state management in the form builder store to accommodate new features and enhance user experience. --- components/process-flow/FormSelector.vue | 263 ++++++ .../process-flow/GatewayConditionManager.vue | 270 ++++++ .../process-flow/ProcessBuilderComponents.vue | 180 ++++ components/process-flow/ProcessFlowCanvas.vue | 196 ++-- components/process-flow/ProcessFlowNodes.js | 347 +++++-- pages/form-builder/index.vue | 53 +- pages/form-builder/manage.vue | 52 +- pages/process-builder/index.vue | 844 +++++++++++++----- pages/process-builder/manage.vue | 242 ++--- prisma/json/json-schema.json | 221 +++++ prisma/schema.prisma | 66 ++ server/api/forms/[id].delete.js | 57 ++ server/api/forms/[id].get.js | 64 ++ server/api/forms/[id].put.js | 78 ++ server/api/forms/create.post.js | 45 + server/api/forms/index.get.js | 47 + server/api/processes/[id].put.js | 85 ++ .../api/processes/[id]/tasks/create.post.js | 71 ++ server/api/processes/create.post.js | 46 + server/api/tasks/[id]/form.delete.js | 63 ++ server/api/tasks/[id]/form.post.js | 99 ++ stores/formBuilder.js | 236 +++-- stores/processBuilder.js | 45 +- 23 files changed, 3072 insertions(+), 598 deletions(-) create mode 100644 components/process-flow/FormSelector.vue create mode 100644 components/process-flow/GatewayConditionManager.vue create mode 100644 components/process-flow/ProcessBuilderComponents.vue create mode 100644 server/api/forms/[id].delete.js create mode 100644 server/api/forms/[id].get.js create mode 100644 server/api/forms/[id].put.js create mode 100644 server/api/forms/create.post.js create mode 100644 server/api/forms/index.get.js create mode 100644 server/api/processes/[id].put.js create mode 100644 server/api/processes/[id]/tasks/create.post.js create mode 100644 server/api/processes/create.post.js create mode 100644 server/api/tasks/[id]/form.delete.js create mode 100644 server/api/tasks/[id]/form.post.js diff --git a/components/process-flow/FormSelector.vue b/components/process-flow/FormSelector.vue new file mode 100644 index 0000000..e4a5fc6 --- /dev/null +++ b/components/process-flow/FormSelector.vue @@ -0,0 +1,263 @@ + + + + + \ No newline at end of file diff --git a/components/process-flow/GatewayConditionManager.vue b/components/process-flow/GatewayConditionManager.vue new file mode 100644 index 0000000..4859de7 --- /dev/null +++ b/components/process-flow/GatewayConditionManager.vue @@ -0,0 +1,270 @@ + + + + + \ No newline at end of file diff --git a/components/process-flow/ProcessBuilderComponents.vue b/components/process-flow/ProcessBuilderComponents.vue new file mode 100644 index 0000000..216f18d --- /dev/null +++ b/components/process-flow/ProcessBuilderComponents.vue @@ -0,0 +1,180 @@ + + + + + \ No newline at end of file diff --git a/components/process-flow/ProcessFlowCanvas.vue b/components/process-flow/ProcessFlowCanvas.vue index cce5d3d..e52eaba 100644 --- a/components/process-flow/ProcessFlowCanvas.vue +++ b/components/process-flow/ProcessFlowCanvas.vue @@ -27,7 +27,7 @@ const props = defineProps({ } }); -const emit = defineEmits(['nodeClick', 'edgeClick', 'paneClick', 'nodesChange', 'edgesChange', 'nodeSelected']); +const emit = defineEmits(['nodeClick', 'edgeClick', 'paneClick', 'nodesChange', 'edgesChange', 'nodeSelected', 'edgeSelected']); // Get the flow instance const { flowInstance } = useVueFlow(); @@ -57,110 +57,10 @@ const { }); // Default nodes if empty -const defaultNodes = [ - { - id: 'start', - type: 'start', - label: 'Start Process', - position: { x: 100, y: 100 }, - data: { - description: 'Process starts here' - } - }, - { - id: 'form1', - type: 'form', - label: 'Request Form', - position: { x: 100, y: 250 }, - data: { - description: 'User fills out request form', - formName: 'Request Form' - } - }, - { - id: 'gateway', - type: 'gateway', - label: 'Approval Required?', - position: { x: 100, y: 400 }, - data: { - description: 'Check if approval is required', - conditions: ['Amount > 1000', 'Special Request'] - } - }, - { - id: 'task1', - type: 'task', - label: 'Manager Approval', - position: { x: 250, y: 550 }, - data: { - description: 'Manager reviews the request', - assignee: 'Department Manager' - } - }, - { - id: 'script1', - type: 'script', - label: 'Process Request', - position: { x: -50, y: 550 }, - data: { - description: 'Automatically process the request', - language: 'JavaScript' - } - }, - { - id: 'end', - type: 'end', - label: 'End Process', - position: { x: 100, y: 700 }, - data: { - description: 'Process completes here' - } - } -]; +const defaultNodes = []; // Default edges if empty -const defaultEdges = [ - { - id: 'start-form1', - source: 'start', - target: 'form1', - animated: true - }, - { - id: 'form1-gateway', - source: 'form1', - target: 'gateway', - animated: true - }, - { - id: 'gateway-task1', - source: 'gateway', - target: 'task1', - animated: true, - label: 'Yes', - type: 'smoothstep' - }, - { - id: 'gateway-script1', - source: 'gateway', - target: 'script1', - animated: true, - label: 'No', - type: 'smoothstep' - }, - { - id: 'task1-end', - source: 'task1', - target: 'end', - animated: true - }, - { - id: 'script1-end', - source: 'script1', - target: 'end', - animated: true - } -]; +const defaultEdges = []; // Flow configuration const flowOptions = ref({ @@ -219,7 +119,18 @@ const onNodeClick = ({ node }) => { }; // Handle edge click -const onEdgeClick = (_, edge) => { +const onEdgeClick = (event, edge) => { + // Create a simplified copy of the edge data + const edgeData = { + id: edge.id, + source: edge.source, + target: edge.target, + label: edge.label || '', + sourceNode: nodes.value.find(node => node.id === edge.source), + targetNode: nodes.value.find(node => node.id === edge.target) + }; + + emit('edgeSelected', edgeData); emit('edgeClick', edge); }; @@ -274,13 +185,46 @@ onEdgesChange((changes) => { const handleConnect = (connection) => { if (!connection.source || !connection.target) return; + // Try to determine if this is coming from a gateway + const sourceNode = nodes.value.find(node => node.id === connection.source); + let label = ''; + + // If the source is a gateway, we should add a label based on conditions + if (sourceNode && sourceNode.type === 'gateway') { + // Check if there's a default path label + if (sourceNode.data && sourceNode.data.defaultPath) { + label = sourceNode.data.defaultPath; + } + + // For existing gateway connections, check if we should use a condition's output + const existingEdges = edges.value.filter(edge => edge.source === connection.source); + if (existingEdges.length === 0 && sourceNode.data.conditions && sourceNode.data.conditions.length > 0) { + // If this is the first connection and we have conditions, use the first condition's output + const firstCondition = sourceNode.data.conditions[0]; + if (firstCondition && firstCondition.output) { + label = firstCondition.output; + } + } else if (sourceNode.data.conditions) { + // If we already have connections, try to find an unused condition + const usedOutputs = existingEdges.map(edge => edge.label); + const unusedCondition = sourceNode.data.conditions.find(condition => + condition.output && !usedOutputs.includes(condition.output) + ); + + if (unusedCondition) { + label = unusedCondition.output; + } + } + } + const newEdge = { id: `${connection.source}-${connection.target}`, source: connection.source, target: connection.target, type: 'smoothstep', animated: true, - style: { stroke: '#555' } + style: { stroke: '#555' }, + label: label }; addEdges([newEdge]); @@ -288,15 +232,38 @@ const handleConnect = (connection) => { }; // Handle node removal -const onNodeDelete = (nodes) => { - removeNodes(nodes); - emit('nodesChange', nodes.value); +const onNodeDelete = (event) => { + // Check if we have a node in the event + if (event && event.node) { + removeNodes([event.node]); + emit('nodesChange', nodes.value); + } }; // Handle edge removal -const onEdgeDelete = (edges) => { - removeEdges(edges); - emit('edgesChange', edges.value); +const onEdgeDelete = (event) => { + // Check if we have an edge in the event + if (event && event.edge) { + removeEdges([event.edge]); + emit('edgesChange', edges.value); + } +}; + +// Handle delete key press to remove selected elements +const onDeleteKeyPress = () => { + const { getSelectedNodes, getSelectedEdges } = flowInstance.value; + const selectedNodes = getSelectedNodes(); + const selectedEdges = getSelectedEdges(); + + if (selectedNodes.length > 0) { + removeNodes(selectedNodes); + emit('nodesChange', nodes.value); + } + + if (selectedEdges.length > 0) { + removeEdges(selectedEdges); + emit('edgesChange', edges.value); + } }; // Handle drop event @@ -359,6 +326,7 @@ const onDragOver = (event) => { @connect="handleConnect" @nodeDoubleClick="onNodeDelete" @edgeDoubleClick="onEdgeDelete" + @keyup.delete="onDeleteKeyPress" > @@ -385,10 +353,10 @@ const onDragOver = (event) => { /* Node styles from ProcessFlowNodes.js are imported globally in a plugin */ .process-flow-container { width: 100%; - height: calc(100vh - 216px); /* Adjust based on header/footer height */ - min-height: 600px; + height: calc(100vh - 190px); /* Adjust based on new header/footer height */ + min-height: 500px; border: 1px solid #e2e8f0; - border-radius: 0.5rem; + border-radius: 0; overflow: hidden; position: relative; flex: 1; diff --git a/components/process-flow/ProcessFlowNodes.js b/components/process-flow/ProcessFlowNodes.js index e796bfd..37325a2 100644 --- a/components/process-flow/ProcessFlowNodes.js +++ b/components/process-flow/ProcessFlowNodes.js @@ -12,27 +12,37 @@ const CustomNode = markRaw({ v-if="type !== 'start'" type="target" position="top" - :style="{ background: '#555' }" + :class="'handle-' + type + '-input'" /> -
-
- -
-
{{ label }}
-
- -
-
- + + +
`, @@ -64,11 +74,11 @@ export const TaskNode = markRaw({ data: this.data, onClick: () => this.$emit('node-click', this.id) }, { - icon: () => h('i', { class: 'material-icons' }, 'assignment'), + icon: () => h('i', { class: 'material-icons text-blue-500' }, 'assignment'), default: () => h('div', { class: 'node-details' }, [ - h('p', { class: 'node-description' }, this.data?.description || 'Task node'), + h('p', { class: 'node-description' }, this.data?.description || 'A general task'), h('div', { class: 'node-assignee' }, [ - h('span', { class: 'node-assignee-label' }, 'Assigned to: '), + h('span', { class: 'node-assignee-label' }, 'Assigned to:'), h('span', { class: 'node-assignee-value' }, this.data?.assignee || 'Unassigned') ]) ]) @@ -88,10 +98,8 @@ export const StartNode = markRaw({ data: this.data, onClick: () => this.$emit('node-click', this.id) }, { - icon: () => h('i', { class: 'material-icons' }, 'play_circle_filled'), - default: () => h('div', { class: 'node-details' }, [ - h('p', { class: 'node-description' }, this.data?.description || 'Process starts here') - ]) + icon: () => h('i', { class: 'material-icons text-green-600' }, 'play_arrow'), + default: () => null }); } }); @@ -108,10 +116,8 @@ export const EndNode = markRaw({ data: this.data, onClick: () => this.$emit('node-click', this.id) }, { - icon: () => h('i', { class: 'material-icons' }, 'stop_circle'), - default: () => h('div', { class: 'node-details' }, [ - h('p', { class: 'node-description' }, this.data?.description || 'Process ends here') - ]) + icon: () => h('i', { class: 'material-icons text-red-600' }, 'stop'), + default: () => null }); } }); @@ -123,18 +129,18 @@ export const GatewayNode = markRaw({ return h(CustomNode, { id: this.id, type: 'gateway', - label: this.label || 'Decision', + label: this.label || 'Gateway', selected: this.selected, data: this.data, onClick: () => this.$emit('node-click', this.id) }, { - icon: () => h('i', { class: 'material-icons' }, 'call_split'), - default: () => h('div', { class: 'node-details' }, [ - h('p', { class: 'node-description' }, this.data?.description || 'Decision point'), - h('div', { class: 'node-conditions' }, [ - h('span', { class: 'node-conditions-label' }, 'Conditions: '), - h('span', { class: 'node-conditions-value' }, this.data?.conditions?.length || '0') - ]) + icon: () => h('i', { class: 'material-icons text-orange-500' }, 'call_split'), + default: () => h('div', { class: 'gateway-details' }, [ + h('div', { class: 'node-conditions-value' }, + this.data?.conditions?.length + ? `${this.data.conditions.length} condition${this.data.conditions.length > 1 ? 's' : ''}` + : '' + ) ]) }); } @@ -144,21 +150,31 @@ export const GatewayNode = markRaw({ export const FormNode = markRaw({ props: ['id', 'type', 'label', 'selected', 'data'], render() { + // Check if we have a form selected + const hasForm = this.data?.formId && this.data?.formName; + + // Create badge content based on form selection status + const badgeContent = hasForm ? + h('span', { class: 'node-badge bg-purple-100 text-purple-600 px-1 text-xs rounded' }, 'Form') : + null; + return h(CustomNode, { id: this.id, type: 'form', - label: this.label || 'Form', + label: this.label || 'Form Task', selected: this.selected, data: this.data, onClick: () => this.$emit('node-click', this.id) }, { - icon: () => h('i', { class: 'material-icons' }, 'description'), - badge: () => this.data?.formId ? h('span', { class: 'node-badge' }, 'F') : null, + icon: () => h('i', { class: 'material-icons text-purple-500' }, 'description'), + badge: () => badgeContent, default: () => h('div', { class: 'node-details' }, [ - h('p', { class: 'node-description' }, this.data?.description || 'Form submission'), + h('p', { class: 'node-description' }, this.data?.description || 'Form submission task'), h('div', { class: 'node-form-info' }, [ - h('span', { class: 'node-form-label' }, 'Form: '), - h('span', { class: 'node-form-value' }, this.data?.formName || 'None selected') + h('span', { class: 'node-form-label' }, 'Form:'), + h('span', { + class: hasForm ? 'node-form-value text-purple-600 font-medium' : 'node-form-value text-gray-400 italic' + }, hasForm ? this.data.formName : 'None selected') ]) ]) }); @@ -177,7 +193,7 @@ export const ScriptNode = markRaw({ data: this.data, onClick: () => this.$emit('node-click', this.id) }, { - icon: () => h('i', { class: 'material-icons' }, 'code'), + icon: () => h('i', { class: 'material-icons script-icon' }, 'code'), default: () => h('div', { class: 'node-details' }, [ h('p', { class: 'node-description' }, this.data?.description || 'Script execution'), h('div', { class: 'node-script-info' }, [ @@ -202,16 +218,11 @@ export const nodeTypes = markRaw({ // Default CSS for the nodes to be imported where needed export const nodeStyles = ` .custom-node { - border-radius: 6px; - padding: 12px; - color: #333; - background: white; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); - width: 200px; - font-size: 12px; - border: 2px solid transparent; - transition: all 0.2s; position: relative; + color: #333; + font-size: 12px; + transition: all 0.2s; + border: 1px solid transparent; } .custom-node.selected { @@ -219,79 +230,255 @@ export const nodeStyles = ` box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.2); } -.custom-node-header { +/* Base styles for different node types */ +.node-task, .node-form, .node-script { + width: 180px; + background: white; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + min-height: 50px; + display: flex; + flex-direction: column; + padding: 0; + border: 1px solid #e0e0e0; +} + +.node-gateway { + width: 50px; + height: 50px; + background: white; + transform: rotate(45deg); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + border: 1px solid #FF9800; +} + +.node-start, .node-end { + width: 40px; + height: 40px; + border-radius: 50%; display: flex; align-items: center; - margin-bottom: 8px; + justify-content: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.node-start { + background: #e8f5e9; + border: 1px solid #4CAF50; +} + +.node-end { + background: #ffebee; + border: 1px solid #f44336; +} + +/* Content positioning */ +.custom-node-content { + padding: 8px; + position: relative; + z-index: 2; +} + +.node-gateway .custom-node-content { + transform: rotate(-45deg); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + padding: 0; +} + +.node-start .custom-node-content, +.node-end .custom-node-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + padding: 0; } .custom-node-icon { - margin-right: 8px; + margin-right: 6px; + display: inline-flex; + align-items: center; +} + +.node-start .custom-node-icon, +.node-end .custom-node-icon { + margin: 0; } .custom-node-icon .material-icons { - font-size: 20px; + font-size: 16px; } -.custom-node-title { +.node-start .material-icons, +.node-end .material-icons { + font-size: 14px; +} + +.node-task .custom-node-title, +.node-form .custom-node-title, +.node-script .custom-node-title { font-weight: 500; - flex-grow: 1; + font-size: 11px; + display: flex; + align-items: center; + margin-bottom: 4px; } -.custom-node-content { - font-size: 12px; - color: #666; +.node-end .custom-node-title { + position: absolute; + width: 60px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + bottom: -29px; + left: 50%; + transform: translateX(-50%); + font-size: 10px; + font-weight: 500; + text-align: center; +} + +.node-start .custom-node-title { + position: absolute; + width: 60px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + bottom: 51px; + left: 50%; + transform: translateX(-50%); + font-size: 10px; + font-weight: 500; + text-align: center; +} + +.node-gateway .custom-node-title { + font-size: 9px; + font-weight: 500; + position: absolute; + width: 60px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + bottom: -18px; + left: 50%; + transform: translateX(-50%); + text-align: center; } .node-details { - margin-top: 8px; + margin-top: 4px; } .node-description { - margin-bottom: 4px; + margin-bottom: 2px; color: #666; + white-space: normal; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + font-size: 10px; } .node-assignee, .node-form-info, .node-script-info, .node-conditions { - font-size: 11px; - color: #888; display: flex; - gap: 4px; -} - -.node-badge { - background: #e2e8f0; - padding: 2px 6px; - border-radius: 4px; font-size: 10px; + color: #666; + align-items: center; +} + +.node-assignee-label, +.node-form-label, +.node-script-label { font-weight: 500; + margin-right: 4px; } -/* Node type specific styles */ -.node-start .custom-node-icon .material-icons { - color: #4CAF50; +.node-form-value, +.node-script-value, +.node-assignee-value { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.node-end .custom-node-icon .material-icons { - color: #f44336; +.node-conditions-value { + font-size: 9px; + color: #666; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; } -.node-task .custom-node-icon .material-icons { - color: #2196F3; +.node-form-id { + font-size: 9px; + color: #999; } -.node-form .custom-node-icon .material-icons { - color: #9C27B0; +.gateway-details { + font-size: 9px; + text-align: center; } -.node-gateway .custom-node-icon .material-icons { - color: #FF9800; +.handle-task-input, +.handle-form-input, +.handle-script-input, +.handle-gateway-input { + top: -5px !important; + width: 8px !important; + height: 8px !important; + border-radius: 50% !important; } -.node-script .custom-node-icon .material-icons { - color: #607D8B; +.handle-task-output, +.handle-form-output, +.handle-script-output, +.handle-gateway-output { + bottom: -5px !important; + width: 8px !important; + height: 8px !important; + border-radius: 50% !important; +} + +.handle-start-output { + bottom: -5px !important; + width: 8px !important; + height: 8px !important; + border-radius: 50% !important; +} + +.handle-end-input { + top: -5px !important; + width: 8px !important; + height: 8px !important; + border-radius: 50% !important; +} + +/* Position handles correctly for gateway node */ +.handle-gateway-input { + transform: translateY(-14px) !important; +} + +.handle-gateway-output { + transform: translateY(14px) !important; +} + +/* Badge style */ +.node-badge { + font-size: 9px; + padding: 1px 4px; + border-radius: 3px; + margin-left: 4px; } `; \ No newline at end of file diff --git a/pages/form-builder/index.vue b/pages/form-builder/index.vue index ff150dd..707deb9 100644 --- a/pages/form-builder/index.vue +++ b/pages/form-builder/index.vue @@ -251,7 +251,20 @@ definePageMeta({ const router = useRouter(); const formStore = useFormBuilderStore(); -const toast = useToast(); +let toast; + +// Try to use the toast composable if available +try { + toast = useToast(); +} catch (error) { + // Create a simple toast object if composable is not available + toast = { + success: (msg) => console.log('Success:', msg), + error: (msg) => console.error('Error:', msg), + info: (msg) => console.info('Info:', msg), + warning: (msg) => console.warn('Warning:', msg) + }; +} const showPreview = ref(false); const showUnsavedChangesModal = ref(false); @@ -271,8 +284,27 @@ const formName = computed({ }); // Initialize the form builder -onMounted(() => { - formStore.loadSavedForms(); +onMounted(async () => { + try { + await formStore.loadSavedForms(); + + // Check if there's a form ID in the URL query parameters + const route = useRoute(); + const formId = route.query.id; + + if (formId) { + try { + await formStore.loadForm(formId); + toast.success(`Form '${formStore.formName}' loaded successfully`); + } catch (error) { + console.error('Error loading form from ID:', error); + toast.error(`Failed to load form: ${error.message || 'Unknown error'}`); + } + } + } catch (error) { + console.error('Error initializing form builder:', error); + toast.error(`Initialization error: ${error.message || 'Unknown error'}`); + } // Add the beforeunload event listener window.addEventListener('beforeunload', handleBeforeUnload); @@ -391,7 +423,7 @@ const handleDrop = (event) => { formStore.addComponent(componentData); }; -const handleSave = () => { +const handleSave = async () => { if (!formStore.formName.trim()) { toast.error("Please enter a form name"); return; @@ -402,8 +434,17 @@ const handleSave = () => { return; } - formStore.saveForm(); - toast.success("Form saved successfully"); + try { + const savedForm = await formStore.saveForm(); + toast.success("Form saved successfully"); + + // Update URL to include the form ID without reloading the page + const newPath = `/form-builder?id=${savedForm.formUUID}`; + window.history.replaceState({}, '', newPath); + } catch (error) { + console.error("Error saving form:", error); + toast.error(`Failed to save form: ${error.message || 'Unknown error'}`); + } }; const handlePreview = () => { diff --git a/pages/form-builder/manage.vue b/pages/form-builder/manage.vue index edbe593..2f642a7 100644 --- a/pages/form-builder/manage.vue +++ b/pages/form-builder/manage.vue @@ -160,13 +160,32 @@ definePageMeta({ const router = useRouter(); const formStore = useFormBuilderStore(); -const toast = useToast(); +let toast; + +// Try to use the toast composable if available +try { + toast = useToast(); +} catch (error) { + // Create a simple toast object if composable is not available + toast = { + success: (msg) => console.log('Success:', msg), + error: (msg) => console.error('Error:', msg), + info: (msg) => console.info('Info:', msg), + warning: (msg) => console.warn('Warning:', msg) + }; +} + const searchQuery = ref(""); const showUnsavedChangesModal = ref(false); // Initialize and load forms -onMounted(() => { - formStore.loadSavedForms(); +onMounted(async () => { + try { + await formStore.loadSavedForms(); + } catch (error) { + console.error("Error loading forms:", error); + toast.error("Failed to load forms: " + (error.message || "Unknown error")); + } }); // Format date for display @@ -213,18 +232,29 @@ const confirmNavigation = () => { router.push("/form-builder"); }; -const editForm = (formId) => { - formStore.loadForm(formId); - router.push("/form-builder"); +const editForm = async (formId) => { + try { + await formStore.loadForm(formId); + router.push("/form-builder"); + } catch (error) { + console.error("Error loading form:", error); + toast.error("Failed to load form: " + (error.message || "Unknown error")); + } }; -const deleteForm = (formId) => { +const deleteForm = async (formId) => { if (confirm("Are you sure you want to delete this form?")) { - const index = formStore.savedForms.findIndex((f) => f.id === formId); - if (index !== -1) { - formStore.savedForms.splice(index, 1); - localStorage.setItem("savedForms", JSON.stringify(formStore.savedForms)); + try { + // Call the API to delete the form + await formStore.deleteForm(formId); + + // Refresh the forms list + await formStore.loadSavedForms(); + toast.success("Form deleted successfully"); + } catch (error) { + console.error("Error deleting form:", error); + toast.error("Failed to delete form: " + (error.message || "Unknown error")); } } }; diff --git a/pages/process-builder/index.vue b/pages/process-builder/index.vue index 59768ad..7ae0014 100644 --- a/pages/process-builder/index.vue +++ b/pages/process-builder/index.vue @@ -1,8 +1,21 @@ \ No newline at end of file diff --git a/prisma/json/json-schema.json b/prisma/json/json-schema.json index d1dadc6..ff0d695 100644 --- a/prisma/json/json-schema.json +++ b/prisma/json/json-schema.json @@ -141,6 +141,24 @@ "items": { "$ref": "#/definitions/userrole" } + }, + "processes": { + "type": "array", + "items": { + "$ref": "#/definitions/process" + } + }, + "forms": { + "type": "array", + "items": { + "$ref": "#/definitions/form" + } + }, + "assignedTasks": { + "type": "array", + "items": { + "$ref": "#/definitions/task" + } } } }, @@ -265,6 +283,200 @@ "$ref": "#/definitions/user" } } + }, + "form": { + "type": "object", + "properties": { + "formID": { + "type": "integer" + }, + "formUUID": { + "type": "string" + }, + "formName": { + "type": "string" + }, + "formDescription": { + "type": [ + "string", + "null" + ] + }, + "formComponents": { + "type": [ + "number", + "string", + "boolean", + "object", + "array", + "null" + ] + }, + "formStatus": { + "type": "string", + "default": "active" + }, + "formCreatedDate": { + "type": "string", + "format": "date-time" + }, + "formModifiedDate": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "creator": { + "anyOf": [ + { + "$ref": "#/definitions/user" + }, + { + "type": "null" + } + ] + }, + "formTasks": { + "type": "array", + "items": { + "$ref": "#/definitions/task" + } + } + } + }, + "process": { + "type": "object", + "properties": { + "processID": { + "type": "integer" + }, + "processUUID": { + "type": "string" + }, + "processName": { + "type": "string" + }, + "processDescription": { + "type": [ + "string", + "null" + ] + }, + "processDefinition": { + "type": [ + "number", + "string", + "boolean", + "object", + "array", + "null" + ] + }, + "processVersion": { + "type": "integer", + "default": 1 + }, + "processStatus": { + "type": "string", + "default": "draft" + }, + "processCreatedDate": { + "type": "string", + "format": "date-time" + }, + "processModifiedDate": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "creator": { + "anyOf": [ + { + "$ref": "#/definitions/user" + }, + { + "type": "null" + } + ] + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/definitions/task" + } + } + } + }, + "task": { + "type": "object", + "properties": { + "taskID": { + "type": "integer" + }, + "taskUUID": { + "type": "string" + }, + "taskNodeId": { + "type": "string" + }, + "taskName": { + "type": "string" + }, + "taskType": { + "type": "string" + }, + "taskData": { + "type": [ + "number", + "string", + "boolean", + "object", + "array", + "null" + ] + }, + "taskStatus": { + "type": "string", + "default": "pending" + }, + "taskCreatedDate": { + "type": "string", + "format": "date-time" + }, + "taskModifiedDate": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "process": { + "$ref": "#/definitions/process" + }, + "form": { + "anyOf": [ + { + "$ref": "#/definitions/form" + }, + { + "type": "null" + } + ] + }, + "assignee": { + "anyOf": [ + { + "$ref": "#/definitions/user" + }, + { + "type": "null" + } + ] + } + } } }, "type": "object", @@ -283,6 +495,15 @@ }, "userrole": { "$ref": "#/definitions/userrole" + }, + "form": { + "$ref": "#/definitions/form" + }, + "process": { + "$ref": "#/definitions/process" + }, + "task": { + "$ref": "#/definitions/task" } } } \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0ee0959..d9323cc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -41,6 +41,9 @@ model user { userModifiedDate DateTime? @db.DateTime(0) audit audit[] userrole userrole[] + processes process[] @relation("ProcessCreator") + forms form[] @relation("FormCreator") + assignedTasks task[] @relation("TaskAssignee") } model role { @@ -76,3 +79,66 @@ model userrole { @@index([userRoleRoleID], map: "FK_userrole_role") @@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 + 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 + 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[] + + @@index([processCreatedBy], map: "FK_process_creator") +} + +model task { + taskID Int @id @default(autoincrement()) + taskUUID String @unique @db.VarChar(36) + taskNodeId String @db.VarChar(255) + taskName String @db.VarChar(255) + taskType String @db.VarChar(50) + taskData Json? @db.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]) + + @@index([taskProcessId], map: "FK_task_process") + @@index([taskFormId], map: "FK_task_form") + @@index([taskAssigneeId], map: "FK_task_assignee") +} diff --git a/server/api/forms/[id].delete.js b/server/api/forms/[id].delete.js new file mode 100644 index 0000000..34b5127 --- /dev/null +++ b/server/api/forms/[id].delete.js @@ -0,0 +1,57 @@ +import { PrismaClient } from '@prisma/client'; + +// Initialize Prisma client +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + // Get the form ID from the route params + const id = event.context.params.id; + + try { + // Try to delete by UUID first + let form; + try { + form = await prisma.form.delete({ + where: { formUUID: id } + }); + } catch (e) { + // If UUID not found, try numeric ID + if (!isNaN(parseInt(id))) { + form = await prisma.form.delete({ + where: { formID: parseInt(id) } + }); + } else { + throw e; + } + } + + return { + success: true, + message: 'Form deleted successfully' + }; + } catch (error) { + console.error(`Error deleting form ${id}:`, error); + + // Handle specific errors + if (error.code === 'P2025') { + return { + success: false, + error: 'Form not found' + }; + } + + // Handle cases where the form has associated tasks + if (error.code === 'P2003') { + return { + success: false, + error: 'Cannot delete form because it is associated with one or more tasks' + }; + } + + return { + success: false, + error: 'Failed to delete form', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }; + } +}); \ No newline at end of file diff --git a/server/api/forms/[id].get.js b/server/api/forms/[id].get.js new file mode 100644 index 0000000..d7fa577 --- /dev/null +++ b/server/api/forms/[id].get.js @@ -0,0 +1,64 @@ +import { PrismaClient } from '@prisma/client'; + +// Initialize Prisma client +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + // Get the form ID from the route params + const id = event.context.params.id; + + try { + // Try to find by UUID first + let form = await prisma.form.findUnique({ + where: { + formUUID: id + }, + include: { + creator: { + select: { + userID: true, + userFullName: true + } + } + } + }); + + // If not found, try to find by numeric ID + if (!form && !isNaN(parseInt(id))) { + form = await prisma.form.findUnique({ + where: { + formID: parseInt(id) + }, + include: { + creator: { + select: { + userID: true, + userFullName: true + } + } + } + }); + } + + // If form not found, return 404 + if (!form) { + return { + success: false, + error: 'Form not found' + }; + } + + return { + success: true, + form + }; + } catch (error) { + console.error(`Error fetching form ${id}:`, error); + + return { + success: false, + error: 'Failed to fetch form', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }; + } +}); \ No newline at end of file diff --git a/server/api/forms/[id].put.js b/server/api/forms/[id].put.js new file mode 100644 index 0000000..b5752ec --- /dev/null +++ b/server/api/forms/[id].put.js @@ -0,0 +1,78 @@ +import { PrismaClient } from '@prisma/client'; + +// Initialize Prisma client +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + // Get the form ID from the route params + const id = event.context.params.id; + + try { + // Parse the request body + const body = await readBody(event); + + // Validate required fields + if (!body.formName) { + return { + success: false, + error: 'Form name is required' + }; + } + + // Prepare update data + const updateData = { + formName: body.formName, + formComponents: body.components || [], + formModifiedDate: new Date() + }; + + // Add optional fields if provided + if (body.formDescription !== undefined) { + updateData.formDescription = body.formDescription; + } + + if (body.status !== undefined) { + updateData.formStatus = body.status; + } + + // Try to update by UUID first + let form; + try { + form = await prisma.form.update({ + where: { formUUID: id }, + data: updateData + }); + } catch (e) { + // If UUID not found, try numeric ID + if (!isNaN(parseInt(id))) { + form = await prisma.form.update({ + where: { formID: parseInt(id) }, + data: updateData + }); + } else { + throw e; + } + } + + return { + success: true, + form + }; + } catch (error) { + console.error(`Error updating form ${id}:`, error); + + // Handle specific errors + if (error.code === 'P2025') { + return { + success: false, + error: 'Form not found' + }; + } + + return { + success: false, + error: 'Failed to update form', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }; + } +}); \ No newline at end of file diff --git a/server/api/forms/create.post.js b/server/api/forms/create.post.js new file mode 100644 index 0000000..3b6144c --- /dev/null +++ b/server/api/forms/create.post.js @@ -0,0 +1,45 @@ +import { PrismaClient } from '@prisma/client'; +import { v4 as uuidv4 } from 'uuid'; + +// Initialize Prisma client +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + try { + // Parse the request body + const body = await readBody(event); + + // Validate required fields + if (!body.formName) { + return { + success: false, + error: 'Form name is required' + }; + } + + // Create a new form in the database + const form = await prisma.form.create({ + data: { + formUUID: uuidv4(), + formName: body.formName, + formDescription: body.formDescription || null, + formComponents: body.components || [], + formStatus: body.status || 'active', + formCreatedBy: body.createdBy || null // In a real app, this would come from the authenticated user + } + }); + + return { + success: true, + form + }; + } catch (error) { + console.error('Error creating form:', error); + + return { + success: false, + error: 'Failed to create form', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }; + } +}); \ No newline at end of file diff --git a/server/api/forms/index.get.js b/server/api/forms/index.get.js new file mode 100644 index 0000000..86a5d69 --- /dev/null +++ b/server/api/forms/index.get.js @@ -0,0 +1,47 @@ +import { PrismaClient } from '@prisma/client'; + +// Initialize Prisma client +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + try { + // Get all active forms + const forms = await prisma.form.findMany({ + where: { + formStatus: 'active' + }, + orderBy: { + formCreatedDate: 'desc' + }, + select: { + formID: true, + formUUID: true, + formName: true, + formDescription: true, + formStatus: true, + formCreatedDate: true, + formModifiedDate: true, + // Don't include the full components data to keep response size small + creator: { + select: { + userID: true, + userFullName: true + } + } + } + }); + + return { + success: true, + forms + }; + } catch (error) { + console.error('Error fetching forms:', error); + + return { + success: false, + error: 'Failed to fetch forms', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }; + } +}); \ No newline at end of file diff --git a/server/api/processes/[id].put.js b/server/api/processes/[id].put.js new file mode 100644 index 0000000..94c59fd --- /dev/null +++ b/server/api/processes/[id].put.js @@ -0,0 +1,85 @@ +import { PrismaClient } from '@prisma/client'; + +// Initialize Prisma client +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + // Get the process ID from the route params + const id = event.context.params.id; + + try { + // Parse the request body + const body = await readBody(event); + + // Validate required fields + if (!body.processName) { + return { + success: false, + error: 'Process name is required' + }; + } + + // Prepare update data + const updateData = { + processName: body.processName, + processModifiedDate: new Date() + }; + + // Add optional fields if provided + if (body.processDescription !== undefined) { + updateData.processDescription = body.processDescription; + } + + if (body.definition !== undefined) { + updateData.processDefinition = body.definition; + } + + if (body.processStatus !== undefined) { + updateData.processStatus = body.processStatus; + } + + if (body.processVersion !== undefined) { + updateData.processVersion = body.processVersion; + } + + // Try to update by UUID first + let process; + try { + process = await prisma.process.update({ + where: { processUUID: id }, + data: updateData + }); + } catch (e) { + // If UUID not found, try numeric ID + if (!isNaN(parseInt(id))) { + process = await prisma.process.update({ + where: { processID: parseInt(id) }, + data: updateData + }); + } else { + throw e; + } + } + + return { + success: true, + process + }; + } catch (error) { + console.error(`Error updating process ${id}:`, error); + + // Handle specific errors + if (error.code === 'P2025') { + return { + success: false, + error: 'Process not found' + }; + } + + return { + success: false, + error: 'Failed to update process', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }; + } +}); \ No newline at end of file diff --git a/server/api/processes/[id]/tasks/create.post.js b/server/api/processes/[id]/tasks/create.post.js new file mode 100644 index 0000000..b96aff6 --- /dev/null +++ b/server/api/processes/[id]/tasks/create.post.js @@ -0,0 +1,71 @@ +import { PrismaClient } from '@prisma/client'; +import { v4 as uuidv4 } from 'uuid'; + +// Initialize Prisma client +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + // Get the process ID from the route params + const processId = event.context.params.id; + + try { + // Parse the request body + const body = await readBody(event); + + // Validate required fields + if (!body.taskName || !body.taskType || !body.taskNodeId) { + return { + success: false, + error: 'Task name, type, and node ID are required' + }; + } + + // Find the process + let process; + + if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(processId)) { + // If it looks like a UUID + process = await prisma.process.findUnique({ + where: { processUUID: processId } + }); + } else if (!isNaN(parseInt(processId))) { + // If it's a numeric ID + process = await prisma.process.findUnique({ + where: { processID: parseInt(processId) } + }); + } + + if (!process) { + return { + success: false, + error: 'Process not found' + }; + } + + // Create a new task + const task = await prisma.task.create({ + data: { + taskUUID: uuidv4(), + taskNodeId: body.taskNodeId, + taskName: body.taskName, + taskType: body.taskType, + taskData: body.taskData || {}, + taskProcessId: process.processID, + taskAssigneeId: body.assigneeId || null + } + }); + + return { + success: true, + task + }; + } catch (error) { + console.error(`Error creating task for process ${processId}:`, error); + + return { + success: false, + error: 'Failed to create task', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }; + } +}); \ No newline at end of file diff --git a/server/api/processes/create.post.js b/server/api/processes/create.post.js new file mode 100644 index 0000000..b6d9e10 --- /dev/null +++ b/server/api/processes/create.post.js @@ -0,0 +1,46 @@ +import { PrismaClient } from '@prisma/client'; +import { v4 as uuidv4 } from 'uuid'; + +// Initialize Prisma client +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + try { + // Parse the request body + const body = await readBody(event); + + // Validate required fields + if (!body.processName) { + return { + success: false, + error: 'Process name is required' + }; + } + + // Create a new process in the database + const process = await prisma.process.create({ + data: { + processUUID: uuidv4(), + processName: body.processName, + processDescription: body.processDescription || null, + processDefinition: body.definition || { nodes: [], edges: [] }, + processVersion: 1, + processStatus: body.status || 'draft', + processCreatedBy: body.createdBy || null // In a real app, this would come from the authenticated user + } + }); + + return { + success: true, + process + }; + } catch (error) { + console.error('Error creating process:', error); + + return { + success: false, + error: 'Failed to create process', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }; + } +}); \ No newline at end of file diff --git a/server/api/tasks/[id]/form.delete.js b/server/api/tasks/[id]/form.delete.js new file mode 100644 index 0000000..fdcb7c6 --- /dev/null +++ b/server/api/tasks/[id]/form.delete.js @@ -0,0 +1,63 @@ +import { PrismaClient } from '@prisma/client'; + +// Initialize Prisma client +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + // Get the task ID from the route params + const taskId = event.context.params.id; + + try { + // Find the task + let task; + + if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(taskId)) { + // If it looks like a UUID + task = await prisma.task.findUnique({ + where: { taskUUID: taskId } + }); + } else if (!isNaN(parseInt(taskId))) { + // If it's a numeric ID + task = await prisma.task.findUnique({ + where: { taskID: parseInt(taskId) } + }); + } + + if (!task) { + return { + success: false, + error: 'Task not found' + }; + } + + // Prepare task data without form information + let taskData = task.taskData || {}; + delete taskData.formName; + delete taskData.formId; + delete taskData.formUuid; + + // Unlink the form from the task + const updatedTask = await prisma.task.update({ + where: { + taskID: task.taskID + }, + data: { + taskFormId: null, + taskData: taskData + } + }); + + return { + success: true, + task: updatedTask + }; + } catch (error) { + console.error(`Error removing form from task ${taskId}:`, error); + + return { + success: false, + error: 'Failed to remove form from task', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }; + } +}); \ No newline at end of file diff --git a/server/api/tasks/[id]/form.post.js b/server/api/tasks/[id]/form.post.js new file mode 100644 index 0000000..ee74d6d --- /dev/null +++ b/server/api/tasks/[id]/form.post.js @@ -0,0 +1,99 @@ +import { PrismaClient } from '@prisma/client'; + +// Initialize Prisma client +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + // Get the task ID from the route params + const taskId = event.context.params.id; + + try { + // Parse the request body + const body = await readBody(event); + + // Validate required fields + if (!body.formId) { + return { + success: false, + error: 'Form ID is required' + }; + } + + // Find the task + let task; + + if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(taskId)) { + // If it looks like a UUID + task = await prisma.task.findUnique({ + where: { taskUUID: taskId } + }); + } else if (!isNaN(parseInt(taskId))) { + // If it's a numeric ID + task = await prisma.task.findUnique({ + where: { taskID: parseInt(taskId) } + }); + } + + if (!task) { + return { + success: false, + error: 'Task not found' + }; + } + + // Find the form + let form; + const formId = body.formId; + + if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(formId)) { + // If it looks like a UUID + form = await prisma.form.findUnique({ + where: { formUUID: formId } + }); + } else if (!isNaN(parseInt(formId))) { + // If it's a numeric ID + form = await prisma.form.findUnique({ + where: { formID: parseInt(formId) } + }); + } + + if (!form) { + return { + success: false, + error: 'Form not found' + }; + } + + // Link the form to the task + const updatedTask = await prisma.task.update({ + where: { + taskID: task.taskID + }, + data: { + taskFormId: form.formID, + taskData: { + ...task.taskData, + formName: form.formName, + formId: form.formID, + formUuid: form.formUUID + } + }, + include: { + form: true + } + }); + + return { + success: true, + task: updatedTask + }; + } catch (error) { + console.error(`Error connecting form to task ${taskId}:`, error); + + return { + success: false, + error: 'Failed to connect form to task', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }; + } +}); \ No newline at end of file diff --git a/stores/formBuilder.js b/stores/formBuilder.js index 0988c62..9f09c5c 100644 --- a/stores/formBuilder.js +++ b/stores/formBuilder.js @@ -12,7 +12,8 @@ export const useFormBuilderStore = defineStore('formBuilder', { hasUnsavedChanges: false, actionHistory: [], currentHistoryIndex: -1, - maxHistoryLength: 30 // Maximum number of history entries to keep + maxHistoryLength: 30, // Maximum number of history entries to keep + currentFormId: null }), getters: { @@ -414,32 +415,184 @@ export const useFormBuilderStore = defineStore('formBuilder', { this.hasUnsavedChanges = false; }, - saveForm() { - const formData = this.formConfig; - - // Add to saved forms array - const existingIndex = this.savedForms.findIndex(f => f.id === formData.id); - if (existingIndex !== -1) { - this.savedForms[existingIndex] = formData; - } else { - this.savedForms.push(formData); + // Get forms from the backend + async getForms() { + try { + // Use the API endpoint to fetch forms + const response = await fetch('/api/forms'); + const result = await response.json(); + + if (result.success && Array.isArray(result.forms)) { + return result.forms; + } else { + console.error('Error in API response:', result.error || 'Unknown error'); + return []; + } + } catch (error) { + console.error('Error fetching forms:', error); + return []; } - - // Save to localStorage for persistence - localStorage.setItem('savedForms', JSON.stringify(this.savedForms)); - - this.hasUnsavedChanges = false; - - // Record in history - this.recordHistory('save_form', { - formName: this.formName, - formDescription: this.formDescription, - componentCount: this.formComponents.length - }); - - return formData; }, + // Load saved forms from the API + async loadSavedForms() { + try { + // Fetch forms from the API + const forms = await this.getForms(); + + // Transform to the format expected by the UI + this.savedForms = forms.map(form => ({ + id: form.formUUID, + name: form.formName, + description: form.formDescription || '', + components: form.formComponents || [], + createdAt: form.formCreatedDate, + updatedAt: form.formModifiedDate + })); + + return this.savedForms; + } catch (error) { + console.error('Error loading saved forms:', error); + this.savedForms = []; + return []; + } + }, + + // Save form to the database + async saveForm() { + try { + const formData = { + formName: this.formName, + formDescription: this.formDescription, + components: this.formComponents.map(c => ({ + type: c.type, + props: c.props + })) + }; + + // Determine if this is a new form or an update + const isNewForm = !this.currentFormId; + let response; + + if (isNewForm) { + // Create a new form + response = await fetch('/api/forms/create', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + } else { + // Update existing form + response = await fetch(`/api/forms/${this.currentFormId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + } + + const result = await response.json(); + + if (result.success) { + // Update store state with the saved form + this.currentFormId = result.form.formUUID; + this.hasUnsavedChanges = false; + + // Record in history + this.recordHistory('save_form', { + formName: this.formName, + formDescription: this.formDescription, + componentCount: this.formComponents.length + }); + + return result.form; + } else { + throw new Error(result.error || 'Failed to save form'); + } + } catch (error) { + console.error('Error saving form:', error); + throw error; + } + }, + + // Load a form from the database + async loadForm(formId) { + if (!formId) { + throw new Error('Form ID is required'); + } + + try { + const response = await fetch(`/api/forms/${formId}`); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `HTTP error ${response.status}`); + } + + const result = await response.json(); + + if (result.success && result.form) { + // Clear existing data + this.formComponents = []; + this.selectedComponentId = null; + + // Set form data + this.formName = result.form.formName; + this.formDescription = result.form.formDescription || ''; + this.currentFormId = result.form.formUUID; + + // Transform components from DB format to store format + if (Array.isArray(result.form.formComponents)) { + this.formComponents = result.form.formComponents.map(c => ({ + ...c, + id: uuidv4() // Assign a new UUID for each component + })); + } + + // Clear and initialize history when loading a form + this.actionHistory = []; + this.currentHistoryIndex = -1; + + // Record initial state in history + this.recordHistory('load_form', { + formName: result.form.formName, + formId: formId + }); + + return result.form; + } else { + throw new Error(result.error || 'Failed to load form'); + } + } catch (error) { + console.error(`Error loading form ${formId}:`, error); + throw error; + } + }, + + // Delete a form from the database + async deleteForm(formId) { + try { + const response = await fetch(`/api/forms/${formId}`, { + method: 'DELETE' + }); + + const result = await response.json(); + + if (result.success) { + return true; + } else { + throw new Error(result.error || 'Failed to delete form'); + } + } catch (error) { + console.error(`Error deleting form ${formId}:`, error); + throw error; + } + }, + + // Clear the current form clearForm() { // Capture the current state before clearing const oldComponents = [...this.formComponents]; @@ -451,6 +604,7 @@ export const useFormBuilderStore = defineStore('formBuilder', { this.selectedComponentId = null; this.formName = 'New Form'; this.formDescription = ''; + this.currentFormId = null; this.hasUnsavedChanges = false; // Clear history when starting a new form and add initial state @@ -463,40 +617,6 @@ export const useFormBuilderStore = defineStore('formBuilder', { }); }, - loadForm(formId) { - const savedForms = JSON.parse(localStorage.getItem('savedForms') || '[]'); - const form = savedForms.find(f => f.id === formId); - - if (form) { - // Clear existing data - this.formComponents = []; - this.selectedComponentId = null; - - // Set form data - this.formName = form.name; - this.formDescription = form.description; - this.formComponents = form.components.map(c => ({ - ...c, - id: uuidv4() - })); - - // Clear and initialize history when loading a form - this.actionHistory = []; - this.currentHistoryIndex = -1; - - // Record initial state in history - this.recordHistory('load_form', { - formName: form.name, - formId: formId - }); - } - }, - - loadSavedForms() { - const savedForms = JSON.parse(localStorage.getItem('savedForms') || '[]'); - this.savedForms = savedForms; - }, - // Optimize the grid layout by analyzing the current components // and adjusting their sizes to fill available spaces optimizeGridLayout() { diff --git a/stores/processBuilder.js b/stores/processBuilder.js index fa398b2..21c0264 100644 --- a/stores/processBuilder.js +++ b/stores/processBuilder.js @@ -181,22 +181,36 @@ export const useProcessBuilderStore = defineStore('processBuilder', { deleteNode(nodeId) { if (!this.currentProcess) return; + // Find the node index const index = this.currentProcess.nodes.findIndex(n => n.id === nodeId); if (index !== -1) { + // Remove the node this.currentProcess.nodes.splice(index, 1); // Remove any edges connected to this node - this.currentProcess.edges = this.currentProcess.edges.filter( - edge => edge.source !== nodeId && edge.target !== nodeId + const edgesToRemove = this.currentProcess.edges.filter( + edge => edge.source === nodeId || edge.target === nodeId ); + + edgesToRemove.forEach(edge => { + const edgeIndex = this.currentProcess.edges.findIndex(e => e.id === edge.id); + if (edgeIndex !== -1) { + this.currentProcess.edges.splice(edgeIndex, 1); + } + }); + // Clear selection if the deleted node was selected if (this.selectedNodeId === nodeId) { this.selectedNodeId = null; } this.saveToHistory('Delete node'); this.unsavedChanges = true; + + return true; // Return success } + + return false; // Return failure }, /** @@ -226,12 +240,35 @@ export const useProcessBuilderStore = defineStore('processBuilder', { /** * Update an edge in the current process */ - updateEdge(edgeId, updates) { + updateEdge(edgeIdOrObject, updates) { if (!this.currentProcess) return; + // Handle different parameter formats + let edgeId, edgeUpdates; + + if (typeof edgeIdOrObject === 'string') { + // Called with (id, updates) + edgeId = edgeIdOrObject; + edgeUpdates = updates || {}; + } else if (typeof edgeIdOrObject === 'object') { + // Called with an edge object + edgeId = edgeIdOrObject.id; + + if (updates) { + // Called with (edge, updates) + edgeUpdates = updates; + } else { + // Called with just the edge object containing updates + edgeUpdates = { ...edgeIdOrObject }; + delete edgeUpdates.id; // Don't update the ID + } + } else { + return; // Invalid parameters + } + const edge = this.currentProcess.edges.find(e => e.id === edgeId); if (edge) { - Object.assign(edge, updates); + Object.assign(edge, edgeUpdates); this.saveToHistory('Update edge'); this.unsavedChanges = true; }