update files interface and fix bugs

This commit is contained in:
Nitwel
2020-10-14 18:18:22 +02:00
parent f15b3ffb99
commit cada867e4e
9 changed files with 115 additions and 664 deletions

View File

@@ -1,55 +1,59 @@
<template>
<v-notice type="warning" v-if="!relations || relations.length !== 2">
<v-notice type="warning" v-if="!junction || !relation">
{{ $t('relationship_not_setup') }}
</v-notice>
<div v-else class="files">
<v-table
inline
:items="previewItems"
:items="displayItems"
:loading="loading"
:headers.sync="tableHeaders"
:item-key="junctionCollectionPrimaryKeyField.field"
:item-key="relationFields.junctionPkField"
:disabled="disabled"
@click:row="editExisting"
@click:row="editItem"
>
<template #item.$thumbnail="{ item }">
<render-display
:value="get(item, [relationCurrentToJunction.junction_field])"
:value="item"
display="file"
:collection="junctionCollection"
:field="relationCurrentToJunction.junction_field"
:collection="relationFields.junctionCollection"
:field="relationFields.relationPkField"
type="file"
/>
</template>
<template #item-append="{ item }" v-if="!disabled">
<v-icon name="close" v-tooltip="$t('deselect')" class="deselect" @click.stop="deselect(item)" />
<v-icon
name="close"
v-tooltip="$t('deselect')"
class="deselect"
@click.stop="deleteItem(item, items)"
/>
</template>
</v-table>
<div class="actions" v-if="!disabled">
<v-button class="new" @click="showUpload = true">{{ $t('upload_file') }}</v-button>
<v-button class="existing" @click="showBrowseModal = true">
<v-button class="existing" @click="selectModalActive = true">
{{ $t('add_existing') }}
</v-button>
</div>
<modal-detail
v-if="!disabled"
:active="showDetailModal"
:collection="junctionCollection"
:primary-key="junctionRowPrimaryKey"
:active="currentlyEditing !== null"
:collection="relationFields.junctionCollection"
:primary-key="currentlyEditing || '+'"
:edits="editsAtStart"
:junction-field="relationCurrentToJunction.junction_field"
:related-primary-key="relatedRowPrimaryKey"
:junction-field="relationFields.junctionRelation"
@input="stageEdits"
@update:active="cancelEdit"
/>
<modal-browse
v-if="!disabled"
:active.sync="showBrowseModal"
:collection="relationJunctionToRelated.one_collection"
:active.sync="selectModalActive"
:collection="relation.one_collection"
:selection="[]"
:filters="selectionFilters"
@input="stageSelection"
@@ -69,17 +73,18 @@
</template>
<script lang="ts">
import { defineComponent, ref, computed, toRefs } from '@vue/composition-api';
import { defineComponent, ref, computed, toRefs, PropType } from '@vue/composition-api';
import { Header as TableHeader } from '@/components/v-table/types';
import ModalBrowse from '@/views/private/components/modal-browse';
import ModalDetail from '@/views/private/components/modal-detail';
import { get } from 'lodash';
import i18n from '@/lang';
import useRelation from './use-relation';
import useSelection from './use-selection';
import usePreview from './use-preview';
import useEdit from './use-edit';
import useActions from '@/interfaces/many-to-many/use-actions';
import useRelation from '@/interfaces/many-to-many/use-relation';
import useSelection from '@/interfaces/many-to-many/use-selection';
import usePreview from '@/interfaces/many-to-many/use-preview';
import useEdit from '@/interfaces/many-to-many/use-edit';
export default defineComponent({
components: { ModalBrowse, ModalDetail },
@@ -97,8 +102,8 @@ export default defineComponent({
required: true,
},
value: {
type: Array,
default: undefined,
type: Array as PropType<(string | number | Record<string, any>)[] | null>,
default: null,
},
disabled: {
type: Boolean,
@@ -108,24 +113,26 @@ export default defineComponent({
setup(props, { emit }) {
const { collection, field, value, primaryKey } = toRefs(props);
const { junction, junctionCollection, relation, relationCollection, relationFields } = useRelation(
collection,
field
);
function emitter(newVal: any[] | null) {
emit('input', newVal);
}
const {
relations,
relationCurrentToJunction,
relationJunctionToRelated,
junctionCollectionPrimaryKeyField,
junctionCollection,
relatedCollectionPrimaryKeyField,
relatedCollection,
} = useRelation({ collection, field });
deleteItem,
getUpdatedItems,
getNewItems,
getPrimaryKeys,
getNewSelectedItems,
getJunctionItem,
getJunctionFromRelatedId,
} = useActions(value, relationFields, emitter);
const fields = computed(() => {
if (!relationCurrentToJunction.value) return [];
if (!relationCurrentToJunction.value.junction_field) return [];
const jf = relationCurrentToJunction.value.junction_field;
return ['id', 'type', 'title'].map((key) => `${jf}.${key}`);
});
const fields = ref(['id', 'type', 'title']);
const tableHeaders = ref<TableHeader[]>([
{
@@ -138,126 +145,74 @@ export default defineComponent({
{
text: i18n.t('title'),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
value: relationCurrentToJunction.value!.junction_field + '.title',
value: 'title',
align: 'left',
sortable: true,
width: 250,
},
]);
const { loading, previewItems, error } = usePreview({
const { loading, displayItems, error, items } = usePreview(
value,
primaryKey,
junctionCollectionPrimaryKeyField,
relatedCollectionPrimaryKeyField,
junctionCollection,
relatedCollection,
relationCurrentToJunction,
relationJunctionToRelated,
fields,
});
relationFields,
getNewSelectedItems,
getUpdatedItems,
getNewItems,
getPrimaryKeys
);
const {
showDetailModal,
cancelEdit,
stageEdits,
editsAtStart,
junctionRowPrimaryKey,
editExisting,
relatedRowPrimaryKey,
initialValues,
} = useEdit({
relationCurrentToJunction,
junctionCollectionPrimaryKeyField,
relatedCollectionPrimaryKeyField,
const { cancelEdit, stageEdits, editsAtStart, editItem, currentlyEditing } = useEdit(
value,
onEdit: (newValue) => emit('input', newValue),
});
items,
relationFields,
emitter,
getJunctionFromRelatedId
);
const { showBrowseModal, stageSelection, selectionFilters } = useSelection({
relationCurrentToJunction,
relatedCollectionPrimaryKeyField,
previewItems,
onStageSelection: (selectionAsJunctionRows) => {
emit('input', [...(props.value || []), ...selectionAsJunctionRows]);
},
});
const { stageSelection, selectModalActive, selectionFilters } = useSelection(
value,
displayItems,
relationFields,
emitter
);
const { showUpload, onUpload } = useUpload();
return {
relations,
relationCurrentToJunction,
relationJunctionToRelated,
junction,
relation,
tableHeaders,
junctionCollectionPrimaryKeyField,
junctionCollection,
loading,
previewItems,
displayItems,
error,
showDetailModal,
currentlyEditing,
cancelEdit,
showUpload,
stageEdits,
editsAtStart,
junctionRowPrimaryKey,
editExisting,
relatedRowPrimaryKey,
showBrowseModal,
selectModalActive,
stageSelection,
selectionFilters,
relatedCollection,
initialValues,
deleteItem,
items,
get,
deselect,
onUpload,
relationFields,
editItem,
};
/**
* Deselect an item. This either means undoing any changes made (new item), or adding $delete: true
* if the junction row already exists.
*/
function deselect(junctionRow: any) {
const primaryKey = junctionRow[junctionCollectionPrimaryKeyField.value.field];
// If the junction row has a primary key, it's an existing item in the junction row, and
// we want to add the $delete flag so the API can delete the row in the junction table,
// effectively deselecting the related item from this item
if (primaryKey) {
// Once you deselect an item, it's removed from the preview table. You can only
// deselect an item once, so we don't have to check if this item was already disabled
emit('input', [
...(props.value || []),
{
[junctionCollectionPrimaryKeyField.value.field]: primaryKey,
$delete: true,
},
]);
return;
}
// If the item doesn't exist yet, there must be a staged edit for it's creation, that's
// the thing we want to filter out of the staged edits.
emit(
'input',
props.value.filter((stagedValue) => {
return stagedValue !== junctionRow && stagedValue !== junctionRow['$stagedEdits'];
})
);
}
function useUpload() {
const showUpload = ref(false);
return { showUpload, onUpload };
function onUpload(file: { id: number; [key: string]: any }) {
if (!relationCurrentToJunction.value) return;
if (!relationCurrentToJunction.value.junction_field) return;
const { junctionRelation } = relationFields.value;
const fileAsJunctionRow = {
[relationCurrentToJunction.value.junction_field]: {
[junctionRelation]: {
id: file.id,
},
};

View File

@@ -1,141 +0,0 @@
import { ref, Ref } from '@vue/composition-api';
import { Field, Relation } from '@/types';
import { set } from 'lodash';
type EditParam = {
relationCurrentToJunction: Ref<Relation | undefined>;
junctionCollectionPrimaryKeyField: Ref<Field>;
relatedCollectionPrimaryKeyField: Ref<Field>;
value: Ref<any[] | null>;
onEdit: (newValue: any[] | null) => void;
};
/**
* Everything regarding the edit experience in the detail modal. This also includes adding
* a new item
*/
export default function useEdit({
relationCurrentToJunction,
junctionCollectionPrimaryKeyField,
relatedCollectionPrimaryKeyField,
value,
onEdit,
}: EditParam) {
const showDetailModal = ref(false);
// The previously made edits when we're starting to edit the item
const editsAtStart = ref<any>(null);
const junctionRowPrimaryKey = ref<number | string>('+');
const relatedRowPrimaryKey = ref<number | string>('+');
const initialValues = ref<any>(null);
const isNew = ref(false);
return {
showDetailModal,
editsAtStart,
addNew,
cancelEdit,
stageEdits,
junctionRowPrimaryKey,
editExisting,
relatedRowPrimaryKey,
initialValues,
};
function addNew() {
editsAtStart.value = null;
showDetailModal.value = true;
junctionRowPrimaryKey.value = '+';
relatedRowPrimaryKey.value = '+';
initialValues.value = null;
isNew.value = true;
}
// The row here is the item in previewItems that's passed to the table
function editExisting(item: any) {
if (!relationCurrentToJunction.value) return;
if (!relationCurrentToJunction.value.junction_field) return;
if (item.$new === true) isNew.value = true;
if (isNew.value === true) {
editsAtStart.value = item;
junctionRowPrimaryKey.value = '+';
showDetailModal.value = true;
initialValues.value = null;
return;
}
initialValues.value = item;
/**
* @NOTE: Keep in mind there's a case where the junction row doesn't exist yet, but
* the related item does (when selecting an existing item)
*/
const junctionPrimaryKey = junctionCollectionPrimaryKeyField.value.field;
const junctionField = relationCurrentToJunction.value.junction_field;
const relatedPrimaryKey = relatedCollectionPrimaryKeyField.value.field;
junctionRowPrimaryKey.value = item[junctionPrimaryKey] || '+';
relatedRowPrimaryKey.value = item[junctionField]?.[relatedPrimaryKey] || '+';
editsAtStart.value = item.$stagedEdits || null;
showDetailModal.value = true;
}
function cancelEdit() {
editsAtStart.value = {};
showDetailModal.value = false;
junctionRowPrimaryKey.value = '+';
}
function stageEdits(edits: any) {
if (!relationCurrentToJunction.value) return;
if (!relationCurrentToJunction.value.junction_field) return;
const junctionPrimaryKey = junctionCollectionPrimaryKeyField.value.field;
const junctionField = relationCurrentToJunction.value.junction_field;
const relatedPrimaryKey = relatedCollectionPrimaryKeyField.value.field;
if (isNew.value) {
edits.$new = true;
}
const currentValue = [...(value.value || [])];
// If there weren't any previously made edits, it's safe to assume this change value
// doesn't exist yet in the staged value
if (!editsAtStart.value) {
// If the item that we edited has any of the primary keys (junction/related), we
// have to make sure we stage those as well. Otherwise the API will treat it as
// a newly created item instead of updated existing
if (junctionRowPrimaryKey.value !== '+') {
set(edits, junctionPrimaryKey, junctionRowPrimaryKey.value);
}
if (relatedRowPrimaryKey.value !== '+') {
set(edits, [junctionField, relatedPrimaryKey], relatedRowPrimaryKey.value);
}
onEdit([...currentValue, edits]);
reset();
return;
}
const newValue =
value.value?.map((stagedValue: any) => {
if (stagedValue === editsAtStart.value) return edits;
return stagedValue;
}) || null;
onEdit(newValue);
reset();
function reset() {
editsAtStart.value = null;
showDetailModal.value = true;
junctionRowPrimaryKey.value = '+';
relatedRowPrimaryKey.value = '+';
isNew.value = false;
}
}
}

View File

@@ -1,235 +0,0 @@
import { Ref, ref, watch } from '@vue/composition-api';
import api from '@/api';
import { Field, Relation } from '@/types';
import { merge } from 'lodash';
import adjustFieldsForDisplay from '@/utils/adjust-fields-for-displays';
/**
* Controls what preview is shown in the table. Has some black magic logic to ensure we're able
* to show the latest edits, while also maintaining a clean staged value set. This is not responsible
* for setting or modifying any data. Preview items should be considered read only
*/
type PreviewParam = {
value: Ref<any[] | null>;
primaryKey: Ref<string | number>;
junctionCollectionPrimaryKeyField: Ref<Field>;
relatedCollectionPrimaryKeyField: Ref<Field>;
junctionCollection: Ref<string>;
relatedCollection: Ref<string>;
relationCurrentToJunction: Ref<Relation | undefined>;
relationJunctionToRelated: Ref<Relation | null | undefined>;
fields: Ref<readonly string[]>;
};
export default function usePreview({
value,
primaryKey,
junctionCollectionPrimaryKeyField,
relatedCollectionPrimaryKeyField,
relationCurrentToJunction,
relationJunctionToRelated,
junctionCollection,
relatedCollection,
fields,
}: PreviewParam) {
const loading = ref(false);
const previewItems = ref<readonly any[]>([]);
const error = ref(null);
// Every time the value changes, we'll reset the preview values. This ensures that we'll
// almost show the most up to date information in the preview table, regardless of if this
// is the first load or a subsequent edit.
watch(value, setPreview, { immediate: true });
return { loading, previewItems, error };
async function setPreview() {
loading.value = true;
try {
const existingItems = await fetchExisting();
const updatedExistingItems = applyUpdatesToExisting(existingItems);
const newlyAddedItems = getNewlyAdded();
const newlySelectedItems = await fetchNewlySelectedItems();
previewItems.value = [...updatedExistingItems, ...newlyAddedItems, ...newlySelectedItems].filter(
(stagedEdit: any) => !stagedEdit['$delete']
);
} catch (err) {
error.value = err;
throw err;
} finally {
loading.value = false;
}
}
/**
* Looks through props.value and applies all staged changes to the existing selected
* items. The array of existing items is an array of junction rows, so we can assume
* those have a primary key
*/
function applyUpdatesToExisting(existing: any[]) {
return existing.map((existingValue) => {
const junctionPrimaryKey = junctionCollectionPrimaryKeyField.value.field;
const existingPrimaryKey = existingValue[junctionPrimaryKey];
const stagedEdits: any = (value.value || []).find((update: any) => {
const updatePrimaryKey = update[junctionPrimaryKey];
return existingPrimaryKey === updatePrimaryKey;
});
if (stagedEdits === undefined) return existingValue;
return {
...merge(existingValue, stagedEdits),
$stagedEdits: stagedEdits,
};
});
}
/**
* To get the currently selected items, we'll fetch the rows from the junction table
* where the field back to the current collection is equal to the primary key. We go
* this route as it's more performant than trying to go an extra level deep in the
* current item.
*/
async function fetchExisting() {
if (!relationCurrentToJunction.value) return;
if (!relationCurrentToJunction.value.junction_field) return;
if (!relationJunctionToRelated.value) return;
if (!relationJunctionToRelated.value.junction_field) return;
// If the current item is being created, we don't have to search for existing relations
// yet, as they can't have been saved yet.
if (primaryKey.value === '+') return [];
const junctionTable = relationCurrentToJunction.value.many_collection;
// The stuff we want to fetch is the related junction row, and the content of the
// deeply related item nested. This should match the value that's set in the fields
// option. We have to make sure we're fetching the primary key of both the junction
// as the related item though, as that makes sure we're able to update the item later,
// instead of adding a new one in the API.
const fieldsToFetch = [...fields.value];
// The following will add the PK and related items PK to the request fields, like
// "id" and "related.id"
const junctionPrimaryKey = junctionCollectionPrimaryKeyField.value.field;
const junctionField = relationCurrentToJunction.value.junction_field;
const relatedPrimaryKey = relatedCollectionPrimaryKeyField.value.field;
const currentInJunction = relationJunctionToRelated.value.junction_field;
if (fieldsToFetch.includes(junctionPrimaryKey) === false) fieldsToFetch.push(junctionPrimaryKey);
if (fieldsToFetch.includes(`${junctionField}.${relatedPrimaryKey}`) === false)
fieldsToFetch.push(`${junctionField}.${relatedPrimaryKey}`);
const response = await api.get(`/items/${junctionTable}`, {
params: {
fields: adjustFieldsForDisplay(fieldsToFetch, junctionCollection.value),
[`filter[${currentInJunction}][_eq]`]: primaryKey.value,
},
});
return response.data.data;
}
/**
* Extract the newly created rows from props.value. Values that don't have a junction row
* primary key and no primary key in the related item are created "totally" new and should
* be added to the array of previews as is.
* NOTE: This does not included items where the junction row is new, but the related item
* isn't.
*/
function getNewlyAdded() {
if (!relationCurrentToJunction.value) return [];
if (!relationCurrentToJunction.value.junction_field) return [];
/**
* @NOTE There's an interesting case here:
*
* If you create both a new junction row _and_ a new related row, any selected existing
* many to one record won't have it's data object staged, as it already exists (so it's just)
* the primary key. This will case a template display to show ???, as it only gets the
* primary key. If you saw an issue about that on GitHub, this is where to find it.
*
* Unlike in fetchNewlySelectedItems(), we can't just fetch the related item, as both
* junction and related are new. We _could_ traverse through the object of changes, see
* if there's any relational field, and fetch the data based on that combined with the
* fields adjusted for the display. While that should work, it's too much of an edge case
* for me for now to worry about..
*/
const junctionPrimaryKey = junctionCollectionPrimaryKeyField.value.field;
return (value.value || []).filter(
(stagedEdit: any) => !stagedEdit.$delete && !stagedEdit[junctionPrimaryKey] && stagedEdit.$new === true
);
}
/**
* The tricky case where the user selects an existing item from the related collection
* This means the junction doesn't have a primary key yet, and the only value that is
* staged is the related item's primary key
* In this function, we'll fetch the full existing item from the related collection,
* so we can still show it's data in the preview table
*/
async function fetchNewlySelectedItems() {
if (!relationCurrentToJunction.value) return [];
if (!relationCurrentToJunction.value.junction_field) return [];
if (!relationJunctionToRelated.value) return [];
if (!relationJunctionToRelated.value.junction_field) return [];
const junctionPrimaryKey = junctionCollectionPrimaryKeyField.value.field;
const junctionField = relationCurrentToJunction.value.junction_field;
const relatedPrimaryKey = relatedCollectionPrimaryKeyField.value.field;
const newlySelectedStagedItems = (value.value || []).filter(
(stagedEdit: any) => !stagedEdit.$delete && !stagedEdit[junctionPrimaryKey] && !stagedEdit.$new
);
const newlySelectedRelatedKeys = newlySelectedStagedItems.map(
(stagedEdit: any) => stagedEdit[junctionField][relatedPrimaryKey]
);
// If there's no newly selected related items, we can return here, as there's nothing
// to fetch
if (newlySelectedRelatedKeys.length === 0) return [];
// The fields option are set from the viewport of the junction table. Seeing we only
// fetch from the related table, we have to filter out all the fields from the junction
// table and remove the junction field prefix from the related table columns
const fieldsToFetch = fields.value
.filter((field) => field.startsWith(junctionField))
.map((relatedField) => {
return relatedField.replace(junctionField + '.', '');
});
if (fieldsToFetch.includes(relatedPrimaryKey) === false) fieldsToFetch.push(relatedPrimaryKey);
const endpoint = relatedCollection.value.startsWith('directus_')
? `/${relatedCollection.value.substring(9)}/${newlySelectedRelatedKeys.join(',')}`
: `/items/${relatedCollection.value}/${newlySelectedRelatedKeys.join(',')}`;
const response = await api.get(endpoint, {
params: {
fields: adjustFieldsForDisplay(fieldsToFetch, junctionCollection.value),
},
});
const data = Array.isArray(response.data.data) ? response.data.data : [response.data.data];
return newlySelectedStagedItems.map((stagedEdit: any) => {
const pk = stagedEdit[junctionField][relatedPrimaryKey];
const relatedItem = data.find((relatedItem: any) => relatedItem[relatedPrimaryKey] === pk);
return merge(
{
[junctionField]: relatedItem,
$stagedEdits: stagedEdit,
},
stagedEdit
);
});
}
}

View File

@@ -1,48 +0,0 @@
import { Ref, computed } from '@vue/composition-api';
import useCollection from '@/composables/use-collection';
import { Relation } from '@/types/';
import { useRelationsStore } from '@/stores/';
type RelationParams = {
collection: Ref<string>;
field: Ref<string>;
};
export default function useRelation({ collection, field }: RelationParams) {
const relationsStore = useRelationsStore();
// We expect two relations to exist for this field: one from this field to the junction
// table, and one from the junction table to the related collection
const relations = computed<Relation[]>(() => {
return relationsStore.getRelationsForField(collection.value, field.value);
});
const relationCurrentToJunction = computed(() => {
return relations.value.find(
(relation: Relation) => relation.one_collection === collection.value && relation.one_field === field.value
);
});
const relationJunctionToRelated = computed(() => {
if (!relationCurrentToJunction.value) return null;
const index = relations.value.indexOf(relationCurrentToJunction.value) === 1 ? 0 : 1;
return relations.value[index];
});
const junctionCollection = computed(() => relations.value[0].many_collection);
const relatedCollection = computed(() => relations.value[1].one_collection);
const { primaryKeyField: junctionCollectionPrimaryKeyField } = useCollection(junctionCollection);
const { primaryKeyField: relatedCollectionPrimaryKeyField } = useCollection(relatedCollection);
return {
relations,
relationCurrentToJunction,
relationJunctionToRelated,
junctionCollection,
junctionCollectionPrimaryKeyField,
relatedCollection,
relatedCollectionPrimaryKeyField,
};
}

View File

@@ -1,77 +0,0 @@
import { Relation, Filter } from '@/types/';
import { Field } from '@/types';
import { Ref, ref, computed } from '@vue/composition-api';
type SelectionParam = {
relationCurrentToJunction: Ref<Relation | undefined>;
relatedCollectionPrimaryKeyField: Ref<Field>;
previewItems: Ref<readonly any[]>;
onStageSelection: (selectionAsJunctionRows: any[]) => void;
};
export default function useSelection({
relationCurrentToJunction,
relatedCollectionPrimaryKeyField,
previewItems,
onStageSelection,
}: SelectionParam) {
const showBrowseModal = ref(false);
const alreadySelectedRelatedPrimaryKeys = computed(() => {
if (!relationCurrentToJunction.value) return [];
if (!relationCurrentToJunction.value.junction_field) return [];
const junctionField = relationCurrentToJunction.value.junction_field;
const relatedPrimaryKey = relatedCollectionPrimaryKeyField.value.field;
return previewItems.value
.filter((previewItem: any) => previewItem[junctionField])
.map((previewItem: any) => {
if (typeof previewItem[junctionField] === 'string' || typeof previewItem[junctionField] === 'number') {
return previewItem[junctionField];
}
return previewItem[junctionField][relatedPrimaryKey];
})
.filter((p) => p);
});
const selectionFilters = computed<Filter[]>(() => {
const relatedPrimaryKey = relatedCollectionPrimaryKeyField.value.field;
const filter: Filter = {
key: 'selection',
field: relatedPrimaryKey,
operator: 'nin',
value: alreadySelectedRelatedPrimaryKeys.value.join(','),
locked: true,
};
return [filter];
});
return { showBrowseModal, stageSelection, selectionFilters };
function stageSelection(selection: any) {
const selectionAsJunctionRows = selection.map((relatedPrimaryKey: string | number) => {
if (!relationCurrentToJunction.value) return;
if (!relationCurrentToJunction.value.junction_field) return;
const junctionField = relationCurrentToJunction.value.junction_field;
const relatedPrimaryKeyField = relatedCollectionPrimaryKeyField.value.field;
return {
[junctionField]: {
// Technically, "junctionField: primaryKey" should be enough for the api
// to do it's thing for newly selected items. However, that would require
// the previewItems check to be way more complex. This shouldn't introduce
// too much overhead in the API, while drastically simplifying this interface
[relatedPrimaryKeyField]: relatedPrimaryKey,
},
};
});
// Seeing the browse modal only shows items that haven't been selected yet (using the
// filter above), we can safely assume that the items don't exist yet in props.value
onStageSelection(selectionAsJunctionRows);
}
}

View File

@@ -43,8 +43,6 @@
</v-button>
</div>
<pre>{{ JSON.stringify(value, null, 4) }}</pre>
<modal-detail
v-if="!disabled"
:active="currentlyEditing !== null"
@@ -59,8 +57,8 @@
v-if="!disabled"
:active.sync="selectModalActive"
:collection="relationCollection.collection"
:selection="selectedPrimaryKeys"
:filters="[]"
:selection="[]"
:filters="selectionFilters"
@input="stageSelection"
multiple
/>
@@ -146,14 +144,11 @@ export default defineComponent({
getJunctionFromRelatedId
);
const { stageSelection, selectModalActive, selectedPrimaryKeys } = useSelection(
items,
const { stageSelection, selectModalActive, selectionFilters } = useSelection(
value,
displayItems,
relationFields,
emitter,
getNewItems,
getJunctionFromRelatedId,
getJunctionItem
emitter
);
return {
@@ -172,7 +167,7 @@ export default defineComponent({
selectModalActive,
deleteItem,
displayItems,
selectedPrimaryKeys,
selectionFilters,
items,
};
},

View File

@@ -24,7 +24,7 @@ export default function useActions(
if (value.value === null || junctionRelation === null) return [];
return value.value.filter(
(item) => typeof item === 'object' && junctionRelation in item && typeof junctionRelation !== 'object'
(item) => typeof item === 'object' && junctionRelation in item && typeof item[junctionRelation] !== 'object'
) as Record<string, any>[];
}

View File

@@ -37,7 +37,7 @@ export default function usePreview(
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 junctionItems = await loadRelatedIds();
const relatedPrimaryKeys = junctionItems.map((junction) => junction[junctionRelation]);
const filteredFields = [...(fields.value.length > 0 ? fields.value : getDefaultFields())];
@@ -92,8 +92,8 @@ export default function usePreview(
{ immediate: true }
);
async function loadRelatedIds(newVal: (string | number | Record<string, any>)[]) {
const { junctionPkField } = relation.value;
async function loadRelatedIds() {
const { junctionPkField, junctionRelation, relationPkField } = relation.value;
try {
const endpoint = relation.value.junctionCollection.startsWith('directus_')
@@ -107,8 +107,12 @@ export default function usePreview(
});
const data = response.data.data as Record<string, any>[];
const updatedItems = getUpdatedItems().map((item) => ({
[junctionRelation]: item[junctionRelation][relationPkField],
}));
// Add all items that already had the id of it's related item
return data.concat(...getNewSelectedItems());
return data.concat(...getNewSelectedItems(), ...updatedItems);
} catch (err) {
error.value = err;
}

View File

@@ -1,14 +1,12 @@
import { Ref, ref, computed } from '@vue/composition-api';
import { RelationInfo } from './use-relation';
import { Filter } from '@/types';
export default function useSelection(
items: Ref<Record<string, any>[]>,
value: Ref<(string | number | Record<string, any>)[] | null>,
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
emit: (newVal: any[] | null) => void
) {
const selectModalActive = ref(false);
@@ -24,31 +22,31 @@ export default function useSelection(
return selectedKeys;
});
const selectionFilters = computed<Filter[]>(() => {
const { relationPkField } = relation.value;
const filter: Filter = {
key: 'selection',
field: relationPkField,
operator: 'nin',
value: selectedPrimaryKeys.value.join(','),
locked: true,
};
return [filter];
});
function stageSelection(newSelection: (number | string)[]) {
const { junctionRelation, junctionPkField } = relation.value;
const newItems = getNewItems();
const { junctionRelation } = relation.value;
console.log('A');
const selection = newSelection
.filter((item) => selectedPrimaryKeys.value.includes(item) === false)
.map((item) => ({ [junctionRelation]: item }));
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];
const newVal = [...selection, ...(value.value || [])];
if (newVal.length === 0) emit(null);
else emit(newVal);
}
return { stageSelection, selectModalActive, selectedPrimaryKeys };
return { stageSelection, selectModalActive, selectedPrimaryKeys, selectionFilters };
}