mirror of
https://github.com/directus/directus.git
synced 2026-04-03 03:00:39 -04:00
Relational setup (#652)
* Add m2o relational setup * Fix type check in localType * wip o2m relational * Finish relational setup on creation * Account for m2m type
This commit is contained in:
@@ -106,6 +106,9 @@
|
||||
"this_collection": "This Collection",
|
||||
"related_collection": "Related Collection",
|
||||
|
||||
"choose_a_collection": "Choose a Collection...",
|
||||
"choose_a_field": "Choose a Field...",
|
||||
|
||||
"submit": "Submit",
|
||||
|
||||
"system": "System",
|
||||
|
||||
@@ -6,17 +6,25 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('junction_collection') }}</div>
|
||||
<v-select :items="collectionItems" />
|
||||
<v-select :items="collectionItems" v-model="junctionCollection" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('related_collection') }}</div>
|
||||
<v-select :items="collectionItems" />
|
||||
<v-select :items="collectionItems" v-model="relatedCollection" />
|
||||
</div>
|
||||
<v-input disabled :value="field.field" />
|
||||
<v-select :items="collectionItems" allow-other />
|
||||
<v-select
|
||||
:disabled="!junctionCollection"
|
||||
:items="junctionFields"
|
||||
v-model="junctionFieldCurrent"
|
||||
/>
|
||||
<div class="spacer" />
|
||||
<div class="spacer" />
|
||||
<v-select :items="collectionItems" allow-other />
|
||||
<v-select
|
||||
:disabled="!junctionCollection"
|
||||
:items="junctionFields"
|
||||
v-model="junctionFieldRelated"
|
||||
/>
|
||||
<v-input disabled value="id" />
|
||||
<v-icon name="arrow_forward" />
|
||||
<v-icon name="arrow_backward" />
|
||||
@@ -28,6 +36,9 @@ import { defineComponent, computed, PropType } from '@vue/composition-api';
|
||||
import useCollectionsStore from '@/stores/collections';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
import useFieldsStore from '@/stores/fields/';
|
||||
import { Relation } from '@/stores/relations/types';
|
||||
import useSync from '@/composables/use-sync';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -39,9 +50,23 @@ export default defineComponent({
|
||||
type: Object as PropType<Field>,
|
||||
required: true,
|
||||
},
|
||||
isNew: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
newRelations: {
|
||||
type: Array as PropType<Partial<Relation>[]>,
|
||||
required: true,
|
||||
},
|
||||
existingRelations: {
|
||||
type: Array as PropType<Relation[]>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
setup(props, { emit }) {
|
||||
const _newRelations = useSync(props, 'newRelations', emit);
|
||||
const collectionsStore = useCollectionsStore();
|
||||
const fieldsStore = useFieldsStore();
|
||||
|
||||
const availableCollections = computed(() => {
|
||||
return orderBy(
|
||||
@@ -63,7 +88,106 @@ export default defineComponent({
|
||||
}))
|
||||
);
|
||||
|
||||
return { availableCollections, collectionItems };
|
||||
const defaultNewRelations = computed<Partial<Relation>[]>(() => [
|
||||
{
|
||||
collection_many: undefined,
|
||||
field_many: undefined,
|
||||
collection_one: props.field.collection,
|
||||
field_one: props.field.field,
|
||||
junction_field: undefined,
|
||||
},
|
||||
{
|
||||
collection_many: undefined,
|
||||
field_many: undefined,
|
||||
collection_one: undefined,
|
||||
field_one: undefined,
|
||||
junction_field: undefined,
|
||||
},
|
||||
]);
|
||||
|
||||
const junctionCollection = computed({
|
||||
get() {
|
||||
return props.newRelations[0]?.collection_many || null;
|
||||
},
|
||||
set(newJunctionCollection: string | null) {
|
||||
let relations: readonly Partial<Relation>[];
|
||||
|
||||
if (_newRelations.value.length === 0) relations = [...defaultNewRelations.value];
|
||||
else relations = [..._newRelations.value];
|
||||
|
||||
relations[0].collection_many = newJunctionCollection || undefined;
|
||||
relations[1].collection_many = newJunctionCollection || undefined;
|
||||
_newRelations.value = relations;
|
||||
},
|
||||
});
|
||||
|
||||
const junctionFields = computed(() => {
|
||||
if (!junctionCollection.value) return [];
|
||||
|
||||
return fieldsStore
|
||||
.getFieldsForCollection(junctionCollection.value)
|
||||
.map((field: Field) => ({
|
||||
text: field.name,
|
||||
value: field.field,
|
||||
}));
|
||||
});
|
||||
|
||||
const relatedCollection = computed({
|
||||
get() {
|
||||
return props.newRelations[1]?.collection_one || null;
|
||||
},
|
||||
set(newRelatedCollection: string | null) {
|
||||
let relations: readonly Partial<Relation>[];
|
||||
|
||||
if (_newRelations.value.length === 0) relations = [...defaultNewRelations.value];
|
||||
else relations = [..._newRelations.value];
|
||||
|
||||
relations[1].collection_one = newRelatedCollection || undefined;
|
||||
_newRelations.value = relations;
|
||||
},
|
||||
});
|
||||
|
||||
const junctionFieldCurrent = computed({
|
||||
get() {
|
||||
return props.newRelations[0]?.field_many || null;
|
||||
},
|
||||
set(newJunctionField: string | null) {
|
||||
let relations: readonly Partial<Relation>[];
|
||||
|
||||
if (_newRelations.value.length === 0) relations = [...defaultNewRelations.value];
|
||||
else relations = [..._newRelations.value];
|
||||
|
||||
relations[0].field_many = newJunctionField || undefined;
|
||||
relations[1].junction_field = newJunctionField || undefined;
|
||||
_newRelations.value = relations;
|
||||
},
|
||||
});
|
||||
|
||||
const junctionFieldRelated = computed({
|
||||
get() {
|
||||
return props.newRelations[1]?.field_many || null;
|
||||
},
|
||||
set(newJunctionField: string | null) {
|
||||
let relations: readonly Partial<Relation>[];
|
||||
|
||||
if (_newRelations.value.length === 0) relations = [...defaultNewRelations.value];
|
||||
else relations = [..._newRelations.value];
|
||||
|
||||
relations[1].field_many = newJunctionField || undefined;
|
||||
relations[0].junction_field = newJunctionField || undefined;
|
||||
_newRelations.value = relations;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
availableCollections,
|
||||
collectionItems,
|
||||
junctionCollection,
|
||||
junctionFields,
|
||||
relatedCollection,
|
||||
junctionFieldCurrent,
|
||||
junctionFieldRelated,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,13 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('related_collection') }}</div>
|
||||
<v-select :items="items" />
|
||||
<v-select
|
||||
v-if="isNew"
|
||||
:placeholder="$t('choose_a_collection')"
|
||||
:items="items"
|
||||
v-model="collectionOne"
|
||||
/>
|
||||
<v-input disabled v-else :value="existingRelation.collection_many" />
|
||||
</div>
|
||||
<v-input disabled :value="field.field" />
|
||||
<v-input disabled value="id" />
|
||||
@@ -19,11 +25,11 @@
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('create_corresponding_field') }}</div>
|
||||
<v-checkbox block :label="$t('add_field_related')" />
|
||||
<v-checkbox block :disabled="isNew === false" :label="$t('add_field_related')" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('corresponding_field_name') }}</div>
|
||||
<v-input />
|
||||
<v-input :disabled="isNew === false" />
|
||||
</div>
|
||||
<v-icon name="arrow_forward" />
|
||||
</div>
|
||||
@@ -35,6 +41,8 @@ import { defineComponent, computed, PropType } from '@vue/composition-api';
|
||||
import useCollectionsStore from '@/stores/collections';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
import { Relation } from '@/stores/relations/types';
|
||||
import useSync from '@/composables/use-sync';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -46,8 +54,21 @@ export default defineComponent({
|
||||
type: Object as PropType<Field>,
|
||||
required: true,
|
||||
},
|
||||
existingRelations: {
|
||||
type: Array as PropType<Relation[]>,
|
||||
required: true,
|
||||
},
|
||||
newRelations: {
|
||||
type: Array as PropType<Partial<Relation>[]>,
|
||||
required: true,
|
||||
},
|
||||
isNew: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
setup(props, { emit }) {
|
||||
const _newRelations = useSync(props, 'newRelations', emit);
|
||||
const collectionsStore = useCollectionsStore();
|
||||
|
||||
const availableCollections = computed(() => {
|
||||
@@ -70,7 +91,31 @@ export default defineComponent({
|
||||
}))
|
||||
);
|
||||
|
||||
return { availableCollections, items };
|
||||
const existingRelation = computed(() => {
|
||||
return props.existingRelations.find((relation) => {
|
||||
return (
|
||||
relation.field_many === props.field.field &&
|
||||
relation.collection_many === props.field.collection
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const collectionOne = computed({
|
||||
get() {
|
||||
return _newRelations.value[0]?.collection_one || null;
|
||||
},
|
||||
set(newCollectionOne: string | null) {
|
||||
_newRelations.value = [
|
||||
{
|
||||
field_many: props.field.field,
|
||||
collection_many: props.field.collection,
|
||||
collection_one: newCollectionOne || undefined,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
return { availableCollections, items, existingRelation, collectionOne };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -6,10 +6,23 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('related_collection') }}</div>
|
||||
<v-select :items="collectionItems" />
|
||||
<v-select
|
||||
v-if="isNew"
|
||||
:placeholder="$t('choose_a_collection')"
|
||||
:items="collectionItems"
|
||||
v-model="collectionMany"
|
||||
/>
|
||||
<v-input disabled v-else :value="existingRelation.collection_many" />
|
||||
</div>
|
||||
<v-input disabled :value="field.field" />
|
||||
<v-select :items="collectionItems" allow-other />
|
||||
<v-select
|
||||
v-if="isNew"
|
||||
:disabled="!collectionMany"
|
||||
:items="collectionFields"
|
||||
v-model="fieldMany"
|
||||
:placeholder="!collectionMany ? $t('choose_a_collection') : $t('choose_a_field')"
|
||||
/>
|
||||
<v-input disabled v-else :value="existingRelation.field_many" />
|
||||
<v-icon name="arrow_forward" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -19,6 +32,9 @@ import { defineComponent, computed, PropType } from '@vue/composition-api';
|
||||
import useCollectionsStore from '@/stores/collections';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
import { Relation } from '@/stores/relations/types';
|
||||
import useSync from '@/composables/use-sync';
|
||||
import useFieldsStore from '@/stores/fields';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -30,9 +46,23 @@ export default defineComponent({
|
||||
type: Object as PropType<Field>,
|
||||
required: true,
|
||||
},
|
||||
existingRelations: {
|
||||
type: Array as PropType<Relation[]>,
|
||||
required: true,
|
||||
},
|
||||
newRelations: {
|
||||
type: Array as PropType<Partial<Relation>[]>,
|
||||
required: true,
|
||||
},
|
||||
isNew: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
setup(props, { emit }) {
|
||||
const collectionsStore = useCollectionsStore();
|
||||
const fieldsStore = useFieldsStore();
|
||||
const _newRelations = useSync(props, 'newRelations', emit);
|
||||
|
||||
const availableCollections = computed(() => {
|
||||
return orderBy(
|
||||
@@ -54,7 +84,64 @@ export default defineComponent({
|
||||
}))
|
||||
);
|
||||
|
||||
return { availableCollections, collectionItems };
|
||||
const existingRelation = computed(() => {
|
||||
return props.existingRelations.find((relation) => {
|
||||
return (
|
||||
relation.field_one === props.field.field &&
|
||||
relation.collection_one === props.field.collection
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const defaultNewRelation = computed(() => ({
|
||||
collection_one: props.field.collection,
|
||||
field_one: props.field.field,
|
||||
}));
|
||||
|
||||
const collectionMany = computed({
|
||||
get() {
|
||||
return props.newRelations[0]?.collection_many || null;
|
||||
},
|
||||
set(newCollectionOne: string | null) {
|
||||
_newRelations.value = [
|
||||
{
|
||||
...(props.newRelations[0] || defaultNewRelation.value),
|
||||
collection_many: newCollectionOne || undefined,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
const fieldMany = computed({
|
||||
get() {
|
||||
return props.newRelations[0]?.field_many || null;
|
||||
},
|
||||
set(newCollectionOne: string | null) {
|
||||
_newRelations.value = [
|
||||
{
|
||||
...(props.newRelations[0] || defaultNewRelation),
|
||||
field_many: newCollectionOne || undefined,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
const collectionFields = computed(() => {
|
||||
if (!collectionMany.value) return [];
|
||||
return fieldsStore.getFieldsForCollection(collectionMany.value).map((field: Field) => ({
|
||||
text: field.name,
|
||||
value: field.field,
|
||||
}));
|
||||
});
|
||||
|
||||
return {
|
||||
availableCollections,
|
||||
collectionItems,
|
||||
collectionMany,
|
||||
fieldMany,
|
||||
existingRelation,
|
||||
collectionFields,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,37 +1,54 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="type-title" v-if="isNew">{{ $t('relationship_setup_title') }}</h2>
|
||||
<v-fancy-select :items="items" :value="value.type" @input="emitValue('type', $event)" />
|
||||
<v-fancy-select
|
||||
:items="items"
|
||||
:value="value.type && value.type.toLowerCase()"
|
||||
@input="emitValue('type', $event)"
|
||||
:disabled="isNew === false"
|
||||
/>
|
||||
|
||||
<many-to-one
|
||||
v-if="value.type === 'm2o'"
|
||||
v-if="value.type && value.type.toLowerCase() === 'm2o'"
|
||||
:collection="value.collection"
|
||||
:field="value"
|
||||
@update:field="emit('input', $event)"
|
||||
:existing-relations="existingRelations"
|
||||
:new-relations.sync="_newRelations"
|
||||
:is-new="isNew"
|
||||
/>
|
||||
<one-to-many
|
||||
v-else-if="value.type === 'o2m'"
|
||||
v-else-if="value.type && value.type.toLowerCase() === 'o2m'"
|
||||
:collection="value.collection"
|
||||
:field="value"
|
||||
@update:field="emit('input', $event)"
|
||||
:existing-relations="existingRelations"
|
||||
:new-relations.sync="_newRelations"
|
||||
:is-new="isNew"
|
||||
/>
|
||||
<many-to-many
|
||||
v-else-if="value.type === 'm2m'"
|
||||
v-else-if="value.type && value.type.toLowerCase() === 'm2m'"
|
||||
:collection="value.collection"
|
||||
:field="value"
|
||||
@update:field="emit('input', $event)"
|
||||
:existing-relations="existingRelations"
|
||||
:new-relations.sync="_newRelations"
|
||||
:is-new="isNew"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
import { defineComponent, PropType, computed, watch } from '@vue/composition-api';
|
||||
import { FancySelectItem } from '@/components/v-fancy-select/types';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
import i18n from '@/lang';
|
||||
import ManyToOne from './field-setup-relationship-m2o.vue';
|
||||
import OneToMany from './field-setup-relationship-o2m.vue';
|
||||
import ManyToMany from './field-setup-relationship-m2m.vue';
|
||||
import useRelationsStore from '@/stores/relations';
|
||||
import { Relation } from '@/stores/relations/types';
|
||||
import useSync from '@/composables/use-sync';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -48,8 +65,22 @@ export default defineComponent({
|
||||
type: Object as PropType<Field>,
|
||||
required: true,
|
||||
},
|
||||
newRelations: {
|
||||
type: Array as PropType<Partial<Relation>[]>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const relationsStore = useRelationsStore();
|
||||
const _newRelations = useSync(props, 'newRelations', emit);
|
||||
|
||||
watch(
|
||||
() => props.value.type,
|
||||
() => {
|
||||
_newRelations.value = [];
|
||||
}
|
||||
);
|
||||
|
||||
const items = computed<FancySelectItem[]>(() => {
|
||||
return [
|
||||
{
|
||||
@@ -70,7 +101,12 @@ export default defineComponent({
|
||||
];
|
||||
});
|
||||
|
||||
return { emitValue, items };
|
||||
const existingRelations = computed(() => {
|
||||
if (props.isNew) return [];
|
||||
return relationsStore.getRelationsForField(props.value.collection, props.value.field);
|
||||
});
|
||||
|
||||
return { emitValue, items, existingRelations, _newRelations };
|
||||
|
||||
function emitValue(key: string, value: any) {
|
||||
emit('input', {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
v-model="field"
|
||||
:local-type.sync="localType"
|
||||
:is-new="existingField === null"
|
||||
:new-relations.sync="newRelations"
|
||||
/>
|
||||
<field-setup-interface
|
||||
v-if="currentTab[0] === 'interface'"
|
||||
@@ -70,6 +71,9 @@ import FieldSetupAdvanced from './field-setup-advanced.vue';
|
||||
import SetupTabs from './setup-tabs.vue';
|
||||
import SetupActions from './setup-actions.vue';
|
||||
import useFieldsStore from '@/stores/fields/';
|
||||
import { Relation } from '@/stores/relations/types';
|
||||
import api from '@/api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
|
||||
import { LocalType } from './types';
|
||||
|
||||
@@ -103,12 +107,15 @@ export default defineComponent({
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const fieldsStore = useFieldsStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const { field, localType } = usefield();
|
||||
const { tabs, currentTab } = useTabs();
|
||||
const { save, saving } = useSave();
|
||||
|
||||
return { field, tabs, currentTab, localType, save, saving };
|
||||
const newRelations = ref<Partial<Relation>[]>([]);
|
||||
|
||||
return { field, tabs, currentTab, localType, save, saving, newRelations };
|
||||
|
||||
function usefield() {
|
||||
const defaults = {
|
||||
@@ -143,17 +150,29 @@ export default defineComponent({
|
||||
const field = ref<any>({ ...defaults });
|
||||
const localType = ref<LocalType>(null);
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
() => {
|
||||
if (!props.existingField) {
|
||||
field.value = { ...defaults };
|
||||
localType.value = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.existingField,
|
||||
(existingField: Field) => {
|
||||
if (existingField) {
|
||||
field.value = existingField;
|
||||
|
||||
if (existingField.type === 'file') {
|
||||
const type = existingField.type.toLowerCase();
|
||||
|
||||
if (type === 'file') {
|
||||
localType.value = 'file';
|
||||
} else if (existingField.type === 'files') {
|
||||
} else if (type === 'files') {
|
||||
localType.value = 'files';
|
||||
} else if (['o2m', 'm2o'].includes(existingField.type)) {
|
||||
} else if (['o2m', 'm2o', 'm2m'].includes(type)) {
|
||||
localType.value = 'relational';
|
||||
} else {
|
||||
localType.value = 'standard';
|
||||
@@ -170,6 +189,14 @@ export default defineComponent({
|
||||
|
||||
function useTabs() {
|
||||
const currentTab = ref(['field']);
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
() => {
|
||||
currentTab.value = ['field'];
|
||||
}
|
||||
);
|
||||
|
||||
const tabs = computed(() => {
|
||||
const tabs = [
|
||||
{
|
||||
@@ -215,6 +242,10 @@ export default defineComponent({
|
||||
try {
|
||||
if (field.value.id === null) {
|
||||
await fieldsStore.createField(props.collection, field.value);
|
||||
|
||||
for (const relation of newRelations.value) {
|
||||
await createRelation(relation);
|
||||
}
|
||||
} else {
|
||||
if (field.value.hasOwnProperty('name')) {
|
||||
delete field.value.name;
|
||||
@@ -233,6 +264,11 @@ export default defineComponent({
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createRelation(relation: Partial<Relation>) {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
await api.post(`/${currentProjectKey}/relations`, relation);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
dashed
|
||||
outlined
|
||||
large
|
||||
@click="openFieldSetup"
|
||||
@click="openFieldSetup()"
|
||||
>
|
||||
<v-icon name="add" />
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
dashed
|
||||
outlined
|
||||
large
|
||||
@click="openFieldSetup"
|
||||
@click="openFieldSetup()"
|
||||
>
|
||||
<v-icon name="add" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user