From 5c00ddddb1fc18e72dbd45eca8808e31e8a7eed0 Mon Sep 17 00:00:00 2001 From: Rijk van Zanten Date: Mon, 11 May 2020 17:05:11 -0400 Subject: [PATCH] Display template display template display (#551) * Add util to find related collection * Allow display.fields to be function * Return fixed relation for user types * Pass type / collection / field to displays * Add template display * Finish display template display template display --- src/displays/index.ts | 2 + src/displays/template/index.ts | 44 +++++++ src/displays/template/template.vue | 121 ++++++++++++++++++ src/displays/types.ts | 11 +- src/layouts/tabular/tabular.vue | 3 + src/stores/relations/relations.ts | 12 ++ .../adjust-fields-for-displays.ts | 15 ++- .../get-related-collection.ts | 22 ++++ src/utils/get-related-collection/index.ts | 4 + .../render-display/render-display.vue | 10 ++ .../render-template/render-template.vue | 68 +++++----- 11 files changed, 279 insertions(+), 33 deletions(-) create mode 100644 src/displays/template/index.ts create mode 100644 src/displays/template/template.vue create mode 100644 src/utils/get-related-collection/get-related-collection.ts create mode 100644 src/utils/get-related-collection/index.ts 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 @@ + + + + + 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 }} @@ -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; +}