Enhance ComponentPreview and RepeatingTable Functionality

- Updated ComponentPreview.vue to improve the handling of repeating groups, including the addition of helper functions for field value retrieval and input handling.
- Enhanced the rendering of repeating groups with better structure and improved user experience, including dynamic item addition and removal.
- Modified RepeatingTable.vue to increase the maximum visible columns from 20 to 50, allowing for better data presentation and horizontal scrolling.
- Improved column width calculations and added a minimum table width to ensure proper layout and usability.
- Updated safeGetField.js to allow for optional warning suppression, enhancing flexibility in data access without unnecessary console warnings.
- Refined styles across components for better visual consistency and usability.
This commit is contained in:
Afiq 2025-08-07 14:17:51 +08:00
parent a14252a844
commit d0cef85b72
4 changed files with 480 additions and 227 deletions

View File

@ -23,12 +23,14 @@
</div> </div>
<!-- Basic Input Types (including radio and checkbox) --> <!-- Basic Input Types (including radio and checkbox) -->
<FormKit v-else-if="isInputType" :id="`preview-${component.id}`" :type="component.type" :name="component.props.name" <FormKit v-else-if="isInputType" :id="`preview-${component.id}`" :type="component.type"
:name="getFieldName(component)"
:label="component.props.label" :help="component.props.help" :placeholder="component.props.placeholder" :label="component.props.label" :help="component.props.help" :placeholder="component.props.placeholder"
:validation="component.props.validation" :validation-visibility="isPreview ? 'live' : 'blur'" :validation="component.props.validation" :validation-visibility="isPreview ? 'live' : 'blur'"
:readonly="component.props.readonly || !isPreview" :readonly="component.props.readonly || !isPreview"
:disabled="!isPreview || (component.props.readonly && ['select', 'searchSelect', 'checkbox', 'radio', 'switch'].includes(component.type))" :disabled="!isPreview || (component.props.readonly && ['select', 'searchSelect', 'checkbox', 'radio', 'switch'].includes(component.type))"
:options="component.props.options || undefined" :value="component.props.value || undefined" :options="component.props.options || undefined"
:value="getFieldValue(component)"
:accept="component.props.accept || undefined" :max="component.props.max || undefined" :accept="component.props.accept || undefined" :max="component.props.max || undefined"
:mask="component.props.mask || undefined" :digits="component.props.digits || undefined" :mask="component.props.mask || undefined" :digits="component.props.digits || undefined"
:multiple="component.props.multiple || undefined" :maxSize="component.props.maxSize || undefined" :multiple="component.props.multiple || undefined" :maxSize="component.props.maxSize || undefined"
@ -42,7 +44,9 @@
'readonly-checkbox': component.props.readonly && component.type === 'checkbox', 'readonly-checkbox': component.props.readonly && component.type === 'checkbox',
'readonly-radio': component.props.readonly && component.type === 'radio', 'readonly-radio': component.props.readonly && component.type === 'radio',
'readonly-switch': component.props.readonly && component.type === 'switch' 'readonly-switch': component.props.readonly && component.type === 'switch'
}" /> }"
@input="handleFieldInput(component, $event)"
@change="handleFieldChange(component, $event)" />
<!-- Heading --> <!-- Heading -->
<div v-else-if="component.type === 'heading'" class="py-2"> <div v-else-if="component.type === 'heading'" class="py-2">
@ -228,35 +232,59 @@
<!-- Preview mode - show functional repeating groups --> <!-- Preview mode - show functional repeating groups -->
<div v-else class="repeating-groups space-y-4"> <div v-else class="repeating-groups space-y-4">
<div v-for="(group, groupIndex) in (safeGetField(component.props.name, previewFormData) || [])" :key="groupIndex"
class="group-item border border-gray-200 rounded-md p-3 bg-gray-50">
<!-- Group items -->
<div v-for="(group, groupIndex) in (safeGetField(component.props.name, previewFormData, { warn: false }) || [])"
:key="`${component.props.name}-${groupIndex}`"
class="group-item border border-gray-200 rounded-md p-3 bg-gray-50 mb-4">
<!-- Item header -->
<div class="flex justify-between items-center mb-3"> <div class="flex justify-between items-center mb-3">
<h4 class="text-sm font-medium text-gray-700">Item {{ groupIndex + 1 }}</h4> <h4 class="text-sm font-medium text-gray-700">Item {{ groupIndex + 1 }}</h4>
<button v-if="(safeGetField(component.props.name, previewFormData)?.length || 0) > (component.props.minItems || 1)" <button v-if="(safeGetField(component.props.name, previewFormData, { warn: false }) || []).length > (component.props.minItems || 1)"
type="button" class="text-red-500 hover:text-red-700 text-sm" @click="removeGroupItem(groupIndex)"> type="button"
{{ component.props.removeText || 'Remove' }} class="text-red-500 hover:text-red-700 text-sm"
@click="removeRepeatingGroupItem(component.props.name, groupIndex)">
{{ component.props.removeText || 'Buang' }}
</button> </button>
</div> </div>
<!-- Fields grid -->
<div class="grid grid-cols-12 gap-2"> <div class="grid grid-cols-12 gap-2">
<!-- Render children components for each group item --> <div v-for="(child, childIndex) in component.props.children"
<template v-for="(child, childIndex) in component.props.children" :key="childIndex"> :key="`field-${groupIndex}-${childIndex}`"
<div class="form-component"
class="form-component" :style="{ gridColumn: child.props.gridColumn || 'span 6' }">
:style="{
gridColumn: child.props.gridColumn || 'span 6' <!-- Render FormKit field -->
}" <FormKit
> :type="child.type"
<component-preview :component="child" :is-preview="true" /> :name="`${component.props.name}[${groupIndex}].${child.props.name}`"
</div> :label="child.props.label"
</template> :placeholder="child.props.placeholder"
:help="child.props.help"
:options="child.props.options"
:validation="child.props.validation"
:rows="child.type === 'textarea' ? (child.props.rows || 3) : undefined"
:model-value="getRepeatingGroupFieldValue(group, child.props.name, child.type)"
@update:model-value="updateRepeatingGroupField(component.props.name, groupIndex, child.props.name, $event)"
/>
</div>
</div> </div>
</div> </div>
<button v-if="(safeGetField(component.props.name, previewFormData)?.length || 0) < (component.props.maxItems || 10)" <!-- Empty state -->
type="button" <div v-if="(safeGetField(component.props.name, previewFormData, { warn: false }) || []).length === 0"
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm" class="text-center py-8 text-gray-500">
@click="addGroupItem"> <p>No items yet. Click "{{ component.props.buttonText || 'Add Item' }}" to add the first item.</p>
</div>
<!-- Add button -->
<button v-if="(safeGetField(component.props.name, previewFormData, { warn: false }) || []).length < (component.props.maxItems || 10)"
type="button"
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
@click="addRepeatingGroupItem(component.props.name, component.props.children)">
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" /> <Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
{{ component.props.buttonText || 'Add Item' }} {{ component.props.buttonText || 'Add Item' }}
</button> </button>
@ -270,7 +298,7 @@
<label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-1"> <label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-1">
{{ component.props.label }} {{ component.props.label }}
<span v-if="component.props.showItemCounter" class="text-xs text-gray-500 ml-2"> <span v-if="component.props.showItemCounter" class="text-xs text-gray-500 ml-2">
({{ (safeGetField(component.props.name, previewFormData) || []).length }}/{{ component.props.maxItems || 20 }}) ({{ (safeGetField(component.props.name, previewFormData, { warn: false }) || []).length }}/{{ component.props.maxItems || 20 }})
</span> </span>
</label> </label>
@ -289,7 +317,7 @@
</div> </div>
<!-- Bulk Operations Toolbar (if enabled) --> <!-- Bulk Operations Toolbar (if enabled) -->
<div v-if="component.props.bulkOperations && (safeGetField(component.props.name, previewFormData) || []).length > 0" <div v-if="component.props.bulkOperations && (safeGetField(component.props.name, previewFormData, { warn: false }) || []).length > 0"
class="mb-3 flex items-center justify-between bg-gray-50 p-2 rounded"> class="mb-3 flex items-center justify-between bg-gray-50 p-2 rounded">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<input type="checkbox" :checked="isAllSelected(component.props.name)" <input type="checkbox" :checked="isAllSelected(component.props.name)"
@ -343,7 +371,7 @@
@change="toggleItemSelection(component.props.name, index)" @change="toggleItemSelection(component.props.name, index)"
class="mr-2 h-4 w-4 rounded border-gray-300" /> class="mr-2 h-4 w-4 rounded border-gray-300" />
<input :type="component.props.itemType || 'text'" <input :type="component.props.itemType || 'text'"
:value="safeGetField(component.props.name, previewFormData)[index]" :value="safeGetField(component.props.name, previewFormData, { warn: false })[index]"
:placeholder="component.props.placeholder" :placeholder="component.props.placeholder"
:class="getItemInputClasses(component.props.name, index, item)" :class="getItemInputClasses(component.props.name, index, item)"
@blur="validateItem(component.props.name, index, item)" @blur="validateItem(component.props.name, index, item)"
@ -352,7 +380,7 @@
class="ml-2 cursor-move text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity"> class="ml-2 cursor-move text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity">
<Icon name="material-symbols:drag-indicator" class="w-5 h-5" /> <Icon name="material-symbols:drag-indicator" class="w-5 h-5" />
</div> </div>
<button v-if="(safeGetField(component.props.name, previewFormData)?.length || 0) > (component.props.minItems || 0)" <button v-if="(safeGetField(component.props.name, previewFormData, { warn: false })?.length || 0) > (component.props.minItems || 0)"
type="button" class="ml-2 text-red-500 hover:text-red-700" @click="removeListItem(index)"> type="button" class="ml-2 text-red-500 hover:text-red-700" @click="removeListItem(index)">
<Icon name="material-symbols:delete-outline" class="w-5 h-5" /> <Icon name="material-symbols:delete-outline" class="w-5 h-5" />
</button> </button>
@ -363,7 +391,7 @@
{{ validationErrors[component.props.name] }} {{ validationErrors[component.props.name] }}
</div> </div>
<button v-if="(safeGetField(component.props.name, previewFormData)?.length || 0) < (component.props.maxItems || 20)" <button v-if="(safeGetField(component.props.name, previewFormData, { warn: false })?.length || 0) < (component.props.maxItems || 20)"
type="button" type="button"
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm" class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm"
@click="addListItem"> @click="addListItem">
@ -372,7 +400,7 @@
</button> </button>
<!-- Export Button (if enabled) --> <!-- Export Button (if enabled) -->
<div v-if="component.props.exportFormat && (safeGetField(component.props.name, previewFormData) || []).length > 0" <div v-if="component.props.exportFormat && (safeGetField(component.props.name, previewFormData, { warn: false }) || []).length > 0"
class="mt-2"> class="mt-2">
<button @click="exportItems(component.props.name, component.props.exportFormat)" <button @click="exportItems(component.props.name, component.props.exportFormat)"
class="text-xs text-green-600 hover:text-green-800 flex items-center"> class="text-xs text-green-600 hover:text-green-800 flex items-center">
@ -387,12 +415,14 @@
<!-- Repeating Table Component --> <!-- Repeating Table Component -->
<div v-else-if="component.type === 'repeating-table'" class="repeating-table-wrapper"> <div v-else-if="component.type === 'repeating-table'" class="repeating-table-wrapper">
<RepeatingTable <div class="table-container-wrapper">
:config="component.props" <RepeatingTable
:model-value="getTableData(component.props.name)" :config="component.props"
:is-preview="isPreview" :model-value="getTableData(component.props.name)"
@update:model-value="updateTableData" :is-preview="isPreview"
/> @update:model-value="updateTableData"
/>
</div>
</div> </div>
<!-- Button Component --> <!-- Button Component -->
@ -679,9 +709,11 @@ import { useNuxtApp } from '#app';
import { useFormBuilderStore } from '~/stores/formBuilder'; import { useFormBuilderStore } from '~/stores/formBuilder';
import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettingsModal.vue'; import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettingsModal.vue';
import { safeGetField } from '~/composables/safeGetField'; import { safeGetField } from '~/composables/safeGetField';
import { onMounted, onUnmounted, watch, computed, nextTick } from 'vue'; import { onMounted, onUnmounted, watch, computed, nextTick, triggerRef, defineComponent, h } from 'vue';
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
// RepeatingGroupContainer component removed - using template approach instead
const props = defineProps({ const props = defineProps({
component: { component: {
type: Object, type: Object,
@ -703,11 +735,20 @@ const emit = defineEmits(['select-nested-component', 'form-data-updated']);
const formStore = useFormBuilderStore(); const formStore = useFormBuilderStore();
const previewFormData = computed(() => formStore.previewFormData || {}); const previewFormData = computed(() => formStore.previewFormData || {});
// Clean up: Debug watcher removed - repeating groups working correctly
// Enhanced dynamic list functionality // Enhanced dynamic list functionality
const searchQuery = ref({}); const searchQuery = ref({});
const selectedItems = ref({}); const selectedItems = ref({});
const validationErrors = ref({}); const validationErrors = ref({});
// Force reactivity for repeating groups
const groupUpdateCounter = ref(0);
// Simplified reactive variables
const isInRepeatingGroup = ref(false);
const repeatingGroupContext = ref(null);
// New reactive state for form sections // New reactive state for form sections
const sectionDropStates = ref({}); const sectionDropStates = ref({});
@ -750,7 +791,7 @@ onMounted(() => {
validationErrors.value[listName] = ''; validationErrors.value[listName] = '';
// Initialize form data with default items if they exist and form data is empty // Initialize form data with default items if they exist and form data is empty
const currentFormData = safeGetField(listName, formStore.previewFormData); const currentFormData = safeGetField(listName, formStore.previewFormData, { warn: false });
const defaultItems = props.component.props.defaultItems; const defaultItems = props.component.props.defaultItems;
if ((!currentFormData || currentFormData.length === 0) && defaultItems && defaultItems.length > 0) { if ((!currentFormData || currentFormData.length === 0) && defaultItems && defaultItems.length > 0) {
@ -767,7 +808,7 @@ onMounted(() => {
const groupName = props.component.props.name; const groupName = props.component.props.name;
if (groupName) { if (groupName) {
// Get current groups or initialize empty array // Get current groups or initialize empty array
const currentGroups = safeGetField(groupName, formStore.previewFormData); const currentGroups = safeGetField(groupName, formStore.previewFormData, { warn: false });
// If no groups exist and minItems is specified, create initial groups // If no groups exist and minItems is specified, create initial groups
if ((!currentGroups || currentGroups.length === 0) && props.component.props.minItems > 0) { if ((!currentGroups || currentGroups.length === 0) && props.component.props.minItems > 0) {
@ -794,6 +835,26 @@ onMounted(() => {
} }
} }
} }
// Initialize repeating tables
if (props.component.type === 'repeating-table') {
const tableName = props.component.props.name;
if (tableName) {
// Get current table data or initialize empty array
const currentData = safeGetField(tableName, formStore.previewFormData, { warn: false });
// If no data exists, initialize it
if (!currentData || !Array.isArray(currentData)) {
const initialData = Array.isArray(props.component.props.defaultData) ? [...props.component.props.defaultData] : [];
nextTick(() => {
const updatedData = { ...formStore.previewFormData, [tableName]: initialData };
formStore.updatePreviewFormData(updatedData);
console.log('[ComponentPreview] Initialized repeating table data:', tableName, initialData);
});
}
}
}
}); });
// Watch for changes to component props, especially defaultItems // Watch for changes to component props, especially defaultItems
@ -824,7 +885,7 @@ watch(() => props.component.props.minItems, (newMinItems, oldMinItems) => {
const groupName = props.component.props.name; const groupName = props.component.props.name;
if (!groupName || newMinItems === oldMinItems) return; if (!groupName || newMinItems === oldMinItems) return;
const currentGroups = safeGetField(groupName, formStore.previewFormData); const currentGroups = safeGetField(groupName, formStore.previewFormData, { warn: false });
const minItems = newMinItems || 1; const minItems = newMinItems || 1;
// If current groups are fewer than minItems, add missing groups // If current groups are fewer than minItems, add missing groups
@ -853,29 +914,12 @@ watch(() => props.component.props.minItems, (newMinItems, oldMinItems) => {
} }
}, { deep: true, immediate: true }); }, { deep: true, immediate: true });
// Controlled update methods to prevent circular reactivity // Legacy updateGroupField removed - now handled by RepeatingGroupContainer
const updateGroupField = (groupName, groupIndex, fieldName, newValue) => {
if (!props.isPreview) return;
const currentGroups = [...(safeGetField(groupName, formStore.previewFormData) || [])];
if (!currentGroups[groupIndex]) return;
// Only update if value actually changed
if (currentGroups[groupIndex][fieldName] === newValue) return;
currentGroups[groupIndex][fieldName] = newValue;
nextTick(() => {
const updatedData = { ...formStore.previewFormData, [groupName]: currentGroups };
formStore.updatePreviewFormData(updatedData);
emit('form-data-updated', updatedData);
});
};
const updateListItem = (listName, index, newValue) => { const updateListItem = (listName, index, newValue) => {
if (!props.isPreview) return; if (!props.isPreview) return;
const currentItems = [...(safeGetField(listName, formStore.previewFormData) || [])]; const currentItems = [...(safeGetField(listName, formStore.previewFormData, { warn: false }) || [])];
if (currentItems[index] === newValue) return; // No change if (currentItems[index] === newValue) return; // No change
// Validate and handle duplicates // Validate and handle duplicates
@ -890,63 +934,7 @@ const updateListItem = (listName, index, newValue) => {
}); });
}; };
// Repeating group and dynamic list functionality // Legacy functions removed - now handled by RepeatingGroupContainer
const addGroupItem = () => {
if (!props.isPreview) return;
const groupName = props.component.props.name;
if (!groupName) return;
// Get current groups or initialize empty array
const currentGroups = safeGetField(groupName, formStore.previewFormData) || [];
// Create a new empty group
const newGroup = {};
// Add fields from configuration
if (props.component.props.fields) {
props.component.props.fields.forEach(field => {
newGroup[field.name] = '';
});
}
// Add the new group to the list
currentGroups.push(newGroup);
// 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) => {
if (!props.isPreview) return;
const groupName = props.component.props.name;
if (!groupName) return;
// 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 = () => { const addListItem = () => {
if (!props.isPreview) return; if (!props.isPreview) return;
@ -955,7 +943,7 @@ const addListItem = () => {
if (!listName) return; if (!listName) return;
// Get current items or initialize empty array // Get current items or initialize empty array
const currentItems = [...(safeGetField(listName, formStore.previewFormData) || [])]; const currentItems = [...(safeGetField(listName, formStore.previewFormData, { warn: false }) || [])];
// Add an empty item // Add an empty item
currentItems.push(''); currentItems.push('');
@ -972,7 +960,7 @@ const removeListItem = (index) => {
if (!listName) return; if (!listName) return;
// Get current items // Get current items
const currentItems = [...(safeGetField(listName, formStore.previewFormData) || [])]; const currentItems = [...(safeGetField(listName, formStore.previewFormData, { warn: false }) || [])];
// Remove the item at the specified index // Remove the item at the specified index
currentItems.splice(index, 1); currentItems.splice(index, 1);
@ -1041,7 +1029,7 @@ const validateItem = (listName, index, value) => {
const checkDuplicates = (listName, newValue, currentIndex) => { const checkDuplicates = (listName, newValue, currentIndex) => {
if (props.component.props.allowDuplicates) return true; if (props.component.props.allowDuplicates) return true;
const currentItems = safeGetField(listName, formStore.previewFormData) || []; const currentItems = safeGetField(listName, formStore.previewFormData, { warn: false }) || [];
const duplicateIndex = currentItems.findIndex((item, index) => const duplicateIndex = currentItems.findIndex((item, index) =>
index !== currentIndex && item.toLowerCase() === newValue.toLowerCase() index !== currentIndex && item.toLowerCase() === newValue.toLowerCase()
); );
@ -1080,7 +1068,7 @@ const getItemInputClasses = (listName, index, value) => {
// Filter items based on search query // Filter items based on search query
const getFilteredItems = (listName) => { const getFilteredItems = (listName) => {
const items = safeGetField(listName, formStore.previewFormData) || []; const items = safeGetField(listName, formStore.previewFormData, { warn: false }) || [];
const query = searchQuery.value[listName]; const query = searchQuery.value[listName];
if (!query || !props.component.props.enableSearch) { if (!query || !props.component.props.enableSearch) {
@ -1113,13 +1101,13 @@ const toggleItemSelection = (listName, index) => {
}; };
const isAllSelected = (listName) => { const isAllSelected = (listName) => {
const items = safeGetField(listName, formStore.previewFormData) || []; const items = safeGetField(listName, formStore.previewFormData, { warn: false }) || [];
const selected = selectedItems.value[listName] || []; const selected = selectedItems.value[listName] || [];
return items.length > 0 && selected.length === items.length; return items.length > 0 && selected.length === items.length;
}; };
const toggleSelectAll = (listName) => { const toggleSelectAll = (listName) => {
const items = safeGetField(listName, formStore.previewFormData) || []; const items = safeGetField(listName, formStore.previewFormData, { warn: false }) || [];
if (isAllSelected(listName)) { if (isAllSelected(listName)) {
selectedItems.value[listName] = []; selectedItems.value[listName] = [];
@ -1130,7 +1118,7 @@ const toggleSelectAll = (listName) => {
const deleteSelectedItems = (listName) => { const deleteSelectedItems = (listName) => {
if (!props.component.props.confirmDelete || confirm('Are you sure you want to delete the selected items?')) { if (!props.component.props.confirmDelete || confirm('Are you sure you want to delete the selected items?')) {
const currentItems = [...(safeGetField(listName, formStore.previewFormData) || [])]; const currentItems = [...(safeGetField(listName, formStore.previewFormData, { warn: false }) || [])];
const selected = selectedItems.value[listName] || []; const selected = selectedItems.value[listName] || [];
// Sort indices in descending order to avoid index shifting issues // Sort indices in descending order to avoid index shifting issues
@ -1152,7 +1140,7 @@ const deleteSelectedItems = (listName) => {
// Import/Export functionality // Import/Export functionality
const exportItems = (listName, format) => { const exportItems = (listName, format) => {
const items = safeGetField(listName, formStore.previewFormData) || []; const items = safeGetField(listName, formStore.previewFormData, { warn: false }) || [];
let content = ''; let content = '';
let filename = `${listName}_items`; let filename = `${listName}_items`;
let mimeType = 'text/plain'; let mimeType = 'text/plain';
@ -1203,7 +1191,7 @@ const showImportModal = (listName) => {
} else { } else {
importedItems = content.split('\n').filter(Boolean); importedItems = content.split('\n').filter(Boolean);
} }
const currentItems = [...(safeGetField(listName, formStore.previewFormData) || [])]; const currentItems = [...(safeGetField(listName, formStore.previewFormData, { warn: false }) || [])];
const newItems = [...currentItems, ...importedItems]; const newItems = [...currentItems, ...importedItems];
const maxItems = props.component.props.maxItems || 20; const maxItems = props.component.props.maxItems || 20;
if (newItems.length > maxItems) { if (newItems.length > maxItems) {
@ -1344,41 +1332,170 @@ const updateListItems = (listName, newItems) => {
const getTableData = (tableName) => { const getTableData = (tableName) => {
if (!tableName) return []; if (!tableName) return [];
// Directly check the form store without using safeGetField to avoid warnings // Use safeGetField for consistent data access
const formData = formStore.previewFormData || {}; const currentData = safeGetField(tableName, formStore.previewFormData, { warn: false });
console.log('[ComponentPreview] getTableData:', tableName, 'currentData:', currentData);
// If field exists and is an array, return it // If field exists and is an array, return it
if (formData.hasOwnProperty(tableName) && Array.isArray(formData[tableName])) { if (Array.isArray(currentData)) {
return formData[tableName]; return currentData;
} }
// If data doesn't exist, initialize it immediately (no nextTick needed for initial render) // If data doesn't exist, initialize it immediately
const initialData = []; const initialData = [];
const updatedFormData = { ...formData, [tableName]: initialData }; const updatedFormData = { ...formStore.previewFormData, [tableName]: initialData };
formStore.updatePreviewFormData(updatedFormData); formStore.updatePreviewFormData(updatedFormData);
console.log('[ComponentPreview] getTableData: initialized new data for:', tableName);
return initialData; return initialData;
}; };
// Simplified helper functions for regular fields only
const getFieldName = (component) => component.props.name;
const getFieldValue = (component) => {
const fieldName = component.props.name;
const formData = formStore.previewFormData;
// Silent check - don't trigger warnings for regular field access
if (formData && formData.hasOwnProperty(fieldName)) {
const value = formData[fieldName];
if (value !== undefined && value !== null) {
return value;
}
}
// Return component default value if no form data exists
return component.props.value || '';
};
const handleFieldInput = (component, event) => {
if (!props.isPreview) return;
const fieldName = component.props.name;
const newValue = event;
// Simple update for regular fields only
const updatedData = { ...formStore.previewFormData, [fieldName]: newValue };
formStore.updatePreviewFormData(updatedData);
emit('form-data-updated', updatedData);
};
const handleFieldChange = (component, event) => {
handleFieldInput(component, event);
};
// Helper function to safely get field values for repeating groups without warnings
const getRepeatingGroupFieldValue = (group, fieldName, fieldType) => {
if (!group || typeof group !== 'object') {
return getDefaultValueForType(fieldType);
}
if (group.hasOwnProperty(fieldName)) {
const value = group[fieldName];
if (value === undefined || value === null) {
return getDefaultValueForType(fieldType);
}
return value;
}
return getDefaultValueForType(fieldType);
};
// Helper function to get default values based on field type
const getDefaultValueForType = (fieldType) => {
switch (fieldType) {
case 'number':
return 0;
case 'checkbox':
return [];
case 'select':
return '';
default:
return '';
}
};
// Simple repeating group functions
const addRepeatingGroupItem = (groupName, children) => {
const currentData = safeGetField(groupName, formStore.previewFormData, { warn: false }) || [];
const newItem = {};
// Initialize fields from children
if (children) {
children.forEach(child => {
if (child.props && child.props.name) {
// Use the same default value logic as getDefaultValueForType
if (child.type === 'select' && child.props.options && child.props.options.length > 0) {
newItem[child.props.name] = child.props.options[0].value || '';
} else {
newItem[child.props.name] = getDefaultValueForType(child.type);
}
}
});
}
const newData = [...currentData, newItem];
const updatedFormData = { ...formStore.previewFormData, [groupName]: newData };
formStore.updatePreviewFormData(updatedFormData);
emit('form-data-updated', updatedFormData);
};
const removeRepeatingGroupItem = (groupName, index) => {
const currentData = safeGetField(groupName, formStore.previewFormData, { warn: false }) || [];
const newData = [...currentData];
newData.splice(index, 1);
const updatedFormData = { ...formStore.previewFormData, [groupName]: newData };
formStore.updatePreviewFormData(updatedFormData);
emit('form-data-updated', updatedFormData);
};
const updateRepeatingGroupField = (groupName, groupIndex, fieldName, value) => {
const currentData = safeGetField(groupName, formStore.previewFormData, { warn: false }) || [];
const newData = [...currentData];
if (!newData[groupIndex]) {
newData[groupIndex] = {};
}
newData[groupIndex][fieldName] = value;
const updatedFormData = { ...formStore.previewFormData, [groupName]: newData };
formStore.updatePreviewFormData(updatedFormData);
emit('form-data-updated', updatedFormData);
};
// Duplicate RepeatingGroupContainer definition removed
// Update table data for repeating-table component // Update table data for repeating-table component
const updateTableData = (newData) => { const updateTableData = (newData) => {
const tableName = props.component.props.name; const tableName = props.component.props.name;
if (!tableName) return; if (!tableName) return;
console.log('[ComponentPreview] updateTableData:', tableName, 'newData:', newData);
// Ensure newData is always an array // Ensure newData is always an array
const safeNewData = Array.isArray(newData) ? newData : []; const safeNewData = Array.isArray(newData) ? newData : [];
// Check if data actually changed to prevent unnecessary updates // Check if data actually changed to prevent unnecessary updates
const currentData = safeGetField(tableName, formStore.previewFormData) || []; const currentData = safeGetField(tableName, formStore.previewFormData, { warn: false }) || [];
const currentDataStr = JSON.stringify(currentData); const currentDataStr = JSON.stringify(currentData);
const newDataStr = JSON.stringify(safeNewData); const newDataStr = JSON.stringify(safeNewData);
if (currentDataStr === newDataStr) return; if (currentDataStr === newDataStr) {
console.log('[ComponentPreview] updateTableData: no change detected, skipping update');
return;
}
nextTick(() => { nextTick(() => {
const updatedFormData = { ...formStore.previewFormData, [tableName]: safeNewData }; const updatedFormData = { ...formStore.previewFormData, [tableName]: safeNewData };
formStore.updatePreviewFormData(updatedFormData); formStore.updatePreviewFormData(updatedFormData);
console.log('[ComponentPreview] updateTableData: updated form data for:', tableName, 'new data:', safeNewData);
// Emit the change for workflow page to sync with its local formData // Emit the change for workflow page to sync with its local formData
emit('form-data-updated', updatedFormData); emit('form-data-updated', updatedFormData);
}); });
@ -1386,8 +1503,8 @@ const updateTableData = (newData) => {
// Form Section Component // Form Section Component
const toggleSectionCollapse = (sectionId) => { const toggleSectionCollapse = (sectionId) => {
// Find the section component and toggle its collapsed state // Find the section component at any nesting level
const section = formStore.formComponents.find(comp => comp.id === sectionId); const section = findContainerRecursively(sectionId);
if (section) { if (section) {
section.props.collapsed = !section.props.collapsed; section.props.collapsed = !section.props.collapsed;
formStore.updateComponent(section); formStore.updateComponent(section);
@ -2066,6 +2183,15 @@ onMounted(() => {
.repeating-table-wrapper{ .repeating-table-wrapper{
margin-bottom: 0.5rem !important; margin-bottom: 0.5rem !important;
width: 100%;
min-width: 0; /* Allow shrinking */
overflow: hidden; /* Prevent table from breaking layout */
}
.table-container-wrapper {
width: 100%;
min-width: 0; /* Allow shrinking */
/* This is where the horizontal scrolling happens */
} }
/* Form Section Component */ /* Form Section Component */

View File

@ -56,22 +56,22 @@
<!-- Data Table --> <!-- Data Table -->
<div class="table-container"> <div class="table-container">
<!-- Performance warning for too many columns --> <!-- Performance warning for too many columns -->
<div v-if="config.columns.length > 20" class="performance-warning"> <div v-if="config.columns.length > 50" class="performance-warning">
<div class="warning-content"> <div class="warning-content">
<Icon name="heroicons:information-circle" class="warning-icon" /> <Icon name="heroicons:information-circle" class="warning-icon" />
<div class="warning-text"> <div class="warning-text">
<p class="warning-title">Performance Notice</p> <p class="warning-title">Performance Notice</p>
<p class="warning-message"> <p class="warning-message">
This table has {{ config.columns.length }} columns. For optimal performance, only the first 20 columns are displayed. This table has {{ config.columns.length }} columns. For optimal performance, only the first 50 columns are displayed.
Consider reducing the number of columns for better user experience. The table is scrollable horizontally to view all data. Consider reducing the number of columns for better user experience.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<div v-if="paginatedData.length > 0" class="table-wrapper"> <div v-if="paginatedData.length > 0" class="table-wrapper">
<div class="table-container" ref="tableContainer"> <div class="table-scroll-container" ref="tableContainer">
<table class="data-table"> <table class="data-table" :style="{ minWidth: calculateMinTableWidth }">
<thead class="table-header"> <thead class="table-header">
<tr> <tr>
<th v-if="config.showRowNumbers" class="row-number-header">#</th> <th v-if="config.showRowNumbers" class="row-number-header">#</th>
@ -79,7 +79,7 @@
v-for="column in visibleColumns" v-for="column in visibleColumns"
:key="column.name" :key="column.name"
class="column-header" class="column-header"
:style="{ width: getColumnWidth(column) }" :style="{ width: getColumnWidth(column), minWidth: getColumnWidth(column) }"
> >
<div class="column-header-content"> <div class="column-header-content">
<span class="header-text">{{ column.label }}</span> <span class="header-text">{{ column.label }}</span>
@ -103,7 +103,7 @@
v-for="column in visibleColumns" v-for="column in visibleColumns"
:key="column.name" :key="column.name"
class="data-cell" class="data-cell"
:style="{ width: getColumnWidth(column) }" :style="{ width: getColumnWidth(column), minWidth: getColumnWidth(column) }"
> >
<div class="cell-content"> <div class="cell-content">
<SimpleCellValue <SimpleCellValue
@ -415,33 +415,55 @@ const filteredData = computed(() => {
// Column virtualization for large datasets // Column virtualization for large datasets
const visibleColumns = computed(() => { const visibleColumns = computed(() => {
const columns = props.config.columns const columns = props.config.columns
const maxColumns = 20 // Limit visible columns for performance const maxColumns = 50 // Increased limit for better user experience with scrolling
if (columns.length <= maxColumns) { if (columns.length <= maxColumns) {
return columns return columns
} }
// Return first 20 columns for performance // Return first 50 columns for performance, but allow scrolling
return columns.slice(0, maxColumns) return columns.slice(0, maxColumns)
}) })
// Column width calculation // Column width calculation
const getColumnWidth = (column) => { const getColumnWidth = (column) => {
const baseWidth = 150 const baseWidth = 150 // Increased base width
const typeWidths = { const typeWidths = {
text: 150, text: 150,
number: 100, number: 100,
email: 200, email: 180,
tel: 120, tel: 120,
date: 120, date: 120,
time: 100, time: 100,
url: 200, url: 180,
select: 150, select: 150,
checkbox: 80, checkbox: 80,
textarea: 200 textarea: 200
} }
return `${typeWidths[column.type] || baseWidth}px` // For tables with many columns, still use reasonable widths
const totalColumns = props.config.columns.length
if (totalColumns > 10) {
const adjustedWidths = {
text: 120,
number: 90,
email: 160,
tel: 110,
date: 110,
time: 90,
url: 160,
select: 120,
checkbox: 70,
textarea: 160
}
const width = `${adjustedWidths[column.type] || baseWidth}px`
console.log(`[RepeatingTable] Column ${column.name} width: ${width}`)
return width
}
const width = `${typeWidths[column.type] || baseWidth}px`
console.log(`[RepeatingTable] Column ${column.name} width: ${width}`)
return width
} }
// Record key generation for better Vue rendering // Record key generation for better Vue rendering
@ -676,6 +698,51 @@ const goToPage = (page) => {
currentPage.value = page currentPage.value = page
} }
// Calculate minimum table width based on columns
const calculateMinTableWidth = computed(() => {
if (!props.config.columns) return 'auto'
let totalWidth = 0
// Add row number column width if enabled
if (props.config.showRowNumbers) {
totalWidth += 60 // Row number column width
}
// Add column widths
visibleColumns.value.forEach(column => {
const width = parseInt(getColumnWidth(column))
totalWidth += width
})
// Add actions column width if enabled
if (showActions.value) {
totalWidth += 100 // Actions column width
}
// Add some padding for borders and spacing
totalWidth += 40
console.log('[RepeatingTable] Calculated minimum width:', totalWidth)
return `${totalWidth}px`
})
// Debug table width on mount
onMounted(() => {
nextTick(() => {
if (tableContainer.value) {
const table = tableContainer.value.querySelector('.data-table')
if (table) {
console.log('[RepeatingTable] Table width:', table.offsetWidth)
console.log('[RepeatingTable] Container width:', tableContainer.value.offsetWidth)
console.log('[RepeatingTable] Scroll width:', table.scrollWidth)
console.log('[RepeatingTable] Has horizontal scroll:', table.scrollWidth > tableContainer.value.offsetWidth)
console.log('[RepeatingTable] Calculated min width:', calculateMinTableWidth.value)
}
}
})
})
// Cleanup on unmount // Cleanup on unmount
onUnmounted(() => { onUnmounted(() => {
columnCache.value.clear() columnCache.value.clear()
@ -686,21 +753,33 @@ onUnmounted(() => {
<style scoped> <style scoped>
/* Table Container with Sticky Header */ /* Table Container with Sticky Header */
.table-container { .table-container {
overflow: hidden;
background: white; background: white;
max-height: 500px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #cbd5e1 #f1f5f9;
width: 100%; width: 100%;
} }
.table-scroll-container {
overflow-x: auto !important;
overflow-y: auto;
max-height: 500px;
scrollbar-width: thin;
scrollbar-color: #cbd5e1 #f1f5f9;
border: 1px solid #e5e7eb;
border-radius: 0.375rem;
/* Ensure horizontal scrolling is always available */
width: 100%;
position: relative;
}
.data-table { .data-table {
table-layout: fixed; table-layout: auto; /* Changed from fixed to auto for better column sizing */
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
border-spacing: 0; border-spacing: 0;
margin: 0; margin: 0;
/* Ensure table is wide enough to trigger scrolling */
white-space: nowrap;
/* Force minimum width to ensure scrolling */
min-width: 100%;
} }
/* Sticky Header */ /* Sticky Header */
@ -718,42 +797,47 @@ onUnmounted(() => {
} }
/* Custom scrollbar for webkit browsers */ /* Custom scrollbar for webkit browsers */
.table-container::-webkit-scrollbar { .table-scroll-container::-webkit-scrollbar {
width: 8px; width: 8px;
height: 8px;
} }
.table-container::-webkit-scrollbar-track { .table-scroll-container::-webkit-scrollbar-track {
background: #f1f5f9; background: #f1f5f9;
border-radius: 4px; border-radius: 4px;
} }
.table-container::-webkit-scrollbar-thumb { .table-scroll-container::-webkit-scrollbar-thumb {
background: #cbd5e1; background: #cbd5e1;
border-radius: 4px; border-radius: 4px;
} }
.table-container::-webkit-scrollbar-thumb:hover { .table-scroll-container::-webkit-scrollbar-thumb:hover {
background: #94a3b8; background: #94a3b8;
} }
/* Horizontal scrollbar styling */
.table-scroll-container::-webkit-scrollbar:horizontal {
height: 8px;
}
.column-header, .column-header,
.data-cell { .data-cell {
white-space: nowrap; white-space: nowrap;
overflow: hidden; padding: 8px 12px;
text-overflow: ellipsis; vertical-align: middle;
max-width: 0; /* Remove text truncation to allow full content display */
} }
.cell-content { .cell-content {
overflow: hidden; /* Allow content to expand naturally */
text-overflow: ellipsis; min-width: max-content;
} }
.cell-value { .cell-value {
display: block; display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
/* Remove text truncation to show full content */
} }
/* Virtual scrolling optimizations */ /* Virtual scrolling optimizations */
@ -801,7 +885,7 @@ onUnmounted(() => {
/* Responsive optimizations */ /* Responsive optimizations */
@media (max-width: 768px) { @media (max-width: 768px) {
.table-content { .table-scroll-container {
max-height: 400px; max-height: 400px;
} }
@ -813,7 +897,7 @@ onUnmounted(() => {
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.table-content { .table-scroll-container {
max-height: 300px; max-height: 300px;
} }
@ -823,6 +907,35 @@ onUnmounted(() => {
max-width: 100px; max-width: 100px;
} }
} }
/* Enhanced scrolling for wide tables */
.table-scroll-container {
position: relative;
}
.table-scroll-container::after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 20px;
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.8));
pointer-events: none;
z-index: 1;
}
.table-scroll-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 20px;
background: linear-gradient(to left, transparent, rgba(255, 255, 255, 0.8));
pointer-events: none;
z-index: 1;
}
.repeating-table-container { .repeating-table-container {
@apply border border-gray-200 rounded-lg overflow-hidden bg-white shadow-sm w-full; @apply border border-gray-200 rounded-lg overflow-hidden bg-white shadow-sm w-full;
min-width: 0; min-width: 0;

View File

@ -1,5 +1,5 @@
// Utility to safely get a field value from a form data object // Utility to safely get a field value from a form data object
export function safeGetField(field, formData) { export function safeGetField(field, formData, options = {}) {
if (formData && Object.prototype.hasOwnProperty.call(formData, field)) { if (formData && Object.prototype.hasOwnProperty.call(formData, field)) {
const value = formData[field]; const value = formData[field];
// If the value is undefined or null, return empty string for backward compatibility // If the value is undefined or null, return empty string for backward compatibility
@ -8,9 +8,14 @@ export function safeGetField(field, formData) {
} }
return value; return value;
} }
if (process.env.NODE_ENV !== 'production') {
// Only warn in development // Only warn for specific cases or when explicitly requested
console.warn(`Field '${field}' is missing or inaccessible.`); if (process.env.NODE_ENV !== 'production' && options.warn !== false) {
// Don't warn for newly added components or fields that haven't been initialized yet
const isNewField = field && (field.includes('_') && /\d+$/.test(field));
if (!isNewField) {
console.warn(`Field '${field}' is missing or inaccessible.`);
}
} }
return ''; return '';
} }

View File

@ -285,8 +285,7 @@
ref="previewForm" ref="previewForm"
type="form" type="form"
@submit="handlePreviewSubmit" @submit="handlePreviewSubmit"
:actions="false" :actions="false"
v-model="previewFormData"
> >
<div <div
class="grid-preview-container" class="grid-preview-container"
@ -2069,43 +2068,43 @@ const handleJsonImport = (event) => {
// Ensure all required info display properties // Ensure all required info display properties
processedProps = { processedProps = {
title: processedProps.title || 'Information', title: processedProps.title || 'Information',
name: processedDefaultProps.name || `info_display_${index + 1}`, name: processedProps.name || `info_display_${index + 1}`,
help: processedDefaultProps.help || '', help: processedProps.help || '',
layout: processedDefaultProps.layout || 'vertical', layout: processedProps.layout || 'vertical',
showBorder: processedDefaultProps.showBorder !== undefined ? processedDefaultProps.showBorder : true, showBorder: processedProps.showBorder !== undefined ? processedProps.showBorder : true,
backgroundColor: processedDefaultProps.backgroundColor || '#f8fafc', backgroundColor: processedProps.backgroundColor || '#f8fafc',
fields: Array.isArray(processedDefaultProps.fields) ? processedDefaultProps.fields : [ fields: Array.isArray(processedProps.fields) ? processedProps.fields : [
{ label: 'Info Item', value: 'Value', key: 'item_1' } { label: 'Info Item', value: 'Value', key: 'item_1' }
], ],
...processedDefaultProps ...processedProps
}; };
break; break;
case 'file': case 'file':
// Ensure all required file upload properties // Ensure all required file upload properties
processedProps = { processedProps = {
label: processedDefaultProps.label || 'File Upload', label: processedProps.label || 'File Upload',
name: processedDefaultProps.name || `file_upload_${index + 1}`, name: processedProps.name || `file_upload_${index + 1}`,
help: processedDefaultProps.help || 'Upload a file', help: processedProps.help || 'Upload a file',
accept: processedDefaultProps.accept || '.pdf,.doc,.docx,.jpg,.jpeg,.png', accept: processedProps.accept || '.pdf,.doc,.docx,.jpg,.jpeg,.png',
...processedDefaultProps ...processedProps
}; };
break; break;
case 'heading': case 'heading':
// Ensure all required heading properties // Ensure all required heading properties
processedProps = { processedProps = {
value: processedDefaultProps.value || 'Heading', value: processedProps.value || 'Heading',
level: processedDefaultProps.level || 2, level: processedProps.level || 2,
...processedDefaultProps ...processedProps
}; };
break; break;
case 'paragraph': case 'paragraph':
// Ensure all required paragraph properties // Ensure all required paragraph properties
processedProps = { processedProps = {
value: processedDefaultProps.value || 'Paragraph text', value: processedProps.value || 'Paragraph text',
...processedDefaultProps ...processedProps
}; };
break; break;
@ -2113,7 +2112,7 @@ const handleJsonImport = (event) => {
case 'radio': case 'radio':
case 'checkbox': case 'checkbox':
// Ensure options array exists // Ensure options array exists
if (!Array.isArray(processedDefaultProps.options) || processedDefaultProps.options.length === 0) { if (!Array.isArray(processedProps.options) || processedProps.options.length === 0) {
processedDefaultProps.options = [ processedDefaultProps.options = [
{ label: 'Option 1', value: 'option_1' }, { label: 'Option 1', value: 'option_1' },
{ label: 'Option 2', value: 'option_2' } { label: 'Option 2', value: 'option_2' }
@ -2537,18 +2536,13 @@ const saveFormSettings = () => {
// Preview form data for script interactions // Preview form data for script interactions
const previewFormData = ref({}); const previewFormData = ref({});
// Initialize preview form data with default values // Helper function to process nested components recursively
watchEffect(() => { const processNestedComponents = (components, existingFormData, newDefaults, hasNewComponents) => {
console.log('[FormBuilder] watchEffect for previewFormData initialization. Current form components:', formStore.formComponents.length); components.forEach(component => {
const existingFormData = { ...previewFormData.value }; // Preserve current user inputs
let newDefaults = {};
let hasNewComponents = false;
formStore.formComponents.forEach(component => {
if (component.props.name) { if (component.props.name) {
// If field is not already in existingFormData, it's a new component or needs initialization // If field is not already in existingFormData, it's a new component or needs initialization
if (!existingFormData.hasOwnProperty(component.props.name)) { if (!existingFormData.hasOwnProperty(component.props.name)) {
hasNewComponents = true; hasNewComponents.value = true;
// Set default values based on component type // Set default values based on component type
switch (component.type) { switch (component.type) {
case 'checkbox': case 'checkbox':
@ -2620,25 +2614,48 @@ watchEffect(() => {
} }
} }
} }
// Process nested components if this component has children
if (component.props.children && Array.isArray(component.props.children)) {
processNestedComponents(component.props.children, existingFormData, newDefaults, hasNewComponents);
}
}); });
};
// Initialize preview form data with default values
watchEffect(() => {
const existingFormData = { ...previewFormData.value }; // Preserve current user inputs
let newDefaults = {};
let hasNewComponents = { value: false };
// Process all components including nested ones
processNestedComponents(formStore.formComponents, existingFormData, newDefaults, hasNewComponents);
// Only update previewFormData if it's the initial load (empty) or new components were added that need defaults // Only update previewFormData if it's the initial load (empty) or new components were added that need defaults
// This prevents overwriting user input when existing components change their props (which also triggers this watchEffect) // This prevents overwriting user input when existing components change their props (which also triggers this watchEffect)
const isInitialLoad = Object.keys(previewFormData.value).length === 0 && Object.keys(newDefaults).length > 0; const isInitialLoad = Object.keys(previewFormData.value).length === 0 && Object.keys(newDefaults).length > 0;
if (isInitialLoad || hasNewComponents) { // CRITICAL FIX: Don't reinitialize if we already have data for repeating groups/arrays
console.log('[FormBuilder] Initializing/merging preview form data. Initial load:', isInitialLoad, 'Has new components:', hasNewComponents); // This prevents data loss when users are editing repeating group items
previewFormData.value = { ...existingFormData, ...newDefaults }; const hasExistingArrayData = Object.keys(existingFormData).some(key => Array.isArray(existingFormData[key]) && existingFormData[key].length > 0);
console.log('[FormBuilder] Preview form data after init/merge:', previewFormData.value);
} else { if (isInitialLoad || (hasNewComponents.value && !hasExistingArrayData)) {
console.log('[FormBuilder] Skipping full previewFormData re-initialization to preserve user input.'); // Smart merge: preserve existing array data, only add truly new defaults
const smartMerge = { ...existingFormData };
// Only add defaults for fields that don't exist at all
Object.keys(newDefaults).forEach(key => {
if (!smartMerge.hasOwnProperty(key)) {
smartMerge[key] = newDefaults[key];
}
});
previewFormData.value = smartMerge;
} }
}); });
// Handle script-driven field changes // Handle script-driven field changes
const handleScriptFieldChange = ({ fieldName, value }) => { const handleScriptFieldChange = ({ fieldName, value }) => {
console.log('[FormBuilder] Script field change:', fieldName, '=', value);
// Update the reactive form data // Update the reactive form data
const newFormData = { const newFormData = {
...previewFormData.value, ...previewFormData.value,
@ -2652,15 +2669,12 @@ const handleScriptFieldChange = ({ fieldName, value }) => {
// Try to force FormKit form to update // Try to force FormKit form to update
nextTick(() => { nextTick(() => {
console.log('[FormBuilder] Updated form data:', newFormData);
// Try to access the FormKit form node and update it directly // Try to access the FormKit form node and update it directly
if (previewForm.value && previewForm.value.node) { if (previewForm.value && previewForm.value.node) {
try { try {
previewForm.value.node.input(newFormData); previewForm.value.node.input(newFormData);
console.log('[FormBuilder] Force updated FormKit form');
} catch (error) { } catch (error) {
console.warn('[FormBuilder] Could not force update FormKit form:', error); // Silent fallback
} }
} }
}); });
@ -2669,26 +2683,16 @@ const handleScriptFieldChange = ({ fieldName, value }) => {
// Handle script-driven field validation // Handle script-driven field validation
const handleScriptFieldValidate = ({ fieldName }) => { const handleScriptFieldValidate = ({ fieldName }) => {
// Could integrate with FormKit validation here // Could integrate with FormKit validation here
console.log(`Validating field: ${fieldName}`);
}; };
// Handle FormKit form input events // Handle FormKit form input events
const handleFormKitInput = (formData, node) => { const handleFormKitInput = (formData, node) => {
console.log('[FormBuilder] FormKit input event received!');
console.log('[FormBuilder] FormKit formData:', JSON.parse(JSON.stringify(formData)));
console.log('[FormBuilder] Current previewFormData before update:', JSON.parse(JSON.stringify(previewFormData.value)));
// Update our reactive form data - this should trigger the FormScriptEngine watcher // Update our reactive form data - this should trigger the FormScriptEngine watcher
const oldPreviewData = { ...previewFormData.value }; const oldPreviewData = { ...previewFormData.value };
previewFormData.value = { ...formData }; previewFormData.value = { ...formData };
console.log('[FormBuilder] Updated previewFormData to:', JSON.parse(JSON.stringify(previewFormData.value)));
console.log('[FormBuilder] Did previewFormData actually change?', JSON.stringify(oldPreviewData) !== JSON.stringify(previewFormData.value));
// Make form data accessible to component previews // Make form data accessible to component previews
formStore.updatePreviewFormData(previewFormData.value); formStore.updatePreviewFormData(previewFormData.value);
console.log('[FormBuilder] FormStore preview data updated to:', JSON.parse(JSON.stringify(formStore.previewFormData)));
}; };
// Watch for changes in previewFormData to trigger FormScriptEngine // Watch for changes in previewFormData to trigger FormScriptEngine
@ -2702,9 +2706,13 @@ watch(() => previewFormData.value, (newData, oldData) => {
return; // No actual change, skip update return; // No actual change, skip update
} }
console.log('[FormBuilder] previewFormData watcher triggered!'); // CRITICAL FIX: Don't sync back to store if we have any array data at all
console.log('[FormBuilder] New data:', newDataStr); // ComponentPreview.vue manages its own array data directly with the store
console.log('[FormBuilder] Old data:', oldDataStr); const hasArrayData = Object.keys(newData).some(key => Array.isArray(newData[key]));
if (hasArrayData) {
return;
}
// Use nextTick to avoid synchronous updates that can cause recursion // Use nextTick to avoid synchronous updates that can cause recursion
nextTick(() => { nextTick(() => {
@ -2712,7 +2720,6 @@ watch(() => previewFormData.value, (newData, oldData) => {
const currentDataStr = JSON.stringify(previewFormData.value); const currentDataStr = JSON.stringify(previewFormData.value);
if (currentDataStr === newDataStr) { if (currentDataStr === newDataStr) {
formStore.updatePreviewFormData(newData); formStore.updatePreviewFormData(newData);
console.log('[FormBuilder] FormStore preview data updated to:', JSON.parse(JSON.stringify(formStore.previewFormData)));
} }
}); });
}, { deep: true, immediate: false }); }, { deep: true, immediate: false });
@ -3379,6 +3386,8 @@ const handleFormRestored = (restoredForm) => {
console.log('Form restored:', restoredForm); console.log('Form restored:', restoredForm);
// You might want to update the form state or show a success message // You might want to update the form state or show a success message
}; };
</script> </script>
<style scoped> <style scoped>