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
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)

View File

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

View File

@ -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 -->

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'
});
}
});