Merge pull request #423 from directus/translations

Translation updates
This commit is contained in:
Rijk van Zanten
2020-09-25 17:03:38 -04:00
committed by GitHub
11 changed files with 620 additions and 230 deletions

View File

@@ -68,9 +68,8 @@ export class FieldsService {
});
const aliasQuery = this.knex
.select<FieldMeta[]>('*')
.from('directus_fields')
.whereIn('special', ['alias', 'o2m', 'm2m']);
.select<any[]>('*')
.from('directus_fields');
if (collection) {
aliasQuery.andWhere('collection', collection);
@@ -78,6 +77,18 @@ export class FieldsService {
let aliasFields = await aliasQuery;
const aliasTypes = ['alias', 'o2m', 'm2m', 'files', 'files', 'translations'];
aliasFields = aliasFields.filter((field) => {
const specials = (field.special || '').split(',');
for (const type of aliasTypes) {
if (specials.includes(type)) return true;
}
return false;
});
aliasFields = (await this.payloadService.processValues('read', aliasFields)) as FieldMeta[];
const aliasFieldsAsField = aliasFields.map((field) => {

View File

@@ -164,7 +164,9 @@ body {
}
&.disabled {
--v-list-item-color: var(--foreground-subdued);
--v-list-item-color: var(--foreground-subdued) !important;
cursor: not-allowed;
}
@at-root {

View File

@@ -68,7 +68,13 @@ export default defineComponent({
const fieldsStore = useFieldsStore();
const relationsStore = useRelationsStore();
const { relations, translationsCollection, languagesCollection, languageField, translationsPrimaryKeyField } = useRelation();
const {
relations,
translationsCollection,
languagesCollection,
languageField,
translationsPrimaryKeyField,
} = useRelation();
const {
languages,
@@ -84,7 +90,7 @@ export default defineComponent({
const { info, primaryKeyField } = useCollection(languagesCollection);
const defaultTemplate = info.value?.meta?.display_template;
return defaultTemplate || props.template || `{{ $${primaryKeyField.value.field} }}`;
return props.template || defaultTemplate || `{{ ${primaryKeyField.value.field} }}`;
});
return {
@@ -113,10 +119,12 @@ export default defineComponent({
const translationsRelation = computed(() => {
if (!relations.value || relations.value.length === 0) return null;
return relations.value.find((relation: Relation) => {
return relation.one_collection === props.collection && relation.one_field === props.field;
}) || null;
})
return (
relations.value.find((relation: Relation) => {
return relation.one_collection === props.collection && relation.one_field === props.field;
}) || null
);
});
const translationsCollection = computed(() => {
if (!translationsRelation.value) return null;
@@ -130,9 +138,11 @@ export default defineComponent({
const languagesRelation = computed(() => {
if (!relations.value || relations.value.length === 0) return null;
return relations.value.find((relation: Relation) => {
return relation.one_collection !== props.collection && relation.one_field !== props.field;
}) || null;
return (
relations.value.find((relation: Relation) => {
return relation.one_collection !== props.collection && relation.one_field !== props.field;
}) || null
);
});
const languagesCollection = computed(() => {
@@ -143,9 +153,15 @@ export default defineComponent({
const languageField = computed(() => {
if (!languagesRelation.value) return null;
return languagesRelation.value.many_field;
})
});
return { relations, translationsCollection, languagesCollection, languageField, translationsPrimaryKeyField };
return {
relations,
translationsCollection,
languagesCollection,
languageField,
translationsPrimaryKeyField,
};
}
function useLanguages() {

View File

@@ -193,6 +193,15 @@
"not_available_for_type": "Not Available for this Type",
"create_translations": "Create Translations",
"auto_generate": "Auto-Generate",
"this_will_auto_setup_fields_relations": "This will automatically setup all required fields and relations.",
"click_here": "Click here",
"to_manually_setup_translations": "to manually setup translations.",
"click_to_manage_translated_fields": "There are no translated fields yet. Click here to create them. | There is one translated field. Click here to manage it. | There are {count} translated fields. Click here to manage them.",
"configure_m2o": "Configure your Many-to-One Relationship...",
"configure_o2m": "Configure your One-to-Many Relationship...",
"configure_m2m": "Configure your Many-to-Many Relationship...",

View File

@@ -1,84 +1,206 @@
<template>
<div>
<h2 class="type-title">{{ $t('configure_translations') }}</h2>
<h2 class="type-title">{{ $t('configure_m2m') }}</h2>
<div class="grid">
<div class="field">
<div class="type-label">{{ $t('this_collection') }}</div>
<v-input disabled :value="collection" />
<v-input disabled :value="relations[0].one_collection" />
</div>
<div class="field">
<div class="type-label">{{ $t('translations_collection') }}</div>
<v-input
db-safe
:class="{ matches: junctionCollectionExists }"
v-model="junctionCollection"
:placeholder="$t('collection') + '...'"
v-model="relations[0].many_collection"
:disabled="isExisting"
:class="{ matches: translationsCollectionExists }"
:disabled="autoFill || isExisting"
db-safe
>
<template #append>
<v-menu show-arrow placement="bottom-end">
<template #activator="{ toggle }">
<v-icon name="list_alt" @click="toggle" v-tooltip="$t('select_existing')" :disabled="isExisting" />
<v-icon
name="list_alt"
@click="toggle"
v-tooltip="$t('select_existing')"
:disabled="autoFill || isExisting"
/>
</template>
<v-list dense class="monospace">
<v-list-item
v-for="item in items"
:key="item.value"
:active="relations[0].many_collection === item.value"
:disabled="item.disabled"
@click="relations[0].many_collection = item.value"
v-for="collection in availableCollections"
:key="collection.collection"
:active="relations[0].many_collection === collection.collection"
@click="relations[0].many_collection = collection.collection"
>
<v-list-item-content>
{{ item.text }}
{{ collection.collection }}
</v-list-item-content>
</v-list-item>
<v-divider />
<v-list-group>
<template #activator>{{ $t('system') }}</template>
<v-list-item
v-for="collection in systemCollections"
:key="collection.collection"
:active="relations[0].many_collection === collection.collection"
@click="relations[0].many_collection = collection.collection"
>
<v-list-item-content>
{{ collection.collection }}
</v-list-item-content>
</v-list-item>
</v-list-group>
</v-list>
</v-menu>
</template>
</v-input>
</div>
<v-input disabled :value="currentCollectionPrimaryKey.field" />
<div class="field">
<div class="type-label">{{ $t('languages_collection') }}</div>
<v-input
:autofocus="autoFill"
:class="{ matches: relatedCollectionExists }"
v-model="relations[1].one_collection"
:placeholder="$t('collection') + '...'"
:disabled="type === 'files' || isExisting"
db-safe
>
<template #append>
<v-menu show-arrow placement="bottom-end">
<template #activator="{ toggle }">
<v-icon
name="list_alt"
@click="toggle"
v-tooltip="$t('select_existing')"
:disabled="type === 'files' || isExisting"
/>
</template>
<v-list dense class="monospace">
<v-list-item
v-for="collection in availableCollections"
:key="collection.collection"
:active="relations[1].one_collection === collection.collection"
@click="relations[1].one_collection = collection.collection"
>
<v-list-item-content>
{{ collection.collection }}
</v-list-item-content>
</v-list-item>
<v-divider />
<v-list-group>
<template #activator>{{ $t('system') }}</template>
<v-list-item
v-for="collection in systemCollections"
:key="collection.collection"
:active="relations[1].one_collection === collection.collection"
@click="relations[1].one_collection = collection.collection"
>
<v-list-item-content>
{{ collection.collection }}
</v-list-item-content>
</v-list-item>
</v-list-group>
</v-list>
</v-menu>
</template>
</v-input>
</div>
<v-input disabled :value="relations[0].one_primary" />
<v-input
db-safe
:class="{ matches: junctionFieldExists(relations[0].many_field) }"
v-model="relations[0].many_field"
:disabled="isExisting"
:placeholder="$t('foreign_key') + '...'"
:class="{ matches: translationsFieldExists }"
:disabled="autoFill || isExisting"
db-safe
>
<template #append v-if="fields && fields.length > 0">
<template #append v-if="junctionCollectionExists">
<v-menu show-arrow placement="bottom-end">
<template #activator="{ toggle }">
<v-icon name="list_alt" @click="toggle" v-tooltip="$t('select_existing')" />
<v-icon
name="list_alt"
@click="toggle"
v-tooltip="$t('select_existing')"
:disabled="autoFill || isExisting"
/>
</template>
<v-list dense class="monospace">
<v-list-item
v-for="field in fields"
:key="field.value"
:active="relations[0].many_field === field.value"
@click="relations[0].many_field = field.value"
:disabled="field.disabled"
v-for="item in junctionFields"
:key="item.value"
:active="relations[0].many_field === item.value"
:disabled="item.disabled"
@click="relations[0].many_field = item.value"
>
<v-list-item-content>
{{ field.text }}
{{ item.text }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-input>
<div class="spacer" />
<div class="spacer" />
<v-input
:class="{ matches: junctionFieldExists(relations[1].many_field) }"
v-model="relations[1].many_field"
:placeholder="$t('foreign_key') + '...'"
:disabled="autoFill || isExisting"
db-safe
>
<template #append v-if="junctionCollectionExists">
<v-menu show-arrow placement="bottom-end">
<template #activator="{ toggle }">
<v-icon
name="list_alt"
@click="toggle"
v-tooltip="$t('select_existing')"
:disabled="autoFill || isExisting"
/>
</template>
<v-list dense class="monospace">
<v-list-item
v-for="item in junctionFields"
:key="item.value"
:active="relations[1].many_field === item.value"
:disabled="item.disabled"
@click="relations[1].many_field = item.value"
>
<v-list-item-content>
{{ item.text }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-input>
<v-input
db-safe
:disabled="relatedCollectionExists"
v-model="relations[1].one_primary"
:placeholder="$t('primary_key') + '...'"
/>
<div class="spacer" />
<v-checkbox :disabled="isExisting" block v-model="autoFill" :label="$t('auto_fill')" />
<v-icon class="arrow" name="arrow_forward" />
<v-icon class="arrow" name="arrow_backward" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from '@vue/composition-api';
import { Relation, Field } from '@/types';
import useSync from '@/composables/use-sync';
import { useFieldsStore, useCollectionsStore } from '@/stores';
import { defineComponent, computed, ref } from '@vue/composition-api';
import { orderBy } from 'lodash';
import { useCollectionsStore, useFieldsStore } from '@/stores/';
import { Field } from '@/types';
import i18n from '@/lang';
import { state } from '../store';
@@ -98,74 +220,85 @@ export default defineComponent({
default: false,
},
},
setup(props, { emit }) {
setup(props) {
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const { items, fields, currentCollectionPrimaryKey, collectionMany } = useRelation();
const translationsCollectionExists = computed(() => {
return collectionsStore.state.collections.find((col) => col.collection === state.relations?.[0].many_collection);
const autoFill = computed({
get() {
return state.autoFillJunctionRelation;
},
set(newAuto: boolean) {
state.autoFillJunctionRelation = newAuto;
},
});
const translationsFieldExists = computed(() => {
if (!state?.relations?.[0].many_collection || !state?.relations?.[0].many_field) return false;
return !!fieldsStore.getField(state.relations[0].many_collection, state.relations[0].many_field);
const availableCollections = computed(() => {
return orderBy(
collectionsStore.state.collections.filter((collection) => {
return collection.collection.startsWith('directus_') === false;
}),
['collection'],
['asc']
);
});
return { relations: state.relations, items, fields, currentCollectionPrimaryKey, collectionMany, translationsCollectionExists, translationsFieldExists };
function useRelation() {
const availableCollections = computed(() => {
return orderBy(
collectionsStore.state.collections.filter((collection) => {
return (
collection.collection.startsWith('directus_') === false &&
collection.collection !== props.collection
);
}),
['collection'],
['asc']
);
});
const items = computed(() =>
availableCollections.value.map((collection) => ({
text: collection.collection,
value: collection.collection,
}))
const systemCollections = computed(() => {
return orderBy(
collectionsStore.state.collections.filter((collection) => {
return collection.collection.startsWith('directus_') === true;
}),
['collection'],
['asc']
);
});
const currentCollectionPrimaryKey = computed(() =>
fieldsStore.getPrimaryKeyFieldForCollection(props.collection)
);
const junctionCollection = computed({
get() {
return state.relations[0].many_collection;
},
set(collection: string) {
state.relations[0].many_collection = collection;
state.relations[1].many_collection = collection;
},
});
const fields = computed(() => {
if (!state.relations[0].many_collection) return [];
const junctionCollectionExists = computed(() => {
return collectionsStore.getCollection(junctionCollection.value) !== null;
});
return fieldsStore.state.fields
.filter((field) => field.collection === state.relations[0].many_collection)
.map((field) => ({
text: field.field,
value: field.field,
disabled:
!field.schema ||
field.schema?.is_primary_key ||
field.type !== currentCollectionPrimaryKey.value.type,
}));
});
const relatedCollectionExists = computed(() => {
return collectionsStore.getCollection(state.relations[1].one_collection) !== null;
});
const collectionMany = computed({
get() {
return state.relations[0].many_collection!;
},
set(collection: string) {
state.relations[0].many_collection = collection;
state.relations[0].many_field = '';
},
});
const junctionFields = computed(() => {
if (!junctionCollection.value) return [];
return { availableCollections, items, fields, currentCollectionPrimaryKey, collectionMany };
return fieldsStore.getFieldsForCollection(junctionCollection.value).map((field: Field) => ({
text: field.field,
value: field.field,
disabled:
state.relations[0].many_field === field.field ||
field.schema?.is_primary_key ||
state.relations[1].many_field === field.field,
}));
});
return {
relations: state.relations,
autoFill,
availableCollections,
systemCollections,
junctionCollection,
junctionFields,
junctionCollectionExists,
relatedCollectionExists,
junctionFieldExists,
};
function junctionFieldExists(fieldKey: string) {
if (!junctionCollection.value) return false;
return !!fieldsStore.getField(junctionCollection.value, fieldKey);
}
},
});
@@ -178,8 +311,8 @@ export default defineComponent({
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 32px;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px 28px;
margin-top: 48px;
.v-input.matches {
@@ -190,14 +323,19 @@ export default defineComponent({
--v-icon-color: var(--primary);
position: absolute;
bottom: 14px;
left: 50%;
transform: translateX(-50%);
}
}
pointer-events: none;
.v-list {
--v-list-item-content-font-family: var(--family-monospace);
&:first-of-type {
bottom: 141px;
left: 32.5%;
}
&:last-of-type {
bottom: 76px;
left: 67.4%;
}
}
}
.type-label {
@@ -208,20 +346,7 @@ export default defineComponent({
margin: 48px 0;
}
.corresponding {
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 32px;
margin-top: 48px;
.arrow {
--v-icon-color: var(--primary);
position: absolute;
bottom: 14px;
left: 50%;
transform: translateX(-50%);
}
.v-list {
--v-list-item-content-font-family: var(--family-monospace);
}
</style>

View File

@@ -1,5 +1,34 @@
<template>
<v-dialog
persistent
:active="true"
v-if="localType === 'translations' && translationsManual === false && field === '+'"
>
<v-card class="auto-translations">
<v-card-title>{{ $t('create_translations') }}</v-card-title>
<v-card-text>
<v-input v-model="fieldData.field" :placeholder="$t('field_name') + '...'" />
<v-notice>
<div>
{{ $t('this_will_auto_setup_fields_relations') }}
<br />
<button class="manual-toggle" @click="translationsManual = true">{{ $t('click_here') }}</button>
{{ $t('to_manually_setup_translations') }}
</div>
</v-notice>
</v-card-text>
<v-card-actions>
<v-button secondary @click="cancelField">{{ $t('cancel') }}</v-button>
<div class="spacer" />
<v-button :disabled="!fieldData.field" :loading="saving" @click="saveField">
{{ $t('auto_generate') }}
</v-button>
</v-card-actions>
</v-card>
</v-dialog>
<v-modal
v-else
:active="true"
:title="
field === '+'
@@ -34,13 +63,6 @@
:type="localType"
/>
<setup-languages
v-if="currentTab[0] === 'languages'"
:is-existing="field !== '+'"
:collection="collection"
:type="localType"
/>
<setup-interface
v-if="currentTab[0] === 'interface'"
:is-existing="field !== '+'"
@@ -76,7 +98,6 @@ import SetupActions from './components/actions.vue';
import SetupSchema from './components/schema.vue';
import SetupRelationship from './components/relationship.vue';
import SetupTranslations from './components/translations.vue';
import SetupLanguages from './components/languages.vue';
import SetupInterface from './components/interface.vue';
import SetupDisplay from './components/display.vue';
import { i18n } from '@/lang';
@@ -88,6 +109,7 @@ import { Field } from '@/types';
import router from '@/router';
import useCollection from '@/composables/use-collection';
import notify from '@/utils/notify';
import { getLocalTypeForField } from '../get-local-type';
import { initLocalStore, state, clearLocalStore } from './store';
@@ -98,7 +120,6 @@ export default defineComponent({
SetupSchema,
SetupRelationship,
SetupTranslations,
SetupLanguages,
SetupInterface,
SetupDisplay,
},
@@ -112,7 +133,9 @@ export default defineComponent({
required: true,
},
type: {
type: String as PropType<'standard' | 'file' | 'files' | 'm2o' | 'o2m' | 'm2m' | 'presentation' | 'translations'>,
type: String as PropType<
'standard' | 'file' | 'files' | 'm2o' | 'o2m' | 'm2m' | 'presentation' | 'translations'
>,
default: null,
},
},
@@ -121,6 +144,8 @@ export default defineComponent({
const fieldsStore = useFieldsStore();
const relationsStore = useRelationsStore();
const translationsManual = ref(false);
const { collection } = toRefs(props);
const { info: collectionInfo } = useCollection(collection);
@@ -134,7 +159,8 @@ export default defineComponent({
const localType = computed(() => {
if (props.field === '+') return props.type;
let type: 'standard' | 'file' | 'files' | 'o2m' | 'm2m' | 'm2o' | 'presentation' | 'translations' = 'standard';
let type: 'standard' | 'file' | 'files' | 'o2m' | 'm2m' | 'm2o' | 'presentation' | 'translations' =
'standard';
type = getLocalTypeForField(props.collection, props.field);
return type;
@@ -158,6 +184,7 @@ export default defineComponent({
localType,
existingField,
collectionInfo,
translationsManual,
};
function useTabs() {
@@ -192,18 +219,17 @@ export default defineComponent({
}
if (localType.value === 'translations') {
tabs.splice(1, 0, ...[
{
text: i18n.t('translations'),
value: 'translations',
disabled: translationsDisabled(),
},
{
text: i18n.t('languages'),
value: 'languages',
disabled: languagesDisabled(),
}
])
tabs.splice(
1,
0,
...[
{
text: i18n.t('translations'),
value: 'translations',
disabled: translationsDisabled(),
},
]
);
}
return tabs;
@@ -221,16 +247,6 @@ export default defineComponent({
return isEmpty(state.fieldData.field);
}
function languagesDisabled() {
return isEmpty(state.fieldData.field) || (
state.relations.length === 0 ||
isEmpty(state.relations[0].many_collection) ||
isEmpty(state.relations[0].many_field) ||
isEmpty(state.relations[0].one_collection) ||
isEmpty(state.relations[0].one_primary)
);
}
function interfaceDisplayDisabled() {
if (['o2m', 'm2o', 'file'].includes(localType.value)) {
return (
@@ -304,6 +320,13 @@ export default defineComponent({
})
);
await Promise.all(
Object.keys(state.newRows).map((collection) => {
const rows = state.newRows[collection];
return api.post(`/items/${collection}`, rows);
})
);
await collectionsStore.hydrate();
await fieldsStore.hydrate();
await relationsStore.hydrate();
@@ -333,48 +356,26 @@ export default defineComponent({
router.push(`/settings/data-model/${props.collection}`);
clearLocalStore();
}
function getLocalTypeForField(
collection: string,
field: string
): 'standard' | 'file' | 'files' | 'o2m' | 'm2m' | 'm2o' | 'presentation' | 'translations' {
const fieldInfo = fieldsStore.getField(collection, field);
const relations = relationsStore.getRelationsForField(collection, field);
if (relations.length === 0) {
if (fieldInfo.type === 'alias') return 'presentation';
return 'standard';
}
if (relations.length === 1) {
const relation = relations[0];
if (relation.one_collection === 'directus_files') return 'file';
if (relation.many_collection === collection) return 'm2o';
return 'o2m';
}
if (relations.length === 2) {
const relationForCurrent = relations.find(
(relation: Relation) =>
(relation.many_collection === collection && relation.many_field === field) ||
(relation.one_collection === collection && relation.one_field === field)
);
if (relationForCurrent?.many_collection === collection && relationForCurrent?.many_field === field)
return 'm2o';
if (
relations[0].one_collection === 'directus_files' ||
relations[1].one_collection === 'directus_files'
) {
return 'files';
} else {
return 'm2m';
}
}
return 'standard';
}
},
});
</script>
<style lang="scss" scoped>
.auto-translations {
.v-input {
--v-input-font-family: var(--family-monospace);
}
.v-notice {
margin-top: 12px;
}
.spacer {
flex-grow: 1;
}
.manual-toggle {
color: var(--primary);
}
}
</style>

View File

@@ -56,6 +56,7 @@ function initLocalStore(
newCollections: [],
newFields: [],
updateFields: [],
newRows: {},
autoFillJunctionRelation: true,
});
@@ -390,27 +391,104 @@ function initLocalStore(
}
if (collectionExists(relatedCollection) === false) {
state.newCollections.push({
$type: 'related',
collection: relatedCollection,
fields: [
{
field: state.relations[1].one_primary,
type: 'integer',
schema: {
has_auto_increment: true,
},
meta: {
hidden: true,
},
if (type === 'translations') {
state.newCollections.push({
$type: 'related',
collection: relatedCollection,
meta: {
icon: 'translate',
},
],
});
fields: [
{
field: state.relations[1].one_primary,
type: 'string',
schema: {
is_primary_key: true,
},
meta: {
interface: 'text-input',
options: {
iconLeft: 'vpn_key',
},
width: 'half',
},
},
{
field: 'name',
type: 'string',
schema: {},
meta: {
interface: 'text-input',
options: {
iconLeft: 'translate',
},
width: 'half',
},
},
],
});
} else {
state.newCollections.push({
$type: 'related',
collection: relatedCollection,
fields: [
{
field: state.relations[1].one_primary,
type: 'integer',
schema: {
has_auto_increment: true,
},
meta: {
hidden: true,
},
},
],
});
}
}
if (type === 'translations') {
if (collectionExists(relatedCollection) === false) {
state.newRows = {
[relatedCollection]: [
{
code: 'en-US',
name: 'English',
},
{
code: 'de-DE',
name: 'German',
},
{
code: 'fr-Fr',
name: 'French',
},
{
code: 'ru-RU',
name: 'Russian',
},
{
code: 'es-ES',
name: 'Spanish',
},
{
code: 'it-IT',
name: 'Italian',
},
{
code: 'pt-BR',
name: 'Portuguese',
},
],
};
} else {
state.newRows = {};
}
}
}, 50);
if (!isExisting) {
state.fieldData.meta.special = ['m2m'];
state.fieldData.meta.special = [type];
state.relations = [
{
@@ -430,6 +508,10 @@ function initLocalStore(
one_primary: type === 'files' ? 'id' : '',
},
];
if (type === 'translations') {
state.fieldData.field = 'translations';
}
}
watch(
@@ -542,15 +624,17 @@ function initLocalStore(
);
state.relations[0].many_collection = `${collection}_translations`;
state.relations[0].many_field = `${collection}_${
fieldsStore.getPrimaryKeyFieldForCollection(collection)?.field
}`;
state.relations[1].one_collection = 'languages';
if (collectionExists('languages')) {
state.relations[1].one_primary = fieldsStore.getPrimaryKeyFieldForCollection('languages')?.field;
} else {
state.relations[1].one_primary = 'id';
state.relations[1].one_primary = 'code';
}
state.relations[1].many_field = `${state.relations[1].one_collection}_${state.relations[1].one_primary}`;

View File

@@ -1,6 +1,46 @@
<template>
<div :class="(field.meta && field.meta.width) || 'full'">
<v-menu attached>
<div v-if="localType === 'translations'" class="group">
<div class="header">
<v-icon class="drag-handle" name="drag_indicator" />
<v-menu show-arrow>
<template #activator="{ toggle }">
<span class="group-options" @click="toggle">
<span class="name" v-tooltip="field.field">{{ field.name }}</span>
<v-icon name="expand_more" />
</span>
</template>
<v-list dense>
<v-list-item :to="`/settings/data-model/${field.collection}/${field.field}`">
<v-list-item-icon><v-icon name="edit" outline /></v-list-item-icon>
<v-list-item-content>
{{ $t('edit_field') }}
</v-list-item-content>
</v-list-item>
<v-divider />
<v-list-item @click="deleteActive = true" class="danger">
<v-list-item-icon><v-icon name="delete" outline /></v-list-item-icon>
<v-list-item-content>
{{ $t('delete_field') }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</div>
<router-link :to="`/settings/data-model/${translationsCollection}`">
<v-notice type="info" icon="translate">
<div>{{ $tc('click_to_manage_translated_fields', translationsFieldsCount) }}</div>
<div class="spacer" />
<v-icon name="launch" />
</v-notice>
</router-link>
</div>
<v-menu v-else attached>
<template #activator="{ toggle, active }">
<v-input class="field" :class="{ hidden, active }" readonly @click="openFieldDetail">
<template #prepend>
@@ -140,12 +180,13 @@
<script lang="ts">
import { defineComponent, PropType, ref, computed } from '@vue/composition-api';
import { Field } from '@/types';
import { useCollectionsStore, useFieldsStore } from '@/stores/';
import { Field, Relation } from '@/types';
import { useCollectionsStore, useFieldsStore, useRelationsStore } from '@/stores/';
import { getInterfaces } from '@/interfaces';
import router from '@/router';
import notify from '@/utils/notify';
import { i18n } from '@/lang';
import { getLocalTypeForField } from '../../get-local-type';
export default defineComponent({
props: {
@@ -155,11 +196,12 @@ export default defineComponent({
},
},
setup(props) {
const relationsStore = useRelationsStore();
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const interfaces = getInterfaces();
const editActive = ref(false);
const fieldsStore = useFieldsStore();
const collectionsStore = useCollectionsStore();
const { deleteActive, deleting, deleteField } = useDeleteField();
const { duplicateActive, duplicateName, collections, duplicateTo, saveDuplicate, duplicating } = useDuplicate();
@@ -170,6 +212,10 @@ export default defineComponent({
const hidden = computed(() => props.field.meta?.hidden === true);
const localType = computed(() => getLocalTypeForField(props.field.collection, props.field.field));
const { translationsCollection, translationsFieldsCount } = useTranslations();
return {
interfaceName,
editActive,
@@ -186,6 +232,9 @@ export default defineComponent({
openFieldDetail,
hidden,
toggleVisibility,
localType,
translationsCollection,
translationsFieldsCount,
};
function setWidth(width: string) {
@@ -275,6 +324,30 @@ export default defineComponent({
router.push(`/settings/data-model/${props.field.collection}/${props.field.field}`);
}
function useTranslations() {
const translationsCollection = computed(() => {
if (localType.value !== 'translations') return null;
const relation = relationsStore.state.relations.find((relation: Relation) => {
return (
relation.one_collection === props.field.collection && relation.one_field === props.field.field
);
});
if (!relation) return null;
return relation.many_collection;
});
const translationsFieldsCount = computed(() => {
const fields = fieldsStore.getFieldsForCollection(translationsCollection.value);
return fields.filter((field: Field) => field.meta?.hidden !== true).length;
});
return { translationsCollection, translationsFieldsCount };
}
},
});
</script>
@@ -282,11 +355,6 @@ export default defineComponent({
<style lang="scss" scoped>
@import '@/styles/mixins/breakpoint';
// The default display: contents doens't play nicely with drag and drop
.v-menu {
display: block;
}
.full,
.fill {
grid-column: 1 / span 2;
@@ -373,4 +441,33 @@ export default defineComponent({
margin-left: 8px;
}
}
.spacer {
flex-grow: 1;
}
.group {
position: relative;
left: -8px;
width: calc(100% + 16px);
padding: 8px;
background-color: var(--background-subdued);
border-radius: var(--border-radius);
.header {
margin-bottom: 8px;
}
.drag-handle {
margin-right: 4px;
}
.group-options {
cursor: pointer;
}
.v-notice {
cursor: pointer;
}
}
</style>

View File

@@ -137,8 +137,8 @@ export default defineComponent({
{
type: 'translations',
icon: 'translate',
text: i18n.t('translations')
}
text: i18n.t('translations'),
},
]);
return {
@@ -169,9 +169,6 @@ export default defineComponent({
.fields-management {
margin-bottom: 24px;
padding: 12px;
background-color: var(--background-subdued);
border-radius: var(--border-radius);
}
.field-grid {

View File

@@ -2,7 +2,7 @@
<private-view :title="collectionInfo && collectionInfo.name">
<template #headline>{{ $t('settings_data_model') }}</template>
<template #title-outer:prepend>
<v-button class="header-icon" rounded icon exact to="/settings/data-model">
<v-button class="header-icon" rounded icon exact @click="$router.go(-1)">
<v-icon name="arrow_back" />
</v-button>
</template>

View File

@@ -0,0 +1,48 @@
import { useFieldsStore, useRelationsStore } from '@/stores';
import { Relation } from '@/types';
export function getLocalTypeForField(
collection: string,
field: string
): 'standard' | 'file' | 'files' | 'o2m' | 'm2m' | 'm2o' | 'presentation' | 'translations' {
const fieldsStore = useFieldsStore();
const relationsStore = useRelationsStore();
const fieldInfo = fieldsStore.getField(collection, field);
const relations = relationsStore.getRelationsForField(collection, field);
if (relations.length === 0) {
if (fieldInfo.type === 'alias') return 'presentation';
return 'standard';
}
if (relations.length === 1) {
const relation = relations[0];
if (relation.one_collection === 'directus_files') return 'file';
if (relation.many_collection === collection) return 'm2o';
return 'o2m';
}
if (relations.length === 2) {
if ((fieldInfo.meta?.special || []).includes('translations')) {
return 'translations';
}
const relationForCurrent = relations.find(
(relation: Relation) =>
(relation.many_collection === collection && relation.many_field === field) ||
(relation.one_collection === collection && relation.one_field === field)
);
if (relationForCurrent?.many_collection === collection && relationForCurrent?.many_field === field)
return 'm2o';
if (relations[0].one_collection === 'directus_files' || relations[1].one_collection === 'directus_files') {
return 'files';
} else {
return 'm2m';
}
}
return 'standard';
}