Enhance Button Mapping Functionality in Form Node Configuration and Component Preview

- Added support for button click event emission in ComponentPreview.vue, allowing for workflow processing of button interactions.
- Introduced a new section in FormNodeConfiguration.vue for mapping button clicks to process variables, enabling users to assign button click events to specific variables.
- Implemented functionality to add and remove button mappings, enhancing the flexibility of button interactions within the form workflow.
- Updated state management to include button mappings, ensuring proper handling of button-related data during form processing.
- Enhanced computed properties to provide options for form buttons, improving user experience in selecting and configuring button mappings.
This commit is contained in:
Afiq 2025-08-08 09:42:29 +08:00
parent 4425e912ab
commit 649a956063
4 changed files with 499 additions and 22 deletions

View File

@ -873,7 +873,7 @@ const props = defineProps({
} }
}); });
const emit = defineEmits(['select-nested-component', 'form-data-updated']); const emit = defineEmits(['select-nested-component', 'form-data-updated', 'button-clicked']);
// Get access to the form builder store // Get access to the form builder store
const formStore = useFormBuilderStore(); const formStore = useFormBuilderStore();
@ -1445,7 +1445,17 @@ const handleButtonClick = () => {
} }
} }
// Emit button click event for workflow processing
emit('button-clicked', {
buttonName: props.component.props.name,
buttonLabel: props.component.props.label,
buttonText: props.component.props.buttonText || props.component.props.label,
buttonType: props.component.props.buttonType || 'button',
timestamp: new Date().toISOString()
});
// Default behavior - log the button click // Default behavior - log the button click
console.log('[ComponentPreview] Button clicked:', props.component.props.name);
}; };
// Check if the component is a standard FormKit input type (excluding specialized components) // Check if the component is a standard FormKit input type (excluding specialized components)

View File

