mirror of
https://github.com/directus/directus.git
synced 2026-01-30 08:47:57 -05:00
Merge pull request #660 from directus/relational-updates
Re-add support for junction fields
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user