mirror of
https://github.com/directus/directus.git
synced 2026-02-03 21:55:11 -05:00
support junction fields
This commit is contained in:
@@ -45,7 +45,7 @@
|
||||
:collection="relationFields.junctionCollection"
|
||||
:primary-key="currentlyEditing || '+'"
|
||||
:edits="editsAtStart"
|
||||
:related-primary-key="relationFields.relationPkField"
|
||||
:related-primary-key="relationFields.relationPkField || '+'"
|
||||
:junction-field="relationFields.junctionRelation"
|
||||
@input="stageEdits"
|
||||
@update:active="cancelEdit"
|
||||
@@ -165,10 +165,8 @@ export default defineComponent({
|
||||
|
||||
const { cancelEdit, stageEdits, editsAtStart, editItem, currentlyEditing } = useEdit(
|
||||
value,
|
||||
items,
|
||||
relationFields,
|
||||
emitter,
|
||||
getJunctionFromRelatedId
|
||||
emitter
|
||||
);
|
||||
|
||||
const { stageSelection, selectModalActive, selectionFilters } = useSelection(
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="one-to-many" v-else>
|
||||
<v-table
|
||||
:loading="loading"
|
||||
:items="displayItems"
|
||||
:items="items"
|
||||
:headers.sync="tableHeaders"
|
||||
show-resize
|
||||
inline
|
||||
@@ -15,13 +15,13 @@
|
||||
<template v-for="header in tableHeaders" v-slot:[`item.${header.value}`]="{ item }">
|
||||
<render-display
|
||||
:key="header.value"
|
||||
:value="item[header.value]"
|
||||
:value="get(item, header.value)"
|
||||
:display="header.field.display"
|
||||
:options="header.field.displayOptions"
|
||||
:interface="header.field.interface"
|
||||
:interface-options="header.field.interfaceOptions"
|
||||
:type="header.field.type"
|
||||
:collection="junctionCollection.collection"
|
||||
:collection="relationFields.junctionCollection"
|
||||
:field="header.field.field"
|
||||
/>
|
||||
</template>
|
||||
@@ -46,9 +46,9 @@
|
||||
<modal-detail
|
||||
v-if="!disabled"
|
||||
:active="currentlyEditing !== null"
|
||||
:collection="relationCollection.collection"
|
||||
:collection="relationFields.junctionCollection"
|
||||
:primary-key="currentlyEditing || '+'"
|
||||
:related-primary-key="relationFields.relationPkField"
|
||||
:related-primary-key="relatedPrimaryKey || '+'"
|
||||
:junction-field="relationFields.junctionRelation"
|
||||
:edits="editsAtStart"
|
||||
@input="stageEdits"
|
||||
@@ -71,6 +71,7 @@
|
||||
import { defineComponent, ref, computed, watch, PropType, toRefs } from '@vue/composition-api';
|
||||
import ModalDetail from '@/views/private/components/modal-detail';
|
||||
import ModalBrowse from '@/views/private/components/modal-browse';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import useActions from './use-actions';
|
||||
import useRelation from './use-relation';
|
||||
@@ -128,7 +129,7 @@ export default defineComponent({
|
||||
getJunctionFromRelatedId,
|
||||
} = useActions(value, relationFields, emitter);
|
||||
|
||||
const { tableHeaders, items, loading, error, displayItems } = usePreview(
|
||||
const { tableHeaders, items, loading, error } = usePreview(
|
||||
value,
|
||||
fields,
|
||||
relationFields,
|
||||
@@ -138,17 +139,15 @@ export default defineComponent({
|
||||
getPrimaryKeys
|
||||
);
|
||||
|
||||
const { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit } = useEdit(
|
||||
const { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit, relatedPrimaryKey } = useEdit(
|
||||
value,
|
||||
items,
|
||||
relationFields,
|
||||
emitter,
|
||||
getJunctionFromRelatedId
|
||||
emitter
|
||||
);
|
||||
|
||||
const { stageSelection, selectModalActive, selectionFilters } = useSelection(
|
||||
value,
|
||||
displayItems,
|
||||
items,
|
||||
relationFields,
|
||||
emitter
|
||||
);
|
||||
@@ -168,10 +167,11 @@ export default defineComponent({
|
||||
stageSelection,
|
||||
selectModalActive,
|
||||
deleteItem,
|
||||
displayItems,
|
||||
selectionFilters,
|
||||
items,
|
||||
relationFields,
|
||||
relatedPrimaryKey,
|
||||
get,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<v-notice type="warning" v-if="relatedCollection === null">
|
||||
<v-notice type="warning" v-if="junctionCollection === null">
|
||||
{{ $t('interfaces.one-to-many.no_collection') }}
|
||||
</v-notice>
|
||||
<div v-else class="form-grid">
|
||||
<div class="field full">
|
||||
<p class="type-label">{{ $t('select_fields') }}</p>
|
||||
<v-field-select
|
||||
:collection="relatedCollection"
|
||||
:collection="junctionCollection"
|
||||
v-model="fields"
|
||||
:inject="relatedCollectionExists ? null : { fields: newFields, collections: newCollections, relations }"
|
||||
:inject="
|
||||
junctionCollectionExists ? null : { fields: newFields, collections: newCollections, relations }
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -20,7 +22,6 @@ import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
import { useRelationsStore } from '@/stores/';
|
||||
import { Relation, Collection } from '@/types';
|
||||
import { useCollectionsStore } from '../../stores';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
collection: {
|
||||
@@ -51,7 +52,6 @@ export default defineComponent({
|
||||
setup(props, { emit }) {
|
||||
const collectionsStore = useCollectionsStore();
|
||||
const relationsStore = useRelationsStore();
|
||||
|
||||
const fields = computed({
|
||||
get() {
|
||||
return props.value?.fields;
|
||||
@@ -63,41 +63,26 @@ export default defineComponent({
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const relatedCollection = computed(() => {
|
||||
const junctionCollection = computed(() => {
|
||||
if (!props.fieldData || !props.relations || props.relations.length === 0) return null;
|
||||
|
||||
const { field } = props.fieldData;
|
||||
|
||||
const junctionRelation = props.relations.find(
|
||||
(relation) => relation.one_collection === props.collection && relation.one_field === field
|
||||
);
|
||||
|
||||
if (junctionRelation === undefined) return;
|
||||
|
||||
const relatedCollection = props.relations.find(
|
||||
(relation) =>
|
||||
relation.one_collection !== props.collection &&
|
||||
relation.many_field === junctionRelation.junction_field
|
||||
);
|
||||
|
||||
return relatedCollection?.one_collection || null;
|
||||
return junctionRelation?.many_collection || null;
|
||||
});
|
||||
|
||||
const relatedCollectionExists = computed(() => {
|
||||
const junctionCollectionExists = computed(() => {
|
||||
return !!collectionsStore.state.collections.find(
|
||||
(collection) => collection.collection === relatedCollection.value
|
||||
(collection) => collection.collection === junctionCollection.value
|
||||
);
|
||||
});
|
||||
|
||||
return { fields, relatedCollection, relatedCollectionExists };
|
||||
return { fields, junctionCollection, junctionCollectionExists };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/mixins/form-grid';
|
||||
|
||||
.form-grid {
|
||||
@include form-grid;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Ref } from '@vue/composition-api';
|
||||
import { RelationInfo } from './use-relation';
|
||||
import { get, has } from 'lodash';
|
||||
|
||||
export default function useActions(
|
||||
value: Ref<(string | number | Record<string, any>)[] | null>,
|
||||
@@ -12,8 +13,7 @@ export default function useActions(
|
||||
|
||||
return (
|
||||
value.value.find(
|
||||
(item) =>
|
||||
(typeof item === 'object' && junctionPkField in item && item[junctionPkField] === id) || item === id
|
||||
(item) => get(item, junctionPkField) === id || (['string', 'number'].includes(typeof item), item === id)
|
||||
) || null
|
||||
);
|
||||
}
|
||||
@@ -34,11 +34,7 @@ export default function useActions(
|
||||
if (value.value === null || junctionRelation === null) return [];
|
||||
|
||||
return value.value.filter(
|
||||
(item) =>
|
||||
typeof item === 'object' &&
|
||||
junctionRelation in item &&
|
||||
typeof item[junctionRelation] === 'object' &&
|
||||
relationPkField in item[junctionRelation] === false
|
||||
(item) => has(item, junctionRelation) && has(item, [junctionRelation, relationPkField]) === false
|
||||
) as Record<string, any>[];
|
||||
}
|
||||
|
||||
@@ -47,19 +43,13 @@ export default function useActions(
|
||||
|
||||
if (value.value === null || junctionRelation === null) return [];
|
||||
|
||||
return value.value.filter(
|
||||
(item) =>
|
||||
typeof item === 'object' &&
|
||||
junctionRelation in item &&
|
||||
typeof item[junctionRelation] === 'object' &&
|
||||
relationPkField in item[junctionRelation] === true
|
||||
) as Record<string, any>[];
|
||||
return value.value.filter((item) => has(item, [junctionRelation, relationPkField])) as Record<string, any>[];
|
||||
}
|
||||
|
||||
function getExistingItems() {
|
||||
if (value.value === null) return [];
|
||||
|
||||
return value.value.filter((item) => typeof item === 'string' || typeof item === 'number');
|
||||
return value.value.filter((item) => ['string', 'number'].includes(typeof item));
|
||||
}
|
||||
|
||||
function getPrimaryKeys(): (string | number)[] {
|
||||
|
||||
@@ -1,67 +1,58 @@
|
||||
import { Ref, ref } from '@vue/composition-api';
|
||||
import { RelationInfo } from './use-relation';
|
||||
import { isEqual } from 'lodash';
|
||||
import { isEqual, get } from 'lodash';
|
||||
|
||||
export default function useEdit(
|
||||
value: Ref<(string | number | Record<string, any>)[] | null>,
|
||||
items: Ref<Record<string, any>[]>,
|
||||
relation: Ref<RelationInfo>,
|
||||
emit: (newVal: any[] | null) => void,
|
||||
getJunctionFromRelatedId: (id: string | number, items: Record<string, any>[]) => Record<string, any> | null
|
||||
emit: (newVal: any[] | null) => void
|
||||
) {
|
||||
// Primary key of the item we're currently editing. If null, the edit modal should be
|
||||
// closed
|
||||
const currentlyEditing = ref<string | number | null>(null);
|
||||
const relatedPrimaryKey = ref<string | number | null>(null);
|
||||
|
||||
// This keeps track of the starting values so we can match with it
|
||||
const editsAtStart = ref<Record<string, any>>({});
|
||||
|
||||
function editItem(item: any) {
|
||||
const { relationPkField } = relation.value;
|
||||
const hasPrimaryKey = relationPkField in item;
|
||||
const { relationPkField, junctionRelation, junctionPkField } = relation.value;
|
||||
|
||||
editsAtStart.value = item;
|
||||
currentlyEditing.value = hasPrimaryKey ? item[relationPkField] : -1;
|
||||
relatedPrimaryKey.value = get(item, [junctionRelation, relationPkField], null);
|
||||
currentlyEditing.value = get(item, [junctionPkField], null);
|
||||
}
|
||||
|
||||
function stageEdits(edits: any) {
|
||||
const { relationPkField, junctionRelation, junctionPkField } = relation.value;
|
||||
const editsWrapped = { [junctionRelation]: edits };
|
||||
const hasPrimaryKey = relationPkField in editsAtStart.value;
|
||||
const junctionItem = hasPrimaryKey
|
||||
? getJunctionFromRelatedId(editsAtStart.value[relationPkField], items.value)
|
||||
: null;
|
||||
|
||||
const newValue = (value.value || []).map((item) => {
|
||||
if (junctionItem !== null && junctionPkField in junctionItem) {
|
||||
const id = junctionItem[junctionPkField];
|
||||
if (currentlyEditing.value !== null) {
|
||||
const id = currentlyEditing.value;
|
||||
|
||||
if (typeof item === 'object' && junctionPkField in item) {
|
||||
if (item[junctionPkField] === id) return { [junctionRelation]: edits, [junctionPkField]: id };
|
||||
} else if (typeof item === 'number' || typeof item === 'string') {
|
||||
if (item === id) return { [junctionRelation]: edits, [junctionPkField]: id };
|
||||
if (item[junctionPkField] === id) return edits;
|
||||
} else if (['number', 'string'].includes(typeof item)) {
|
||||
if (item === id) return edits;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof item === 'object' && relationPkField in edits && junctionRelation in item) {
|
||||
const id = edits[relationPkField];
|
||||
const relatedItem = item[junctionRelation] as string | number | Record<string, any>;
|
||||
if (typeof relatedItem === 'object' && relationPkField in relatedItem) {
|
||||
if (relatedItem[relationPkField] === id) return editsWrapped;
|
||||
} else if (typeof relatedItem === 'string' || typeof relatedItem === 'number') {
|
||||
if (relatedItem === id) return editsWrapped;
|
||||
}
|
||||
if (relatedPrimaryKey.value != null) {
|
||||
const id = relatedPrimaryKey.value;
|
||||
|
||||
if (get(item, [junctionRelation], null) === id) return edits;
|
||||
if (get(item, [junctionRelation, relationPkField], null) === id) return edits;
|
||||
}
|
||||
|
||||
if (isEqual({ [junctionRelation]: editsAtStart.value }, item)) {
|
||||
return editsWrapped;
|
||||
if (isEqual(editsAtStart.value, item)) {
|
||||
return edits;
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
if (hasPrimaryKey === false && newValue.includes(editsWrapped) === false) {
|
||||
newValue.push(editsWrapped);
|
||||
if (relatedPrimaryKey.value === null && currentlyEditing.value === null && newValue.includes(edits) === false) {
|
||||
newValue.push(edits);
|
||||
}
|
||||
|
||||
if (newValue.length === 0) emit(null);
|
||||
@@ -71,7 +62,8 @@ export default function useEdit(
|
||||
function cancelEdit() {
|
||||
editsAtStart.value = {};
|
||||
currentlyEditing.value = null;
|
||||
relatedPrimaryKey.value = null;
|
||||
}
|
||||
|
||||
return { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit };
|
||||
return { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit, relatedPrimaryKey };
|
||||
}
|
||||
|
||||
@@ -24,6 +24,21 @@ export default function usePreview(
|
||||
const items = ref<Record<string, any>[]>([]);
|
||||
const error = ref(null);
|
||||
|
||||
function getRelatedFields(fields: string[]) {
|
||||
const { junctionRelation } = relation.value;
|
||||
|
||||
return fields
|
||||
.map((field) => {
|
||||
const sections = field.split('.');
|
||||
if (junctionRelation === sections[0] && sections.length === 2) return sections[1];
|
||||
})
|
||||
.filter((i) => i);
|
||||
}
|
||||
|
||||
function getJunctionFields() {
|
||||
return (fields.value || []).filter((field) => field.includes('.') === false);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => value.value,
|
||||
async (newVal) => {
|
||||
@@ -40,7 +55,7 @@ export default function usePreview(
|
||||
const junctionItems = await loadRelatedIds();
|
||||
const relatedPrimaryKeys = junctionItems.map((junction) => junction[junctionRelation]);
|
||||
|
||||
const filteredFields = [...(fields.value.length > 0 ? fields.value : getDefaultFields())];
|
||||
const filteredFields = [...(fields.value.length > 0 ? getRelatedFields(fields.value) : getDefaultFields())];
|
||||
|
||||
if (filteredFields.includes(relationPkField) === false) filteredFields.push(relationPkField);
|
||||
|
||||
@@ -103,12 +118,18 @@ export default function usePreview(
|
||||
const primaryKeys = getPrimaryKeys();
|
||||
|
||||
if (primaryKeys.length > 0) {
|
||||
const filteredFields = getJunctionFields();
|
||||
|
||||
if (filteredFields.includes(junctionPkField) === false) filteredFields.push(junctionPkField);
|
||||
if (filteredFields.includes(junctionRelation) === false) filteredFields.push(junctionRelation);
|
||||
|
||||
const endpoint = relation.value.junctionCollection.startsWith('directus_')
|
||||
? `/${relation.value.junctionCollection.substring(9)}`
|
||||
: `/items/${relation.value.junctionCollection}`;
|
||||
|
||||
const response = await api.get(endpoint, {
|
||||
params: {
|
||||
fields: filteredFields,
|
||||
[`filter[${junctionPkField}][_in]`]: getPrimaryKeys().join(','),
|
||||
},
|
||||
});
|
||||
@@ -127,19 +148,19 @@ export default function usePreview(
|
||||
return [];
|
||||
}
|
||||
|
||||
const displayItems = computed(() => {
|
||||
const { junctionRelation } = relation.value;
|
||||
return items.value.map((item) => item[junctionRelation]);
|
||||
});
|
||||
|
||||
// Seeing we don't care about saving those tableHeaders, we can reset it whenever the
|
||||
// fields prop changes (most likely when we're navigating to a different o2m context)
|
||||
watch(
|
||||
() => fields.value,
|
||||
() => {
|
||||
tableHeaders.value = (fields.value.length > 0 ? fields.value : getDefaultFields())
|
||||
const { junctionRelation, junctionCollection } = relation.value;
|
||||
|
||||
tableHeaders.value = (fields.value.length > 0
|
||||
? fields.value
|
||||
: getDefaultFields().map((field) => `${junctionRelation}.${field}`)
|
||||
)
|
||||
.map((fieldKey) => {
|
||||
const field = fieldsStore.getField(relation.value.relationCollection, fieldKey);
|
||||
let field = fieldsStore.getField(junctionCollection, fieldKey);
|
||||
|
||||
if (!field) return null;
|
||||
|
||||
@@ -171,5 +192,5 @@ export default function usePreview(
|
||||
return fields.slice(0, 3).map((field: Field) => field.field);
|
||||
}
|
||||
|
||||
return { tableHeaders, displayItems, items, loading, error };
|
||||
return { tableHeaders, items, loading, error };
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { Ref, ref, computed } from '@vue/composition-api';
|
||||
import { RelationInfo } from './use-relation';
|
||||
import { get } from 'lodash';
|
||||
import { Filter } from '@/types';
|
||||
|
||||
export default function useSelection(
|
||||
value: Ref<(string | number | Record<string, any>)[] | null>,
|
||||
displayItems: Ref<Record<string, any>[]>,
|
||||
items: Ref<Record<string, any>[]>,
|
||||
relation: Ref<RelationInfo>,
|
||||
emit: (newVal: any[] | null) => void
|
||||
) {
|
||||
const selectModalActive = ref(false);
|
||||
|
||||
const selectedPrimaryKeys = computed(() => {
|
||||
if (displayItems.value === null) return [];
|
||||
if (items.value === null) return [];
|
||||
|
||||
const { relationPkField } = relation.value;
|
||||
const { relationPkField, junctionRelation } = relation.value;
|
||||
|
||||
const selectedKeys: (number | string)[] = displayItems.value
|
||||
.filter((currentItem) => relationPkField in currentItem)
|
||||
.map((currentItem) => currentItem[relationPkField]);
|
||||
const selectedKeys: (number | string)[] = items.value
|
||||
.map((currentItem) => get(currentItem, [junctionRelation, relationPkField]))
|
||||
.filter((i) => i);
|
||||
|
||||
return selectedKeys;
|
||||
});
|
||||
@@ -39,11 +40,7 @@ export default function useSelection(
|
||||
});
|
||||
|
||||
function stageSelection(newSelection: (number | string)[]) {
|
||||
const { junctionRelation } = relation.value;
|
||||
|
||||
const selection = newSelection
|
||||
.filter((item) => selectedPrimaryKeys.value.includes(item) === false)
|
||||
.map((item) => ({ [junctionRelation]: item }));
|
||||
const selection = newSelection.filter((item) => selectedPrimaryKeys.value.includes(item) === false);
|
||||
|
||||
const newVal = [...selection, ...(value.value || [])];
|
||||
if (newVal.length === 0) emit(null);
|
||||
|
||||
Reference in New Issue
Block a user