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 HTML content configured
+
Component ID: {{ childNode.nodeId || 'Not set' }}
+
+
+
+
+
+
+
+ {{ action.label }}
+
+
+
+
+
+
Variables Available: {{ Object.keys(processVariables).join(', ') || 'None' }}
+
HTML Length: {{ (childNode.htmlCode || '').length }} characters
+
CSS Length: {{ (childNode.cssCode || '').length }} characters
+
JS Length: {{ (childNode.jsCode || '').length }} characters
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+ Page Information
+
+
+
+
+ Page Title
+
+
+
+
+
+
+ Description
+ (Optional)
+
+
+
+
+
+
+
+
+
+ How should components be arranged?
+
+
+
+
+
+
+
+
+
Organized Rows & Columns
+
Perfect for forms, cards, or structured content
+
+
+
+
+
+
+
+
+
+
+
Side by Side
+
Components arranged horizontally, responsive
+
+
+
+
+
+
+
+
+
+
+
One Below Another
+
Components stacked vertically, great for mobile
+
+
+
+
+
+
+
+
+
+
+
+
+ Layout Settings
+
+
+
+
+
+ How many columns?
+ (1-6 columns)
+
+
+
+ {{ num }}
+
+
+
+
+
+
+
+ Space between components
+ (How much gap between each component)
+
+
+
+
+
+
+
+ Space around content
+ (Padding inside the container)
+
+
+
+
+
+
+
+
+
+ Components in this Page
+
+
+
+
+
+
+
Add Components
+
+ Drag Form , HTML , or Table nodes from the component palette into the PageWrapper container on the canvas.
+
+
+
+
+ Form
+
+
+
+ HTML
+
+
+
+ Table
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Component Reference
+
+
Unique identifier for this component
+
+
+
+
+
+
+
+
+
+
+ Show/hide based on conditions
+
+
+
+
+
Show this component when:
+
+
+ Variable
+
+
+
+
+ Condition
+
+ equals
+ doesn't equal
+ is greater than
+ is less than
+ greater or equal
+ less or equal
+
+
+
+ Value
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ showComponentSettings[index] ? 'Hide Settings' : 'Show Settings' }}
+
+
+
+
+
+
+
+
+
+ Advanced Options
+
+
+
+
+
+
+
Background Color
+
+
+ {{ localData.backgroundColor }}
+
+
+
+
+
+
Custom CSS Styling
+
+
Add custom CSS to style your page components
+
+
+
+
+
Custom JavaScript
+
+
Add custom JavaScript for dynamic behavior
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
Configure Page Wrapper
+
+ Create a multi-component page by adding forms, HTML content, and tables. Configure layout,
+ conditional logic, and variable mappings for each child component.
+
+
+
+
+
+
+
+
+
+
+
+
+ Quick Reference Guide
+
+
+
+ Grid Layout: Organize components in rows and columns
+ Flex Layout: Responsive horizontal or vertical arrangement
+ Stacked Layout: Components arranged vertically
+ Conditional Logic: Show/hide components based on process variables
+ Variable Mapping: Pass data between process and child components
+
+
+
+
+
+
+
+
+ Layout Preview
+
+
+
+
+
+
+
+ {{ getChildNodeTypeName(child.type) }}
+
+
+
+
+
+
+
+ {{ getChildNodeTypeName(child.type) }}
+
+
+
+
+
+
+
+ {{ getChildNodeTypeName(child.type) }}
+
+
+
+
+
+
No child components to preview
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
{{ pageWrapperData.title }}
+
+ {{ pageWrapperData.description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Table component coming soon
+
+
+
+
+
+
+
+ Unknown component type: {{ childNode.type }}
+
+
+
+
+
+
+
+
+
No Components
+
This page wrapper doesn't have any child components configured.
+
+
+
+
+
+
+ Back
+
+
+
+
+
+ {{ submitButtonText }}
+
+
+
+
+
+
Debug Information
+
+
Layout: {{ pageWrapperData.layout }}
+
Child Nodes: {{ childNodes.length }}
+
Visible Nodes: {{ visibleChildNodes.length }}
+
Form Data: {{ JSON.stringify(childFormData, null, 2) }}
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ web_asset
+
+
{{ nodeLabel }}
+
+
+
+
+
{{ data?.description || 'Page container with components' }}
+
+ Layout:
+
+ {{ getLayoutDisplayName() }}
+
+
+
+ Components:
+
+ {{ visualChildNodes.length }}
+
+
+
+
+
+
+
+
+
+
+ {{ getLayoutDisplayName() }}
+
+
+
+
+
+
+ {{ childNode.label || getChildNodeTypeName(childNode.type) }}
+
+
+
+
+
+
+
+
+ {{ hasChildNodes ? 'Drop more components' : 'Drag Form, HTML, or Table nodes here' }}
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
*/