mirror of
https://github.com/directus/directus.git
synced 2026-01-29 14:48:02 -05:00
Add m2o corresponding
This commit is contained in:
@@ -46,6 +46,7 @@
|
||||
v-for="item in _items"
|
||||
:key="item.value"
|
||||
:active="multiple ? (value || []).includes(item.value) : value === item.value"
|
||||
:disabled="item.disabled"
|
||||
@click="multiple ? null : $emit('input', item.value)"
|
||||
>
|
||||
<v-list-item-content>
|
||||
@@ -117,6 +118,7 @@ import { useCustomSelection, useCustomSelectionMultiple } from '@/composables/us
|
||||
type Item = {
|
||||
text: string;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type ItemsRaw = (string | any)[];
|
||||
@@ -207,6 +209,7 @@ export default defineComponent({
|
||||
return {
|
||||
text: item[props.itemText],
|
||||
value: item[props.itemValue],
|
||||
disabled: item.disabled,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ export default defineInterface(({ i18n }) => ({
|
||||
name: i18n.t('file'),
|
||||
icon: 'note_add',
|
||||
component: InterfaceFile,
|
||||
types: ['string'],
|
||||
types: ['uuid'],
|
||||
relationship: 'm2o',
|
||||
options: [],
|
||||
}));
|
||||
|
||||
@@ -7,5 +7,6 @@ export default defineInterface(({ i18n }) => ({
|
||||
icon: 'note_add',
|
||||
component: InterfaceFiles,
|
||||
types: ['alias'],
|
||||
relationship: 'm2m',
|
||||
options: [],
|
||||
}));
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
"configure_m2o": "Configure your Many-to-One Relationship...",
|
||||
"configure_m2m": "Configure your Many-to-Many Relationship...",
|
||||
|
||||
"choose_a_type": "Choose a Type...",
|
||||
"determined_by_relationship": "Determined by Relationship",
|
||||
|
||||
"include_seconds": "Include Seconds",
|
||||
|
||||
"add_note": "Add a helpful note for users...",
|
||||
|
||||
@@ -100,6 +100,8 @@ export default defineComponent({
|
||||
return fieldsStore.getFieldsForCollection(junctionCollection.value).map((field: Field) => ({
|
||||
text: field.field,
|
||||
value: field.field,
|
||||
disabled:
|
||||
_relations.value[0].field_many === field.field || _relations.value[1].field_many === field.field,
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -24,11 +24,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 :label="$t('add_field_related')" v-model="hasCorresponding" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('corresponding_field_name') }}</div>
|
||||
<v-input />
|
||||
<v-input :disabled="hasCorresponding === false" v-model="correspondingField" />
|
||||
</div>
|
||||
<v-icon name="arrow_forward" />
|
||||
</div>
|
||||
@@ -36,8 +36,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
import { defineComponent, PropType, computed, watch } from '@vue/composition-api';
|
||||
import { Relation } from '@/stores/relations/types';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
import { orderBy } from 'lodash';
|
||||
import useSync from '@/composables/use-sync';
|
||||
import useCollectionsStore from '@/stores/collections';
|
||||
@@ -53,6 +54,10 @@ export default defineComponent({
|
||||
type: Array as PropType<Relation[]>,
|
||||
required: true,
|
||||
},
|
||||
newFields: {
|
||||
type: Array as PropType<DeepPartial<Field>[]>,
|
||||
required: true,
|
||||
},
|
||||
fieldData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
@@ -64,36 +69,107 @@ export default defineComponent({
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const _relations = useSync(props, 'relations', emit);
|
||||
const _newFields = useSync(props, 'newFields', emit);
|
||||
|
||||
const collectionsStore = useCollectionsStore();
|
||||
const fieldsStore = useFieldsStore();
|
||||
|
||||
const availableCollections = computed(() => {
|
||||
return orderBy(
|
||||
collectionsStore.state.collections.filter((collection) => {
|
||||
return (
|
||||
collection.collection.startsWith('directus_') === false &&
|
||||
collection.collection !== props.collection
|
||||
);
|
||||
}),
|
||||
['collection'],
|
||||
['asc']
|
||||
const { items, relatedPrimary } = useRelation();
|
||||
const { hasCorresponding, correspondingField } = useCorresponding();
|
||||
|
||||
return { _relations, _newFields, items, relatedPrimary, hasCorresponding, correspondingField };
|
||||
|
||||
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 items = computed(() =>
|
||||
availableCollections.value.map((collection) => ({
|
||||
text: collection.name,
|
||||
value: collection.collection,
|
||||
}))
|
||||
);
|
||||
const relatedPrimary = computed(() => {
|
||||
return _relations.value[0].collection_one
|
||||
? fieldsStore.getPrimaryKeyFieldForCollection(_relations.value[0].collection_one)?.field
|
||||
: null;
|
||||
});
|
||||
|
||||
const relatedPrimary = computed(() => {
|
||||
return _relations.value[0].collection_one
|
||||
? fieldsStore.getPrimaryKeyFieldForCollection(_relations.value[0].collection_one)?.field
|
||||
: null;
|
||||
});
|
||||
return { items, relatedPrimary };
|
||||
}
|
||||
|
||||
return { _relations, items, relatedPrimary };
|
||||
function useCorresponding() {
|
||||
const hasCorresponding = computed({
|
||||
get() {
|
||||
return _newFields.value.length > 0;
|
||||
},
|
||||
set(enabled: boolean) {
|
||||
if (enabled === true) {
|
||||
_newFields.value = [
|
||||
{
|
||||
field: '',
|
||||
collection: _relations.value[0].collection_one,
|
||||
system: {
|
||||
special: 'o2m',
|
||||
interface: 'one-to-many',
|
||||
},
|
||||
},
|
||||
];
|
||||
} else {
|
||||
_newFields.value = [];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => _relations.value[0].collection_one,
|
||||
() => {
|
||||
if (hasCorresponding.value === true) {
|
||||
_newFields.value = [
|
||||
{
|
||||
...(_newFields.value[0] || {}),
|
||||
collection: _relations.value[0].collection_one,
|
||||
},
|
||||
];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const correspondingField = computed({
|
||||
get() {
|
||||
return _newFields.value?.[0]?.field || null;
|
||||
},
|
||||
set(field: string | null) {
|
||||
_newFields.value = [
|
||||
{
|
||||
...(_newFields.value[0] || {}),
|
||||
field: field || '',
|
||||
},
|
||||
];
|
||||
|
||||
_relations.value = [
|
||||
{
|
||||
..._relations.value[0],
|
||||
field_one: field,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
return { hasCorresponding, correspondingField };
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
:collection="collection"
|
||||
:field-data="fieldData"
|
||||
:relations.sync="_relations"
|
||||
:new-fields.sync="_newFields"
|
||||
:type="type"
|
||||
v-if="type === 'm2o' || type === 'file'"
|
||||
/>
|
||||
@@ -10,6 +11,7 @@
|
||||
:collection="collection"
|
||||
:field-data="fieldData"
|
||||
:relations.sync="_relations"
|
||||
:new-fields.sync="_newFields"
|
||||
:type="type"
|
||||
v-else-if="type === 'o2m'"
|
||||
/>
|
||||
@@ -17,6 +19,7 @@
|
||||
:collection="collection"
|
||||
:field-data="fieldData"
|
||||
:relations.sync="_relations"
|
||||
:new-fields.sync="_newFields"
|
||||
:type="type"
|
||||
v-else-if="type === 'm2m' || type === 'files'"
|
||||
/>
|
||||
@@ -25,6 +28,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from '@vue/composition-api';
|
||||
import { Relation } from '@/stores/relations/types';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
import useSync from '@/composables/use-sync';
|
||||
|
||||
import RelationshipM2o from './relationship-m2o.vue';
|
||||
@@ -46,6 +50,10 @@ export default defineComponent({
|
||||
type: Array as PropType<Relation[]>,
|
||||
required: true,
|
||||
},
|
||||
newFields: {
|
||||
type: Array as PropType<DeepPartial<Field>[]>,
|
||||
required: true,
|
||||
},
|
||||
fieldData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
@@ -57,8 +65,9 @@ export default defineComponent({
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const _relations = useSync(props, 'relations', emit);
|
||||
const _newFields = useSync(props, 'newFields', emit);
|
||||
|
||||
return { _relations };
|
||||
return { _relations, _newFields };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
:value="_field.database.type"
|
||||
@input="setType"
|
||||
:items="typesWithLabels"
|
||||
:placeholder="typePlaceholder"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -101,10 +102,18 @@ export default defineComponent({
|
||||
);
|
||||
|
||||
const typeDisabled = computed(() => {
|
||||
return ['file', 'files', 'o2m', 'm2m'].includes(props.type);
|
||||
return ['file', 'files', 'o2m', 'm2m', 'm2o'].includes(props.type);
|
||||
});
|
||||
|
||||
return { _field, typesWithLabels, setType, typeDisabled };
|
||||
const typePlaceholder = computed(() => {
|
||||
if (props.type === 'm2o') {
|
||||
return i18n.t('determined_by_relationship');
|
||||
}
|
||||
|
||||
return i18n.t('choose_a_type');
|
||||
});
|
||||
|
||||
return { _field, typesWithLabels, setType, typeDisabled, typePlaceholder };
|
||||
|
||||
function setType(value: typeof types[number]) {
|
||||
if (value === 'uuid') {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
v-if="currentTab[0] === 'relationship'"
|
||||
:field-data.sync="fieldData"
|
||||
:relations.sync="relations"
|
||||
:new-fields.sync="newFields"
|
||||
:type="type"
|
||||
/>
|
||||
|
||||
@@ -58,6 +59,7 @@ import { isEmpty } from 'lodash';
|
||||
import api from '@/api';
|
||||
import { Relation } from '@/stores/relations/types';
|
||||
import { useFieldsStore } from '@/stores/fields';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -92,11 +94,11 @@ export default defineComponent({
|
||||
const fieldsStore = useFieldsStore();
|
||||
|
||||
const { tabs, currentTab } = useTabs();
|
||||
const { fieldData, relations } = useData();
|
||||
const { fieldData, relations, newFields } = useData();
|
||||
|
||||
const saving = ref(false);
|
||||
|
||||
return { active, tabs, currentTab, fieldData, saveField, saving, relations };
|
||||
return { active, tabs, currentTab, fieldData, saveField, saving, relations, newFields };
|
||||
|
||||
function useTabs() {
|
||||
const tabs = computed(() => {
|
||||
@@ -136,7 +138,7 @@ export default defineComponent({
|
||||
function relationshipDisabled() {
|
||||
return (
|
||||
isEmpty(fieldData.field) ||
|
||||
(['o2m', 'm2m', 'files'].includes(props.type) === false && isEmpty(fieldData.database.type))
|
||||
(['o2m', 'm2m', 'files', 'm2o'].includes(props.type) === false && isEmpty(fieldData.database.type))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -151,8 +153,15 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
if (['m2m', 'files'].includes(props.type)) {
|
||||
/** @todo extend with all values */
|
||||
return relations.value.length !== 2;
|
||||
return (
|
||||
relations.value.length !== 2 ||
|
||||
isEmpty(relations.value[0].collection_many) ||
|
||||
isEmpty(relations.value[0].field_many) ||
|
||||
isEmpty(relations.value[0].field_one) ||
|
||||
isEmpty(relations.value[1].collection_many) ||
|
||||
isEmpty(relations.value[1].field_many) ||
|
||||
isEmpty(relations.value[1].collection_one)
|
||||
);
|
||||
}
|
||||
|
||||
return isEmpty(fieldData.field) || isEmpty(fieldData.database.type);
|
||||
@@ -183,6 +192,10 @@ export default defineComponent({
|
||||
|
||||
const relations = ref<Partial<Relation>[]>([]);
|
||||
|
||||
// Allow the panes to create additional fields outside of this one. This is used to
|
||||
// auto generated related o2m columns / junction collections etc
|
||||
const newFields = ref<DeepPartial<Field>[]>([]);
|
||||
|
||||
if (props.type === 'file') {
|
||||
fieldData.database.type = 'uuid';
|
||||
|
||||
@@ -221,6 +234,16 @@ export default defineComponent({
|
||||
relations.value[0].field_many = fieldData.field;
|
||||
}
|
||||
);
|
||||
|
||||
// Make sure to keep the current m2o field type in sync with the primary key of the
|
||||
// selected related collection
|
||||
watch(
|
||||
() => relations.value[0].collection_one,
|
||||
() => {
|
||||
const field = fieldsStore.getPrimaryKeyFieldForCollection(relations.value[0].collection_one);
|
||||
fieldData.database.type = field.database.type;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (props.type === 'm2m' || props.type === 'files') {
|
||||
@@ -280,7 +303,7 @@ export default defineComponent({
|
||||
);
|
||||
}
|
||||
|
||||
return { fieldData, relations };
|
||||
return { fieldData, relations, newFields };
|
||||
}
|
||||
|
||||
async function saveField() {
|
||||
@@ -288,6 +311,13 @@ export default defineComponent({
|
||||
|
||||
try {
|
||||
await api.post(`/fields/${props.collection}`, fieldData);
|
||||
|
||||
await Promise.all(
|
||||
newFields.value.map((newField) => {
|
||||
return api.post(`/fields/${newField.collection}`, newField);
|
||||
})
|
||||
);
|
||||
|
||||
await api.post(`/relations`, relations.value);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
Reference in New Issue
Block a user