organize codebase and fix bugs

This commit is contained in:
Nitwel
2020-10-14 12:23:40 +02:00
parent 7fcb4717a4
commit f15b3ffb99
9 changed files with 560 additions and 455 deletions

1
api/package-lock.json generated
View File

@@ -69,7 +69,6 @@
"color": "^3.1.2",
"color-string": "^1.5.3",
"cropperjs": "^1.5.7",
"csslint": "^1.0.5",
"date-fns": "^2.14.0",
"diff": "^4.0.2",
"highlight.js": "^10.2.0",

162
app/package-lock.json generated
View File

@@ -2432,6 +2432,51 @@
"tslint": "^5.20.1",
"webpack": "^4.0.0",
"yorkie": "^2.0.0"
},
"dependencies": {
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"fork-ts-checker-webpack-plugin-v5": {
"version": "npm:fork-ts-checker-webpack-plugin@5.2.0",
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.0.tgz",
"integrity": "sha512-NEKcI0+osT5bBFZ1SFGzJMQETjQWZrSvMO1g0nAR/w0t328Z41eN8BJEIZyFCl2HsuiJpa9AN474Nh2qLVwGLQ==",
"dev": true,
"optional": true,
"requires": {
"@babel/code-frame": "^7.8.3",
"@types/json-schema": "^7.0.5",
"chalk": "^4.1.0",
"cosmiconfig": "^6.0.0",
"deepmerge": "^4.2.2",
"fs-extra": "^9.0.0",
"memfs": "^3.1.2",
"minimatch": "^3.0.4",
"schema-utils": "2.7.0",
"semver": "^7.3.2",
"tapable": "^1.0.0"
}
},
"schema-utils": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
"dev": true,
"optional": true,
"requires": {
"@types/json-schema": "^7.0.4",
"ajv": "^6.12.2",
"ajv-keywords": "^3.4.1"
}
}
}
},
"@vue/cli-plugin-unit-jest": {
@@ -2571,6 +2616,17 @@
"unique-filename": "^1.1.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@@ -2654,6 +2710,18 @@
"graceful-fs": "^4.1.6"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"optional": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -2767,6 +2835,18 @@
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.0.0-beta.8",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-beta.8.tgz",
"integrity": "sha512-oouKUQWWHbSihqSD7mhymGPX1OQ4hedzAHyvm8RdyHh6m3oIvoRF+NM45i/bhNOlo8jCnuJhaSUf/6oDjv978g==",
"dev": true,
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
}
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@@ -7503,51 +7583,6 @@
}
}
},
"fork-ts-checker-webpack-plugin-v5": {
"version": "npm:fork-ts-checker-webpack-plugin@5.2.0",
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.0.tgz",
"integrity": "sha512-NEKcI0+osT5bBFZ1SFGzJMQETjQWZrSvMO1g0nAR/w0t328Z41eN8BJEIZyFCl2HsuiJpa9AN474Nh2qLVwGLQ==",
"dev": true,
"optional": true,
"requires": {
"@babel/code-frame": "^7.8.3",
"@types/json-schema": "^7.0.5",
"chalk": "^4.1.0",
"cosmiconfig": "^6.0.0",
"deepmerge": "^4.2.2",
"fs-extra": "^9.0.0",
"memfs": "^3.1.2",
"minimatch": "^3.0.4",
"schema-utils": "2.7.0",
"semver": "^7.3.2",
"tapable": "^1.0.0"
},
"dependencies": {
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"schema-utils": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
"dev": true,
"optional": true,
"requires": {
"@types/json-schema": "^7.0.4",
"ajv": "^6.12.2",
"ajv-keywords": "^3.4.1"
}
}
}
},
"form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
@@ -16173,43 +16208,6 @@
}
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.0.0-beta.8",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-beta.8.tgz",
"integrity": "sha512-oouKUQWWHbSihqSD7mhymGPX1OQ4hedzAHyvm8RdyHh6m3oIvoRF+NM45i/bhNOlo8jCnuJhaSUf/6oDjv978g==",
"dev": true,
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
},
"dependencies": {
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"optional": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
}
}
},
"vue-router": {
"version": "3.4.6",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.6.tgz",

View File

@@ -27,7 +27,12 @@
</template>
<template #item-append="{ item }" v-if="!disabled">
<v-icon name="close" v-tooltip="$t('deselect')" class="deselect" @click.stop="deleteItem(item)" />
<v-icon
name="close"
v-tooltip="$t('deselect')"
class="deselect"
@click.stop="deleteItem(item, items)"
/>
</template>
</v-table>
@@ -38,6 +43,8 @@
</v-button>
</div>
<pre>{{ JSON.stringify(value, null, 4) }}</pre>
<modal-detail
v-if="!disabled"
:active="currentlyEditing !== null"
@@ -62,23 +69,20 @@
<script lang="ts">
import { defineComponent, ref, computed, watch, PropType, toRefs } from '@vue/composition-api';
import api from '@/api';
import useCollection from '@/composables/use-collection';
import { useCollectionsStore, useRelationsStore, useFieldsStore } from '@/stores/';
import ModalDetail from '@/views/private/components/modal-detail';
import ModalBrowse from '@/views/private/components/modal-browse';
import { Filter, Field } from '@/types';
import { Header } from '@/components/v-table/types';
import { Relation } from '@/types';
import { cloneDeep, isEqual } from 'lodash';
import useActions from './actions';
import useActions from './use-actions';
import useRelation from './use-relation';
import usePreview from './use-preview';
import useEdit from './use-edit';
import useSelection from './use-selection';
export default defineComponent({
components: { ModalDetail, ModalBrowse },
props: {
value: {
type: Array as PropType<(number | string | Record<string, any>)[]>,
type: Array as PropType<(number | string | Record<string, any>)[] | null>,
default: null,
},
primaryKey: {
@@ -103,15 +107,16 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { value } = toRefs(props);
const relationsStore = useRelationsStore();
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const { value, collection, field, fields } = toRefs(props);
const { junction, junctionCollection, relation, relationCollection, relationFields } = useRelation();
const { tableHeaders, items, loading, error, displayItems, getJunctionFromRelatedId } = useTable();
const { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit } = useEdits();
const { stageSelection, selectModalActive, selectedPrimaryKeys } = useSelection();
function emitter(newVal: any[] | null) {
emit('input', newVal);
}
const { junction, junctionCollection, relation, relationCollection, relationFields } = useRelation(
collection,
field
);
const {
deleteItem,
@@ -120,7 +125,36 @@ export default defineComponent({
getPrimaryKeys,
getNewSelectedItems,
getJunctionItem,
} = useActions(value, items, relationFields, (newValue) => emit('input', newValue));
getJunctionFromRelatedId,
} = useActions(value, relationFields, emitter);
const { tableHeaders, items, loading, error, displayItems } = usePreview(
value,
fields,
relationFields,
getNewSelectedItems,
getUpdatedItems,
getNewItems,
getPrimaryKeys
);
const { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit } = useEdit(
value,
items,
relationFields,
emitter,
getJunctionFromRelatedId
);
const { stageSelection, selectModalActive, selectedPrimaryKeys } = useSelection(
items,
displayItems,
relationFields,
emitter,
getNewItems,
getJunctionFromRelatedId,
getJunctionItem
);
return {
junction,
@@ -141,310 +175,6 @@ export default defineComponent({
selectedPrimaryKeys,
items,
};
/**
* Holds info about the current relationship, like related collection, primary key field
* of the other collection etc
*/
function useRelation() {
const relations = computed(() => {
return relationsStore.getRelationsForField(props.collection, props.field) as Relation[];
});
const junction = computed(() => {
return relations.value.find((relation) => relation.one_collection === props.collection) as Relation;
});
const relation = computed(() => {
return relations.value.find((relation) => relation.one_collection !== props.collection) as Relation;
});
const junctionCollection = computed(() => {
return collectionsStore.getCollection(junction.value.many_collection)!;
});
const relationCollection = computed(() => {
return collectionsStore.getCollection(relation.value.one_collection)!;
});
const { primaryKeyField: junctionPrimaryKeyField } = useCollection(junctionCollection.value.collection);
const { primaryKeyField: relationPrimaryKeyField } = useCollection(relationCollection.value.collection);
const relationFields = computed(() => {
return {
junctionPkField: junctionPrimaryKeyField.value.field,
relationPkField: relationPrimaryKeyField.value.field,
junctionRelation: junction.value.junction_field as string,
};
});
return {
junction,
junctionCollection,
relation,
relationCollection,
relationFields,
};
}
function useTable() {
// Using a ref for the table headers here means that the table itself can update the
// values if it needs to. This allows the user to manually resize the columns for example
const tableHeaders = ref<Header[]>([]);
const loading = ref(false);
const items = ref<Record<string, any>[]>([]);
const error = ref(null);
watch(
() => props.value,
async (newVal) => {
loading.value = true;
const { junctionRelation, relationPkField, junctionPkField } = relationFields.value;
if (junctionRelation === null) return;
// Load the junction items so we have access to the id's in the related collection
const junctionItems = await loadRelatedIds(newVal);
const relatedPrimaryKeys = junctionItems.map((junction) => junction[junctionRelation]);
const fields = [...(props.fields.length > 0 ? props.fields : getDefaultFields())];
if (fields.includes(relationPkField) === false) fields.push(relationPkField);
try {
const endpoint = relationCollection.value.collection.startsWith('directus_')
? `/${relationCollection.value.collection.substring(9)}`
: `/items/${relationCollection.value.collection}`;
const response = await api.get(endpoint, {
params: {
fields: fields,
[`filter[${relationPkField}][_in]`]: relatedPrimaryKeys.join(','),
},
});
const responseData = (response.data.data as Record<string, any>[]) || [];
// 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;
const newJunction = cloneDeep(junction);
newJunction[junctionRelation] = data;
return newJunction;
}) as Record<string, any>[];
const updatedItems = getUpdatedItems();
const newItems = getNewItems();
// Replace existing items with it's updated counterparts
const newVal = existingItems
.map((item) => {
const updatedItem = updatedItems.find(
(updated) => updated[junctionPkField] === item[junctionPkField]
);
if (updatedItem !== undefined) return updatedItem;
return item;
})
.concat(...newItems);
items.value = newVal;
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
},
{ immediate: true }
);
async function loadRelatedIds(newVal: (string | number | Record<string, any>)[]) {
const { junctionPkField } = relationFields.value;
try {
const endpoint = junctionCollection.value.collection.startsWith('directus_')
? `/${junctionCollection.value.collection.substring(9)}`
: `/items/${junctionCollection.value.collection}`;
const response = await api.get(endpoint, {
params: {
[`filter[${junctionPkField}][_in]`]: getPrimaryKeys().join(','),
},
});
const data = response.data.data as Record<string, any>[];
// Add all items that already had the id of it's related item
return data.concat(...getNewSelectedItems());
} catch (err) {
error.value = err;
}
return [];
}
function getJunctionFromRelatedId(id: string | number) {
const { relationPkField, junctionRelation } = relationFields.value;
return (
items.value.find((item) => {
return;
typeof item === 'object' &&
junctionRelation in item &&
typeof item[junctionRelation] === 'object' &&
relationPkField in item[junctionRelation] &&
item[junctionRelation][relationPkField] === id;
}) || null
);
}
const displayItems = computed(() => {
const { junctionRelation } = relationFields.value;
return items.value.map((item) => item[junctionRelation]);
});
// Seeing we don't care about saving those tableHeaders, we can reset it whenever the
// fields prop changes (most likely when we're navigating to a different o2m context)
watch(
() => props.fields,
() => {
tableHeaders.value = (props.fields.length > 0 ? props.fields : getDefaultFields())
.map((fieldKey) => {
const field = fieldsStore.getField(relationCollection.value.collection, fieldKey);
if (!field) return null;
const header: Header = {
text: field.name,
value: fieldKey,
align: 'left',
sortable: true,
width: null,
field: {
display: field.meta?.display,
displayOptions: field.meta?.display_options,
interface: field.meta?.interface,
interfaceOptions: field.meta?.options,
type: field.type,
field: field.field,
},
};
return header;
})
.filter((h) => h) as Header[];
},
{ immediate: true }
);
return { tableHeaders, displayItems, items, loading, error, getJunctionFromRelatedId };
}
function useEdits() {
// Primary key of the item we're currently editing. If null, the edit modal should be
// closed
const currentlyEditing = ref<string | number | null>(null);
// This keeps track of the starting values so we can match with it
const editsAtStart = ref<Record<string, any>>({});
return { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit };
function editItem(item: any) {
const { relationPkField } = relationFields.value;
const hasPrimaryKey = relationPkField in item;
editsAtStart.value = item;
currentlyEditing.value = hasPrimaryKey ? item[relationPkField] : -1;
}
function stageEdits(edits: any) {
const { relationPkField, junctionRelation, junctionPkField } = relationFields.value;
const editsWrapped = { [junctionRelation]: edits };
const hasPrimaryKey = relationPkField in editsAtStart.value;
const junctionItem = hasPrimaryKey
? getJunctionFromRelatedId(editsAtStart.value[relationPkField])
: null;
const newValue = props.value.map((item) => {
if (junctionItem !== null && junctionPkField in junctionItem) {
const id = junctionItem[junctionPkField];
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 (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 (isEqual({ [junctionRelation]: editsAtStart.value }, item)) {
return editsWrapped;
}
return item;
});
if (hasPrimaryKey === false && newValue.includes(editsWrapped) === false) {
newValue.push(editsWrapped);
}
emit('input', newValue);
}
function cancelEdit() {
editsAtStart.value = {};
currentlyEditing.value = null;
}
}
function useSelection() {
const selectModalActive = ref(false);
const selectedPrimaryKeys = computed<(number | string)[]>(() => {
if (displayItems.value === null) return [];
const { relationPkField } = relationFields.value;
return displayItems.value
.filter((currentItem) => relationPkField in currentItem)
.map((currentItem) => currentItem[relationPkField]);
});
return { stageSelection, selectModalActive, selectedPrimaryKeys };
function stageSelection(newSelection: (number | string)[]) {
const { junctionRelation, junctionPkField } = relationFields.value;
const newItems = getNewItems();
if (junctionRelation === null) return;
const selection = newSelection.map((item) => {
const junction = getJunctionFromRelatedId(item);
if (junction === null) return { [junctionRelation]: item };
const updatedItem = getJunctionItem(junction[junctionPkField]);
return updatedItem === null ? { [junctionRelation]: item } : updatedItem;
});
emit('input', [...selection, ...newItems]);
}
}
function getDefaultFields(): string[] {
const fields = fieldsStore.getFieldsForCollection(relationCollection.value.collection);
return fields.slice(0, 3).map((field: Field) => field.field);
}
},
});
</script>

View File

@@ -1,20 +1,15 @@
import { Ref } from '@vue/composition-api';
export type RelationFields = {
junctionPkField: string;
relationPkField: string;
junctionRelation: string;
};
import { RelationInfo } from './use-relation';
export default function useActions(
value: Ref<(string | number | Record<string, any>)[]>,
items: Ref<Record<string, any>[]>,
relationFields: Ref<RelationFields>,
value: Ref<(string | number | Record<string, any>)[] | null>,
relation: Ref<RelationInfo>,
emit: (newValue: any[] | null) => void
) {
function getJunctionItem(id: string | number) {
const { junctionPkField } = relationFields.value;
const { junctionPkField } = relation.value;
if (value.value === null) return null;
return (
value.value.find(
(item) =>
@@ -24,18 +19,20 @@ export default function useActions(
}
function getNewSelectedItems() {
const { junctionRelation } = relationFields.value;
const { junctionRelation } = relation.value;
if (value.value === null || junctionRelation === null) return [];
return value.value.filter(
(item) => typeof item === 'object' && junctionRelation in item && typeof junctionRelation !== 'object'
) as Record<string, any>[];
}
function getNewItems() {
const { junctionRelation, relationPkField } = relationFields.value;
const { junctionRelation, relationPkField } = relation.value;
if (value.value === null || junctionRelation === null) return [];
return value.value.filter(
(item) =>
typeof item === 'object' &&
@@ -46,9 +43,10 @@ export default function useActions(
}
function getUpdatedItems() {
const { junctionRelation, relationPkField } = relationFields.value;
const { junctionRelation, relationPkField } = relation.value;
if (value.value === null || junctionRelation === null) return [];
return value.value.filter(
(item) =>
typeof item === 'object' &&
@@ -60,13 +58,15 @@ export default function useActions(
function getExistingItems() {
if (value.value === null) return [];
return value.value.filter((item) => typeof item === 'string' || typeof item === 'number');
}
function getPrimaryKeys() {
const { junctionPkField } = relationFields.value;
function getPrimaryKeys(): (string | number)[] {
const { junctionPkField } = relation.value;
if (value.value === null) return [];
return value.value
.map((item) => {
if (typeof item === 'object') {
@@ -78,9 +78,9 @@ export default function useActions(
.filter((i) => i);
}
function getRelatedPrimaryKeys() {
function getRelatedPrimaryKeys(): (string | number)[] {
if (value.value === null) return [];
const { junctionRelation, relationPkField } = relationFields.value;
const { junctionRelation, relationPkField } = relation.value;
return value.value
.map((junctionItem) => {
if (
@@ -100,26 +100,29 @@ export default function useActions(
.filter((i) => i);
}
function deleteItem(item: Record<string, any>) {
const { junctionRelation, relationPkField } = relationFields.value;
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);
if (id !== undefined) return deleteItemWithId(id, items);
if (junctionRelation === null) return;
emit(
value.value.filter((junctionItem) => {
if (typeof junctionItem !== 'object' || junctionRelation in junctionItem === false) return true;
return junctionItem[junctionRelation] !== item;
})
);
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) {
const { junctionRelation, relationPkField, junctionPkField } = relationFields.value;
function deleteItemWithId(id: string | number, items: Record<string, any>[]) {
if (value.value === null) return;
const { junctionRelation, relationPkField, junctionPkField } = relation.value;
const junctionItem = items.value.find(
const junctionItem = items.find(
(item) =>
junctionRelation in item &&
relationPkField in item[junctionRelation] &&
@@ -130,27 +133,45 @@ export default function useActions(
// If it is a newly selected Item
if (junctionPkField in junctionItem === false) {
emit(
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;
})
);
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
emit(
value.value.filter((item) => {
if (typeof item === 'object' && junctionPkField in item) {
return junctionItem[junctionPkField] !== item[junctionPkField];
} else {
return junctionItem[junctionPkField] !== 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);
}
function getJunctionFromRelatedId(id: string | number, items: Record<string, any>[]) {
const { relationPkField, junctionRelation } = 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
);
}
@@ -162,6 +183,7 @@ export default function useActions(
getExistingItems,
getPrimaryKeys,
getRelatedPrimaryKeys,
getJunctionFromRelatedId,
deleteItem,
deleteItemWithId,
};

View File

@@ -0,0 +1,77 @@
import { Ref, ref } from '@vue/composition-api';
import { RelationInfo } from './use-relation';
import { isEqual } 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
) {
// Primary key of the item we're currently editing. If null, the edit modal should be
// closed
const currentlyEditing = 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;
editsAtStart.value = item;
currentlyEditing.value = hasPrimaryKey ? item[relationPkField] : -1;
}
function stageEdits(edits: any) {
const { relationPkField, junctionRelation, junctionPkField } = relation.value;
const editsWrapped = { [junctionRelation]: edits };
const hasPrimaryKey = relationPkField in editsAtStart.value;
const junctionItem = hasPrimaryKey
? getJunctionFromRelatedId(editsAtStart.value[relationPkField], items.value)
: null;
const newValue = (value.value || []).map((item) => {
if (junctionItem !== null && junctionPkField in junctionItem) {
const id = junctionItem[junctionPkField];
if (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 (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 (isEqual({ [junctionRelation]: editsAtStart.value }, item)) {
return editsWrapped;
}
return item;
});
if (hasPrimaryKey === false && newValue.includes(editsWrapped) === false) {
newValue.push(editsWrapped);
}
if (newValue.length === 0) emit(null);
else emit(newValue);
}
function cancelEdit() {
editsAtStart.value = {};
currentlyEditing.value = null;
}
return { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit };
}

View File

@@ -0,0 +1,163 @@
import { Ref, ref, watch, computed } from '@vue/composition-api';
import { Header } from '@/components/v-table/types';
import { RelationInfo } from './use-relation';
import { useFieldsStore } from '@/stores/';
import { Field, Collection } from '@/types';
import api from '@/api';
import { cloneDeep } from 'lodash';
export default function usePreview(
value: Ref<(string | number | Record<string, any>)[] | null>,
fields: Ref<string[]>,
relation: Ref<RelationInfo>,
getNewSelectedItems: () => Record<string, any>[],
getUpdatedItems: () => Record<string, any>[],
getNewItems: () => Record<string, any>[],
getPrimaryKeys: () => (string | number)[]
) {
// Using a ref for the table headers here means that the table itself can update the
// values if it needs to. This allows the user to manually resize the columns for example
const fieldsStore = useFieldsStore();
const tableHeaders = ref<Header[]>([]);
const loading = ref(false);
const items = ref<Record<string, any>[]>([]);
const error = ref(null);
watch(
() => value.value,
async (newVal) => {
if (newVal === null) {
items.value = [];
return;
}
loading.value = true;
const { junctionRelation, relationPkField, junctionPkField } = relation.value;
if (junctionRelation === null) return;
// Load the junction items so we have access to the id's in the related collection
const junctionItems = await loadRelatedIds(newVal);
const relatedPrimaryKeys = junctionItems.map((junction) => junction[junctionRelation]);
const filteredFields = [...(fields.value.length > 0 ? fields.value : getDefaultFields())];
if (filteredFields.includes(relationPkField) === false) filteredFields.push(relationPkField);
try {
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(','),
},
});
const responseData = (response.data.data as Record<string, any>[]) || [];
// 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;
const newJunction = cloneDeep(junction);
newJunction[junctionRelation] = data;
return newJunction;
}) as Record<string, any>[];
const updatedItems = getUpdatedItems();
const newItems = getNewItems();
// Replace existing items with it's updated counterparts
const newVal = existingItems
.map((item) => {
const updatedItem = updatedItems.find(
(updated) => updated[junctionPkField] === item[junctionPkField]
);
if (updatedItem !== undefined) return updatedItem;
return item;
})
.concat(...newItems);
items.value = newVal;
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
},
{ immediate: true }
);
async function loadRelatedIds(newVal: (string | number | Record<string, any>)[]) {
const { junctionPkField } = relation.value;
try {
const endpoint = relation.value.junctionCollection.startsWith('directus_')
? `/${relation.value.junctionCollection.substring(9)}`
: `/items/${relation.value.junctionCollection}`;
const response = await api.get(endpoint, {
params: {
[`filter[${junctionPkField}][_in]`]: getPrimaryKeys().join(','),
},
});
const data = response.data.data as Record<string, any>[];
// Add all items that already had the id of it's related item
return data.concat(...getNewSelectedItems());
} catch (err) {
error.value = err;
}
return [];
}
const displayItems = computed(() => {
const { junctionRelation } = relation.value;
return items.value.map((item) => item[junctionRelation]);
});
// Seeing we don't care about saving those tableHeaders, we can reset it whenever the
// fields prop changes (most likely when we're navigating to a different o2m context)
watch(
() => fields.value,
() => {
tableHeaders.value = (fields.value.length > 0 ? fields.value : getDefaultFields())
.map((fieldKey) => {
const field = fieldsStore.getField(relation.value.relationCollection, fieldKey);
if (!field) return null;
const header: Header = {
text: field.name,
value: fieldKey,
align: 'left',
sortable: true,
width: null,
field: {
display: field.meta?.display,
displayOptions: field.meta?.display_options,
interface: field.meta?.interface,
interfaceOptions: field.meta?.options,
type: field.type,
field: field.field,
},
};
return header;
})
.filter((h) => h) as Header[];
},
{ immediate: true }
);
function getDefaultFields(): string[] {
const fields = fieldsStore.getFieldsForCollection(relation.value.relationCollection);
return fields.slice(0, 3).map((field: Field) => field.field);
}
return { tableHeaders, displayItems, items, loading, error };
}

View File

@@ -0,0 +1,60 @@
import { Ref, computed } from '@vue/composition-api';
import { useCollectionsStore, useRelationsStore } from '@/stores/';
import useCollection from '@/composables/use-collection';
import { Relation } from '@/types';
export type RelationInfo = {
junctionPkField: string;
relationPkField: string;
junctionRelation: string;
junctionCollection: string;
relationCollection: string;
};
export default function useRelation(collection: Ref<string>, field: Ref<string>) {
const relationsStore = useRelationsStore();
const collectionsStore = useCollectionsStore();
const relations = computed(() => {
return relationsStore.getRelationsForField(collection.value, field.value) as Relation[];
});
const junction = computed(() => {
return relations.value.find((relation) => relation.one_collection === collection.value) as Relation;
});
const relation = computed(() => {
return relations.value.find((relation) => relation.one_collection !== collection.value) as Relation;
});
const junctionCollection = computed(() => {
return collectionsStore.getCollection(junction.value.many_collection)!;
});
const relationCollection = computed(() => {
return collectionsStore.getCollection(relation.value.one_collection)!;
});
const { primaryKeyField: junctionPrimaryKeyField } = useCollection(junctionCollection.value.collection);
const { primaryKeyField: relationPrimaryKeyField } = useCollection(relationCollection.value.collection);
const relationFields = computed(() => {
return {
junctionPkField: junctionPrimaryKeyField.value.field,
relationPkField: relationPrimaryKeyField.value.field,
junctionRelation: junction.value.junction_field as string,
junctionCollection: junctionCollection.value.collection,
relationCollection: relationCollection.value.collection,
} as RelationInfo;
});
return {
junction,
junctionCollection,
relation,
relationCollection,
relationFields,
junctionPrimaryKeyField,
relationPrimaryKeyField,
};
}

View File

@@ -0,0 +1,54 @@
import { Ref, ref, computed } from '@vue/composition-api';
import { RelationInfo } from './use-relation';
export default function useSelection(
items: Ref<Record<string, any>[]>,
displayItems: Ref<Record<string, any>[]>,
relation: Ref<RelationInfo>,
emit: (newVal: any[] | null) => void,
getNewItems: () => Record<string, any>[],
getJunctionFromRelatedId: (id: string | number, items: Record<string, any>[]) => Record<string, any> | null,
getJunctionItem: (id: string | number) => string | number | Record<string, any> | null
) {
const selectModalActive = ref(false);
const selectedPrimaryKeys = computed(() => {
if (displayItems.value === null) return [];
const { relationPkField } = relation.value;
const selectedKeys: (number | string)[] = displayItems.value
.filter((currentItem) => relationPkField in currentItem)
.map((currentItem) => currentItem[relationPkField]);
return selectedKeys;
});
function stageSelection(newSelection: (number | string)[]) {
const { junctionRelation, junctionPkField } = relation.value;
const newItems = getNewItems();
console.log('A');
const selection = newSelection.map((item) => {
const junction = getJunctionFromRelatedId(item, items.value);
console.log('----');
console.log('item', item);
console.log('items', items.value);
console.log('junction', junction);
if (junction === null) return { [junctionRelation]: item };
const updatedItem = getJunctionItem(junction[junctionPkField]);
console.log(item, ' has Updated: ', updatedItem);
return updatedItem === null ? { [junctionRelation]: item } : updatedItem;
});
const newVal = [...selection, ...newItems];
if (newVal.length === 0) emit(null);
else emit(newVal);
}
return { stageSelection, selectModalActive, selectedPrimaryKeys };
}

View File

@@ -75,7 +75,7 @@ export default defineComponent({
components: { ModalDetail, ModalBrowse },
props: {
value: {
type: Array as PropType<(number | string | Record<string, any>)[]>,
type: Array as PropType<(number | string | Record<string, any>)[] | null>,
default: null,
},
primaryKey: {
@@ -175,6 +175,7 @@ export default defineComponent({
}
function deleteItem(item: Record<string, any>) {
if (props.value === null) return;
const relatedPrimKey = relatedPrimaryKeyField.value.field;
if (relatedPrimKey in item === false) {
@@ -333,7 +334,7 @@ export default defineComponent({
const hasPrimaryKey = pkField in edits;
const newValue = props.value.map((item) => {
const newValue = (props.value || []).map((item) => {
if (
typeof item === 'object' &&
pkField in item &&
@@ -358,7 +359,8 @@ export default defineComponent({
newValue.push(edits);
}
emit('input', newValue);
if (newValue.length === 0) emit('input', null);
else emit('input', newValue);
}
function cancelEdit() {