From 7df119417a6b927ec1128d78ed294c18376f6654 Mon Sep 17 00:00:00 2001 From: Md Afiq Iskandar Date: Thu, 31 Jul 2025 10:28:34 +0800 Subject: [PATCH] Add Page Wrapper Node and Enhance Process Flow Management - Introduced a new 'Page Wrapper' node type to allow multiple components (forms, HTML, tables) to be rendered as a single page, enhancing the flexibility of the process builder. - Implemented child node management within the Page Wrapper, including adding, updating, and removing child nodes through the process store. - Enhanced the ProcessFlowCanvas to support the new Page Wrapper node, including event handling for child node interactions and configuration. - Updated the workflow page to handle submissions and interactions for Page Wrapper nodes, ensuring seamless integration with existing process flows. - Improved documentation in CLAUDE.md to provide guidance on the new Page Wrapper architecture and its components. - Refactored related components and store logic to accommodate the new functionality, ensuring a cohesive user experience across the application. --- CLAUDE.md | 648 ++++++++++++- components/process-flow/ChildFormRenderer.vue | 367 ++++++++ components/process-flow/ChildHtmlRenderer.vue | 494 ++++++++++ .../process-flow/PageWrapperConfiguration.vue | 882 ++++++++++++++++++ .../PageWrapperConfigurationModal.vue | 226 +++++ .../process-flow/PageWrapperRenderer.vue | 565 +++++++++++ .../process-flow/ProcessBuilderComponents.vue | 37 + components/process-flow/ProcessFlowCanvas.vue | 36 + .../process-flow/custom/PageWrapperNode.vue | 800 ++++++++++++++++ pages/process-builder/index.vue | 171 +++- pages/workflow/[id].vue | 124 +++ stores/processBuilder.js | 183 ++++ 12 files changed, 4531 insertions(+), 2 deletions(-) create mode 100644 components/process-flow/ChildFormRenderer.vue create mode 100644 components/process-flow/ChildHtmlRenderer.vue create mode 100644 components/process-flow/PageWrapperConfiguration.vue create mode 100644 components/process-flow/PageWrapperConfigurationModal.vue create mode 100644 components/process-flow/PageWrapperRenderer.vue create mode 100644 components/process-flow/custom/PageWrapperNode.vue diff --git a/CLAUDE.md b/CLAUDE.md index 2893b92..91e32dc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,7 @@ # CLAUDE.md +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + ## AI Guidance - This documentation is for AI memory/context. Use it to answer questions, generate code, and reason about the system. - Always follow the schema and field descriptions exactly as written. @@ -390,4 +392,648 @@ This codebase represents a sophisticated, production-ready BPM platform with com - **Touch Support**: Mobile-optimized drag-and-drop with gesture recognition - **Keyboard Shortcuts**: Ctrl+1/2/3 for panel management, standard undo/redo support - **Responsive Canvas**: Adaptive grid layout with zoom and pan capabilities -- **Device Preview**: Mobile, tablet, and desktop preview modes for form integration testing \ No newline at end of file +- **Device Preview**: Mobile, tablet, and desktop preview modes for form integration testing + +## Process Node Architecture Deep Dive + +### Node Creation and Registration System + +**Available Node Types (`/components/process-flow/ProcessBuilderComponents.vue`):** +- **Core Node Types**: start, end, form, api, gateway, notification, business-rule, script, html, subprocess +- **Design Elements**: swimlane-horizontal, swimlane-vertical, rectangle-shape, text-annotation + +**Node Definition Structure:** +```javascript +{ + type: 'node-type', // Required: Unique node type identifier + name: 'Display Name', // Required: Human-readable name for UI + category: 'Core|Shape', // Required: Category for component palette + icon: 'material-symbols:icon', // Required: Icon for component palette + description: 'Node purpose', // Required: Tooltip description + defaultProps: { // Required: Default node configuration + label: 'Node Label', // Default label shown on canvas + data: { // Node-specific configuration data + // Core properties (all nodes) + description: 'Description', // Node description for tooltips + shape: 'rectangle|diamond|circle', // Visual shape + backgroundColor: '#ffffff', // Background color + borderColor: '#000000', // Border color + textColor: '#000000', // Text color + + // Node-type specific properties + // (varies by node type - see specific node schemas below) + } + } +} +``` + +### Node Implementation Components + +**Vue Component Structure (all in `/components/process-flow/custom/`):** +- **Node Rendering**: `[NodeType]Node.vue` (e.g., `FormNode.vue`, `ApiNode.vue`) +- **Configuration Modal**: `[NodeType]NodeConfigurationModal.vue` +- **Configuration Component**: `[NodeType]NodeConfiguration.vue` + +**Required Node Component Structure:** +```vue + + + +``` + +### Node Data Schemas by Type + +#### Form Node (`type: 'form'`) +```javascript +data: { + // Required fields + formId: null, // Form ID reference + formName: null, // Form name for display + + // Assignment configuration + assignedRoles: [], // Array of role IDs + assignedUsers: [], // Array of user IDs + assignmentType: 'role', // 'role' | 'user' | 'variable' + assignmentVariable: '', // Variable name for dynamic assignment + + // Variable mapping + inputMappings: [], // Array of { processVar, formField } mappings + outputMappings: [], // Array of { formField, processVar } mappings + + // Timing configuration + dueDate: null, // Due date for task + dueDateVariable: '', // Variable for dynamic due date + priority: 'medium', // 'low' | 'medium' | 'high' + + // Visual properties (inherited from base) + shape: 'rectangle', + backgroundColor: '#faf5ff', + borderColor: '#9333ea', + textColor: '#6b21a8' +} +``` + +#### API Node (`type: 'api'`) +```javascript +data: { + // Required API configuration + apiMethod: 'GET', // HTTP method + apiUrl: '', // API endpoint URL + + // Request configuration + requestBody: '', // JSON request body + headers: '{"Content-Type": "application/json"}', // Request headers (JSON string) + + // Authentication + authType: 'none', // 'none' | 'bearer' | 'basic' | 'apikey' + authToken: '', // Auth token/API key + authUsername: '', // Basic auth username + authPassword: '', // Basic auth password + + // Response handling + outputVariable: 'apiResponse', // Variable to store response + errorVariable: 'apiError', // Variable to store errors + continueOnError: false, // Continue process flow on API error + + // Timeout configuration + timeout: 30000, // Request timeout in milliseconds + + // Visual properties + shape: 'rectangle', + backgroundColor: '#eff6ff', + borderColor: '#3b82f6', + textColor: '#1e40af' +} +``` + +#### Gateway Node (`type: 'gateway'`) +```javascript +data: { + // Decision logic + conditions: [], // Array of condition objects: + // [{ + // id: string, + // variable: string, + // operator: '==' | '!=' | '>' | '<' | '>=' | '<=' | 'contains' | 'startsWith' | 'endsWith', + // value: any, + // logicalOperator: 'AND' | 'OR', + // path: string // Edge label for this condition + // }] + + defaultPath: 'Default', // Default path when no conditions match + evaluationMode: 'sequential', // 'sequential' | 'parallel' + + // Visual properties + shape: 'diamond', + backgroundColor: '#fff7ed', + borderColor: '#f97316', + textColor: '#c2410c' +} +``` + +#### Business Rule Node (`type: 'business-rule'`) +```javascript +data: { + // Rule groups for complex business logic + ruleGroups: [], // Array of rule group objects: + // [{ + // id: string, + // name: string, + // operator: 'AND' | 'OR', + // rules: [{ + // id: string, + // condition: string, // JavaScript expression + // action: 'SET' | 'CALCULATE' | 'VALIDATE', + // variable: string, // Target variable + // value: any, // Value to set/calculate + // errorMessage: string // Error message for validation + // }] + // }] + + priority: 'medium', // Execution priority + executionMode: 'sequential', // 'sequential' | 'parallel' + continueOnError: false, // Continue on rule failure + + // Visual properties + shape: 'rectangle', + backgroundColor: '#fdf4ff', + borderColor: '#a855f7', + textColor: '#7c3aed' +} +``` + +#### Script Node (`type: 'script'`) +```javascript +data: { + // Script configuration + scriptCode: '', // JavaScript code to execute + scriptLanguage: 'javascript', // Currently only 'javascript' supported + + // Variable mapping + inputVariables: [], // Array of variable names to pass to script + outputVariables: [], // Array of variable names to extract from script + + // Execution configuration + timeout: 30000, // Script execution timeout + sandbox: true, // Execute in sandboxed environment + + // Visual properties + shape: 'rectangle', + backgroundColor: '#f9fafb', + borderColor: '#6b7280', + textColor: '#374151' +} +``` + +#### Notification Node (`type: 'notification'`) +```javascript +data: { + // Notification configuration + notificationType: 'info', // 'info' | 'warning' | 'error' | 'success' + + // Recipients + recipientType: 'user', // 'user' | 'role' | 'email' | 'variable' + recipientUser: '', // User ID for 'user' type + recipientRole: '', // Role ID for 'role' type + recipientVariable: '', // Variable name for 'variable' type + recipientEmail: '', // Email address for 'email' type + + // Message content + subject: '', // Notification subject/title + message: '', // Notification message body + + // Delivery options + deliveryOptions: { + inApp: true, // In-app notification + email: false, // Email notification + sms: false // SMS notification + }, + + // Timing + priority: 'medium', // 'low' | 'medium' | 'high' | 'urgent' + expiration: { + enabled: false, // Enable expiration + value: 24, // Expiration value + unit: 'hours' // 'minutes' | 'hours' | 'days' + }, + + // Visual properties + shape: 'rectangle', + backgroundColor: '#f0f9ff', + borderColor: '#0ea5e9', + textColor: '#0284c7' +} +``` + +#### HTML Node (`type: 'html'`) +```javascript +data: { + // Content configuration + htmlCode: '', // HTML content + cssCode: '', // CSS styles + jsCode: '', // JavaScript code + + // Variable integration + inputVariables: [], // Variables to pass to HTML context + outputVariables: [], // Variables to extract from HTML + allowVariableAccess: true, // Allow HTML to access process variables + + // Behavior + autoRefresh: false, // Auto-refresh content + refreshInterval: 30000, // Refresh interval in milliseconds + + // Visual properties + shape: 'rectangle', + backgroundColor: '#e0f2fe', + borderColor: '#0ea5e9', + textColor: '#0c4a6e' +} +``` + +#### Subprocess Node (`type: 'subprocess'`) +```javascript +data: { + // Subprocess configuration + subprocessId: null, // Target subprocess ID + subprocessName: '', // Subprocess name for display + + // Variable mapping + inputMappings: [], // Map parent variables to subprocess + outputMappings: [], // Map subprocess variables to parent + + // Execution configuration + executionMode: 'synchronous', // 'synchronous' | 'asynchronous' + inheritVariables: true, // Inherit parent process variables + + // Visual properties + shape: 'rectangle', + backgroundColor: '#f0fdfa', + borderColor: '#14b8a6', + textColor: '#134e4a' +} +``` + +### Node Store Management (`/stores/processBuilder.js`) + +**Key Node Operations:** +- `addNode(node)`: Add node to process (lines 700-752) +- `updateNode(nodeId, updates)`: Update existing node (lines 757-766) +- `deleteNode(nodeId)`: Remove node and connected edges (lines 771-814) +- `getNodeValidation(nodeId)`: Get validation issues for node (lines 78-80) + +**Node State Management:** +- Nodes stored in `currentProcess.nodes` array +- Selection tracked in `selectedNodeId` +- Validation results in `validationResults` Map +- Auto-save triggers on node changes with 2-second debounce + +### Adding New Node Types + +**Step 1: Define Node Type in ProcessBuilderComponents.vue** +```javascript +// Add to availableComponents array +{ + type: 'your-node-type', + name: 'Your Node Name', + category: 'Core', + icon: 'material-symbols:your-icon', + description: 'Node description', + defaultProps: { + label: 'Your Node', + data: { + // Your node-specific data structure + // Always include visual properties: + shape: 'rectangle', + backgroundColor: '#ffffff', + borderColor: '#000000', + textColor: '#000000' + } + } +} +``` + +**Step 2: Create Node Vue Component** +Create `/components/process-flow/custom/YourNodeTypeNode.vue` following the standard node component structure. + +**Step 3: Create Configuration Components** +- `/components/process-flow/YourNodeTypeConfiguration.vue` - Configuration UI +- `/components/process-flow/YourNodeTypeConfigurationModal.vue` - Modal wrapper + +**Step 4: Register in Main Process Builder** +Add imports and modal state management in `/pages/process-builder/index.vue`. + +**Step 5: Add Validation Rules (Optional)** +Implement validation logic in the process store's validation system. + +### Node Connection System + +**Handle Configuration:** +- All functional nodes must have 4 directional handles: top, bottom, left, right +- Handle IDs follow pattern: `${nodeId}-${direction}` +- Use Vue Flow's Handle component with proper positioning +- Connection validation via `isValidConnection` callback + +**Edge Management:** +- Edges stored in `currentProcess.edges` array +- Edge IDs typically follow pattern: `${sourceNodeId}-${targetNodeId}` +- Support for conditional edges with labels on gateway nodes +- Automatic cleanup when nodes are deleted + +### Node Validation System + +**Validation Integration:** +- Each node can have validation issues (errors, warnings, info) +- ValidationIndicator component shows issues visually +- Validation results stored in process store's validationResults Map +- Real-time validation updates trigger UI changes + +**Common Validation Patterns:** +- Required field validation (formId for form nodes, apiUrl for API nodes) +- Connection validation (start/end node requirements) +- Variable reference validation +- Configuration completeness checks + +## PageWrapper Architecture + +### Overview +The **PageWrapper** node is a revolutionary approach to page rendering that allows multiple child components (FormNode, HtmlNode, TableNode) to be rendered together as a single page. This replaces the previous one-node-per-page limitation. + +### Key Concepts +- **Container-based Rendering**: PageWrapper acts as a container that manages multiple child components +- **Conditional Logic**: Each child component can have its own show/hide conditions based on process variables +- **Variable Mapping**: Independent input/output variable mapping for each child component +- **Flexible Layouts**: Grid, flex, and stacked layout options with responsive design +- **Backward Compatibility**: Existing standalone FormNode and HtmlNode continue to work + +### PageWrapper Node Structure +```javascript +{ + type: 'page-wrapper', + data: { + // Layout configuration + layout: 'grid', // 'grid' | 'flex' | 'stacked' + columns: 2, // for grid layout + gap: '1rem', // spacing between components + padding: '1rem', // container padding + + // Child components configuration + childNodes: [ + { + id: 'child-1', // Unique child ID + type: 'form', // 'form' | 'html' | 'table' + nodeId: 'form-123', // Reference to actual form/html/table node + position: { row: 1, col: 1 }, // Grid position (for grid layout) + conditionalLogic: { + enabled: true, + variable: 'showForm', + operator: '==', + value: true + }, + variableMapping: { + inputs: [{ processVar: 'userName', childVar: 'name' }], + outputs: [{ childVar: 'email', processVar: 'userEmail' }] + } + } + ], + + // Page-level settings + title: 'Multi-Component Page', + backgroundColor: '#ffffff', + customCSS: '', // Page-level CSS + customJS: '', // Page-level JavaScript + pageVariables: {} // Page-scoped variables + } +} +``` + +### Child Component Types + +#### Form Child Component +- **Purpose**: Renders a form within the PageWrapper +- **Configuration**: References an existing form by `nodeId` +- **Variable Mapping**: Maps process variables to form fields and vice versa +- **Conditional Display**: Can be shown/hidden based on process variables + +#### HTML Child Component +- **Purpose**: Renders HTML content within the PageWrapper +- **Configuration**: Contains HTML, CSS, and JavaScript code +- **Variable Integration**: HTML can access and update process variables +- **Interactive Elements**: Supports buttons, forms, and custom interactions + +#### Table Child Component (Future) +- **Purpose**: Renders tabular data within the PageWrapper +- **Configuration**: Data source, columns, filtering, sorting options +- **Actions**: Row selection, inline editing, custom actions + +### Implementation Architecture + +#### Core Components +1. **PageWrapperNode.vue** (`/components/process-flow/custom/`) + - Visual representation in process builder + - Shows child component count and layout type + - Handles selection and configuration access + +2. **PageWrapperConfiguration.vue** (`/components/process-flow/`) + - Configuration UI for PageWrapper settings + - Child component management (add, remove, configure) + - Layout and styling options + +3. **PageWrapperConfigurationModal.vue** (`/components/process-flow/`) + - Modal wrapper for configuration + - Layout preview functionality + - Integration with process builder + +4. **PageWrapperRenderer.vue** (`/components/process-flow/`) + - Runtime rendering engine for PageWrapper execution + - Handles child component rendering and layout + - Manages variable mapping and conditional logic + - Coordinates form submissions and HTML interactions + +#### Child Renderers +1. **ChildFormRenderer.vue** (`/components/process-flow/`) + - Renders form components within PageWrapper + - Handles form validation and submission + - Applies input/output variable mappings + - Supports conditional logic and field states + +2. **ChildHtmlRenderer.vue** (`/components/process-flow/`) + - Renders HTML content within PageWrapper + - Executes custom CSS and JavaScript + - Handles HTML interactions and variable updates + - Provides safe execution context for custom scripts + +### Process Store Integration + +#### Parent-Child Node Management +- `addChildNode(parentId, childNodeConfig)`: Add child to PageWrapper +- `updateChildNode(parentId, childId, updates)`: Update child configuration +- `removeChildNode(parentId, childId)`: Remove child from PageWrapper +- `getChildNodes(parentId)`: Get all children of PageWrapper +- `getChildNode(parentId, childId)`: Get specific child configuration + +#### Enhanced Node Operations +- `deleteNodeWithChildren(nodeId)`: Handles PageWrapper deletion with cleanup +- `isChildNode(nodeId)`: Check if node is referenced as child +- `getParentNodeId(childNodeId)`: Find parent PageWrapper for child reference + +### Workflow Execution Integration + +#### Execution Flow +1. **Process Reaches PageWrapper**: Node type 'page-wrapper' detected +2. **Child Component Loading**: Each child component loads its referenced content +3. **Variable Mapping**: Process variables mapped to child components as inputs +4. **Conditional Rendering**: Child components shown/hidden based on conditions +5. **User Interaction**: Users interact with forms, HTML, tables simultaneously +6. **Data Collection**: Variable mappings collect outputs from child components +7. **Page Submission**: All child data merged and process continues + +#### Event Handling +- `handlePageWrapperSubmit`: Processes page-level submission +- `handleChildFormSubmit`: Handles individual form submissions within page +- `handleChildHtmlAction`: Processes HTML interactions (buttons, forms) +- `handleVariableUpdate`: Updates process variables from child components +- `handleValidationError`: Manages validation errors across child components + +### Layout System + +#### Grid Layout +- **Columns**: Configurable number of columns (1-6) +- **Positioning**: Child components specify row and column +- **Responsive**: Automatically stacks on mobile devices +- **Gap**: Configurable spacing between grid items + +#### Flex Layout +- **Direction**: Horizontal with wrap support +- **Responsive**: Components adapt to available width +- **Alignment**: Flexible arrangement with equal spacing +- **Mobile-friendly**: Stacks vertically on small screens + +#### Stacked Layout +- **Vertical**: All components arranged vertically +- **Full-width**: Each component takes full container width +- **Spacing**: Consistent vertical spacing between components +- **Simple**: No complex positioning required + +### Variable Management + +#### Input Mapping +- Maps process variables to child component fields +- Applied when PageWrapper loads +- Supports type conversion and default values +- Real-time updates when process variables change + +#### Output Mapping +- Maps child component fields to process variables +- Applied on form submission or HTML interaction +- Supports data transformation and validation +- Merges data from multiple child components + +#### Page-level Variables +- Scoped to the PageWrapper instance +- Shared between child components +- Can be used for component coordination +- Persisted during page interaction + +### Conditional Logic System + +#### Variable-based Conditions +- Show/hide child components based on process variables +- Supports multiple operators: ==, !=, >, <, >=, <= +- Works with string, number, and boolean values +- Real-time evaluation when variables change + +#### Complex Logic (Future) +- AND/OR combinations +- Multiple condition groups +- JavaScript expressions +- Time-based conditions + +### Migration Strategy + +#### From Standalone Nodes +1. **Assessment**: Identify processes with sequential form/HTML nodes +2. **Grouping**: Group related nodes into logical pages +3. **Configuration**: Create PageWrapper with child components +4. **Variable Mapping**: Set up input/output mappings +5. **Testing**: Verify functionality matches original flow + +#### Backward Compatibility +- Existing processes continue to work unchanged +- Standalone FormNode and HtmlNode still supported +- Gradual migration path available +- No breaking changes to existing functionality + +### Development Guidelines + +#### Creating Child Components +1. **Reference Existing Nodes**: Child components reference existing form/HTML nodes +2. **Variable Mapping**: Always configure input/output mappings +3. **Conditional Logic**: Use for dynamic UI behavior +4. **Layout Consideration**: Design for responsive layouts +5. **Testing**: Test with various screen sizes and variable combinations + +#### Performance Considerations +- **Lazy Loading**: Child components load content on demand +- **Conditional Rendering**: Hidden components don't render +- **Variable Watching**: Efficient reactivity system +- **Memory Management**: Proper cleanup on component destruction + +#### Best Practices +- **Logical Grouping**: Group related functionality into single PageWrapper +- **Clear Naming**: Use descriptive titles and IDs for child components +- **Variable Scope**: Use appropriate variable mapping to avoid conflicts +- **Layout Planning**: Consider mobile-first design approach +- **Testing**: Validate with realistic data and user scenarios \ No newline at end of file diff --git a/components/process-flow/ChildFormRenderer.vue b/components/process-flow/ChildFormRenderer.vue new file mode 100644 index 0000000..1d111c5 --- /dev/null +++ b/components/process-flow/ChildFormRenderer.vue @@ -0,0 +1,367 @@ + + + + + \ No newline at end of file diff --git a/components/process-flow/ChildHtmlRenderer.vue b/components/process-flow/ChildHtmlRenderer.vue new file mode 100644 index 0000000..f15d5c3 --- /dev/null +++ b/components/process-flow/ChildHtmlRenderer.vue @@ -0,0 +1,494 @@ + + + + + \ No newline at end of file diff --git a/components/process-flow/PageWrapperConfiguration.vue b/components/process-flow/PageWrapperConfiguration.vue new file mode 100644 index 0000000..cb9d252 --- /dev/null +++ b/components/process-flow/PageWrapperConfiguration.vue @@ -0,0 +1,882 @@ + + + + + \ No newline at end of file diff --git a/components/process-flow/PageWrapperConfigurationModal.vue b/components/process-flow/PageWrapperConfigurationModal.vue new file mode 100644 index 0000000..4d10de6 --- /dev/null +++ b/components/process-flow/PageWrapperConfigurationModal.vue @@ -0,0 +1,226 @@ + + + + + \ No newline at end of file diff --git a/components/process-flow/PageWrapperRenderer.vue b/components/process-flow/PageWrapperRenderer.vue new file mode 100644 index 0000000..09654b7 --- /dev/null +++ b/components/process-flow/PageWrapperRenderer.vue @@ -0,0 +1,565 @@ + + + + + \ No newline at end of file diff --git a/components/process-flow/ProcessBuilderComponents.vue b/components/process-flow/ProcessBuilderComponents.vue index 18291cb..065c3b9 100644 --- a/components/process-flow/ProcessBuilderComponents.vue +++ b/components/process-flow/ProcessBuilderComponents.vue @@ -281,6 +281,43 @@ const availableComponents = [ } } }, + { + type: 'page-wrapper', + name: 'Page Wrapper', + category: 'Core', + icon: 'material-symbols:web-asset', + description: 'Container for multiple components (forms, HTML, tables) to render as a single page', + defaultProps: { + label: 'Page Wrapper', + data: { + description: 'Multi-component page container', + + // Layout configuration + layout: 'grid', // 'grid' | 'flex' | 'stacked' + columns: 2, // for grid layout + gap: '1rem', // spacing between child components + padding: '1rem', // container padding + + // Child nodes array + childNodes: [], // Array of child node configurations + + // Page-level settings + title: 'Multi-Component Page', + backgroundColor: '#ffffff', + customCSS: '', + customJS: '', + + // Variable management + pageVariables: {}, // Page-scoped variables + + // Visual properties + shape: 'rectangle', + backgroundColor: '#f8f9fa', + borderColor: '#6c757d', + textColor: '#495057' + } + } + }, // Design Elements / Shapes { diff --git a/components/process-flow/ProcessFlowCanvas.vue b/components/process-flow/ProcessFlowCanvas.vue index 8664e7f..568e3b4 100644 --- a/components/process-flow/ProcessFlowCanvas.vue +++ b/components/process-flow/ProcessFlowCanvas.vue @@ -8,6 +8,7 @@ import { defineExpose, nextTick, markRaw, + provide, } from "vue"; import { VueFlow, useVueFlow, Panel } from "@vue-flow/core"; import { Background } from "@vue-flow/background"; @@ -34,6 +35,7 @@ import SwimlaneHorizontal from "~/components/process-flow/custom/SwimlaneHorizon import SwimlaneVertical from "~/components/process-flow/custom/SwimlaneVertical.vue"; import TextAnnotation from "~/components/process-flow/custom/TextAnnotation.vue"; import ProcessGroup from "~/components/process-flow/custom/ProcessGroup.vue"; +import PageWrapperNode from "~/components/process-flow/custom/PageWrapperNode.vue"; import "@vue-flow/core/dist/style.css"; import "@vue-flow/core/dist/theme-default.css"; import "@vue-flow/controls/dist/style.css"; @@ -59,6 +61,7 @@ const customNodeTypes = { "swimlane-vertical": markRaw(SwimlaneVertical), "text-annotation": markRaw(TextAnnotation), "process-group": markRaw(ProcessGroup), + "page-wrapper": markRaw(PageWrapperNode), }; // Add Material Icons import @@ -93,6 +96,10 @@ const emit = defineEmits([ "edgeSelected", "selectionChange", "nodesSelection", + "child-click", + "header-click", + "child-add", + "child-drop", ]); // Get the flow instance @@ -305,6 +312,35 @@ const onPaneClick = () => { emit("paneClick"); }; +// Handle child node events from PageWrapper +const onChildNodeClick = (eventData) => { + console.log('Canvas received child-click:', eventData); + emit("child-click", eventData); +}; + +const onPageWrapperHeaderClick = (nodeId) => { + console.log('Canvas received header-click:', nodeId); + emit("header-click", nodeId); +}; + +const onChildAdd = (parentId) => { + console.log('Canvas received child-add:', parentId); + emit("child-add", parentId); +}; + +const onChildDrop = (eventData) => { + console.log('Canvas received child-drop:', eventData); + emit("child-drop", eventData); +}; + +// Provide child node event handlers to PageWrapper components +provide('childNodeEventHandlers', { + onChildNodeClick, + onPageWrapperHeaderClick, + onChildAdd, + onChildDrop, +}); + // Window resize handler const resizeFlow = () => { setTimeout(() => { diff --git a/components/process-flow/custom/PageWrapperNode.vue b/components/process-flow/custom/PageWrapperNode.vue new file mode 100644 index 0000000..4bd11da --- /dev/null +++ b/components/process-flow/custom/PageWrapperNode.vue @@ -0,0 +1,800 @@ + + + + + \ No newline at end of file diff --git a/pages/process-builder/index.vue b/pages/process-builder/index.vue index 9f11333..1d90832 100644 --- a/pages/process-builder/index.vue +++ b/pages/process-builder/index.vue @@ -20,6 +20,7 @@ import NotificationNodeConfigurationModal from '~/components/process-flow/Notifi import ScriptNodeConfigurationModal from '~/components/process-flow/ScriptNodeConfigurationModal.vue'; import HtmlNodeConfigurationModal from '~/components/process-flow/HtmlNodeConfigurationModal.vue'; import SubprocessNodeConfigurationModal from '~/components/process-flow/SubprocessNodeConfigurationModal.vue'; +import PageWrapperConfigurationModal from '~/components/process-flow/PageWrapperConfigurationModal.vue'; import ProcessTemplatesModal from '~/components/ProcessTemplatesModal.vue'; import ProcessSettingsModal from '~/components/process-flow/ProcessSettingsModal.vue'; import ProcessHistoryModal from '~/components/ProcessHistoryModal.vue'; @@ -137,6 +138,7 @@ const showNotificationConfigModal = ref(false); const showScriptConfigModal = ref(false); const showHtmlConfigModal = ref(false); const showSubprocessConfigModal = ref(false); +const showPageWrapperConfigModal = ref(false); const showTemplatesModal = ref(false); const showProcessSettings = ref(false); const showDropdown = ref(false); @@ -561,7 +563,8 @@ const handleKeyboardShortcuts = (event) => { showNotificationConfigModal.value || showScriptConfigModal.value || showHtmlConfigModal.value || - showSubprocessConfigModal.value || + showSubprocessConfigModal.value || + showPageWrapperConfigModal.value || showTemplatesModal.value || showProcessSettings.value || showProcessHistoryModal.value || @@ -1183,6 +1186,127 @@ const onEdgeSelected = (edge) => { processStore.selectEdge(edge.id); }; +// Handle child node click within PageWrapper +const onChildNodeClick = ({ parentId, childNode }) => { + console.log('Child node clicked:', childNode.id, 'in parent:', parentId); + + // Create a pseudo-node for the child to work with existing configuration system + const childNodeData = { + id: `${parentId}-child-${childNode.id}`, + type: childNode.type, + label: childNode.label || getChildNodeTypeName(childNode.type), + data: { + ...childNode, + isChildNode: true, + parentId: parentId, + // Map child node properties to match expected node data structure + formId: childNode.nodeId, // For form children + htmlCode: childNode.htmlCode, // For HTML children + cssCode: childNode.cssCode, + jsCode: childNode.jsCode + } + }; + + // Set selected data to enable configuration + selectedNodeData.value = childNodeData; + selectedNode.value = null; // Clear parent node selection + selectedEdgeData.value = null; + + // Don't select in store since it's not a real process node + processStore.selectedNodeId = null; +}; + +// Handle header click on PageWrapper (configure container) +const onPageWrapperHeaderClick = (nodeId) => { + console.log('PageWrapper header clicked:', nodeId); + + // Find the actual PageWrapper node + const node = processStore.currentProcess?.nodes.find(n => n.id === nodeId); + if (node) { + onNodeSelected(node); + } +}; + +// Handle adding child to PageWrapper +const onChildAdd = (parentId) => { + console.log('Add child to PageWrapper:', parentId); + + // Show component palette or add default child + // For now, add a default form child + const newChild = { + id: `child-${Date.now()}`, + type: 'form', + label: 'New Form', + nodeId: null, // Will be set when form is selected + position: { row: 1, col: 1 }, + conditionalLogic: { + enabled: false, + variable: '', + operator: '==', + value: '' + }, + variableMapping: { + inputs: [], + outputs: [] + } + }; + + // Add to parent node + processStore.addChildNode(parentId, newChild); + + // Show success message + if (toast) { + toast.success('Child component added'); + } +}; + +// Handle dropping node into PageWrapper +const onChildDrop = ({ parentId, childData, dropPosition }) => { + console.log('Node dropped into PageWrapper:', childData.type, 'into', parentId); + + // Create child node configuration from dropped data + const newChild = { + id: `child-${Date.now()}`, + type: childData.type, + label: childData.label || getChildNodeTypeName(childData.type), + nodeId: null, // Will be set when actual node is selected/created + position: { + row: Math.ceil(dropPosition.y / 80) || 1, + col: Math.ceil(dropPosition.x / 120) || 1 + }, + conditionalLogic: { + enabled: false, + variable: '', + operator: '==', + value: '' + }, + variableMapping: { + inputs: [], + outputs: [] + }, + // Copy relevant data from dropped component + ...childData.data + }; + + // Add to parent node + processStore.addChildNode(parentId, newChild); + + // Show success message + if (toast) { + toast.success(`${getChildNodeTypeName(childData.type)} added to page`); + } +}; + +// Helper function to get child node type name +const getChildNodeTypeName = (type) => { + const nameMap = { + form: 'Form', + html: 'HTML Content', + table: 'Table' + }; + return nameMap[type] || 'Component'; +}; + // Update edge label const updateEdgeLabel = (value) => { if (selectedEdgeData.value) { @@ -2300,6 +2424,28 @@ const handleSubprocessNodeUpdate = async (updatedData) => { } }; +// Handle Page Wrapper node update +const handlePageWrapperNodeUpdate = async (updatedData) => { + if (selectedNodeData.value && selectedNodeData.value.type === 'page-wrapper') { + // Make sure to update the label both in data and at the root level + const newLabel = updatedData.title || updatedData.label || 'Page Wrapper'; + + // Update the data + selectedNodeData.value.data = { + ...updatedData, + label: newLabel // Ensure label is in data + }; + + // Also update the root label + selectedNodeData.value.label = newLabel; + + // Update the node in store and refresh + await updateNodeInStore(); + + console.log('Page Wrapper node updated:', selectedNodeData.value.id, 'Child nodes:', updatedData.childNodes?.length || 0); + } +}; + // Handle process restoration from history const handleProcessRestored = (restoredProcess) => { // The process has been restored in the backend, so we need to reload it @@ -3064,6 +3210,10 @@ const sendToBack = () => { @pane-click="onPaneClick" @nodes-change="onNodesChange" @edges-change="onEdgesChange" + @child-click="onChildNodeClick" + @header-click="onPageWrapperHeaderClick" + @child-add="onChildAdd" + @child-drop="onChildDrop" class="w-full h-full" /> @@ -3553,6 +3703,15 @@ const sendToBack = () => { Configure Sub-process + + +
+

