Add translations field type

This commit is contained in:
rijkvanzanten
2020-09-15 11:18:30 -04:00
parent e1daf095f0
commit 4608f34192
9 changed files with 506 additions and 53 deletions

View File

@@ -6,6 +6,7 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('translations'),
icon: 'replay',
types: ['alias'],
relationship: 'translations',
component: InterfaceTranslations,
options: [
/** @todo add custom options component */

View File

@@ -10,7 +10,7 @@ export type InterfaceConfig = {
component: Component;
options: DeepPartial<Field>[] | Component;
types: typeof types[number][];
relationship?: null | 'm2o' | 'o2m' | 'm2m';
relationship?: null | 'm2o' | 'o2m' | 'm2m' | 'translations';
hideLabel?: boolean;
hideLoader?: boolean;
system?: boolean;

View File

@@ -153,6 +153,8 @@
"field_m2o": "M2O Relationship",
"field_o2m": "O2M Relationship",
"field_m2m": "M2M Relationship",
"field_translations": "Translations",
"languages": "Languages",
"reset_page_preferences": "Reset Page Preferences",
@@ -185,6 +187,8 @@
"configure_m2o": "Configure your Many-to-One Relationship...",
"configure_o2m": "Configure your One-to-Many Relationship...",
"configure_m2m": "Configure your Many-to-Many Relationship...",
"configure_translations": "Configure your Translations...",
"configure_languages": "Configure your Languages...",
"add_m2o_to_collection": "Add Many-to-One to \"{collection}\"",
"add_o2m_to_collection": "Add One-to-Many to \"{collection}\"",
@@ -297,6 +301,8 @@
"item_in": "Item {primaryKey} in {collection} | Items {primaryKey} in {collection}",
"this_collection": "This Collection",
"related_collection": "Related Collection",
"translations_collection": "Translations Collection",
"languages_collection": "Languages Collection",
"related_values": "Related Values",

View File

@@ -0,0 +1,145 @@
<template>
<div>
<h2 class="type-title">{{ $t('configure_languages') }}</h2>
<div class="grid">
<div class="field">
<div class="type-label">{{ $t('translations_collection') }}</div>
<v-input disabled :value="relations[1].many_collection" />
</div>
<div class="field">
<div class="type-label">{{ $t('languages_collection') }}</div>
<v-input :class="{ matches: languagesCollectionExists }" db-safe key="languages-collection" v-model="relations[1].one_collection" :disabled="isExisting" :placeholder="$t('collection') + '...'">
<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" />
</template>
<v-list dense class="monospace">
<v-list-item
v-for="item in items"
:key="item.value"
:active="relations[1].one_collection === item.value"
:disabled="item.disabled"
@click="relations[1].one_collection = item.value"
>
<v-list-item-content>
{{ item.text }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-input>
</div>
<v-input :value="relations[1].many_field" :placeholder="$t('foreign_key') + '...'"/>
<v-input db-safe :disabled="languagesCollectionExists" v-model="relations[1].one_primary" :placeholder="$t('primary_key') + '...'" />
<v-icon class="arrow" name="arrow_back" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, watch } from '@vue/composition-api';
import { Relation } from '@/types';
import { Field } from '@/types';
import { orderBy } from 'lodash';
import useSync from '@/composables/use-sync';
import { useCollectionsStore, useFieldsStore } from '@/stores';
import i18n from '@/lang';
import { state } from '../store';
export default defineComponent({
props: {
type: {
type: String,
required: true,
},
collection: {
type: String,
required: true,
},
isExisting: {
type: Boolean,
default: false,
},
},
setup(props, { emit }) {
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const { items } = useRelation();
const languagesCollectionExists = computed(() => {
return !!collectionsStore.getCollection(state.relations[1].one_collection);
});
return {
relations: state.relations,
items,
fieldData: state.fieldData,
languagesCollectionExists,
};
function useRelation() {
const availableCollections = computed(() => {
return orderBy(
collectionsStore.state.collections.filter((collection) => {
return collection.collection.startsWith('directus_') === false;
}),
['collection'],
['asc']
);
});
const items = computed(() =>
availableCollections.value.map((collection) => ({
text: collection.collection,
value: collection.collection,
}))
);
return { items };
}
},
});
</script>
<style lang="scss" scoped>
.grid {
--v-select-font-family: var(--family-monospace);
--v-input-font-family: var(--family-monospace);
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 32px;
margin-top: 48px;
.v-input.matches {
--v-input-color: var(--primary);
}
.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);
}
.v-divider {
margin: 48px 0;
}
.type-label {
margin-bottom: 8px;
}
</style>

View File

@@ -39,7 +39,6 @@
<v-input v-model="fieldData.meta.note" :placeholder="$t('add_note')" />
</div>
<!-- @todo base default value field type on selected type -->
<div class="field full" v-if="fieldData.schema">
<div class="label type-label">{{ $t('default_value') }}</div>
<v-input
@@ -228,7 +227,7 @@ export default defineComponent({
});
const typeDisabled = computed(() => {
return ['file', 'files', 'o2m', 'm2m', 'm2o'].includes(props.type);
return ['file', 'files', 'o2m', 'm2m', 'm2o', 'translations'].includes(props.type);
});
const typePlaceholder = computed(() => {

View File

@@ -0,0 +1,227 @@
<template>
<div>
<h2 class="type-title">{{ $t('configure_translations') }}</h2>
<div class="grid">
<div class="field">
<div class="type-label">{{ $t('this_collection') }}</div>
<v-input disabled :value="collection" />
</div>
<div class="field">
<div class="type-label">{{ $t('translations_collection') }}</div>
<v-input
db-safe
:placeholder="$t('collection') + '...'"
v-model="relations[0].many_collection"
:disabled="isExisting"
:class="{ matches: translationsCollectionExists }"
>
<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" />
</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-list-item-content>
{{ item.text }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-input>
</div>
<v-input disabled :value="currentCollectionPrimaryKey.field" />
<v-input
db-safe
v-model="relations[0].many_field"
:disabled="isExisting"
:placeholder="$t('foreign_key') + '...'"
:class="{ matches: translationsFieldExists }"
>
<template #append v-if="fields && fields.length > 0">
<v-menu show-arrow placement="bottom-end">
<template #activator="{ toggle }">
<v-icon name="list_alt" @click="toggle" v-tooltip="$t('select_existing')" />
</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-list-item-content>
{{ field.text }}
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-input>
<v-icon class="arrow" name="arrow_forward" />
</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 { orderBy } from 'lodash';
import i18n from '@/lang';
import { state } from '../store';
export default defineComponent({
props: {
type: {
type: String,
required: true,
},
collection: {
type: String,
required: true,
},
isExisting: {
type: Boolean,
default: false,
},
},
setup(props, { emit }) {
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 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);
});
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 currentCollectionPrimaryKey = computed(() =>
fieldsStore.getPrimaryKeyFieldForCollection(props.collection)
);
const fields = computed(() => {
if (!state.relations[0].many_collection) return [];
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 collectionMany = computed({
get() {
return state.relations[0].many_collection!;
},
set(collection: string) {
state.relations[0].many_collection = collection;
state.relations[0].many_field = '';
},
});
return { availableCollections, items, fields, currentCollectionPrimaryKey, collectionMany };
}
},
});
</script>
<style lang="scss" scoped>
.grid {
--v-select-font-family: var(--family-monospace);
--v-input-font-family: var(--family-monospace);
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 32px;
margin-top: 48px;
.v-input.matches {
--v-input-color: var(--primary);
}
.v-icon.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);
}
.type-label {
margin-bottom: 8px;
}
.v-divider {
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%);
}
}
</style>

