Merge pull request #296 from directus/relational-setup

Relational setup tweaks
This commit is contained in:
Rijk van Zanten
2020-09-14 14:52:49 -04:00
committed by GitHub
9 changed files with 359 additions and 62 deletions

View File

@@ -73,8 +73,10 @@ export default class CollectionsService {
}
});
const collectionInfo = omit(payload, 'fields');
await collectionItemsService.create(collectionInfo);
await collectionItemsService.create({
...(payload.meta || {}),
collection: payload.collection,
});
const fieldPayloads = payload
.fields!.filter((field) => field.meta)

View File

@@ -31,7 +31,7 @@ export async function login(credentials: LoginCredentials) {
await hydrate();
}
let refreshTimeout: number;
let refreshTimeout: any;
export async function refresh({ navigate }: LogoutOptions = { navigate: true }) {
const appStore = useAppStore();

View File

@@ -538,6 +538,10 @@
"inline_title": "Inline Title",
"auto_format_casing": "Auto-format casing",
"auto_fill": "Auto Fill",
"corresponding_field": "Corresponding Field",
"errors": {
"COLLECTION_NOT_FOUND": "Collection doesn't exist.",
"FIELD_NOT_FOUND": "Field not found.",

View File

@@ -8,11 +8,11 @@
</div>
<div class="field">
<div class="type-label">{{ $t('junction_collection') }}</div>
<v-input :class="{ matches: junctionCollectionExists }" v-model="junctionCollection" :placeholder="$t('collection') + '...'" :disabled="isExisting" db-safe>
<v-input :class="{ matches: junctionCollectionExists }" v-model="junctionCollection" :placeholder="$t('collection') + '...'" :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')" />
<v-icon name="list_alt" @click="toggle" v-tooltip="$t('select_existing')" :disabled="autoFill || isExisting" />
</template>
<v-list dense class="monospace">
@@ -20,6 +20,7 @@
v-for="item in collectionItems"
:key="item.value"
:active="relations[0].many_collection === item.value"
:disabled="item.disabled"
@click="relations[0].many_collection = item.value"
>
<v-list-item-content>
@@ -33,11 +34,11 @@
</div>
<div class="field">
<div class="type-label">{{ $t('related_collection') }}</div>
<v-input :class="{ matches: relatedCollectionExists }" v-model="relations[1].one_collection" :placeholder="$t('collection') + '...'" :disabled="type === 'files' || isExisting" db-safe>
<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')" />
<v-icon name="list_alt" @click="toggle" v-tooltip="$t('select_existing')" :disabled="type === 'files' || isExisting" />
</template>
<v-list dense class="monospace">
@@ -45,6 +46,7 @@
v-for="item in collectionItems"
:key="item.value"
:active="relations[1].one_collection === item.value"
:disabled="item.disabled"
@click="relations[1].one_collection = item.value"
>
<v-list-item-content>
@@ -57,11 +59,11 @@
</v-input>
</div>
<v-input disabled :value="relations[0].one_primary" />
<v-input v-model="relations[0].many_field" :placeholder="$t('foreign_key') + '...'" :disabled="isExisting" db-safe>
<v-input :class="{ matches: junctionFieldExists(relations[0].many_field)}" v-model="relations[0].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')" />
<v-icon name="list_alt" @click="toggle" v-tooltip="$t('select_existing')" :disabled="autoFill || isExisting" />
</template>
<v-list dense class="monospace">
@@ -69,6 +71,7 @@
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>
@@ -81,11 +84,11 @@
</v-input>
<div class="spacer" />
<div class="spacer" />
<v-input v-model="relations[1].many_field" :placeholder="$t('foreign_key') + '...'" :disabled="isExisting" db-safe>
<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')" />
<v-icon name="list_alt" @click="toggle" v-tooltip="$t('select_existing')" :disabled="autoFill || isExisting" />
</template>
<v-list dense class="monospace">
@@ -93,6 +96,7 @@
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>
@@ -104,17 +108,34 @@
</template>
</v-input>
<v-input db-safe :disabled="relatedCollectionExists" v-model="relations[1].one_primary" :placeholder="$t('primary_key') + '...'" />
<div class="spacer" />
<v-checkbox block v-model="autoFill" :label="$t('auto_fill')" />
<v-icon class="arrow" name="arrow_forward" />
<v-icon class="arrow" name="arrow_backward" />
</div>
<v-divider large :inline-title="false" v-if="!isExisting">{{ $t('corresponding_field') }}</v-divider>
<div class="corresponding" v-if="!isExisting">
<div class="field">
<div class="type-label">{{ $t('create_field') }}</div>
<v-checkbox block :label="correspondingLabel" v-model="hasCorresponding" />
</div>
<div class="field">
<div class="type-label">{{ $t('field_name') }}</div>
<v-input :disabled="hasCorresponding === false" v-model="correspondingField" :placeholder="$t('field_name') + '...'" db-safe />
</div>
<v-icon name="arrow_forward" class="arrow" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
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';
@@ -137,13 +158,19 @@ export default defineComponent({
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const autoFill = computed({
get() {
return state.autoFillJunctionRelation;
},
set(newAuto: boolean) {
state.autoFillJunctionRelation = newAuto;
}
})
const availableCollections = computed(() => {
return orderBy(
collectionsStore.state.collections.filter((collection) => {
return (
collection.collection.startsWith('directus_') === false &&
collection.collection !== props.collection
);
return (collection.collection.startsWith('directus_') === false);
}),
['collection'],
['asc']
@@ -188,7 +215,72 @@ export default defineComponent({
}));
});
return { relations: state.relations, collectionItems, junctionCollection, junctionFields, junctionCollectionExists, relatedCollectionExists };
const { hasCorresponding, correspondingField, correspondingLabel } = useCorresponding();
return { relations: state.relations, autoFill, collectionItems, junctionCollection, junctionFields, junctionCollectionExists, relatedCollectionExists, junctionFieldExists, hasCorresponding, correspondingField, correspondingLabel };
function junctionFieldExists(fieldKey: string) {
if (!junctionCollection.value) return false;
return !!fieldsStore.getField(junctionCollection.value, fieldKey);
}
function useCorresponding() {
const hasCorresponding = computed({
get() {
return !!state.newFields.find((field: any) => field.$type === 'corresponding');
},
set(enabled: boolean) {
if (enabled === true) {
state.newFields = [
{
$type: 'corresponding',
field: state.relations[0].one_collection,
collection: state.relations[1].one_collection,
meta: {
special: 'm2m',
interface: 'many-to-many',
},
},
];
state.relations[1].one_field = state.relations[0].one_collection;
} else {
state.newFields = state.newFields.filter((field: any) => field.$type !== 'corresponding');
state.relations[1].one_field = null;
}
},
});
const correspondingField = computed({
get() {
return state.newFields?.find((field: any) => field.$type === 'corresponding')?.field || null;
},
set(field: string | null) {
state.newFields = state.newFields.map((newField: any) => {
if (newField.$type === 'corresponding') {
return {
...newField,
field: field
}
}
return newField;
})
state.relations[1].one_field = field;
},
});
const correspondingLabel = computed(() => {
if (state.relations[0].one_collection) {
return i18n.t('add_m2m_to_collection', { collection: state.relations[1].one_collection });
}
return i18n.t('add_field_related');
});
return { hasCorresponding, correspondingField, correspondingLabel };
}
},
});
</script>
@@ -216,13 +308,13 @@ export default defineComponent({
pointer-events: none;
&:first-of-type {
bottom: 78px;
left: 32.7%;
bottom: 141px;
left: 32.5%;
}
&:last-of-type {
bottom: 14px;
left: 67.5%;
bottom: 76px;
left: 67.4%;
}
}
}
@@ -230,4 +322,29 @@ export default defineComponent({
.type-label {
margin-bottom: 8px;
}
.v-divider {
margin: 48px 0;
}
.v-list {
--v-list-item-content-font-family: var(--family-monospace);
}
.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

@@ -8,11 +8,11 @@
</div>
<div class="field">
<div class="type-label">{{ $t('related_collection') }}</div>
<v-input :class="{ matches: isNewCollection === false }" db-safe key="related-collection" v-model="relations[0].one_collection" :disabled="isExisting" :placeholder="$t('collection') + '...'">
<v-input :class="{ matches: relatedCollectionExists }" db-safe key="related-collection" v-model="relations[0].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')" />
<v-icon name="list_alt" @click="toggle" v-tooltip="$t('select_existing')" :disabled="isExisting" />
</template>
<v-list dense class="monospace">
@@ -20,6 +20,7 @@
v-for="item in items"
:key="item.value"
:active="relations[0].one_collection === item.value"
:disabled="item.disabled"
@click="relations[0].one_collection = item.value"
>
<v-list-item-content>
@@ -32,19 +33,19 @@
</v-input>
</div>
<v-input disabled :value="relations[0].many_field" />
<v-input db-safe :disabled="isNewCollection === false" v-model="relations[0].one_primary" :placeholder="$t('primary_key') + '...'" />
<v-input db-safe :disabled="relatedCollectionExists" v-model="relations[0].one_primary" :placeholder="$t('primary_key') + '...'" />
<v-icon class="arrow" name="arrow_back" />
</div>
<v-divider v-if="!isExisting" />
<v-divider large :inline-title="false" v-if="!isExisting">{{ $t('corresponding_field') }}</v-divider>
<div class="grid" v-if="!isExisting">
<div class="field">
<div class="type-label">{{ $t('create_corresponding_field') }}</div>
<div class="type-label">{{ $t('create_field') }}</div>
<v-checkbox block :label="correspondingLabel" v-model="hasCorresponding" />
</div>
<div class="field">
<div class="type-label">{{ $t('corresponding_field_name') }}</div>
<div class="type-label">{{ $t('field_name') }}</div>
<v-input :disabled="hasCorresponding === false" v-model="correspondingField" :placeholder="$t('field_name') + '...'" db-safe />
</div>
<v-icon name="arrow_forward" class="arrow" />
@@ -85,8 +86,8 @@ export default defineComponent({
const { items } = useRelation();
const { hasCorresponding, correspondingField, correspondingLabel } = useCorresponding();
const isNewCollection = computed(() => {
return collectionsStore.getCollection(state.relations[0].one_collection) === null;
const relatedCollectionExists = computed(() => {
return !!collectionsStore.getCollection(state.relations[0].one_collection);
});
return {
@@ -96,7 +97,7 @@ export default defineComponent({
correspondingField,
correspondingLabel,
fieldData: state.fieldData,
isNewCollection,
relatedCollectionExists,
};
function useRelation() {
@@ -127,40 +128,38 @@ export default defineComponent({
},
set(enabled: boolean) {
if (enabled === true) {
state.newFields = [
{
field: state.relations[0].one_collection,
collection: state.relations[0].one_collection,
meta: {
special: 'o2m',
interface: 'one-to-many',
},
state.newFields.push({
$type: 'corresponding',
field: state.relations[0].one_collection,
collection: state.relations[0].one_collection,
meta: {
special: 'o2m',
interface: 'one-to-many',
},
];
});
} else {
state.newFields = [];
state.newFields = state.newFields.filter((field: any) => field.$type !== 'corresponding');
}
},
});
const correspondingField = computed({
get() {
return state.newFields?.[0]?.field || null;
return state.newFields?.find((field: any) => field.$type === 'corresponding')?.field || null;
},
set(field: string | null) {
state.newFields = [
{
...(state.newFields[0] || {}),
field: field || '',
},
];
state.newFields = state.newFields.map((newField: any) => {
if (newField.$type === 'corresponding') {
return {
...newField,
field
}
}
state.relations = [
{
...state.relations[0],
one_field: field,
},
];
return newField;
});
state.relations[0].one_field = field;
},
});

View File

@@ -13,12 +13,12 @@
:placeholder="$t('collection') + '...'"
v-model="relations[0].many_collection"
:disabled="isExisting"
:class="{ matches: isExisting }"
:class="{ matches: relatedCollectionExists }"
>
<template #append>
<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="isExisting" />
</template>
<v-list dense class="monospace">
@@ -26,6 +26,7 @@
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>
@@ -43,6 +44,7 @@
v-model="relations[0].many_field"
:disabled="isExisting"
:placeholder="$t('foreign_key') + '...'"
:class="{ matches: relatedFieldExists }"
>
<template #append v-if="fields && fields.length > 0">
<v-menu show-arrow placement="bottom-end">
@@ -68,6 +70,20 @@
</v-input>
<v-icon class="arrow" name="arrow_forward" />
</div>
<v-divider large :inline-title="false" v-if="!isExisting">{{ $t('corresponding_field') }}</v-divider>
<div class="corresponding" v-if="!isExisting">
<div class="field">
<div class="type-label">{{ $t('create_field') }}</div>
<v-checkbox block :label="correspondingLabel" v-model="hasCorresponding" />
</div>
<div class="field">
<div class="type-label">{{ $t('field_name') }}</div>
<v-input disabled v-model="relations[0].many_field" :placeholder="$t('field_name') + '...'" db-safe />
</div>
<v-icon name="arrow_forward" class="arrow" />
</div>
</div>
</template>
@@ -101,8 +117,18 @@ export default defineComponent({
const fieldsStore = useFieldsStore();
const { items, fields, currentCollectionPrimaryKey, collectionMany } = useRelation();
const { hasCorresponding, correspondingLabel } = useCorresponding();
return { relations: state.relations, items, fields, currentCollectionPrimaryKey, collectionMany };
const relatedCollectionExists = computed(() => {
return collectionsStore.state.collections.find((col) => col.collection === state.relations?.[0].many_collection);
});
const relatedFieldExists = 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, hasCorresponding, correspondingLabel, relatedCollectionExists, relatedFieldExists };
function useRelation() {
const availableCollections = computed(() => {
@@ -156,6 +182,74 @@ export default defineComponent({
return { availableCollections, items, fields, currentCollectionPrimaryKey, collectionMany };
}
function useCorresponding() {
const hasCorresponding = computed({
get() {
if (!state?.relations?.[0]?.many_collection || !state?.relations?.[0]?.many_field) return false;
if (relatedFieldExists.value === true) {
return state.updateFields.find((updateField: any) => updateField.field === state.relations[0].many_field)?.meta?.interface === 'many-to-one' || fieldsStore.getField(state.relations[0].many_collection, state.relations[0].many_field)?.meta?.interface === 'many-to-one';
} else {
return state.newFields.find((newField: any) => newField.$type === 'manyRelated')?.meta?.interface === 'many-to-one';
}
},
set(enabled: boolean) {
if (!state?.relations?.[0]?.many_field) return;
if (relatedFieldExists.value === true) {
if (enabled === true) {
state.updateFields = [
{
collection: state.relations[0].one_collection,
field: state.relations[0].many_field,
meta: {
interface: 'many-to-one',
special: 'm2o',
}
}
]
} else {
state.updateFields = [
{
collection: state.relations[0].one_collection,
field: state.relations[0].many_field,
meta: {
interface: null,
special: null,
}
}
]
}
} else {
state.newFields = state.newFields.map((newField: any) => {
if (newField.$type === 'manyRelated') {
if (!newField.meta) newField.meta = {};
if (enabled === true) {
newField.meta.interface = 'many-to-one';
newField.meta.special = 'many-to-one';
} else {
newField.meta.interface = null;
newField.meta.special = null;
}
}
return newField;
})
}
},
});
const correspondingLabel = computed(() => {
if (state.relations[0].many_collection) {
return i18n.t('add_m2o_to_collection', { collection: state.relations[0].many_collection });
}
return i18n.t('add_field_related');
});
return { hasCorresponding, correspondingLabel };
}
},
});
</script>
@@ -192,4 +286,25 @@ export default defineComponent({
.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

@@ -220,8 +220,6 @@ export default defineComponent({
async function saveField() {
saving.value = true;
console.log(state);
try {
if (props.field !== '+') {
await api.patch(`/fields/${props.collection}/${props.field}`, state.fieldData);
@@ -243,6 +241,13 @@ export default defineComponent({
})
);
await Promise.all(
state.updateFields.map((updateField: Partial<Field> & { $type: string }) => {
delete updateField.$type;
return api.post(`/fields/${updateField.collection}/${updateField.field}`, updateField);
})
);
await Promise.all(
state.relations.map((relation: Partial<Relation>) => {
if (relation.id) {
@@ -300,6 +305,10 @@ export default defineComponent({
}
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'

View File

@@ -6,7 +6,7 @@
*/
import { useFieldsStore, useRelationsStore, useCollectionsStore } from '@/stores/';
import { reactive, watch, computed, ComputedRef } from '@vue/composition-api';
import { reactive, watch, computed, ComputedRef, WatchStopHandle } from '@vue/composition-api';
import { clone, throttle } from 'lodash';
import { getInterfaces } from '@/interfaces';
import { getDisplays } from '@/displays';
@@ -55,6 +55,9 @@ function initLocalStore(
relations: [],
newCollections: [],
newFields: [],
updateFields: [],
autoFillJunctionRelation: true,
});
availableInterfaces = computed<InterfaceConfig[]>(() => {
@@ -245,6 +248,8 @@ function initLocalStore(
]
}
];
state.relations[0].many_primary = 'id';
}
if (collectionExists(collectionName)) {
@@ -253,6 +258,7 @@ function initLocalStore(
} else {
state.newFields = [
{
$type: 'manyRelated',
collection: collectionName,
field: fieldName,
type: fieldsStore.getPrimaryKeyFieldForCollection(collection)?.type,
@@ -263,6 +269,7 @@ function initLocalStore(
} else {
state.newFields = [
{
$type: 'manyRelated',
collection: collectionName,
field: fieldName,
type: 'integer',
@@ -270,6 +277,8 @@ function initLocalStore(
}
]
}
console.log(state.newFields);
}, 50);
if (!isExisting) {
@@ -324,6 +333,10 @@ function initLocalStore(
state.newCollections.push({
$type: 'junction',
collection: junctionCollection,
meta: {
hidden: true,
icon: 'import_export',
},
fields: [
{
field: 'id',
@@ -337,6 +350,9 @@ function initLocalStore(
}
]
});
state.relations[0].many_primary = 'id';
state.relations[1].many_primary = 'id';
}
if (fieldExists(junctionCollection, manyCurrent) === false) {
@@ -383,8 +399,6 @@ function initLocalStore(
]
})
}
console.log(state.newCollections, state.newFields);
}, 50);
if (!isExisting) {
@@ -414,6 +428,19 @@ function initLocalStore(
() => 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}`;
}
}
}
);
@@ -462,6 +489,30 @@ function initLocalStore(
],
syncNewCollectionsM2M
)
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 === 'presentation') {

View File

@@ -204,7 +204,7 @@ export default defineComponent({
type: 'success',
});
router.push('/settings/data-model');
router.push(`/settings/data-model/${collectionName.value}`);
} catch (error) {
console.log(error);
saveError.value = error;