@ -242,6 +242,127 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Button Variables Mapping (Button Clicks Process) -->
<div class="mb-3">
<div class="flex justify-between items-center mb-3">
<div>
<h5 class="text-sm font-medium flex items-center">
<span class="w-5 h-5 rounded-full bg-purple-100 text-purple-700 flex items-center justify-center mr-2 text-xs">
<Icon name="material-symbols:smart-button" />
</span>
Button Variables (Button Clicks Process)
</h5>
<p class="text-xs text-gray-500 ml-7">Assign button click events to process variables</p>
</div>
<RsButton
@click="addButtonMapping()"
variant="secondary"
size="sm"
class="btn-sm-emerald"
>
<Icon name="material-symbols:add" class="mr-1" /> Add Mapping
</RsButton>
</div>
<!-- No button mappings placeholder -->
<div v-if="!localNodeData.buttonMappings || localNodeData.buttonMappings.length === 0"
class="py-4 text-center text-gray-500 text-sm border border-dashed rounded-md bg-white">
<p class="mb-2">
No button mappings defined. Button clicks will not be tracked in process variables.
</p>
<RsButton
@click="addButtonMapping()"
variant="secondary"
size="sm"
class="btn-sm-emerald"
>
<Icon name="material-symbols:add" class="mr-1" /> Add First Mapping
</RsButton>
</div>
<div v-else class="space-y-3">
<div v-for="(mapping, index) in localNodeData.buttonMappings" :key="'button-' + index"
class="p-4 border rounded-md bg-purple-50">
<div class="flex justify-between mb-3">
<h5 class="text-sm font-medium flex items-center">
<Icon name="material-symbols:smart-button" class="text-purple-600 mr-1" />
Button Click to Process Variable #{{ index + 1 }}
</h5>
<button
@click="removeButtonMapping(index)"
class="text-red-500 hover:text-red-700 p-1 rounded hover:bg-red-50"
title="Remove mapping"
>
<Icon name="material-symbols:delete-outline" />
</button>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Form Button</label>
<FormKit
type="select"
v-model="mapping.formButton"
:options="formButtonOptions"
placeholder="Select a form button"
:disabled="formButtons.length === 0"
:classes="{ outer: 'mb-0' }"
/>
<p class="mt-1 text-xs text-gray-500">
{{ formButtons.length === 0 ? 'No form buttons available' : 'The source button in the form' }}
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Process Variable</label>
<VariableBrowser
v-model="mapping.processVariable"
:availableVariables="availableVariables"
placeholder="Select process variable"
:allowCreate="true"
@update:modelValue="saveChanges"
/>
<p class="mt-1 text-xs text-gray-500">
The target variable to store button click information
</p>
</div>
</div>
<!-- Action/Value Row -->
<div class="mt-4 grid grid-cols-1 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Click Action Value</label>
<FormKit
type="select"
v-model="mapping.action"
:options="[
{ label: 'Button Clicked (true/false)', value: 'clicked' },
{ label: 'Button Name', value: 'button_name' },
{ label: 'Button Label', value: 'button_label' },
{ label: 'Custom Value', value: 'custom' }
]"
placeholder="Select what value to store"
:classes="{ outer: 'mb-0' }"
/>
<p class="mt-1 text-xs text-gray-500">What value to assign to the process variable when button is clicked</p>
</div>
<!-- Custom Value Field (only shown when action is 'custom') -->
<div v-if="mapping.action === 'custom'">
<label class="block text-sm font-medium text-gray-700 mb-1">Custom Value</label>
<FormKit
type="text"
v-model="mapping.customValue"
placeholder="Enter custom value"
:classes="{ outer: 'mb-0' }"
/>
<p class="mt-1 text-xs text-gray-500">The custom value to assign when button is clicked</p>
</div>
</div>
</div>
</div>
</div>
</div> </div>
<!-- Step 3: Field Conditions (only if form is selected) --> <!-- Step 3: Field Conditions (only if form is selected) -->
@ -621,9 +742,11 @@ const emit = defineEmits(['update']);
// Get the variable store for creating variables // Get the variable store for creating variables
const processStore = useProcessBuilderStore(); const processStore = useProcessBuilderStore();
// State for form fields // State for form fields and buttons
const formFields = ref([]); const formFields = ref([]);
const formButtons = ref([]);
const isLoadingFields = ref(false); const isLoadingFields = ref(false);
const isLoadingButtons = ref(false);
// Local state for node data - create a deep copy to avoid mutation issues // Local state for node data - create a deep copy to avoid mutation issues
const localNodeData = ref({ const localNodeData = ref({
@ -634,6 +757,7 @@ const localNodeData = ref({
formUuid: null, formUuid: null,
inputMappings: [], inputMappings: [],
outputMappings: [], outputMappings: [],
buttonMappings: [],
fieldConditions: [], fieldConditions: [],
assignmentType: 'public', assignmentType: 'public',
assignedUsers: [], assignedUsers: [],
@ -662,6 +786,9 @@ watch(() => props.nodeData, async (newNodeData) => {
outputMappings: Array.isArray(newNodeData.outputMappings) outputMappings: Array.isArray(newNodeData.outputMappings)
? newNodeData.outputMappings.map(mapping => ({ ...mapping })) ? newNodeData.outputMappings.map(mapping => ({ ...mapping }))
: [], : [],
buttonMappings: Array.isArray(newNodeData.buttonMappings)
? newNodeData.buttonMappings.map(mapping => ({ ...mapping }))
: [],
fieldConditions: Array.isArray(newNodeData.fieldConditions) fieldConditions: Array.isArray(newNodeData.fieldConditions)
? newNodeData.fieldConditions.map(condition => ({ ...condition })) ? newNodeData.fieldConditions.map(condition => ({ ...condition }))
: [], : [],
@ -672,9 +799,12 @@ watch(() => props.nodeData, async (newNodeData) => {
assignmentVariableType: newNodeData.assignmentVariableType || 'user_id' assignmentVariableType: newNodeData.assignmentVariableType || 'user_id'
}; };
// Load form fields if form is already selected // Load form fields and buttons if form is already selected
if (newNodeData.formId || newNodeData.formUuid) { if (newNodeData.formId || newNodeData.formUuid) {
await loadFormFields(newNodeData.formId || newNodeData.formUuid); await Promise.all([
loadFormFields(newNodeData.formId || newNodeData.formUuid),
loadFormButtons(newNodeData.formId || newNodeData.formUuid)
]);
} }
} }
}, { immediate: true, deep: true }); }, { immediate: true, deep: true });
@ -690,6 +820,7 @@ async function handleFormSelection(form) {
formUuid: form.formUUID, formUuid: form.formUUID,
label: form.formName || 'Form Task', label: form.formName || 'Form Task',
description: `Form: ${form.formName}`, description: `Form: ${form.formName}`,
buttonMappings: [],
fieldConditions: [], fieldConditions: [],
assignmentType: 'public', assignmentType: 'public',
assignedUsers: [], assignedUsers: [],
@ -698,8 +829,11 @@ async function handleFormSelection(form) {
assignmentVariableType: 'user_id' assignmentVariableType: 'user_id'
}; };
// Load form fields for this form // Load form fields and buttons for this form
await loadFormFields(form.formID || form.formUUID); await Promise.all([
loadFormFields(form.formID || form.formUUID),
loadFormButtons(form.formID || form.formUUID)
]);
saveChanges(); saveChanges();
} }
@ -715,6 +849,7 @@ function clearFormSelection() {
description: 'Form submission task', description: 'Form submission task',
inputMappings: [], inputMappings: [],
outputMappings: [], outputMappings: [],
buttonMappings: [],
fieldConditions: [], fieldConditions: [],
assignmentType: 'public', assignmentType: 'public',
assignedUsers: [], assignedUsers: [],
@ -723,8 +858,9 @@ function clearFormSelection() {
assignmentVariableType: 'user_id' assignmentVariableType: 'user_id'
}; };
// Clear form fields // Clear form fields and buttons
formFields.value = []; formFields.value = [];
formButtons.value = [];
saveChanges(); saveChanges();
} }
@ -769,6 +905,28 @@ function removeOutputMapping(index) {
saveChanges(); saveChanges();
} }
// Add a button mapping
function addButtonMapping() {
if (!localNodeData.value.buttonMappings) {
localNodeData.value.buttonMappings = [];
}
localNodeData.value.buttonMappings.push({
formButton: '',
processVariable: '',
action: 'clicked',
customValue: ''
});
saveChanges();
}
// Remove a button mapping
function removeButtonMapping(index) {
localNodeData.value.buttonMappings.splice(index, 1);
saveChanges();
}
// Generate a variable name from a form field // Generate a variable name from a form field
function getVariableNameFromFormField(formField) { function getVariableNameFromFormField(formField) {
// Handle cases where formField is not a string // Handle cases where formField is not a string
@ -860,6 +1018,33 @@ async function loadFormFields(formId) {
} }
} }
// Load form buttons for the selected form
async function loadFormButtons(formId) {
if (!formId) {
formButtons.value = [];
return;
}
isLoadingButtons.value = true;
try {
const response = await fetch(`/api/forms/${formId}/buttons`);
const result = await response.json();
if (result.success && result.buttons) {
formButtons.value = result.buttons;
} else {
console.error('Failed to load form buttons:', result.error);
formButtons.value = [];
}
} catch (error) {
console.error('Error loading form buttons:', error);
formButtons.value = [];
} finally {
isLoadingButtons.value = false;
}
}
// Computed property for form field options (for FormKit select) // Computed property for form field options (for FormKit select)
const formFieldOptions = computed(() => { const formFieldOptions = computed(() => {
return formFields.value.map(field => ({ return formFields.value.map(field => ({
@ -869,6 +1054,15 @@ const formFieldOptions = computed(() => {
})); }));
}); });
// Computed property for form button options (for FormKit select)
const formButtonOptions = computed(() => {
return formButtons.value.map(button => ({
label: `${button.label} (${button.name})`,
value: button.name,
description: button.description || `${button.buttonType} button`
}));
});
// Computed property for process variable options (for FormKit select) // Computed property for process variable options (for FormKit select)
const processVariableOptions = computed(() => { const processVariableOptions = computed(() => {
return props.availableVariables.map(variable => ({ return props.availableVariables.map(variable => ({
@ -946,6 +1140,8 @@ function saveChanges() {
localNodeData.value.inputMappings.map(mapping => ({ ...mapping })) : [], localNodeData.value.inputMappings.map(mapping => ({ ...mapping })) : [],
outputMappings: localNodeData.value.outputMappings ? outputMappings: localNodeData.value.outputMappings ?
localNodeData.value.outputMappings.map(mapping => ({ ...mapping })) : [], localNodeData.value.outputMappings.map(mapping => ({ ...mapping })) : [],
buttonMappings: localNodeData.value.buttonMappings ?
localNodeData.value.buttonMappings.map(mapping => ({ ...mapping })) : [],
fieldConditions: localNodeData.value.fieldConditions ? fieldConditions: localNodeData.value.fieldConditions ?
localNodeData.value.fieldConditions.map(condition => ({ ...condition })) : [], localNodeData.value.fieldConditions.map(condition => ({ ...condition })) : [],
assignmentType: localNodeData.value.assignmentType || 'public', assignmentType: localNodeData.value.assignmentType || 'public',

View File

@ -45,6 +45,7 @@ const tasks = ref([]);
const conditionalLogicScript = ref(''); const conditionalLogicScript = ref('');
const combinedScript = ref(''); const combinedScript = ref('');
const fieldStates = ref({}); const fieldStates = ref({});
const visitedNodesInSession = ref(new Set());
// Get process ID from route // Get process ID from route
const processId = computed(() => route.params.id); const processId = computed(() => route.params.id);
@ -195,6 +196,9 @@ const loadProcess = async () => {
// Capture URL parameters for variables configured to do so // Capture URL parameters for variables configured to do so
captureUrlParameters(); captureUrlParameters();
// Reset visited nodes tracking for new process execution
visitedNodesInSession.value.clear();
// Start the process execution (case instance) // Start the process execution (case instance)
await startProcessExecution(); await startProcessExecution();
@ -260,24 +264,59 @@ function getNextNodeIdForDecision(currentNodeId) {
const currentNodeObj = workflowData.value.nodes.find(n => n.id === currentNodeId); const currentNodeObj = workflowData.value.nodes.find(n => n.id === currentNodeId);
const outgoingEdges = getOutgoingEdges(currentNodeId); const outgoingEdges = getOutgoingEdges(currentNodeId);
if (!currentNodeObj || !outgoingEdges.length) return null; console.log('[Gateway Debug] getNextNodeIdForDecision - Node:', currentNodeObj?.data?.label);
console.log('[Gateway Debug] getNextNodeIdForDecision - Outgoing edges:', outgoingEdges);
const { conditions = [] } = currentNodeObj.data || {}; if (!currentNodeObj || !outgoingEdges.length) {
console.log('[Gateway Debug] No node or edges found');
return null;
}
const { conditions = [], defaultPath } = currentNodeObj.data || {};
console.log('[Gateway Debug] Conditions:', conditions);
console.log('[Gateway Debug] Default path:', defaultPath);
// Evaluate condition groups (each group represents a path) // Evaluate condition groups (each group represents a path)
for (const conditionGroup of conditions) { for (const conditionGroup of conditions) {
if (evaluateConditionGroup(conditionGroup, processVariables.value)) { const conditionResult = evaluateConditionGroup(conditionGroup, processVariables.value);
console.log('[Gateway Debug] Condition group result:', conditionGroup.output, '=', conditionResult);
if (conditionResult) {
// Find the edge that matches this condition group's output label // Find the edge that matches this condition group's output label
const edge = outgoingEdges.find(e => e.label === conditionGroup.output || e.data?.condition === conditionGroup.output); const edge = outgoingEdges.find(e =>
e.label === conditionGroup.output ||
e.data?.condition === conditionGroup.output
);
console.log('[Gateway Debug] Found matching edge for condition:', edge);
if (edge) return edge.target; if (edge) return edge.target;
} }
} }
// If no conditions match, look for default path // If no conditions match, look for default path
const defaultEdge = outgoingEdges.find(e => e.data?.isDefault || e.label?.toLowerCase().includes('default')); let defaultEdge = null;
if (defaultEdge) return defaultEdge.target;
// First try to find edge with explicit default path name
if (defaultPath) {
defaultEdge = outgoingEdges.find(e => e.label === defaultPath);
console.log('[Gateway Debug] Looking for default path by name:', defaultPath, 'found:', defaultEdge);
}
// Then look for edge marked as default or containing 'default' in label
if (!defaultEdge) {
defaultEdge = outgoingEdges.find(e =>
e.data?.isDefault ||
e.label?.toLowerCase().includes('default')
);
console.log('[Gateway Debug] Looking for default edge by pattern, found:', defaultEdge);
}
if (defaultEdge) {
console.log('[Gateway Debug] Using default edge:', defaultEdge);
return defaultEdge.target;
}
// Fallback to first edge // Fallback to first edge
console.log('[Gateway Debug] Fallback to first edge:', outgoingEdges[0]);
return outgoingEdges[0]?.target || null; return outgoingEdges[0]?.target || null;
} }
@ -464,13 +503,23 @@ function evaluateCondition(condition, variables) {
// Move to a specific node by its ID // Move to a specific node by its ID
const moveToSpecificNode = (targetNodeId) => { const moveToSpecificNode = (targetNodeId) => {
// Safety check: prevent revisiting the same node too many times
const visitCount = Array.from(visitedNodesInSession.value).filter(id => id === targetNodeId).length;
if (visitCount > 3) {
console.warn('[Workflow] Safety check: Node', targetNodeId, 'visited too many times, stopping to prevent infinite loop');
error.value = 'Workflow loop detected. Please check your process configuration.';
return false;
}
const nextIndex = workflowData.value.nodes.findIndex(n => n.id === targetNodeId); const nextIndex = workflowData.value.nodes.findIndex(n => n.id === targetNodeId);
if (nextIndex !== -1) { if (nextIndex !== -1) {
visitedNodesInSession.value.add(targetNodeId);
currentStep.value = nextIndex; currentStep.value = nextIndex;
const node = workflowData.value.nodes[currentStep.value]; const node = workflowData.value.nodes[currentStep.value];
console.log(`[Workflow] Moved to node: ${node.type} - ${node.data?.label || node.label}`); console.log(`[Workflow] Moved to node: ${node.type} - ${node.data?.label || node.label}`);
return true; return true;
} }
console.warn('[Workflow] Target node not found:', targetNodeId);
return false; return false;
}; };
@ -605,6 +654,39 @@ function applyOutputMappings(nodeData, formData, processVars) {
}); });
} }
// Apply button mappings to assign button click data to process variables
function applyButtonMappings(nodeData, buttonEvent, processVars) {
const { buttonMappings = [] } = nodeData;
console.log('[Workflow] Applying button mappings:', buttonMappings, 'for button:', buttonEvent.buttonName);
buttonMappings.forEach(mapping => {
const { formButton, processVariable, action, customValue } = mapping;
if (formButton === buttonEvent.buttonName && processVariable) {
let valueToAssign;
switch (action) {
case 'clicked':
valueToAssign = true;
break;
case 'button_name':
valueToAssign = buttonEvent.buttonName;
break;
case 'button_label':
valueToAssign = buttonEvent.buttonLabel;
break;
case 'custom':
valueToAssign = customValue || '';
break;
default:
valueToAssign = true;
}
processVars[processVariable] = valueToAssign;
console.log(`[Workflow] Button mapping applied: ${formButton} -> ${processVariable} = ${valueToAssign}`);
}
});
}
// Apply field conditions for dynamic form behavior // Apply field conditions for dynamic form behavior
function applyFieldConditions(nodeData, processVars) { function applyFieldConditions(nodeData, processVars) {
const { fieldConditions = [] } = nodeData; const { fieldConditions = [] } = nodeData;
@ -1025,6 +1107,33 @@ const handleFormDataUpdate = (updatedData) => {
formStore.updatePreviewFormData(formData.value); formStore.updatePreviewFormData(formData.value);
}; };
// Handle button click events and apply button mappings
const handleButtonClick = (buttonEvent) => {
console.log('[Workflow] Button clicked:', buttonEvent);
// Apply button mappings if configured
if (currentNode.value?.data?.buttonMappings?.length > 0) {
applyButtonMappings(currentNode.value.data, buttonEvent, processVariables.value);
// Check if any button mapping has navigation action
const buttonMapping = currentNode.value.data.buttonMappings.find(
mapping => mapping.formButton === buttonEvent.buttonName && mapping.navigationAction === 'continue'
);
if (buttonMapping) {
console.log('[Workflow] Button mapping has navigation action, moving to next step');
// Wait a moment for variables to be updated then continue
nextTick(() => {
moveToNextStep();
// If next node is a gateway, execute it automatically
if (currentNode.value && ['decision', 'gateway'].includes(currentNode.value.type)) {
executeDecisionNode();
}
});
}
}
};
// Handle conditional logic script generation // Handle conditional logic script generation
const handleConditionalLogicGenerated = (generatedScript) => { const handleConditionalLogicGenerated = (generatedScript) => {
console.log('[WorkflowExecution] Conditional logic script generated'); console.log('[WorkflowExecution] Conditional logic script generated');
@ -1183,6 +1292,8 @@ watch(currentStep, async (newStep) => {
if (currentNode.value) { if (currentNode.value) {
stepLoading.value = true; // Start loading for any node stepLoading.value = true; // Start loading for any node
try { try {
console.log('[Workflow] Step changed to:', newStep, 'Node type:', currentNode.value.type, 'Node label:', currentNode.value.data?.label);
if (["api", "script", "notification"].includes(currentNode.value.type)) { if (["api", "script", "notification"].includes(currentNode.value.type)) {
await executeCurrentStep(); await executeCurrentStep();
} else if (currentNode.value.type === "form") { } else if (currentNode.value.type === "form") {
@ -1304,9 +1415,13 @@ watch(currentStep, async (newStep) => {
} }
} }
} else if (["decision", "gateway"].includes(currentNode.value.type)) { } else if (["decision", "gateway"].includes(currentNode.value.type)) {
console.log('[Workflow] Processing gateway/decision node');
await executeDecisionNode(); await executeDecisionNode();
} }
// html nodes are handled in template - no auto-execution needed // html nodes are handled in template - no auto-execution needed
} catch (err) {
console.error('[Workflow] Error in step change watch:', err);
error.value = 'Error processing workflow step: ' + (err.message || err);
} finally { } finally {
stepLoading.value = false; // End loading after all async work stepLoading.value = false; // End loading after all async work
} }
@ -1349,21 +1464,36 @@ const executeDecisionNode = async () => {
// Debug: Log process variables before evaluating gateway // Debug: Log process variables before evaluating gateway
console.log('[Gateway Debug] Current processVariables:', JSON.stringify(processVariables.value, null, 2)); console.log('[Gateway Debug] Current processVariables:', JSON.stringify(processVariables.value, null, 2));
console.log('[Gateway Debug] Current node:', currentNodeObj);
console.log('[Gateway Debug] Execution type:', executionType);
if (executionType === 'automatic') { if (executionType === 'automatic') {
// Automatic decision based on conditions // Automatic decision based on conditions
const nextNodeId = getNextNodeIdForDecision(currentNodeObj.id); const nextNodeId = getNextNodeIdForDecision(currentNodeObj.id);
console.log('[Gateway Debug] Next node ID from decision:', nextNodeId);
if (nextNodeId) { if (nextNodeId) {
if (moveToSpecificNode(nextNodeId)) { if (moveToSpecificNode(nextNodeId)) {
const node = workflowData.value.nodes[currentStep.value]; const node = workflowData.value.nodes[currentStep.value];
console.log(`[Workflow] Decision made automatically, moving to: ${node.data?.label || node.label}`); console.log(`[Workflow] Decision made automatically, moving to: ${node.data?.label || node.label}`);
// Continue execution if the next node is also auto-executable
if (currentNode.value && ['api', 'script', 'notification'].includes(currentNode.value.type)) {
await executeCurrentStep();
} else if (currentNode.value && ['decision', 'gateway'].includes(currentNode.value.type)) {
// Recursively handle nested gateways
await executeDecisionNode();
}
} }
} else {
console.warn('[Gateway Debug] No valid next node found for automatic decision');
error.value = 'Unable to determine next step in workflow';
} }
} }
// Manual decisions are handled in the template with buttons // Manual decisions are handled in the template with buttons
} catch (err) { } catch (err) {
console.error('[Workflow] Error executing decision:', err); console.error('[Workflow] Error executing decision:', err);
error.value = 'Failed to execute decision'; error.value = 'Failed to execute decision: ' + (err.message || err);
} finally { } finally {
stepLoading.value = false; stepLoading.value = false;
} }
@ -1397,18 +1527,44 @@ function computeWorkflowPath() {
const path = []; const path = [];
const nodes = workflowData.value?.nodes || []; const nodes = workflowData.value?.nodes || [];
const edges = workflowData.value?.edges || []; const edges = workflowData.value?.edges || [];
if (!nodes.length) return path; if (!nodes.length) return path;
const visitedNodes = new Set();
let node = nodes.find(n => n.type === 'start'); let node = nodes.find(n => n.type === 'start');
while (node) {
while (node && !visitedNodes.has(node.id)) {
visitedNodes.add(node.id);
path.push(node.id); path.push(node.id);
// Stop if we've reached the end node
if (node.type === 'end') break;
const outgoingEdges = edges.filter(e => e.source === node.id); const outgoingEdges = edges.filter(e => e.source === node.id);
if (outgoingEdges.length > 1) {
console.warn('[Workflow] Multiple outgoing edges found for node', node.id, outgoingEdges); // Handle different cases for outgoing edges
// For progress, just follow the first edge for now if (outgoingEdges.length === 0) {
// No more edges, end of path
break;
} else if (outgoingEdges.length === 1) {
// Single path, continue normally
const edge = outgoingEdges[0];
node = nodes.find(n => n.id === edge.target);
} else {
// Multiple paths (gateway/decision node)
// For linear path computation, we can't determine which path to take
// without executing the conditions, so we stop here
console.log('[Workflow] Branching detected at node', node.data?.label || node.id, '- stopping linear path computation');
break;
}
// Safety check to prevent infinite loops
if (path.length > 50) {
console.warn('[Workflow] Path computation safety limit reached, stopping');
break;
} }
const edge = outgoingEdges[0];
node = edge ? nodes.find(n => n.id === edge.target) : null;
} }
return path; return path;
} }
@ -1423,9 +1579,44 @@ watch(
); );
const currentStepIndex = computed(() => { const currentStepIndex = computed(() => {
return workflowPath.value.indexOf(currentNode.value?.id) + 1; if (!currentNode.value?.id) return 0;
// For workflows with branching, use node position in the overall flow
const nodeIndex = workflowPath.value.indexOf(currentNode.value.id);
if (nodeIndex !== -1) {
return nodeIndex + 1;
}
// Fallback: count how many steps we've been through
const allNodes = workflowData.value?.nodes || [];
const startIndex = allNodes.findIndex(n => n.type === 'start');
const currentIndex = allNodes.findIndex(n => n.id === currentNode.value.id);
if (startIndex !== -1 && currentIndex !== -1) {
return Math.min(currentIndex - startIndex + 1, allNodes.length);
}
return 1;
});
const totalSteps = computed(() => {
// For workflows with branching, estimate total based on all nodes
const allNodes = workflowData.value?.nodes || [];
const linearPath = workflowPath.value.length;
// If we have a complete linear path, use it
if (linearPath > 0 && allNodes.some(n => n.type === 'end')) {
const hasEndInPath = workflowPath.value.some(nodeId =>
allNodes.find(n => n.id === nodeId)?.type === 'end'
);
if (hasEndInPath) {
return linearPath;
}
}
// Otherwise, estimate based on total nodes (excluding start/end for better UX)
return Math.max(allNodes.filter(n => !['start', 'end'].includes(n.type)).length, 1);
}); });
const totalSteps = computed(() => workflowPath.value.length);
// Computed: Interpolated HTML content for HTML nodes // Computed: Interpolated HTML content for HTML nodes
const interpolatedHtmlContent = computed(() => { const interpolatedHtmlContent = computed(() => {
@ -1789,6 +1980,7 @@ const getWorkflowSubmitButtonStyle = () => {
:is-preview="true" :is-preview="true"
:field-states="fieldStates" :field-states="fieldStates"
@form-data-updated="handleFormDataUpdate" @form-data-updated="handleFormDataUpdate"
@button-clicked="handleButtonClick"
/> />
</template> </template>
<!-- Submit button - respects form builder configuration --> <!-- Submit button - respects form builder configuration -->

View File

@ -0,0 +1,79 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default defineEventHandler(async (event) => {
try {
const formId = event.context.params.id;
if (!formId) {
throw createError({
statusCode: 400,
statusMessage: 'Form ID is required'
});
}
// Fetch the form from database
const form = await prisma.form.findFirst({
where: {
OR: [
{ formID: parseInt(formId) || -1 },
{ formUUID: formId }
]
},
select: {
formID: true,
formUUID: true,
formName: true,
formComponents: true
}
});
if (!form) {
throw createError({
statusCode: 404,
statusMessage: 'Form not found'
});
}
// Extract form buttons from the form components
const formButtons = [];
if (form.formComponents && Array.isArray(form.formComponents)) {
form.formComponents.forEach(component => {
// Only include button components that have names
if (component.type === 'button' && component.props && component.props.name) {
formButtons.push({
name: component.props.name,
label: component.props.label || component.props.name,
buttonText: component.props.buttonText || component.props.label || 'Button',
buttonType: component.props.buttonType || 'button',
variant: component.props.variant || 'primary',
disabled: component.props.disabled || false,
description: component.props.help || `${component.props.buttonType || 'button'} button`
});
}
});
}
return {
success: true,
formId: form.formID,
formUuid: form.formUUID,
formName: form.formName,
buttons: formButtons
};
} catch (error) {
console.error('Error fetching form buttons:', error);
if (error.statusCode) {
throw error;
}
throw createError({
statusCode: 500,
statusMessage: 'Internal server error while fetching form buttons'
});
}
});