View File

@@ -27,6 +27,20 @@
:type="localType"
/>
<setup-translations
v-if="currentTab[0] === 'translations'"
:is-existing="field !== '+'"
:collection="collection"
: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 !== '+'"
@@ -61,6 +75,8 @@ import SetupTabs from './components/tabs.vue';
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';
@@ -81,6 +97,8 @@ export default defineComponent({
SetupActions,
SetupSchema,
SetupRelationship,
SetupTranslations,
SetupLanguages,
SetupInterface,
SetupDisplay,
},
@@ -94,7 +112,7 @@ export default defineComponent({
required: true,
},
type: {
type: String as PropType<'standard' | 'file' | 'files' | 'm2o' | 'o2m' | 'm2m' | 'presentation'>,
type: String as PropType<'standard' | 'file' | 'files' | 'm2o' | 'o2m' | 'm2m' | 'presentation' | 'translations'>,
default: null,
},
},
@@ -116,7 +134,7 @@ export default defineComponent({
const localType = computed(() => {
if (props.field === '+') return props.type;
let type: 'standard' | 'file' | 'files' | 'o2m' | 'm2m' | 'm2o' | 'presentation' = 'standard';
let type: 'standard' | 'file' | 'files' | 'o2m' | 'm2m' | 'm2o' | 'presentation' | 'translations' = 'standard';
type = getLocalTypeForField(props.collection, props.field);
return type;
@@ -173,6 +191,21 @@ 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(),
}
])
}
return tabs;
});
@@ -181,10 +214,20 @@ export default defineComponent({
return { tabs, currentTab };
function relationshipDisabled() {
return (
isEmpty(state.fieldData.field) ||
(['o2m', 'm2m', 'files', 'm2o'].includes(localType.value) === false &&
isEmpty(state.fieldData.type))
return isEmpty(state.fieldData.field);
}
function translationsDisabled() {
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)
);
}
@@ -199,7 +242,7 @@ export default defineComponent({
);
}
if (['m2m', 'files'].includes(localType.value)) {
if (['m2m', 'files', 'translations'].includes(localType.value)) {
return (
state.relations.length !== 2 ||
isEmpty(state.relations[0].many_collection) ||
@@ -294,11 +337,14 @@ export default defineComponent({
function getLocalTypeForField(
collection: string,
field: string
): 'standard' | 'file' | 'files' | 'o2m' | 'm2m' | 'm2o' {
): 'standard' | 'file' | 'files' | 'o2m' | 'm2m' | 'm2o' | 'presentation' | 'translations' {
const fieldInfo = fieldsStore.getField(collection, field);
const relations = relationsStore.getRelationsForField(collection, field);
if (relations.length === 0) return 'standard';
if (relations.length === 0) {
if (fieldInfo.type === 'alias') return 'presentation';
return 'standard';
}
if (relations.length === 1) {
const relation = relations[0];

View File

@@ -27,7 +27,7 @@ export { state, availableInterfaces, availableDisplays, initLocalStore, clearLoc
function initLocalStore(
collection: string,
field: string,
type: 'standard' | 'file' | 'files' | 'm2o' | 'o2m' | 'm2m' | 'presentation'
type: 'standard' | 'file' | 'files' | 'm2o' | 'o2m' | 'm2m' | 'presentation' | 'translations'
) {
const interfaces = getInterfaces();
const displays = getDisplays();
@@ -75,6 +75,8 @@ function initLocalStore(
matchesRelation = inter.relationship === 'm2o';
} else if (type === 'files') {
matchesRelation = inter.relationship === 'm2m';
} else if (type === 'translations') {
matchesRelation = inter.relationship === 'translations';
} else {
matchesRelation = inter.relationship === type;
}
@@ -277,8 +279,6 @@ function initLocalStore(
}
]
}
console.log(state.newFields);
}, 50);
if (!isExisting) {
@@ -321,7 +321,7 @@ function initLocalStore(
)
}
if (type === 'm2m' || type === 'files') {
if (type === 'm2m' || type === 'files' || type === 'translations') {
delete state.fieldData.schema;
delete state.fieldData.type;
@@ -424,26 +424,6 @@ function initLocalStore(
];
}
watch(
() => state.fieldData.field,
() => {
state.relations[0].one_field = state.fieldData.field;
if (collectionExists(state.fieldData.field)) {
state.relations[0].many_collection = `${state.relations[0].one_collection}_${state.relations[1].one_collection}`;
state.relations[0].many_field = `${state.relations[0].one_collection}_${state.relations[0].one_primary}`;
state.relations[1].one_collection = state.fieldData.field;
state.relations[1].one_primary = fieldsStore.getPrimaryKeyFieldForCollection(collection)?.field;
state.relations[1].many_collection = `${state.relations[0].one_collection}_${state.relations[1].one_collection}`;
state.relations[1].many_field = `${state.relations[1].one_collection}_${state.relations[1].one_primary}`;
if (state.relations[0].many_field === state.relations[1].many_field) {
state.relations[1].many_field = `${state.relations[1].one_collection}_related_${state.relations[1].one_primary}`;
}
}
}
);
watch(
() => state.relations[0].many_collection,
() => {
@@ -490,34 +470,75 @@ function initLocalStore(
syncNewCollectionsM2M
)
let stop: WatchStopHandle;
watch(
() => state.fieldData.field,
() => {
state.relations[0].one_field = state.fieldData.field;
watch(() => state.autoFillJunctionRelation, (startWatching) => {
if (startWatching) {
stop = watch([() => state.relations[1].one_collection, () => state.relations[1].one_primary], ([newRelatedCollection, newRelatedPrimary]: string[]) => {
if (newRelatedCollection) {
state.relations[0].many_collection = `${state.relations[0].one_collection}_${state.relations[1].one_collection}`;
state.relations[1].many_collection = `${state.relations[0].one_collection}_${state.relations[1].one_collection}`;
state.relations[0].many_field = `${state.relations[0].one_collection}_${state.relations[0].one_primary}`;
}
if (newRelatedPrimary) {
state.relations[1].many_field = `${state.relations[1].one_collection}_${state.relations[1].one_primary}`;
}
if (collectionExists(state.fieldData.field) && type !== 'translations') {
state.relations[0].many_collection = `${state.relations[0].one_collection}_${state.relations[1].one_collection}`;
state.relations[0].many_field = `${state.relations[0].one_collection}_${state.relations[0].one_primary}`;
state.relations[1].one_collection = state.fieldData.field;
state.relations[1].one_primary = fieldsStore.getPrimaryKeyFieldForCollection(collection)?.field;
state.relations[1].many_collection = `${state.relations[0].one_collection}_${state.relations[1].one_collection}`;
state.relations[1].many_field = `${state.relations[1].one_collection}_${state.relations[1].one_primary}`;
if (state.relations[0].many_field === state.relations[1].many_field) {
state.relations[1].many_field = `${state.relations[1].one_collection}_related_${state.relations[1].one_primary}`;
}
});
} else {
stop?.();
}
}
}, { immediate: true });
);
if (type !== 'translations') {
let stop: WatchStopHandle;
watch(() => state.autoFillJunctionRelation, (startWatching) => {
if (startWatching) {
stop = watch([() => state.relations[1].one_collection, () => state.relations[1].one_primary], ([newRelatedCollection, newRelatedPrimary]: string[]) => {
if (newRelatedCollection) {
state.relations[0].many_collection = `${state.relations[0].one_collection}_${state.relations[1].one_collection}`;
state.relations[1].many_collection = `${state.relations[0].one_collection}_${state.relations[1].one_collection}`;
state.relations[0].many_field = `${state.relations[0].one_collection}_${state.relations[0].one_primary}`;
}
if (newRelatedPrimary) {
state.relations[1].many_field = `${state.relations[1].one_collection}_${state.relations[1].one_primary}`;
}
if (state.relations[0].many_field === state.relations[1].many_field) {
state.relations[1].many_field = `${state.relations[1].one_collection}_related_${state.relations[1].one_primary}`;
}
});
} else {
stop?.();
}
}, { immediate: true });
}
if (type === 'translations') {
watch(() => state.relations[0].many_collection, (newManyCollection: string) => {
state.relations[1].many_collection = newManyCollection;
}, { immediate: true });
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].many_field = `${state.relations[1].one_collection}_${state.relations[1].one_primary}`;
}
}
if (type === 'presentation') {
delete state.fieldData.schema;
delete state.fieldData.type;
state.fieldData.meta.special = 'alias';
}

View File

@@ -131,6 +131,14 @@ export default defineComponent({
icon: 'import_export',
text: i18n.t('m2m_relationship'),
},
{
divider: true,
},
{
type: 'translations',
icon: 'translate',
text: i18n.t('translations')
}
]);
return {