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
@ -490,6 +490,8 @@
|
|||||||
:model-value="getTableData(component.props.name)"
|
:model-value="getTableData(component.props.name)"
|
||||||
:is-preview="isPreview"
|
:is-preview="isPreview"
|
||||||
@update:model-value="updateTableData"
|
@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>
|
||||||
</div>
|
</div>
|
||||||
@ -1411,12 +1413,13 @@ const getTableData = (tableName) => {
|
|||||||
return currentData;
|
return currentData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If data doesn't exist, initialize it immediately
|
// If data doesn't exist, initialize it
|
||||||
const initialData = [];
|
const initialData = [];
|
||||||
|
nextTick(() => {
|
||||||
const updatedFormData = { ...formStore.previewFormData, [tableName]: initialData };
|
const updatedFormData = { ...formStore.previewFormData, [tableName]: initialData };
|
||||||
formStore.updatePreviewFormData(updatedFormData);
|
formStore.updatePreviewFormData(updatedFormData);
|
||||||
|
|
||||||
console.log('[ComponentPreview] getTableData: initialized new data for:', tableName);
|
console.log('[ComponentPreview] getTableData: initialized new data for:', tableName);
|
||||||
|
});
|
||||||
|
|
||||||
return initialData;
|
return initialData;
|
||||||
};
|
};
|
||||||
@ -2513,6 +2516,47 @@ onMounted(() => {
|
|||||||
box-shadow: 0 0 15px rgba(59, 130, 246, 0.5);
|
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 Component Styles */
|
||||||
.custom-html-wrapper {
|
.custom-html-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -106,11 +106,10 @@
|
|||||||
:style="{ width: getColumnWidth(column), minWidth: getColumnWidth(column) }"
|
:style="{ width: getColumnWidth(column), minWidth: getColumnWidth(column) }"
|
||||||
>
|
>
|
||||||
<div class="cell-content">
|
<div class="cell-content">
|
||||||
<SimpleCellValue
|
<!-- Direct template rendering instead of SimpleCellValue component -->
|
||||||
:value="record[column.name]"
|
<span class="cell-value">
|
||||||
:column="column"
|
{{ formatCellValue(record[column.name], column) }}
|
||||||
:record="record"
|
</span>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="showActions" class="actions-cell">
|
<td v-if="showActions" class="actions-cell">
|
||||||
@ -222,7 +221,7 @@
|
|||||||
type="form"
|
type="form"
|
||||||
:actions="false"
|
:actions="false"
|
||||||
v-model="formData"
|
v-model="formData"
|
||||||
@submit="saveRecord"
|
@submit="(formData) => { console.log('[RepeatingTable] Form submitted with:', formData); saveRecord(formData); }"
|
||||||
class="record-form"
|
class="record-form"
|
||||||
>
|
>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
@ -298,59 +297,7 @@ import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { useDebounceFn } from '@vueuse/core'
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
|
|
||||||
// Simple cell component that shows data immediately
|
// SimpleCellValue component removed - using direct template rendering instead
|
||||||
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>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
config: {
|
config: {
|
||||||
@ -367,7 +314,7 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue', 'debug-data', 'mounted'])
|
||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
const data = ref([...(props.modelValue || [])])
|
const data = ref([...(props.modelValue || [])])
|
||||||
@ -380,6 +327,9 @@ const formData = ref({})
|
|||||||
const formId = ref(`table-form-${uuidv4()}`)
|
const formId = ref(`table-form-${uuidv4()}`)
|
||||||
const tableContainer = ref(null)
|
const tableContainer = ref(null)
|
||||||
|
|
||||||
|
// Debug flag
|
||||||
|
const debug = ref(true)
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
@ -401,15 +351,20 @@ watch(searchQuery, (newQuery) => {
|
|||||||
|
|
||||||
// Computed properties with memoization
|
// Computed properties with memoization
|
||||||
const filteredData = computed(() => {
|
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()
|
const query = searchQuery.value.toLowerCase()
|
||||||
return data.value.filter(record => {
|
const result = data.value.filter(record => {
|
||||||
return props.config.columns.some(column => {
|
return props.config.columns.some(column => {
|
||||||
const value = record[column.name]
|
const value = record[column.name]
|
||||||
return value && value.toString().toLowerCase().includes(query)
|
return value && value.toString().toLowerCase().includes(query)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
console.log('[RepeatingTable] filteredData (with search):', result)
|
||||||
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
// Column virtualization for large datasets
|
// Column virtualization for large datasets
|
||||||
@ -466,12 +421,11 @@ const getColumnWidth = (column) => {
|
|||||||
return width
|
return width
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record key generation for better Vue rendering
|
// Record key generation for better Vue rendering - production safe
|
||||||
const getRecordKey = (record, index) => {
|
const getRecordKey = (record, index) => {
|
||||||
if (!recordKeys.value.has(record)) {
|
// Use a more reliable key generation that works in production
|
||||||
recordKeys.value.set(record, `record-${index}-${Date.now()}`)
|
const recordId = record.id || record._id || `record-${index}`
|
||||||
}
|
return `${recordId}-${index}`
|
||||||
return recordKeys.value.get(record)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAddDisabled = computed(() => {
|
const isAddDisabled = computed(() => {
|
||||||
@ -494,7 +448,15 @@ const totalPages = computed(() => {
|
|||||||
const paginatedData = computed(() => {
|
const paginatedData = computed(() => {
|
||||||
const start = (currentPage.value - 1) * pageSize.value
|
const start = (currentPage.value - 1) * pageSize.value
|
||||||
const end = start + 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(() => {
|
const startRecord = computed(() => {
|
||||||
@ -538,7 +500,7 @@ const visiblePages = computed(() => {
|
|||||||
// Guard to prevent recursive updates
|
// Guard to prevent recursive updates
|
||||||
const isUpdatingFromProps = ref(false)
|
const isUpdatingFromProps = ref(false)
|
||||||
|
|
||||||
// Watch for external data changes
|
// Watch for external data changes with SSR safety
|
||||||
watch(() => props.modelValue, (newValue) => {
|
watch(() => props.modelValue, (newValue) => {
|
||||||
// Handle the case where newValue might be undefined/null
|
// Handle the case where newValue might be undefined/null
|
||||||
const safeNewValue = newValue || []
|
const safeNewValue = newValue || []
|
||||||
@ -549,6 +511,12 @@ watch(() => props.modelValue, (newValue) => {
|
|||||||
|
|
||||||
if (newDataStr === currentDataStr) return
|
if (newDataStr === currentDataStr) return
|
||||||
|
|
||||||
|
// Prevent hydration issues by ensuring updates happen on client side
|
||||||
|
if (process.server) {
|
||||||
|
data.value = [...safeNewValue]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
isUpdatingFromProps.value = true
|
isUpdatingFromProps.value = true
|
||||||
data.value = [...safeNewValue]
|
data.value = [...safeNewValue]
|
||||||
// Clear caches when data changes
|
// Clear caches when data changes
|
||||||
@ -564,14 +532,20 @@ watch(() => props.modelValue, (newValue) => {
|
|||||||
watch(data, (newData, oldData) => {
|
watch(data, (newData, oldData) => {
|
||||||
if (isUpdatingFromProps.value) return
|
if (isUpdatingFromProps.value) return
|
||||||
|
|
||||||
|
// Prevent emissions during SSR to avoid hydration issues
|
||||||
|
if (process.server) return
|
||||||
|
|
||||||
// Check if data actually changed
|
// Check if data actually changed
|
||||||
const newDataStr = JSON.stringify(newData)
|
const newDataStr = JSON.stringify(newData)
|
||||||
const oldDataStr = JSON.stringify(oldData)
|
const oldDataStr = JSON.stringify(oldData)
|
||||||
|
|
||||||
if (newDataStr === oldDataStr) return
|
if (newDataStr === oldDataStr) return
|
||||||
|
|
||||||
|
console.log('[RepeatingTable] Data changed, emitting update:', newData)
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
emit('update:modelValue', [...newData])
|
emit('update:modelValue', [...newData])
|
||||||
|
emit('debug-data', newData)
|
||||||
})
|
})
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
@ -585,6 +559,7 @@ const openAddModal = () => {
|
|||||||
formData.value[column.name] = getDefaultValue(column.type)
|
formData.value[column.name] = getDefaultValue(column.type)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('[RepeatingTable] openAddModal - initialized formData:', formData.value)
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -608,14 +583,35 @@ 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) {
|
if (editingIndex.value !== null) {
|
||||||
// Update existing record
|
// Update existing record
|
||||||
data.value[editingIndex.value] = { ...formData }
|
data.value[editingIndex.value] = {
|
||||||
} else {
|
...submittedFormData,
|
||||||
// Add new record
|
id: data.value[editingIndex.value].id || `record-${editingIndex.value}-${Date.now()}`
|
||||||
data.value.push({ ...formData })
|
|
||||||
}
|
}
|
||||||
|
console.log('[RepeatingTable] Updated record at index:', editingIndex.value, 'with data:', submittedFormData)
|
||||||
|
} else {
|
||||||
|
// 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()
|
closeModal()
|
||||||
}
|
}
|
||||||
@ -698,6 +694,34 @@ const goToPage = (page) => {
|
|||||||
currentPage.value = 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
|
// Calculate minimum table width based on columns
|
||||||
const calculateMinTableWidth = computed(() => {
|
const calculateMinTableWidth = computed(() => {
|
||||||
if (!props.config.columns) return 'auto'
|
if (!props.config.columns) return 'auto'
|
||||||
@ -729,6 +753,9 @@ const calculateMinTableWidth = computed(() => {
|
|||||||
|
|
||||||
// Debug table width on mount
|
// Debug table width on mount
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
console.log('[RepeatingTable] Component mounted with data:', data.value)
|
||||||
|
emit('mounted')
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (tableContainer.value) {
|
if (tableContainer.value) {
|
||||||
const table = tableContainer.value.querySelector('.data-table')
|
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) {
|
updatePreviewFormData(data) {
|
||||||
this.previewFormData = { ...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
|
// Optimize the grid layout by analyzing the current components
|
||||||
|
Loading…
x
Reference in New Issue
Block a user