Skip to content

Commit 4c95ece

Browse files
authored
Merge pull request #30 from smartlabsAT/feature/issue-29-fix-relational-status-display
Fix relational fields with display templates showing dashes instead of configured values
2 parents 9af0f68 + fb3274e commit 4c95ece

4 files changed

Lines changed: 261 additions & 82 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "directus-extension-super-table",
3-
"version": "0.2.10",
3+
"version": "0.2.11",
44
"description": "A powerful and feature-rich table layout extension for Directus 11+ with inline editing, quick filters, and manual sorting",
55
"keywords": [
66
"directus",

src/components/EditableCellRelational.vue

Lines changed: 101 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -66,32 +66,9 @@
6666
:field="actualFieldKey"
6767
:alignment="align"
6868
/>
69-
<!-- Use custom RelationalCell for relational fields -->
70-
<RelationalCell
71-
v-else-if="isRelationalInterface && !getInterfaceType()?.includes('select')"
72-
:value="value"
73-
:field="actualFieldKey"
74-
:item="item"
75-
/>
76-
<!-- Use custom StatusCell for status field -->
77-
<StatusCell
78-
v-else-if="actualFieldKey === 'status' && getInterfaceType() === 'select-dropdown'"
79-
:value="value"
80-
:options="interfaceOptions"
81-
:field="actualFieldKey"
82-
:edit-mode="props.editMode"
83-
:align="props.align"
84-
/>
85-
<!-- Use custom SelectCell for other select-dropdown interfaces -->
86-
<SelectCell
87-
v-else-if="getInterfaceType() === 'select-dropdown'"
88-
:value="value"
89-
:options="interfaceOptions"
90-
:field="actualFieldKey"
91-
/>
92-
<!-- Use render-display for other types -->
69+
<!-- ABSOLUTE PRIORITY: User-configured display templates (ALL field types) -->
9370
<render-display
94-
v-else
71+
v-if="field?.display"
9572
:value="value"
9673
:display="field?.display"
9774
:options="field?.displayOptions"
@@ -101,31 +78,51 @@
10178
:collection="field?.collection"
10279
:field="field?.field"
10380
/>
81+
<!-- FALLBACK 1: Custom SelectCell for select-dropdown fields WITHOUT display template -->
82+
<SelectCell
83+
v-else-if="getInterfaceType() === 'select-dropdown'"
84+
:value="value"
85+
:options="interfaceOptions"
86+
:field="actualFieldKey"
87+
/>
88+
<!-- FALLBACK 2: Custom RelationalCell for relational fields WITHOUT display template -->
89+
<RelationalCell
90+
v-else-if="isRelationalInterface"
91+
:value="value"
92+
:field="actualFieldKey"
93+
:item="item"
94+
/>
95+
<!-- FINAL FALLBACK: Raw value display for fields without any special handling -->
96+
<span v-else class="raw-value">
97+
{{ value != null ? String(value) : '—' }}
98+
</span>
10499
</template>
105100
</InlineEditPopover>
106101

107-
<!-- Display only for relational fields -->
102+
<!-- ABSOLUTE PRIORITY: Display templates for relational fields -->
103+
<div
104+
v-else-if="field?.display"
105+
class="editable-cell relational"
106+
:style="{ textAlign: props.align || 'left' }"
107+
>
108+
<!-- Direct Display Value (already rendered in computed) -->
109+
<span class="template-display">{{ displayValue }}</span>
110+
</div>
111+
112+
<!-- FALLBACK: Display only for relational fields without display templates -->
108113
<div v-else class="editable-cell relational" :style="{ textAlign: props.align || 'left' }">
109-
<render-display
110-
:value="displayValue"
111-
:display="field?.display"
112-
:options="field?.displayOptions"
113-
:interface="field?.interface"
114-
:interface-options="field?.interfaceOptions"
115-
:type="field?.type"
116-
:collection="field?.collection"
117-
:field="field?.field"
118-
/>
114+
<span class="raw-value">
115+
{{ displayValue != null ? String(displayValue) : '—' }}
116+
</span>
119117
</div>
120118
</template>
121119

122120
<script lang="ts" setup>
123-
import { computed } from 'vue';
121+
import { computed, onBeforeMount, markRaw, ref } from 'vue';
124122
import type { Field, Item } from '@directus/types';
125123
import InlineEditPopover from './InlineEditPopover.vue';
126124
import BooleanToggleCell from './CellRenderers/BooleanToggleCell.vue';
127125
import SelectCell from './CellRenderers/SelectCell.vue';
128-
import StatusCell from './CellRenderers/StatusCell.vue';
129126
import ImageCell from './CellRenderers/ImageCell.vue';
130127
import RelationalCell from './CellRenderers/RelationalCell.vue';
131128
import ColorCell from './CellRenderers/ColorCell.vue';
@@ -151,6 +148,21 @@ const emit = defineEmits<{
151148
'navigate-prev': [];
152149
}>();
153150
151+
// Simple cache for relational objects - only cache on mount to avoid corruption
152+
const relationalCache = ref<Record<string, any>>({});
153+
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);
161+
}
162+
});
163+
}
164+
});
165+
154166
// Computed
155167
const primaryKeyField = computed(() => {
156168
return Object.keys(props.item).find((key) => key === 'id' || key.endsWith('_id')) || 'id';
@@ -206,6 +218,28 @@ const displayValue = computed(() => {
206218
return null;
207219
}
208220
221+
// Handle relational fields with display templates
222+
const template =
223+
props.field?.displayOptions?.template || props.field?.meta?.display_options?.template;
224+
225+
if (template && props.field?.display) {
226+
const relationalValue = props.item[props.fieldKey];
227+
228+
// If we have an object, use it
229+
if (relationalValue && typeof relationalValue === 'object') {
230+
return renderTemplate(relationalValue, template);
231+
}
232+
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 '';
241+
}
242+
209243
// For other relational fields, use the aliased getter if provided
210244
if (props.getDisplayValue) {
211245
return props.getDisplayValue(props.item, props.fieldKey);
@@ -335,6 +369,35 @@ function getInterfaceType() {
335369
return props.field?.interface || props.field?.meta?.interface;
336370
}
337371
372+
// Manual Template Rendering - Production Fix for render-display issue
373+
function renderTemplate(value: any, template: string): string {
374+
if (!template || template === null || template === undefined) {
375+
// No template - return formatted value
376+
return value != null ? String(value) : '';
377+
}
378+
379+
if (!value) {
380+
return '';
381+
}
382+
383+
// Handle object values for related fields
384+
if (typeof value === 'object' && value !== null) {
385+
let result = template;
386+
387+
// Replace template variables with actual values
388+
Object.keys(value).forEach((key) => {
389+
const regex = new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g');
390+
const fieldValue = value[key];
391+
result = result.replace(regex, fieldValue != null ? String(fieldValue) : '');
392+
});
393+
394+
return result;
395+
}
396+
397+
// Handle simple values - replace all template vars with the same value
398+
return template.replace(/\{\{.*?\}\}/g, String(value));
399+
}
400+
338401
function handleUpdate(value: any) {
339402
const primaryKey = Object.keys(props.item).find((key) => key === 'id' || key.endsWith('_id'));
340403

src/super-table.vue

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,6 @@ import { debounce } from 'lodash';
275275
import { useStores, useCollection, useSync, useApi } from '@directus/extensions-sdk';
276276
import { formatTitle } from '@directus/format-title';
277277
import { getDefaultDisplayForType } from './utils/getDefaultDisplayForType';
278-
import { adjustFieldsForDisplays } from './utils/adjustFieldsForDisplays';
279278
import { useTableApi } from './composables/api';
280279
import { useAliasFields } from './composables/useAliasFields';
281280
import { useLanguageSelector } from './composables/useLanguageSelector';
@@ -442,23 +441,22 @@ const fieldsForAliasing = computed(() => {
442441
});
443442
444443
// Use alias fields for proper relational data handling
445-
const { aliasQuery, getFromAliasedItem } = useAliasFields(fieldsForAliasing, collection);
444+
const { aliasedFields, aliasQuery, getFromAliasedItem } = useAliasFields(
445+
fieldsForAliasing,
446+
collection
447+
);
446448
447-
// Adjust fields for displays
449+
// Create fields for API query using the aliased fields (following original Directus pattern)
448450
const fieldsWithRelational = computed(() => {
449451
if (!props.collection) return [];
450452
451-
// Get unique fields without language suffixes for API query
452-
// We fetch all translation data and filter client-side by language
453-
const uniqueFields = [
454-
...new Set(
455-
fields.value.map((field: string) => {
456-
return field.includes(':') ? field.split(':')[0] : field;
457-
})
458-
),
459-
];
453+
// Extract all fields from aliasedFields (this includes display-adjusted fields)
454+
const allDisplayFields = Object.values(aliasedFields.value).flatMap((aliasInfo) => {
455+
return aliasInfo.fields || [aliasInfo.key];
456+
});
460457
461-
const adjustedFields: string[] = adjustFieldsForDisplays(uniqueFields, props.collection);
458+
// Remove duplicates
459+
const adjustedFields = [...new Set(allDisplayFields)];
462460
463461
// CRITICAL: Always include the primary key field for navigation and identification
464462
const pkField = getPrimaryKeyFieldName();
@@ -585,7 +583,7 @@ const tableHeaders = computed(() => {
585583
? true
586584
: !field.type || !nonSortableTypes.includes(field.type);
587585
588-
return {
586+
const headerResult = {
589587
text: headerText,
590588
value: field.key,
591589
description,
@@ -603,6 +601,8 @@ const tableHeaders = computed(() => {
603601
},
604602
sortable: isSortable,
605603
};
604+
605+
return headerResult;
606606
});
607607
});
608608
@@ -804,6 +804,7 @@ const deep = computed(() => {
804804
// Remove language suffix if present
805805
const actualField = field.includes(':') ? field.split(':')[0] : field;
806806
807+
// Handle dot-notation relational fields (like "user_created.first_name")
807808
if (actualField.includes('.')) {
808809
const parts = actualField.split('.');
809810
const rootField = parts[0];
@@ -824,6 +825,23 @@ const deep = computed(() => {
824825
};
825826
}
826827
}
828+
} else {
829+
// Handle pure relational fields (like "image_data", "status_id", etc.)
830+
// Check if this field is relational by looking at field metadata
831+
const fieldMeta = fieldsStore.getField(collection.value, actualField);
832+
833+
if (
834+
fieldMeta?.meta?.special?.includes('m2o') ||
835+
fieldMeta?.meta?.special?.includes('o2m') ||
836+
fieldMeta?.meta?.special?.includes('m2m') ||
837+
fieldMeta?.meta?.special?.includes('m2a')
838+
) {
839+
if (!deepFields[actualField]) {
840+
deepFields[actualField] = {
841+
_fields: ['*'],
842+
};
843+
}
844+
}
827845
}
828846
});
829847

0 commit comments

Comments
 (0)