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:
parent
a14252a844
commit
d0cef85b72
@ -23,12 +23,14 @@
|
||||
</div>
|
||||
|
||||
<!-- 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"
|
||||
:validation="component.props.validation" :validation-visibility="isPreview ? 'live' : 'blur'"
|
||||
:readonly="component.props.readonly || !isPreview"
|
||||
: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"
|
||||
:mask="component.props.mask || undefined" :digits="component.props.digits || undefined"
|
||||
:multiple="component.props.multiple || undefined" :maxSize="component.props.maxSize || undefined"
|
||||
@ -42,7 +44,9 @@
|
||||
'readonly-checkbox': component.props.readonly && component.type === 'checkbox',
|
||||
'readonly-radio': component.props.readonly && component.type === 'radio',
|
||||
'readonly-switch': component.props.readonly && component.type === 'switch'
|
||||
}" />
|
||||
}"
|
||||
@input="handleFieldInput(component, $event)"
|
||||
@change="handleFieldChange(component, $event)" />
|
||||
|
||||
<!-- Heading -->
|
||||
<div v-else-if="component.type === 'heading'" class="py-2">
|
||||
@ -228,35 +232,59 @@
|
||||
|
||||
<!-- Preview mode - show functional repeating groups -->
|
||||
<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">
|
||||
<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)"
|
||||
type="button" class="text-red-500 hover:text-red-700 text-sm" @click="removeGroupItem(groupIndex)">
|
||||
{{ component.props.removeText || 'Remove' }}
|
||||
<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="removeRepeatingGroupItem(component.props.name, groupIndex)">
|
||||
{{ component.props.removeText || 'Buang' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Fields grid -->
|
||||
<div class="grid grid-cols-12 gap-2">
|
||||
<!-- Render children components for each group item -->
|
||||
<template v-for="(child, childIndex) in component.props.children" :key="childIndex">
|
||||
<div
|
||||
class="form-component"
|
||||
:style="{
|
||||
gridColumn: child.props.gridColumn || 'span 6'
|
||||
}"
|
||||
>
|
||||
<component-preview :component="child" :is-preview="true" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-for="(child, childIndex) in component.props.children"
|
||||
:key="`field-${groupIndex}-${childIndex}`"
|
||||
class="form-component"
|
||||
:style="{ gridColumn: child.props.gridColumn || 'span 6' }">
|
||||
|
||||
<!-- Render FormKit field -->
|
||||
<FormKit
|
||||
:type="child.type"
|
||||
:name="`${component.props.name}[${groupIndex}].${child.props.name}`"
|
||||
:label="child.props.label"
|
||||
: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>
|
||||
|
||||
<button v-if="(safeGetField(component.props.name, previewFormData)?.length || 0) < (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="addGroupItem">
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-if="(safeGetField(component.props.name, previewFormData, { warn: false }) || []).length === 0"
|
||||
class="text-center py-8 text-gray-500">
|
||||
<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" />
|
||||
{{ component.props.buttonText || 'Add Item' }}
|
||||
</button>
|
||||
@ -270,7 +298,7 @@
|
||||
<label v-if="component.props.label" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ component.props.label }}
|
||||
<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>
|
||||
</label>
|
||||
|
||||
@ -289,7 +317,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="checkbox" :checked="isAllSelected(component.props.name)"
|
||||
@ -343,7 +371,7 @@
|
||||
@change="toggleItemSelection(component.props.name, index)"
|
||||
class="mr-2 h-4 w-4 rounded border-gray-300" />
|
||||
<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"
|
||||
:class="getItemInputClasses(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">
|
||||
<Icon name="material-symbols:drag-indicator" class="w-5 h-5" />
|
||||
</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)">
|
||||
<Icon name="material-symbols:delete-outline" class="w-5 h-5" />
|
||||
</button>
|
||||
@ -363,7 +391,7 @@
|
||||
{{ validationErrors[component.props.name] }}
|
||||
</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"
|
||||
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">
|
||||
@ -372,7 +400,7 @@
|
||||
</button>
|
||||
|
||||
<!-- 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">
|
||||
<button @click="exportItems(component.props.name, component.props.exportFormat)"
|
||||
class="text-xs text-green-600 hover:text-green-800 flex items-center">
|
||||
@ -387,12 +415,14 @@
|
||||
|
||||
<!-- Repeating Table Component -->
|
||||
<div v-else-if="component.type === 'repeating-table'" class="repeating-table-wrapper">
|
||||
<RepeatingTable
|
||||
:config="component.props"
|
||||
:model-value="getTableData(component.props.name)"
|
||||
:is-preview="isPreview"
|
||||
@update:model-value="updateTableData"
|
||||
/>
|
||||
<div class="table-container-wrapper">
|
||||
<RepeatingTable
|
||||
:config="component.props"
|
||||
:model-value="getTableData(component.props.name)"
|
||||
:is-preview="isPreview"
|
||||
@update:model-value="updateTableData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Button Component -->
|
||||
@ -679,9 +709,11 @@ import { useNuxtApp } from '#app';
|
||||
import { useFormBuilderStore } from '~/stores/formBuilder';
|
||||
import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettingsModal.vue';
|
||||
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';
|
||||
|
||||
// RepeatingGroupContainer component removed - using template approach instead
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
@ -703,11 +735,20 @@ const emit = defineEmits(['select-nested-component', 'form-data-updated']);
|
||||
const formStore = useFormBuilderStore();
|
||||
const previewFormData = computed(() => formStore.previewFormData || {});
|
||||
|
||||
// Clean up: Debug watcher removed - repeating groups working correctly
|
||||
|
||||
// Enhanced dynamic list functionality
|
||||
const searchQuery = ref({});
|
||||
const selectedItems = 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
|
||||
const sectionDropStates = ref({});
|
||||
|
||||
@ -750,7 +791,7 @@ onMounted(() => {
|
||||
validationErrors.value[listName] = '';
|
||||
|
||||
// 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;
|
||||
|
||||
if ((!currentFormData || currentFormData.length === 0) && defaultItems && defaultItems.length > 0) {
|
||||
@ -767,7 +808,7 @@ onMounted(() => {
|
||||
const groupName = props.component.props.name;
|
||||
if (groupName) {
|
||||
// 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 ((!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
|
||||
@ -824,7 +885,7 @@ watch(() => props.component.props.minItems, (newMinItems, oldMinItems) => {
|
||||
const groupName = props.component.props.name;
|
||||
if (!groupName || newMinItems === oldMinItems) return;
|
||||
|
||||
const currentGroups = safeGetField(groupName, formStore.previewFormData);
|
||||
const currentGroups = safeGetField(groupName, formStore.previewFormData, { warn: false });
|
||||
const minItems = newMinItems || 1;
|
||||
|
||||
// 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 });
|
||||
|
||||
// Controlled update methods to prevent circular reactivity
|
||||
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);
|
||||
});
|
||||
};
|
||||
// Legacy updateGroupField removed - now handled by RepeatingGroupContainer
|
||||
|
||||
const updateListItem = (listName, index, newValue) => {
|
||||
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
|
||||
|
||||
// Validate and handle duplicates
|
||||
@ -890,63 +934,7 @@ const updateListItem = (listName, index, newValue) => {
|
||||
});
|
||||
};
|
||||
|
||||
// Repeating group and dynamic list functionality
|
||||
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);
|
||||
};
|
||||
// Legacy functions removed - now handled by RepeatingGroupContainer
|
||||
|
||||
const addListItem = () => {
|
||||
if (!props.isPreview) return;
|
||||
@ -955,7 +943,7 @@ const addListItem = () => {
|
||||
if (!listName) return;
|
||||
|
||||
// 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
|
||||
currentItems.push('');
|
||||
@ -972,7 +960,7 @@ const removeListItem = (index) => {
|
||||
if (!listName) return;
|
||||
|
||||
// Get current items
|
||||
const currentItems = [...(safeGetField(listName, formStore.previewFormData) || [])];
|
||||
const currentItems = [...(safeGetField(listName, formStore.previewFormData, { warn: false }) || [])];
|
||||
|
||||
// Remove the item at the specified index
|
||||
currentItems.splice(index, 1);
|
||||
@ -1041,7 +1029,7 @@ const validateItem = (listName, index, value) => {
|
||||
const checkDuplicates = (listName, newValue, currentIndex) => {
|
||||
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) =>
|
||||
index !== currentIndex && item.toLowerCase() === newValue.toLowerCase()
|
||||
);
|
||||
@ -1080,7 +1068,7 @@ const getItemInputClasses = (listName, index, value) => {
|
||||
|
||||
// Filter items based on search query
|
||||
const getFilteredItems = (listName) => {
|
||||
const items = safeGetField(listName, formStore.previewFormData) || [];
|
||||
const items = safeGetField(listName, formStore.previewFormData, { warn: false }) || [];
|
||||
const query = searchQuery.value[listName];
|
||||
|
||||
if (!query || !props.component.props.enableSearch) {
|
||||
@ -1113,13 +1101,13 @@ const toggleItemSelection = (listName, index) => {
|
||||
};
|
||||
|
||||
const isAllSelected = (listName) => {
|
||||
const items = safeGetField(listName, formStore.previewFormData) || [];
|
||||
const items = safeGetField(listName, formStore.previewFormData, { warn: false }) || [];
|
||||
const selected = selectedItems.value[listName] || [];
|
||||
return items.length > 0 && selected.length === items.length;
|
||||
};
|
||||
|
||||
const toggleSelectAll = (listName) => {
|
||||
const items = safeGetField(listName, formStore.previewFormData) || [];
|
||||
const items = safeGetField(listName, formStore.previewFormData, { warn: false }) || [];
|
||||
|
||||
if (isAllSelected(listName)) {
|
||||
selectedItems.value[listName] = [];
|
||||
@ -1130,7 +1118,7 @@ const toggleSelectAll = (listName) => {
|
||||
|
||||
const deleteSelectedItems = (listName) => {
|
||||
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] || [];
|
||||
|
||||
// Sort indices in descending order to avoid index shifting issues
|
||||
@ -1152,7 +1140,7 @@ const deleteSelectedItems = (listName) => {
|
||||
|
||||
// Import/Export functionality
|
||||
const exportItems = (listName, format) => {
|
||||
const items = safeGetField(listName, formStore.previewFormData) || [];
|
||||
const items = safeGetField(listName, formStore.previewFormData, { warn: false }) || [];
|
||||
let content = '';
|
||||
let filename = `${listName}_items`;
|
||||
let mimeType = 'text/plain';
|
||||
@ -1203,7 +1191,7 @@ const showImportModal = (listName) => {
|
||||
} else {
|
||||
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 maxItems = props.component.props.maxItems || 20;
|
||||
if (newItems.length > maxItems) {
|
||||
@ -1344,41 +1332,170 @@ const updateListItems = (listName, newItems) => {
|
||||
const getTableData = (tableName) => {
|
||||
if (!tableName) return [];
|
||||
|
||||
// Directly check the form store without using safeGetField to avoid warnings
|
||||
const formData = formStore.previewFormData || {};
|
||||
// Use safeGetField for consistent data access
|
||||
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 (formData.hasOwnProperty(tableName) && Array.isArray(formData[tableName])) {
|
||||
return formData[tableName];
|
||||
if (Array.isArray(currentData)) {
|
||||
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 updatedFormData = { ...formData, [tableName]: initialData };
|
||||
const updatedFormData = { ...formStore.previewFormData, [tableName]: initialData };
|
||||
formStore.updatePreviewFormData(updatedFormData);
|
||||
|
||||
console.log('[ComponentPreview] getTableData: initialized new data for:', tableName);
|
||||
|
||||
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
|
||||
const updateTableData = (newData) => {
|
||||
const tableName = props.component.props.name;
|
||||
if (!tableName) return;
|
||||
|
||||
console.log('[ComponentPreview] updateTableData:', tableName, 'newData:', newData);
|
||||
|
||||
// Ensure newData is always an array
|
||||
const safeNewData = Array.isArray(newData) ? newData : [];
|
||||
|
||||
// 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 newDataStr = JSON.stringify(safeNewData);
|
||||
|
||||
if (currentDataStr === newDataStr) return;
|
||||
if (currentDataStr === newDataStr) {
|
||||
console.log('[ComponentPreview] updateTableData: no change detected, skipping update');
|
||||
return;
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
const updatedFormData = { ...formStore.previewFormData, [tableName]: safeNewData };
|
||||
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('form-data-updated', updatedFormData);
|
||||
});
|
||||
@ -1386,8 +1503,8 @@ const updateTableData = (newData) => {
|
||||
|
||||
// Form Section Component
|
||||
const toggleSectionCollapse = (sectionId) => {
|
||||
// Find the section component and toggle its collapsed state
|
||||
const section = formStore.formComponents.find(comp => comp.id === sectionId);
|
||||
// Find the section component at any nesting level
|
||||
const section = findContainerRecursively(sectionId);
|
||||
if (section) {
|
||||
section.props.collapsed = !section.props.collapsed;
|
||||
formStore.updateComponent(section);
|
||||
@ -2066,6 +2183,15 @@ onMounted(() => {
|
||||
|
||||
.repeating-table-wrapper{
|
||||
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 */
|
||||
|
@ -56,22 +56,22 @@
|
||||
<!-- Data Table -->
|
||||
<div class="table-container">
|
||||
<!-- 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">
|
||||
<Icon name="heroicons:information-circle" class="warning-icon" />
|
||||
<div class="warning-text">
|
||||
<p class="warning-title">Performance Notice</p>
|
||||
<p class="warning-message">
|
||||
This table has {{ config.columns.length }} columns. For optimal performance, only the first 20 columns are displayed.
|
||||
Consider reducing the number of columns for better user experience.
|
||||
This table has {{ config.columns.length }} columns. For optimal performance, only the first 50 columns are displayed.
|
||||
The table is scrollable horizontally to view all data. Consider reducing the number of columns for better user experience.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="paginatedData.length > 0" class="table-wrapper">
|
||||
<div class="table-container" ref="tableContainer">
|
||||
<table class="data-table">
|
||||
<div class="table-scroll-container" ref="tableContainer">
|
||||
<table class="data-table" :style="{ minWidth: calculateMinTableWidth }">
|
||||
<thead class="table-header">
|
||||
<tr>
|
||||
<th v-if="config.showRowNumbers" class="row-number-header">#</th>
|
||||
@ -79,7 +79,7 @@
|
||||
v-for="column in visibleColumns"
|
||||
:key="column.name"
|
||||
class="column-header"
|
||||
:style="{ width: getColumnWidth(column) }"
|
||||
:style="{ width: getColumnWidth(column), minWidth: getColumnWidth(column) }"
|
||||
>
|
||||
<div class="column-header-content">
|
||||
<span class="header-text">{{ column.label }}</span>
|
||||
@ -103,7 +103,7 @@
|
||||
v-for="column in visibleColumns"
|
||||
:key="column.name"
|
||||
class="data-cell"
|
||||
:style="{ width: getColumnWidth(column) }"
|
||||
:style="{ width: getColumnWidth(column), minWidth: getColumnWidth(column) }"
|
||||
>
|
||||
<div class="cell-content">
|
||||
<SimpleCellValue
|
||||
@ -415,33 +415,55 @@ const filteredData = computed(() => {
|
||||
// Column virtualization for large datasets
|
||||
const visibleColumns = computed(() => {
|
||||
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) {
|
||||
return columns
|
||||
}
|
||||
|
||||
// Return first 20 columns for performance
|
||||
// Return first 50 columns for performance, but allow scrolling
|
||||
return columns.slice(0, maxColumns)
|
||||
})
|
||||
|
||||
// Column width calculation
|
||||
const getColumnWidth = (column) => {
|
||||
const baseWidth = 150
|
||||
const baseWidth = 150 // Increased base width
|
||||
const typeWidths = {
|
||||
text: 150,
|
||||
number: 100,
|
||||
email: 200,
|
||||
email: 180,
|
||||
tel: 120,
|
||||
date: 120,
|
||||
time: 100,
|
||||
url: 200,
|
||||
url: 180,
|
||||
select: 150,
|
||||
checkbox: 80,
|
||||
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
|
||||
@ -676,6 +698,51 @@ const goToPage = (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
|
||||
onUnmounted(() => {
|
||||
columnCache.value.clear()
|
||||
@ -686,21 +753,33 @@ onUnmounted(() => {
|
||||
<style scoped>
|
||||
/* Table Container with Sticky Header */
|
||||
.table-container {
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #cbd5e1 #f1f5f9;
|
||||
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 {
|
||||
table-layout: fixed;
|
||||
table-layout: auto; /* Changed from fixed to auto for better column sizing */
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 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 */
|
||||
@ -718,42 +797,47 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
/* Custom scrollbar for webkit browsers */
|
||||
.table-container::-webkit-scrollbar {
|
||||
.table-scroll-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-track {
|
||||
.table-scroll-container::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb {
|
||||
.table-scroll-container::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb:hover {
|
||||
.table-scroll-container::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
/* Horizontal scrollbar styling */
|
||||
.table-scroll-container::-webkit-scrollbar:horizontal {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.column-header,
|
||||
.data-cell {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 0;
|
||||
padding: 8px 12px;
|
||||
vertical-align: middle;
|
||||
/* Remove text truncation to allow full content display */
|
||||
}
|
||||
|
||||
.cell-content {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
/* Allow content to expand naturally */
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.cell-value {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
/* Remove text truncation to show full content */
|
||||
}
|
||||
|
||||
/* Virtual scrolling optimizations */
|
||||
@ -801,7 +885,7 @@ onUnmounted(() => {
|
||||
|
||||
/* Responsive optimizations */
|
||||
@media (max-width: 768px) {
|
||||
.table-content {
|
||||
.table-scroll-container {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
@ -813,7 +897,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.table-content {
|
||||
.table-scroll-container {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
@ -823,6 +907,35 @@ onUnmounted(() => {
|
||||
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 {
|
||||
@apply border border-gray-200 rounded-lg overflow-hidden bg-white shadow-sm w-full;
|
||||
min-width: 0;
|
||||
|
@ -1,5 +1,5 @@
|
||||
// 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)) {
|
||||
const value = formData[field];
|
||||
// If the value is undefined or null, return empty string for backward compatibility
|
||||
@ -8,9 +8,14 @@ export function safeGetField(field, formData) {
|
||||
}
|
||||
return value;
|
||||
}
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// Only warn in development
|
||||
console.warn(`Field '${field}' is missing or inaccessible.`);
|
||||
|
||||
// Only warn for specific cases or when explicitly requested
|
||||
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 '';
|
||||
}
|
@ -285,8 +285,7 @@
|
||||
ref="previewForm"
|
||||
type="form"
|
||||
@submit="handlePreviewSubmit"
|
||||
:actions="false"
|
||||
v-model="previewFormData"
|
||||
:actions="false"
|
||||
>
|
||||
<div
|
||||
class="grid-preview-container"
|
||||
@ -2069,43 +2068,43 @@ const handleJsonImport = (event) => {
|
||||
// Ensure all required info display properties
|
||||
processedProps = {
|
||||
title: processedProps.title || 'Information',
|
||||
name: processedDefaultProps.name || `info_display_${index + 1}`,
|
||||
help: processedDefaultProps.help || '',
|
||||
layout: processedDefaultProps.layout || 'vertical',
|
||||
showBorder: processedDefaultProps.showBorder !== undefined ? processedDefaultProps.showBorder : true,
|
||||
backgroundColor: processedDefaultProps.backgroundColor || '#f8fafc',
|
||||
fields: Array.isArray(processedDefaultProps.fields) ? processedDefaultProps.fields : [
|
||||
name: processedProps.name || `info_display_${index + 1}`,
|
||||
help: processedProps.help || '',
|
||||
layout: processedProps.layout || 'vertical',
|
||||
showBorder: processedProps.showBorder !== undefined ? processedProps.showBorder : true,
|
||||
backgroundColor: processedProps.backgroundColor || '#f8fafc',
|
||||
fields: Array.isArray(processedProps.fields) ? processedProps.fields : [
|
||||
{ label: 'Info Item', value: 'Value', key: 'item_1' }
|
||||
],
|
||||
...processedDefaultProps
|
||||
...processedProps
|
||||
};
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
// Ensure all required file upload properties
|
||||
processedProps = {
|
||||
label: processedDefaultProps.label || 'File Upload',
|
||||
name: processedDefaultProps.name || `file_upload_${index + 1}`,
|
||||
help: processedDefaultProps.help || 'Upload a file',
|
||||
accept: processedDefaultProps.accept || '.pdf,.doc,.docx,.jpg,.jpeg,.png',
|
||||
...processedDefaultProps
|
||||
label: processedProps.label || 'File Upload',
|
||||
name: processedProps.name || `file_upload_${index + 1}`,
|
||||
help: processedProps.help || 'Upload a file',
|
||||
accept: processedProps.accept || '.pdf,.doc,.docx,.jpg,.jpeg,.png',
|
||||
...processedProps
|
||||
};
|
||||
break;
|
||||
|
||||
case 'heading':
|
||||
// Ensure all required heading properties
|
||||
processedProps = {
|
||||
value: processedDefaultProps.value || 'Heading',
|
||||
level: processedDefaultProps.level || 2,
|
||||
...processedDefaultProps
|
||||
value: processedProps.value || 'Heading',
|
||||
level: processedProps.level || 2,
|
||||
...processedProps
|
||||
};
|
||||
break;
|
||||
|
||||
case 'paragraph':
|
||||
// Ensure all required paragraph properties
|
||||
processedProps = {
|
||||
value: processedDefaultProps.value || 'Paragraph text',
|
||||
...processedDefaultProps
|
||||
value: processedProps.value || 'Paragraph text',
|
||||
...processedProps
|
||||
};
|
||||
break;
|
||||
|
||||
@ -2113,7 +2112,7 @@ const handleJsonImport = (event) => {
|
||||
case 'radio':
|
||||
case 'checkbox':
|
||||
// Ensure options array exists
|
||||
if (!Array.isArray(processedDefaultProps.options) || processedDefaultProps.options.length === 0) {
|
||||
if (!Array.isArray(processedProps.options) || processedProps.options.length === 0) {
|
||||
processedDefaultProps.options = [
|
||||
{ label: 'Option 1', value: 'option_1' },
|
||||
{ label: 'Option 2', value: 'option_2' }
|
||||
@ -2537,18 +2536,13 @@ const saveFormSettings = () => {
|
||||
// Preview form data for script interactions
|
||||
const previewFormData = ref({});
|
||||
|
||||
// Initialize preview form data with default values
|
||||
watchEffect(() => {
|
||||
console.log('[FormBuilder] watchEffect for previewFormData initialization. Current form components:', formStore.formComponents.length);
|
||||
const existingFormData = { ...previewFormData.value }; // Preserve current user inputs
|
||||
let newDefaults = {};
|
||||
let hasNewComponents = false;
|
||||
|
||||
formStore.formComponents.forEach(component => {
|
||||
// Helper function to process nested components recursively
|
||||
const processNestedComponents = (components, existingFormData, newDefaults, hasNewComponents) => {
|
||||
components.forEach(component => {
|
||||
if (component.props.name) {
|
||||
// If field is not already in existingFormData, it's a new component or needs initialization
|
||||
if (!existingFormData.hasOwnProperty(component.props.name)) {
|
||||
hasNewComponents = true;
|
||||
hasNewComponents.value = true;
|
||||
// Set default values based on component type
|
||||
switch (component.type) {
|
||||
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
|
||||
// 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;
|
||||
|
||||
if (isInitialLoad || hasNewComponents) {
|
||||
console.log('[FormBuilder] Initializing/merging preview form data. Initial load:', isInitialLoad, 'Has new components:', hasNewComponents);
|
||||
previewFormData.value = { ...existingFormData, ...newDefaults };
|
||||
console.log('[FormBuilder] Preview form data after init/merge:', previewFormData.value);
|
||||
} else {
|
||||
console.log('[FormBuilder] Skipping full previewFormData re-initialization to preserve user input.');
|
||||
// CRITICAL FIX: Don't reinitialize if we already have data for repeating groups/arrays
|
||||
// This prevents data loss when users are editing repeating group items
|
||||
const hasExistingArrayData = Object.keys(existingFormData).some(key => Array.isArray(existingFormData[key]) && existingFormData[key].length > 0);
|
||||
|
||||
if (isInitialLoad || (hasNewComponents.value && !hasExistingArrayData)) {
|
||||
// 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
|
||||
const handleScriptFieldChange = ({ fieldName, value }) => {
|
||||
console.log('[FormBuilder] Script field change:', fieldName, '=', value);
|
||||
|
||||
// Update the reactive form data
|
||||
const newFormData = {
|
||||
...previewFormData.value,
|
||||
@ -2652,15 +2669,12 @@ const handleScriptFieldChange = ({ fieldName, value }) => {
|
||||
|
||||
// Try to force FormKit form to update
|
||||
nextTick(() => {
|
||||
console.log('[FormBuilder] Updated form data:', newFormData);
|
||||
|
||||
// Try to access the FormKit form node and update it directly
|
||||
if (previewForm.value && previewForm.value.node) {
|
||||
try {
|
||||
previewForm.value.node.input(newFormData);
|
||||
console.log('[FormBuilder] Force updated FormKit form');
|
||||
} 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
|
||||
const handleScriptFieldValidate = ({ fieldName }) => {
|
||||
// Could integrate with FormKit validation here
|
||||
console.log(`Validating field: ${fieldName}`);
|
||||
};
|
||||
|
||||
// Handle FormKit form input events
|
||||
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
|
||||
const oldPreviewData = { ...previewFormData.value };
|
||||
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
|
||||
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
|
||||
@ -2702,9 +2706,13 @@ watch(() => previewFormData.value, (newData, oldData) => {
|
||||
return; // No actual change, skip update
|
||||
}
|
||||
|
||||
console.log('[FormBuilder] previewFormData watcher triggered!');
|
||||
console.log('[FormBuilder] New data:', newDataStr);
|
||||
console.log('[FormBuilder] Old data:', oldDataStr);
|
||||
// CRITICAL FIX: Don't sync back to store if we have any array data at all
|
||||
// ComponentPreview.vue manages its own array data directly with the store
|
||||
const hasArrayData = Object.keys(newData).some(key => Array.isArray(newData[key]));
|
||||
|
||||
if (hasArrayData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use nextTick to avoid synchronous updates that can cause recursion
|
||||
nextTick(() => {
|
||||
@ -2712,7 +2720,6 @@ watch(() => previewFormData.value, (newData, oldData) => {
|
||||
const currentDataStr = JSON.stringify(previewFormData.value);
|
||||
if (currentDataStr === newDataStr) {
|
||||
formStore.updatePreviewFormData(newData);
|
||||
console.log('[FormBuilder] FormStore preview data updated to:', JSON.parse(JSON.stringify(formStore.previewFormData)));
|
||||
}
|
||||
});
|
||||
}, { deep: true, immediate: false });
|
||||
@ -3379,6 +3386,8 @@ const handleFormRestored = (restoredForm) => {
|
||||
console.log('Form restored:', restoredForm);
|
||||
// You might want to update the form state or show a success message
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
Loading…
x
Reference in New Issue
Block a user