diff --git a/src/displays/index.ts b/src/displays/index.ts
index ab9c32f036..037d84dd67 100644
--- a/src/displays/index.ts
+++ b/src/displays/index.ts
@@ -8,6 +8,7 @@ import DisplayImage from './image';
import DisplayUser from './user';
import DisplayRating from './rating';
import DisplayDateTime from './datetime';
+import DisplayTemplate from './template';
export const displays = [
DisplayIcon,
@@ -20,5 +21,6 @@ export const displays = [
DisplayUser,
DisplayRating,
DisplayDateTime,
+ DisplayTemplate,
];
export default displays;
diff --git a/src/displays/template/index.ts b/src/displays/template/index.ts
new file mode 100644
index 0000000000..e853253597
--- /dev/null
+++ b/src/displays/template/index.ts
@@ -0,0 +1,44 @@
+import { defineDisplay } from '@/displays/define';
+import DisplayTemplate from './template.vue';
+import getFieldsFromTemplate from '@/utils/get-fields-from-template';
+import adjustFieldsForDisplays from '@/utils/adjust-fields-for-displays';
+import getRelatedCollection from '@/utils/get-related-collection';
+import useCollection from '@/composables/use-collection';
+import { ref } from '@vue/composition-api';
+
+type Options = {
+ template: string;
+};
+
+export default defineDisplay(({ i18n }) => ({
+ id: 'template',
+ name: i18n.t('template'),
+ icon: 'text_fields',
+ handler: DisplayTemplate,
+ options: [
+ {
+ field: 'template',
+ name: i18n.t('template'),
+ interface: 'text-input',
+ width: 'full',
+ },
+ ],
+ types: ['string'],
+ fields: (options: Options, { field, collection }) => {
+ const relatedCollection = getRelatedCollection(collection, field);
+ const { primaryKeyField } = useCollection(ref(relatedCollection));
+
+ if (!relatedCollection) return [];
+
+ const fields = adjustFieldsForDisplays(
+ getFieldsFromTemplate(options.template),
+ relatedCollection
+ );
+
+ if (fields.includes(primaryKeyField.value.field) === false) {
+ fields.push(primaryKeyField.value.field);
+ }
+
+ return fields;
+ },
+}));
diff --git a/src/displays/template/template.vue b/src/displays/template/template.vue
new file mode 100644
index 0000000000..2aac21d868
--- /dev/null
+++ b/src/displays/template/template.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+ {{ $tc('item_count', value.length) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/displays/types.ts b/src/displays/types.ts
index 6006076175..298d7c7b96 100644
--- a/src/displays/types.ts
+++ b/src/displays/types.ts
@@ -12,6 +12,15 @@ export type DisplayHandlerFunction = (
context: DisplayHandlerFunctionContext
) => string | null;
+export type DisplayFieldsFunction = (
+ options: any,
+ context: {
+ collection: string;
+ field: string;
+ type: string;
+ }
+) => string[];
+
export type DisplayConfig = {
id: string;
icon: string;
@@ -20,7 +29,7 @@ export type DisplayConfig = {
handler: DisplayHandlerFunction | Component;
options: null | Partial[] | Component;
types: string[];
- fields?: string[];
+ fields?: string[] | DisplayFieldsFunction;
};
export type DisplayContext = { i18n: VueI18n };
diff --git a/src/layouts/tabular/tabular.vue b/src/layouts/tabular/tabular.vue
index 27bdd66215..89d5707793 100644
--- a/src/layouts/tabular/tabular.vue
+++ b/src/layouts/tabular/tabular.vue
@@ -94,6 +94,8 @@
:interface="header.field.interface"
:interface-options="header.field.interfaceOptions"
:type="header.field.type"
+ :collection="collection"
+ :field="header.field.field"
/>
@@ -429,6 +431,7 @@ export default defineComponent({
interface: field.interface,
interfaceOptions: field.options,
type: field.type,
+ field: field.field,
},
}));
},
diff --git a/src/stores/relations/relations.ts b/src/stores/relations/relations.ts
index 15417c4f7e..c5a7b9aad7 100644
--- a/src/stores/relations/relations.ts
+++ b/src/stores/relations/relations.ts
@@ -47,6 +47,18 @@ export const useRelationsStore = createStore({
] as Relation[];
}
+ if (['user', 'user_created', 'user_updated', 'owner'].includes(fieldInfo.type)) {
+ return [
+ {
+ collection_many: collection,
+ field_many: field,
+ collection_one: 'directus_users',
+ field_one: null,
+ junction_field: null,
+ },
+ ] as Relation[];
+ }
+
return this.getRelationsForCollection(collection).filter((relation: Relation) => {
return relation.field_many === field || relation.field_one === field;
});
diff --git a/src/utils/adjust-fields-for-displays/adjust-fields-for-displays.ts b/src/utils/adjust-fields-for-displays/adjust-fields-for-displays.ts
index 93778e5f6f..442453218a 100644
--- a/src/utils/adjust-fields-for-displays/adjust-fields-for-displays.ts
+++ b/src/utils/adjust-fields-for-displays/adjust-fields-for-displays.ts
@@ -1,5 +1,6 @@
import useFieldsStore from '@/stores/fields';
import displays from '@/displays';
+import { Field } from '@/stores/fields/types';
export default function adjustFieldsForDisplays(
fields: readonly string[],
@@ -7,9 +8,9 @@ export default function adjustFieldsForDisplays(
) {
const fieldsStore = useFieldsStore();
- const adjustedFields = fields
+ const adjustedFields: string[] = fields
.map((fieldKey) => {
- const field = fieldsStore.getField(parentCollection, fieldKey);
+ const field: Field = fieldsStore.getField(parentCollection, fieldKey);
if (!field) return fieldKey;
if (field.display === null) return fieldKey;
@@ -24,6 +25,16 @@ export default function adjustFieldsForDisplays(
);
}
+ if (typeof display.fields === 'function') {
+ return display
+ .fields(field.display_options, {
+ collection: parentCollection,
+ field: fieldKey,
+ type: field.type,
+ })
+ .map((relatedFieldKey: string) => `${fieldKey}.${relatedFieldKey}`);
+ }
+
return fieldKey;
})
.flat();
diff --git a/src/utils/get-related-collection/get-related-collection.ts b/src/utils/get-related-collection/get-related-collection.ts
new file mode 100644
index 0000000000..af8ed86591
--- /dev/null
+++ b/src/utils/get-related-collection/get-related-collection.ts
@@ -0,0 +1,22 @@
+import useRelationsStore from '@/stores/relations';
+import useFieldsStore from '@/stores/fields';
+
+export default function getRelatedCollection(collection: string, field: string) {
+ const relationsStore = useRelationsStore();
+ const fieldsStore = useFieldsStore();
+
+ const relations = relationsStore.getRelationsForField(collection, field);
+
+ const fieldInfo = fieldsStore.getField(collection, field);
+ const type = fieldInfo.type.toLowerCase();
+
+ let relatedCollection: string | null = null;
+
+ if (['user', 'user_updated', 'owner', 'file', 'm2o'].includes(type)) {
+ relatedCollection = relations[0].collection_one;
+ } else if (type === 'o2m') {
+ relatedCollection = relations[0].collection_many;
+ }
+
+ return relatedCollection;
+}
diff --git a/src/utils/get-related-collection/index.ts b/src/utils/get-related-collection/index.ts
new file mode 100644
index 0000000000..943a067297
--- /dev/null
+++ b/src/utils/get-related-collection/index.ts
@@ -0,0 +1,4 @@
+import getRelatedCollection from './get-related-collection';
+
+export { getRelatedCollection };
+export default getRelatedCollection;
diff --git a/src/views/private/components/render-display/render-display.vue b/src/views/private/components/render-display/render-display.vue
index 98cab44bde..bd6f3a53c0 100644
--- a/src/views/private/components/render-display/render-display.vue
+++ b/src/views/private/components/render-display/render-display.vue
@@ -12,6 +12,8 @@
:interface-options="interfaceOptions"
:value="value"
:type="type"
+ :collection="collection"
+ :field="field"
/>
@@ -47,6 +49,14 @@ export default defineComponent({
type: String,
required: true,
},
+ collection: {
+ type: String,
+ required: true,
+ },
+ field: {
+ type: String,
+ required: true,
+ },
},
setup(props) {
const displayInfo = computed(
diff --git a/src/views/private/components/render-template/render-template.vue b/src/views/private/components/render-template/render-template.vue
index a2f8e319f6..94351e83bc 100644
--- a/src/views/private/components/render-template/render-template.vue
+++ b/src/views/private/components/render-template/render-template.vue
@@ -12,7 +12,7 @@
:type="part.type"
v-bind="part.options"
/>
- {{ part }}
+ {{ part }}
@@ -47,43 +47,47 @@ export default defineComponent({
const regex = /({{.*?}})/g;
const parts = computed(() =>
- props.template.split(regex).map((part) => {
- if (part.startsWith('{{') === false) return part;
+ props.template
+ .split(regex)
+ .map((part) => {
+ if (part.startsWith('{{') === false) return part;
- const fieldKey = part.replace(/{{/g, '').replace(/}}/g, '').trim();
- const field: Field | null = fieldsStore.getField(props.collection, fieldKey);
+ const fieldKey = part.replace(/{{/g, '').replace(/}}/g, '').trim();
+ const field: Field | null = fieldsStore.getField(props.collection, fieldKey);
- // Instead of crashing when the field doesn't exist, we'll render a couple question
- // marks to indicate it's absence
- if (!field) return '???';
+ // Instead of crashing when the field doesn't exist, we'll render a couple question
+ // marks to indicate it's absence
+ if (!field) return '???';
- // Try getting the value from the item, return some question marks if it doesn't exist
- const value = get(props.item, fieldKey);
- if (value === undefined) return '???';
+ // Try getting the value from the item, return some question marks if it doesn't exist
+ const value = get(props.item, fieldKey);
+ if (value === undefined) return '???';
- // If no display is configured, we can render the raw value
- if (field.display === null) return value;
+ // If no display is configured, we can render the raw value
+ if (field.display === null) return value;
- const displayInfo = displays.find((display) => display.id === field.display);
+ const displayInfo = displays.find((display) => display.id === field.display);
- // If used display doesn't exist in the current project, return raw value
- if (!displayInfo) return value;
+ // If used display doesn't exist in the current project, return raw value
+ if (!displayInfo) return value;
- // If the display handler is a function, we parse the value and return the result
- if (typeof displayInfo.handler === 'function') {
- const handler = displayInfo.handler as Function;
- return handler(value, field.display_options);
- }
+ // If the display handler is a function, we parse the value and return the result
+ if (typeof displayInfo.handler === 'function') {
+ const handler = displayInfo.handler as Function;
+ return handler(value, field.display_options);
+ }
- return {
- component: field.display,
- options: field.display_options,
- value: value,
- interface: field.interface,
- interfaceOptions: field.options,
- type: field.type,
- };
- })
+ return {
+ component: field.display,
+ options: field.display_options,
+ value: value,
+ interface: field.interface,
+ interfaceOptions: field.options,
+ type: field.type,
+ };
+ })
+ .map((p) => (typeof p === 'string' ? p.trim() : p))
+ .filter((p) => p)
);
return { parts };
@@ -99,4 +103,8 @@ export default defineComponent({
.subdued {
color: var(--foreground-subdued);
}
+
+.raw:not(:last-child) {
+ margin-right: 4px;
+}