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
|
// 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)
|
||||||
|
@ -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',
|
||||||
|
@ -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 -->
|
||||||
|
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