Enhance Workflow Execution with Iframe Support and Error Notification

- Introduced iframe mode detection to streamline the user experience when embedded in other applications.
- Added functionality to update the URL upon process completion, allowing for better integration with parent applications.
- Implemented error notification to the parent iframe, ensuring that any issues during workflow execution are communicated effectively.
- Enhanced UI responsiveness by adjusting styles based on iframe mode, improving overall usability and visual consistency.
- Updated process completion and error handling logic to support seamless multi-process workflows.
This commit is contained in:
Md Afiq Iskandar 2025-07-29 13:09:27 +08:00
parent 1448aef0ed
commit e4b1c7e444
3 changed files with 946 additions and 53 deletions

View File

@ -0,0 +1,725 @@
# Iframe Integration Guide
This guide explains how to integrate Corrad workflow forms into external applications using iframes. This enables seamless embedding of business processes into customer portals, external websites, or any web application.
## Table of Contents
- [Overview](#overview)
- [Basic Integration](#basic-integration)
- [Advanced Configuration](#advanced-configuration)
- [Multi-Process Workflows](#multi-process-workflows)
- [Error Handling](#error-handling)
- [Security Considerations](#security-considerations)
- [Examples](#examples)
- [Troubleshooting](#troubleshooting)
## Overview
Corrad provides a powerful iframe integration system that allows you to embed workflow forms into external applications. The system supports:
- **Clean UI Mode**: Hide all debug information and UI chrome
- **Seamless Multi-Process**: Chain multiple workflows together
- **Real-time Communication**: Parent-child message passing
- **Error Handling**: Graceful error management
- **Progress Tracking**: Monitor workflow completion
## Basic Integration
### Simple Iframe Embedding
```html
<!-- Basic iframe integration -->
<iframe
src="/workflow/your-process-id?debug=false"
width="100%"
height="600px"
style="border: none;">
</iframe>
```
### URL Parameters
| Parameter | Description | Example |
|-----------|-------------|---------|
| `debug=false` | Enable iframe mode (hide UI chrome) | `?debug=false` |
| `hideComplete=true` | Hide completion message | `?hideComplete=true` |
| `theme=dark` | Apply custom theme (if supported) | `?theme=dark` |
### Complete Example
```html
<!DOCTYPE html>
<html>
<head>
<title>Workflow Integration</title>
<style>
.workflow-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.workflow-iframe {
width: 100%;
height: 600px;
border: 1px solid #ddd;
border-radius: 8px;
}
</style>
</head>
<body>
<div class="workflow-container">
<h1>Complete Your Application</h1>
<iframe
id="workflow-iframe"
src="/workflow/application-form?debug=false&hideComplete=true"
class="workflow-iframe">
</iframe>
</div>
<script>
// Listen for workflow completion
window.addEventListener('message', (event) => {
if (event.data.type === 'workflow-complete') {
console.log('Workflow completed:', event.data);
// Handle completion
showSuccessMessage('Application submitted successfully!');
} else if (event.data.type === 'workflow-error') {
console.error('Workflow error:', event.data);
showErrorMessage(event.data.error);
}
});
function showSuccessMessage(message) {
alert(message);
}
function showErrorMessage(error) {
alert('Error: ' + error);
}
</script>
</body>
</html>
```
## Advanced Configuration
### Message Event Structure
The workflow sends the following message types to the parent iframe:
#### Completion Message
```javascript
{
type: 'workflow-complete',
processId: 'application-form',
processName: 'Application Form',
hideCompletionMessage: true,
timestamp: '2024-01-15T10:30:00.000Z'
}
```
#### Error Message
```javascript
{
type: 'workflow-error',
processId: 'application-form',
processName: 'Application Form',
error: 'API call failed: Network error',
timestamp: '2024-01-15T10:30:00.000Z'
}
```
### Advanced Integration Example
```html
<!DOCTYPE html>
<html>
<head>
<title>Advanced Workflow Integration</title>
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #007bff;
transition: width 0.3s ease;
}
.workflow-iframe {
width: 100%;
height: 600px;
border: 1px solid #ddd;
border-radius: 8px;
}
.status {
margin-top: 10px;
font-size: 14px;
color: #6c757d;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Employee Onboarding</h1>
<div class="progress-bar">
<div id="progress-fill" class="progress-fill" style="width: 0%"></div>
</div>
<div id="status" class="status">Starting process...</div>
</div>
<iframe
id="workflow-iframe"
src="/workflow/personal-info?debug=false&hideComplete=true"
class="workflow-iframe">
</iframe>
</div>
<script>
let currentStep = 0;
const totalSteps = 5;
// Workflow steps configuration
const workflowSteps = [
{ id: 'personal-info', name: 'Personal Information' },
{ id: 'employment', name: 'Employment Details' },
{ id: 'documents', name: 'Document Upload' },
{ id: 'benefits', name: 'Benefits Selection' },
{ id: 'final', name: 'Final Review' }
];
// Listen for workflow messages
window.addEventListener('message', (event) => {
if (event.data.type === 'workflow-complete') {
console.log('Workflow step completed:', event.data);
if (event.data.hideCompletionMessage) {
// Auto-advance to next step
setTimeout(() => {
loadNextStep();
}, 100);
}
} else if (event.data.type === 'workflow-error') {
console.error('Workflow error:', event.data);
showError(event.data.error);
}
});
function loadNextStep() {
currentStep++;
if (currentStep >= workflowSteps.length) {
// All steps complete
showFinalSuccess();
return;
}
const step = workflowSteps[currentStep];
const iframe = document.getElementById('workflow-iframe');
// Update iframe source
iframe.src = `/workflow/${step.id}?debug=false&hideComplete=true`;
// Update progress
updateProgress();
}
function updateProgress() {
const progress = ((currentStep + 1) / totalSteps) * 100;
document.getElementById('progress-fill').style.width = progress + '%';
document.getElementById('status').textContent =
`Step ${currentStep + 1} of ${totalSteps}: ${workflowSteps[currentStep].name}`;
}
function showFinalSuccess() {
document.getElementById('workflow-iframe').style.display = 'none';
document.querySelector('.header').innerHTML = `
<h1>🎉 Onboarding Complete!</h1>
<p>All required information has been submitted successfully.</p>
<button onclick="downloadReport()">Download Report</button>
`;
}
function showError(error) {
document.getElementById('status').textContent = `Error: ${error}`;
document.getElementById('status').style.color = '#dc3545';
}
function downloadReport() {
// Implement report download logic
alert('Report download functionality would be implemented here');
}
// Initialize progress
updateProgress();
</script>
</body>
</html>
```
## Multi-Process Workflows
### Seamless Multi-Step Process
For complex workflows with multiple steps, you can chain processes together seamlessly:
```javascript
// Multi-process workflow configuration
const processes = [
{ id: 'personal-info', name: 'Personal Information' },
{ id: 'employment', name: 'Employment Details' },
{ id: 'documents', name: 'Document Upload' },
{ id: 'references', name: 'References' },
{ id: 'background', name: 'Background Check' },
{ id: 'medical', name: 'Medical Information' },
{ id: 'emergency', name: 'Emergency Contacts' },
{ id: 'benefits', name: 'Benefits Selection' },
{ id: 'payroll', name: 'Payroll Information' },
{ id: 'final', name: 'Final Review' }
];
let currentProcessIndex = 0;
function loadProcess(processIndex) {
if (processIndex >= processes.length) {
// All processes complete
showFinalSuccess();
return;
}
const process = processes[processIndex];
const iframe = document.getElementById('workflow-iframe');
// Load process with hidden completion message
iframe.src = `/workflow/${process.id}?debug=false&hideComplete=true`;
currentProcessIndex = processIndex;
// Update progress indicator
updateProgress(processIndex + 1, processes.length);
}
// Listen for completion and auto-advance
window.addEventListener('message', (event) => {
if (event.data.type === 'workflow-complete') {
console.log(`Process ${event.data.processName} completed`);
if (event.data.hideCompletionMessage) {
// Seamlessly move to next process
setTimeout(() => {
loadProcess(currentProcessIndex + 1);
}, 100);
}
} else if (event.data.type === 'workflow-error') {
console.error('Process failed:', event.data.error);
showErrorMessage(event.data.error);
}
});
// Start the first process
loadProcess(0);
```
## Error Handling
### Comprehensive Error Management
```javascript
// Enhanced error handling
window.addEventListener('message', (event) => {
if (event.data.type === 'workflow-complete') {
handleWorkflowComplete(event.data);
} else if (event.data.type === 'workflow-error') {
handleWorkflowError(event.data);
}
});
function handleWorkflowComplete(data) {
console.log('Workflow completed:', data);
// Store completion data
localStorage.setItem('workflow-completion', JSON.stringify(data));
// Update UI
updateCompletionUI(data);
// Auto-advance if configured
if (data.hideCompletionMessage) {
setTimeout(() => {
loadNextProcess();
}, 100);
}
}
function handleWorkflowError(data) {
console.error('Workflow error:', data);
// Log error for debugging
console.error('Error details:', {
processId: data.processId,
processName: data.processName,
error: data.error,
timestamp: data.timestamp
});
// Show user-friendly error message
showErrorMessage(data.error);
// Optionally retry or allow manual restart
showRetryOptions();
}
function showErrorMessage(error) {
// Create error notification
const errorDiv = document.createElement('div');
errorDiv.className = 'error-notification';
errorDiv.innerHTML = `
<div class="error-content">
<h3>⚠️ An error occurred</h3>
<p>${error}</p>
<button onclick="retryCurrentProcess()">Retry</button>
<button onclick="skipCurrentProcess()">Skip</button>
</div>
`;
document.body.appendChild(errorDiv);
}
function retryCurrentProcess() {
const iframe = document.getElementById('workflow-iframe');
iframe.src = iframe.src; // Reload current process
}
function skipCurrentProcess() {
// Move to next process
loadNextProcess();
}
```
## Security Considerations
### Cross-Origin Communication
When integrating iframes, consider these security aspects:
1. **Origin Validation**: Verify message origin
2. **Content Security Policy**: Configure appropriate CSP headers
3. **Sandbox Attributes**: Use iframe sandbox for additional security
```html
<!-- Secure iframe configuration -->
<iframe
src="/workflow/process-id?debug=false"
sandbox="allow-scripts allow-same-origin allow-forms"
referrerpolicy="no-referrer">
</iframe>
```
### Message Validation
```javascript
// Validate message origin
window.addEventListener('message', (event) => {
// Verify origin (replace with your domain)
if (event.origin !== 'https://your-workflow-domain.com') {
console.warn('Message from unauthorized origin:', event.origin);
return;
}
// Validate message structure
if (!event.data || typeof event.data.type !== 'string') {
console.warn('Invalid message structure:', event.data);
return;
}
// Process valid message
handleWorkflowMessage(event.data);
});
```
## Examples
### Customer Portal Integration
```html
<!-- Customer portal with workflow integration -->
<div class="customer-portal">
<header>
<h1>Welcome, {{ customer.name }}</h1>
<nav>
<a href="#dashboard">Dashboard</a>
<a href="#applications">Applications</a>
<a href="#documents">Documents</a>
</nav>
</header>
<main>
<div class="application-section">
<h2>Complete Your Application</h2>
<div class="progress-indicator">
<span id="progress-text">Step 1 of 5</span>
<div class="progress-bar">
<div id="progress-fill"></div>
</div>
</div>
<iframe
id="application-iframe"
src="/workflow/customer-application?debug=false&hideComplete=true"
style="width: 100%; height: 600px; border: none;">
</iframe>
</div>
</main>
</div>
<script>
// Customer portal integration
const applicationSteps = [
'personal-info',
'employment-history',
'document-upload',
'references',
'final-submission'
];
let currentStep = 0;
window.addEventListener('message', (event) => {
if (event.data.type === 'workflow-complete') {
currentStep++;
updateProgress();
if (currentStep < applicationSteps.length) {
// Load next step
const iframe = document.getElementById('application-iframe');
iframe.src = `/workflow/${applicationSteps[currentStep]}?debug=false&hideComplete=true`;
} else {
// Application complete
showApplicationComplete();
}
}
});
function updateProgress() {
const progress = ((currentStep + 1) / applicationSteps.length) * 100;
document.getElementById('progress-fill').style.width = progress + '%';
document.getElementById('progress-text').textContent = `Step ${currentStep + 1} of ${applicationSteps.length}`;
}
function showApplicationComplete() {
document.getElementById('application-iframe').style.display = 'none';
document.querySelector('.application-section').innerHTML = `
<div class="completion-message">
<h2>🎉 Application Submitted!</h2>
<p>Your application has been successfully submitted. We'll review it and contact you soon.</p>
<button onclick="downloadApplication()">Download Application</button>
</div>
`;
}
</script>
```
### Employee Onboarding System
```html
<!-- Employee onboarding with multiple workflows -->
<div class="onboarding-system">
<div class="onboarding-header">
<h1>Employee Onboarding</h1>
<div class="step-indicator">
<div class="step active" data-step="1">Personal Info</div>
<div class="step" data-step="2">Employment</div>
<div class="step" data-step="3">Documents</div>
<div class="step" data-step="4">Benefits</div>
<div class="step" data-step="5">Final</div>
</div>
</div>
<div class="workflow-container">
<iframe
id="onboarding-iframe"
src="/workflow/personal-info?debug=false&hideComplete=true"
style="width: 100%; height: 700px; border: none;">
</iframe>
</div>
</div>
<script>
// Employee onboarding integration
const onboardingSteps = [
{ id: 'personal-info', name: 'Personal Information' },
{ id: 'employment', name: 'Employment Details' },
{ id: 'documents', name: 'Document Upload' },
{ id: 'benefits', name: 'Benefits Selection' },
{ id: 'final', name: 'Final Review' }
];
let currentStepIndex = 0;
window.addEventListener('message', (event) => {
if (event.data.type === 'workflow-complete') {
console.log(`Step ${event.data.processName} completed`);
if (event.data.hideCompletionMessage) {
// Auto-advance to next step
setTimeout(() => {
loadNextStep();
}, 200);
}
} else if (event.data.type === 'workflow-error') {
handleOnboardingError(event.data);
}
});
function loadNextStep() {
currentStepIndex++;
if (currentStepIndex >= onboardingSteps.length) {
// Onboarding complete
showOnboardingComplete();
return;
}
const step = onboardingSteps[currentStepIndex];
const iframe = document.getElementById('onboarding-iframe');
// Update iframe
iframe.src = `/workflow/${step.id}?debug=false&hideComplete=true`;
// Update step indicator
updateStepIndicator(currentStepIndex + 1);
}
function updateStepIndicator(activeStep) {
document.querySelectorAll('.step').forEach((step, index) => {
if (index + 1 <= activeStep) {
step.classList.add('active');
} else {
step.classList.remove('active');
}
});
}
function showOnboardingComplete() {
document.getElementById('onboarding-iframe').style.display = 'none';
document.querySelector('.workflow-container').innerHTML = `
<div class="completion-message">
<h2>🎉 Onboarding Complete!</h2>
<p>Welcome to the team! Your onboarding process has been completed successfully.</p>
<div class="next-steps">
<h3>Next Steps:</h3>
<ul>
<li>Check your email for login credentials</li>
<li>Complete your first-day orientation</li>
<li>Meet with your manager</li>
</ul>
</div>
</div>
`;
}
function handleOnboardingError(data) {
console.error('Onboarding error:', data);
// Show error message and retry options
showErrorModal(data.error);
}
</script>
```
## Troubleshooting
### Common Issues and Solutions
#### 1. Iframe Not Loading
**Problem**: Iframe shows blank or doesn't load
**Solution**: Check URL parameters and CORS settings
```javascript
// Debug iframe loading
const iframe = document.getElementById('workflow-iframe');
iframe.onload = () => {
console.log('Iframe loaded successfully');
};
iframe.onerror = () => {
console.error('Iframe failed to load');
};
```
#### 2. Messages Not Received
**Problem**: Parent iframe not receiving completion messages
**Solution**: Verify event listener and origin settings
```javascript
// Debug message reception
window.addEventListener('message', (event) => {
console.log('Message received:', event.data);
// Your message handling logic
});
```
#### 3. Cross-Origin Issues
**Problem**: CORS errors when loading iframe
**Solution**: Configure proper CORS headers on the workflow server
```nginx
# Nginx configuration example
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type";
```
#### 4. URL Parameter Issues
**Problem**: URL parameters not working as expected
**Solution**: Verify parameter encoding
```javascript
// Proper URL construction
const baseUrl = '/workflow/process-id';
const params = new URLSearchParams({
debug: 'false',
hideComplete: 'true'
});
const fullUrl = `${baseUrl}?${params.toString()}`;
```
### Debug Checklist
- [ ] Iframe source URL is correct
- [ ] URL parameters are properly encoded
- [ ] Event listener is attached to window
- [ ] Origin validation is configured
- [ ] CORS headers are set on server
- [ ] Console shows no JavaScript errors
- [ ] Network tab shows successful iframe load
### Support
For additional support with iframe integration:
1. Check the browser console for error messages
2. Verify network connectivity to the workflow server
3. Test with different browsers
4. Review server logs for any backend issues
5. Contact support with specific error details
---
This documentation provides comprehensive guidance for integrating Corrad workflows into external applications. The iframe system is designed to be flexible, secure, and easy to implement while providing a seamless user experience.

View File

@ -3973,7 +3973,7 @@ const sendToBack = () => {
}
/* Ensure all text elements in nodes inherit the custom text color */
:deep(.custom-node *:not(.icon)) {
:deep(.custom-node *:not(.icon):not([class^="validation-"])) {
color: inherit !important;
}

View File

@ -12,8 +12,6 @@ definePageMeta({
title: "Process Execution",
description: "Execute and run through a business process workflow",
layout: "empty",
middleware: ["auth"],
requiresAuth: true,
});
// Get route and router
@ -23,6 +21,16 @@ const router = useRouter();
// Get form builder store for ComponentPreview data sharing
const formStore = useFormBuilderStore();
// Check if we're in iframe/embed mode
const isIframeMode = computed(() => {
return route.query.debug === 'false';
});
// Check if we should hide completion message (for seamless multi-process workflows)
const hideCompletionMessage = computed(() => {
return route.query.hideComplete === 'true';
});
// State
const loading = ref(true);
const process = ref(null);
@ -68,6 +76,13 @@ const isProcessComplete = computed(() => {
return currentNode.value?.type === 'end' || currentStep.value >= (workflowData.value?.nodes?.length || 0);
});
// Watch for process completion and update URL
watch(isProcessComplete, (newValue) => {
if (newValue) {
updateUrlOnComplete();
}
});
// Load process data
const loadProcess = async () => {
try {
@ -96,10 +111,12 @@ const loadProcess = async () => {
} else {
error.value = response.message || 'Failed to load process';
notifyParentOfError(error.value);
}
} catch (err) {
console.error('[Workflow] Error loading process:', err);
error.value = 'Failed to load process data';
notifyParentOfError(error.value);
} finally {
loading.value = false;
}
@ -128,6 +145,7 @@ const startProcessExecution = async () => {
} catch (err) {
console.error('[Workflow] Error starting process execution:', err);
error.value = 'Failed to start process execution';
notifyParentOfError(error.value);
}
};
@ -637,6 +655,7 @@ const executeCurrentStep = async () => {
}
} else {
error.value = 'API call failed: ' + (err.message || err);
notifyParentOfError(error.value);
}
}
} else {
@ -675,6 +694,7 @@ const executeCurrentStep = async () => {
}
} else {
error.value = 'API call failed: ' + (err.message || err);
notifyParentOfError(error.value);
}
}
}
@ -690,6 +710,7 @@ const executeCurrentStep = async () => {
} catch (err) {
console.error('[Workflow] Error executing script node:', err);
error.value = 'Script execution failed: ' + (err.message || err);
notifyParentOfError(error.value);
}
}
// Only auto-progress if there's a single outgoing edge
@ -811,6 +832,7 @@ const executeCurrentStep = async () => {
}
} else {
error.value = 'Notification failed: ' + (err.message || err);
notifyParentOfError(error.value);
}
}
} else if (['decision', 'gateway'].includes(currentNode.value?.type)) {
@ -823,6 +845,7 @@ const executeCurrentStep = async () => {
} catch (err) {
console.error('[Workflow] Error executing step:', err);
error.value = 'Failed to execute step';
notifyParentOfError(error.value);
} finally {
stepLoading.value = false;
}
@ -893,6 +916,139 @@ const goHome = () => {
router.push('/');
};
// Update URL when process completes
const updateUrlOnComplete = () => {
if (isIframeMode.value) {
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set('complete', 'true');
// Use replaceState to avoid adding to browser history
window.history.replaceState({}, '', currentUrl.toString());
console.log('[Workflow] URL updated with complete=true');
// Also notify parent iframe if we're in an iframe
try {
if (window.parent !== window) {
window.parent.postMessage({
type: 'workflow-complete',
processId: processId.value,
processName: process.value?.processName,
hideCompletionMessage: hideCompletionMessage.value,
timestamp: new Date().toISOString()
}, '*');
console.log('[Workflow] Message sent to parent iframe');
}
} catch (error) {
console.log('[Workflow] Could not send message to parent:', error);
}
}
};
// Notify parent iframe of errors
const notifyParentOfError = (errorMessage) => {
if (isIframeMode.value) {
try {
if (window.parent !== window) {
window.parent.postMessage({
type: 'workflow-error',
processId: processId.value,
processName: process.value?.processName,
error: errorMessage,
timestamp: new Date().toISOString()
}, '*');
console.log('[Workflow] Error message sent to parent iframe');
}
} catch (error) {
console.log('[Workflow] Could not send error message to parent:', error);
}
}
};
/*
* PARENT IFRAME INTEGRATION GUIDE:
*
* The workflow will send messages to the parent iframe when in iframe mode (?debug=false):
*
* 1. Process Completion:
* - URL will be updated with ?complete=true
* - Message sent: { type: 'workflow-complete', processId, processName, hideCompletionMessage, timestamp }
*
* 2. Process Error:
* - Message sent: { type: 'workflow-error', processId, processName, error, timestamp }
*
* URL Parameters:
* - ?debug=false: Enable iframe mode (hide UI chrome)
* - ?hideComplete=true: Hide completion message (for seamless multi-process workflows)
*
* Example parent iframe listener:
*
* window.addEventListener('message', (event) => {
* if (event.data.type === 'workflow-complete') {
* console.log('Workflow completed:', event.data);
* // Handle completion - redirect to next process if needed
* if (event.data.hideCompletionMessage) {
* // User won't see completion message, redirect immediately
* redirectToNextProcess();
* }
* } else if (event.data.type === 'workflow-error') {
* console.log('Workflow error:', event.data);
* // Handle error
* }
* });
*
* To check URL for completion:
* const urlParams = new URLSearchParams(window.location.search);
* if (urlParams.get('complete') === 'true') {
* // Process is complete
* }
*
* MULTI-PROCESS WORKFLOW EXAMPLE:
*
* // Parent system with multiple processes
* const processes = [
* { id: 'process1', name: 'Personal Information' },
* { id: 'process2', name: 'Employment Details' },
* { id: 'process3', name: 'Document Upload' },
* { id: 'process4', name: 'Final Review' }
* ];
*
* let currentProcessIndex = 0;
*
* function loadProcess(processIndex) {
* if (processIndex >= processes.length) {
* // All processes complete
* showFinalSuccess();
* return;
* }
*
* const process = processes[processIndex];
* const iframe = document.getElementById('workflow-iframe');
*
* // Load process with hidden completion message
* iframe.src = `/workflow/${process.id}?debug=false&hideComplete=true`;
* currentProcessIndex = processIndex;
* }
*
* // Listen for completion and auto-advance
* window.addEventListener('message', (event) => {
* if (event.data.type === 'workflow-complete') {
* console.log(`Process ${event.data.processName} completed`);
*
* if (event.data.hideCompletionMessage) {
* // Seamlessly move to next process
* setTimeout(() => {
* loadProcess(currentProcessIndex + 1);
* }, 100); // Small delay for smooth transition
* }
* } else if (event.data.type === 'workflow-error') {
* console.error('Process failed:', event.data.error);
* showErrorMessage(event.data.error);
* }
* });
*
* // Start the first process
* loadProcess(0);
*/
// Load process on mount
onMounted(() => {
loadProcess();
@ -1219,9 +1375,9 @@ function getConditionGroupResult(conditionGroup, variables) {
</script>
<template>
<div class="min-h-screen bg-gray-50">
<!-- Header -->
<header class="bg-white border-b border-gray-200 px-6 py-4 shadow-sm">
<div :class="isIframeMode ? 'min-h-screen bg-white' : 'min-h-screen bg-gray-50'">
<!-- Header - Hidden in iframe mode -->
<header v-if="!isIframeMode" class="bg-white border-b border-gray-200 px-6 py-4 shadow-sm">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<Icon
@ -1258,7 +1414,7 @@ function getConditionGroupResult(conditionGroup, variables) {
</header>
<!-- Main Content -->
<div class="container mx-auto px-6 py-8 max-w-[1200px]">
<div :class="isIframeMode ? 'p-4' : 'container mx-auto px-6 py-8 max-w-[1200px]'">
<!-- Loading State -->
<div v-if="loading" class="flex justify-center items-center py-12">
<div class="text-center">
@ -1268,24 +1424,24 @@ function getConditionGroupResult(conditionGroup, variables) {
</div>
<!-- Error State -->
<div v-else-if="error" class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
<div v-else-if="error" :class="isIframeMode ? 'p-6 text-center' : 'bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center'">
<Icon name="material-symbols:error-outline" class="w-16 h-16 text-red-400 mx-auto mb-4" />
<h3 class="text-lg font-medium text-gray-900 mb-2">Error</h3>
<p class="text-gray-600 mb-6">{{ error }}</p>
<RsButton @click="goHome" variant="primary">
<RsButton v-if="!isIframeMode" @click="goHome" variant="primary">
<Icon name="material-symbols:home" class="mr-2" />
Go Home
</RsButton>
</div>
<!-- Process Complete -->
<div v-else-if="isProcessComplete" class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
<!-- Process Complete - Hidden when hideComplete=true -->
<div v-else-if="isProcessComplete && !hideCompletionMessage" :class="isIframeMode ? 'p-6 text-center' : 'bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center'">
<Icon name="material-symbols:check-circle" class="w-16 h-16 text-green-500 mx-auto mb-4" />
<h2 class="text-2xl font-bold text-gray-900 mb-2">Process Complete!</h2>
<h2 :class="isIframeMode ? 'text-xl font-bold text-gray-900 mb-2' : 'text-2xl font-bold text-gray-900 mb-2'">Process Complete!</h2>
<p class="text-gray-600 mb-6">
The workflow "{{ process.processName }}" has been completed successfully.
</p>
<div class="flex justify-center gap-3">
<div v-if="!isIframeMode" class="flex justify-center gap-3">
<RsButton @click="loadProcess" variant="secondary">
<Icon name="material-symbols:refresh" class="mr-2" />
Run Again
@ -1297,10 +1453,18 @@ function getConditionGroupResult(conditionGroup, variables) {
</div>
</div>
<!-- Process Complete but Hidden - Show minimal loading state -->
<div v-else-if="isProcessComplete && hideCompletionMessage" class="flex justify-center items-center py-8">
<div class="text-center">
<Icon name="material-symbols:progress-activity" class="w-6 h-6 animate-spin text-blue-500 mx-auto mb-2" />
<p class="text-gray-500 text-sm">Processing...</p>
</div>
</div>
<!-- Current Step -->
<div v-else-if="currentNode" class="space-y-6">
<!-- Step Info -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div v-else-if="currentNode" :class="isIframeMode ? 'space-y-2' : 'space-y-6'">
<!-- Step Info - Hidden in iframe mode -->
<div v-if="!isIframeMode" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div class="flex items-center gap-4 mb-4">
<div :class="[
'p-3 rounded-lg',
@ -1323,8 +1487,8 @@ function getConditionGroupResult(conditionGroup, variables) {
</div>
<!-- Form Step -->
<div v-if="currentNode.type === 'form'" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div class="mb-4">
<div v-if="currentNode.type === 'form'" :class="isIframeMode ? 'p-2' : 'bg-white rounded-xl shadow-sm border border-gray-200 p-6'">
<div v-if="!isIframeMode" class="mb-4">
<h3 class="text-lg font-semibold text-gray-900 mb-2">
{{ currentNode.data?.formName || currentNode.data?.label || 'Please fill out the form' }}
</h3>
@ -1382,21 +1546,23 @@ function getConditionGroupResult(conditionGroup, variables) {
:field-states="fieldStates"
/>
</template>
<div class="flex justify-end pt-6 border-t border-gray-200">
<!-- Place submit button in a full-width row at the end of the grid -->
<div class="col-span-12 flex justify-start pt-6 border-t border-gray-200">
<RsButton
@click="validateAndSubmit"
:disabled="stepLoading"
variant="primary"
class="col-span-12"
>
<Icon v-if="stepLoading" name="material-symbols:progress-activity" class="w-4 h-4 animate-spin mr-2" />
{{ stepLoading ? 'Processing...' : 'Submit & Continue' }}
{{ stepLoading ? 'Processing...' : 'Submit' }}
</RsButton>
</div>
</div>
</FormKit>
<!-- Script Info (for debugging) -->
<div v-if="combinedScript" class="mt-4 p-3 bg-gray-50 rounded-lg border">
<!-- Script Info (for debugging) - Hidden in iframe mode -->
<div v-if="combinedScript && !isIframeMode" class="mt-4 p-3 bg-gray-50 rounded-lg border">
<details>
<summary class="text-sm font-medium text-gray-700 cursor-pointer">Form Logic Available (Development)</summary>
<div class="mt-2 space-y-2">
@ -1425,10 +1591,11 @@ function getConditionGroupResult(conditionGroup, variables) {
</div>
</div>
<!-- API/Script/Notification Step -->
<div v-else-if="['api', 'script', 'notification'].includes(currentNode.type)" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<!-- Processing State -->
<div v-if="stepLoading" class="text-center py-8">
<!-- API/Script/Notification Step - Hidden in iframe mode unless multiple paths -->
<div v-else-if="['api', 'script', 'notification'].includes(currentNode.type) && (!isIframeMode || !canAutoProgress(currentNode))"
:class="isIframeMode ? 'p-2' : 'bg-white rounded-xl shadow-sm border border-gray-200 p-6'">
<!-- Processing State - Hidden in iframe mode -->
<div v-if="stepLoading && !isIframeMode" class="text-center py-8">
<Icon name="material-symbols:progress-activity" class="w-8 h-8 animate-spin text-blue-500 mx-auto mb-4" />
<h3 class="text-lg font-semibold text-gray-900 mb-2">
{{
@ -1442,7 +1609,7 @@ function getConditionGroupResult(conditionGroup, variables) {
<!-- Completed State with Multiple Paths -->
<div v-else-if="!canAutoProgress(currentNode)" class="space-y-4">
<div class="text-center">
<div v-if="!isIframeMode" class="text-center">
<Icon name="material-symbols:check-circle" class="w-8 h-8 text-green-500 mx-auto mb-4" />
<h3 class="text-lg font-semibold text-gray-900 mb-2">
{{
@ -1455,16 +1622,16 @@ function getConditionGroupResult(conditionGroup, variables) {
</div>
<!-- Multiple Path Options -->
<div class="border-t border-gray-200 pt-4">
<h4 class="font-medium text-gray-700 mb-3">Choose your next step:</h4>
<div :class="isIframeMode ? '' : 'border-t border-gray-200 pt-4'">
<h4 v-if="!isIframeMode" class="font-medium text-gray-700 mb-3">Choose your next step:</h4>
<div class="grid gap-2">
<template v-for="edge in getOutgoingEdges(currentNode.id)" :key="edge.id">
<RsButton
@click="makeDecision(edge.target)"
variant="outline"
class="justify-between p-3"
:class="isIframeMode ? 'justify-between p-2' : 'justify-between p-3'"
>
<span>Continue to: {{ getNodeLabel(edge.target) }}</span>
<span>{{ isIframeMode ? getNodeLabel(edge.target) : `Continue to: ${getNodeLabel(edge.target)}` }}</span>
<Icon name="material-symbols:arrow-right-alt" />
</RsButton>
</template>
@ -1474,8 +1641,8 @@ function getConditionGroupResult(conditionGroup, variables) {
</div>
<!-- HTML Content Step -->
<div v-else-if="currentNode.type === 'html'" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div class="mb-4">
<div v-else-if="currentNode.type === 'html'" :class="isIframeMode ? 'p-2' : 'bg-white rounded-xl shadow-sm border border-gray-200 p-6'">
<div v-if="!isIframeMode" class="mb-4">
<h3 class="text-lg font-semibold text-gray-900 mb-2">
{{ currentNode.data?.title || currentNode.data?.label || 'Content Display' }}
</h3>
@ -1485,30 +1652,31 @@ function getConditionGroupResult(conditionGroup, variables) {
</div>
<!-- HTML Content -->
<div class="html-content-container border border-gray-200 rounded-lg p-4 mb-6">
<div :class="isIframeMode ? 'mb-4' : 'html-content-container border border-gray-200 rounded-lg p-4 mb-6'">
<div
v-if="interpolatedHtmlContent"
v-html="interpolatedHtmlContent"
class="prose max-w-none"
></div>
<div v-else class="text-center text-gray-500 py-8">
<div v-else-if="!isIframeMode" class="text-center text-gray-500 py-8">
<Icon name="material-symbols:code-blocks" class="w-12 h-12 mx-auto mb-2" />
<p>No HTML content provided</p>
</div>
</div>
<!-- Continue Options -->
<div class="pt-4 border-t border-gray-200">
<div :class="isIframeMode ? 'pt-2' : 'pt-4 border-t border-gray-200'">
<!-- Single Path - Simple Continue Button -->
<div v-if="canAutoProgress(currentNode)" class="flex justify-end">
<div v-if="canAutoProgress(currentNode)" :class="isIframeMode ? 'flex justify-center' : 'flex justify-end'">
<RsButton
@click="moveToNextStep"
:disabled="stepLoading"
variant="primary"
:class="isIframeMode ? 'px-4 py-2' : ''"
>
<Icon v-if="stepLoading" name="material-symbols:progress-activity" class="w-4 h-4 animate-spin mr-2" />
<template v-if="!stepLoading">
Continue to: {{ getNextNodeObject(currentNode)?.data?.label || getNextNodeObject(currentNode)?.label || 'Next Step' }}
{{ isIframeMode ? 'Continue' : `Continue to: ${getNextNodeObject(currentNode)?.data?.label || getNextNodeObject(currentNode)?.label || 'Next Step'}` }}
</template>
<template v-else>
Processing...
@ -1518,15 +1686,15 @@ function getConditionGroupResult(conditionGroup, variables) {
<!-- Multiple Paths - Show Options -->
<div v-else class="space-y-3">
<h4 class="font-medium text-gray-700">Choose your next step:</h4>
<h4 v-if="!isIframeMode" class="font-medium text-gray-700">Choose your next step:</h4>
<div class="grid gap-2">
<template v-for="edge in getOutgoingEdges(currentNode.id)" :key="edge.id">
<RsButton
@click="makeDecision(edge.target)"
variant="outline"
class="justify-between p-3"
:class="isIframeMode ? 'justify-between p-2' : 'justify-between p-3'"
>
<span>Continue to: {{ getNodeLabel(edge.target) }}</span>
<span>{{ isIframeMode ? getNodeLabel(edge.target) : `Continue to: ${getNodeLabel(edge.target)}` }}</span>
<Icon name="material-symbols:arrow-right-alt" />
</RsButton>
</template>
@ -1536,8 +1704,8 @@ function getConditionGroupResult(conditionGroup, variables) {
</div>
<!-- Decision/Gateway Step -->
<div v-else-if="['decision', 'gateway'].includes(currentNode.type)" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div class="mb-4">
<div v-else-if="['decision', 'gateway'].includes(currentNode.type)" :class="isIframeMode ? 'p-2' : 'bg-white rounded-xl shadow-sm border border-gray-200 p-6'">
<div v-if="!isIframeMode" class="mb-4">
<h3 class="text-lg font-semibold text-gray-900 mb-2">
{{ currentNode.data?.title || currentNode.data?.label || 'Make a Decision' }}
</h3>
@ -1548,17 +1716,17 @@ function getConditionGroupResult(conditionGroup, variables) {
<!-- Manual Decision Options -->
<div v-if="currentNode.data?.executionType === 'manual'" class="space-y-4">
<p class="text-gray-700 mb-4">Please choose one of the following options:</p>
<p v-if="!isIframeMode" class="text-gray-700 mb-4">Please choose one of the following options:</p>
<div class="grid gap-3">
<template v-for="edge in workflowData.edges.filter(e => e.source === currentNode.id)" :key="edge.id">
<RsButton
@click="makeDecision(edge.target)"
variant="outline-primary"
class="justify-start p-4 h-auto"
:class="isIframeMode ? 'justify-start p-2 h-auto' : 'justify-start p-4 h-auto'"
>
<div class="text-left">
<div class="font-medium">{{ edge.label || 'Option' }}</div>
<div v-if="edge.data?.description" class="text-sm text-gray-500 mt-1">
<div :class="isIframeMode ? 'font-medium text-sm' : 'font-medium'">{{ edge.label || 'Option' }}</div>
<div v-if="edge.data?.description && !isIframeMode" class="text-sm text-gray-500 mt-1">
{{ edge.data.description }}
</div>
</div>
@ -1568,8 +1736,8 @@ function getConditionGroupResult(conditionGroup, variables) {
</div>
</div>
<!-- Automatic Decision Processing -->
<div v-else class="text-center py-8">
<!-- Automatic Decision Processing - Hidden in iframe mode -->
<div v-else-if="!isIframeMode" class="text-center py-8">
<Icon name="material-symbols:progress-activity" class="w-8 h-8 animate-spin text-yellow-500 mx-auto mb-4" />
<h4 class="text-lg font-medium text-gray-900 mb-2">Evaluating Conditions</h4>
<p class="text-gray-600">Please wait while we determine the next step...</p>
@ -1623,8 +1791,8 @@ function getConditionGroupResult(conditionGroup, variables) {
</RsButton>
</div>
<!-- Variable Mapping Debug (always visible for any node) -->
<div v-if="currentNode" class="bg-gray-100 rounded-lg p-4">
<!-- Variable Mapping Debug (hidden in iframe mode) -->
<div v-if="currentNode && !isIframeMode" class="bg-gray-100 rounded-lg p-4">
<details>
<summary class="font-medium text-gray-700 cursor-pointer mb-2">Variable Mapping Debug</summary>
<div class="space-y-3 text-xs">
@ -1662,8 +1830,8 @@ function getConditionGroupResult(conditionGroup, variables) {
</details>
</div>
<!-- Process Variables Debug (only in development) -->
<div v-if="Object.keys(processVariables || {}).length > 0" class="bg-gray-100 rounded-lg p-4 mt-4">
<!-- Process Variables Debug (hidden in iframe mode) -->
<div v-if="Object.keys(processVariables || {}).length > 0 && !isIframeMode" class="bg-gray-100 rounded-lg p-4 mt-4">
<details>
<summary class="font-medium text-gray-700 cursor-pointer mb-2">Process Variables (Debug)</summary>
<pre class="text-xs text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(processVariables, null, 2) }}</pre>