mirror of
https://github.com/directus/directus.git
synced 2026-01-29 12:27:55 -05:00
@@ -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) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
48
app/src/modules/settings/routes/data-model/get-local-type.ts
Normal file
48
app/src/modules/settings/routes/data-model/get-local-type.ts
Normal 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';
|
||||
}
|
||||
Reference in New Issue
Block a user