|
118 | 118 | </template> |
119 | 119 |
|
120 | 120 | <script lang="ts" setup> |
121 | | -import { computed, onBeforeMount, markRaw, ref, watch } from 'vue'; |
| 121 | +import { computed, onBeforeMount, markRaw, ref } from 'vue'; |
122 | 122 | import type { Field, Item } from '@directus/types'; |
123 | 123 | import InlineEditPopover from './InlineEditPopover.vue'; |
124 | 124 | import BooleanToggleCell from './CellRenderers/BooleanToggleCell.vue'; |
@@ -148,47 +148,21 @@ const emit = defineEmits<{ |
148 | 148 | 'navigate-prev': []; |
149 | 149 | }>(); |
150 | 150 |
|
151 | | -// PHASE 1: Initial data cache to prevent corruption during re-renders |
152 | | -const stableDataCache = ref<Item | null>(null); |
| 151 | +// Simple cache for relational objects - only cache on mount to avoid corruption |
| 152 | +const relationalCache = ref<Record<string, any>>({}); |
153 | 153 |
|
154 | | -// SMART CACHE UPDATE: Only cache better data, never worse data |
155 | | -function updateStableCache() { |
156 | | - if (!props.item) return; |
157 | | -
|
158 | | - try { |
159 | | - const newItem = JSON.parse(JSON.stringify(props.item)); |
160 | | -
|
161 | | - // If cache is empty, always update |
162 | | - if (!stableDataCache.value) { |
163 | | - stableDataCache.value = markRaw(newItem); |
164 | | - return; |
165 | | - } |
166 | | -
|
167 | | - // SMART UPDATE: Only update cache if new data has MORE object data |
168 | | - for (const key of Object.keys(newItem)) { |
169 | | - const newValue = newItem[key]; |
170 | | - const cachedValue = stableDataCache.value[key]; |
171 | | -
|
172 | | - // If new value is an object and cached is primitive, update cache |
173 | | - if (newValue && typeof newValue === 'object' && typeof cachedValue !== 'object') { |
174 | | - stableDataCache.value = markRaw(newItem); |
175 | | - return; |
| 154 | +onBeforeMount(() => { |
| 155 | + // Cache relational objects once on mount |
| 156 | + if (props.item) { |
| 157 | + Object.keys(props.item).forEach((key) => { |
| 158 | + const value = props.item[key]; |
| 159 | + if (value && typeof value === 'object' && value !== null) { |
| 160 | + relationalCache.value[key] = markRaw(value); |
176 | 161 | } |
177 | | - } |
178 | | -
|
179 | | - // Cache stays the same - we have better data already |
180 | | - } catch { |
181 | | - // Silent cache update error handling |
| 162 | + }); |
182 | 163 | } |
183 | | -} |
184 | | -
|
185 | | -onBeforeMount(() => { |
186 | | - updateStableCache(); |
187 | 164 | }); |
188 | 165 |
|
189 | | -// Watch props.item for changes and update cache intelligently - but debounced to prevent excessive calls |
190 | | -watch(() => props.item, updateStableCache, { deep: true, immediate: true, flush: 'post' }); |
191 | | -
|
192 | 166 | // Computed |
193 | 167 | const primaryKeyField = computed(() => { |
194 | 168 | return Object.keys(props.item).find((key) => key === 'id' || key.endsWith('_id')) || 'id'; |
@@ -244,45 +218,26 @@ const displayValue = computed(() => { |
244 | 218 | return null; |
245 | 219 | } |
246 | 220 |
|
247 | | - // 🚨 CRITICAL FIX: Display template logic MUST come BEFORE getDisplayValue! |
248 | | - // SPECIAL HANDLING for relational fields with display templates |
249 | | -
|
250 | | - const isRelatedValues = props.field?.display === 'related-values'; |
251 | | - const isImage = props.field?.display === 'image'; |
252 | | - const isFile = props.field?.display === 'file'; |
253 | | - const isUser = props.field?.display === 'user'; |
| 221 | + // Handle relational fields with display templates |
| 222 | + const template = |
| 223 | + props.field?.displayOptions?.template || props.field?.meta?.display_options?.template; |
254 | 224 |
|
255 | | - // Check if this is a relational field with display template that needs special handling |
256 | | - if ( |
257 | | - props.field?.display && |
258 | | - props.field.display !== null && |
259 | | - (isRelatedValues || isImage || isFile || isUser) |
260 | | - ) { |
261 | | - // For relational displays, we need to get the full object and render the template |
| 225 | + if (template && props.field?.display) { |
262 | 226 | const relationalValue = props.item[props.fieldKey]; |
263 | 227 |
|
264 | | - // Check if field has a custom template |
265 | | - const template = |
266 | | - props.field?.displayOptions?.template || props.field?.meta?.display_options?.template; |
267 | | -
|
268 | | - if (template && relationalValue) { |
269 | | - // Silent recovery: Check for data corruption and fix it automatically |
270 | | - if (typeof relationalValue !== 'object' || relationalValue === null) { |
271 | | - // Try to get stable object from our cache |
272 | | - const stableObject = buildStableRelationalObject(props.fieldKey); |
273 | | - if (stableObject && typeof stableObject === 'object') { |
274 | | - return renderTemplate(stableObject, template); |
275 | | - } |
276 | | -
|
277 | | - // Last resort: show fallback without noisy logs |
278 | | - return '—'; |
279 | | - } |
280 | | -
|
| 228 | + // If we have an object, use it |
| 229 | + if (relationalValue && typeof relationalValue === 'object') { |
281 | 230 | return renderTemplate(relationalValue, template); |
282 | 231 | } |
283 | 232 |
|
284 | | - // Fallback: if no template but is relational display, return the object itself |
285 | | - return relationalValue; |
| 233 | + // If corrupted (primitive value), try cache fallback |
| 234 | + const cachedValue = relationalCache.value[props.fieldKey]; |
| 235 | + if (cachedValue) { |
| 236 | + return renderTemplate(cachedValue, template); |
| 237 | + } |
| 238 | +
|
| 239 | + // No data available |
| 240 | + return '—'; |
286 | 241 | } |
287 | 242 |
|
288 | 243 | // For other relational fields, use the aliased getter if provided |
@@ -443,48 +398,6 @@ function renderTemplate(value: any, template: string): string { |
443 | 398 | return template.replace(/\{\{.*?\}\}/g, String(value)); |
444 | 399 | } |
445 | 400 |
|
446 | | -// FINAL FIX: Direct access to relational data without relying on dot-notation |
447 | | -function buildStableRelationalObject(fieldKey: string): any { |
448 | | - try { |
449 | | - // STEP 1: Get the direct field value first |
450 | | - const directValue = props.item[fieldKey]; |
451 | | -
|
452 | | - // STEP 2: If we have a valid object, use it directly (this is our data!) |
453 | | - if (directValue && typeof directValue === 'object' && directValue !== null) { |
454 | | - return directValue; |
455 | | - } |
456 | | -
|
457 | | - // STEP 3: Try cached data as fallback |
458 | | - if (stableDataCache.value && stableDataCache.value[fieldKey]) { |
459 | | - const cachedValue = stableDataCache.value[fieldKey]; |
460 | | -
|
461 | | - if (cachedValue && typeof cachedValue === 'object' && cachedValue !== null) { |
462 | | - return cachedValue; |
463 | | - } |
464 | | - } |
465 | | -
|
466 | | - // STEP 4: Legacy dot-notation fallback (for compatibility) |
467 | | - const rawItem = JSON.parse(JSON.stringify(props.item)); |
468 | | - const relatedObject: any = {}; |
469 | | -
|
470 | | - Object.keys(rawItem).forEach((key) => { |
471 | | - if (key.startsWith(`${fieldKey}.`)) { |
472 | | - const subField = key.substring(fieldKey.length + 1); |
473 | | - relatedObject[subField] = rawItem[key]; |
474 | | - } |
475 | | - }); |
476 | | -
|
477 | | - if (Object.keys(relatedObject).length > 0) { |
478 | | - return relatedObject; |
479 | | - } |
480 | | -
|
481 | | - return null; |
482 | | - } catch { |
483 | | - // Silent error handling |
484 | | - return null; |
485 | | - } |
486 | | -} |
487 | | -
|
488 | 401 | function handleUpdate(value: any) { |
489 | 402 | const primaryKey = Object.keys(props.item).find((key) => key === 'id' || key.endsWith('_id')); |
490 | 403 |
|
|
0 commit comments