Add translations display (#8264)

* add translations display

* add progress and lang preview

* format changes

* remove unused

* remove unused

* fix errors and clean up style

* make lang title bold

* Fix imports

* 🧹 Little cleanup

Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
Nitwel
2021-10-14 01:31:15 +02:00
committed by GitHub
parent 4794c103b1
commit 55e3b342ec
15 changed files with 606 additions and 207 deletions

View File

@@ -20,6 +20,8 @@ type UsableRelation = {
relationInfo: ComputedRef<RelationInfo>;
junctionPrimaryKeyField: ComputedRef<Field | null>;
relationPrimaryKeyField: ComputedRef<Field | null>;
junctionFields: ComputedRef<Field[]>;
relationFields: ComputedRef<Field[]>;
};
export default function useRelation(collection: Ref<string>, field: Ref<string>): UsableRelation {
@@ -53,8 +55,12 @@ export default function useRelation(collection: Ref<string>, field: Ref<string>)
return collectionsStore.getCollection(relation.value.related_collection!)!;
});
const { primaryKeyField: junctionPrimaryKeyField } = useCollection(junctionCollection.value.collection);
const { primaryKeyField: relationPrimaryKeyField } = useCollection(relationCollection.value.collection);
const { primaryKeyField: junctionPrimaryKeyField, fields: junctionFields } = useCollection(
junctionCollection.value.collection
);
const { primaryKeyField: relationPrimaryKeyField, fields: relationFields } = useCollection(
relationCollection.value.collection
);
const relationInfo = computed(() => {
return {
@@ -75,5 +81,7 @@ export default function useRelation(collection: Ref<string>, field: Ref<string>)
relationInfo,
junctionPrimaryKeyField,
relationPrimaryKeyField,
junctionFields,
relationFields,
};
}

View File

@@ -0,0 +1,59 @@
import { defineDisplay } from '@directus/shared/utils';
import adjustFieldsForDisplays from '@/utils/adjust-fields-for-displays';
import { getFieldsFromTemplate } from '@directus/shared/utils';
import options from './options.vue';
import DisplayTranslations from './translations.vue';
import { useFieldsStore, useRelationsStore } from '@/stores';
type Options = {
template: string;
languageField: string;
};
export default defineDisplay({
id: 'translations',
name: '$t:displays.translations.translations',
description: '$t:displays.translations.description',
icon: 'translate',
component: DisplayTranslations,
options: options,
types: ['alias', 'string', 'uuid', 'integer', 'bigInteger', 'json'],
groups: ['m2m'],
fields: (options: Options | null, { field, collection }) => {
const fieldsStore = useFieldsStore();
const relationsStore = useRelationsStore();
const relations = relationsStore.getRelationsForField(collection, field);
const translationsRelation = relations.find(
(relation) => relation.related_collection === collection && relation.meta?.one_field === field
);
const languagesRelation = relations.find((relation) => relation !== translationsRelation);
const translationCollection = translationsRelation?.related_collection;
const languagesCollection = languagesRelation?.related_collection;
if (!translationCollection || !languagesCollection) return [];
const translationsPrimaryKeyField = fieldsStore.getPrimaryKeyFieldForCollection(translationCollection);
const languagesPrimaryKeyField = fieldsStore.getPrimaryKeyFieldForCollection(languagesCollection);
const fields = options?.template
? adjustFieldsForDisplays(getFieldsFromTemplate(options.template), translationCollection)
: [];
if (translationsPrimaryKeyField && !fields.includes(translationsPrimaryKeyField.field)) {
fields.push(translationsPrimaryKeyField.field);
}
if (languagesRelation?.field && !fields.includes(languagesRelation.field)) {
fields.push(`${languagesRelation.field}.${languagesPrimaryKeyField.field}`);
if (options?.languageField) {
fields.push(`${languagesRelation.field}.${options.languageField}`);
}
}
return fields;
},
});

View File

@@ -0,0 +1,150 @@
<template>
<v-notice v-if="collection === null" type="warning">
{{ t('interfaces.list-o2m.no_collection') }}
</v-notice>
<div v-else class="form-grid">
<div class="field full">
<p class="type-label">{{ t('display_template') }}</p>
<v-field-template v-model="template" :collection="translationsCollection.collection" :depth="2" />
</div>
<div class="field full">
<p class="type-label">{{ t('displays.translations.language_field') }}</p>
<v-select v-model="languageField" :items="languageFields" item-text="name" item-value="field" />
</div>
<div class="field half">
<p class="type-label">{{ t('displays.translations.default_language') }}</p>
<v-select
v-model="defaultLanguage"
show-deselect
:placeholder="t('select_an_item')"
:items="languages"
:item-text="languageField ?? langCode.field"
:item-value="langCode.field"
/>
</div>
<div class="field half">
<p class="type-label">{{ t('displays.translations.user_language') }}</p>
<v-checkbox v-model="userLanguage" block :label="t('displays.translations.enable')" />
</div>
</div>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { Field, Relation } from '@directus/shared/types';
import { defineComponent, PropType, computed, ref, watch, toRefs } from 'vue';
import api from '@/api';
import useRelation from '@/composables/use-m2m';
export default defineComponent({
props: {
value: {
type: Object as PropType<Record<string, any>>,
default: null,
},
fieldData: {
type: Object as PropType<Field>,
default: null,
},
relations: {
type: Array as PropType<Relation[]>,
default: () => [],
},
collection: {
type: String,
default: null,
},
},
emits: ['input'],
setup(props, { emit }) {
const { t } = useI18n();
const { collection } = toRefs(props);
const field = computed(() => props.fieldData.field);
const languageField = computed({
get() {
return props.value?.languageField;
},
set(newVal: string) {
emit('input', {
...(props.value || {}),
languageField: newVal,
});
},
});
const defaultLanguage = computed({
get() {
return props.value?.defaultLanguage;
},
set(newVal: string) {
emit('input', {
...(props.value || {}),
defaultLanguage: newVal,
});
},
});
const userLanguage = computed({
get() {
return props.value?.userLanguage;
},
set(newVal: string) {
emit('input', {
...(props.value || {}),
userLanguage: newVal,
});
},
});
const template = computed({
get() {
return props.value?.template;
},
set(newTemplate: string) {
emit('input', {
...(props.value || {}),
template: newTemplate,
});
},
});
const {
junctionCollection: translationsCollection,
relationCollection: languagesCollection,
relationPrimaryKeyField: langCode,
relationFields: languageFields,
} = useRelation(collection, field);
const languages = ref<Record<string, any>[]>([]);
watch(
languagesCollection,
async (newCollection) => {
const response = await api.get(`items/${newCollection.collection}`, {
params: {
limit: -1,
},
});
languages.value = response.data.data;
},
{ immediate: true }
);
return {
t,
template,
translationsCollection,
languagesCollection,
userLanguage,
languages,
defaultLanguage,
langCode,
languageField,
languageFields,
};
},
});
</script>

View File

@@ -0,0 +1,172 @@
<template>
<value-null v-if="!junctionCollection.collection" />
<v-menu v-else show-arrow :disabled="value.length === 0">
<template #activator="{ toggle }">
<render-template
:template="internalTemplate"
:item="displayItem"
:collection="junctionCollection.collection"
@click.stop="toggle"
/>
</template>
<v-list class="links">
<v-list-item v-for="item in translations" :key="item.id">
<v-list-item-content>
<div class="header">
<div class="lang">
<v-icon name="translate" small />
{{ item.lang }}
</div>
<v-progress-linear v-tooltip="`${item.progress}%`" :value="item.progress" colorful />
</div>
<render-template :template="internalTemplate" :item="item.item" :collection="junctionCollection.collection" />
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</template>
<script lang="ts">
import { defineComponent, computed, PropType, toRefs } from 'vue';
import ValueNull from '@/views/private/components/value-null';
import useRelation from '@/composables/use-m2m';
import { useUserStore } from '@/stores';
import { notEmpty } from '@/utils/is-empty';
export default defineComponent({
components: { ValueNull },
props: {
collection: {
type: String,
required: true,
},
field: {
type: String,
required: true,
},
value: {
type: [Array] as PropType<Record<string, any>[]>,
default: null,
},
template: {
type: String,
default: null,
},
userLanguage: {
type: Boolean,
default: false,
},
defaultLanguage: {
type: String,
default: null,
},
languageField: {
type: String,
default: null,
},
type: {
type: String,
required: true,
},
},
setup(props) {
const { collection, field } = toRefs(props);
const {
junctionPrimaryKeyField: primaryKeyField,
junctionCollection,
relation,
junctionFields,
relationPrimaryKeyField,
} = useRelation(collection, field);
const internalTemplate = computed(() => {
return (
props.template || junctionCollection.value?.meta?.display_template || `{{ ${primaryKeyField.value!.field} }}`
);
});
const displayItem = computed(() => {
const langPkField = relationPrimaryKeyField.value?.field;
if (!langPkField) return {};
let item =
props.value.find((val) => val?.[relation.value.field]?.[langPkField] === props.defaultLanguage) ??
props.value[0];
if (props.userLanguage) {
const user = useUserStore();
item =
props.value.find((val) => val?.[relation.value.field]?.[langPkField] === user.currentUser?.language) ?? item;
}
return item ?? {};
});
const writableFields = computed(() =>
junctionFields.value.filter(
(field) => field.type !== 'alias' && field.meta?.hidden === false && field.meta.readonly === false
)
);
const translations = computed(() => {
return props.value.map((item) => {
const filledFields = writableFields.value.filter((field) => {
return field.field in item && notEmpty(item?.[field.field]);
}).length;
return {
id: item?.[primaryKeyField.value?.field ?? 'id'],
lang: item?.[relation.value.field]?.[props.languageField ?? relationPrimaryKeyField.value?.field],
progress: Math.round((filledFields / writableFields.value.length) * 100),
item,
};
});
});
return { primaryKeyField, internalTemplate, junctionCollection, displayItem, translations };
},
});
</script>
<style lang="scss" scoped>
.v-list {
width: 300px;
}
.header {
display: flex;
gap: 20px;
align-items: center;
justify-content: space-between;
color: var(--foreground-subdued);
font-size: 12px;
.lang {
font-weight: 600;
}
.v-icon {
margin-right: 4px;
}
.v-progress-linear {
flex: 1;
width: unset;
max-width: 100px;
border-radius: 4px;
}
}
.v-list-item-content {
padding-top: 4px;
padding-bottom: 2px;
}
.v-list-item:not(:first-child) {
.header {
padding-top: 8px;
border-top: var(--border-width) solid var(--border-subdued);
}
}
</style>

View File

@@ -108,7 +108,7 @@ import { get } from 'lodash';
import Draggable from 'vuedraggable';
import useActions from '../list-m2m/use-actions';
import useRelation from '../list-m2m/use-relation';
import useRelation from '@/composables/use-m2m';
import usePreview from '../list-m2m/use-preview';
import useEdit from '../list-m2m/use-edit';
import useSelection from '../list-m2m/use-selection';

View File

@@ -85,7 +85,7 @@ import { get } from 'lodash';
import Draggable from 'vuedraggable';
import useActions from './use-actions';
import useRelation from './use-relation';
import useRelation from '@/composables/use-m2m';
import usePreview from './use-preview';
import useEdit from './use-edit';
import usePermissions from './use-permissions';

View File

@@ -1,6 +1,6 @@
import { get, has, isEqual } from 'lodash';
import { Ref } from 'vue';
import { RelationInfo } from './use-relation';
import { RelationInfo } from '@/composables/use-m2m';
type UsableActions = {
getJunctionItem: (id: string | number) => string | number | Record<string, any> | null;

View File

@@ -1,6 +1,6 @@
import { Ref, ref } from 'vue';
import { get, isEqual } from 'lodash';
import { RelationInfo } from './use-relation';
import { RelationInfo } from '@/composables/use-m2m';
type UsableEdit = {
currentlyEditing: Ref<string | number | null>;

View File

@@ -5,7 +5,7 @@ import { Field } from '@directus/shared/types';
import { addRelatedPrimaryKeyToFields } from '@/utils/add-related-primary-key-to-fields';
import { cloneDeep, get, merge } from 'lodash';
import { Ref, ref, watch } from 'vue';
import { RelationInfo } from './use-relation';
import { RelationInfo } from '@/composables/use-m2m';
import { getEndpoint } from '@/utils/get-endpoint';
type UsablePreview = {

View File

@@ -1,6 +1,6 @@
import { get } from 'lodash';
import { computed, ComputedRef, Ref, ref } from 'vue';
import { RelationInfo } from './use-relation';
import { RelationInfo } from '@/composables/use-m2m';
type UsableSelection = {
stageSelection: (newSelection: (number | string)[]) => void;

View File

@@ -1,7 +1,7 @@
import { Sort } from '@/components/v-table/types';
import { sortBy } from 'lodash';
import { computed, ComputedRef, Ref, ref } from 'vue';
import { RelationInfo } from './use-relation';
import { RelationInfo } from '@/composables/use-m2m';
type UsableSort = {
sort: Ref<Sort>;

View File

@@ -13,8 +13,8 @@
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { Field, Relation } from '@directus/shared/types';
import { defineComponent, PropType, computed } from 'vue';
import { useFieldsStore } from '@/stores/';
import { defineComponent, PropType, computed, toRefs } from 'vue';
import useRelation from '@/composables/use-m2m';
export default defineComponent({
props: {
@@ -38,8 +38,9 @@ export default defineComponent({
emits: ['input'],
setup(props, { emit }) {
const { t } = useI18n();
const { collection } = toRefs(props);
const fieldsStore = useFieldsStore();
const field = computed(() => props.fieldData.field);
const languageField = computed({
get() {
@@ -53,34 +54,10 @@ export default defineComponent({
},
});
const translationsRelation = computed(() => {
if (!props.fieldData || !props.relations || props.relations.length === 0) return null;
const { field } = props.fieldData;
return (
props.relations.find(
(relation) => relation.related_collection === props.collection && relation.meta?.one_field === field
) ?? null
);
});
const languageRelation = computed(() => {
if (!props.fieldData || !props.relations || props.relations.length === 0) return null;
if (!translationsRelation.value) return null;
return (
props.relations.find(
(relation) =>
relation.collection === translationsRelation.value?.collection &&
relation.meta?.junction_field === translationsRelation.value?.field
) ?? null
);
});
const languageCollection = computed(() => languageRelation.value?.related_collection ?? null);
const languageCollectionFields = computed(() => {
if (!languageCollection.value) return [];
return fieldsStore.getFieldsForCollection(languageCollection.value);
});
const { relationCollection: languageCollection, relationFields: languageCollectionFields } = useRelation(
collection,
field
);
return {
t,

View File

@@ -52,12 +52,12 @@ import { computed, defineComponent, PropType, Ref, ref, toRefs, watch, unref } f
import { useFieldsStore, useRelationsStore, useUserStore } from '@/stores/';
import { useI18n } from 'vue-i18n';
import api from '@/api';
import { Relation } from '@directus/shared/types';
import { useCollection } from '@directus/shared/composables';
import { unexpectedError } from '@/utils/unexpected-error';
import { cloneDeep, isEqual, assign } from 'lodash';
import { notEmpty } from '@/utils/is-empty';
import { useWindowSize } from '@/composables/use-window-size';
import useRelation from '@/composables/use-m2m';
export default defineComponent({
components: { LanguageSelect },
@@ -85,7 +85,7 @@ export default defineComponent({
},
emits: ['input'],
setup(props, { emit }) {
const { collection } = toRefs(props);
const { collection, field } = toRefs(props);
const fieldsStore = useFieldsStore();
const relationsStore = useRelationsStore();
const { t } = useI18n();
@@ -108,15 +108,18 @@ export default defineComponent({
});
const {
relationsForField,
translationsRelation,
translationsCollection,
translationsPrimaryKeyField,
languagesRelation,
languagesCollection,
languagesPrimaryKeyField,
translationsLanguageField,
} = useRelations();
junction: translationsRelation,
junctionCollection: translationsCollection,
junctionPrimaryKeyField: translationsPrimaryKeyField,
relation: languagesRelation,
relationCollection: languagesCollection,
relationPrimaryKeyField: languagesPrimaryKeyField,
} = useRelation(collection, field);
const translationsLanguageField = computed(() => {
if (!languagesRelation.value) return null;
return languagesRelation.value.field;
});
const { languageOptions, loading: languagesLoading } = useLanguages();
const {
@@ -131,7 +134,7 @@ export default defineComponent({
const fields = computed(() => {
if (translationsCollection.value === null) return [];
return fieldsStore.getFieldsForCollection(translationsCollection.value);
return fieldsStore.getFieldsForCollection(translationsCollection.value.collection);
});
const splitViewAvailable = computed(() => {
@@ -150,7 +153,6 @@ export default defineComponent({
t,
languageOptions,
fields,
relationsForField,
translationsRelation,
translationsCollection,
translationsPrimaryKeyField,
@@ -171,63 +173,6 @@ export default defineComponent({
valuesLoading,
};
function useRelations() {
const relationsForField = computed<Relation[]>(() => {
return relationsStore.getRelationsForField(props.collection, props.field);
});
const translationsRelation = computed(() => {
if (!relationsForField.value) return null;
return (
relationsForField.value.find(
(relation: Relation) =>
relation.related_collection === props.collection && relation.meta?.one_field === props.field
) || null
);
});
const translationsCollection = computed(() => {
if (!translationsRelation.value) return null;
return translationsRelation.value.collection;
});
const translationsPrimaryKeyField = computed<string | null>(() => {
if (!translationsRelation.value) return null;
return fieldsStore.getPrimaryKeyFieldForCollection(translationsRelation.value.collection).field;
});
const languagesRelation = computed(() => {
if (!relationsForField.value) return null;
return relationsForField.value.find((relation: Relation) => relation !== translationsRelation.value) || null;
});
const languagesCollection = computed(() => {
if (!languagesRelation.value) return null;
return languagesRelation.value.related_collection;
});
const languagesPrimaryKeyField = computed<string | null>(() => {
if (!languagesRelation.value || !languagesRelation.value.related_collection) return null;
return fieldsStore.getPrimaryKeyFieldForCollection(languagesRelation.value.related_collection).field;
});
const translationsLanguageField = computed(() => {
if (!languagesRelation.value) return null;
return languagesRelation.value.field;
});
return {
relationsForField,
translationsRelation,
translationsCollection,
translationsPrimaryKeyField,
languagesRelation,
languagesCollection,
languagesPrimaryKeyField,
translationsLanguageField,
};
}
function useLanguages() {
const languages = ref<Record<string, any>[]>([]);
const loading = ref(false);
@@ -249,7 +194,7 @@ export default defineComponent({
return languages.value.map((language) => {
if (languagesPrimaryKeyField.value === null) return language;
const langCode = language[languagesPrimaryKeyField.value];
const langCode = language[languagesPrimaryKeyField.value.field];
const initialValue = items.value.find((item) => item[langField] === langCode) ?? {};
@@ -264,10 +209,10 @@ export default defineComponent({
}).length;
return {
text: language[props.languageField ?? languagesPrimaryKeyField.value],
text: language[props.languageField ?? languagesPrimaryKeyField.value.field],
value: langCode,
edited: edits !== undefined,
progress: (filledFields / totalFields) * 100,
progress: Math.round((filledFields / totalFields) * 100),
max: totalFields,
current: filledFields,
};
@@ -285,12 +230,12 @@ export default defineComponent({
fields.add(props.languageField);
}
fields.add(languagesPrimaryKeyField.value);
fields.add(languagesPrimaryKeyField.value.field);
loading.value = true;
try {
const response = await api.get(`/items/${languagesCollection.value}`, {
const response = await api.get<any>(`/items/${languagesCollection.value.collection}`, {
params: {
fields: Array.from(fields),
limit: -1,
@@ -301,16 +246,15 @@ export default defineComponent({
languages.value = response.data.data;
if (!firstLang.value) {
const languages = response.data.data;
const userLang = languages?.find(
const userLang = response.data.data?.find(
(lang) => lang[languagesPrimaryKeyField.value] === userStore.currentUser.language
)?.[languagesPrimaryKeyField.value];
firstLang.value = userLang ?? languages?.[0]?.[languagesPrimaryKeyField.value];
firstLang.value = userLang || response.data.data?.[0]?.[languagesPrimaryKeyField.value.field];
}
if (!secondLang.value) {
secondLang.value = response.data.data?.[1]?.[languagesPrimaryKeyField.value];
secondLang.value = response.data.data?.[1]?.[languagesPrimaryKeyField.value.field];
}
} catch (err: any) {
unexpectedError(err);
@@ -350,9 +294,9 @@ export default defineComponent({
);
const value = computed(() => {
const pkField = translationsPrimaryKeyField.value;
const pkField = translationsPrimaryKeyField.value?.field;
if (pkField === null) return [];
if (!pkField) return [];
const value = [...items.value.map((item) => item[pkField])] as (number | string | Record<string, any>)[];
@@ -398,7 +342,7 @@ export default defineComponent({
loading.value = true;
try {
const response = await api.get(`/items/${translationsCollection.value}`, {
const response = await api.get(`/items/${translationsCollection.value.collection}`, {
params: {
fields: '*',
limit: -1,
@@ -420,14 +364,14 @@ export default defineComponent({
}
function updateValue(edits: Record<string, any>, lang: string) {
const pkField = translationsPrimaryKeyField.value;
const pkField = translationsPrimaryKeyField.value?.field;
const langField = translationsLanguageField.value;
const existing = getExistingValue(lang);
const values = assign({}, existing, edits);
if (pkField === null || langField === null) return;
if (!pkField || !langField) return;
let copyValue = cloneDeep(value.value ?? []);

View File

@@ -1376,7 +1376,7 @@ interfaces:
display_template: Display Template
no_collection: No Collection
toggle_split_view: Toggle Split View
language_field: Language Field
language_field: Language Indicator Field
list-o2m-tree-view:
description: Tree view for nested recursive one-to-many items
recursive_only: The tree view interface only works for recursive relationships.
@@ -1413,6 +1413,13 @@ interfaces:
start_open: Start Open
start_closed: Start Closed
displays:
translations:
translations: Translations
description: Preview translations
enable: Enable
user_language: Use Current User Language
default_language: Default Language
language_field: Language Indicator Field
boolean:
boolean: Boolean
description: Display on and off states

View File

@@ -6,118 +6,200 @@ _Changes marked with a :warning: contain potential breaking changes depending on
### :warning: Potential Breaking Changes
* Custom displays's handler function was renamed to `component` to be consistent with the other app extensions
* If you're upgrading from 95, and had some troubles with migrating due to "group" on directus_fields (https://github.com/directus/directus/issues/8369) on that version, please remove row `20210927A` from `directus_migrations` and re-run the migrations.
- Custom displays's handler function was renamed to `component` to be consistent with the other app extensions
- If you're upgrading from 95, and had some troubles with migrating due to "group" on directus_fields
(https://github.com/directus/directus/issues/8369) on that version, please remove row `20210927A` from
`directus_migrations` and re-run the migrations.
### :sparkles: New Features
- **App**
- [#8570](https://github.com/directus/directus/pull/8570) Add new advanced filters experience ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#8570](https://github.com/directus/directus/pull/8570) Add new advanced filters experience
([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#7492](https://github.com/directus/directus/pull/7492) Add Filter interface ([@Nitwel](https://github.com/Nitwel))
### :rocket: Improvements
- **App**
- [#8614](https://github.com/directus/directus/pull/8614) Show file-image actions button upon focus; Use hover style for focuse… ([@dimitrov-adrian](https://github.com/dimitrov-adrian))
- [#8598](https://github.com/directus/directus/pull/8598) added robots.txt in order to disallow any indexing by search engines ([@sensedrive](https://github.com/sensedrive))
- [#8566](https://github.com/directus/directus/pull/8566) smaller and bolder breadcrumb ([@benhaynes](https://github.com/benhaynes))
- [#8564](https://github.com/directus/directus/pull/8564) update orange colors ([@benhaynes](https://github.com/benhaynes))
- [#8554](https://github.com/directus/directus/pull/8554) autofocus input for Import from URL dialog ([@azrikahar](https://github.com/azrikahar))
- [#8468](https://github.com/directus/directus/pull/8468) Removed unused properties from ModuleConfig ([@nickrum](https://github.com/nickrum))
- [#8388](https://github.com/directus/directus/pull/8388) Remove invalid CSS from presets item view ([@licitdev](https://github.com/licitdev))
- [#8108](https://github.com/directus/directus/pull/8108) Add save and delete shortcuts ([@Nitwel](https://github.com/Nitwel))
- [#7546](https://github.com/directus/directus/pull/7546) Use display template for button links ([@Nitwel](https://github.com/Nitwel))
- [#8614](https://github.com/directus/directus/pull/8614) Show file-image actions button upon focus; Use hover style
for focuse… ([@dimitrov-adrian](https://github.com/dimitrov-adrian))
- [#8598](https://github.com/directus/directus/pull/8598) added robots.txt in order to disallow any indexing by search
engines ([@sensedrive](https://github.com/sensedrive))
- [#8566](https://github.com/directus/directus/pull/8566) smaller and bolder breadcrumb
([@benhaynes](https://github.com/benhaynes))
- [#8564](https://github.com/directus/directus/pull/8564) update orange colors
([@benhaynes](https://github.com/benhaynes))
- [#8554](https://github.com/directus/directus/pull/8554) autofocus input for Import from URL dialog
([@azrikahar](https://github.com/azrikahar))
- [#8468](https://github.com/directus/directus/pull/8468) Removed unused properties from ModuleConfig
([@nickrum](https://github.com/nickrum))
- [#8388](https://github.com/directus/directus/pull/8388) Remove invalid CSS from presets item view
([@licitdev](https://github.com/licitdev))
- [#8108](https://github.com/directus/directus/pull/8108) Add save and delete shortcuts
([@Nitwel](https://github.com/Nitwel))
- [#7546](https://github.com/directus/directus/pull/7546) Use display template for button links
([@Nitwel](https://github.com/Nitwel))
- **API**
- [#8597](https://github.com/directus/directus/pull/8597) Check for duplicate migration keys ([@heyarne](https://github.com/heyarne))
- [#8397](https://github.com/directus/directus/pull/8397) Refactor action value from authenticate to login in directus_activity ([@licitdev](https://github.com/licitdev))
- [#8041](https://github.com/directus/directus/pull/8041) Convert to object default json value ([@joselcvarela](https://github.com/joselcvarela))
- [#8597](https://github.com/directus/directus/pull/8597) Check for duplicate migration keys
([@heyarne](https://github.com/heyarne))
- [#8397](https://github.com/directus/directus/pull/8397) Refactor action value from authenticate to login in
directus_activity ([@licitdev](https://github.com/licitdev))
- [#8041](https://github.com/directus/directus/pull/8041) Convert to object default json value
([@joselcvarela](https://github.com/joselcvarela))
- **Extensions**
- [#8593](https://github.com/directus/directus/pull/8593) Make directus:extension.hidden optional ([@nickrum](https://github.com/nickrum))
- [#8593](https://github.com/directus/directus/pull/8593) Make directus:extension.hidden optional
([@nickrum](https://github.com/nickrum))
### :bug: Bug Fixes
- **App**
- [#8603](https://github.com/directus/directus/pull/8603) Ignore WYSIWYG change on first load ([@azrikahar](https://github.com/azrikahar))
- [#8602](https://github.com/directus/directus/pull/8602) fix orderBy to prioritize system fields first ([@azrikahar](https://github.com/azrikahar))
- [#8567](https://github.com/directus/directus/pull/8567) Fix data model edits tracking ([@licitdev](https://github.com/licitdev))
- [#8533](https://github.com/directus/directus/pull/8533) Add permission check during hydration of insights store ([@licitdev](https://github.com/licitdev))
- [#8528](https://github.com/directus/directus/pull/8528) Set integer type on tileSize ([@dimitrov-adrian](https://github.com/dimitrov-adrian))
- [#8513](https://github.com/directus/directus/pull/8513) Add empty object check for permissions ([@licitdev](https://github.com/licitdev))
- [#8509](https://github.com/directus/directus/pull/8509) Add revert event handling in users module ([@licitdev](https://github.com/licitdev))
- [#8504](https://github.com/directus/directus/pull/8504) Hide revision's revert button for created entries ([@licitdev](https://github.com/licitdev))
- [#8379](https://github.com/directus/directus/pull/8379) Fix marginTop not implemented in presentation divider ([@licitdev](https://github.com/licitdev))
- [#8373](https://github.com/directus/directus/pull/8373) Add discard confirmation prompt for project settings ([@licitdev](https://github.com/licitdev))
- [#8365](https://github.com/directus/directus/pull/8365) Fix relative link routing in button links ([@licitdev](https://github.com/licitdev))
- [#8603](https://github.com/directus/directus/pull/8603) Ignore WYSIWYG change on first load
([@azrikahar](https://github.com/azrikahar))
- [#8602](https://github.com/directus/directus/pull/8602) fix orderBy to prioritize system fields first
([@azrikahar](https://github.com/azrikahar))
- [#8567](https://github.com/directus/directus/pull/8567) Fix data model edits tracking
([@licitdev](https://github.com/licitdev))
- [#8533](https://github.com/directus/directus/pull/8533) Add permission check during hydration of insights store
([@licitdev](https://github.com/licitdev))
- [#8528](https://github.com/directus/directus/pull/8528) Set integer type on tileSize
([@dimitrov-adrian](https://github.com/dimitrov-adrian))
- [#8513](https://github.com/directus/directus/pull/8513) Add empty object check for permissions
([@licitdev](https://github.com/licitdev))
- [#8509](https://github.com/directus/directus/pull/8509) Add revert event handling in users module
([@licitdev](https://github.com/licitdev))
- [#8504](https://github.com/directus/directus/pull/8504) Hide revision's revert button for created entries
([@licitdev](https://github.com/licitdev))
- [#8379](https://github.com/directus/directus/pull/8379) Fix marginTop not implemented in presentation divider
([@licitdev](https://github.com/licitdev))
- [#8373](https://github.com/directus/directus/pull/8373) Add discard confirmation prompt for project settings
([@licitdev](https://github.com/licitdev))
- [#8365](https://github.com/directus/directus/pull/8365) Fix relative link routing in button links
([@licitdev](https://github.com/licitdev))
- **drive**
- [#8601](https://github.com/directus/directus/pull/8601) Turn GCS credentials from camelCase to snake_case ([@azrikahar](https://github.com/azrikahar))
- [#8601](https://github.com/directus/directus/pull/8601) Turn GCS credentials from camelCase to snake_case
([@azrikahar](https://github.com/azrikahar))
- **API**
- [#8575](https://github.com/directus/directus/pull/8575) Fix field permissions check in aggregate ([@azrikahar](https://github.com/azrikahar))
- [#8553](https://github.com/directus/directus/pull/8553) pass MutationOptions to createOne ([@azrikahar](https://github.com/azrikahar))
- [#8526](https://github.com/directus/directus/pull/8526) Fix password exception crashing server ([@aidenfoxx](https://github.com/aidenfoxx))
- [#8490](https://github.com/directus/directus/pull/8490) Disable Cron hooks when only the CLI is running ([@nickrum](https://github.com/nickrum))
- [#8423](https://github.com/directus/directus/pull/8423) Fix sanitize aggregate on parse objects ([@joselcvarela](https://github.com/joselcvarela))
- [#8404](https://github.com/directus/directus/pull/8404) Fix group migration on MySQL ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#8399](https://github.com/directus/directus/pull/8399) Fix email migration for MS SQL ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#8391](https://github.com/directus/directus/pull/8391) Add defaults for null fields in permissions ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#8389](https://github.com/directus/directus/pull/8389) Send correct payload to auth provider for oauth ([@aidenfoxx](https://github.com/aidenfoxx))
- [#8375](https://github.com/directus/directus/pull/8375) fix "add conditions to fields" migration ([@azrikahar](https://github.com/azrikahar))
- [#8575](https://github.com/directus/directus/pull/8575) Fix field permissions check in aggregate
([@azrikahar](https://github.com/azrikahar))
- [#8553](https://github.com/directus/directus/pull/8553) pass MutationOptions to createOne
([@azrikahar](https://github.com/azrikahar))
- [#8526](https://github.com/directus/directus/pull/8526) Fix password exception crashing server
([@aidenfoxx](https://github.com/aidenfoxx))
- [#8490](https://github.com/directus/directus/pull/8490) Disable Cron hooks when only the CLI is running
([@nickrum](https://github.com/nickrum))
- [#8423](https://github.com/directus/directus/pull/8423) Fix sanitize aggregate on parse objects
([@joselcvarela](https://github.com/joselcvarela))
- [#8404](https://github.com/directus/directus/pull/8404) Fix group migration on MySQL
([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#8399](https://github.com/directus/directus/pull/8399) Fix email migration for MS SQL
([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#8391](https://github.com/directus/directus/pull/8391) Add defaults for null fields in permissions
([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#8389](https://github.com/directus/directus/pull/8389) Send correct payload to auth provider for oauth
([@aidenfoxx](https://github.com/aidenfoxx))
- [#8375](https://github.com/directus/directus/pull/8375) fix "add conditions to fields" migration
([@azrikahar](https://github.com/azrikahar))
### :sponge: Optimizations
- **Misc.**
- [#8616](https://github.com/directus/directus/pull/8616) Update the Dockerfile link in readme ([@nickrum](https://github.com/nickrum))
- [#8599](https://github.com/directus/directus/pull/8599) Add .nvmrc to improve dev flow for nvm users ([@sensedrive](https://github.com/sensedrive))
- [#8590](https://github.com/directus/directus/pull/8590) Recommend npm init directus-project to create a project ([@nickrum](https://github.com/nickrum))
- [#8489](https://github.com/directus/directus/pull/8489) Allow unused vars starting with underscore ([@paescuj](https://github.com/paescuj))
- [#8469](https://github.com/directus/directus/pull/8469) e2e test improvement ([@rijkvanzanten](https://github.com/rijkvanzanten))
- [#8616](https://github.com/directus/directus/pull/8616) Update the Dockerfile link in readme
([@nickrum](https://github.com/nickrum))
- [#8599](https://github.com/directus/directus/pull/8599) Add .nvmrc to improve dev flow for nvm users
([@sensedrive](https://github.com/sensedrive))
- [#8590](https://github.com/directus/directus/pull/8590) Recommend npm init directus-project to create a project
([@nickrum](https://github.com/nickrum))
- [#8489](https://github.com/directus/directus/pull/8489) Allow unused vars starting with underscore
([@paescuj](https://github.com/paescuj))
- [#8469](https://github.com/directus/directus/pull/8469) e2e test improvement
([@rijkvanzanten](https://github.com/rijkvanzanten))
- **API**
- [#8478](https://github.com/directus/directus/pull/8478) Move extension management into a class ([@nickrum](https://github.com/nickrum))
- [#8383](https://github.com/directus/directus/pull/8383) Remove duplicate directus_migrations collection ([@nickrum](https://github.com/nickrum))
- [#8478](https://github.com/directus/directus/pull/8478) Move extension management into a class
([@nickrum](https://github.com/nickrum))
- [#8383](https://github.com/directus/directus/pull/8383) Remove duplicate directus_migrations collection
([@nickrum](https://github.com/nickrum))
- **App**
- :warning: [#8475](https://github.com/directus/directus/pull/8475) Drop support for display handler functions in favor of functional components and make the routes module config required ([@nickrum](https://github.com/nickrum))
- [#8474](https://github.com/directus/directus/pull/8474) Fix types of mime package ([@nickrum](https://github.com/nickrum))
- [#8382](https://github.com/directus/directus/pull/8382) Fix popper modifier validation error ([@nickrum](https://github.com/nickrum))
- :warning: [#8475](https://github.com/directus/directus/pull/8475) Drop support for display handler functions in
favor of functional components and make the routes module config required ([@nickrum](https://github.com/nickrum))
- [#8474](https://github.com/directus/directus/pull/8474) Fix types of mime package
([@nickrum](https://github.com/nickrum))
- [#8382](https://github.com/directus/directus/pull/8382) Fix popper modifier validation error
([@nickrum](https://github.com/nickrum))
- **Extensions**
- :warning: [#8475](https://github.com/directus/directus/pull/8475) Drop support for display handler functions in favor of functional components and make the routes module config required ([@nickrum](https://github.com/nickrum))
- :warning: [#8475](https://github.com/directus/directus/pull/8475) Drop support for display handler functions in
favor of functional components and make the routes module config required ([@nickrum](https://github.com/nickrum))
### :memo: Documentation
- [#8590](https://github.com/directus/directus/pull/8590) Recommend npm init directus-project to create a project ([@nickrum](https://github.com/nickrum))
- [#8590](https://github.com/directus/directus/pull/8590) Recommend npm init directus-project to create a project
([@nickrum](https://github.com/nickrum))
### :package: Dependency Updates
- [#8622](https://github.com/directus/directus/pull/8622) Update dependency @types/markdown-it to v12.2.3 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8608](https://github.com/directus/directus/pull/8608) Update dependency vite to v2.6.4 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8605](https://github.com/directus/directus/pull/8605) Update dependency pinia to v2.0.0-rc.12 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8594](https://github.com/directus/directus/pull/8594) Update dependency vue-i18n to v9.1.9 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8591](https://github.com/directus/directus/pull/8591) Update dependency tedious to v13.1.0 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8585](https://github.com/directus/directus/pull/8585) Update dependency eslint-plugin-vue to v7.19.1 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8573](https://github.com/directus/directus/pull/8573) Update dependency nanoid to v3.1.29 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8571](https://github.com/directus/directus/pull/8571) Update dependency @types/markdown-it to v12.2.2 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8558](https://github.com/directus/directus/pull/8558) Update dependency vite to v2.6.3 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8557](https://github.com/directus/directus/pull/8557) Update dependency @vitejs/plugin-vue to v1.9.3 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8551](https://github.com/directus/directus/pull/8551) Update dependency eslint-plugin-vue to v7.19.0 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8548](https://github.com/directus/directus/pull/8548) Update typescript-eslint monorepo to v4.33.0 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8547](https://github.com/directus/directus/pull/8547) Update dependency npm to v7.24.2 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8532](https://github.com/directus/directus/pull/8532) Update dependency slugify to v1.6.1 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8530](https://github.com/directus/directus/pull/8530) Update dependency lint-staged to v11.2.0 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8525](https://github.com/directus/directus/pull/8525) Update dependency vue-i18n to v9.1.8 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8515](https://github.com/directus/directus/pull/8515) Update dependency pinia to v2.0.0-rc.11 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8500](https://github.com/directus/directus/pull/8500) Update dependency @types/codemirror to v5.60.4 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8496](https://github.com/directus/directus/pull/8496) Update dependency @types/node-cron to v2.0.5 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8494](https://github.com/directus/directus/pull/8494) Update dependency @rollup/plugin-commonjs to v21 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8484](https://github.com/directus/directus/pull/8484) Update dependency rollup to v2.58.0 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8466](https://github.com/directus/directus/pull/8466) Update dependency vite to v2.6.2 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8455](https://github.com/directus/directus/pull/8455) Update dependency @popperjs/core to v2.10.2 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8453](https://github.com/directus/directus/pull/8453) Update dependency pinia to v2.0.0-rc.10 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8436](https://github.com/directus/directus/pull/8436) Update dependency vite to v2.6.1 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8430](https://github.com/directus/directus/pull/8430) Update dependency vite to v2.6.0 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8429](https://github.com/directus/directus/pull/8429) Update dependency codemirror to v5.63.1 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8426](https://github.com/directus/directus/pull/8426) Update jest monorepo to v27.2.4 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8414](https://github.com/directus/directus/pull/8414) Update dependency tedious to v13 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8405](https://github.com/directus/directus/pull/8405) Pin dependency tmp to v0.0.33 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8403](https://github.com/directus/directus/pull/8403) Update dependency @types/dompurify to v2.3.0 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8380](https://github.com/directus/directus/pull/8380) Update jest monorepo to v27.2.3 ([@renovate[bot]](https://github.com/apps/renovate))
- [#8622](https://github.com/directus/directus/pull/8622) Update dependency @types/markdown-it to v12.2.3
([@renovate[bot]](https://github.com/apps/renovate))
- [#8608](https://github.com/directus/directus/pull/8608) Update dependency vite to v2.6.4
([@renovate[bot]](https://github.com/apps/renovate))
- [#8605](https://github.com/directus/directus/pull/8605) Update dependency pinia to v2.0.0-rc.12
([@renovate[bot]](https://github.com/apps/renovate))
- [#8594](https://github.com/directus/directus/pull/8594) Update dependency vue-i18n to v9.1.9
([@renovate[bot]](https://github.com/apps/renovate))
- [#8591](https://github.com/directus/directus/pull/8591) Update dependency tedious to v13.1.0
([@renovate[bot]](https://github.com/apps/renovate))
- [#8585](https://github.com/directus/directus/pull/8585) Update dependency eslint-plugin-vue to v7.19.1
([@renovate[bot]](https://github.com/apps/renovate))
- [#8573](https://github.com/directus/directus/pull/8573) Update dependency nanoid to v3.1.29
([@renovate[bot]](https://github.com/apps/renovate))
- [#8571](https://github.com/directus/directus/pull/8571) Update dependency @types/markdown-it to v12.2.2
([@renovate[bot]](https://github.com/apps/renovate))
- [#8558](https://github.com/directus/directus/pull/8558) Update dependency vite to v2.6.3
([@renovate[bot]](https://github.com/apps/renovate))
- [#8557](https://github.com/directus/directus/pull/8557) Update dependency @vitejs/plugin-vue to v1.9.3
([@renovate[bot]](https://github.com/apps/renovate))
- [#8551](https://github.com/directus/directus/pull/8551) Update dependency eslint-plugin-vue to v7.19.0
([@renovate[bot]](https://github.com/apps/renovate))
- [#8548](https://github.com/directus/directus/pull/8548) Update typescript-eslint monorepo to v4.33.0
([@renovate[bot]](https://github.com/apps/renovate))
- [#8547](https://github.com/directus/directus/pull/8547) Update dependency npm to v7.24.2
([@renovate[bot]](https://github.com/apps/renovate))
- [#8532](https://github.com/directus/directus/pull/8532) Update dependency slugify to v1.6.1
([@renovate[bot]](https://github.com/apps/renovate))
- [#8530](https://github.com/directus/directus/pull/8530) Update dependency lint-staged to v11.2.0
([@renovate[bot]](https://github.com/apps/renovate))
- [#8525](https://github.com/directus/directus/pull/8525) Update dependency vue-i18n to v9.1.8
([@renovate[bot]](https://github.com/apps/renovate))
- [#8515](https://github.com/directus/directus/pull/8515) Update dependency pinia to v2.0.0-rc.11
([@renovate[bot]](https://github.com/apps/renovate))
- [#8500](https://github.com/directus/directus/pull/8500) Update dependency @types/codemirror to v5.60.4
([@renovate[bot]](https://github.com/apps/renovate))
- [#8496](https://github.com/directus/directus/pull/8496) Update dependency @types/node-cron to v2.0.5
([@renovate[bot]](https://github.com/apps/renovate))
- [#8494](https://github.com/directus/directus/pull/8494) Update dependency @rollup/plugin-commonjs to v21
([@renovate[bot]](https://github.com/apps/renovate))
- [#8484](https://github.com/directus/directus/pull/8484) Update dependency rollup to v2.58.0
([@renovate[bot]](https://github.com/apps/renovate))
- [#8466](https://github.com/directus/directus/pull/8466) Update dependency vite to v2.6.2
([@renovate[bot]](https://github.com/apps/renovate))
- [#8455](https://github.com/directus/directus/pull/8455) Update dependency @popperjs/core to v2.10.2
([@renovate[bot]](https://github.com/apps/renovate))
- [#8453](https://github.com/directus/directus/pull/8453) Update dependency pinia to v2.0.0-rc.10
([@renovate[bot]](https://github.com/apps/renovate))
- [#8436](https://github.com/directus/directus/pull/8436) Update dependency vite to v2.6.1
([@renovate[bot]](https://github.com/apps/renovate))
- [#8430](https://github.com/directus/directus/pull/8430) Update dependency vite to v2.6.0
([@renovate[bot]](https://github.com/apps/renovate))
- [#8429](https://github.com/directus/directus/pull/8429) Update dependency codemirror to v5.63.1
([@renovate[bot]](https://github.com/apps/renovate))
- [#8426](https://github.com/directus/directus/pull/8426) Update jest monorepo to v27.2.4
([@renovate[bot]](https://github.com/apps/renovate))
- [#8414](https://github.com/directus/directus/pull/8414) Update dependency tedious to v13
([@renovate[bot]](https://github.com/apps/renovate))
- [#8405](https://github.com/directus/directus/pull/8405) Pin dependency tmp to v0.0.33
([@renovate[bot]](https://github.com/apps/renovate))
- [#8403](https://github.com/directus/directus/pull/8403) Update dependency @types/dompurify to v2.3.0
([@renovate[bot]](https://github.com/apps/renovate))
- [#8380](https://github.com/directus/directus/pull/8380) Update jest monorepo to v27.2.3
([@renovate[bot]](https://github.com/apps/renovate))
Directus refs/tags/v9.0.0-rc.96