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 <mail@nitwel.de>
Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
Co-authored-by: Jan Arends <jan.arends@mailbox.org>
This commit is contained in:
Brainslug
2023-05-02 21:23:38 +02:00
committed by GitHub
parent 2432306fea
commit 8a772802fa
6 changed files with 248 additions and 79 deletions

View File

@@ -0,0 +1,5 @@
---
'@directus/app': patch
---
Fixed an issue that would cause the display template to fail on the calendar layout

View File

@@ -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',
});
});
});

View File

@@ -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<Record<string, AliasFields>>;
aliasQuery: ComputedRef<Query['alias']>;
aliasedKeys: ComputedRef<string[]>;
getFromAliasedItem: <K, T extends Record<string, K>>(item: T, key: string) => K | undefined;
};
export function useAliasFields(fields: Ref<string[]>, collection: Ref<string | null>): 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[]> | string[],
collection: Ref<string | null> | string | null
): UsableAliasFields {
const aliasedFields = computed(() => {
const aliasedFields: Record<string, AliasFields> = {};
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<Record<string, number>>((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<string[]>((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<Record<string, string>>((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<K, T extends Record<string, K>>(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 };
}

View File

@@ -25,7 +25,7 @@
>
<template v-for="header in tableHeaders" :key="header.value" #[`item.${header.value}`]="{ item }">
<render-display
:value="getDisplayValue(item, header.value)"
:value="getFromAliasedItem(item, header.key)"
:display="header.field.display"
:options="header.field.displayOptions"
:interface="header.field.interface"
@@ -178,17 +178,17 @@ export default {
</script>
<script lang="ts" setup>
import { HeaderRaw } from '@/components/v-table/types';
import { useAliasFields } from '@/composables/use-alias-fields';
import { useShortcut } from '@/composables/use-shortcut';
import { usePermissionsStore } from '@/stores/permissions';
import { useUserStore } from '@/stores/user';
import { Collection } from '@/types/collections';
import { useSync } from '@directus/composables';
import { Field, Filter, Item, ShowSelect } from '@directus/types';
import { ComponentPublicInstance, inject, ref, Ref, watch, computed, toRefs } from 'vue';
import { ComponentPublicInstance, Ref, computed, inject, ref, toRefs, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { get } from '@directus/utils';
import { AliasFields } from '@/composables/use-alias-fields';
import { usePermissionsStore } from '@/stores/permissions';
import { useUserStore } from '@/stores/user';
import { HeaderRaw } from '@/components/v-table/types';
interface Props {
collection: string;
@@ -239,7 +239,7 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits(['update:selection', 'update:tableHeaders', 'update:limit', 'update:fields']);
const { t } = useI18n();
const { collection, aliasedFields, aliasedKeys } = toRefs(props);
const { collection } = toRefs(props);
const selectionWritable = useSync(props, 'selection', emit);
const tableHeadersWritable = useSync(props, 'tableHeaders', emit);
@@ -283,26 +283,7 @@ const showManualSort = computed(() => {
const fieldsWritable = useSync(props, 'fields', emit);
function getDisplayValue(item: Item, key: string) {
const aliasInfo = Object.values(aliasedFields.value).find((field) => field.key === key);
if (!aliasInfo) return get(item, key);
const dealiasedItem = Object.keys(item).reduce<Item>((result, itemKey) => {
if (aliasedKeys.value.includes(itemKey)) {
if (itemKey !== aliasInfo.fieldAlias) return result;
const name = aliasedFields.value[itemKey].fieldName;
result[name] = item[itemKey];
} else {
// Don't overwrite already dealiased keys
if (itemKey in result === false) result[itemKey] = item[itemKey];
}
return result;
}, {});
return get(dealiasedItem, key);
}
const { getFromAliasedItem } = useAliasFields(fieldsWritable, collection);
function addField(fieldKey: string) {
fieldsWritable.value = [...fieldsWritable.value, fieldKey];

View File

@@ -1,11 +1,11 @@
import { useAliasFields } from '@/composables/use-alias-fields';
import { useExtension } from '@/composables/use-extension';
import { useFieldsStore } from '@/stores/fields';
import { Field } from '@directus/types';
import { get, getFieldsFromTemplate } from '@directus/utils';
import { render, renderFn } from 'micromustache';
import { computed, ComputedRef, Ref, ref, unref } from 'vue';
import { set } from 'lodash';
import { useExtension } from '@/composables/use-extension';
import { render, renderFn } from 'micromustache';
import { ComputedRef, Ref, computed, unref } from 'vue';
type StringTemplate = {
fieldsInTemplate: ComputedRef<string[]>;
@@ -67,15 +67,12 @@ export function renderDisplayStringTemplate(
set(fieldsUsed, key, fieldsStore.getField(collection, key));
}
const { aliasFields } = useAliasFields(ref(fields));
const parsedItem: Record<string, any> = {};
const { getFromAliasedItem } = useAliasFields(fields, collection);
for (const key of fields) {
const value =
!aliasFields.value?.[key] || get(item, key) !== undefined
? get(item, key)
: get(item, aliasFields.value[key].fullAlias);
const value = getFromAliasedItem(item, key);
const display = useExtension(
'display',

View File

@@ -1,11 +1,10 @@
import { useAliasFields } from '@/composables/use-alias-fields';
import { useExtension } from '@/composables/use-extension';
import { useFieldsStore } from '@/stores/fields';
import { get } from '@directus/utils';
import { Field, Item } from '@directus/types';
import { saveAs } from 'file-saver';
import { parse } from 'json2csv';
import { computed, ref } from 'vue';
import { useExtension } from '@/composables/use-extension';
import { computed } from 'vue';
/**
* Saves the given collection + items combination as a CSV file
@@ -19,7 +18,7 @@ export async function saveAsCSV(collection: string, fields: string[], items: Ite
fieldsUsed[key] = fieldsStore.getField(collection, key);
}
const { aliasFields } = useAliasFields(ref(fields));
const { getFromAliasedItem } = useAliasFields(fields, collection);
const parsedItems = [];
@@ -43,10 +42,7 @@ export async function saveAsCSV(collection: string, fields: string[], items: Ite
name = fieldsUsed[key]?.name ?? key;
}
const value =
!aliasFields.value?.[key] || item[key] !== undefined
? get(item, key)
: get(item, aliasFields.value[key].fullAlias);
const value = getFromAliasedItem(item, key);
const display = useExtension(
'display',