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:
parent
1448aef0ed
commit
e4b1c7e444
725
content/documentation/08-iframe-integration.md
Normal file
725
content/documentation/08-iframe-integration.md
Normal 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.
|
@ -3973,7 +3973,7 @@ const sendToBack = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure all text elements in nodes inherit the custom text color */
|
/* 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;
|
color: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,8 +12,6 @@ definePageMeta({
|
|||||||
title: "Process Execution",
|
title: "Process Execution",
|
||||||
description: "Execute and run through a business process workflow",
|
description: "Execute and run through a business process workflow",
|
||||||
layout: "empty",
|
layout: "empty",
|
||||||
middleware: ["auth"],
|
|
||||||
requiresAuth: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get route and router
|
// Get route and router
|
||||||
@ -23,6 +21,16 @@ const router = useRouter();
|
|||||||
// Get form builder store for ComponentPreview data sharing
|
// Get form builder store for ComponentPreview data sharing
|
||||||
const formStore = useFormBuilderStore();
|
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
|
// State
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const process = ref(null);
|
const process = ref(null);
|
||||||
@ -68,6 +76,13 @@ const isProcessComplete = computed(() => {
|
|||||||
return currentNode.value?.type === 'end' || currentStep.value >= (workflowData.value?.nodes?.length || 0);
|
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
|
// Load process data
|
||||||
const loadProcess = async () => {
|
const loadProcess = async () => {
|
||||||
try {
|
try {
|
||||||
@ -96,10 +111,12 @@ const loadProcess = async () => {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
error.value = response.message || 'Failed to load process';
|
error.value = response.message || 'Failed to load process';
|
||||||
|
notifyParentOfError(error.value);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Workflow] Error loading process:', err);
|
console.error('[Workflow] Error loading process:', err);
|
||||||
error.value = 'Failed to load process data';
|
error.value = 'Failed to load process data';
|
||||||
|
notifyParentOfError(error.value);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@ -128,6 +145,7 @@ const startProcessExecution = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Workflow] Error starting process execution:', err);
|
console.error('[Workflow] Error starting process execution:', err);
|
||||||
error.value = 'Failed to start process execution';
|
error.value = 'Failed to start process execution';
|
||||||
|
notifyParentOfError(error.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -637,6 +655,7 @@ const executeCurrentStep = async () => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error.value = 'API call failed: ' + (err.message || err);
|
error.value = 'API call failed: ' + (err.message || err);
|
||||||
|
notifyParentOfError(error.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -675,6 +694,7 @@ const executeCurrentStep = async () => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error.value = 'API call failed: ' + (err.message || err);
|
error.value = 'API call failed: ' + (err.message || err);
|
||||||
|
notifyParentOfError(error.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -690,6 +710,7 @@ const executeCurrentStep = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Workflow] Error executing script node:', err);
|
console.error('[Workflow] Error executing script node:', err);
|
||||||
error.value = 'Script execution failed: ' + (err.message || err);
|
error.value = 'Script execution failed: ' + (err.message || err);
|
||||||
|
notifyParentOfError(error.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Only auto-progress if there's a single outgoing edge
|
// Only auto-progress if there's a single outgoing edge
|
||||||
@ -811,6 +832,7 @@ const executeCurrentStep = async () => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error.value = 'Notification failed: ' + (err.message || err);
|
error.value = 'Notification failed: ' + (err.message || err);
|
||||||
|
notifyParentOfError(error.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (['decision', 'gateway'].includes(currentNode.value?.type)) {
|
} else if (['decision', 'gateway'].includes(currentNode.value?.type)) {
|
||||||
@ -823,6 +845,7 @@ const executeCurrentStep = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Workflow] Error executing step:', err);
|
console.error('[Workflow] Error executing step:', err);
|
||||||
error.value = 'Failed to execute step';
|
error.value = 'Failed to execute step';
|
||||||
|
notifyParentOfError(error.value);
|
||||||
} finally {
|
} finally {
|
||||||
stepLoading.value = false;
|
stepLoading.value = false;
|
||||||
}
|
}
|
||||||
@ -893,6 +916,139 @@ const goHome = () => {
|
|||||||
router.push('/');
|
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
|
// Load process on mount
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadProcess();
|
loadProcess();
|
||||||
@ -1219,9 +1375,9 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gray-50">
|
<div :class="isIframeMode ? 'min-h-screen bg-white' : 'min-h-screen bg-gray-50'">
|
||||||
<!-- Header -->
|
<!-- Header - Hidden in iframe mode -->
|
||||||
<header class="bg-white border-b border-gray-200 px-6 py-4 shadow-sm">
|
<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 justify-between">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<Icon
|
<Icon
|
||||||
@ -1258,7 +1414,7 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- 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 -->
|
<!-- Loading State -->
|
||||||
<div v-if="loading" class="flex justify-center items-center py-12">
|
<div v-if="loading" class="flex justify-center items-center py-12">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
@ -1268,24 +1424,24 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error State -->
|
<!-- 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" />
|
<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>
|
<h3 class="text-lg font-medium text-gray-900 mb-2">Error</h3>
|
||||||
<p class="text-gray-600 mb-6">{{ error }}</p>
|
<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" />
|
<Icon name="material-symbols:home" class="mr-2" />
|
||||||
Go Home
|
Go Home
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Process Complete -->
|
<!-- Process Complete - Hidden when hideComplete=true -->
|
||||||
<div v-else-if="isProcessComplete" class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
<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" />
|
<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">
|
<p class="text-gray-600 mb-6">
|
||||||
The workflow "{{ process.processName }}" has been completed successfully.
|
The workflow "{{ process.processName }}" has been completed successfully.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex justify-center gap-3">
|
<div v-if="!isIframeMode" class="flex justify-center gap-3">
|
||||||
<RsButton @click="loadProcess" variant="secondary">
|
<RsButton @click="loadProcess" variant="secondary">
|
||||||
<Icon name="material-symbols:refresh" class="mr-2" />
|
<Icon name="material-symbols:refresh" class="mr-2" />
|
||||||
Run Again
|
Run Again
|
||||||
@ -1297,10 +1453,18 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Current Step -->
|
||||||
<div v-else-if="currentNode" class="space-y-6">
|
<div v-else-if="currentNode" :class="isIframeMode ? 'space-y-2' : 'space-y-6'">
|
||||||
<!-- Step Info -->
|
<!-- Step Info - Hidden in iframe mode -->
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
<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="flex items-center gap-4 mb-4">
|
||||||
<div :class="[
|
<div :class="[
|
||||||
'p-3 rounded-lg',
|
'p-3 rounded-lg',
|
||||||
@ -1323,8 +1487,8 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form Step -->
|
<!-- Form Step -->
|
||||||
<div v-if="currentNode.type === 'form'" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
<div v-if="currentNode.type === 'form'" :class="isIframeMode ? 'p-2' : 'bg-white rounded-xl shadow-sm border border-gray-200 p-6'">
|
||||||
<div class="mb-4">
|
<div v-if="!isIframeMode" class="mb-4">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">
|
||||||
{{ currentNode.data?.formName || currentNode.data?.label || 'Please fill out the form' }}
|
{{ currentNode.data?.formName || currentNode.data?.label || 'Please fill out the form' }}
|
||||||
</h3>
|
</h3>
|
||||||
@ -1382,21 +1546,23 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
:field-states="fieldStates"
|
:field-states="fieldStates"
|
||||||
/>
|
/>
|
||||||
</template>
|
</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
|
<RsButton
|
||||||
@click="validateAndSubmit"
|
@click="validateAndSubmit"
|
||||||
:disabled="stepLoading"
|
:disabled="stepLoading"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
class="col-span-12"
|
||||||
>
|
>
|
||||||
<Icon v-if="stepLoading" name="material-symbols:progress-activity" class="w-4 h-4 animate-spin mr-2" />
|
<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>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormKit>
|
</FormKit>
|
||||||
|
|
||||||
<!-- Script Info (for debugging) -->
|
<!-- Script Info (for debugging) - Hidden in iframe mode -->
|
||||||
<div v-if="combinedScript" class="mt-4 p-3 bg-gray-50 rounded-lg border">
|
<div v-if="combinedScript && !isIframeMode" class="mt-4 p-3 bg-gray-50 rounded-lg border">
|
||||||
<details>
|
<details>
|
||||||
<summary class="text-sm font-medium text-gray-700 cursor-pointer">Form Logic Available (Development)</summary>
|
<summary class="text-sm font-medium text-gray-700 cursor-pointer">Form Logic Available (Development)</summary>
|
||||||
<div class="mt-2 space-y-2">
|
<div class="mt-2 space-y-2">
|
||||||
@ -1425,10 +1591,11 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API/Script/Notification Step -->
|
<!-- API/Script/Notification Step - Hidden in iframe mode unless multiple paths -->
|
||||||
<div v-else-if="['api', 'script', 'notification'].includes(currentNode.type)" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
<div v-else-if="['api', 'script', 'notification'].includes(currentNode.type) && (!isIframeMode || !canAutoProgress(currentNode))"
|
||||||
<!-- Processing State -->
|
:class="isIframeMode ? 'p-2' : 'bg-white rounded-xl shadow-sm border border-gray-200 p-6'">
|
||||||
<div v-if="stepLoading" class="text-center py-8">
|
<!-- 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" />
|
<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">
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">
|
||||||
{{
|
{{
|
||||||
@ -1442,7 +1609,7 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
|
|
||||||
<!-- Completed State with Multiple Paths -->
|
<!-- Completed State with Multiple Paths -->
|
||||||
<div v-else-if="!canAutoProgress(currentNode)" class="space-y-4">
|
<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" />
|
<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">
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">
|
||||||
{{
|
{{
|
||||||
@ -1455,16 +1622,16 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Multiple Path Options -->
|
<!-- Multiple Path Options -->
|
||||||
<div class="border-t border-gray-200 pt-4">
|
<div :class="isIframeMode ? '' : 'border-t border-gray-200 pt-4'">
|
||||||
<h4 class="font-medium text-gray-700 mb-3">Choose your next step:</h4>
|
<h4 v-if="!isIframeMode" class="font-medium text-gray-700 mb-3">Choose your next step:</h4>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<template v-for="edge in getOutgoingEdges(currentNode.id)" :key="edge.id">
|
<template v-for="edge in getOutgoingEdges(currentNode.id)" :key="edge.id">
|
||||||
<RsButton
|
<RsButton
|
||||||
@click="makeDecision(edge.target)"
|
@click="makeDecision(edge.target)"
|
||||||
variant="outline"
|
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" />
|
<Icon name="material-symbols:arrow-right-alt" />
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</template>
|
</template>
|
||||||
@ -1474,8 +1641,8 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- HTML Content Step -->
|
<!-- HTML Content Step -->
|
||||||
<div v-else-if="currentNode.type === 'html'" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
<div v-else-if="currentNode.type === 'html'" :class="isIframeMode ? 'p-2' : 'bg-white rounded-xl shadow-sm border border-gray-200 p-6'">
|
||||||
<div class="mb-4">
|
<div v-if="!isIframeMode" class="mb-4">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">
|
||||||
{{ currentNode.data?.title || currentNode.data?.label || 'Content Display' }}
|
{{ currentNode.data?.title || currentNode.data?.label || 'Content Display' }}
|
||||||
</h3>
|
</h3>
|
||||||
@ -1485,30 +1652,31 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- HTML Content -->
|
<!-- 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
|
<div
|
||||||
v-if="interpolatedHtmlContent"
|
v-if="interpolatedHtmlContent"
|
||||||
v-html="interpolatedHtmlContent"
|
v-html="interpolatedHtmlContent"
|
||||||
class="prose max-w-none"
|
class="prose max-w-none"
|
||||||
></div>
|
></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" />
|
<Icon name="material-symbols:code-blocks" class="w-12 h-12 mx-auto mb-2" />
|
||||||
<p>No HTML content provided</p>
|
<p>No HTML content provided</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Continue Options -->
|
<!-- 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 -->
|
<!-- 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
|
<RsButton
|
||||||
@click="moveToNextStep"
|
@click="moveToNextStep"
|
||||||
:disabled="stepLoading"
|
:disabled="stepLoading"
|
||||||
variant="primary"
|
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" />
|
<Icon v-if="stepLoading" name="material-symbols:progress-activity" class="w-4 h-4 animate-spin mr-2" />
|
||||||
<template v-if="!stepLoading">
|
<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>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
Processing...
|
Processing...
|
||||||
@ -1518,15 +1686,15 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
|
|
||||||
<!-- Multiple Paths - Show Options -->
|
<!-- Multiple Paths - Show Options -->
|
||||||
<div v-else class="space-y-3">
|
<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">
|
<div class="grid gap-2">
|
||||||
<template v-for="edge in getOutgoingEdges(currentNode.id)" :key="edge.id">
|
<template v-for="edge in getOutgoingEdges(currentNode.id)" :key="edge.id">
|
||||||
<RsButton
|
<RsButton
|
||||||
@click="makeDecision(edge.target)"
|
@click="makeDecision(edge.target)"
|
||||||
variant="outline"
|
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" />
|
<Icon name="material-symbols:arrow-right-alt" />
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</template>
|
</template>
|
||||||
@ -1536,8 +1704,8 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Decision/Gateway Step -->
|
<!-- 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 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 class="mb-4">
|
<div v-if="!isIframeMode" class="mb-4">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">
|
||||||
{{ currentNode.data?.title || currentNode.data?.label || 'Make a Decision' }}
|
{{ currentNode.data?.title || currentNode.data?.label || 'Make a Decision' }}
|
||||||
</h3>
|
</h3>
|
||||||
@ -1548,17 +1716,17 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
|
|
||||||
<!-- Manual Decision Options -->
|
<!-- Manual Decision Options -->
|
||||||
<div v-if="currentNode.data?.executionType === 'manual'" class="space-y-4">
|
<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">
|
<div class="grid gap-3">
|
||||||
<template v-for="edge in workflowData.edges.filter(e => e.source === currentNode.id)" :key="edge.id">
|
<template v-for="edge in workflowData.edges.filter(e => e.source === currentNode.id)" :key="edge.id">
|
||||||
<RsButton
|
<RsButton
|
||||||
@click="makeDecision(edge.target)"
|
@click="makeDecision(edge.target)"
|
||||||
variant="outline-primary"
|
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="text-left">
|
||||||
<div class="font-medium">{{ edge.label || 'Option' }}</div>
|
<div :class="isIframeMode ? 'font-medium text-sm' : 'font-medium'">{{ edge.label || 'Option' }}</div>
|
||||||
<div v-if="edge.data?.description" class="text-sm text-gray-500 mt-1">
|
<div v-if="edge.data?.description && !isIframeMode" class="text-sm text-gray-500 mt-1">
|
||||||
{{ edge.data.description }}
|
{{ edge.data.description }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1568,8 +1736,8 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Automatic Decision Processing -->
|
<!-- Automatic Decision Processing - Hidden in iframe mode -->
|
||||||
<div v-else class="text-center py-8">
|
<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" />
|
<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>
|
<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>
|
<p class="text-gray-600">Please wait while we determine the next step...</p>
|
||||||
@ -1623,8 +1791,8 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Variable Mapping Debug (always visible for any node) -->
|
<!-- Variable Mapping Debug (hidden in iframe mode) -->
|
||||||
<div v-if="currentNode" class="bg-gray-100 rounded-lg p-4">
|
<div v-if="currentNode && !isIframeMode" class="bg-gray-100 rounded-lg p-4">
|
||||||
<details>
|
<details>
|
||||||
<summary class="font-medium text-gray-700 cursor-pointer mb-2">Variable Mapping Debug</summary>
|
<summary class="font-medium text-gray-700 cursor-pointer mb-2">Variable Mapping Debug</summary>
|
||||||
<div class="space-y-3 text-xs">
|
<div class="space-y-3 text-xs">
|
||||||
@ -1662,8 +1830,8 @@ function getConditionGroupResult(conditionGroup, variables) {
|
|||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Process Variables Debug (only in development) -->
|
<!-- Process Variables Debug (hidden in iframe mode) -->
|
||||||
<div v-if="Object.keys(processVariables || {}).length > 0" class="bg-gray-100 rounded-lg p-4 mt-4">
|
<div v-if="Object.keys(processVariables || {}).length > 0 && !isIframeMode" class="bg-gray-100 rounded-lg p-4 mt-4">
|
||||||
<details>
|
<details>
|
||||||
<summary class="font-medium text-gray-700 cursor-pointer mb-2">Process Variables (Debug)</summary>
|
<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>
|
<pre class="text-xs text-gray-600 bg-white p-2 rounded border overflow-auto">{{ JSON.stringify(processVariables, null, 2) }}</pre>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user