Merge pull request #660 from directus/relational-updates

Re-add support for junction fields
This commit is contained in:
Rijk van Zanten
2020-10-15 18:51:33 -04:00
committed by GitHub
9 changed files with 268 additions and 296 deletions

View File

@@ -78,10 +78,12 @@ function registerHooks(hooks: string[]) {
function registerHook(hook: string) {
const hookPath = path.resolve(extensionsPath, 'hooks', hook, 'index.js');
const hookInstance: HookRegisterFunction | { default?: HookRegisterFunction } = require(hookPath);
const hookInstance:
| HookRegisterFunction
| { default?: HookRegisterFunction } = require(hookPath);
let register: HookRegisterFunction = hookInstance as HookRegisterFunction;
if (typeof hookInstance !== "function") {
if (typeof hookInstance !== 'function') {
if (hookInstance.default) {
register = hookInstance.default;
}
@@ -108,10 +110,12 @@ function registerEndpoints(endpoints: string[], router: Router) {
function registerEndpoint(endpoint: string) {
const endpointPath = path.resolve(extensionsPath, 'endpoints', endpoint, 'index.js');
const endpointInstance: EndpointRegisterFunction | { default?: EndpointRegisterFunction } = require(endpointPath);
const endpointInstance:
| EndpointRegisterFunction
| { default?: EndpointRegisterFunction } = require(endpointPath);
let register: EndpointRegisterFunction = endpointInstance as EndpointRegisterFunction;
if (typeof endpointInstance !== "function") {
if (typeof endpointInstance !== 'function') {
if (endpointInstance.default) {
register = endpointInstance.default;
}

View File

@@ -5,30 +5,25 @@
<div v-else class="files">
<v-table
inline
:items="displayItems"
:items="items"
:loading="loading"
:headers.sync="tableHeaders"
:item-key="relationFields.junctionPkField"
:item-key="relationInfo.junctionPkField"
:disabled="disabled"
@click:row="editItem"
>
<template #item.$thumbnail="{ item }">
<render-display
:value="item"
:value="get(item, relationInfo.junctionField)"
display="file"
:collection="relationFields.junctionCollection"
:field="relationFields.relationPkField"
:collection="relationInfo.junctionCollection"
:field="relationInfo.relationPkField"
type="file"
/>
</template>
<template #item-append="{ item }" v-if="!disabled">
<v-icon
name="close"
v-tooltip="$t('deselect')"
class="deselect"
@click.stop="deleteItem(item, items)"
/>
<v-icon name="close" v-tooltip="$t('deselect')" class="deselect" @click.stop="deleteItem(item)" />
</template>
</v-table>
@@ -41,12 +36,12 @@
<modal-item
v-if="!disabled"
:active="currentlyEditing !== null"
:collection="relationFields.junctionCollection"
:active="editModalActive"
:collection="relationInfo.junctionCollection"
:primary-key="currentlyEditing || '+'"
:edits="editsAtStart"
:related-primary-key="relationFields.relationPkField"
:junction-field="relationFields.junctionRelation"
:related-primary-key="relatedPrimaryKey || '+'"
:junction-field="relationInfo.junctionField"
@input="stageEdits"
@update:active="cancelEdit"
/>
@@ -54,7 +49,7 @@
<modal-collection
v-if="!disabled"
:active.sync="selectModalActive"
:collection="relation.one_collection"
:collection="relationInfo.relationCollection"
:selection="[]"
:filters="selectionFilters"
@input="stageSelection"
@@ -114,7 +109,7 @@ export default defineComponent({
setup(props, { emit }) {
const { collection, field, value, primaryKey } = toRefs(props);
const { junction, junctionCollection, relation, relationCollection, relationFields } = useRelation(
const { junction, junctionCollection, relation, relationCollection, relationInfo } = useRelation(
collection,
field
);
@@ -131,9 +126,12 @@ export default defineComponent({
getNewSelectedItems,
getJunctionItem,
getJunctionFromRelatedId,
} = useActions(value, relationFields, emitter);
} = useActions(value, relationInfo, emitter);
const fields = ref(['id', 'type', 'title']);
const fields = computed(() => {
const { junctionField } = relationInfo.value;
return ['id', 'type', 'title'].map((key) => `${junctionField}.${key}`);
});
const tableHeaders = ref<TableHeader[]>([
{
@@ -146,35 +144,37 @@ export default defineComponent({
{
text: i18n.t('title'),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
value: 'title',
value: relationInfo.value.junctionField + '.' + 'title',
align: 'left',
sortable: true,
width: 250,
},
]);
const { loading, displayItems, error, items } = usePreview(
const { loading, error, items } = usePreview(
value,
fields,
relationFields,
relationInfo,
getNewSelectedItems,
getUpdatedItems,
getNewItems,
getPrimaryKeys
);
const { cancelEdit, stageEdits, editsAtStart, editItem, currentlyEditing } = useEdit(
value,
items,
relationFields,
emitter,
getJunctionFromRelatedId
);
const {
cancelEdit,
stageEdits,
editsAtStart,
editItem,
currentlyEditing,
editModalActive,
relatedPrimaryKey,
} = useEdit(value, relationInfo, emitter);
const { stageSelection, selectModalActive, selectionFilters } = useSelection(
value,
displayItems,
relationFields,
items,
relationInfo,
emitter
);
@@ -186,7 +186,6 @@ export default defineComponent({
tableHeaders,
junctionCollection,
loading,
displayItems,
error,
currentlyEditing,
cancelEdit,
@@ -200,8 +199,10 @@ export default defineComponent({
items,
get,
onUpload,
relationFields,
relationInfo,
editItem,
editModalActive,
relatedPrimaryKey,
};
function useUpload() {
@@ -214,11 +215,11 @@ export default defineComponent({
if (files.length === 0) return;
const { junctionRelation } = relationFields.value;
const { junctionField } = relationInfo.value;
const file = files[0];
const fileAsJunctionRow = {
[junctionRelation]: {
[junctionField]: {
id: file.id,
title: file.title,
type: file.type,

View File

@@ -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,29 +15,24 @@
<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="relationInfo.junctionCollection"
:field="header.field.field"
/>
</template>
<template #item-append="{ item }" v-if="!disabled">
<v-icon
name="close"
v-tooltip="$t('deselect')"
class="deselect"
@click.stop="deleteItem(item, items)"
/>
<v-icon name="close" v-tooltip="$t('deselect')" class="deselect" @click.stop="deleteItem(item)" />
</template>
</v-table>
<div class="actions" v-if="!disabled">
<v-button class="new" @click="currentlyEditing = '+'">{{ $t('create_new') }}</v-button>
<v-button class="new" @click="editModalActive = true">{{ $t('create_new') }}</v-button>
<v-button class="existing" @click="selectModalActive = true">
{{ $t('add_existing') }}
</v-button>
@@ -45,11 +40,11 @@
<modal-item
v-if="!disabled"
:active="currentlyEditing !== null"
:collection="relationCollection.collection"
:active="editModalActive"
:collection="relationInfo.junctionCollection"
:primary-key="currentlyEditing || '+'"
:related-primary-key="relationFields.relationPkField"
:junction-field="relationFields.junctionRelation"
:related-primary-key="relatedPrimaryKey || '+'"
:junction-field="relationInfo.junctionField"
:edits="editsAtStart"
@input="stageEdits"
@update:active="cancelEdit"
@@ -71,6 +66,7 @@
import { defineComponent, ref, computed, watch, PropType, toRefs } from '@vue/composition-api';
import ModalItem from '@/views/private/components/modal-item';
import ModalCollection from '@/views/private/components/modal-collection';
import { get } from 'lodash';
import useActions from './use-actions';
import useRelation from './use-relation';
@@ -113,7 +109,7 @@ export default defineComponent({
emit('input', newVal);
}
const { junction, junctionCollection, relation, relationCollection, relationFields } = useRelation(
const { junction, junctionCollection, relation, relationCollection, relationInfo } = useRelation(
collection,
field
);
@@ -126,30 +122,32 @@ export default defineComponent({
getNewSelectedItems,
getJunctionItem,
getJunctionFromRelatedId,
} = useActions(value, relationFields, emitter);
} = useActions(value, relationInfo, emitter);
const { tableHeaders, items, loading, error, displayItems } = usePreview(
const { tableHeaders, items, loading, error } = usePreview(
value,
fields,
relationFields,
relationInfo,
getNewSelectedItems,
getUpdatedItems,
getNewItems,
getPrimaryKeys
);
const { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit } = useEdit(
value,
items,
relationFields,
emitter,
getJunctionFromRelatedId
);
const {
currentlyEditing,
editItem,
editsAtStart,
stageEdits,
cancelEdit,
relatedPrimaryKey,
editModalActive,
} = useEdit(value, relationInfo, emitter);
const { stageSelection, selectModalActive, selectionFilters } = useSelection(
value,
displayItems,
relationFields,
items,
relationInfo,
emitter
);
@@ -168,10 +166,12 @@ export default defineComponent({
stageSelection,
selectModalActive,
deleteItem,
displayItems,
selectionFilters,
items,
relationFields,
relationInfo,
relatedPrimaryKey,
get,
editModalActive,
};
},
});

View File

@@ -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;
}

View File

@@ -1,178 +1,131 @@
import { Ref } from '@vue/composition-api';
import { RelationInfo } from './use-relation';
import { get, has, isEqual } from 'lodash';
export default function useActions(
value: Ref<(string | number | Record<string, any>)[] | null>,
relation: Ref<RelationInfo>,
emit: (newValue: any[] | null) => void
) {
// Returns the junction item with the given Id.
function getJunctionItem(id: string | number) {
const { junctionPkField } = relation.value;
if (value.value === null) return null;
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
);
}
// Returns all items that have no junction item yet, but an related item does exist.
function getNewSelectedItems() {
const { junctionRelation } = relation.value;
const { junctionField } = relation.value;
if (value.value === null || junctionRelation === null) return [];
if (value.value === null || junctionField === null) return [];
return value.value.filter(
(item) => typeof item === 'object' && junctionRelation in item && typeof item[junctionRelation] !== 'object'
(item) => typeof item === 'object' && junctionField in item && typeof item[junctionField] !== 'object'
) as Record<string, any>[];
}
// Returns all items that do not have an existing junction and related item.
function getNewItems() {
const { junctionRelation, relationPkField } = relation.value;
const { junctionField, relationPkField } = relation.value;
if (value.value === null || junctionRelation === null) return [];
if (value.value === null || junctionField === null) return [];
return value.value.filter(
(item) =>
typeof item === 'object' &&
junctionRelation in item &&
typeof item[junctionRelation] === 'object' &&
relationPkField in item[junctionRelation] === false
typeof get(item, junctionField) === 'object' && has(item, [junctionField, relationPkField]) === false
) as Record<string, any>[];
}
// Returns a list of items which related or junction item does exist but had changes.
function getUpdatedItems() {
const { junctionRelation, relationPkField } = relation.value;
const { junctionField, relationPkField } = relation.value;
if (value.value === null || junctionRelation === null) return [];
if (value.value === null || junctionField === 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, [junctionField, relationPkField])) as Record<string, any>[];
}
// Returns only items that do not have any changes what so ever.
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));
}
// Get a list of junction item ids.
function getPrimaryKeys(): (string | number)[] {
const { junctionPkField } = relation.value;
if (value.value === null) return [];
return value.value
.map((item) => {
if (typeof item === 'object') {
if (junctionPkField in item) return item[junctionPkField];
} else {
return item;
}
})
.filter((i) => i);
return value.value.reduce((acc: any[], item) => {
const deepId = get(item, [junctionPkField]) as number | string | undefined;
if (['string', 'number'].includes(typeof item)) acc.push(item);
else if (deepId !== undefined) acc.push(deepId);
return acc;
}, []) as (string | number)[];
}
function getRelatedPrimaryKeys(): (string | number)[] {
// Get a list of ids of the related items.
function getRelatedPrimaryKeys() {
if (value.value === null) return [];
const { junctionRelation, relationPkField } = relation.value;
return value.value
.map((junctionItem) => {
if (
typeof junctionItem !== 'object' ||
junctionRelation === null ||
junctionRelation in junctionItem === false
)
return undefined;
const item = junctionItem[junctionRelation];
const { junctionField, relationPkField } = relation.value;
return value.value.reduce((acc: any[], item) => {
const relatedId = get(item, junctionField) as number | string | undefined;
const deepRelatedId = get(item, [junctionField, relationPkField]) as number | string | undefined;
if (relatedId !== undefined) acc.push(relatedId);
else if (deepRelatedId !== undefined) acc.push(deepRelatedId);
return acc;
}, []) as (string | number)[];
}
function deleteItem(deletingItem: Record<string, any>) {
if (value.value === null) return;
const { junctionField, relationPkField, junctionPkField } = relation.value;
const junctionId = get(deletingItem, junctionPkField) as number | string | undefined;
const relatedId = get(deletingItem, [junctionField, relationPkField]) as number | string | undefined;
const newValue = value.value.filter((item) => {
if (junctionId !== undefined) {
if (typeof item === 'object') {
if (junctionRelation in item) return item[relationPkField];
return get(item, [junctionPkField]) !== junctionId;
} else {
return item;
return item !== junctionId;
}
})
.filter((i) => i);
}
function deleteItem(item: Record<string, any>, items: Record<string, any>[]) {
if (value.value === null) return;
const { junctionRelation, relationPkField } = relation.value;
const id = item[relationPkField] as number | string | undefined;
if (id !== undefined) return deleteItemWithId(id, items);
if (junctionRelation === null) return;
const newVal = value.value.filter((junctionItem) => {
if (typeof junctionItem !== 'object' || junctionRelation in junctionItem === false) return true;
return junctionItem[junctionRelation] !== item;
});
if (newVal.length === 0) emit(null);
else emit(newVal);
}
function deleteItemWithId(id: string | number, items: Record<string, any>[]) {
if (value.value === null) return;
const { junctionRelation, relationPkField, junctionPkField } = relation.value;
const junctionItem = items.find(
(item) =>
junctionRelation in item &&
relationPkField in item[junctionRelation] &&
item[junctionRelation][relationPkField] === id
);
if (junctionItem === undefined) return;
// If it is a newly selected Item
if (junctionPkField in junctionItem === false) {
const newVal = value.value.filter((item) => {
if (typeof item === 'object' && junctionRelation in item) {
const jItem = item[junctionRelation];
return typeof jItem === 'object' ? jItem[relationPkField] !== id : jItem !== id;
}
return true;
});
if (newVal.length === 0) emit(null);
else emit(newVal);
return;
}
// If it is an already existing item
const newVal = value.value.filter((item) => {
if (typeof item === 'object' && junctionPkField in item) {
return junctionItem[junctionPkField] !== item[junctionPkField];
} else {
return junctionItem[junctionPkField] !== item;
}
});
if (newVal.length === 0) emit(null);
else emit(newVal);
if (relatedId !== undefined) {
const itemRelatedId = get(item, [junctionField, relationPkField]);
if (['string', 'number'].includes(typeof itemRelatedId)) {
return itemRelatedId !== relatedId;
}
const junctionFieldId = get(item, [junctionField]);
if (['string', 'number'].includes(typeof junctionFieldId)) {
return junctionFieldId !== relatedId;
}
}
return isEqual(item, deletingItem) === false;
});
emit(newValue);
}
function getJunctionFromRelatedId(id: string | number, items: Record<string, any>[]) {
const { relationPkField, junctionRelation } = relation.value;
const { relationPkField, junctionField } = relation.value;
return (
items.find((item) => {
return (
typeof item === 'object' &&
junctionRelation in item &&
typeof item[junctionRelation] === 'object' &&
relationPkField in item[junctionRelation] &&
item[junctionRelation][relationPkField] === id
);
}) || null
);
return items.find((item) => get(item, [junctionField, relationPkField]) === id) || null;
}
return {
@@ -185,6 +138,5 @@ export default function useActions(
getRelatedPrimaryKeys,
getJunctionFromRelatedId,
deleteItem,
deleteItemWithId,
};
}

View File

@@ -1,77 +1,72 @@
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 editModalActive = ref(false);
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, junctionField, junctionPkField } = relation.value;
editModalActive.value = true;
editsAtStart.value = item;
currentlyEditing.value = hasPrimaryKey ? item[relationPkField] : -1;
currentlyEditing.value = get(item, [junctionPkField], null);
relatedPrimaryKey.value = get(item, [junctionField, relationPkField], 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 { relationPkField, junctionField, junctionPkField } = relation.value;
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, [junctionField], null) === id) return edits;
if (get(item, [junctionField, 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);
else emit(newValue);
cancelEdit();
}
function cancelEdit() {
editModalActive.value = false;
editsAtStart.value = {};
currentlyEditing.value = null;
relatedPrimaryKey.value = null;
}
return { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit };
return { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit, relatedPrimaryKey, editModalActive };
}

View File

@@ -4,7 +4,7 @@ import { RelationInfo } from './use-relation';
import { useFieldsStore } from '@/stores/';
import { Field, Collection } from '@/types';
import api from '@/api';
import { cloneDeep } from 'lodash';
import { cloneDeep, get } from 'lodash';
export default function usePreview(
value: Ref<(string | number | Record<string, any>)[] | null>,
@@ -24,6 +24,20 @@ export default function usePreview(
const items = ref<Record<string, any>[]>([]);
const error = ref(null);
function getRelatedFields(fields: string[]) {
const { junctionField } = relation.value;
return fields.reduce((acc: string[], field) => {
const sections = field.split('.');
if (junctionField === sections[0] && sections.length === 2) acc.push(sections[1]);
return acc;
}, []);
}
function getJunctionFields() {
return (fields.value || []).filter((field) => field.includes('.') === false);
}
watch(
() => value.value,
async (newVal) => {
@@ -33,14 +47,19 @@ export default function usePreview(
}
loading.value = true;
const { junctionRelation, relationPkField, junctionPkField } = relation.value;
if (junctionRelation === null) return;
const { junctionField, relationPkField, junctionPkField, relationCollection } = relation.value;
if (junctionField === null) return;
// Load the junction items so we have access to the id's in the related collection
const junctionItems = await loadRelatedIds();
const relatedPrimaryKeys = junctionItems.map((junction) => junction[junctionRelation]);
const filteredFields = [...(fields.value.length > 0 ? fields.value : getDefaultFields())];
const relatedPrimaryKeys = junctionItems.reduce((acc, junction) => {
const id = get(junction, junctionField);
if (id !== null) acc.push(id);
return acc;
}, []) as (string | number)[];
const filteredFields = [...(fields.value.length > 0 ? getRelatedFields(fields.value) : getDefaultFields())];
if (filteredFields.includes(relationPkField) === false) filteredFields.push(relationPkField);
@@ -48,27 +67,23 @@ export default function usePreview(
let responseData: Record<string, any>[] = [];
if (relatedPrimaryKeys.length > 0) {
const endpoint = relation.value.relationCollection.startsWith('directus_')
? `/${relation.value.relationCollection.substring(9)}`
: `/items/${relation.value.relationCollection}`;
const response = await api.get(endpoint, {
params: {
fields: filteredFields,
[`filter[${relationPkField}][_in]`]: relatedPrimaryKeys.join(','),
},
});
responseData = response?.data.data as Record<string, any>[];
responseData = await request(
relationCollection,
filteredFields,
relationPkField,
relatedPrimaryKeys
);
}
// Insert the related items into the junction items
const existingItems = responseData.map((data) => {
const id = data[relationPkField];
const junction = junctionItems.find((junction) => junction[junctionRelation] === id);
if (junction === undefined) return;
responseData = responseData.map((data) => {
const id = get(data, relationPkField);
const junction = junctionItems.find((junction) => junction[junctionField] === id);
if (junction === undefined || id === undefined) return;
const newJunction = cloneDeep(junction);
newJunction[junctionRelation] = data;
newJunction[junctionField] = data;
return newJunction;
}) as Record<string, any>[];
@@ -76,7 +91,7 @@ export default function usePreview(
const newItems = getNewItems();
// Replace existing items with it's updated counterparts
const newVal = existingItems
responseData = responseData
.map((item) => {
const updatedItem = updatedItems.find(
(updated) => updated[junctionPkField] === item[junctionPkField]
@@ -85,7 +100,8 @@ export default function usePreview(
return item;
})
.concat(...newItems);
items.value = newVal;
items.value = responseData;
} catch (err) {
error.value = err;
} finally {
@@ -96,27 +112,23 @@ export default function usePreview(
);
async function loadRelatedIds() {
const { junctionPkField, junctionRelation, relationPkField } = relation.value;
const { junctionPkField, junctionField, relationPkField, junctionCollection } = relation.value;
try {
let data: Record<string, any>[] = [];
const primaryKeys = getPrimaryKeys();
if (primaryKeys.length > 0) {
const endpoint = relation.value.junctionCollection.startsWith('directus_')
? `/${relation.value.junctionCollection.substring(9)}`
: `/items/${relation.value.junctionCollection}`;
const filteredFields = getJunctionFields();
const response = await api.get(endpoint, {
params: {
[`filter[${junctionPkField}][_in]`]: getPrimaryKeys().join(','),
},
});
data = response?.data.data as Record<string, any>[];
if (filteredFields.includes(junctionPkField) === false) filteredFields.push(junctionPkField);
if (filteredFields.includes(junctionField) === false) filteredFields.push(junctionField);
data = await request(junctionCollection, filteredFields, junctionPkField, primaryKeys);
}
const updatedItems = getUpdatedItems().map((item) => ({
[junctionRelation]: item[junctionRelation][relationPkField],
[junctionField]: item[junctionField][relationPkField],
}));
// Add all items that already had the id of it's related item
@@ -127,19 +139,38 @@ export default function usePreview(
return [];
}
const displayItems = computed(() => {
const { junctionRelation } = relation.value;
return items.value.map((item) => item[junctionRelation]);
});
async function request(
collection: string,
fields: string[] | null,
filteredField: string,
primaryKeys: (string | number)[] | null
) {
if (fields === null || fields.length === 0 || primaryKeys === null || primaryKeys.length === 0) return [];
const endpoint = collection.startsWith('directus_') ? `/${collection.substring(9)}` : `/items/${collection}`;
const response = await api.get(endpoint, {
params: {
fields: fields,
[`filter[${filteredField}][_in]`]: primaryKeys.join(','),
},
});
return response?.data.data as Record<string, any>[];
}
// 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 { junctionField, junctionCollection } = relation.value;
tableHeaders.value = (fields.value.length > 0
? fields.value
: getDefaultFields().map((field) => `${junctionField}.${field}`)
)
.map((fieldKey) => {
const field = fieldsStore.getField(relation.value.relationCollection, fieldKey);
let field = fieldsStore.getField(junctionCollection, fieldKey);
if (!field) return null;
@@ -171,5 +202,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 };
}

View File

@@ -6,7 +6,7 @@ import { Relation } from '@/types';
export type RelationInfo = {
junctionPkField: string;
relationPkField: string;
junctionRelation: string;
junctionField: string;
junctionCollection: string;
relationCollection: string;
};
@@ -38,11 +38,11 @@ export default function useRelation(collection: Ref<string>, field: Ref<string>)
const { primaryKeyField: junctionPrimaryKeyField } = useCollection(junctionCollection.value.collection);
const { primaryKeyField: relationPrimaryKeyField } = useCollection(relationCollection.value.collection);
const relationFields = computed(() => {
const relationInfo = computed(() => {
return {
junctionPkField: junctionPrimaryKeyField.value.field,
relationPkField: relationPrimaryKeyField.value.field,
junctionRelation: junction.value.junction_field as string,
junctionField: junction.value.junction_field as string,
junctionCollection: junctionCollection.value.collection,
relationCollection: relationCollection.value.collection,
} as RelationInfo;
@@ -53,7 +53,7 @@ export default function useRelation(collection: Ref<string>, field: Ref<string>)
junctionCollection,
relation,
relationCollection,
relationFields,
relationInfo,
junctionPrimaryKeyField,
relationPrimaryKeyField,
};

View File

@@ -1,23 +1,26 @@
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, junctionField } = relation.value;
const selectedKeys: (number | string)[] = displayItems.value
.filter((currentItem) => relationPkField in currentItem)
.map((currentItem) => currentItem[relationPkField]);
const selectedKeys = items.value.reduce((acc, current) => {
const key = get(current, [junctionField, relationPkField]);
if (key !== undefined) acc.push(key);
return acc;
}, []) as (number | string)[];
return selectedKeys;
});
@@ -39,11 +42,12 @@ export default function useSelection(
});
function stageSelection(newSelection: (number | string)[]) {
const { junctionRelation } = relation.value;
const { junctionField } = relation.value;
const selection = newSelection
.filter((item) => selectedPrimaryKeys.value.includes(item) === false)
.map((item) => ({ [junctionRelation]: item }));
const selection = newSelection.reduce((acc, item) => {
if (selectedPrimaryKeys.value.includes(item) === false) acc.push({ [junctionField]: item });
return acc;
}, new Array());
const newVal = [...selection, ...(value.value || [])];
if (newVal.length === 0) emit(null);