finish junction support on m2m

This commit is contained in:
Nitwel
2020-10-15 12:32:40 +02:00
parent b0f2c92c53
commit 21df8bf239
6 changed files with 173 additions and 187 deletions

View File

@@ -21,35 +21,32 @@
:interface="header.field.interface"
:interface-options="header.field.interfaceOptions"
:type="header.field.type"
:collection="relationFields.junctionCollection"
: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>
</div>
<pre>{{ JSON.stringify(value, null, 4) }}</pre>
<modal-item
v-if="!disabled"
:active="currentlyEditing !== null"
:collection="relationFields.junctionCollection"
:active="editModalActive"
:collection="relationInfo.junctionCollection"
:primary-key="currentlyEditing || '+'"
:related-primary-key="relatedPrimaryKey || '+'"
:junction-field="relationFields.junctionRelation"
:junction-field="relationInfo.junctionField"
:edits="editsAtStart"
@input="stageEdits"
@update:active="cancelEdit"
@@ -114,7 +111,7 @@ export default defineComponent({
emit('input', newVal);
}
const { junction, junctionCollection, relation, relationCollection, relationFields } = useRelation(
const { junction, junctionCollection, relation, relationCollection, relationInfo } = useRelation(
collection,
field
);
@@ -127,28 +124,32 @@ export default defineComponent({
getNewSelectedItems,
getJunctionItem,
getJunctionFromRelatedId,
} = useActions(value, relationFields, emitter);
} = useActions(value, relationInfo, emitter);
const { tableHeaders, items, loading, error } = usePreview(
value,
fields,
relationFields,
relationInfo,
getNewSelectedItems,
getUpdatedItems,
getNewItems,
getPrimaryKeys
);
const { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit, relatedPrimaryKey } = useEdit(
value,
relationFields,
emitter
);
const {
currentlyEditing,
editItem,
editsAtStart,
stageEdits,
cancelEdit,
relatedPrimaryKey,
editModalActive,
} = useEdit(value, relationInfo, emitter);
const { stageSelection, selectModalActive, selectionFilters } = useSelection(
value,
items,
relationFields,
relationInfo,
emitter
);
@@ -169,9 +170,10 @@ export default defineComponent({
deleteItem,
selectionFilters,
items,
relationFields,
relationInfo,
relatedPrimaryKey,
get,
editModalActive,
};
},
});

View File

@@ -1,6 +1,6 @@
import { Ref } from '@vue/composition-api';
import { RelationInfo } from './use-relation';
import { get, has } from 'lodash';
import { get, has, isEqual } from 'lodash';
export default function useActions(
value: Ref<(string | number | Record<string, any>)[] | null>,
@@ -19,31 +19,32 @@ export default function useActions(
}
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>[];
}
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) => has(item, junctionRelation) && has(item, [junctionRelation, relationPkField]) === false
(item) =>
typeof get(item, junctionField) === 'object' && has(item, [junctionField, relationPkField]) === false
) as Record<string, any>[];
}
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) => has(item, [junctionRelation, relationPkField])) as Record<string, any>[];
return value.value.filter((item) => has(item, [junctionField, relationPkField])) as Record<string, any>[];
}
function getExistingItems() {
@@ -57,112 +58,69 @@ export default function useActions(
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)[] {
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;
console.log(junctionId, relatedId);
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 {
@@ -175,6 +133,5 @@ export default function useActions(
getRelatedPrimaryKeys,
getJunctionFromRelatedId,
deleteItem,
deleteItemWithId,
};
}

View File

@@ -7,8 +7,7 @@ export default function useEdit(
relation: Ref<RelationInfo>,
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);
@@ -16,15 +15,16 @@ export default function useEdit(
const editsAtStart = ref<Record<string, any>>({});
function editItem(item: any) {
const { relationPkField, junctionRelation, junctionPkField } = relation.value;
const { relationPkField, junctionField, junctionPkField } = relation.value;
editModalActive.value = true;
editsAtStart.value = item;
relatedPrimaryKey.value = get(item, [junctionRelation, relationPkField], null);
currentlyEditing.value = get(item, [junctionPkField], null);
relatedPrimaryKey.value = get(item, [junctionField, relationPkField], null);
}
function stageEdits(edits: any) {
const { relationPkField, junctionRelation, junctionPkField } = relation.value;
const { relationPkField, junctionField, junctionPkField } = relation.value;
const newValue = (value.value || []).map((item) => {
if (currentlyEditing.value !== null) {
@@ -40,8 +40,8 @@ export default function useEdit(
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 (get(item, [junctionField], null) === id) return edits;
if (get(item, [junctionField, relationPkField], null) === id) return edits;
}
if (isEqual(editsAtStart.value, item)) {
@@ -57,13 +57,16 @@ export default function useEdit(
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, relatedPrimaryKey };
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>,
@@ -25,14 +25,13 @@ export default function usePreview(
const error = ref(null);
function getRelatedFields(fields: string[]) {
const { junctionRelation } = relation.value;
const { junctionField } = relation.value;
return fields
.map((field) => {
const sections = field.split('.');
if (junctionRelation === sections[0] && sections.length === 2) return sections[1];
})
.filter((i) => i);
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() {
@@ -48,12 +47,17 @@ 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 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())];
@@ -63,35 +67,36 @@ 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
);
}
console.log('responseData1', responseData);
// 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>[];
const updatedItems = getUpdatedItems();
const newItems = getNewItems();
console.log('updatedItems', updatedItems);
console.log('newItems', newItems);
console.log('responseData2', responseData);
// Replace existing items with it's updated counterparts
const newVal = existingItems
responseData = responseData
.map((item) => {
const updatedItem = updatedItems.find(
(updated) => updated[junctionPkField] === item[junctionPkField]
@@ -100,7 +105,9 @@ export default function usePreview(
return item;
})
.concat(...newItems);
items.value = newVal;
console.log('responseData3', responseData);
items.value = responseData;
} catch (err) {
error.value = err;
} finally {
@@ -111,33 +118,24 @@ 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();
console.log('PKS', primaryKeys);
if (primaryKeys.length > 0) {
const filteredFields = getJunctionFields();
if (filteredFields.includes(junctionPkField) === false) filteredFields.push(junctionPkField);
if (filteredFields.includes(junctionRelation) === false) filteredFields.push(junctionRelation);
if (filteredFields.includes(junctionField) === false) filteredFields.push(junctionField);
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(','),
},
});
data = response?.data.data as Record<string, any>[];
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
@@ -148,16 +146,35 @@ export default function usePreview(
return [];
}
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,
() => {
const { junctionRelation, junctionCollection } = relation.value;
const { junctionField, junctionCollection } = relation.value;
tableHeaders.value = (fields.value.length > 0
? fields.value
: getDefaultFields().map((field) => `${junctionRelation}.${field}`)
: getDefaultFields().map((field) => `${junctionField}.${field}`)
)
.map((fieldKey) => {
let field = fieldsStore.getField(junctionCollection, fieldKey);

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

@@ -14,11 +14,13 @@ export default function useSelection(
const selectedPrimaryKeys = computed(() => {
if (items.value === null) return [];
const { relationPkField, junctionRelation } = relation.value;
const { relationPkField, junctionField } = relation.value;
const selectedKeys: (number | string)[] = items.value
.map((currentItem) => get(currentItem, [junctionRelation, relationPkField]))
.filter((i) => i);
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;
});
@@ -40,7 +42,12 @@ export default function useSelection(
});
function stageSelection(newSelection: (number | string)[]) {
const selection = newSelection.filter((item) => selectedPrimaryKeys.value.includes(item) === false);
const { junctionField } = relation.value;
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);