Add error alert on delete / soft delete from browse

This commit is contained in:
rijkvanzanten
2020-08-31 15:04:36 -04:00
parent d8ae3b9a8d
commit e10c288920
9 changed files with 190 additions and 45 deletions

View File

@@ -1,7 +1,15 @@
import { BaseException } from './base';
import { Permission } from '../types';
type Extensions = {
field?: string;
collection?: string;
item?: string | number | (string | number)[];
action?: Permission['action'];
}
export class ForbiddenException extends BaseException {
constructor(message = `You don't have permission to access this.`) {
super(message, 403, 'NO_PERMISSION');
constructor(message = `You don't have permission to access this.`, extensions?: Extensions) {
super(message, 403, 'NO_PERMISSION', extensions);
}
}

View File

@@ -239,9 +239,12 @@ export default class AuthorizationService {
const result = await itemsService.readByKey(pk as any, query, action);
if (!result) throw '';
if (Array.isArray(pk) && result.length !== pk.length) throw '';
} catch {
throw new ForbiddenException(
`You're not allowed to ${action} item "${pk}" in collection "${collection}".`
`You're not allowed to ${action} item "${pk}" in collection "${collection}".`, {
collection, item: pk, action
}
);
}
}

View File

@@ -56,10 +56,12 @@ export const onError = async (error: RequestError) => {
if (status === 401 && code === 'INVALID_CREDENTIALS' && error.request.responseURL.includes('refresh') === false) {
try {
await refresh();
return api.request(error.config);
} catch {
logout({ reason: LogoutReason.ERROR_SESSION_EXPIRED });
return Promise.reject();
}
return api.request(error.config);
}
return Promise.reject(error);

View File

@@ -122,6 +122,7 @@ export default defineComponent({
const sizeClass = useSizeClass(props);
const component = computed<'a' | 'router-link' | 'button'>(() => {
if (props.disabled) return 'button';
if (notEmpty(props.href)) return 'a';
if (notEmpty(props.to)) return 'router-link';
return 'button';

View File

@@ -1,6 +1,6 @@
<template>
<div class="v-error">
<output>{{ code }}</output>
<output>Code: {{ code }}</output>
<v-icon
v-tooltip="$t('copy_details')"
v-if="showCopy"
@@ -34,7 +34,8 @@ export default defineComponent({
return { code, copyError, showCopy, copied };
async function copyError() {
await navigator.clipboard.writeText(JSON.stringify(props.error, null, 2));
const error = props.error?.response?.data || props.error;
await navigator.clipboard.writeText(JSON.stringify(error, null, 2));
copied.value = true;
}
},

View File

@@ -23,6 +23,7 @@
"move_to_trash": "Move to Trash",
"move_to_trash_confirm": "Are you sure you want to move this item to the trash?",
"move_to_trash_confirm_count": "No Items Selected | Are you sure you want to move this item to the trash? | Are you sure you want to move these {count} items to the trash?",
"nested_files_folders_will_be_moved": "Nested files and folders will be moved one level up.",

View File

@@ -48,8 +48,15 @@
<v-dialog v-model="confirmDelete" v-if="selection.length > 0">
<template #activator="{ on }">
<v-button rounded icon class="action-delete" @click="on" v-tooltip.bottom="$t('delete')">
<v-icon name="delete" outline />
<v-button
:disabled="batchDeleteAllowed !== true"
rounded
icon
class="action-delete"
@click="on"
v-tooltip.bottom="batchDeleteAllowed ? $t('delete') : $t('not_allowed')"
>
<v-icon name="delete_forever" outline />
</v-button>
</template>
@@ -67,13 +74,45 @@
</v-card>
</v-dialog>
<v-dialog
v-model="confirmSoftDelete"
v-if="selection.length > 0 && currentCollection.meta && currentCollection.meta.soft_delete_field"
>
<template #activator="{ on }">
<v-button
:disabled="batchSoftDeleteAllowed !== true"
rounded
icon
class="action-soft-delete"
@click="on"
v-tooltip.bottom="batchEditAllowed ? $t('move_to_trash') : $t('not_allowed')"
>
<v-icon name="delete" outline />
</v-button>
</template>
<v-card>
<v-card-title>{{ $tc('move_to_trash_confirm_count', selection.length) }}</v-card-title>
<v-card-actions>
<v-button @click="confirmSoftDelete = false" secondary>
{{ $t('cancel') }}
</v-button>
<v-button @click="batchDelete" class="action-soft-delete" :loading="softDeleting">
{{ $t('move_to_trash') }}
</v-button>
</v-card-actions>
</v-card>
</v-dialog>
<v-button
rounded
icon
class="action-batch"
v-if="selection.length > 1"
:to="batchLink"
v-tooltip.bottom="$t('edit')"
v-tooltip.bottom="batchEditAllowed ? $t('edit') : $t('not_allowed')"
:disabled="batchEditAllowed === false"
>
<v-icon name="edit" outline />
</v-button>
@@ -152,6 +191,18 @@
<layout-drawer-detail @input="viewType = $event" :value="viewType" />
<portal-target name="drawer" />
</template>
<v-dialog v-if="deleteError" active>
<v-card>
<v-card-title>{{ $t('something_went_wrong') }}</v-card-title>
<v-card-text>
<v-error :error="deleteError" />
</v-card-text>
<v-card-actions>
<v-button @click="deleteError = null">{{ $t('done') }}</v-button>
</v-card-actions>
</v-card>
</v-dialog>
</private-view>
</template>
@@ -169,6 +220,7 @@ import BookmarkAdd from '@/views/private/components/bookmark-add';
import BookmarkEdit from '@/views/private/components/bookmark-edit';
import router from '@/router';
import marked from 'marked';
import { usePermissionsStore } from '@/stores';
type Item = {
[field: string]: any;
@@ -195,6 +247,7 @@ export default defineComponent({
},
},
setup(props) {
const permissionsStore = usePermissionsStore();
const layout = ref<LayoutComponent | null>(null);
const { collection } = toRefs(props);
@@ -215,7 +268,15 @@ export default defineComponent({
saveCurrentAsBookmark,
title: bookmarkName,
} = usePreset(collection, bookmarkID);
const { confirmDelete, deleting, batchDelete } = useBatchDelete();
const {
confirmDelete,
deleting,
batchDelete,
confirmSoftDelete,
softDelete,
softDeleting,
error: deleteError,
} = useBatchDelete();
const {
bookmarkDialogActive,
@@ -235,6 +296,8 @@ export default defineComponent({
{ immediate: true }
);
const { batchEditAllowed, batchSoftDeleteAllowed, batchDeleteAllowed } = usePermissions();
return {
addNewLink,
batchDelete,
@@ -262,6 +325,13 @@ export default defineComponent({
breadcrumb,
marked,
clearFilters,
confirmSoftDelete,
softDelete,
softDeleting,
batchEditAllowed,
batchSoftDeleteAllowed,
batchDeleteAllowed,
deleteError,
};
function useBreadcrumb() {
@@ -291,28 +361,50 @@ export default defineComponent({
const confirmDelete = ref(false);
const deleting = ref(false);
return { confirmDelete, deleting, batchDelete };
const confirmSoftDelete = ref(false);
const softDeleting = ref(false);
const error = ref<any>();
return { confirmDelete, deleting, batchDelete, confirmSoftDelete, softDeleting, softDelete, error };
async function batchDelete() {
deleting.value = true;
confirmDelete.value = false;
const batchPrimaryKeys = selection.value;
try {
await api.delete(`/items/${props.collection}/${batchPrimaryKeys}`);
await layout.value?.refresh?.();
selection.value = [];
confirmDelete.value = false;
} catch (err) {
console.error(err);
error.value = err;
} finally {
deleting.value = false;
}
}
async function softDelete() {
if (!currentCollection.value?.meta?.soft_delete_field) return;
softDeleting.value = true;
const batchPrimaryKeys = selection.value;
try {
await api.patch(`/items/${props.collection}/${batchPrimaryKeys}`);
await layout.value?.refresh?.();
selection.value = [];
confirmSoftDelete.value = false;
} catch (err) {
error.value = err;
} finally {
softDeleting.value = false;
}
}
}
function useLinks() {
@@ -374,6 +466,36 @@ export default defineComponent({
filters.value = [];
searchQuery.value = null;
}
function usePermissions() {
const batchEditAllowed = computed(() => {
const updatePermissions = permissionsStore.state.permissions.find(
(permission) => permission.action === 'update' && permission.collection === collection.value
);
return !!updatePermissions;
});
const batchSoftDeleteAllowed = computed(() => {
if (!currentCollection.value?.meta?.soft_delete_field) return false;
const updatePermissions = permissionsStore.state.permissions.find(
(permission) => permission.action === 'update' && permission.collection === collection.value
);
if (!updatePermissions) return false;
if (!updatePermissions.fields) return false;
if (updatePermissions.fields === '*') return true;
return updatePermissions.fields.split(',').includes(currentCollection.value.meta.soft_delete_field);
});
const batchDeleteAllowed = computed(() => {
const deletePermissions = permissionsStore.state.permissions.find(
(permission) => permission.action === 'delete' && permission.collection === collection.value
);
return !!deletePermissions;
});
return { batchEditAllowed, batchSoftDeleteAllowed, batchDeleteAllowed };
}
},
});
</script>
@@ -386,6 +508,13 @@ export default defineComponent({
--v-button-color-hover: var(--danger);
}
.action-soft-delete {
--v-button-background-color: var(--warning-25);
--v-button-color: var(--warning);
--v-button-background-color-hover: var(--warning-50);
--v-button-color-hover: var(--warning);
}
.action-batch {
--v-button-background-color: var(--warning-25);
--v-button-color: var(--warning);

View File

@@ -189,8 +189,9 @@ import i18n from '@/lang';
import marked from 'marked';
import useShortcut from '@/composables/use-shortcut';
import { NavigationGuard } from 'vue-router';
import { usePermissionsStore, useUserStore } from '@/stores';
import { useUserStore } from '@/stores';
import generateJoi from '@/utils/generate-joi';
import { isAllowed } from '@/utils/is-allowed';
type Values = {
[field: string]: any;
@@ -216,7 +217,6 @@ export default defineComponent({
},
},
setup(props) {
const permissionsStore = usePermissionsStore();
const userStore = useUserStore();
const { collection, primaryKey } = toRefs(props);
@@ -384,42 +384,17 @@ export default defineComponent({
}
function usePermissions() {
const permissions = computed(() => {
return permissionsStore.state.permissions.filter(
(permission) => permission.collection === props.collection
);
});
const deleteAllowed = computed(() => isAllowed('delete'));
const saveAllowed = computed(() => isAllowed('update'));
const deleteAllowed = computed(() => isAllowed(collection.value, 'delete', item.value));
const saveAllowed = computed(() => isAllowed(collection.value, 'update', item.value));
const softDeleteAllowed = computed(() => {
if (!collectionInfo.value?.meta?.soft_delete_field) return false;
return isAllowed('update', {
return isAllowed(collection.value, 'update', {
[collectionInfo.value.meta.soft_delete_field]: collectionInfo.value.meta.soft_delete_value,
});
});
return { deleteAllowed, saveAllowed, softDeleteAllowed };
function isAllowed(action: string, value?: any) {
value = value || item.value;
if (userStore.isAdmin.value === true) return true;
const permissionInfo = permissions.value.find((permission) => permission.action === action);
if (!permissionInfo) return false;
const schema = generateJoi(permissionInfo.permissions, { allowUnknown: permissionInfo.fields === '*' });
const { error } = schema.validate(value);
if (!error) {
return true;
}
return false;
}
}
},
});

View File

@@ -0,0 +1,25 @@
import { usePermissionsStore, useUserStore } from '@/stores';
import { Permission } from '@/types';
import generateJoi from '@/utils/generate-joi';
export function isAllowed(collection: string, action: Permission['action'], value: Record<string, any>) {
const permissionsStore = usePermissionsStore();
const userStore = useUserStore();
if (userStore.isAdmin.value === true) return true;
const permissions = permissionsStore.state.permissions;
const permissionInfo = permissions.find((permission) => permission.action === action);
if (!permissionInfo) return false;
const schema = generateJoi(permissionInfo.permissions, { allowUnknown: permissionInfo.fields === '*' });
const { error } = schema.validate(value);
if (!error) {
return true;
}
return false;
}