From b549f588263cea37774d394347925bb1fa15e4f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarda=20Kot=C4=9B=C5=A1ovec?= Date: Wed, 4 Dec 2024 12:57:10 +0100 Subject: [PATCH] pkp/pkp-lib#6528 Initial UI for bulk delete of incomplete submissions for admins and authors --- src/components/Table/TableCellSelect.vue | 20 +++ src/composables/useCurrentUser.js | 22 ++- src/pages/dashboard/DashboardPage.vue | 10 +- .../components/DashboardBulkDeleteButton.vue | 27 ++++ .../DashboardTable/CellBulkDelete.vue | 40 +++++ .../DashboardTable/DashboardTable.vue | 12 +- .../composables/useDashboardBulkDelete.js | 140 ++++++++++++++++++ src/pages/dashboard/dashboardPageStore.js | 43 +++++- 8 files changed, 308 insertions(+), 6 deletions(-) create mode 100644 src/components/Table/TableCellSelect.vue create mode 100644 src/pages/dashboard/components/DashboardBulkDeleteButton.vue create mode 100644 src/pages/dashboard/components/DashboardTable/CellBulkDelete.vue create mode 100644 src/pages/dashboard/composables/useDashboardBulkDelete.js diff --git a/src/components/Table/TableCellSelect.vue b/src/components/Table/TableCellSelect.vue new file mode 100644 index 000000000..a6ffc8d18 --- /dev/null +++ b/src/components/Table/TableCellSelect.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/composables/useCurrentUser.js b/src/composables/useCurrentUser.js index 726a89d26..50627b88a 100644 --- a/src/composables/useCurrentUser.js +++ b/src/composables/useCurrentUser.js @@ -14,5 +14,25 @@ export function useCurrentUser() { return pkp.currentUser.id; } - return {hasCurrentUserAtLeastOneRole, getCurrentUserId}; + function hasCurrentUserAtLeastOneAssignedRoleInAnyStage( + submission, + roles = [], + ) { + const assignedRoleIds = []; + submission.stages.forEach((stage) => { + stage.currentUserAssignedRoles.forEach((assignedRoleId) => { + if (!assignedRoleIds.includes(assignedRoleId)) { + assignedRoleIds.push(assignedRoleId); + } + }); + }); + + return roles.some((role) => assignedRoleIds.includes(role)); + } + + return { + hasCurrentUserAtLeastOneRole, + getCurrentUserId, + hasCurrentUserAtLeastOneAssignedRoleInAnyStage, + }; } diff --git a/src/pages/dashboard/DashboardPage.vue b/src/pages/dashboard/DashboardPage.vue index 3b58c8e8d..6960d94d4 100644 --- a/src/pages/dashboard/DashboardPage.vue +++ b/src/pages/dashboard/DashboardPage.vue @@ -8,9 +8,12 @@
- - {{ t('common.filter') }} - +
+ + {{ t('common.filter') }} + + +
+ + {{ t('admin.submissions.incomplete.bulkDelete.button') }} + + + + diff --git a/src/pages/dashboard/components/DashboardTable/CellBulkDelete.vue b/src/pages/dashboard/components/DashboardTable/CellBulkDelete.vue new file mode 100644 index 000000000..811249b85 --- /dev/null +++ b/src/pages/dashboard/components/DashboardTable/CellBulkDelete.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/pages/dashboard/components/DashboardTable/DashboardTable.vue b/src/pages/dashboard/components/DashboardTable/DashboardTable.vue index c46dee716..e69135738 100644 --- a/src/pages/dashboard/components/DashboardTable/DashboardTable.vue +++ b/src/pages/dashboard/components/DashboardTable/DashboardTable.vue @@ -4,6 +4,11 @@ @sort="(columnId) => $emit('sortColumn', columnId)" > + + + {{ t('admin.submissions.incomplete.bulkDelete.column.description') }} + + + diff --git a/src/pages/dashboard/composables/useDashboardBulkDelete.js b/src/pages/dashboard/composables/useDashboardBulkDelete.js new file mode 100644 index 000000000..4774f2360 --- /dev/null +++ b/src/pages/dashboard/composables/useDashboardBulkDelete.js @@ -0,0 +1,140 @@ +import {ref, computed} from 'vue'; + +import {useCurrentUser} from '@/composables/useCurrentUser'; +import {useFetch} from '@/composables/useFetch'; +import {useUrl} from '@/composables/useUrl'; +import {useModal} from '@/composables/useModal'; +import {useLocalize} from '@/composables/useLocalize'; +import {DashboardPageTypes} from '../dashboardPageStore'; + +export function useDashboardBulkDelete({ + submissions, + dashboardPage, + onSubmissionDeleteCallback, +}) { + const {t} = useLocalize(); + + // Display only for admins on editorial dashboard and for author dashboard, where user can delete own submissions + const bulkDeleteDisplayDeleteButton = computed(() => { + if ( + dashboardPage === DashboardPageTypes.EDITORIAL_DASHBOARD && + hasCurrentUserAtLeastOneRole([pkp.const.ROLE_ID_SITE_ADMIN]) + ) { + return true; + } else if (dashboardPage === DashboardPageTypes.MY_SUBMISSIONS) { + return true; + } + + return false; + }); + + const bulkDeleteEnabled = ref(false); + const bulkDeleteSelectedItems = ref([]); + function bulkDeleteSelectItem(submissionId) { + if (!bulkDeleteSelectedItems.value.includes(submissionId)) { + bulkDeleteSelectedItems.value.push(submissionId); + } + } + + function bulkDeleteDeselectItem(submissionId) { + bulkDeleteSelectedItems.value = bulkDeleteSelectedItems.value.filter( + (id) => id !== submissionId, + ); + } + + function bulkDeleteToggleEnabled() { + bulkDeleteEnabled.value = !bulkDeleteEnabled.value; + } + + const { + hasCurrentUserAtLeastOneAssignedRoleInAnyStage, + hasCurrentUserAtLeastOneRole, + } = useCurrentUser(); + + function canBeDeleted(submission) { + // incomplete submission can be deleted by author of the submission or admin + if (submission.submissionProgress) + if ( + hasCurrentUserAtLeastOneRole([pkp.const.ROLE_ID_SITE_ADMIN]) || + hasCurrentUserAtLeastOneAssignedRoleInAnyStage(submission, [ + pkp.const.ROLE_ID_AUTHOR, + ]) + ) { + return true; + } + + return false; + } + + const bulkDeleteSubmissionIdsCanBeDeleted = computed(() => { + const submissionIds = []; + if (submissions.value && submissions.value?.length) { + submissions.value.forEach((submission) => { + if (canBeDeleted(submission)) { + submissionIds.push(submission.id); + } + }); + } + + return submissionIds; + }); + + async function apiCall() { + const {apiUrl} = useUrl(`_submissions`); + const {fetch} = useFetch(apiUrl, { + query: {ids: bulkDeleteSelectedItems.value}, + method: 'DELETE', + }); + + await fetch(); + + bulkDeleteResetSelection(); + onSubmissionDeleteCallback(); + } + + function bulkDeleteResetSelection() { + bulkDeleteSelectedItems.value = []; + bulkDeleteEnabled.value = false; + } + + function bulkDeleteActionDelete() { + const {openDialog} = useModal(); + openDialog({ + title: t('admin.submissions.incomplete.bulkDelete.confirm'), + message: t('admin.submissions.incomplete.bulkDelete.body'), + actions: [ + { + label: t('common.confirm'), + isPrimary: true, + callback: async (close) => { + await apiCall(); + close(); + }, + }, + { + label: t('common.cancel'), + isWarnable: true, + callback: (close) => { + close(); + }, + }, + ], + modalStyle: 'negative', + close: () => {}, + }); + } + + return { + bulkDeleteDisplayDeleteButton, + bulkDeleteEnabled, + bulkDeleteToggleEnabled, + + bulkDeleteSelectedItems, + bulkDeleteSelectItem, + bulkDeleteDeselectItem, + bulkDeleteResetSelection, + bulkDeleteActionDelete, + + bulkDeleteSubmissionIdsCanBeDeleted, + }; +} diff --git a/src/pages/dashboard/dashboardPageStore.js b/src/pages/dashboard/dashboardPageStore.js index 212317cc4..e64d8f66e 100644 --- a/src/pages/dashboard/dashboardPageStore.js +++ b/src/pages/dashboard/dashboardPageStore.js @@ -12,7 +12,7 @@ import {defineComponentStore} from '@/utils/defineComponentStore'; import {useWorkflowActions} from '../workflow/composables/useWorkflowActions'; import {useReviewerManagerActions} from '@/managers/ReviewerManager/useReviewerManagerActions'; - +import {useDashboardBulkDelete} from './composables/useDashboardBulkDelete'; import {useParticipantManagerActions} from '@/managers/ParticipantManager/useParticipantManagerActions'; import {useEditorialLogic} from './composables/useEditorialLogic'; @@ -234,6 +234,35 @@ export const useDashboardPageStore = defineComponentStore( fetchSubmissions(); } + /** + * Bulk delete + */ + const { + bulkDeleteDisplayDeleteButton, + + bulkDeleteEnabled, + bulkDeleteToggleEnabled, + + bulkDeleteSelectedItems, + bulkDeleteSelectItem, + bulkDeleteDeselectItem, + bulkDeleteSubmissionIdsCanBeDeleted, + bulkDeleteActionDelete, + + bulkDeleteResetSelection, + } = useDashboardBulkDelete({ + submissions, + dashboardPage: pageInitConfig.dashboardPage, + onSubmissionDeleteCallback: () => { + fetchSubmissions(); + }, + }); + + // reset selection when changing view/search/filters + watch(submissionsQuery, () => { + bulkDeleteResetSelection(); + }); + /** * Reviewer actions, * available for review assignments popup @@ -406,6 +435,18 @@ export const useDashboardPageStore = defineComponentStore( fetchSubmissions, setCurrentPage, + // Bulk delete + bulkDeleteDisplayDeleteButton, + + bulkDeleteEnabled, + bulkDeleteToggleEnabled, + + bulkDeleteSelectedItems, + bulkDeleteSelectItem, + bulkDeleteDeselectItem, + bulkDeleteSubmissionIdsCanBeDeleted, + bulkDeleteActionDelete, + // Workflow Page workflowSubmissionId,