Configure multi-component page layout and child components.

+ + + Configure Page Wrapper + +
@@ -3732,6 +3891,16 @@ const sendToBack = () => { @update="handleSubprocessNodeUpdate" /> + + + { 'decision': { label: 'Decision Point', icon: 'material-symbols:alt-route', color: 'yellow' }, 'gateway': { label: 'Decision Gateway', icon: 'material-symbols:fork-right', color: 'yellow' }, 'html': { label: 'HTML Content', icon: 'material-symbols:code-blocks', color: 'indigo' }, + 'page-wrapper': { label: 'Multi-Component Page', icon: 'material-symbols:web-asset', color: 'teal' }, 'notification': { label: 'Notification', icon: 'material-symbols:notifications', color: 'pink' }, 'end': { label: 'Process Complete', icon: 'material-symbols:check-circle', color: 'green' } }; @@ -868,6 +870,17 @@ const getStepInfo = (node) => { return stepTypes[node?.type] || { label: 'Unknown Step', icon: 'material-symbols:help', color: 'gray' }; }; +// Get next step label for submit buttons +const getNextStepLabel = () => { + if (nextNode.value) { + if (nextNode.value.type === 'end') { + return 'Complete Process'; + } + return `Continue to ${getStepInfo(nextNode.value).label}`; + } + return 'Continue'; +}; + // Load form data from database const loadFormData = async (formId) => { try { @@ -1092,6 +1105,11 @@ watch(currentStep, async (newStep) => { formStore.updatePreviewFormData(formData.value); } } + } else if (currentNode.value.type === "page-wrapper") { + // PageWrapper nodes are user-interactive and don't auto-advance + // They will be handled in the template rendering section + console.log(`[Workflow] Processing PageWrapper node: ${currentNode.value.data?.title || currentNode.value.label}`); + return; } else if (["decision", "gateway"].includes(currentNode.value.type)) { await executeDecisionNode(); } @@ -1129,6 +1147,92 @@ const validateAndSubmit = () => { } }; +// Handle PageWrapper submit +const handlePageWrapperSubmit = async (pageData) => { + try { + stepLoading.value = true; + console.log('[Workflow] PageWrapper submitted. Data:', pageData); + + // Merge all child form data into process variables + if (pageData.childFormData) { + Object.values(pageData.childFormData).forEach(childData => { + Object.assign(processVariables.value, childData); + }); + } + + // Apply any page-level variable updates + if (pageData.processVariables) { + Object.assign(processVariables.value, pageData.processVariables); + } + + moveToNextStep(); + console.log('[Workflow] After PageWrapper submit, current node:', currentNode.value); + + // Auto-execute next step if it's API/script + if (currentNode.value && ['api', 'script'].includes(currentNode.value.type)) { + await executeCurrentStep(); + } + } catch (error) { + console.error('[Workflow] Error in PageWrapper submit:', error); + } finally { + stepLoading.value = false; + } +}; + +// Handle child form submit within PageWrapper +const handleChildFormSubmit = ({ childNode, formData }) => { + console.log('[Workflow] Child form submitted:', childNode.id, formData); + // Form data is already handled by PageWrapperRenderer through variable mapping +}; + +// Handle child form change within PageWrapper +const handleChildFormChange = ({ childNode, formData }) => { + console.log('[Workflow] Child form changed:', childNode.id, formData); + // Real-time form updates - could be used for auto-save or validation +}; + +// Handle child HTML action within PageWrapper +const handleChildHtmlAction = ({ childNode, action }) => { + console.log('[Workflow] Child HTML action:', childNode.id, action); + + // Handle different HTML action types + switch (action.type) { + case 'button-click': + console.log(`Button clicked: ${action.actionId}`, action.data); + break; + case 'form-submit': + console.log(`HTML form submitted: ${action.actionId}`, action.data); + // Apply form data to process variables + Object.assign(processVariables.value, action.data); + break; + case 'message': + // Could integrate with toast notifications + console.log(`HTML message: ${action.message}`); + break; + case 'error': + console.error(`HTML error: ${action.error}`); + break; + } +}; + +// Handle child table action within PageWrapper +const handleChildTableAction = ({ childNode, action }) => { + console.log('[Workflow] Child table action:', childNode.id, action); + // Future: Handle table interactions like row selection, filtering, etc. +}; + +// Handle variable updates from child components +const handleVariableUpdate = ({ variable, value, source }) => { + console.log(`[Workflow] Variable updated by ${source}: ${variable} = ${value}`); + processVariables.value[variable] = value; +}; + +// Handle validation errors from child components +const handleValidationError = ({ childNode, errors }) => { + console.log('[Workflow] Validation errors in child:', childNode.id, errors); + // Could be used to prevent page submission or show error messages +}; + // Handle decision node execution (automatic or manual) const executeDecisionNode = async () => { try { @@ -1703,6 +1807,26 @@ function getConditionGroupResult(conditionGroup, variables) { + +
+ +
+
diff --git a/stores/processBuilder.js b/stores/processBuilder.js index 8f0545e..3b34d89 100644 --- a/stores/processBuilder.js +++ b/stores/processBuilder.js @@ -813,6 +813,189 @@ export const useProcessBuilderStore = defineStore('processBuilder', { } }, + /** + * Add a child node to a PageWrapper parent + */ + addChildNode(parentId, childNodeConfig) { + if (!this.currentProcess) return; + + const parentNode = this.currentProcess.nodes.find(n => n.id === parentId); + if (!parentNode || parentNode.type !== 'page-wrapper') { + console.warn('Parent node not found or not a PageWrapper:', parentId); + return; + } + + // Ensure childNodes array exists + if (!parentNode.data.childNodes) { + parentNode.data.childNodes = []; + } + + // Generate unique child node ID if not provided + const childNode = { + id: childNodeConfig.id || `child-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + type: childNodeConfig.type || 'form', + nodeId: childNodeConfig.nodeId || '', + position: childNodeConfig.position || { row: 1, col: 1 }, + conditionalLogic: { + enabled: false, + variable: '', + operator: '==', + value: '', + ...childNodeConfig.conditionalLogic + }, + variableMapping: { + inputs: [], + outputs: [], + ...childNodeConfig.variableMapping + } + }; + + parentNode.data.childNodes.push(childNode); + this.saveToHistory('Add child node'); + this.unsavedChanges = true; + + console.log('👶 Store: Added child node', childNode.id, 'to parent', parentId); + return childNode; + }, + + /** + * Update a child node within a PageWrapper parent + */ + updateChildNode(parentId, childId, updates) { + if (!this.currentProcess) return; + + const parentNode = this.currentProcess.nodes.find(n => n.id === parentId); + if (!parentNode || parentNode.type !== 'page-wrapper') { + console.warn('Parent node not found or not a PageWrapper:', parentId); + return; + } + + if (!parentNode.data.childNodes) { + return; + } + + const childIndex = parentNode.data.childNodes.findIndex(c => c.id === childId); + if (childIndex !== -1) { + Object.assign(parentNode.data.childNodes[childIndex], updates); + this.saveToHistory('Update child node'); + this.unsavedChanges = true; + + console.log('📝 Store: Updated child node', childId, 'in parent', parentId); + } + }, + + /** + * Remove a child node from a PageWrapper parent + */ + removeChildNode(parentId, childId) { + if (!this.currentProcess) return; + + const parentNode = this.currentProcess.nodes.find(n => n.id === parentId); + if (!parentNode || parentNode.type !== 'page-wrapper') { + console.warn('Parent node not found or not a PageWrapper:', parentId); + return; + } + + if (!parentNode.data.childNodes) { + return; + } + + const childIndex = parentNode.data.childNodes.findIndex(c => c.id === childId); + if (childIndex !== -1) { + parentNode.data.childNodes.splice(childIndex, 1); + this.saveToHistory('Remove child node'); + this.unsavedChanges = true; + + console.log('🗑️ Store: Removed child node', childId, 'from parent', parentId); + return true; + } + + return false; + }, + + /** + * Get all child nodes for a PageWrapper parent + */ + getChildNodes(parentId) { + if (!this.currentProcess) return []; + + const parentNode = this.currentProcess.nodes.find(n => n.id === parentId); + if (!parentNode || parentNode.type !== 'page-wrapper') { + return []; + } + + return parentNode.data.childNodes || []; + }, + + /** + * Get a specific child node by ID + */ + getChildNode(parentId, childId) { + const childNodes = this.getChildNodes(parentId); + return childNodes.find(c => c.id === childId) || null; + }, + + /** + * Check if a node is a child node (has a parent) + */ + isChildNode(nodeId) { + if (!this.currentProcess) return false; + + // Find any PageWrapper node that contains this node as a child + return this.currentProcess.nodes.some(node => { + if (node.type === 'page-wrapper' && node.data.childNodes) { + return node.data.childNodes.some(child => child.nodeId === nodeId); + } + return false; + }); + }, + + /** + * Get the parent node ID for a child node + */ + getParentNodeId(childNodeId) { + if (!this.currentProcess) return null; + + const parentNode = this.currentProcess.nodes.find(node => { + if (node.type === 'page-wrapper' && node.data.childNodes) { + return node.data.childNodes.some(child => child.nodeId === childNodeId); + } + return false; + }); + + return parentNode ? parentNode.id : null; + }, + + /** + * Enhanced delete node that handles PageWrapper child nodes + */ + deleteNodeWithChildren(nodeId) { + if (!this.currentProcess) return; + + const node = this.currentProcess.nodes.find(n => n.id === nodeId); + if (!node) return false; + + // If it's a PageWrapper, we don't need to do anything special + // Child nodes are just configuration data, not separate process nodes + + // If it's a regular node that might be referenced by child nodes, + // we should clean up those references + if (node.type === 'form' || node.type === 'html' || node.type === 'table') { + // Find any PageWrapper nodes that reference this node as a child + this.currentProcess.nodes.forEach(parentNode => { + if (parentNode.type === 'page-wrapper' && parentNode.data.childNodes) { + // Remove any child node configurations that reference this node + parentNode.data.childNodes = parentNode.data.childNodes.filter( + child => child.nodeId !== nodeId + ); + } + }); + } + + // Now delete the node normally + return this.deleteNode(nodeId); + }, + /** * Add an edge to the current process */