Enhance Workflow Execution with Notification and Decision Node Features
- Added support for notification nodes in the workflow execution process, allowing users to send notifications with customizable messages and recipients. - Implemented decision and gateway nodes to facilitate user interaction and automatic condition evaluation, enhancing workflow flexibility. - Updated the execution logic to handle multiple paths based on user decisions or automatic evaluations, improving user experience during workflow execution. - Enhanced the HTML content handling to support dynamic variable interpolation, allowing for more personalized content display in HTML nodes. - Introduced debugging features for variable mapping and field conditions, aiding developers in understanding and troubleshooting workflow behavior.
This commit is contained in:
parent
ab5a257283
commit
406bcaea08
@ -54,6 +54,28 @@
|
||||
"animated": true,
|
||||
"sourceHandle": "html-1752550500000-right",
|
||||
"targetHandle": "end-1752546716111-left"
|
||||
},
|
||||
{
|
||||
"id": "html-1752550500000-notification-1752621850786-1752621852927",
|
||||
"data": {},
|
||||
"type": "custom",
|
||||
"label": "",
|
||||
"source": "html-1752550500000",
|
||||
"target": "notification-1752621850786",
|
||||
"animated": true,
|
||||
"sourceHandle": "html-1752550500000-bottom",
|
||||
"targetHandle": "notification-1752621850786-left"
|
||||
},
|
||||
{
|
||||
"id": "notification-1752621850786-end-1752546716111-1752621856004",
|
||||
"data": {},
|
||||
"type": "custom",
|
||||
"label": "",
|
||||
"source": "notification-1752621850786",
|
||||
"target": "end-1752546716111",
|
||||
"animated": true,
|
||||
"sourceHandle": "notification-1752621850786-right",
|
||||
"targetHandle": "end-1752546716111-left"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
@ -131,7 +153,7 @@
|
||||
},
|
||||
"type": "script",
|
||||
"label": "Script Task",
|
||||
"position": { "x": 1200, "y": 330 }
|
||||
"position": { "x": 1185, "y": 330 }
|
||||
},
|
||||
{
|
||||
"id": "html-1752550500000",
|
||||
@ -148,12 +170,37 @@
|
||||
},
|
||||
"type": "html",
|
||||
"label": "Show Result",
|
||||
"position": { "x": 1560, "y": 105 }
|
||||
"position": { "x": 1425, "y": 75 }
|
||||
},
|
||||
{
|
||||
"id": "notification-1752621850786",
|
||||
"data": {
|
||||
"label": "Notification",
|
||||
"message": "This is the notification",
|
||||
"subject": "This is the notification",
|
||||
"priority": "medium",
|
||||
"expiration": { "unit": "hours", "value": 24, "enabled": false },
|
||||
"description": "Send notification to users",
|
||||
"htmlMessage": "",
|
||||
"messageFormat": "text",
|
||||
"recipientRole": "",
|
||||
"recipientType": "email",
|
||||
"recipientUser": "",
|
||||
"recipientEmail": "mdafiqiskandar@gmail.com",
|
||||
"recipientGroup": "",
|
||||
"deliveryOptions": { "sms": false, "email": false, "inApp": true },
|
||||
"richTextMessage": "",
|
||||
"notificationType": "info",
|
||||
"recipientVariable": ""
|
||||
},
|
||||
"type": "notification",
|
||||
"label": "Notification",
|
||||
"position": { "x": 1590, "y": 360 }
|
||||
}
|
||||
],
|
||||
"viewport": {
|
||||
"x": -660.4278622401489,
|
||||
"y": 202.3364877443377,
|
||||
"zoom": 0.8153893887682282
|
||||
"x": -572.7398904538663,
|
||||
"y": 253.200827139402,
|
||||
"zoom": 0.8444612090080732
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import FormScriptEngine from '~/components/FormScriptEngine.vue';
|
||||
import ConditionalLogicEngine from '~/components/ConditionalLogicEngine.vue';
|
||||
@ -130,24 +130,115 @@ const startProcessExecution = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Helper: Get next node ID by following edges
|
||||
function getNextNodeId(currentNodeId) {
|
||||
const edge = workflowData.value.edges.find(e => e.source === currentNodeId);
|
||||
return edge ? edge.target : null;
|
||||
// Helper: Get all outgoing edges for a node
|
||||
function getOutgoingEdges(currentNodeId) {
|
||||
return workflowData.value.edges.filter(e => e.source === currentNodeId);
|
||||
}
|
||||
|
||||
// Move to next step in workflow (edge-based)
|
||||
// Helper: Get next node ID by following edges (for single-path nodes)
|
||||
function getNextNodeId(currentNodeId) {
|
||||
const edges = getOutgoingEdges(currentNodeId);
|
||||
return edges.length > 0 ? edges[0].target : null;
|
||||
}
|
||||
|
||||
// Helper: Check if a node can auto-progress (has single outgoing edge)
|
||||
function canAutoProgress(node) {
|
||||
const outgoingEdges = getOutgoingEdges(node.id);
|
||||
return outgoingEdges.length === 1;
|
||||
}
|
||||
|
||||
// Helper: Get next node ID for decision/gateway nodes based on conditions
|
||||
function getNextNodeIdForDecision(currentNodeId) {
|
||||
const currentNodeObj = workflowData.value.nodes.find(n => n.id === currentNodeId);
|
||||
const outgoingEdges = getOutgoingEdges(currentNodeId);
|
||||
|
||||
if (!currentNodeObj || !outgoingEdges.length) return null;
|
||||
|
||||
const { conditions = [], defaultPath } = currentNodeObj.data || {};
|
||||
|
||||
// Evaluate conditions
|
||||
for (const condition of conditions) {
|
||||
if (evaluateCondition(condition, processVariables.value)) {
|
||||
// Find the edge that matches this condition's target
|
||||
const edge = outgoingEdges.find(e => e.data?.conditionId === condition.id || e.label === condition.label);
|
||||
if (edge) return edge.target;
|
||||
}
|
||||
}
|
||||
|
||||
// If no conditions match, use default path
|
||||
if (defaultPath) {
|
||||
const defaultEdge = outgoingEdges.find(e => e.data?.isDefault || e.label?.toLowerCase().includes('default'));
|
||||
if (defaultEdge) return defaultEdge.target;
|
||||
}
|
||||
|
||||
// Fallback to first edge
|
||||
return outgoingEdges[0]?.target || null;
|
||||
}
|
||||
|
||||
// Helper: Evaluate a single condition
|
||||
function evaluateCondition(condition, variables) {
|
||||
const { variable, operator, value } = condition;
|
||||
const variableValue = variables[variable];
|
||||
|
||||
switch (operator) {
|
||||
case 'equals':
|
||||
case '==':
|
||||
return variableValue == value;
|
||||
case 'not_equals':
|
||||
case '!=':
|
||||
return variableValue != value;
|
||||
case 'greater_than':
|
||||
case '>':
|
||||
return Number(variableValue) > Number(value);
|
||||
case 'less_than':
|
||||
case '<':
|
||||
return Number(variableValue) < Number(value);
|
||||
case 'greater_than_or_equal':
|
||||
case '>=':
|
||||
return Number(variableValue) >= Number(value);
|
||||
case 'less_than_or_equal':
|
||||
case '<=':
|
||||
return Number(variableValue) <= Number(value);
|
||||
case 'contains':
|
||||
return String(variableValue).includes(String(value));
|
||||
case 'not_contains':
|
||||
return !String(variableValue).includes(String(value));
|
||||
case 'is_empty':
|
||||
return !variableValue || variableValue === '' || variableValue === null || variableValue === undefined;
|
||||
case 'is_not_empty':
|
||||
return variableValue && variableValue !== '' && variableValue !== null && variableValue !== undefined;
|
||||
default:
|
||||
console.warn('[Workflow] Unknown condition operator:', operator);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Move to a specific node by its ID
|
||||
const moveToSpecificNode = (targetNodeId) => {
|
||||
const nextIndex = workflowData.value.nodes.findIndex(n => n.id === targetNodeId);
|
||||
if (nextIndex !== -1) {
|
||||
currentStep.value = nextIndex;
|
||||
const node = workflowData.value.nodes[currentStep.value];
|
||||
console.log(`[Workflow] Moved to node: ${node.type} - ${node.data?.label || node.label}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Move to next step in workflow (only for single-path auto-progressable nodes)
|
||||
const moveToNextStep = () => {
|
||||
const currentNode = workflowData.value.nodes[currentStep.value];
|
||||
if (!currentNode) return;
|
||||
|
||||
// Only auto-progress if there's exactly one outgoing edge
|
||||
if (!canAutoProgress(currentNode)) {
|
||||
console.log(`[Workflow] Node ${currentNode.id} has multiple paths, waiting for user choice`);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextNodeId = getNextNodeId(currentNode.id);
|
||||
if (nextNodeId) {
|
||||
const nextIndex = workflowData.value.nodes.findIndex(n => n.id === nextNodeId);
|
||||
if (nextIndex !== -1) {
|
||||
currentStep.value = nextIndex;
|
||||
const node = workflowData.value.nodes[currentStep.value];
|
||||
console.log(`[Workflow] Entered node: ${node.type} - ${node.data?.label || node.label}`);
|
||||
}
|
||||
moveToSpecificNode(nextNodeId);
|
||||
}
|
||||
};
|
||||
|
||||
@ -205,12 +296,22 @@ const executeCurrentStep = async () => {
|
||||
processVariables.value[outputVariable] = response;
|
||||
processVariables.value[errorVariable] = null;
|
||||
console.log('[Workflow] API call success. Output variable set:', outputVariable, response);
|
||||
moveToNextStep();
|
||||
|
||||
// Only auto-progress if there's a single outgoing edge
|
||||
if (canAutoProgress(currentNode.value)) {
|
||||
moveToNextStep();
|
||||
} else {
|
||||
console.log('[Workflow] API completed, multiple paths available - waiting for user choice');
|
||||
}
|
||||
} catch (err) {
|
||||
processVariables.value[errorVariable] = err;
|
||||
console.error('[Workflow] API call failed:', err);
|
||||
if (continueOnError) {
|
||||
moveToNextStep();
|
||||
if (canAutoProgress(currentNode.value)) {
|
||||
moveToNextStep();
|
||||
} else {
|
||||
console.log('[Workflow] API failed but continuing, multiple paths available - waiting for user choice');
|
||||
}
|
||||
} else {
|
||||
error.value = 'API call failed: ' + (err.message || err);
|
||||
}
|
||||
@ -219,7 +320,57 @@ const executeCurrentStep = async () => {
|
||||
console.log(`[Workflow] Executing script node: ${currentNode.value.data?.label || currentNode.value.label}`);
|
||||
// Simulate script execution
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
moveToNextStep();
|
||||
|
||||
// Only auto-progress if there's a single outgoing edge
|
||||
if (canAutoProgress(currentNode.value)) {
|
||||
moveToNextStep();
|
||||
} else {
|
||||
console.log('[Workflow] Script completed, multiple paths available - waiting for user choice');
|
||||
}
|
||||
} else if (currentNode.value?.type === 'html') {
|
||||
console.log(`[Workflow] Processing HTML node: ${currentNode.value.data?.label || currentNode.value.label}`);
|
||||
// HTML nodes are user-interactive, so we don't auto-advance
|
||||
// The user will click a button to continue
|
||||
return;
|
||||
} else if (currentNode.value?.type === 'notification') {
|
||||
console.log(`[Workflow] Sending notification: ${currentNode.value.data?.label || currentNode.value.label}`);
|
||||
// Enhanced notification node execution
|
||||
const {
|
||||
notificationType = 'info',
|
||||
recipients = [],
|
||||
message = '',
|
||||
subject = '',
|
||||
continueOnError = true
|
||||
} = currentNode.value.data || {};
|
||||
|
||||
try {
|
||||
// Simulate notification sending
|
||||
console.log('[Workflow] Notification sent:', { type: notificationType, recipients, message, subject });
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call delay
|
||||
|
||||
// Only auto-progress if there's a single outgoing edge
|
||||
if (canAutoProgress(currentNode.value)) {
|
||||
moveToNextStep();
|
||||
} else {
|
||||
console.log('[Workflow] Notification sent, multiple paths available - waiting for user choice');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Workflow] Notification failed:', err);
|
||||
if (continueOnError) {
|
||||
if (canAutoProgress(currentNode.value)) {
|
||||
moveToNextStep();
|
||||
} else {
|
||||
console.log('[Workflow] Notification failed but continuing, multiple paths available - waiting for user choice');
|
||||
}
|
||||
} else {
|
||||
error.value = 'Notification failed: ' + (err.message || err);
|
||||
}
|
||||
}
|
||||
} else if (['decision', 'gateway'].includes(currentNode.value?.type)) {
|
||||
console.log(`[Workflow] Processing decision node: ${currentNode.value.data?.label || currentNode.value.label}`);
|
||||
// Decision nodes require user interaction or automatic condition evaluation
|
||||
// We'll handle this in the template section
|
||||
return;
|
||||
}
|
||||
// Add more node types as needed
|
||||
} catch (err) {
|
||||
@ -238,6 +389,9 @@ const getStepInfo = (node) => {
|
||||
'api': { label: 'API Call', icon: 'material-symbols:api', color: 'purple' },
|
||||
'script': { label: 'Script Execution', icon: 'material-symbols:code', color: 'orange' },
|
||||
'decision': { label: 'Decision Point', icon: 'material-symbols:alt-route', color: 'yellow' },
|
||||
'gateway': { label: 'Decision Gateway', icon: 'material-symbols:fork-right', color: 'yellow' },
|
||||
'html': { label: 'HTML Content', icon: 'material-symbols:code-blocks', color: 'indigo' },
|
||||
'notification': { label: 'Notification', icon: 'material-symbols:notifications', color: 'pink' },
|
||||
'end': { label: 'Process Complete', icon: 'material-symbols:check-circle', color: 'green' }
|
||||
};
|
||||
|
||||
@ -300,7 +454,7 @@ onMounted(() => {
|
||||
// Watch for step changes to auto-execute non-form steps or load form data
|
||||
watch(currentStep, async (newStep) => {
|
||||
if (currentNode.value) {
|
||||
if (['api', 'script'].includes(currentNode.value.type)) {
|
||||
if (['api', 'script', 'notification'].includes(currentNode.value.type)) {
|
||||
await executeCurrentStep();
|
||||
} else if (currentNode.value.type === 'form') {
|
||||
// Load form data for form nodes
|
||||
@ -313,7 +467,11 @@ watch(currentStep, async (newStep) => {
|
||||
formStore.updatePreviewFormData(formData.value);
|
||||
}
|
||||
}
|
||||
} else if (['decision', 'gateway'].includes(currentNode.value.type)) {
|
||||
// Handle decision nodes
|
||||
await executeDecisionNode();
|
||||
}
|
||||
// html nodes are handled in template - no auto-execution needed
|
||||
}
|
||||
});
|
||||
|
||||
@ -337,6 +495,51 @@ const validateAndSubmit = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle decision node execution (automatic or manual)
|
||||
const executeDecisionNode = async () => {
|
||||
try {
|
||||
stepLoading.value = true;
|
||||
const currentNodeObj = workflowData.value.nodes[currentStep.value];
|
||||
const { executionType = 'automatic' } = currentNodeObj.data || {};
|
||||
|
||||
if (executionType === 'automatic') {
|
||||
// Automatic decision based on conditions
|
||||
const nextNodeId = getNextNodeIdForDecision(currentNodeObj.id);
|
||||
if (nextNodeId) {
|
||||
if (moveToSpecificNode(nextNodeId)) {
|
||||
const node = workflowData.value.nodes[currentStep.value];
|
||||
console.log(`[Workflow] Decision made automatically, moving to: ${node.data?.label || node.label}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Manual decisions are handled in the template with buttons
|
||||
} catch (err) {
|
||||
console.error('[Workflow] Error executing decision:', err);
|
||||
error.value = 'Failed to execute decision';
|
||||
} finally {
|
||||
stepLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle manual decision choice
|
||||
const makeDecision = (targetNodeId) => {
|
||||
if (moveToSpecificNode(targetNodeId)) {
|
||||
const node = workflowData.value.nodes[currentStep.value];
|
||||
console.log(`[Workflow] Manual decision made, moving to: ${node.data?.label || node.label}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper: Interpolate process variables in HTML content
|
||||
const interpolateHtmlContent = (htmlContent) => {
|
||||
if (!htmlContent || typeof htmlContent !== 'string') return htmlContent;
|
||||
|
||||
// Replace {{ processVariables.variableName }} with actual values
|
||||
return htmlContent.replace(/\{\{\s*processVariables\.(\w+)\s*\}\}/g, (match, variableName) => {
|
||||
const value = processVariables.value[variableName];
|
||||
return value !== undefined && value !== null ? String(value) : '';
|
||||
});
|
||||
};
|
||||
|
||||
// Compute the workflow path by following edges from start to end
|
||||
function computeWorkflowPath() {
|
||||
const path = [];
|
||||
@ -371,6 +574,53 @@ const currentStepIndex = computed(() => {
|
||||
return workflowPath.value.indexOf(currentNode.value?.id) + 1;
|
||||
});
|
||||
const totalSteps = computed(() => workflowPath.value.length);
|
||||
|
||||
// Computed: Interpolated HTML content for HTML nodes
|
||||
const interpolatedHtmlContent = computed(() => {
|
||||
if (currentNode.value?.type !== 'html') return '';
|
||||
|
||||
const htmlContent = currentNode.value?.data?.htmlCode || currentNode.value?.data?.htmlContent || '';
|
||||
return interpolateHtmlContent(htmlContent);
|
||||
});
|
||||
|
||||
// Computed: CSS styles for HTML nodes
|
||||
const htmlNodeStyles = computed(() => {
|
||||
if (currentNode.value?.type !== 'html') return '';
|
||||
return currentNode.value?.data?.cssCode || '';
|
||||
});
|
||||
|
||||
// CSS injection for HTML nodes
|
||||
let currentStyleElement = null;
|
||||
|
||||
// Function to inject CSS
|
||||
const injectHtmlNodeCSS = (cssCode) => {
|
||||
// Remove previous style element
|
||||
if (currentStyleElement) {
|
||||
currentStyleElement.remove();
|
||||
currentStyleElement = null;
|
||||
}
|
||||
|
||||
// Add new styles if available
|
||||
if (cssCode && cssCode.trim()) {
|
||||
currentStyleElement = document.createElement('style');
|
||||
currentStyleElement.textContent = cssCode;
|
||||
currentStyleElement.setAttribute('data-workflow-html-node', 'true');
|
||||
document.head.appendChild(currentStyleElement);
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for HTML node CSS changes and inject styles
|
||||
watch(htmlNodeStyles, (newStyles) => {
|
||||
injectHtmlNodeCSS(newStyles);
|
||||
}, { immediate: true });
|
||||
|
||||
// Cleanup styles on unmount
|
||||
onUnmounted(() => {
|
||||
if (currentStyleElement) {
|
||||
currentStyleElement.remove();
|
||||
currentStyleElement = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -463,6 +713,9 @@ const totalSteps = computed(() => workflowPath.value.length);
|
||||
getStepInfo(currentNode).color === 'green' ? 'bg-green-100 text-green-600' :
|
||||
getStepInfo(currentNode).color === 'purple' ? 'bg-purple-100 text-purple-600' :
|
||||
getStepInfo(currentNode).color === 'orange' ? 'bg-orange-100 text-orange-600' :
|
||||
getStepInfo(currentNode).color === 'yellow' ? 'bg-yellow-100 text-yellow-600' :
|
||||
getStepInfo(currentNode).color === 'indigo' ? 'bg-indigo-100 text-indigo-600' :
|
||||
getStepInfo(currentNode).color === 'pink' ? 'bg-pink-100 text-pink-600' :
|
||||
'bg-gray-100 text-gray-600'
|
||||
]">
|
||||
<Icon :name="getStepInfo(currentNode).icon" class="w-6 h-6" />
|
||||
@ -573,13 +826,162 @@ const totalSteps = computed(() => workflowPath.value.length);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API/Script Step -->
|
||||
<div v-else-if="['api', 'script'].includes(currentNode.type)" class="bg-white rounded-xl shadow-sm border border-gray-200 p-8 text-center">
|
||||
<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">
|
||||
{{ currentNode.type === 'api' ? 'Calling API...' : 'Executing Script...' }}
|
||||
</h3>
|
||||
<p class="text-gray-600">Please wait while we process this step</p>
|
||||
<!-- 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">
|
||||
<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">
|
||||
{{
|
||||
currentNode.type === 'api' ? 'Calling API...' :
|
||||
currentNode.type === 'script' ? 'Executing Script...' :
|
||||
'Sending Notification...'
|
||||
}}
|
||||
</h3>
|
||||
<p class="text-gray-600">Please wait while we process this step</p>
|
||||
</div>
|
||||
|
||||
<!-- Completed State with Multiple Paths -->
|
||||
<div v-else-if="!canAutoProgress(currentNode)" class="space-y-4">
|
||||
<div 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">
|
||||
{{
|
||||
currentNode.type === 'api' ? 'API Call Complete' :
|
||||
currentNode.type === 'script' ? 'Script Executed' :
|
||||
'Notification Sent'
|
||||
}}
|
||||
</h3>
|
||||
<p class="text-gray-600 mb-6">{{ currentNode.data?.label || currentNode.label }}</p>
|
||||
</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="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"
|
||||
>
|
||||
<span>{{ edge.label || 'Continue' }}</span>
|
||||
<Icon name="material-symbols:arrow-right-alt" />
|
||||
</RsButton>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">
|
||||
{{ currentNode.data?.title || currentNode.data?.label || 'Content Display' }}
|
||||
</h3>
|
||||
<p v-if="currentNode.data?.description" class="text-gray-600 text-sm">
|
||||
{{ currentNode.data.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- HTML Content -->
|
||||
<div class="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">
|
||||
<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">
|
||||
<!-- Single Path - Simple Continue Button -->
|
||||
<div v-if="canAutoProgress(currentNode)" class="flex justify-end">
|
||||
<RsButton
|
||||
@click="moveToNextStep"
|
||||
:disabled="stepLoading"
|
||||
variant="primary"
|
||||
>
|
||||
<Icon v-if="stepLoading" name="material-symbols:progress-activity" class="w-4 h-4 animate-spin mr-2" />
|
||||
{{ stepLoading ? 'Processing...' : 'Continue' }}
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- Multiple Paths - Show Options -->
|
||||
<div v-else class="space-y-3">
|
||||
<h4 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"
|
||||
>
|
||||
<span>{{ edge.label || 'Continue' }}</span>
|
||||
<Icon name="material-symbols:arrow-right-alt" />
|
||||
</RsButton>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">
|
||||
{{ currentNode.data?.title || currentNode.data?.label || 'Make a Decision' }}
|
||||
</h3>
|
||||
<p v-if="currentNode.data?.description" class="text-gray-600 text-sm">
|
||||
{{ currentNode.data.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<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"
|
||||
class="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">
|
||||
{{ edge.data.description }}
|
||||
</div>
|
||||
</div>
|
||||
<Icon name="material-symbols:arrow-right-alt" class="ml-auto" />
|
||||
</RsButton>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Automatic Decision Processing -->
|
||||
<div v-else 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>
|
||||
|
||||
<!-- Show conditions being evaluated -->
|
||||
<div v-if="currentNode.data?.conditions?.length" class="mt-6 text-left">
|
||||
<h5 class="font-medium text-gray-700 mb-3">Conditions:</h5>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(condition, index) in currentNode.data.conditions" :key="index"
|
||||
class="flex items-center gap-2 text-sm text-gray-600 bg-gray-50 p-2 rounded">
|
||||
<Icon name="material-symbols:rule" class="w-4 h-4" />
|
||||
<span>{{ condition.variable }} {{ condition.operator }} {{ condition.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Other Step Types -->
|
||||
|
Loading…
x
Reference in New Issue
Block a user