Files
directus/app/src/composables/use-item.ts
Rijk van Zanten c48309ab68 Last eslint tweak (#18198)
* Should be there now

* Format
2023-04-14 17:40:50 -04:00

475 lines
13 KiB
TypeScript

import api from '@/api';
import { VALIDATION_TYPES } from '@/constants';
import { i18n } from '@/lang';
import { useFieldsStore } from '@/stores/fields';
import { useRelationsStore } from '@/stores/relations';
import { APIError } from '@/types/error';
import { notify } from '@/utils/notify';
import { translate } from '@/utils/translate-object-values';
import { unexpectedError } from '@/utils/unexpected-error';
import { validateItem } from '@/utils/validate-item';
import { useCollection } from '@directus/composables';
import { getEndpoint } from '@directus/utils';
import { AxiosResponse } from 'axios';
import { mergeWith } from 'lodash';
import { computed, ComputedRef, Ref, ref, unref, watch } from 'vue';
import { usePermissions } from './use-permissions';
import { Field, Query, Relation } from '@directus/types';
import { getDefaultValuesFromFields } from '@/utils/get-default-values-from-fields';
type UsableItem = {
edits: Ref<Record<string, any>>;
hasEdits: Ref<boolean>;
item: Ref<Record<string, any> | null>;
error: Ref<any>;
loading: Ref<boolean>;
saving: Ref<boolean>;
refresh: () => void;
save: () => Promise<any>;
isNew: ComputedRef<boolean>;
remove: () => Promise<void>;
deleting: Ref<boolean>;
archive: () => Promise<void>;
isArchived: ComputedRef<boolean | null>;
archiving: Ref<boolean>;
saveAsCopy: () => Promise<any>;
isBatch: ComputedRef<boolean>;
getItem: () => Promise<void>;
validationErrors: Ref<any[]>;
};
export function useItem(
collection: Ref<string>,
primaryKey: Ref<string | number | null>,
query: Ref<Query> | Query = {}
): UsableItem {
const { info: collectionInfo, primaryKeyField } = useCollection(collection);
const item = ref<Record<string, any> | null>(null);
const error = ref<any>(null);
const validationErrors = ref<any[]>([]);
const loading = ref(false);
const saving = ref(false);
const deleting = ref(false);
const archiving = ref(false);
const edits = ref<Record<string, any>>({});
const hasEdits = computed(() => Object.keys(edits.value).length > 0);
const isNew = computed(() => primaryKey.value === '+');
const isBatch = computed(() => typeof primaryKey.value === 'string' && primaryKey.value.includes(','));
const isSingle = computed(() => !!collectionInfo.value?.meta?.singleton);
const isArchived = computed(() => {
if (!collectionInfo.value?.meta?.archive_field) return null;
if (collectionInfo.value.meta.archive_value === 'true') {
return item.value?.[collectionInfo.value.meta.archive_field] === true;
}
return item.value?.[collectionInfo.value.meta.archive_field] === collectionInfo.value.meta.archive_value;
});
const { fields: fieldsWithPermissions } = usePermissions(collection, item, isNew);
const itemEndpoint = computed(() => {
if (isSingle.value) {
return getEndpoint(collection.value);
}
return `${getEndpoint(collection.value)}/${encodeURIComponent(primaryKey.value as string)}`;
});
const defaultValues = getDefaultValuesFromFields(fieldsWithPermissions);
watch([collection, primaryKey, query], refresh, { immediate: true });
return {
edits,
hasEdits,
item,
error,
loading,
saving,
refresh,
save,
isNew,
remove,
deleting,
archive,
isArchived,
archiving,
saveAsCopy,
isBatch,
getItem,
validationErrors,
};
async function getItem() {
loading.value = true;
error.value = null;
try {
const response = await api.get(itemEndpoint.value, { params: unref(query) });
setItemValueToResponse(response);
} catch (err: any) {
error.value = err;
} finally {
loading.value = false;
}
}
async function save() {
saving.value = true;
validationErrors.value = [];
const payloadToValidate = mergeWith(
{},
defaultValues.value,
item.value,
edits.value,
function (from: any, to: any) {
if (typeof to !== 'undefined') {
return to;
}
}
);
const errors = validateItem(payloadToValidate, fieldsWithPermissions.value, isNew.value);
if (errors.length > 0) {
validationErrors.value = errors;
saving.value = false;
throw errors;
}
try {
let response;
if (isNew.value === true) {
response = await api.post(getEndpoint(collection.value), edits.value);
notify({
title: i18n.global.t('item_create_success', isBatch.value ? 2 : 1),
});
} else {
response = await api.patch(itemEndpoint.value, edits.value);
notify({
title: i18n.global.t('item_update_success', isBatch.value ? 2 : 1),
});
}
setItemValueToResponse(response);
edits.value = {};
return response.data.data;
} catch (err: any) {
saveErrorHandler(err);
} finally {
saving.value = false;
}
}
async function saveAsCopy() {
saving.value = true;
validationErrors.value = [];
const fields = collectionInfo.value?.meta?.item_duplication_fields || ['*'];
const itemData = await api.get(itemEndpoint.value, { params: { fields } });
const newItem: { [field: string]: any } = {
...(itemData.data.data || {}),
...edits.value,
};
// Make sure to delete the primary key if it's has auto increment enabled
if (primaryKeyField.value && primaryKeyField.value.field in newItem) {
if (primaryKeyField.value.schema?.has_auto_increment || primaryKeyField.value.meta?.special?.includes('uuid')) {
delete newItem[primaryKeyField.value.field];
}
}
// Make sure to delete nested relational primary keys
const fieldsStore = useFieldsStore();
const relationsStore = useRelationsStore();
const relations = relationsStore.getRelationsForCollection(collection.value);
for (const relation of relations) {
const relatedPrimaryKeyField = fieldsStore.getPrimaryKeyFieldForCollection(relation.collection);
const existsJunctionRelated = relationsStore.relations.find((r) => {
return r.collection === relation.collection && r.meta?.many_field === relation.meta?.junction_field;
});
if (relation.meta?.one_field && relation.meta.one_field in newItem) {
const fieldsToFetch = fields
.filter((field) => field.startsWith(relation.meta!.one_field!))
.map((field) => field.split('.').slice(1).join('.'));
if (Array.isArray(newItem[relation.meta.one_field])) {
const existingItems = await findExistingRelatedItems(
newItem,
relation,
relatedPrimaryKeyField,
fieldsToFetch
);
newItem[relation.meta.one_field] = newItem[relation.meta.one_field].map((relatedItem: any) => {
if (typeof relatedItem !== 'object' && existingItems.length > 0) {
relatedItem = existingItems.find((existingItem: any) => existingItem.id === relatedItem);
}
delete relatedItem[relatedPrimaryKeyField!.field];
updateJunctionRelatedKey(relation, existsJunctionRelated, fieldsStore, relatedItem);
return relatedItem;
});
} else {
const createdRelatedItems = newItem[relation.meta.one_field]?.create;
const updatedRelatedItems = newItem[relation.meta.one_field]?.update;
const deletedRelatedItems = newItem[relation.meta.one_field]?.delete;
let existingItems: any[] = await findExistingRelatedItems(
item.value,
relation,
relatedPrimaryKeyField,
fieldsToFetch
);
existingItems = existingItems.filter((i) => {
return deletedRelatedItems.indexOf(i[relatedPrimaryKeyField!.field]) === -1;
});
for (const item of updatedRelatedItems) {
updateJunctionRelatedKey(relation, existsJunctionRelated, fieldsStore, item);
}
for (const item of existingItems) {
updateExistingRelatedItems(updatedRelatedItems, item, relatedPrimaryKeyField, relation);
}
updatedRelatedItems.length = 0;
for (const item of existingItems) {
delete item[relatedPrimaryKeyField!.field];
createdRelatedItems.push(item);
}
}
}
}
const errors = validateItem(newItem, fieldsWithPermissions.value, isNew.value);
if (errors.length > 0) {
validationErrors.value = errors;
saving.value = false;
throw errors;
}
try {
const response = await api.post(getEndpoint(collection.value), newItem);
notify({
title: i18n.global.t('item_create_success', 1),
});
// Reset edits to the current item
edits.value = {};
return primaryKeyField.value ? response.data.data[primaryKeyField.value.field] : null;
} catch (err: any) {
saveErrorHandler(err);
} finally {
saving.value = false;
}
async function findExistingRelatedItems(
item: any,
relation: Relation,
relatedPrimaryKeyField: Field | null,
fieldsToFetch: string[]
) {
const existingIds = item?.[relation.meta!.one_field!].filter((item: any) => typeof item !== 'object');
let existingItems: any[] = [];
if (existingIds.length > 0) {
const response = await api.get(getEndpoint(relation.collection), {
params: {
fields: [relatedPrimaryKeyField!.field, ...fieldsToFetch],
[`filter[${relatedPrimaryKeyField!.field}][_in]`]: existingIds.join(','),
},
});
existingItems = response.data.data;
}
return existingItems;
}
function updateExistingRelatedItems(
updatedRelatedItems: any,
item: any,
relatedPrimaryKeyField: Field | null,
relation: Relation
) {
for (const updatedItem of updatedRelatedItems) {
copyUserEditValuesToExistingItem(item, relatedPrimaryKeyField, updatedItem, relation);
}
}
function copyUserEditValuesToExistingItem(
item: any,
relatedPrimaryKeyField: Field | null,
updatedItem: any,
relation: Relation
) {
if (item[relatedPrimaryKeyField!.field] === updatedItem[relatedPrimaryKeyField!.field]) {
const columns = fields.filter((s) => s.startsWith(relation.meta!.one_field!));
for (const col of columns) {
const colName = col.split('.')[1];
item[colName] = updatedItem[colName];
}
}
}
}
function updateJunctionRelatedKey(
relation: Relation,
existsJunctionRelated: Relation | undefined,
fieldsStore: any,
item: any
) {
if (relation.meta?.junction_field && existsJunctionRelated?.related_collection) {
const junctionRelatedPrimaryKeyField = fieldsStore.getPrimaryKeyFieldForCollection(
existsJunctionRelated.related_collection
);
if (relation.meta.junction_field in item && junctionRelatedPrimaryKeyField.schema!.is_generated) {
delete item[relation.meta.junction_field][junctionRelatedPrimaryKeyField!.field];
}
}
}
function saveErrorHandler(err: any) {
if (err?.response?.data?.errors) {
validationErrors.value = err.response.data.errors
.filter((err: APIError) => VALIDATION_TYPES.includes(err?.extensions?.code))
.map((err: APIError) => {
return err.extensions;
});
const otherErrors = err.response.data.errors.filter(
(err: APIError) => VALIDATION_TYPES.includes(err?.extensions?.code) === false
);
if (otherErrors.length > 0) {
otherErrors.forEach(unexpectedError);
}
} else {
unexpectedError(err);
}
throw err;
}
async function archive() {
if (!collectionInfo.value?.meta?.archive_field) return;
archiving.value = true;
const field = collectionInfo.value.meta.archive_field;
let archiveValue: any = collectionInfo.value.meta.archive_value;
if (archiveValue === 'true') archiveValue = true;
if (archiveValue === 'false') archiveValue = false;
let unarchiveValue: any = collectionInfo.value.meta.unarchive_value;
if (unarchiveValue === 'true') unarchiveValue = true;
if (unarchiveValue === 'false') unarchiveValue = false;
try {
let value: any = item.value && item.value[field] === archiveValue ? unarchiveValue : archiveValue;
if (value === 'true') value = true;
if (value === 'false') value = false;
await api.patch(itemEndpoint.value, {
[field]: value,
});
item.value = {
...item.value,
[field]: value,
};
notify({
title:
value === archiveValue
? i18n.global.t('item_delete_success', isBatch.value ? 2 : 1)
: i18n.global.t('item_update_success', isBatch.value ? 2 : 1),
});
} catch (err: any) {
unexpectedError(err);
throw err;
} finally {
archiving.value = false;
}
}
async function remove() {
deleting.value = true;
try {
await api.delete(itemEndpoint.value);
item.value = null;
notify({
title: i18n.global.t('item_delete_success', isBatch.value ? 2 : 1),
});
} catch (err: any) {
unexpectedError(err);
throw err;
} finally {
deleting.value = false;
}
}
function refresh() {
error.value = null;
loading.value = false;
saving.value = false;
deleting.value = false;
if (isNew.value === true) {
item.value = null;
} else {
getItem();
}
}
function setItemValueToResponse(response: AxiosResponse) {
if (
(collection.value.startsWith('directus_') && collection.value !== 'directus_collections') ||
(collection.value === 'directus_collections' && response.data.data.collection?.startsWith('directus_'))
) {
response.data.data = translate(response.data.data);
}
if (isBatch.value === false) {
item.value = response.data.data;
} else {
const valuesThatAreEqual = { ...response.data.data[0] };
response.data.data.forEach((existingItem: any) => {
for (const [key, value] of Object.entries(existingItem)) {
if (valuesThatAreEqual[key] !== value) {
delete valuesThatAreEqual[key];
}
}
});
item.value = valuesThatAreEqual;
}
}
}