Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow bulk deletion of Incomplete Submissions #390

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/components/ListPanel/ListPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
<slot name="itemsEmpty">{{ currentEmptyLabel }}</slot>
</div>
<ul v-else class="listPanel__itemsList">
<li
v-if="!!$slots['sub-action']"
class="listPanel__item listPanel__item__sub_action"
>
<slot name="sub-action"></slot>
</li>
<li v-for="item in items" :key="item.id" class="listPanel__item">
<slot name="item" :item="item">
<div class="listPanel__itemSummary">
Expand Down Expand Up @@ -333,6 +339,11 @@ export default {
}
}

.listPanel__item__sub_action {
padding-top: 0;
padding-bottom: 0;
}

.listPanel__footer {
padding: 0.5rem;
}
Expand Down
41 changes: 39 additions & 2 deletions src/components/ListPanel/submissions/SubmissionsListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
<div class="listPanel__itemSummary">
<div class="listPanel__itemIdentity listPanel__itemIdentity--submission">
<div class="listPanel__item--submission__id">
{{ item.id }}
<input
v-if="currentUserCanBulkDeleteIncompleteSubmissions"
type="checkbox"
name="incomplete-submissions"
:checked="isSelectedForDeletion"
class="listPanel__item--submission__checkbox"
@change="toggleSelection"
/>
<span>{{ item.id }}</span>
</div>
<div class="listPanel__itemTitle">
<span v-if="currentUserIsReviewer">
Expand Down Expand Up @@ -254,6 +262,10 @@ export default {
type: Object,
required: true,
},
isSelectedForDeletion: {
type: Boolean,
required: false,
},
},
data() {
return {
Expand Down Expand Up @@ -299,6 +311,18 @@ export default {
return false; // @todo
},

/**
* Can the current user bulk delete incomplete submissions?
*
* @return {Boolean}
*/
currentUserCanBulkDeleteIncompleteSubmissions() {
return (
this.userHasRole(pkp.const.ROLE_ID_SITE_ADMIN) &&
this.item.submissionProgress
);
},

/**
* Can the current user view the info center?
*
Expand Down Expand Up @@ -867,6 +891,11 @@ export default {

return hasRole;
},

/** Toggle selection of a Submission for bulk delete */
toggleSelection() {
this.$emit('selectedForBulkDelete', this.item.id);
},
},
};
</script>
Expand All @@ -880,7 +909,7 @@ export default {

.listPanel__itemIdentity--submission,
.listPanel__itemExpanded--submission {
padding-inline-start: 2.5rem;
padding-inline-start: 5rem;
}

.listPanel__item--submission__id {
Expand All @@ -890,6 +919,14 @@ export default {
font-size: @font-tiny;
line-height: 1.5rem; // Match baseline of title/author
color: @text;
display: flex;
justify-content: flex-end;
gap: 1rem;
width: 3rem;
}

.listPanel__item--submission__checkbox {
cursor: pointer;
}

.listPanel__item--submission__title,
Expand Down
172 changes: 170 additions & 2 deletions src/components/ListPanel/submissions/SubmissionsListPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
:search-phrase="searchPhrase"
@search-phrase-changed="setSearchPhrase"
/>
<PkpButton
v-if="currentUserCanBulkDeleteIncompleteSubmissions"
:is-warnable="hasSubmissionsSelectedForDeletion"
@click="promptDeleteConfirmation"
>
{{ t('common.delete') }}
</PkpButton>
<PkpButton
:is-active="isSidebarVisible"
@click="isSidebarVisible = !isSidebarVisible"
Expand Down Expand Up @@ -69,6 +76,21 @@
</template>
</template>

<template v-if="hasSubmissionsSelectedForDeletion" #sub-action>
<div>
<span class="font-bold">
{{ t('admin.submissions.incomplete.bulkDelete.selectionStatus') }}
</span>
<span class="text-primary">
<PkpButton
:is-link="true"
@click="toggleAllIncompleteSubmissionsSelection()"
>
{{ checkAllIncompleteSubmissionText }}
</PkpButton>
</span>
</div>
</template>
<template #item="{item}">
<slot name="item" :item="item">
<SubmissionsListItem
Expand All @@ -77,7 +99,11 @@
:api-url="apiUrl"
:info-url="infoUrl"
:assign-participant-url="assignParticipantUrl"
:is-selected-for-deletion="
selectedIncompleteSubmissions.includes(item.id)
"
@addFilter="addFilter"
@selectedForBulkDelete="toggleSubmissionSelection"
/>
</slot>
</template>
Expand Down Expand Up @@ -106,6 +132,7 @@ import PkpFilterAutosuggest from '@/components/Filter/FilterAutosuggest.vue';
import PkpHeader from '@/components/Header/Header.vue';
import Search from '@/components/Search/Search.vue';
import SubmissionsListItem from '@/components/ListPanel/submissions/SubmissionsListItem.vue';
import dialog from '@/mixins/dialog';
import fetch from '@/mixins/fetch';

export default {
Expand All @@ -122,7 +149,7 @@ export default {
Search,
SubmissionsListItem,
},
mixins: [fetch],
mixins: [fetch, dialog],
props: {
/** The URL to make a new submission. */
addUrl: {
Expand Down Expand Up @@ -187,6 +214,7 @@ export default {
data() {
return {
isSidebarVisible: false,
selectedIncompleteSubmissions: [],
};
},
computed: {
Expand Down Expand Up @@ -216,6 +244,52 @@ export default {
])
);
},

/**
* Does the user currently have any Incomplete submissions selected for deletion?
* @returns {Boolean}
*/
hasSubmissionsSelectedForDeletion() {
return !!this.selectedIncompleteSubmissions.length;
},

/**
* Has the user selected all incomplete submissions?
* @returns {Boolean}
*/
hasSelectedAllIncompleteSubmissions() {
return (
this.selectedIncompleteSubmissions.length ===
this.incompleteSubmissions.length
);
},

/**
* Get the Incomplete Submissions
* @returns {Array}
*/
incompleteSubmissions() {
return this.items.filter((submission) => submission.submissionProgress);
},

/**
* Returns a text label for toggling the selection of all incomplete submissions.
* If all incomplete submissions are selected, it returns the label for deselecting all.
* If not all incomplete submissions are selected, it returns the label for selecting all.
*/
checkAllIncompleteSubmissionText() {
return this.hasSelectedAllIncompleteSubmissions
? this.t('common.deselectAll')
: this.t('common.selectAll');
},

/**
* Can the current user bulk delete incomplete submissions?
* @return {Boolean}
*/
currentUserCanBulkDeleteIncompleteSubmissions() {
return this.userHasRole(pkp.const.ROLE_ID_SITE_ADMIN);
},
},
mounted() {
/**
Expand Down Expand Up @@ -342,7 +416,6 @@ export default {
itemsMax,
});
},

/**
* Helper function to determine if the current user has a role
*
Expand All @@ -363,6 +436,101 @@ export default {

return hasRole;
},

/**
* Toggles the selection of a specific Incomplete submission with given ID.
* Adds the submission to the selection list if it is not already selected.
* Removes the submission from the selection list if it is currently selected.
*/
toggleSubmissionSelection(id) {
const existingEntry = this.selectedIncompleteSubmissions.find(
(submissionId) => submissionId === id,
);

if (!existingEntry) {
this.selectedIncompleteSubmissions.push(id);
} else {
this.selectedIncompleteSubmissions =
this.selectedIncompleteSubmissions.filter(
(selectedId) => selectedId !== id,
);
}
},

/**
* Selects or deselects all incomplete submissions based on current selection state.
* If all Incomplete submissions are selected, it clears the selection.
* If not all Incomplete submissions are selected, it selects them all.
*/
toggleAllIncompleteSubmissionsSelection() {
this.selectedIncompleteSubmissions = this
.hasSelectedAllIncompleteSubmissions
? []
: this.incompleteSubmissions.map(({id}) => id);
},

/**
* Delete selected submissions
*/
deleteIncompleteSubmissions(closeDialog) {
const self = this;

$.ajax({
url:
this.apiUrl +
`?${$.param({ids: self.selectedIncompleteSubmissions.join(',')})}`,
type: 'POST',
headers: {
'X-Csrf-Token': pkp.currentUser.csrfToken,
'X-Http-Method-Override': 'DELETE',
},
error: self.ajaxErrorCallback,
success() {
self.setItems(
self.items.filter(
(item) => !self.selectedIncompleteSubmissions.includes(item.id),
),
self.itemsMax - 1,
);
pkp.eventBus.$emit(
'notify',
self.t('admin.submissions.incomplete.bulkDelete.success'),
'success',
);
self.selectedIncompleteSubmissions = [];
closeDialog();
},
});
},

/**
* Display a confirmation prompt before deleting submissions
*/
promptDeleteConfirmation() {
if (!this.hasSubmissionsSelectedForDeletion) {
return;
}

this.openDialog({
name: 'bulkDeleteConfirmation',
title: this.t('admin.submissions.incomplete.bulkDelete.confirm'),
message: this.t('admin.submissions.incomplete.bulkDelete.body'),
actions: [
{
label: this.t('common.confirm'),
isPrimary: true,
callback: (close) => {
this.deleteIncompleteSubmissions(close);
},
},
{
label: this.t('common.cancel'),
isWarnable: true,
callback: (close) => close(),
},
],
});
},
},
};
</script>