mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
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:
5
.changeset/hip-cats-pump.md
Normal file
5
.changeset/hip-cats-pump.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Fixed an issue that would cause the display template to fail on the calendar layout
|
||||
131
app/src/composables/use-alias-fields.test.ts
Normal file
131
app/src/composables/use-alias-fields.test.ts
Normal 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user