diff --git a/api/v1/_submissions/PKPBackendSubmissionsController.php b/api/v1/_submissions/PKPBackendSubmissionsController.php
index 09e29bdb00c..ca3f184e443 100644
--- a/api/v1/_submissions/PKPBackendSubmissionsController.php
+++ b/api/v1/_submissions/PKPBackendSubmissionsController.php
@@ -20,6 +20,7 @@
use APP\core\Application;
use APP\facades\Repo;
use APP\submission\Collector;
+use APP\submission\Submission;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@@ -87,6 +88,16 @@ public function getGroupRoutes(): void
),
]);
+ Route::delete('', $this->bulkDeleteIncompleteSubmissions(...))
+ ->name('_submission.incomplete.delete')
+ ->middleware([
+ self::roleAuthorizer([
+ Role::ROLE_ID_SITE_ADMIN,
+ Role::ROLE_ID_MANAGER,
+ Role::ROLE_ID_AUTHOR,
+ ]),
+ ]);
+
Route::delete('{submissionId}', $this->delete(...))
->name('_submission.delete')
->middleware([
@@ -430,6 +441,78 @@ public function delete(Request $illuminateRequest): JsonResponse
return response()->json([], Response::HTTP_OK);
}
+ /**
+ * Delete a list of incomplete submissions
+ */
+ public function bulkDeleteIncompleteSubmissions(Request $illuminateRequest): JsonResponse
+ {
+ $submissionIdsRaw = paramToArray($illuminateRequest->query('ids') ?? []);
+
+ if (empty($submissionIdsRaw)) {
+ return response()->json([
+ 'error' => __('api.submission.400.missingQueryParam'),
+ ], Response::HTTP_BAD_REQUEST);
+ }
+
+ $submissionIds = [];
+
+ foreach ($submissionIdsRaw as $id) {
+ $integerId = intval($id);
+
+ if (!$integerId) {
+ return response()->json([
+ 'error' => __('api.submission.400.invalidId', ['id' => $id])
+ ], Response::HTTP_BAD_REQUEST);
+ }
+
+ $submissionIds[] = $id;
+ }
+
+ $collector = $this->getSubmissionCollector($illuminateRequest->query())
+ ->filterBySubmissionIds($submissionIds)
+ ->filterByIncomplete(true);
+
+ $request = Application::get()->getRequest();
+ $context = $this->getRequest()->getContext();
+ $user = $request->getUser();
+
+ if ($user->hasRole([Role::ROLE_ID_AUTHOR], $context->getId())) {
+ $userId = $request->getUser()->getId();
+ $collector->assignedTo([$userId]);
+ }
+
+ $submissions = $collector->getMany()->all();
+
+ $submissionIdsFound = array_map(fn (Submission $submission) => $submission->getData('id'), $submissions);
+
+ if (array_diff($submissionIds, $submissionIdsFound)) {
+ return response()->json([
+ 'error' => __('api.404.resourceNotFound')
+ ], Response::HTTP_NOT_FOUND);
+ }
+
+
+ foreach ($submissions as $submission) {
+ if ($context->getId() != $submission->getData('contextId')) {
+ return response()->json([
+ 'error' => __('api.submissions.403.deleteSubmissionOutOfContext'),
+ ], Response::HTTP_FORBIDDEN);
+ }
+
+ if (!Repo::submission()->canCurrentUserDelete($submission)) {
+ return response()->json([
+ 'error' => __('api.submissions.403.unauthorizedDeleteSubmission'),
+ ], Response::HTTP_FORBIDDEN);
+ }
+ }
+
+ foreach ($submissions as $submission) {
+ Repo::submission()->delete($submission);
+ }
+
+ return response()->json([], Response::HTTP_OK);
+ }
+
/**
* Configure a submission Collector based on the query params
*/
diff --git a/classes/submission/Collector.php b/classes/submission/Collector.php
index a6e70fa18c1..6b3e3703cf1 100644
--- a/classes/submission/Collector.php
+++ b/classes/submission/Collector.php
@@ -55,6 +55,7 @@ abstract class Collector implements CollectorInterface, ViewsCount
public DAO $dao;
public ?array $categoryIds = null;
public ?array $contextIds = null;
+ public ?array $submissionIds = null;
public ?int $count = null;
public ?int $daysInactive = null;
public bool $isIncomplete = false;
@@ -252,6 +253,17 @@ public function filterByRevisionsSubmitted(?bool $revisionsSubmitted): AppCollec
return $this;
}
+ /**
+ * Limit results to only submissions with the specified IDs
+ *
+ * @param ?int[] $submissionIds Submission IDs
+ */
+ public function filterBySubmissionIds(?array $submissionIds): static
+ {
+ $this->submissionIds = $submissionIds;
+ return $this;
+ }
+
/**
* Limit results to submissions assigned to these users
*
@@ -372,6 +384,10 @@ public function getQueryBuilder(): Builder
$q->whereIn('s.context_id', $this->contextIds);
}
+ if (isset($this->submissionIds)) {
+ $q->whereIn('s.submission_id', array_map(intval(...), $this->submissionIds));
+ }
+
// Prepare keywords (allows short and numeric words)
$keywords = collect(Application::getSubmissionSearchIndex()->filterKeywords($this->searchPhrase, false, true, true))
->unique()
diff --git a/locale/en/api.po b/locale/en/api.po
index 6a5aa36f309..6cb0eae72f5 100644
--- a/locale/en/api.po
+++ b/locale/en/api.po
@@ -347,5 +347,11 @@ msgstr "Only 'accept' and 'decline' are valid values"
msgid "api.submission.400.sectionDoesNotExist"
msgstr "The provided section does not exist."
+msgid "api.submission.400.missingQueryParam"
+msgstr "The request is missing the required query parameter `ids`. Please provide the `ids` of the submissions you wish to delete."
+
+msgid "api.submission.400.invalidId"
+msgstr "Invalid ID: \"{$id}\" provided."
+
msgid "api.publications.403.noEnabledIdentifiers"
msgstr "Publication identifiers form is unavailable as there are no enabled Identifiers."
diff --git a/locale/en/common.po b/locale/en/common.po
index e2f967f80eb..0cf9741dbfd 100644
--- a/locale/en/common.po
+++ b/locale/en/common.po
@@ -806,7 +806,7 @@ msgid "common.replaceFile"
msgstr "Replace file"
msgid "common.requiredField"
-msgstr "Required fields are marked with an asterisk: *"
+msgstr "Required fields are marked with an asterisk: *"
msgid "common.required"
msgstr "Required"
diff --git a/locale/en/submission.po b/locale/en/submission.po
index 52cfb1960ae..74021324efd 100644
--- a/locale/en/submission.po
+++ b/locale/en/submission.po
@@ -2633,4 +2633,16 @@ msgid "fileManager.productionReadyFilesDescription"
msgstr "These are the files that will be sent for publication"
msgid "reviewerManager.reviewerStatus"
-msgstr "Reviewer status"
\ No newline at end of file
+msgstr "Reviewer status"
+
+msgid "dashboard.submissions.incomplete.bulkDelete.confirm"
+msgstr "Confirm Delete of Incomplete Submissions"
+
+msgid "dashboard.submissions.incomplete.bulkDelete.body"
+msgstr "Are you sure you want to delete the selected items? This action cannot be undone. Please confirm to proceed."
+
+msgid "dashboard.submissions.incomplete.bulkDelete.column.description"
+msgstr "Select incomplete submissions to be deleted."
+
+msgid "dashboard.submissions.incomplete.bulkDelete.button"
+msgstr "Delete Incomplete Submissions"