From 8a772802fa736d70b923ba5d3b1a78adbf959104 Mon Sep 17 00:00:00 2001 From: Brainslug Date: Tue, 2 May 2023 21:23:38 +0200 Subject: [PATCH] updated uses of useAliasFields (#18267) * updated uses of useAliasFields * use fields instead * updated ternary check * remove dealiasing * Linty lint * temporary changes * finish alias cleanup * Fix linter warnings * fixed imports from merge * removed unsed variables * Add changeset --------- Co-authored-by: Nitwel Co-authored-by: Pascal Jufer Co-authored-by: rijkvanzanten Co-authored-by: Jan Arends --- .changeset/hip-cats-pump.md | 5 + app/src/composables/use-alias-fields.test.ts | 131 +++++++++++++++++++ app/src/composables/use-alias-fields.ts | 129 +++++++++++++----- app/src/layouts/tabular/tabular.vue | 35 ++--- app/src/utils/render-string-template.ts | 15 +-- app/src/utils/save-as-csv.ts | 12 +- 6 files changed, 248 insertions(+), 79 deletions(-) create mode 100644 .changeset/hip-cats-pump.md create mode 100644 app/src/composables/use-alias-fields.test.ts diff --git a/.changeset/hip-cats-pump.md b/.changeset/hip-cats-pump.md new file mode 100644 index 0000000000..0a40706d4e --- /dev/null +++ b/.changeset/hip-cats-pump.md @@ -0,0 +1,5 @@ +--- +'@directus/app': patch +--- + +Fixed an issue that would cause the display template to fail on the calendar layout diff --git a/app/src/composables/use-alias-fields.test.ts b/app/src/composables/use-alias-fields.test.ts new file mode 100644 index 0000000000..53af2cb569 --- /dev/null +++ b/app/src/composables/use-alias-fields.test.ts @@ -0,0 +1,131 @@ +import { mount } from '@vue/test-utils'; +import { describe, expect, test, vi } from 'vitest'; +import { defineComponent } from 'vue'; +import { useAliasFields } from './use-alias-fields'; + +vi.mock('@/utils/adjust-fields-for-displays', () => { + return { + adjustFieldsForDisplays: (fields: string[], _collection: string) => { + if (fields.includes('image')) { + return ['image.url.file', 'image.alt']; + } + + return fields; + }, + }; +}); + +const TestComponent = defineComponent({ + props: ['fields', 'collection'], // eslint-disable-line vue/require-prop-types + setup(props) { + return useAliasFields(props.fields, props.collection); + }, + render: () => '', +}); + +describe('useAliasFields', () => { + test('aliasing no collisions', () => { + const fields = ['id', 'image.url']; + + const wrapper = mount(TestComponent, { + props: { + fields, + collection: 'articles', + }, + }); + + expect(wrapper.vm.aliasedFields).toEqual({ + id: { + aliased: false, + fieldName: 'id', + fields: ['id'], + key: 'id', + }, + 'image.url': { + aliased: false, + fieldName: 'image', + fields: ['image.url'], + key: 'image.url', + }, + }); + + expect(wrapper.vm.aliasQuery).toEqual({}); + + expect(wrapper.vm.aliasedKeys).toEqual([]); + + expect( + wrapper.vm.getFromAliasedItem( + { + id: 1, + image: { + url: 'https://example.com', + }, + }, + 'image.url' + ) + ).toEqual('https://example.com'); + }); + + test('aliasing with collisions', () => { + const fields = ['id', 'image', 'image.url']; + + const wrapper = mount(TestComponent, { + props: { + fields, + collection: 'articles', + }, + }); + + expect(wrapper.vm.aliasedFields).toEqual({ + id: { + key: 'id', + fieldName: 'id', + fields: ['id'], + aliased: false, + }, + '5faa95b': { + key: 'image', + fieldName: 'image', + fieldAlias: '5faa95b', + fields: ['5faa95b.url.file', '5faa95b.alt'], + aliased: true, + }, + '3468cda4': { + key: 'image.url', + fieldName: 'image', + fieldAlias: '3468cda4', + fields: ['3468cda4.url'], + aliased: true, + }, + }); + + expect(wrapper.vm.aliasQuery).toEqual({ + '5faa95b': 'image', + '3468cda4': 'image', + }); + + expect(wrapper.vm.aliasedKeys).toEqual(['5faa95b', '3468cda4']); + + const item = { + id: 1, + '5faa95b': { + url: { + file: 'https://example.com', + }, + alt: 'Example', + }, + '3468cda4': { + url: 'https://example.com', + }, + }; + + expect(wrapper.vm.getFromAliasedItem(item, 'image.url')).toEqual('https://example.com'); + + expect(wrapper.vm.getFromAliasedItem(item, 'image')).toEqual({ + url: { + file: 'https://example.com', + }, + alt: 'Example', + }); + }); +}); diff --git a/app/src/composables/use-alias-fields.ts b/app/src/composables/use-alias-fields.ts index 9be196d01a..4dae82a656 100644 --- a/app/src/composables/use-alias-fields.ts +++ b/app/src/composables/use-alias-fields.ts @@ -1,63 +1,122 @@ -import { getSimpleHash } from '@directus/utils'; -import { Query } from '@directus/types'; -import { computed, ComputedRef, Ref } from 'vue'; import { adjustFieldsForDisplays } from '@/utils/adjust-fields-for-displays'; +import { Query } from '@directus/types'; +import { get, getSimpleHash } from '@directus/utils'; +import { ComputedRef, Ref, computed, unref } from 'vue'; -export type AliasFields = { - fieldName: string; - fieldAlias: string; - fields: string[]; - key: string; -}; +export type AliasFields = + | { + fieldName: string; + fieldAlias: string; + fields: string[]; + key: string; + aliased: true; + } + | { + fieldName: string; + fields: string[]; + key: string; + aliased: false; + }; type UsableAliasFields = { aliasedFields: ComputedRef>; aliasQuery: ComputedRef; aliasedKeys: ComputedRef; + getFromAliasedItem: >(item: T, key: string) => K | undefined; }; -export function useAliasFields(fields: Ref, collection: Ref): UsableAliasFields { +/** + * Generates aliases for field collisions when fetching the data for each display. + * @param fields This list of fields to be aliased + * @param collection The collection the fields belong to + * @returns Info about the display fields and if the original fields were aliased + */ +export function useAliasFields( + fields: Ref | string[], + collection: Ref | string | null +): UsableAliasFields { const aliasedFields = computed(() => { const aliasedFields: Record = {}; - if (!fields.value || fields.value.length === 0 || !collection.value) return aliasedFields; + const _fields = unref(fields); + const _collection = unref(collection); - for (const field of fields.value) { - const alias = getSimpleHash(field); + if (!_fields || _fields.length === 0 || !_collection) return aliasedFields; - const fullFields = adjustFieldsForDisplays([field], collection.value).map((field) => { - if (field.includes('.')) { - return `${alias}.${field.split('.').slice(1).join('.')}`; - } else { - return field; - } - }); + const fieldNameCount = _fields.reduce>((acc, field) => { + const fieldName = field.split('.')[0]; + acc[fieldName] = (acc[fieldName] || 0) + 1; + return acc; + }, {}); - aliasedFields[alias] = { - key: field, - fieldName: field.split('.')[0], - fieldAlias: alias, - fields: fullFields, - }; + for (const field of _fields) { + const fieldName = field.split('.')[0]; + + if (fieldNameCount[fieldName] > 1 === false) { + aliasedFields[field] = { + key: field, + fieldName, + fields: adjustFieldsForDisplays([field], _collection), + aliased: false, + }; + } else { + const alias = getSimpleHash(field); + + aliasedFields[alias] = { + key: field, + fieldName, + fieldAlias: alias, + fields: adjustFieldsForDisplays([field], _collection).map((displayField) => { + if (displayField.includes('.')) { + return `${alias}.${displayField.split('.').slice(1).join('.')}`; + } else { + return alias; + } + }), + aliased: true, + }; + } } return aliasedFields; }); const aliasedKeys = computed(() => { - return Object.values(aliasedFields.value).map((field) => field.fieldAlias); + return Object.values(aliasedFields.value).reduce((acc, field) => { + if (field.aliased) { + acc.push(field.fieldAlias); + } + + return acc; + }, []); }); const aliasQuery = computed(() => { if (!aliasedFields.value) return null; - return Object.values(aliasedFields.value).reduce( - (acc, value) => ({ - ...acc, - [value.fieldAlias]: value.fieldName, - }), - {} as Query['alias'] - ); + return Object.values(aliasedFields.value).reduce>((acc, value) => { + if (value.aliased) { + acc[value.fieldAlias] = value.fieldName; + } + + return acc; + }, {}); }); - return { aliasedFields, aliasQuery, aliasedKeys }; + /** + * Returns the value of the given key from the given item, taking into account aliased fields + * @param item The item to get the value from + * @param key The key to get the value for without any alias + * @returns The value of the given key from the given item + */ + function getFromAliasedItem>(item: T, key: string): K | undefined { + const aliasInfo = Object.values(aliasedFields.value).find((field) => field.key === key); + + if (!aliasInfo || !aliasInfo.aliased) return get(item, key); + + if (key.includes('.') === false) return get(item, aliasInfo.fieldAlias); + + return get(item, `${aliasInfo.fieldAlias}.${key.split('.').slice(1).join('.')}`); + } + + return { aliasedFields, aliasQuery, aliasedKeys, getFromAliasedItem }; } diff --git a/app/src/layouts/tabular/tabular.vue b/app/src/layouts/tabular/tabular.vue index 5cad62b913..1d9dce97eb 100644 --- a/app/src/layouts/tabular/tabular.vue +++ b/app/src/layouts/tabular/tabular.vue @@ -25,7 +25,7 @@ >