Enhance ComponentPreview and RepeatingTable with Debugging and Loading States
- Updated ComponentPreview.vue to include debugging logs for table data and component mounting, improving traceability during development. - Modified RepeatingTable.vue to replace the SimpleCellValue component with direct template rendering for better performance and clarity. - Enhanced form submission logging in RepeatingTable.vue to provide detailed insights into form data handling. - Introduced loading state styles in ComponentPreview.vue to improve user experience during data fetching and rendering. - Ensured SSR safety in form data updates within formBuilder.js to prevent hydration issues, enhancing application stability.
This commit is contained in:
parent
bf5e1630b3
commit
cc5093655a
@ -482,7 +482,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Repeating Table Component -->
|
||||
<!-- Repeating Table Component -->
|
||||
<div v-else-if="component.type === 'repeating-table'" class="repeating-table-wrapper">
|
||||
<div class="table-container-wrapper">
|
||||
<RepeatingTable
|
||||
@ -490,6 +490,8 @@
|
||||
:model-value="getTableData(component.props.name)"
|
||||
:is-preview="isPreview"
|
||||
@update:model-value="updateTableData"
|
||||
@debug-data="(data) => console.log('[ComponentPreview] Table debug data:', data)"
|
||||
@mounted="() => console.log('[ComponentPreview] RepeatingTable mounted with data:', getTableData(component.props.name))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -1411,12 +1413,13 @@ const getTableData = (tableName) => {
|
||||
return currentData;
|
||||
}
|
||||
|
||||
// If data doesn't exist, initialize it immediately
|
||||
// If data doesn't exist, initialize it
|
||||
const initialData = [];
|
||||
const updatedFormData = { ...formStore.previewFormData, [tableName]: initialData };
|
||||
formStore.updatePreviewFormData(updatedFormData);
|
||||
|
||||
console.log('[ComponentPreview] getTableData: initialized new data for:', tableName);
|
||||
nextTick(() => {
|
||||
const updatedFormData = { ...formStore.previewFormData, [tableName]: initialData };
|
||||
formStore.updatePreviewFormData(updatedFormData);
|
||||
console.log('[ComponentPreview] getTableData: initialized new data for:', tableName);
|
||||
});
|
||||
|
||||
return initialData;
|
||||
};
|
||||
@ -2513,6 +2516,47 @@ onMounted(() => {
|
||||
box-shadow: 0 0 15px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
/* Table Loading State Styles */
|
||||
.table-loading-state {
|
||||
@apply border border-gray-200 rounded-lg overflow-hidden bg-white shadow-sm w-full;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loading-header {
|
||||
@apply p-4 md:p-6 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200;
|
||||
}
|
||||
|
||||
.loading-title {
|
||||
@apply text-lg font-semibold text-gray-800 leading-tight mb-1;
|
||||
}
|
||||
|
||||
.loading-subtitle {
|
||||
@apply text-sm text-gray-600 leading-relaxed;
|
||||
}
|
||||
|
||||
.loading-table {
|
||||
@apply p-4;
|
||||
}
|
||||
|
||||
.loading-row {
|
||||
@apply flex gap-4 mb-3;
|
||||
}
|
||||
|
||||
.loading-header-row {
|
||||
@apply border-b border-gray-200 pb-3 mb-4;
|
||||
}
|
||||
|
||||
.loading-cell {
|
||||
@apply flex-1 h-6 bg-gray-200 rounded animate-pulse;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.loading-header-row .loading-cell {
|
||||
@apply h-4 bg-gray-300;
|
||||
}
|
||||
|
||||
/* Custom HTML Component Styles */
|
||||
.custom-html-wrapper {
|
||||
width: 100%;
|
||||
|
@ -106,11 +106,10 @@
|
||||
:style="{ width: getColumnWidth(column), minWidth: getColumnWidth(column) }"
|
||||
>
|
||||
<div class="cell-content">
|
||||
<SimpleCellValue
|
||||
:value="record[column.name]"
|
||||
:column="column"
|
||||
:record="record"
|
||||
/>
|
||||
<!-- Direct template rendering instead of SimpleCellValue component -->
|
||||
<span class="cell-value">
|
||||
{{ formatCellValue(record[column.name], column) }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td v-if="showActions" class="actions-cell">
|
||||
@ -222,7 +221,7 @@
|
||||
type="form"
|
||||
:actions="false"
|
||||
v-model="formData"
|
||||
@submit="saveRecord"
|
||||
@submit="(formData) => { console.log('[RepeatingTable] Form submitted with:', formData); saveRecord(formData); }"
|
||||
class="record-form"
|
||||
>
|
||||
<div class="form-grid">
|
||||
@ -298,59 +297,7 @@ import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
|
||||
// Simple cell component that shows data immediately
|
||||
const SimpleCellValue = defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Boolean, Object],
|
||||
default: null
|
||||
},
|
||||
column: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
record: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const formatValue = computed(() => {
|
||||
const value = props.value
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '-'
|
||||
}
|
||||
|
||||
if (props.column.type === 'date') {
|
||||
return new Date(value).toLocaleDateString()
|
||||
}
|
||||
|
||||
if (props.column.type === 'time') {
|
||||
return value
|
||||
}
|
||||
|
||||
if (props.column.type === 'checkbox') {
|
||||
return value ? 'Yes' : 'No'
|
||||
}
|
||||
|
||||
if (props.column.type === 'select' && props.column.options) {
|
||||
const option = props.column.options.find(opt => opt.value === value)
|
||||
return option ? option.label : value
|
||||
}
|
||||
|
||||
return value.toString()
|
||||
})
|
||||
|
||||
return {
|
||||
formatValue
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<span class="cell-value">
|
||||
{{ formatValue }}
|
||||
</span>
|
||||
`
|
||||
})
|
||||
// SimpleCellValue component removed - using direct template rendering instead
|
||||
|
||||
const props = defineProps({
|
||||
config: {
|
||||
@ -367,7 +314,7 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits(['update:modelValue', 'debug-data', 'mounted'])
|
||||
|
||||
// Reactive state
|
||||
const data = ref([...(props.modelValue || [])])
|
||||
@ -380,6 +327,9 @@ const formData = ref({})
|
||||
const formId = ref(`table-form-${uuidv4()}`)
|
||||
const tableContainer = ref(null)
|
||||
|
||||
// Debug flag
|
||||
const debug = ref(true)
|
||||
|
||||
// Pagination state
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
@ -401,15 +351,20 @@ watch(searchQuery, (newQuery) => {
|
||||
|
||||
// Computed properties with memoization
|
||||
const filteredData = computed(() => {
|
||||
if (!searchQuery.value) return data.value
|
||||
if (!searchQuery.value) {
|
||||
console.log('[RepeatingTable] filteredData (no search):', data.value)
|
||||
return data.value
|
||||
}
|
||||
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
return data.value.filter(record => {
|
||||
const result = data.value.filter(record => {
|
||||
return props.config.columns.some(column => {
|
||||
const value = record[column.name]
|
||||
return value && value.toString().toLowerCase().includes(query)
|
||||
})
|
||||
})
|
||||
console.log('[RepeatingTable] filteredData (with search):', result)
|
||||
return result
|
||||
})
|
||||
|
||||
// Column virtualization for large datasets
|
||||
@ -466,12 +421,11 @@ const getColumnWidth = (column) => {
|
||||
return width
|
||||
}
|
||||
|
||||
// Record key generation for better Vue rendering
|
||||
// Record key generation for better Vue rendering - production safe
|
||||
const getRecordKey = (record, index) => {
|
||||
if (!recordKeys.value.has(record)) {
|
||||
recordKeys.value.set(record, `record-${index}-${Date.now()}`)
|
||||
}
|
||||
return recordKeys.value.get(record)
|
||||
// Use a more reliable key generation that works in production
|
||||
const recordId = record.id || record._id || `record-${index}`
|
||||
return `${recordId}-${index}`
|
||||
}
|
||||
|
||||
const isAddDisabled = computed(() => {
|
||||
@ -494,7 +448,15 @@ const totalPages = computed(() => {
|
||||
const paginatedData = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return filteredData.value.slice(start, end)
|
||||
const result = filteredData.value.slice(start, end)
|
||||
console.log('[RepeatingTable] paginatedData computed:', {
|
||||
start,
|
||||
end,
|
||||
filteredDataLength: filteredData.value.length,
|
||||
resultLength: result.length,
|
||||
result: result
|
||||
})
|
||||
return result
|
||||
})
|
||||
|
||||
const startRecord = computed(() => {
|
||||
@ -538,7 +500,7 @@ const visiblePages = computed(() => {
|
||||
// Guard to prevent recursive updates
|
||||
const isUpdatingFromProps = ref(false)
|
||||
|
||||
// Watch for external data changes
|
||||
// Watch for external data changes with SSR safety
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
// Handle the case where newValue might be undefined/null
|
||||
const safeNewValue = newValue || []
|
||||
@ -549,6 +511,12 @@ watch(() => props.modelValue, (newValue) => {
|
||||
|
||||
if (newDataStr === currentDataStr) return
|
||||
|
||||
// Prevent hydration issues by ensuring updates happen on client side
|
||||
if (process.server) {
|
||||
data.value = [...safeNewValue]
|
||||
return
|
||||
}
|
||||
|
||||
isUpdatingFromProps.value = true
|
||||
data.value = [...safeNewValue]
|
||||
// Clear caches when data changes
|
||||
@ -564,14 +532,20 @@ watch(() => props.modelValue, (newValue) => {
|
||||
watch(data, (newData, oldData) => {
|
||||
if (isUpdatingFromProps.value) return
|
||||
|
||||
// Prevent emissions during SSR to avoid hydration issues
|
||||
if (process.server) return
|
||||
|
||||
// Check if data actually changed
|
||||
const newDataStr = JSON.stringify(newData)
|
||||
const oldDataStr = JSON.stringify(oldData)
|
||||
|
||||
if (newDataStr === oldDataStr) return
|
||||
|
||||
console.log('[RepeatingTable] Data changed, emitting update:', newData)
|
||||
|
||||
nextTick(() => {
|
||||
emit('update:modelValue', [...newData])
|
||||
emit('debug-data', newData)
|
||||
})
|
||||
}, { deep: true })
|
||||
|
||||
@ -585,6 +559,7 @@ const openAddModal = () => {
|
||||
formData.value[column.name] = getDefaultValue(column.type)
|
||||
})
|
||||
|
||||
console.log('[RepeatingTable] openAddModal - initialized formData:', formData.value)
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
@ -608,15 +583,36 @@ const submitForm = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const saveRecord = (formData) => {
|
||||
const saveRecord = (submittedFormData) => {
|
||||
console.log('[RepeatingTable] saveRecord called with:', submittedFormData)
|
||||
console.log('[RepeatingTable] Current data before save:', data.value)
|
||||
|
||||
if (editingIndex.value !== null) {
|
||||
// Update existing record
|
||||
data.value[editingIndex.value] = { ...formData }
|
||||
data.value[editingIndex.value] = {
|
||||
...submittedFormData,
|
||||
id: data.value[editingIndex.value].id || `record-${editingIndex.value}-${Date.now()}`
|
||||
}
|
||||
console.log('[RepeatingTable] Updated record at index:', editingIndex.value, 'with data:', submittedFormData)
|
||||
} else {
|
||||
// Add new record
|
||||
data.value.push({ ...formData })
|
||||
// Add new record with unique ID
|
||||
const newRecord = {
|
||||
...submittedFormData,
|
||||
id: `record-${data.value.length}-${Date.now()}`
|
||||
}
|
||||
data.value.push(newRecord)
|
||||
console.log('[RepeatingTable] Added new record with data:', newRecord)
|
||||
}
|
||||
|
||||
console.log('[RepeatingTable] Data after save:', data.value)
|
||||
|
||||
// Force reactivity update in production
|
||||
nextTick(() => {
|
||||
// Trigger a reactive update by reassigning the array
|
||||
data.value = [...data.value]
|
||||
console.log('[RepeatingTable] Forced reactivity update, data:', data.value)
|
||||
})
|
||||
|
||||
closeModal()
|
||||
}
|
||||
|
||||
@ -698,6 +694,34 @@ const goToPage = (page) => {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
// Format cell value directly in template
|
||||
const formatCellValue = (value, column) => {
|
||||
console.log(`[formatCellValue] Column: ${column.name}, Value:`, value, 'Type:', typeof value)
|
||||
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '-'
|
||||
}
|
||||
|
||||
if (column.type === 'date') {
|
||||
return new Date(value).toLocaleDateString()
|
||||
}
|
||||
|
||||
if (column.type === 'time') {
|
||||
return value
|
||||
}
|
||||
|
||||
if (column.type === 'checkbox') {
|
||||
return value ? 'Yes' : 'No'
|
||||
}
|
||||
|
||||
if (column.type === 'select' && column.options) {
|
||||
const option = column.options.find(opt => opt.value === value)
|
||||
return option ? option.label : value
|
||||
}
|
||||
|
||||
return value.toString()
|
||||
}
|
||||
|
||||
// Calculate minimum table width based on columns
|
||||
const calculateMinTableWidth = computed(() => {
|
||||
if (!props.config.columns) return 'auto'
|
||||
@ -729,6 +753,9 @@ const calculateMinTableWidth = computed(() => {
|
||||
|
||||
// Debug table width on mount
|
||||
onMounted(() => {
|
||||
console.log('[RepeatingTable] Component mounted with data:', data.value)
|
||||
emit('mounted')
|
||||
|
||||
nextTick(() => {
|
||||
if (tableContainer.value) {
|
||||
const table = tableContainer.value.querySelector('.data-table')
|
||||
|
39
plugins/form-hydration.client.js
Normal file
39
plugins/form-hydration.client.js
Normal file
@ -0,0 +1,39 @@
|
||||
export default defineNuxtPlugin(() => {
|
||||
// This plugin runs only on the client side to handle form data hydration
|
||||
const { $formStore } = useNuxtApp()
|
||||
|
||||
// Ensure form store data is properly initialized on client hydration
|
||||
if (process.client) {
|
||||
// Add any additional client-side form data initialization logic here
|
||||
console.log('[FormHydration] Client-side form data hydration plugin loaded')
|
||||
|
||||
// Handle any form data that needs to be restored after hydration
|
||||
const restoreFormData = () => {
|
||||
try {
|
||||
// Check if there's any persisted form data that needs restoration
|
||||
const persistedData = sessionStorage.getItem('form-builder-data')
|
||||
if (persistedData && $formStore) {
|
||||
const parsed = JSON.parse(persistedData)
|
||||
if (parsed.previewFormData) {
|
||||
$formStore.updatePreviewFormData(parsed.previewFormData)
|
||||
console.log('[FormHydration] Restored form data from session storage')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[FormHydration] Failed to restore form data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Restore data after page load
|
||||
window.addEventListener('load', restoreFormData)
|
||||
|
||||
// Clean up
|
||||
return {
|
||||
provide: {
|
||||
formHydration: {
|
||||
restoreFormData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
@ -1002,9 +1002,21 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
});
|
||||
},
|
||||
|
||||
// Update preview form data
|
||||
// Update preview form data - SSR safe
|
||||
updatePreviewFormData(data) {
|
||||
this.previewFormData = { ...data };
|
||||
|
||||
// Only persist to sessionStorage on client side to prevent hydration issues
|
||||
if (process.client) {
|
||||
try {
|
||||
sessionStorage.setItem('form-builder-data', JSON.stringify({
|
||||
previewFormData: this.previewFormData,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
} catch (error) {
|
||||
console.warn('[FormStore] Failed to persist form data:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Optimize the grid layout by analyzing the current components
|
||||
|
Loading…
x
Reference in New Issue
Block a user