Merge pull request #759 from directus/error-handling

Add error handling
This commit is contained in:
Rijk van Zanten
2020-11-05 23:31:30 +01:00
committed by GitHub
66 changed files with 461 additions and 425 deletions

View File

@@ -14,6 +14,7 @@
<script lang="ts">
import { defineComponent, computed, PropType, ref } from '@vue/composition-api';
import { isPlainObject } from 'lodash';
export default defineComponent({
props: {
@@ -35,7 +36,9 @@ export default defineComponent({
async function copyError() {
const error = props.error?.response?.data || props.error;
await navigator.clipboard.writeText(JSON.stringify(error, null, 2));
await navigator.clipboard.writeText(
JSON.stringify(error, isPlainObject(error) ? null : Object.getOwnPropertyNames(error), 2)
);
copied.value = true;
}
},

View File

@@ -92,6 +92,7 @@ import uploadFile from '@/utils/upload-file';
import DrawerCollection from '@/views/private/components/drawer-collection';
import api from '@/api';
import useItem from '@/composables/use-item';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
components: { DrawerCollection },
@@ -118,16 +119,15 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { uploading, progress, error, upload, onBrowseSelect, done, numberOfFiles } = useUpload();
const { uploading, progress, upload, onBrowseSelect, done, numberOfFiles } = useUpload();
const { onDragEnter, onDragLeave, onDrop, dragging } = useDragging();
const { url, isValidURL, loading: urlLoading, error: urlError, importFromURL } = useURLImport();
const { url, isValidURL, loading: urlLoading, importFromURL } = useURLImport();
const { setSelection } = useSelection();
const activeDialog = ref<'choose' | 'url' | null>(null);
return {
uploading,
progress,
error,
onDragEnter,
onDragLeave,
onDrop,
@@ -148,14 +148,12 @@ export default defineComponent({
const progress = ref(0);
const numberOfFiles = ref(0);
const done = ref(0);
const error = ref(null);
return { uploading, progress, error, upload, onBrowseSelect, numberOfFiles, done };
return { uploading, progress, upload, onBrowseSelect, numberOfFiles, done };
async function upload(files: FileList) {
uploading.value = true;
progress.value = 0;
error.value = null;
try {
numberOfFiles.value = files.length;
@@ -185,8 +183,7 @@ export default defineComponent({
uploadedFile && emit('input', uploadedFile);
}
} catch (err) {
console.error(err);
error.value = err;
unexpectedError(err);
} finally {
uploading.value = false;
done.value = 0;
@@ -255,7 +252,6 @@ export default defineComponent({
function useURLImport() {
const url = ref('');
const loading = ref(false);
const error = ref(null);
const isValidURL = computed(() => {
try {
@@ -266,7 +262,7 @@ export default defineComponent({
}
});
return { url, loading, error, isValidURL, importFromURL };
return { url, loading, isValidURL, importFromURL };
async function importFromURL() {
loading.value = true;
@@ -280,7 +276,7 @@ export default defineComponent({
activeDialog.value = null;
url.value = '';
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -1,10 +1,11 @@
import api from '@/api';
import { Ref, ref, watch, computed } from '@vue/composition-api';
import notify from '@/utils/notify';
import i18n from '@/lang';
import useCollection from '@/composables/use-collection';
import { AxiosResponse } from 'axios';
import { APIError } from '@/types';
import { notify } from '@/utils/notify';
import { unexpectedError } from '@/utils/unexpected-error';
export function useItem(collection: Ref<string>, primaryKey: Ref<string | number | null>) {
const { info: collectionInfo, primaryKeyField } = useCollection(collection);
@@ -108,33 +109,15 @@ export function useItem(collection: Ref<string>, primaryKey: Ref<string | number
edits.value = {};
return response.data.data;
} catch (err) {
if (isNew.value) {
notify({
title: i18n.tc('item_create_failed', isBatch.value ? 2 : 1),
type: 'error',
});
} else {
notify({
title: i18n.tc('item_update_failed', isBatch.value ? 2 : 1),
text: i18n.tc('item_in', isBatch.value ? 2 : 1, {
collection: collection.value,
primaryKey: isBatch.value
? (primaryKey.value as string).split(',').join(', ')
: primaryKey.value,
}),
type: 'error',
});
}
if (err?.response?.data?.errors) {
validationErrors.value = err.response.data.errors
.filter((err: APIError) => err.extensions.code === 'FAILED_VALIDATION')
.filter((err: APIError) => err?.extensions?.code === 'FAILED_VALIDATION')
.map((err: APIError) => {
return err.extensions;
});
} else {
unexpectedError(err);
}
throw err;
} finally {
saving.value = false;
}
@@ -164,23 +147,14 @@ export function useItem(collection: Ref<string>, primaryKey: Ref<string | number
return primaryKeyField.value ? response.data.data[primaryKeyField.value.field] : null;
} catch (err) {
notify({
title: i18n.t('item_create_failed'),
text: i18n.tc('item_in', isBatch.value ? 2 : 1, {
collection: collection.value,
primaryKey: isBatch.value ? (primaryKey.value as string).split(',').join(', ') : primaryKey.value,
}),
type: 'error',
});
if (err?.response?.data?.errors) {
validationErrors.value = err.response.data.errors
.filter((err: APIError) => err.extensions.code === 'FAILED_VALIDATION')
.filter((err: APIError) => err?.extensions?.code === 'FAILED_VALIDATION')
.map((err: APIError) => {
return err.extensions;
});
} else {
throw err;
unexpectedError(err);
}
} finally {
saving.value = false;
@@ -222,16 +196,7 @@ export function useItem(collection: Ref<string>, primaryKey: Ref<string | number
type: 'success',
});
} catch (err) {
notify({
title: i18n.tc('item_delete_failed', isBatch.value ? 2 : 1),
text: i18n.tc('item_in', isBatch.value ? 2 : 1, {
collection: collection.value,
primaryKey: isBatch.value ? (primaryKey.value as string).split(',').join(', ') : primaryKey.value,
}),
type: 'error',
});
throw err;
unexpectedError(err);
} finally {
archiving.value = false;
}
@@ -250,16 +215,7 @@ export function useItem(collection: Ref<string>, primaryKey: Ref<string | number
type: 'success',
});
} catch (err) {
notify({
title: i18n.tc('item_delete_failed', isBatch.value ? 2 : 1),
text: i18n.tc('item_in', isBatch.value ? 2 : 1, {
collection: collection.value,
primaryKey: isBatch.value ? (primaryKey.value as string).split(',').join(', ') : primaryKey.value,
}),
type: 'error',
});
throw err;
unexpectedError(err);
} finally {
deleting.value = false;
}

View File

@@ -17,19 +17,23 @@ export async function registerDisplays() {
try {
const customResponse = await api.get('/extensions/displays');
if (customResponse.data.data && Array.isArray(customResponse.data.data) && customResponse.data.data.length > 0) {
if (
customResponse.data.data &&
Array.isArray(customResponse.data.data) &&
customResponse.data.data.length > 0
) {
for (const customKey of customResponse.data.data) {
try {
const module = await import(/* webpackIgnore: true */ `/extensions/displays/${customKey}/index.js`);
modules.push(module.default);
} catch (err) {
console.error(`Couldn't load custom displays "${customKey}"`);
console.error(err);
console.warn(`Couldn't load custom displays "${customKey}"`);
console.warn(err);
}
}
}
} catch {
console.error(`Couldn't load custom displays`);
console.warn(`Couldn't load custom displays`);
}
displays.value = modules;

View File

@@ -116,6 +116,7 @@ import DrawerCollection from '@/views/private/components/drawer-collection';
import api from '@/api';
import readableMimeType from '@/utils/readable-mime-type';
import getRootPath from '@/utils/get-root-path';
import { unexpectedError } from '@/utils/unexpected-error';
type FileInfo = {
id: number;
@@ -137,7 +138,7 @@ export default defineComponent({
},
setup(props, { emit }) {
const activeDialog = ref<'upload' | 'choose' | 'url' | null>(null);
const { loading, error, file, fetchFile } = useFile();
const { loading, file, fetchFile } = useFile();
watch(() => props.value, fetchFile, { immediate: true });
@@ -155,20 +156,18 @@ export default defineComponent({
return assetURL.value + `?key=system-small-cover`;
});
const { url, isValidURL, loading: urlLoading, error: urlError, importFromURL } = useURLImport();
const { url, isValidURL, loading: urlLoading, importFromURL } = useURLImport();
return {
activeDialog,
setSelection,
loading,
error,
file,
fileExtension,
imageThumbnail,
onUpload,
url,
urlLoading,
urlError,
importFromURL,
isValidURL,
assetURL,
@@ -176,16 +175,14 @@ export default defineComponent({
function useFile() {
const loading = ref(false);
const error = ref(null);
const file = ref<FileInfo | null>(null);
return { loading, error, file, fetchFile };
return { loading, file, fetchFile };
async function fetchFile() {
if (props.value === null) {
file.value = null;
loading.value = false;
error.value = null;
return;
}
@@ -200,7 +197,7 @@ export default defineComponent({
file.value = response.data.data;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}
@@ -224,7 +221,6 @@ export default defineComponent({
function useURLImport() {
const url = ref('');
const loading = ref(false);
const error = ref(null);
const isValidURL = computed(() => {
try {
@@ -235,7 +231,7 @@ export default defineComponent({
}
});
return { url, loading, error, isValidURL, importFromURL };
return { url, loading, isValidURL, importFromURL };
async function importFromURL() {
loading.value = true;
@@ -251,7 +247,7 @@ export default defineComponent({
url.value = '';
emit('input', file.value?.id);
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -56,9 +56,9 @@ import formatFilesize from '@/utils/format-filesize';
import i18n from '@/lang';
import FileLightbox from '@/views/private/components/file-lightbox';
import ImageEditor from '@/views/private/components/image-editor';
import { nanoid } from 'nanoid';
import getRootPath from '@/utils/get-root-path';
import { unexpectedError } from '@/utils/unexpected-error';
type Image = {
id: string; // uuid
@@ -84,7 +84,6 @@ export default defineComponent({
setup(props, { emit }) {
const loading = ref(false);
const image = ref<Image | null>(null);
const error = ref(null);
const lightboxActive = ref(false);
const editorActive = ref(false);
@@ -136,7 +135,6 @@ export default defineComponent({
return {
loading,
image,
error,
src,
meta,
lightboxActive,
@@ -159,7 +157,7 @@ export default defineComponent({
image.value = response.data.data;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}
@@ -179,7 +177,6 @@ export default defineComponent({
loading.value = false;
image.value = null;
error.value = null;
lightboxActive.value = false;
editorActive.value = false;
}

View File

@@ -109,6 +109,7 @@ import getFieldsFromTemplate from '@/utils/get-fields-from-template';
import api from '@/api';
import DrawerItem from '@/views/private/components/drawer-item';
import DrawerCollection from '@/views/private/components/drawer-collection';
import { unexpectedError } from '@/utils/unexpected-error';
/**
* @NOTE
@@ -192,7 +193,6 @@ export default defineComponent({
function useCurrent() {
const currentItem = ref<Record<string, any> | null>(null);
const loading = ref(false);
const error = ref(null);
watch(
() => props.value,
@@ -262,7 +262,7 @@ export default defineComponent({
currentItem.value = response.data.data;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}
@@ -274,7 +274,6 @@ export default defineComponent({
const items = ref<Record<string, any>[] | null>(null);
const loading = ref(false);
const error = ref(null);
watch(relatedCollection, () => {
fetchTotalCount();
@@ -308,7 +307,7 @@ export default defineComponent({
items.value = response.data.data;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -75,6 +75,7 @@ import { Filter, Field } from '@/types';
import { Header, Sort } from '@/components/v-table/types';
import { isEqual, sortBy } from 'lodash';
import { get } from 'lodash';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
components: { DrawerItem, DrawerCollection },
@@ -114,7 +115,7 @@ export default defineComponent({
const fieldsStore = useFieldsStore();
const { relation, relatedCollection, relatedPrimaryKeyField } = useRelation();
const { tableHeaders, items, loading, error } = useTable();
const { tableHeaders, items, loading } = useTable();
const { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit } = useEdits();
const { stageSelection, selectModalActive, selectionFilters } = useSelection();
const { sort, sortItems, sortedItems } = useSort();
@@ -263,7 +264,6 @@ export default defineComponent({
const tableHeaders = ref<Header[]>([]);
const loading = ref(false);
const items = ref<Record<string, any>[]>([]);
const error = ref(null);
watch(
() => props.value,
@@ -318,7 +318,7 @@ export default defineComponent({
})
.concat(...newItems);
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}
@@ -360,7 +360,7 @@ export default defineComponent({
{ immediate: true }
);
return { tableHeaders, items, loading, error };
return { tableHeaders, items, loading };
}
function useEdits() {

View File

@@ -17,19 +17,25 @@ export async function registerInterfaces() {
try {
const customResponse = await api.get('/extensions/interfaces');
if (customResponse.data.data && Array.isArray(customResponse.data.data) && customResponse.data.data.length > 0) {
if (
customResponse.data.data &&
Array.isArray(customResponse.data.data) &&
customResponse.data.data.length > 0
) {
for (const customKey of customResponse.data.data) {
try {
const module = await import(/* webpackIgnore: true */ `/extensions/interfaces/${customKey}/index.js`);
const module = await import(
/* webpackIgnore: true */ `/extensions/interfaces/${customKey}/index.js`
);
modules.push(module.default);
} catch (err) {
console.error(`Couldn't load custom interface "${customKey}"`);
console.error(err);
console.warn(`Couldn't load custom interface "${customKey}"`);
console.warn(err);
}
}
}
} catch {
console.error(`Couldn't load custom interfaces`);
console.warn(`Couldn't load custom interfaces`);
}
interfaces.value = modules;

View File

@@ -36,6 +36,7 @@ import { Relation } from '@/types';
import getFieldsFromTemplate from '@/utils/get-fields-from-template';
import DrawerItem from '@/views/private/components/drawer-item/drawer-item.vue';
import { useCollection } from '../../composables/use-collection';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
components: { DrawerItem },
@@ -197,7 +198,7 @@ export default defineComponent({
const response = await api.get(`/items/${languagesCollection.value}`, { params: { fields } });
languages.value = response.data.data;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -94,6 +94,7 @@ import api from '@/api';
import DrawerItem from '@/views/private/components/drawer-item';
import DrawerCollection from '@/views/private/components/drawer-collection';
import { userName } from '@/utils/user-name';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
components: { DrawerItem, DrawerCollection },
@@ -148,7 +149,6 @@ export default defineComponent({
function useCurrent() {
const currentUser = ref<Record<string, any> | null>(null);
const loading = ref(false);
const error = ref(null);
watch(
() => props.value,
@@ -195,7 +195,7 @@ export default defineComponent({
currentUser.value = response.data.data;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}
@@ -207,7 +207,6 @@ export default defineComponent({
const users = ref<Record<string, any>[] | null>(null);
const loading = ref(false);
const error = ref(null);
fetchTotalCount();
users.value = null;
@@ -231,7 +230,7 @@ export default defineComponent({
users.value = response.data.data;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -612,6 +612,7 @@
"ITEM_LIMIT_REACHED": "Item limit reached.",
"ITEM_NOT_FOUND": "Item not found.",
"ROUTE_NOT_FOUND": "Not found.",
"UNKNOWN": "Unexpected Error",
"-1": "Couldn't Reach API"
},
@@ -1017,10 +1018,10 @@
"ip": "IP Address",
"user_agent": "User Agent",
"comment": "Comment"
},
"directus_collections": {
"translations": "Collection Name Translations"
},
},
"directus_collections": {
"translations": "Collection Name Translations"
},
"directus_files": {
"title": "Title",
"tags": "Tags",
@@ -1053,55 +1054,51 @@
"avatar": "Avatar",
"theme": "Theme",
"tfa_secret": "Two-Factor Authentication"
},
"directus_settings": {
"project_name": "Project Name",
"project_url": "Project URL",
"project_color": "Project Color",
"project_logo": "Project Logo",
"public_foreground": "Public Foreground",
"public_background": "Public Background",
"public_note": "Public Note",
"auth_password_policy": "Auth Password Policy",
"auth_idle_timeout": "Auth Idle Timeout",
"auth_login_attempts": "Auth Login Attempts",
"storage_asset_presets": "Storage Asset Presets",
"storage_asset_transform": "Storage Asset Transform",
"telemetry": "Telemetry"
},
"directus_fields": {
"collection": "Collection Name",
"icon": "Collection Icon",
"note": "Note",
"hidden": "Hidden",
"singleton": "Singleton",
"translation": "Field Name Translations",
"display_template": "Template"
},
"directus_roles": {
"name": "Role Name",
"icon": "Role Icon",
"description": "Description",
"ip_access": "IP Access",
"app_access": "App Access",
"admin_access": "Admin Access",
"enforce_tfa": "Require 2FA",
"users": "Users in Role",
"module_list": "Module Navigation",
"collection_list": "Collection Navigation"
}
},
"directus_settings": {
"project_name": "Project Name",
"project_url": "Project URL",
"project_color": "Project Color",
"project_logo": "Project Logo",
"public_foreground": "Public Foreground",
"public_background": "Public Background",
"public_note": "Public Note",
"auth_password_policy": "Auth Password Policy",
"auth_idle_timeout": "Auth Idle Timeout",
"auth_login_attempts": "Auth Login Attempts",
"storage_asset_presets": "Storage Asset Presets",
"storage_asset_transform": "Storage Asset Transform",
"telemetry": "Telemetry"
},
"directus_fields": {
"collection": "Collection Name",
"icon": "Collection Icon",
"note": "Note",
"hidden": "Hidden",
"singleton": "Singleton",
"translation": "Field Name Translations",
"display_template": "Template"
},
"directus_roles": {
"name": "Role Name",
"icon": "Role Icon",
"description": "Description",
"ip_access": "IP Access",
"app_access": "App Access",
"admin_access": "Admin Access",
"enforce_tfa": "Require 2FA",
"users": "Users in Role",
"module_list": "Module Navigation",
"collection_list": "Collection Navigation"
}
},
"no_fields_in_collection": "There are no fields in \"{collection}\" yet",
"do_nothing": "Do Nothing",
"generate_and_save_uuid": "Generate and Save UUID",
"save_current_user_id": "Save Current User ID",
"save_current_user_role": "Save Current User Role",
"save_current_datetime": "Save Current Date/Time",
"modules": {},
"coming_soon": "Coming Soon",
"comment": "Comment",
"config_error": "Missing Config",
@@ -1150,7 +1147,6 @@
"db_updated": "Database successfully updated",
"default": "Default",
"delete": "Delete",
"delete_are_you_sure": "This action is permanent and can not be undone. Are you sure you would like to proceed?",
"delete_confirmation": "Delete Confirmation",
"delete_field_are_you_sure": "Are you sure you want to delete the field \"{field}\"? This action can not be undone.",
@@ -1232,12 +1228,12 @@
"uuid": "A Universally Unique Identifier"
},
"js_types": {
"object": "Object",
"array": "Array",
"string": "String",
"number": "Number",
"boolean": "Boolean",
"undefined": "Undefined"
"object": "Object",
"array": "Array",
"string": "String",
"number": "Number",
"boolean": "Boolean",
"undefined": "Undefined"
},
"file": "File",
"file_library": "File Library",
@@ -1426,5 +1422,6 @@
"wrapping_up": "Wrapping Up",
"wrong_super_admin_password": "The super admin password you provided is incorrect.",
"writable_fields_copy": "Select the fields that the user can edit",
"yes": "Yes"
"yes": "Yes",
"report_error": "Report Error"
}

View File

@@ -16,19 +16,23 @@ export async function registerLayouts() {
try {
const customResponse = await api.get('/extensions/layouts');
if (customResponse.data.data && Array.isArray(customResponse.data.data) && customResponse.data.data.length > 0) {
if (
customResponse.data.data &&
Array.isArray(customResponse.data.data) &&
customResponse.data.data.length > 0
) {
for (const customKey of customResponse.data.data) {
try {
const module = await import(/* webpackIgnore: true */ `/extensions/layouts/${customKey}/index.js`);
modules.push(module.default);
} catch (err) {
console.error(`Couldn't load custom layout "${customKey}"`);
console.error(err);
console.warn(`Couldn't load custom layout "${customKey}"`);
console.warn(err);
}
}
}
} catch {
console.error(`Couldn't load custom layouts`);
console.warn(`Couldn't load custom layouts`);
}
layouts.value = modules;

View File

@@ -48,7 +48,6 @@
<script lang="ts">
import { defineComponent, computed, toRefs, ref, watch } from '@vue/composition-api';
import { i18n } from '@/lang';
import router from '@/router';
import api from '@/api';
import { userName } from '@/utils/user-name';

View File

@@ -58,6 +58,7 @@
import { defineComponent, PropType, ref, computed } from '@vue/composition-api';
import { Preset } from '@/types';
import { useUserStore, usePresetsStore } from '@/stores';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
props: {
@@ -106,8 +107,8 @@ export default defineComponent({
});
renameActive.value = false;
} catch (error) {
console.error(error);
} catch (err) {
unexpectedError(err);
} finally {
renameSaving.value = false;
}
@@ -127,8 +128,8 @@ export default defineComponent({
try {
await presetsStore.delete(props.bookmark.id);
deleteActive.value = false;
} catch (error) {
console.error(error);
} catch (err) {
unexpectedError(err);
} finally {
deleteSaving.value = false;
}

View File

@@ -263,6 +263,7 @@ import router from '@/router';
import marked from 'marked';
import { usePermissionsStore, useUserStore } from '@/stores';
import DrawerBatch from '@/views/private/components/drawer-batch';
import { unexpectedError } from '@/utils/unexpected-error';
type Item = {
[field: string]: any;
@@ -513,8 +514,8 @@ export default defineComponent({
router.push(`/collections/${newBookmark.collection}?bookmark=${newBookmark.id}`);
bookmarkDialogActive.value = false;
} catch (error) {
console.log(error);
} catch (err) {
unexpectedError(err);
} finally {
creatingBookmark.value = false;
}

View File

@@ -26,6 +26,7 @@ import { defineComponent, ref } from '@vue/composition-api';
import useFolders from '../composables/use-folders';
import api from '@/api';
import router from '@/router';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
props: {
@@ -38,11 +39,10 @@ export default defineComponent({
const dialogActive = ref(false);
const saving = ref(false);
const newFolderName = ref(null);
const savingError = ref(null);
const { fetchFolders } = useFolders();
return { addFolder, dialogActive, newFolderName, saving, savingError };
return { addFolder, dialogActive, newFolderName, saving };
async function addFolder() {
saving.value = true;
@@ -60,7 +60,7 @@ export default defineComponent({
router.push({ path: '/files', query: { folder: newFolder.data.data.id } });
} catch (err) {
savingError.value = err;
unexpectedError(err);
} finally {
saving.value = false;
}

View File

@@ -36,6 +36,7 @@
import { defineComponent, ref, computed, PropType } from '@vue/composition-api';
import api from '@/api';
import FolderPickerListItem from './folder-picker-list-item.vue';
import { unexpectedError } from '@/utils/unexpected-error';
type FolderRaw = {
id: string;
@@ -64,7 +65,6 @@ export default defineComponent({
setup(props) {
const loading = ref(false);
const folders = ref<FolderRaw[]>([]);
const error = ref<any>(null);
const tree = computed<Folder[]>(() => {
return folders.value
.filter((folder) => folder.parent === null)
@@ -125,7 +125,7 @@ export default defineComponent({
folders.value = response.data.data;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -112,10 +112,10 @@
<script lang="ts">
import { defineComponent, PropType, ref, watch, computed } from '@vue/composition-api';
import useFolders, { Folder } from '../composables/use-folders';
import notify from '@/utils/notify';
import api from '@/api';
import FolderPicker from './folder-picker.vue';
import router from '@/router';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
name: 'navigation-folder',
@@ -169,8 +169,8 @@ export default defineComponent({
await api.patch(`/folders/${props.folder.id}`, {
name: renameValue.value,
});
} catch (error) {
console.error(error);
} catch (err) {
unexpectedError(err);
} finally {
renameSaving.value = false;
await fetchFolders();
@@ -193,8 +193,8 @@ export default defineComponent({
await api.patch(`/folders/${props.folder.id}`, {
parent: moveValue.value,
});
} catch (error) {
console.error(error);
} catch (err) {
unexpectedError(err);
} finally {
moveSaving.value = false;
await fetchFolders();
@@ -255,8 +255,8 @@ export default defineComponent({
}
deleteActive.value = false;
} catch (error) {
console.error(error);
} catch (err) {
unexpectedError(err);
} finally {
await fetchFolders();
deleteSaving.value = false;

View File

@@ -1,5 +1,4 @@
import api from '@/api';
import i18n from '@/lang';
import { ref, Ref } from '@vue/composition-api';
import { TranslateResult } from 'vue-i18n';

View File

@@ -164,6 +164,7 @@ import { subDays } from 'date-fns';
import useFolders from '../composables/use-folders';
import useEventListener from '@/composables/use-event-listener';
import uploadFiles from '@/utils/upload-files';
import { unexpectedError } from '@/utils/unexpected-error';
type Item = {
[field: string]: any;
@@ -183,12 +184,12 @@ export default defineComponent({
},
},
setup(props) {
const notificationsStore = useNotificationsStore();
const { folders } = useFolders();
const layoutRef = ref<LayoutComponent | null>(null);
const selection = ref<Item[]>([]);
const userStore = useUserStore();
const notificationsStore = useNotificationsStore();
const { layout, layoutOptions, layoutQuery, filters, searchQuery } = usePreset(ref('directus_files'));
const { batchLink } = useLinks();
@@ -307,7 +308,7 @@ export default defineComponent({
selection.value = [];
await layoutRef.value?.refresh();
} catch (err) {
console.error(err);
unexpectedError(err);
} finally {
deleting.value = false;
}
@@ -387,7 +388,7 @@ export default defineComponent({
await Vue.nextTick();
await refresh();
} catch (err) {
console.error(err);
unexpectedError(err);
} finally {
moveToDialogActive.value = false;
moving.value = false;

View File

@@ -185,6 +185,7 @@ import SaveOptions from '@/views/private/components/save-options';
import FilePreview from '@/views/private/components/file-preview';
import ImageEditor from '@/views/private/components/image-editor';
import { nanoid } from 'nanoid';
import FileLightbox from '@/views/private/components/file-lightbox';
import { useFieldsStore } from '@/stores/';
import { Field } from '@/types';
import FileInfoSidebarDetail from '../components/file-info-sidebar-detail.vue';
@@ -196,6 +197,8 @@ import FilesNotFound from './not-found.vue';
import useShortcut from '@/composables/use-shortcut';
import ReplaceFile from '../components/replace-file.vue';
import { usePermissions } from '@/composables/use-permissions';
import { notify } from '@/utils/notify';
import { unexpectedError } from '@/utils/unexpected-error';
type Values = {
[field: string]: any;
@@ -420,12 +423,28 @@ export default defineComponent({
moving.value = true;
try {
await api.patch(`/files/${props.primaryKey}`, {
folder: selectedFolder.value,
});
const response = await api.patch(
`/files/${props.primaryKey}`,
{
folder: selectedFolder.value,
},
{
params: {
fields: 'folder.name',
},
}
);
await refresh();
const folder = response.data.data.folder?.name || i18n.t('file_library');
notify({
title: i18n.t('file_moved', { folder }),
type: 'success',
icon: 'folder_move',
});
} catch (err) {
console.error(err);
unexpectedError(err);
} finally {
moveToDialogActive.value = false;
moving.value = false;

View File

@@ -37,13 +37,13 @@ export async function loadModules() {
});
loadedModules.push(module.default);
} catch (err) {
console.error(`Couldn't load custom module "${customKey}"`);
console.error(err);
console.warn(`Couldn't load custom module "${customKey}"`);
console.warn(err);
}
}
}
} catch {
console.error(`Couldn't load custom modules`);
console.warn(`Couldn't load custom modules`);
}
}

View File

@@ -121,10 +121,11 @@ import { useFieldsStore, useRelationsStore, useCollectionsStore } from '@/stores
import { Field } from '@/types';
import router from '@/router';
import useCollection from '@/composables/use-collection';
import notify from '@/utils/notify';
import { getLocalTypeForField } from '../get-local-type';
import { notify } from '@/utils/notify';
import { initLocalStore, state, clearLocalStore } from './store';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
components: {
@@ -373,8 +374,8 @@ export default defineComponent({
router.push(`/settings/data-model/${props.collection}`);
clearLocalStore();
} catch (error) {
console.error(error);
} catch (err) {
unexpectedError(err);
} finally {
saving.value = false;
}

View File

@@ -217,10 +217,11 @@ import { Field, Relation } from '@/types';
import { useCollectionsStore, useFieldsStore, useRelationsStore } from '@/stores/';
import { getInterfaces } from '@/interfaces';
import router from '@/router';
import notify from '@/utils/notify';
import { i18n } from '@/lang';
import { cloneDeep } from 'lodash';
import { getLocalTypeForField } from '../../get-local-type';
import { notify } from '@/utils/notify';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
props: {
@@ -368,8 +369,8 @@ export default defineComponent({
});
duplicateActive.value = false;
} catch (error) {
console.log(error);
} catch (err) {
unexpectedError(err);
} finally {
duplicating.value = false;
}

View File

@@ -6,22 +6,6 @@
persistent
@cancel="$router.push('/settings/data-model')"
>
<v-dialog :active="saveError !== null" @toggle="saveError = null" @esc="saveError = null">
<v-card class="selectable">
<v-card-title>
{{ saveError && saveError.message }}
</v-card-title>
<v-card-text>
{{ saveError && saveError.response && saveError.response.data.errors[0].message }}
</v-card-text>
<v-card-actions>
<v-button @click="saveError = null">{{ $t('dismiss') }}</v-button>
</v-card-actions>
</v-card>
</v-dialog>
<template #sidebar>
<v-tabs vertical v-model="currentTab">
<v-tab value="collection">{{ $t('collection_setup') }}</v-tab>
@@ -138,8 +122,10 @@ import { defineComponent, ref, reactive } from '@vue/composition-api';
import api from '@/api';
import { Field, Relation } from '@/types';
import { useFieldsStore, useCollectionsStore, useRelationsStore } from '@/stores/';
import notify from '@/utils/notify';
import { notify } from '@/utils/notify';
import router from '@/router';
import i18n from '@/lang';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
setup() {
@@ -200,7 +186,6 @@ export default defineComponent({
});
const saving = ref(false);
const saveError = ref(null);
return {
currentTab,
@@ -209,7 +194,6 @@ export default defineComponent({
primaryKeyFieldName,
primaryKeyFieldType,
collectionName,
saveError,
saving,
singleton,
};
@@ -246,9 +230,8 @@ export default defineComponent({
});
router.push(`/settings/data-model/${collectionName.value}`);
} catch (error) {
console.log(error);
saveError.value = error;
} catch (err) {
unexpectedError(err);
} finally {
saving.value = false;
}

View File

@@ -96,7 +96,6 @@ import SettingsNavigation from '../../../components/navigation.vue';
import api from '@/api';
import { Header } from '@/components/v-table/types';
import i18n from '@/lang';
import { useCollectionsStore } from '@/stores/';
import { getLayouts } from '@/layouts';
import { TranslateResult } from 'vue-i18n';
@@ -104,6 +103,8 @@ import router from '@/router';
import ValueNull from '@/views/private/components/value-null';
import PresetsInfoSidebarDetail from './components/presets-info-sidebar-detail.vue';
import { userName } from '@/utils/user-name';
import i18n from '@/lang';
import { unexpectedError } from '@/utils/unexpected-error';
type PresetRaw = {
id: number;
@@ -131,7 +132,7 @@ export default defineComponent({
const selection = ref<Preset[]>([]);
const { addNewLink } = useLinks();
const { loading, presets, error, getPresets } = usePresets();
const { loading, presets, getPresets } = usePresets();
const { headers } = useTable();
const { confirmDelete, deleting, deleteSelection } = useDelete();
@@ -142,7 +143,6 @@ export default defineComponent({
usePresets,
loading,
presets,
error,
getPresets,
headers,
selection,
@@ -163,7 +163,6 @@ export default defineComponent({
function usePresets() {
const loading = ref(false);
const presetsRaw = ref<PresetRaw[] | null>(null);
const error = ref(null);
const presets = computed<Preset[]>(() => {
return (presetsRaw.value || []).map((preset) => {
@@ -190,7 +189,7 @@ export default defineComponent({
});
});
return { loading, presetsRaw, error, getPresets, presets };
return { loading, presetsRaw, getPresets, presets };
async function getPresets() {
loading.value = true;
@@ -213,7 +212,7 @@ export default defineComponent({
});
presetsRaw.value = response.data.data;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -21,11 +21,11 @@
import { defineComponent, ref } from '@vue/composition-api';
import api from '@/api';
import marked from 'marked';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
setup() {
const loading = ref(false);
const error = ref<any>(null);
const bookmarksCount = ref<number | null>(null);
const presetsCount = ref<number | null>(null);
@@ -48,7 +48,7 @@ export default defineComponent({
bookmarksCount.value = response.data.meta.filter_count as number;
presetsCount.value = (response.data.meta.total_count as number) - bookmarksCount.value;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -103,6 +103,7 @@ import { getLayouts } from '@/layouts';
import router from '@/router';
import marked from 'marked';
import { userName } from '@/utils/user-name';
import { unexpectedError } from '@/utils/unexpected-error';
type User = {
id: number;
@@ -145,7 +146,7 @@ export default defineComponent({
const { loading: usersLoading, users } = useUsers();
const { loading: rolesLoading, roles } = useRoles();
const { loading: presetLoading, error, preset } = usePreset();
const { loading: presetLoading, preset } = usePreset();
const { fields } = useForm();
const { edits, hasEdits, initialValues, values, layoutQuery, layoutOptions } = useValues();
const { save, saving } = useSave();
@@ -156,7 +157,6 @@ export default defineComponent({
return {
backLink,
loading,
error,
preset,
edits,
fields,
@@ -213,7 +213,7 @@ export default defineComponent({
edits.value = {};
} catch (err) {
console.error(err);
unexpectedError(err);
} finally {
saving.value = false;
router.push(`/settings/presets`);
@@ -233,8 +233,8 @@ export default defineComponent({
try {
await api.delete(`/presets/${props.id}`);
router.push(`/settings/presets`);
} catch (error) {
console.error(error);
} catch (err) {
unexpectedError(err);
} finally {
deleting.value = false;
}
@@ -321,12 +321,11 @@ export default defineComponent({
function usePreset() {
const loading = ref(false);
const error = ref(null);
const preset = ref<Preset | null>(null);
fetchPreset();
return { loading, error, preset, fetchPreset };
return { loading, preset, fetchPreset };
async function fetchPreset() {
loading.value = true;
@@ -336,7 +335,7 @@ export default defineComponent({
preset.value = response.data.data;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}
@@ -353,12 +352,11 @@ export default defineComponent({
function useUsers() {
const loading = ref(false);
const error = ref(null);
const users = ref<User[] | null>(null);
fetchUsers();
return { loading, error, users };
return { loading, users };
async function fetchUsers() {
loading.value = true;
@@ -375,7 +373,7 @@ export default defineComponent({
id: user.id,
}));
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}
@@ -384,12 +382,11 @@ export default defineComponent({
function useRoles() {
const loading = ref(false);
const error = ref(null);
const roles = ref<Role[] | null>(null);
fetchRoles();
return { loading, error, roles };
return { loading, roles };
async function fetchRoles() {
loading.value = true;
@@ -403,7 +400,7 @@ export default defineComponent({
roles.value = response.data.data;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -39,6 +39,7 @@ import { defineComponent, ref } from '@vue/composition-api';
import api from '@/api';
import router from '@/router';
import { permissions } from './app-required-permissions';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
setup() {
@@ -46,19 +47,17 @@ export default defineComponent({
const appAccess = ref(true);
const adminAccess = ref(false);
const { saving, error, save } = useSave();
const { saving, save } = useSave();
return { roleName, saving, error, save, appAccess, adminAccess };
return { roleName, saving, save, appAccess, adminAccess };
function useSave() {
const saving = ref(false);
const error = ref<any>();
return { saving, error, save };
return { saving, save };
async function save() {
saving.value = true;
error.value = null;
try {
const roleResponse = await api.post('/roles', {
@@ -79,7 +78,7 @@ export default defineComponent({
router.push(`/settings/roles/${roleResponse.data.data.id}`);
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
saving.value = false;
}

View File

@@ -66,6 +66,7 @@ import marked from 'marked';
import { Header as TableHeader } from '@/components/v-table/types';
import ValueNull from '@/views/private/components/value-null';
import router from '@/router';
import { unexpectedError } from '@/utils/unexpected-error';
type Role = {
id: number;
@@ -81,7 +82,6 @@ export default defineComponent({
setup() {
const roles = ref<Role[]>([]);
const loading = ref(false);
const error = ref<any>(null);
const tableHeaders: TableHeader[] = [
{
@@ -120,7 +120,7 @@ export default defineComponent({
return `/settings/roles/+`;
});
return { marked, loading, roles, error, tableHeaders, addNewLink, navigateToRole };
return { marked, loading, roles, tableHeaders, addNewLink, navigateToRole };
async function fetchRoles() {
loading.value = true;
@@ -146,7 +146,7 @@ export default defineComponent({
}),
];
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -52,6 +52,7 @@ import { defineComponent, PropType, computed, inject, ref } from '@vue/compositi
import { Collection, Permission } from '@/types';
import api from '@/api';
import router from '@/router';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
props: {
@@ -118,7 +119,7 @@ export default defineComponent({
validation: null,
});
} catch (err) {
console.error(err);
unexpectedError(err);
} finally {
await refresh?.();
saving.value = false;
@@ -132,7 +133,7 @@ export default defineComponent({
fields: '*',
});
} catch (err) {
console.error(err);
unexpectedError(err);
} finally {
await refresh?.();
saving.value = false;
@@ -148,7 +149,7 @@ export default defineComponent({
try {
await api.delete(`/permissions/${props.permission.id}`);
} catch (err) {
console.error(err);
unexpectedError(err);
} finally {
await refresh?.();
saving.value = false;

View File

@@ -68,6 +68,7 @@ import PermissionsOverviewRow from './permissions-overview-row.vue';
import { Permission } from '@/types';
import api from '@/api';
import { permissions as appRequiredPermissions } from '../../app-required-permissions';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
components: { PermissionsOverviewHeader, PermissionsOverviewRow },
@@ -99,7 +100,7 @@ export default defineComponent({
const systemVisible = ref(false);
const { permissions, loading, error, fetchPermissions, refreshPermission, refreshing } = usePermissions();
const { permissions, loading, fetchPermissions, refreshPermission, refreshing } = usePermissions();
const { resetActive, resetSystemPermissions, resetting, resetError } = useReset();
@@ -113,7 +114,6 @@ export default defineComponent({
systemCollections,
permissions,
loading,
error,
fetchPermissions,
refreshPermission,
refreshing,
@@ -127,12 +127,10 @@ export default defineComponent({
const permissions = ref<Permission[]>([]);
const loading = ref(false);
const refreshing = ref<number[]>([]);
const error = ref();
return { permissions, loading, error, fetchPermissions, refreshPermission, refreshing };
return { permissions, loading, fetchPermissions, refreshPermission, refreshing };
async function fetchPermissions() {
error.value = null;
loading.value = true;
try {
@@ -148,7 +146,7 @@ export default defineComponent({
permissions.value = response.data.data;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}
@@ -167,7 +165,7 @@ export default defineComponent({
return permission;
});
} catch (err) {
console.log(`Couldn't refresh permissions ${id}`);
unexpectedError(err);
} finally {
refreshing.value = refreshing.value.filter((inProgressID) => inProgressID !== id);
}

View File

@@ -66,8 +66,7 @@ export default function usePermissions(role: Ref<number>) {
await fetchPermissions();
} catch (err) {
console.error(err);
throw err;
error.value = err;
}
}
@@ -83,8 +82,7 @@ export default function usePermissions(role: Ref<number>) {
await fetchPermissions();
} catch (err) {
console.error(err);
throw err;
error.value = err;
}
}
}

View File

@@ -39,7 +39,13 @@
</v-card>
</v-dialog>
<v-button rounded icon @click="userInviteModalActive = true" v-tooltip.bottom="$t('invite_users')" class="invite-user">
<v-button
rounded
icon
@click="userInviteModalActive = true"
v-tooltip.bottom="$t('invite_users')"
class="invite-user"
>
<v-icon name="person_add" />
</v-button>

View File

@@ -11,6 +11,7 @@ import { defineComponent, PropType, ref, inject } from '@vue/composition-api';
import { Permission } from '@/types';
import api from '@/api';
import router from '@/router';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
props: {
@@ -25,20 +26,18 @@ export default defineComponent({
},
setup(props, { emit }) {
const loading = ref(false);
const error = ref<any>();
return { save, loading };
async function save() {
loading.value = true;
error.value = null;
try {
await api.patch(`/permissions/${props.permission.id}`, props.permission);
emit('refresh');
router.push(`/settings/roles/${props.roleKey || 'public'}`);
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -22,7 +22,6 @@ import { defineComponent, ref, reactive, computed, watch } from '@vue/compositio
import api from '@/api';
import { Permission, Role } from '@/types';
import { useFieldsStore, useCollectionsStore } from '@/stores/';
import notify from '@/utils/notify';
import router from '@/router';
import i18n from '@/lang';
import Actions from './components/actions.vue';
@@ -32,6 +31,7 @@ import Permissions from './components/permissions.vue';
import Fields from './components/fields.vue';
import Validation from './components/validation.vue';
import Presets from './components/presets.vue';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
components: { Actions, Tabs, Permissions, Fields, Validation, Presets },
@@ -50,7 +50,6 @@ export default defineComponent({
const permission = ref<Permission>();
const role = ref<Role>();
const error = ref<any>();
const loading = ref(false);
const collectionName = computed(() => {
@@ -127,7 +126,7 @@ export default defineComponent({
{ immediate: true }
);
return { permission, role, error, loading, modalTitle, tabs, currentTab };
return { permission, role, loading, modalTitle, tabs, currentTab };
async function load() {
loading.value = true;
@@ -141,10 +140,10 @@ export default defineComponent({
const response = await api.get(`/permissions/${props.permissionKey}`);
permission.value = response.data.data;
} catch (err) {
error.value = err;
if (err?.response?.status === 403) {
router.push(`/settings/roles/${props.roleKey || 'public'}`);
} else {
unexpectedError(err);
}
} finally {
loading.value = false;

View File

@@ -181,6 +181,7 @@ import { isAllowed } from '@/utils/is-allowed';
import useCollection from '@/composables/use-collection';
import { userName } from '@/utils/user-name';
import { usePermissions } from '@/composables/use-permissions';
import { unexpectedError } from '@/utils/unexpected-error';
type Values = {
[field: string]: any;
@@ -308,7 +309,6 @@ export default defineComponent({
title,
item,
loading,
error,
isNew,
breadcrumb,
edits,
@@ -411,13 +411,12 @@ export default defineComponent({
function useUserPreview() {
const loading = ref(false);
const error = ref(null);
const avatarSrc = ref<string | null>(null);
const roleName = ref<string | null>(null);
watch(() => props.primaryKey, getUserPreviewData, { immediate: true });
return { loading, error, avatarSrc, roleName };
return { loading, avatarSrc, roleName };
async function getUserPreviewData() {
if (props.primaryKey === '+') return;
@@ -436,7 +435,7 @@ export default defineComponent({
: null;
roleName.value = response.data.data.role.name;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -18,21 +18,20 @@ import api from '@/api';
import { hydrate } from '@/hydrate';
import router from '@/router';
import { userName } from '@/utils/user-name';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
setup() {
const loading = ref(false);
const error = ref(null);
const name = ref<string | null>(null);
const lastPage = ref<string | null>(null);
fetchUser();
return { name, lastPage, loading, error, hydrateAndLogin };
return { name, lastPage, loading, hydrateAndLogin };
async function fetchUser() {
loading.value = true;
error.value = null;
try {
const response = await api.get(`/users/me`, {
@@ -44,7 +43,7 @@ export default defineComponent({
name.value = userName(response.data.data);
lastPage.value = response.data.data.last_page;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -91,7 +91,6 @@ export default defineComponent({
const lastPage = userStore.state.currentUser?.last_page;
router.push(lastPage || '/collections');
} catch (err) {
/** @todo use new error code */
if (
err.response?.data?.errors?.[0]?.extensions?.code === 'INVALID_OTP' &&
requiresTFA.value === false

View File

@@ -14,12 +14,12 @@
import { defineComponent, ref, onMounted } from '@vue/composition-api';
import api from '@/api';
import getRootPath from '@/utils/get-root-path';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
setup() {
const providers = ref([]);
const loading = ref(false);
const error = ref(null);
onMounted(() => fetchProviders());
@@ -27,7 +27,6 @@ export default defineComponent({
async function fetchProviders() {
loading.value = true;
error.value = null;
try {
const response = await api.get('/auth/oauth/');
@@ -41,8 +40,7 @@ export default defineComponent({
};
});
} catch (err) {
error.value = err;
console.error(err);
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -5,7 +5,8 @@ import i18n from '@/lang/';
import { notEmpty } from '@/utils/is-empty/';
import VueI18n from 'vue-i18n';
import formatTitle from '@directus/format-title';
import notify from '@/utils/notify';
import { notify } from '@/utils/notify';
import { unexpectedError } from '@/utils/unexpected-error';
export const useCollectionsStore = createStore({
id: 'collectionsStore',
@@ -63,13 +64,8 @@ export const useCollectionsStore = createStore({
type: 'success',
title: i18n.t('update_collection_success'),
});
} catch (error) {
notify({
type: 'error',
title: i18n.t('update_collection_failed'),
text: collection,
});
throw error;
} catch (err) {
unexpectedError(err);
}
},
async deleteCollection(collection: string) {
@@ -80,13 +76,8 @@ export const useCollectionsStore = createStore({
type: 'success',
title: i18n.t('delete_collection_success'),
});
} catch (error) {
notify({
type: 'error',
title: i18n.t('delete_collection_failed'),
text: collection,
});
throw error;
} catch (err) {
unexpectedError(err);
}
},
getCollection(collectionKey: string): Collection | null {

View File

@@ -8,6 +8,7 @@ import { useRelationsStore } from '@/stores/';
import { Relation, FieldRaw, Field } from '@/types';
import { merge } from 'lodash';
import { nanoid } from 'nanoid';
import { unexpectedError } from '@/utils/unexpected-error';
const fakeFilesField: Field = {
collection: 'directus_files',
@@ -120,10 +121,10 @@ export const useFieldsStore = createStore({
});
return field;
} catch (error) {
} catch (err) {
// reset the changes if the api sync failed
this.state.fields = stateClone;
throw error;
unexpectedError(err);
}
},
async updateField(collectionKey: string, fieldKey: string, updates: Record<string, Partial<Field>>) {
@@ -150,10 +151,10 @@ export const useFieldsStore = createStore({
return field;
});
} catch (error) {
} catch (err) {
// reset the changes if the api sync failed
this.state.fields = stateClone;
throw error;
unexpectedError(err);
}
},
async updateFields(collectionKey: string, updates: Partial<Field>[]) {
@@ -192,10 +193,10 @@ export const useFieldsStore = createStore({
return field;
});
}
} catch (error) {
} catch (err) {
// reset the changes if the api sync failed
this.state.fields = stateClone;
throw error;
unexpectedError(err);
}
},
async deleteField(collectionKey: string, fieldKey: string) {
@@ -208,9 +209,9 @@ export const useFieldsStore = createStore({
try {
await api.delete(`/fields/${collectionKey}/${fieldKey}`);
} catch (error) {
} catch (err) {
this.state.fields = stateClone;
throw error;
unexpectedError(err);
}
},
getPrimaryKeyFieldForCollection(collection: string) {

View File

@@ -6,6 +6,7 @@ import { reverse, sortBy } from 'lodash';
export const useNotificationsStore = createStore({
id: 'notificationsStore',
state: () => ({
dialogs: [] as Notification[],
queue: [] as Notification[],
previous: [] as Notification[],
}),
@@ -14,14 +15,27 @@ export const useNotificationsStore = createStore({
const id = nanoid();
const timestamp = Date.now();
this.state.queue = [
...this.state.queue,
{
...notification,
id,
timestamp,
},
];
if (notification.dialog === true) {
notification.persist = true;
this.state.dialogs = [
...this.state.dialogs,
{
...notification,
id,
timestamp,
},
];
} else {
this.state.queue = [
...this.state.queue,
{
...notification,
id,
timestamp,
},
];
}
if (notification.persist !== true) {
setTimeout(() => {
@@ -32,20 +46,27 @@ export const useNotificationsStore = createStore({
return id;
},
hide(id: string) {
const toBeHidden = this.state.queue.find((n) => n.id === id);
const queues = [...this.state.queue, ...this.state.dialogs];
const toBeHidden = queues.find((n) => n.id === id);
if (!toBeHidden) return;
this.state.queue = this.state.queue.filter((n) => n.id !== id);
if (toBeHidden.dialog === true) this.state.dialogs = this.state.dialogs.filter((n) => n.id !== id);
else this.state.queue = this.state.queue.filter((n) => n.id !== id);
this.state.previous = [...this.state.previous, toBeHidden];
},
remove(id: string) {
const toBeRemoved = this.state.queue.find((n) => n.id === id);
const queues = [...this.state.queue, ...this.state.dialogs];
const toBeRemoved = queues.find((n) => n.id === id);
if (!toBeRemoved) return;
this.state.queue = this.state.queue.filter((n) => n.id !== id);
if (toBeRemoved.dialog === true) this.state.dialogs = this.state.dialogs.filter((n) => n.id !== id);
else this.state.queue = this.state.queue.filter((n) => n.id !== id);
},
update(id: string, updates: Partial<Notification>) {
this.state.queue = this.state.queue.map(updateIfNeeded);
this.state.dialogs = this.state.dialogs.map(updateIfNeeded);
this.state.previous = this.state.queue.map(updateIfNeeded);
function updateIfNeeded(notification: Notification) {
@@ -61,7 +82,7 @@ export const useNotificationsStore = createStore({
},
getters: {
lastFour(state) {
const all = [...state.queue, ...state.previous];
const all = [...state.queue, ...state.previous.filter((l) => l.dialog !== true)];
const chronologicalAll = reverse(sortBy(all, ['timestamp']));
const newestFour = chronologicalAll.slice(0, 4);
return reverse(newestFour);

View File

@@ -1,8 +1,9 @@
import { createStore } from 'pinia';
import api from '@/api';
import notify from '@/utils/notify';
import { i18n } from '@/lang';
import { merge } from 'lodash';
import { notify } from '@/utils/notify';
import { unexpectedError } from '@/utils/unexpected-error';
export const useSettingsStore = createStore({
id: 'settingsStore',
@@ -11,10 +12,8 @@ export const useSettingsStore = createStore({
}),
actions: {
async hydrate() {
try {
const response = await api.get(`/settings`);
this.state.settings = response.data.data;
} catch {}
const response = await api.get(`/settings`);
this.state.settings = response.data.data;
},
async dehydrate() {
@@ -36,14 +35,9 @@ export const useSettingsStore = createStore({
title: i18n.t('settings_update_success'),
type: 'success',
});
} catch (error) {
} catch (err) {
this.state.settings = settingsCopy;
notify({
title: i18n.t('settings_update_failed'),
text: Object.keys(updates).join(', '),
type: 'error',
});
unexpectedError(err);
}
},
},

View File

@@ -10,6 +10,8 @@ export interface NotificationRaw {
closeable?: boolean;
progress?: number;
loading?: boolean;
dialog?: boolean;
error?: Error;
}
export interface Notification extends NotificationRaw {

9
app/src/utils/notify.ts Normal file
View File

@@ -0,0 +1,9 @@
import { useNotificationsStore } from '@/stores/';
import { NotificationRaw } from '@/types';
let store: any;
export function notify(notification: NotificationRaw) {
if (!store) store = useNotificationsStore();
store.add(notification);
}

View File

@@ -1,4 +0,0 @@
import notify from './notify';
export { notify };
export default notify;

View File

@@ -1,23 +0,0 @@
import Vue from 'vue';
import VueCompositionAPI from '@vue/composition-api';
import notify from './notify';
import { useNotificationsStore } from '@/stores/';
describe('Utils / Notify', () => {
beforeAll(() => {
Vue.use(VueCompositionAPI);
});
it('Calls notificationsStore.add with the passed notification', () => {
const notificationsStore = useNotificationsStore();
jest.spyOn(notificationsStore as any, 'add');
const notification = {
title: 'test',
};
notify(notification);
expect(notificationsStore.add).toHaveBeenCalledWith(notification);
});
});

View File

@@ -1,7 +0,0 @@
import { useNotificationsStore } from '@/stores/';
import { NotificationRaw } from '@/types';
export default function notify(notification: NotificationRaw) {
const notificationsStore = useNotificationsStore();
notificationsStore.add(notification);
}

View File

@@ -0,0 +1,22 @@
import { useNotificationsStore } from '@/stores/';
import { i18n } from '@/lang';
import { RequestError } from '@/api';
let store: any;
export function unexpectedError(error: Error | RequestError) {
if (!store) store = useNotificationsStore();
const code = (error as RequestError).response?.data?.errors?.[0]?.extensions?.code || 'UNKNOWN';
const message = (error as RequestError).response?.data?.errors?.[0]?.message || error.message || undefined;
console.warn(error);
store.add({
title: i18n.t(`errors.${code}`),
text: message,
type: 'error',
dialog: true,
error,
});
}

View File

@@ -1,9 +1,9 @@
import api from '@/api';
import notify from '@/utils/notify';
import i18n from '@/lang';
import { notify } from '@/utils/notify';
import emitter, { Events } from '@/events';
import { unexpectedError } from '../unexpected-error';
export default async function uploadFile(
file: File,
@@ -48,12 +48,8 @@ export default async function uploadFile(
emitter.emit(Events.upload);
return response.data.data;
} catch (error) {
if (options?.notifications) {
notify({
title: i18n.t('upload_file_failed'),
});
}
} catch (err) {
unexpectedError(err);
}
function onUploadProgress(progressEvent: { loaded: number; total: number }) {

View File

@@ -1,6 +1,7 @@
import uploadFile from '@/utils/upload-file';
import notify from '@/utils/notify';
import i18n from '@/lang';
import { notify } from '@/utils/notify';
import { unexpectedError } from '../unexpected-error';
export default async function uploadFiles(
files: File[],
@@ -34,14 +35,7 @@ export default async function uploadFiles(
}
return uploadedFiles;
} catch (error) {
if (options?.notifications) {
notify({
title: i18n.t('upload_files_failed', { count: files.length }),
type: 'error',
});
}
throw error;
} catch (err) {
unexpectedError(err);
}
}

View File

@@ -24,11 +24,11 @@
<script lang="ts">
import { defineComponent, ref, PropType } from '@vue/composition-api';
import notify from '@/utils/notify';
import api from '@/api';
import i18n from '@/lang';
import useShortcut from '@/composables/use-shortcut';
import { notify } from '@/utils/notify';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
props: {
@@ -72,11 +72,8 @@ export default defineComponent({
title: i18n.t('post_comment_success'),
type: 'success',
});
} catch {
notify({
title: i18n.t('post_comment_failed'),
type: 'error',
});
} catch (err) {
unexpectedError(err);
} finally {
saving.value = false;
}

View File

@@ -69,6 +69,7 @@ import { userName } from '@/utils/user-name';
import api from '@/api';
import localizedFormat from '@/utils/localized-format';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
props: {
@@ -114,8 +115,8 @@ export default defineComponent({
await api.delete(`/activity/comment/${props.activity.id}`);
await props.refresh();
confirmDelete.value = false;
} catch (error) {
console.error(error);
} catch (err) {
unexpectedError(err);
} finally {
deleting.value = false;
}

View File

@@ -41,6 +41,7 @@ import marked from 'marked';
import useShortcut from '@/composables/use-shortcut';
import api from '@/api';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
components: { CommentItemHeader },
@@ -84,8 +85,8 @@ export default defineComponent({
comment: edits.value,
});
await props.refresh();
} catch (error) {
console.error(error);
} catch (err) {
unexpectedError(err);
} finally {
savingEdits.value = false;
editing.value = false;

View File

@@ -39,6 +39,7 @@ import useCollection from '@/composables/use-collection';
import { useFieldsStore, useRelationsStore } from '@/stores';
import i18n from '@/lang';
import { Relation, Field } from '@/types';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
model: {
@@ -84,7 +85,7 @@ export default defineComponent({
junctionRelatedCollectionInfo,
setJunctionEdits,
} = useJunction();
const { _edits, loading, error, item } = useItem();
const { _edits, loading, item } = useItem();
const { save, cancel } = useActions();
const { collection } = toRefs(props);
@@ -115,7 +116,6 @@ export default defineComponent({
_active,
_edits,
loading,
error,
item,
save,
cancel,
@@ -162,7 +162,6 @@ export default defineComponent({
});
const loading = ref(false);
const error = ref(null);
const item = ref<Record<string, any> | null>(null);
watch(
@@ -173,7 +172,6 @@ export default defineComponent({
if (props.relatedPrimaryKey !== '+') fetchRelatedItem();
} else {
loading.value = false;
error.value = null;
item.value = null;
localEdits.value = {};
}
@@ -181,7 +179,7 @@ export default defineComponent({
{ immediate: true }
);
return { _edits, loading, error, item, fetchItem };
return { _edits, loading, item, fetchItem };
async function fetchItem() {
loading.value = true;
@@ -201,7 +199,7 @@ export default defineComponent({
item.value = response.data.data;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}
@@ -224,7 +222,7 @@ export default defineComponent({
[junctionFieldInfo.value.field]: response.data.data,
};
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -31,6 +31,7 @@ import { nanoid } from 'nanoid';
import FilePreview from '@/views/private/components/file-preview';
import getRootPath from '@/utils/get-root-path';
import { unexpectedError } from '@/utils/unexpected-error';
type File = {
type: string;
@@ -69,7 +70,6 @@ export default defineComponent({
});
const loading = ref(false);
const error = ref(null);
const file = ref<File | null>(null);
const cacheBuster = ref(nanoid());
@@ -87,7 +87,7 @@ export default defineComponent({
{ immediate: true }
);
return { _active, cacheBuster, loading, error, file, fileSrc };
return { _active, cacheBuster, loading, file, fileSrc };
async function fetchFile() {
cacheBuster.value = nanoid();
@@ -103,7 +103,7 @@ export default defineComponent({
file.value = response.data.data;
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -114,6 +114,7 @@ import Vue from 'vue';
import Cropper from 'cropperjs';
import { nanoid } from 'nanoid';
import throttle from 'lodash/throttle';
import { unexpectedError } from '@/utils/unexpected-error';
type Image = {
type: string;
@@ -260,7 +261,7 @@ export default defineComponent({
emit('refresh');
_active.value = false;
} catch (err) {
console.error(err);
unexpectedError(err);
} finally {
saving.value = false;
}

View File

@@ -0,0 +1,4 @@
import NotificationDialogs from './notification-dialogs.vue';
export { NotificationDialogs };
export default NotificationDialogs;

View File

@@ -0,0 +1,89 @@
<template>
<div class="notification-dialogs">
<v-dialog :active="true" v-for="notification in notifications" :key="notification.id" persist>
<v-card :class="[notification.type]">
<v-card-title>{{ notification.title }}</v-card-title>
<v-card-text v-if="notification.text">
{{ notification.text }}
<v-error v-if="notification.error" :error="notification.error" />
</v-card-text>
<v-card-actions>
<v-button
secondary
v-if="notification.type === 'error' && admin && notification.code === 'UNKNOWN'"
>
<a target="_blank" :href="getGitHubIssueLink(notification.id, notification)">
{{ $t('report_error') }}
</a>
</v-button>
<v-button @click="done(notification.id)">{{ $t('dismiss') }}</v-button>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref, watch } from '@vue/composition-api';
import SidebarButton from '../sidebar-button';
import NotificationItem from '../notification-item';
import { useNotificationsStore, useUserStore } from '@/stores/';
import router from '@/router';
import { Notification } from '@/types';
import { useProjectInfo } from '@/modules/settings/composables/use-project-info';
export default defineComponent({
components: { SidebarButton, NotificationItem },
setup(props) {
const { parsedInfo } = useProjectInfo();
const notificationsStore = useNotificationsStore();
const userStore = useUserStore();
const notifications = computed(() => notificationsStore.state.dialogs);
return { notifications, admin: userStore.isAdmin, done, getGitHubIssueLink };
function getGitHubIssueLink(id: string, notification: Notification) {
const debugInfo = `<!-- Please put a detailed explanation of the problem here. -->
---
### Project details
Directus Version: ${parsedInfo.value?.directus.version}
Environment: ${process.env.NODE_ENV}
OS: ${parsedInfo.value?.os.type} ${parsedInfo.value?.os.version}
Node: ${parsedInfo.value?.node.version}
### Error
Title: ${notification.title || 'none'}
Message: ${notification.text || 'none'}
<details>
<summary>Stack Trace</summary>
<pre>
${JSON.stringify(notification.error, Object.getOwnPropertyNames(notification.error), 2)}
</pre>
</details>
`;
return `https://github.com/directus/next/issues/new?body=${encodeURIComponent(debugInfo)}`;
}
function done(id: string) {
notificationsStore.remove(id);
}
},
});
</script>
<style lang="scss" scoped>
.notification-dialogs {
position: relative;
}
.v-error {
margin-top: 12px;
}
</style>

View File

@@ -48,6 +48,7 @@ import i18n from '@/lang';
import formatLocalized from '@/utils/localized-format';
import RevisionItem from './revision-item.vue';
import RevisionsDrawer from './revisions-drawer.vue';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
components: { RevisionItem, RevisionsDrawer },
@@ -62,10 +63,7 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { revisions, revisionsByDate, loading, error, refresh } = useRevisions(
props.collection,
props.primaryKey
);
const { revisions, revisionsByDate, loading, refresh } = useRevisions(props.collection, props.primaryKey);
const hasCreate = computed(() => {
// We expect the very first revision record to be a creation
@@ -83,7 +81,6 @@ export default defineComponent({
revisions,
revisionsByDate,
loading,
error,
refresh,
hasCreate,
modalActive,
@@ -100,15 +97,13 @@ export default defineComponent({
function useRevisions(collection: string, primaryKey: number | string) {
const revisions = ref<Revision[] | null>(null);
const revisionsByDate = ref<RevisionsByDate[] | null>(null);
const error = ref(null);
const loading = ref(false);
getRevisions();
return { revisions, revisionsByDate, error, loading, refresh };
return { revisions, revisionsByDate, loading, refresh };
async function getRevisions() {
error.value = null;
loading.value = true;
try {
@@ -172,7 +167,7 @@ export default defineComponent({
revisionsByDate.value = orderBy(revisionsGrouped, ['date'], ['desc']);
revisions.value = orderBy(response.data.data, ['activity.timestamp'], ['desc']);
} catch (err) {
error.value = err;
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -58,6 +58,7 @@ import RevisionsDrawerPicker from './revisions-drawer-picker.vue';
import RevisionsDrawerPreview from './revisions-drawer-preview.vue';
import RevisionsDrawerUpdates from './revisions-drawer-updates.vue';
import api from '@/api';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
components: { RevisionsDrawerPicker, RevisionsDrawerPreview, RevisionsDrawerUpdates },
@@ -126,7 +127,7 @@ export default defineComponent({
_active.value = false;
emit('revert');
} catch (err) {
console.error(err);
unexpectedError(err);
} finally {
reverting.value = false;
}

View File

@@ -34,6 +34,7 @@ import { defineComponent, computed, ref, PropType, watch } from '@vue/compositio
import api from '@/api';
import { useNotificationsStore } from '@/stores';
import i18n from '@/lang';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
model: {
@@ -79,12 +80,7 @@ export default defineComponent({
);
emit('toggle', false);
} catch (err) {
notifications.add({
title: i18n.t('server_error'),
text: err.message,
persist: true,
type: 'error',
});
unexpectedError(err);
} finally {
loading.value = false;
}

View File

@@ -51,6 +51,7 @@
<v-overlay class="sidebar-overlay" :active="sidebarOpen" @click="sidebarOpen = false" />
<notifications-group v-if="notificationsPreviewActive === false" :dense="sidebarOpen === false" />
<notification-dialogs />
</div>
</template>
@@ -63,6 +64,7 @@ import ProjectInfo from './components/project-info';
import SidebarButton from './components/sidebar-button/';
import NotificationsGroup from './components/notifications-group/';
import NotificationsPreview from './components/notifications-preview/';
import NotificationDialogs from './components/notification-dialogs/';
import { useUserStore, useAppStore } from '@/stores';
import i18n from '@/lang';
import emitter, { Events } from '@/events';
@@ -76,6 +78,7 @@ export default defineComponent({
SidebarButton,
NotificationsGroup,
NotificationsPreview,
NotificationDialogs,
},
props: {
title: {

View File

@@ -133,8 +133,8 @@ export default defineComponent({
transition: max-width var(--medium) var(--transition);
.content {
max-width: 100%;
width: 340px;
max-width: 100%;
}
&.wide {
@@ -161,8 +161,8 @@ export default defineComponent({
background-size: cover;
.foreground {
max-width: 400px;
width: 80%;
max-width: 400px;
}
.note {