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:
parent
4425e912ab
commit
649a956063
@ -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
|
||||
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
|
||||
console.log('[ComponentPreview] Button clicked:', props.component.props.name);
|
||||
};
|
||||
|
||||
// Check if the component is a standard FormKit input type (excluding specialized components)
|
||||
|
@ -242,6 +242,127 @@
|
||||
</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>
|
||||
|
||||
<!-- Step 3: Field Conditions (only if form is selected) -->
|
||||
@ -621,9 +742,11 @@ const emit = defineEmits(['update']);
|
||||
// Get the variable store for creating variables
|
||||
const processStore = useProcessBuilderStore();
|
||||
|
||||
// State for form fields
|
||||
// State for form fields and buttons
|
||||
const formFields = ref([]);
|
||||
const formButtons = ref([]);
|
||||
const isLoadingFields = ref(false);
|
||||
const isLoadingButtons = ref(false);
|
||||
|
||||
// Local state for node data - create a deep copy to avoid mutation issues
|
||||
const localNodeData = ref({
|
||||
@ -634,6 +757,7 @@ const localNodeData = ref({
|
||||
formUuid: null,
|
||||
inputMappings: [],
|
||||
outputMappings: [],
|
||||
buttonMappings: [],
|
||||
fieldConditions: [],
|
||||
assignmentType: 'public',
|
||||
assignedUsers: [],
|
||||
@ -662,6 +786,9 @@ watch(() => props.nodeData, async (newNodeData) => {
|
||||
outputMappings: Array.isArray(newNodeData.outputMappings)
|
||||
? newNodeData.outputMappings.map(mapping => ({ ...mapping }))
|
||||
: [],
|
||||
buttonMappings: Array.isArray(newNodeData.buttonMappings)
|
||||
? newNodeData.buttonMappings.map(mapping => ({ ...mapping }))
|
||||
: [],
|
||||
fieldConditions: Array.isArray(newNodeData.fieldConditions)
|
||||
? newNodeData.fieldConditions.map(condition => ({ ...condition }))
|
||||
: [],
|
||||
@ -672,9 +799,12 @@ watch(() => props.nodeData, async (newNodeData) => {
|
||||
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) {
|
||||
await loadFormFields(newNodeData.formId || newNodeData.formUuid);
|
||||
await Promise.all([
|
||||
loadFormFields(newNodeData.formId || newNodeData.formUuid),
|
||||
loadFormButtons(newNodeData.formId || newNodeData.formUuid)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
@ -690,6 +820,7 @@ async function handleFormSelection(form) {
|
||||
formUuid: form.formUUID,
|
||||
label: form.formName || 'Form Task',
|
||||
description: `Form: ${form.formName}`,
|
||||
buttonMappings: [],
|
||||
fieldConditions: [],
|
||||
assignmentType: 'public',
|
||||
assignedUsers: [],
|
||||
@ -698,8 +829,11 @@ async function handleFormSelection(form) {
|
||||
assignmentVariableType: 'user_id'
|
||||
};
|
||||
|
||||
// Load form fields for this form
|
||||
await loadFormFields(form.formID || form.formUUID);
|
||||
// Load form fields and buttons for this form
|
||||
await Promise.all([
|
||||
loadFormFields(form.formID || form.formUUID),
|
||||
loadFormButtons(form.formID || form.formUUID)
|
||||
]);
|
||||
|
||||
saveChanges();
|
||||
}
|
||||
@ -715,6 +849,7 @@ function clearFormSelection() {
|
||||
description: 'Form submission task',
|
||||
inputMappings: [],
|
||||
outputMappings: [],
|
||||
buttonMappings: [],
|
||||
fieldConditions: [],
|
||||
assignmentType: 'public',
|
||||
assignedUsers: [],
|
||||
@ -723,8 +858,9 @@ function clearFormSelection() {
|
||||
assignmentVariableType: 'user_id'
|
||||
};
|
||||
|
||||
// Clear form fields
|
||||
// Clear form fields and buttons
|
||||
formFields.value = [];
|
||||
formButtons.value = [];
|
||||
|
||||
saveChanges();
|
||||
}
|
||||
@ -769,6 +905,28 @@ function removeOutputMapping(index) {
|
||||
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
|
||||
function getVariableNameFromFormField(formField) {
|
||||
// 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)
|
||||
const formFieldOptions = computed(() => {
|
||||
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)
|
||||
const processVariableOptions = computed(() => {
|
||||
return props.availableVariables.map(variable => ({
|
||||
@ -946,6 +1140,8 @@ function saveChanges() {
|
||||
localNodeData.value.inputMappings.map(mapping => ({ ...mapping })) : [],
|
||||
outputMappings: localNodeData.value.outputMappings ?
|
||||
localNodeData.value.outputMappings.map(mapping => ({ ...mapping })) : [],
|
||||
buttonMappings: localNodeData.value.buttonMappings ?
|
||||
localNodeData.value.buttonMappings.map(mapping => ({ ...mapping })) : [],
|
||||
fieldConditions: localNodeData.value.fieldConditions ?
|
||||
localNodeData.value.fieldConditions.map(condition => ({ ...condition })) : [],
|
||||
assignmentType: localNodeData.value.assignmentType || 'public',
|
||||
|
@ -45,6 +45,7 @@ const tasks = ref([]);
|
||||
const conditionalLogicScript = ref('');
|
||||
const combinedScript = ref('');
|
||||
const fieldStates = ref({});
|
||||
const visitedNodesInSession = ref(new Set());
|
||||
|
||||
// Get process ID from route
|
||||
const processId = computed(() => route.params.id);
|
||||
@ -195,6 +196,9 @@ const loadProcess = async () => {
|
||||
// Capture URL parameters for variables configured to do so
|
||||
captureUrlParameters();
|
||||
|
||||
// Reset visited nodes tracking for new process execution
|
||||
visitedNodesInSession.value.clear();
|
||||
|
||||
// Start the process execution (case instance)
|
||||
await startProcessExecution();
|
||||
|
||||
@ -260,24 +264,59 @@ function getNextNodeIdForDecision(currentNodeId) {
|
||||
const currentNodeObj = workflowData.value.nodes.find(n => n.id === 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)
|
||||
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
|
||||
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 no conditions match, look for default path
|
||||
const defaultEdge = outgoingEdges.find(e => e.data?.isDefault || e.label?.toLowerCase().includes('default'));
|
||||
if (defaultEdge) return defaultEdge.target;
|
||||
let defaultEdge = null;
|
||||
|
||||
// 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
|
||||
console.log('[Gateway Debug] Fallback to first edge:', outgoingEdges[0]);
|
||||
return outgoingEdges[0]?.target || null;
|
||||
}
|
||||
|
||||
@ -464,13 +503,23 @@ function evaluateCondition(condition, variables) {
|
||||
|
||||
// Move to a specific node by its ID
|
||||
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);
|
||||
if (nextIndex !== -1) {
|
||||
visitedNodesInSession.value.add(targetNodeId);
|
||||
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;
|
||||
}
|
||||
console.warn('[Workflow] Target node not found:', targetNodeId);
|
||||
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
|
||||
function applyFieldConditions(nodeData, processVars) {
|
||||
const { fieldConditions = [] } = nodeData;
|
||||
@ -1025,6 +1107,33 @@ const handleFormDataUpdate = (updatedData) => {
|
||||
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
|
||||
const handleConditionalLogicGenerated = (generatedScript) => {
|
||||
console.log('[WorkflowExecution] Conditional logic script generated');
|
||||
@ -1183,6 +1292,8 @@ watch(currentStep, async (newStep) => {
|
||||
if (currentNode.value) {
|
||||
stepLoading.value = true; // Start loading for any node
|
||||
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)) {
|
||||
await executeCurrentStep();
|
||||
} else if (currentNode.value.type === "form") {
|
||||
@ -1304,9 +1415,13 @@ watch(currentStep, async (newStep) => {
|
||||
}
|
||||
}
|
||||
} else if (["decision", "gateway"].includes(currentNode.value.type)) {
|
||||
console.log('[Workflow] Processing gateway/decision node');
|
||||
await executeDecisionNode();
|
||||
}
|
||||
// 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 {
|
||||
stepLoading.value = false; // End loading after all async work
|
||||
}
|
||||
@ -1349,21 +1464,36 @@ const executeDecisionNode = async () => {
|
||||
|
||||
// Debug: Log process variables before evaluating gateway
|
||||
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') {
|
||||
// Automatic decision based on conditions
|
||||
const nextNodeId = getNextNodeIdForDecision(currentNodeObj.id);
|
||||
console.log('[Gateway Debug] Next node ID from decision:', nextNodeId);
|
||||
|
||||
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}`);
|
||||
|
||||
// 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
|
||||
} catch (err) {
|
||||
console.error('[Workflow] Error executing decision:', err);
|
||||
error.value = 'Failed to execute decision';
|
||||
error.value = 'Failed to execute decision: ' + (err.message || err);
|
||||
} finally {
|
||||
stepLoading.value = false;
|
||||
}
|
||||
@ -1397,18 +1527,44 @@ function computeWorkflowPath() {
|
||||
const path = [];
|
||||
const nodes = workflowData.value?.nodes || [];
|
||||
const edges = workflowData.value?.edges || [];
|
||||
|
||||
if (!nodes.length) return path;
|
||||
|
||||
const visitedNodes = new Set();
|
||||
let node = nodes.find(n => n.type === 'start');
|
||||
while (node) {
|
||||
|
||||
while (node && !visitedNodes.has(node.id)) {
|
||||
visitedNodes.add(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);
|
||||
if (outgoingEdges.length > 1) {
|
||||
console.warn('[Workflow] Multiple outgoing edges found for node', node.id, outgoingEdges);
|
||||
// For progress, just follow the first edge for now
|
||||
|
||||
// Handle different cases for outgoing edges
|
||||
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;
|
||||
}
|
||||
|
||||
@ -1423,9 +1579,44 @@ watch(
|
||||
);
|
||||
|
||||
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
|
||||
const interpolatedHtmlContent = computed(() => {
|
||||
@ -1789,6 +1980,7 @@ const getWorkflowSubmitButtonStyle = () => {
|
||||
:is-preview="true"
|
||||
:field-states="fieldStates"
|
||||
@form-data-updated="handleFormDataUpdate"
|
||||
@button-clicked="handleButtonClick"
|
||||
/>
|
||||
</template>
|
||||
<!-- Submit button - respects form builder configuration -->
|
||||
|
79
server/api/forms/[id]/buttons.get.js
Normal file
79
server/api/forms/[id]/buttons.get.js
Normal 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'
|
||||
});
|
||||
}
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user