Enhance Form Builder with Repeating Group Functionality and Data Management
- Added support for initializing and managing repeating groups in ComponentPreview.vue, allowing for dynamic creation of group items based on minimum item settings. - Implemented event emission for form data updates to notify parent components, ensuring synchronization with FormKit integration. - Enhanced FormBuilderFieldSettingsModal.vue with new configuration options for repeating tables, including minimum and maximum records, button text settings, and table behavior controls. - Updated safeGetField.js to ensure backward compatibility by returning empty strings for undefined or null values. - Improved workflow page to handle form data updates from ComponentPreview, maintaining data consistency across the application. - Enhanced form store logic to initialize preview form data for repeating groups upon component addition and form loading.
This commit is contained in:
parent
a2b707eb6e
commit
3abaf7afe5
@ -547,7 +547,7 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select-nested-component']);
|
||||
const emit = defineEmits(['select-nested-component', 'form-data-updated']);
|
||||
|
||||
// Get access to the form builder store
|
||||
const formStore = useFormBuilderStore();
|
||||
@ -606,6 +606,37 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize repeating groups
|
||||
if (props.component.type === 'repeating-group') {
|
||||
const groupName = props.component.props.name;
|
||||
if (groupName) {
|
||||
// Get current groups or initialize empty array
|
||||
const currentGroups = safeGetField(groupName, formStore.previewFormData);
|
||||
|
||||
// If no groups exist and minItems is specified, create initial groups
|
||||
if ((!currentGroups || currentGroups.length === 0) && props.component.props.minItems > 0) {
|
||||
const initialGroups = [];
|
||||
const minItems = props.component.props.minItems || 1;
|
||||
|
||||
for (let i = 0; i < minItems; i++) {
|
||||
const newGroup = {};
|
||||
|
||||
// Add fields from configuration
|
||||
if (props.component.props.fields) {
|
||||
props.component.props.fields.forEach(field => {
|
||||
newGroup[field.name] = '';
|
||||
});
|
||||
}
|
||||
|
||||
initialGroups.push(newGroup);
|
||||
}
|
||||
|
||||
const updatedData = { ...formStore.previewFormData, [groupName]: initialGroups };
|
||||
formStore.updatePreviewFormData(updatedData);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for changes to component props, especially defaultItems
|
||||
@ -622,6 +653,39 @@ watch(() => props.component.props.defaultItems, (newDefaultItems, oldDefaultItem
|
||||
}
|
||||
}, { deep: true, immediate: true });
|
||||
|
||||
// Watch for changes to repeating group props
|
||||
watch(() => props.component.props.minItems, (newMinItems, oldMinItems) => {
|
||||
if (props.component.type === 'repeating-group') {
|
||||
const groupName = props.component.props.name;
|
||||
if (!groupName) return;
|
||||
|
||||
const currentGroups = safeGetField(groupName, formStore.previewFormData);
|
||||
const minItems = newMinItems || 1;
|
||||
|
||||
// If current groups are fewer than minItems, add missing groups
|
||||
if ((!currentGroups || currentGroups.length < minItems) && minItems > 0) {
|
||||
const groupsToAdd = minItems - (currentGroups?.length || 0);
|
||||
const updatedGroups = [...(currentGroups || [])];
|
||||
|
||||
for (let i = 0; i < groupsToAdd; i++) {
|
||||
const newGroup = {};
|
||||
|
||||
// Add fields from configuration
|
||||
if (props.component.props.fields) {
|
||||
props.component.props.fields.forEach(field => {
|
||||
newGroup[field.name] = '';
|
||||
});
|
||||
}
|
||||
|
||||
updatedGroups.push(newGroup);
|
||||
}
|
||||
|
||||
const updatedData = { ...formStore.previewFormData, [groupName]: updatedGroups };
|
||||
formStore.updatePreviewFormData(updatedData);
|
||||
}
|
||||
}
|
||||
}, { deep: true, immediate: true });
|
||||
|
||||
// Repeating group and dynamic list functionality
|
||||
const addGroupItem = () => {
|
||||
if (props.isPreview) return;
|
||||
@ -648,6 +712,9 @@ const addGroupItem = () => {
|
||||
// Update the form data
|
||||
const updatedData = { ...formStore.previewFormData, [groupName]: currentGroups };
|
||||
formStore.updatePreviewFormData(updatedData);
|
||||
|
||||
// Also emit an event to notify parent components about the data change
|
||||
emit('form-data-updated', updatedData);
|
||||
};
|
||||
|
||||
const removeGroupItem = (index) => {
|
||||
@ -659,12 +726,22 @@ const removeGroupItem = (index) => {
|
||||
// Get current groups
|
||||
const currentGroups = [...(safeGetField(groupName, formStore.previewFormData) || [])];
|
||||
|
||||
// Check if we can remove this item (respect minimum items)
|
||||
const minItems = props.component.props.minItems || 1;
|
||||
if (currentGroups.length <= minItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the group at the specified index
|
||||
currentGroups.splice(index, 1);
|
||||
|
||||
// Update the form data
|
||||
const updatedData = { ...formStore.previewFormData, [groupName]: currentGroups };
|
||||
formStore.updatePreviewFormData(updatedData);
|
||||
|
||||
// Also emit an event to notify parent components about the data change
|
||||
// This is important for FormKit integration
|
||||
emit('form-data-updated', updatedData);
|
||||
};
|
||||
|
||||
const addListItem = () => {
|
||||
|
@ -939,6 +939,255 @@ if (name && email) {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Data Table Configuration -->
|
||||
<template v-if="component.type === 'repeating-table'">
|
||||
<div class="space-y-6">
|
||||
<!-- Table Limits -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
type="number"
|
||||
label="Minimum Records"
|
||||
name="minRecords"
|
||||
v-model="configModel.minRecords"
|
||||
help="Minimum number of records required"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
:min="0"
|
||||
placeholder="0"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="number"
|
||||
label="Maximum Records"
|
||||
name="maxRecords"
|
||||
v-model="configModel.maxRecords"
|
||||
help="Maximum number of records allowed"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
:min="1"
|
||||
placeholder="50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Button Text Settings -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Add Button Text"
|
||||
name="buttonText"
|
||||
v-model="configModel.buttonText"
|
||||
help="Text for the add record button"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="Add Record"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Edit Button Text"
|
||||
name="editText"
|
||||
v-model="configModel.editText"
|
||||
help="Text for the edit button"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="Edit"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Delete Button Text"
|
||||
name="deleteText"
|
||||
v-model="configModel.deleteText"
|
||||
help="Text for the delete button"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
placeholder="Delete"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Table Behavior Settings -->
|
||||
<div class="space-y-4">
|
||||
<h5 class="text-sm font-medium text-gray-700 border-b pb-2">Table Behavior</h5>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
type="switch"
|
||||
label="Show Row Numbers"
|
||||
name="showRowNumbers"
|
||||
v-model="configModel.showRowNumbers"
|
||||
help="Display row numbers in the table"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="switch"
|
||||
label="Allow Edit"
|
||||
name="allowEdit"
|
||||
v-model="configModel.allowEdit"
|
||||
help="Allow editing existing records"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="switch"
|
||||
label="Allow Delete"
|
||||
name="allowDelete"
|
||||
v-model="configModel.allowDelete"
|
||||
help="Allow deleting records"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="switch"
|
||||
label="Confirm Delete"
|
||||
name="confirmDelete"
|
||||
v-model="configModel.confirmDelete"
|
||||
help="Require confirmation before deleting"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="switch"
|
||||
label="Enable Search"
|
||||
name="enableSearch"
|
||||
v-model="configModel.enableSearch"
|
||||
help="Add search functionality to the table"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="switch"
|
||||
label="Enable Export"
|
||||
name="enableExport"
|
||||
v-model="configModel.enableExport"
|
||||
help="Allow exporting table data"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table Columns Management -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h5 class="text-sm font-medium text-gray-700">Table Columns</h5>
|
||||
<RsButton @click="addTableColumn" variant="primary" size="sm">
|
||||
<Icon name="heroicons:plus" class="w-4 h-4 mr-1" />
|
||||
Add Column
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<div class="border rounded-md p-3 bg-gray-50 space-y-3">
|
||||
<div v-for="(column, index) in (configModel.columns || [])" :key="index" class="border p-3 rounded bg-white">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h4 class="font-medium text-sm text-gray-800">Column {{ index + 1 }}</h4>
|
||||
<button
|
||||
@click="removeTableColumn(index)"
|
||||
class="text-red-500 hover:text-red-700"
|
||||
type="button"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Column Name"
|
||||
v-model="column.name"
|
||||
placeholder="e.g., name, email, phone"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Column Label"
|
||||
v-model="column.label"
|
||||
placeholder="e.g., Name, Email, Phone"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Column Type"
|
||||
v-model="column.type"
|
||||
:options="[
|
||||
{ label: 'Text', value: 'text' },
|
||||
{ label: 'Number', value: 'number' },
|
||||
{ label: 'Email', value: 'email' },
|
||||
{ label: 'Phone', value: 'tel' },
|
||||
{ label: 'Date', value: 'date' },
|
||||
{ label: 'Time', value: 'time' },
|
||||
{ label: 'URL', value: 'url' },
|
||||
{ label: 'Select', value: 'select' },
|
||||
{ label: 'Checkbox', value: 'checkbox' },
|
||||
{ label: 'Text Area', value: 'textarea' }
|
||||
]"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Placeholder"
|
||||
v-model="column.placeholder"
|
||||
placeholder="Enter placeholder text"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Validation Rules"
|
||||
v-model="column.validation"
|
||||
placeholder="e.g., required|email"
|
||||
:classes="{ outer: 'field-wrapper' }"
|
||||
/>
|
||||
|
||||
<div class="flex items-center">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="column.required" class="mr-2" />
|
||||
<span class="text-sm text-gray-700">Required Field</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options for select type -->
|
||||
<div v-if="column.type === 'select'" class="mt-4">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h6 class="text-sm font-medium text-gray-700">Select Options</h6>
|
||||
<button
|
||||
@click="addColumnOption(index)"
|
||||
class="text-blue-600 hover:text-blue-800 text-sm"
|
||||
type="button"
|
||||
>
|
||||
<Icon name="heroicons:plus" class="w-3 h-3 mr-1" />
|
||||
Add Option
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div v-for="(option, optionIndex) in (column.options || [])" :key="optionIndex" class="flex items-center space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
v-model="option.label"
|
||||
placeholder="Option label"
|
||||
class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
v-model="option.value"
|
||||
placeholder="Option value"
|
||||
class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm"
|
||||
/>
|
||||
<button
|
||||
@click="removeColumnOption(index, optionIndex)"
|
||||
class="text-red-500 hover:text-red-700"
|
||||
type="button"
|
||||
>
|
||||
<Icon name="material-symbols:delete-outline" class="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Repeating Group Configuration -->
|
||||
<template v-if="component.type === 'repeating-group'">
|
||||
<div class="space-y-6">
|
||||
|
@ -1,7 +1,12 @@
|
||||
// Utility to safely get a field value from a form data object
|
||||
export function safeGetField(field, formData) {
|
||||
if (formData && Object.prototype.hasOwnProperty.call(formData, field)) {
|
||||
return formData[field] !== undefined && formData[field] !== null ? formData[field] : '';
|
||||
const value = formData[field];
|
||||
// If the value is undefined or null, return empty string for backward compatibility
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// Only warn in development
|
||||
|
@ -1017,6 +1017,14 @@ const handleScriptFieldChange = ({ fieldName, value }) => {
|
||||
formStore.updatePreviewFormData(formData.value);
|
||||
};
|
||||
|
||||
// Handle form data updates from ComponentPreview
|
||||
const handleFormDataUpdate = (updatedData) => {
|
||||
// Update the form data with the new data
|
||||
formData.value = { ...formData.value, ...updatedData };
|
||||
// Also update form store to keep them in sync
|
||||
formStore.updatePreviewFormData(formData.value);
|
||||
};
|
||||
|
||||
// Handle conditional logic script generation
|
||||
const handleConditionalLogicGenerated = (generatedScript) => {
|
||||
console.log('[WorkflowExecution] Conditional logic script generated');
|
||||
@ -1205,7 +1213,38 @@ watch(currentStep, async (newStep) => {
|
||||
// Update form store with form components and data for ComponentPreview
|
||||
if (currentForm.value?.formComponents) {
|
||||
formStore.formComponents = currentForm.value.formComponents;
|
||||
formStore.updatePreviewFormData(formData.value);
|
||||
|
||||
// Initialize repeating groups in form data
|
||||
const updatedFormData = { ...formData.value };
|
||||
currentForm.value.formComponents.forEach(component => {
|
||||
if (component.type === 'repeating-group' && component.props?.name) {
|
||||
const groupName = component.props.name;
|
||||
const minItems = component.props.minItems || 1;
|
||||
|
||||
// If the field doesn't exist or is not an array, initialize it
|
||||
if (!updatedFormData[groupName] || !Array.isArray(updatedFormData[groupName])) {
|
||||
const initialGroups = [];
|
||||
|
||||
for (let i = 0; i < minItems; i++) {
|
||||
const newGroup = {};
|
||||
|
||||
// Add fields from configuration
|
||||
if (component.props.fields) {
|
||||
component.props.fields.forEach(field => {
|
||||
newGroup[field.name] = '';
|
||||
});
|
||||
}
|
||||
|
||||
initialGroups.push(newGroup);
|
||||
}
|
||||
|
||||
updatedFormData[groupName] = initialGroups;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
formData.value = updatedFormData;
|
||||
formStore.updatePreviewFormData(updatedFormData);
|
||||
}
|
||||
}
|
||||
} else if (["decision", "gateway"].includes(currentNode.value.type)) {
|
||||
@ -1693,6 +1732,7 @@ const getWorkflowSubmitButtonStyle = () => {
|
||||
:component="component"
|
||||
:is-preview="false"
|
||||
:field-states="fieldStates"
|
||||
@form-data-updated="handleFormDataUpdate"
|
||||
/>
|
||||
</template>
|
||||
<!-- Submit button - respects form builder configuration -->
|
||||
|
@ -377,6 +377,30 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
};
|
||||
|
||||
this.formComponents.push(newComponent);
|
||||
|
||||
// Initialize preview form data for repeating groups
|
||||
if (newComponent.type === 'repeating-group' && newComponent.props?.name) {
|
||||
const groupName = newComponent.props.name;
|
||||
const minItems = newComponent.props.minItems || 1;
|
||||
|
||||
const initialGroups = [];
|
||||
for (let i = 0; i < minItems; i++) {
|
||||
const newGroup = {};
|
||||
|
||||
// Add fields from configuration
|
||||
if (newComponent.props.fields) {
|
||||
newComponent.props.fields.forEach(field => {
|
||||
newGroup[field.name] = '';
|
||||
});
|
||||
}
|
||||
|
||||
initialGroups.push(newGroup);
|
||||
}
|
||||
|
||||
const updatedFormData = { ...this.previewFormData, [groupName]: initialGroups };
|
||||
this.updatePreviewFormData(updatedFormData);
|
||||
}
|
||||
|
||||
// Explicitly select the new component
|
||||
this.selectedComponentId = newComponentId;
|
||||
this.hasUnsavedChanges = true;
|
||||
@ -801,6 +825,33 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
}));
|
||||
}
|
||||
|
||||
// Initialize preview form data with repeating groups
|
||||
const initialFormData = {};
|
||||
this.formComponents.forEach(component => {
|
||||
if (component.type === 'repeating-group' && component.props?.name) {
|
||||
const groupName = component.props.name;
|
||||
const minItems = component.props.minItems || 1;
|
||||
|
||||
const initialGroups = [];
|
||||
for (let i = 0; i < minItems; i++) {
|
||||
const newGroup = {};
|
||||
|
||||
// Add fields from configuration
|
||||
if (component.props.fields) {
|
||||
component.props.fields.forEach(field => {
|
||||
newGroup[field.name] = '';
|
||||
});
|
||||
}
|
||||
|
||||
initialGroups.push(newGroup);
|
||||
}
|
||||
|
||||
initialFormData[groupName] = initialGroups;
|
||||
}
|
||||
});
|
||||
|
||||
this.updatePreviewFormData(initialFormData);
|
||||
|
||||
// Clear and initialize history when loading a form
|
||||
this.actionHistory = [];
|
||||
this.currentHistoryIndex = -1;
|
||||
@ -876,6 +927,9 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
this.actionHistory = [];
|
||||
this.currentHistoryIndex = -1;
|
||||
|
||||
// Clear preview form data
|
||||
this.updatePreviewFormData({});
|
||||
|
||||
// Record the initial empty state
|
||||
this.recordHistory('new_form', {
|
||||
message: 'Created a new empty form'
|
||||
|
Loading…
x
Reference in New Issue
Block a user