Skip to content

Commit

Permalink
pkp/pkp-lib#6528 Initial UI for bulk delete of incomplete submissions…
Browse files Browse the repository at this point in the history
… for admins and authors
  • Loading branch information
jardakotesovec committed Dec 5, 2024
1 parent 14c7d53 commit 8168537
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 6 deletions.
20 changes: 20 additions & 0 deletions src/components/Table/TableCellSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<TableCell>
<input
type="checkbox"
:checked="props.checked"
:aria-describedby="describedBy"
@change="emit('change', $event.target.checked)"
/>
</TableCell>
</template>

<script setup>
import TableCell from './TableCell.vue';
const props = defineProps({
checked: {type: Boolean, required: true},
describedBy: {type: String, required: true},
});
const emit = defineEmits(['change']);
</script>
22 changes: 21 additions & 1 deletion src/composables/useCurrentUser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
10 changes: 7 additions & 3 deletions src/pages/dashboard/DashboardPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
</h2>
<div class="mt-2">
<div class="flex justify-between">
<PkpButton @click="store.openFiltersModal">
{{ t('common.filter') }}
</PkpButton>
<div class="flex flex-row space-x-2">
<PkpButton @click="store.openFiltersModal">
{{ t('common.filter') }}
</PkpButton>
<DashboardBulkDeleteButton />
</div>
<div>
<Search
:search-phrase="store.searchPhrase"
Expand Down Expand Up @@ -46,6 +49,7 @@
import PkpButton from '@/components/Button/Button.vue';
import DashboardActiveFilters from './components/DashboardActiveFilters.vue';
import DashboardTable from './components/DashboardTable/DashboardTable.vue';
import DashboardBulkDeleteButton from './components/DashboardBulkDeleteButton.vue';
import Search from '@/components/Search/Search.vue';
import {useDashboardPageStore} from './dashboardPageStore';
Expand Down
27 changes: 27 additions & 0 deletions src/pages/dashboard/components/DashboardBulkDeleteButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<template>
<PkpButton
v-if="store.bulkDeleteDisplayDeleteButton"
:is-disabled="store.bulkDeleteSubmissionIdsCanBeDeleted.length === 0"
:is-active="
store.bulkDeleteEnabled && store.bulkDeleteSelectedItems.length === 0
"
:is-warnable="!!store.bulkDeleteSelectedItems.length"
@click="
() =>
store.bulkDeleteSelectedItems.length
? store.bulkDeleteActionDelete()
: store.bulkDeleteToggleEnabled()
"
>
{{ t('admin.submissions.incomplete.bulkDelete.button') }}
</PkpButton>
</template>

<script setup>
import PkpButton from '@/components/Button/Button.vue';
import {useLocalize} from '@/composables/useLocalize';
import {useDashboardPageStore} from '../dashboardPageStore';
const store = useDashboardPageStore();
const {t} = useLocalize();
</script>
40 changes: 40 additions & 0 deletions src/pages/dashboard/components/DashboardTable/CellBulkDelete.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<template>
<!--<TableCellSelect :value="isChecked" @change="toggle"></TableCellSelect>-->

<TableCellSelect
v-if="canBeDeleted"
:checked="isChecked"
:described-by="'submission-title-' + item.id"
@change="change"
/>
<TableCell v-else></TableCell>
</template>

<script setup>
import {computed} from 'vue';
import TableCellSelect from '@/components/Table/TableCellSelect.vue';
import TableCell from '@/components/Table/TableCell.vue';
import {useDashboardPageStore} from '@/pages/dashboard/dashboardPageStore';
const props = defineProps({item: {type: Object, required: true}});
const dashboardStore = useDashboardPageStore();
const isChecked = computed(() => {
return dashboardStore.bulkDeleteSelectedItems.includes(props.item.id);
});
const canBeDeleted = computed(() => {
return dashboardStore.bulkDeleteSubmissionIdsCanBeDeleted.includes(
props.item.id,
);
});
function change(checked) {
if (checked) {
dashboardStore.bulkDeleteSelectItem(props.item.id);
} else {
dashboardStore.bulkDeleteDeselectItem(props.item.id);
}
}
</script>
12 changes: 11 additions & 1 deletion src/pages/dashboard/components/DashboardTable/DashboardTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
@sort="(columnId) => $emit('sortColumn', columnId)"
>
<TableHeader>
<TableColumn v-if="dashboardStore.bulkDeleteEnabled">
<span class="sr-only">
{{ t('admin.submissions.incomplete.bulkDelete.column.description') }}
</span>
</TableColumn>
<TableColumn
v-for="column in columns"
:id="column.id"
Expand All @@ -15,6 +20,7 @@
</TableHeader>
<TableBody>
<TableRow v-for="item in items" :key="item.id">
<CellBulkDelete v-if="dashboardStore.bulkDeleteEnabled" :item="item" />
<component
:is="cellComponents[column.componentName] || column.componentName"
v-for="column in columns"
Expand All @@ -38,7 +44,7 @@ import TableHeader from '@/components/Table/TableHeader.vue';
import TableBody from '@/components/Table/TableBody.vue';
import TableRow from '@/components/Table/TableRow.vue';
import TablePagination from '@/components/Table/TablePagination.vue';
import CellBulkDelete from './CellBulkDelete.vue';
import CellSubmissionActions from './CellSubmissionActions.vue';
import CellSubmissionActivity from './CellSubmissionActivity/CellSubmissionActivity.vue';
import CellSubmissionDays from './CellSubmissionDays.vue';
Expand All @@ -51,6 +57,8 @@ import CellReviewAssignmentTitle from './CellReviewAssignmentTitle.vue';
import CellReviewAssignmentActivity from './CellReviewAssignmentActivity.vue';
import CellReviewAssignmentActions from './CellReviewAssignmentActions.vue';
import {useDashboardPageStore} from '@/pages/dashboard/dashboardPageStore';
defineProps({
items: {type: Array, required: true},
columns: {type: Array, required: true},
Expand All @@ -71,4 +79,6 @@ const cellComponents = {
CellReviewAssignmentActivity,
CellReviewAssignmentActions,
};
const dashboardStore = useDashboardPageStore();
</script>
140 changes: 140 additions & 0 deletions src/pages/dashboard/composables/useDashboardBulkDelete.js
Original file line number Diff line number Diff line change
@@ -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,
};
}
43 changes: 42 additions & 1 deletion src/pages/dashboard/dashboardPageStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -406,6 +435,18 @@ export const useDashboardPageStore = defineComponentStore(
fetchSubmissions,
setCurrentPage,

// Bulk delete
bulkDeleteDisplayDeleteButton,

bulkDeleteEnabled,
bulkDeleteToggleEnabled,

bulkDeleteSelectedItems,
bulkDeleteSelectItem,
bulkDeleteDeselectItem,
bulkDeleteSubmissionIdsCanBeDeleted,
bulkDeleteActionDelete,

// Workflow Page
workflowSubmissionId,

Expand Down

0 comments on commit 8168537

Please sign in to comment.