diff --git a/backend/src/openarchiefbeheer/destruction/tests/e2e/features/test_feature_list_delete.py b/backend/src/openarchiefbeheer/destruction/tests/e2e/features/test_feature_list_delete.py new file mode 100644 index 00000000..98a87ec0 --- /dev/null +++ b/backend/src/openarchiefbeheer/destruction/tests/e2e/features/test_feature_list_delete.py @@ -0,0 +1,42 @@ +# fmt: off + +from django.test import tag + +from openarchiefbeheer.utils.tests.e2e import browser_page +from openarchiefbeheer.utils.tests.gherkin import GherkinLikeTestCase + +from ....constants import ListStatus + + +@tag("e2e") +@tag("gh-558") +class FeatureListDeleteTests(GherkinLikeTestCase): + async def test_scenario_delete_list(self): + async with browser_page() as page: + record_manger = await self.given.record_manager_exists() + await self.given.assignee_exists(user=record_manger) + await self.given.list_exists(name="Destruction list to delete", status=ListStatus.new) + + await self.when.record_manager_logs_in(page) + await self.then.path_should_be(page, "/destruction-lists") + + await self.when.user_clicks_button(page, "Destruction list to delete") + await self.when.user_clicks_button(page, "Lijst verwijderen") + await self.when.user_fills_form_field(page, "Type naam van de lijst ter bevestiging", "Destruction list to delete") + await self.when.user_clicks_button(page, "Lijst verwijderen", 1) + + await self.then.path_should_be(page, "/destruction-lists") + await self.then.not_.list_should_exist(page, name="Destruction list to delete") + await self.then.not_.page_should_contain_text(page, "Destruction list to delete") + + async def test_scenario_cannot_delete_if_status_not_new(self): + async with browser_page() as page: + record_manger = await self.given.record_manager_exists() + await self.given.assignee_exists(user=record_manger) + await self.given.list_exists(name="Destruction list to delete", status=ListStatus.ready_to_delete) + + await self.when.record_manager_logs_in(page) + await self.then.path_should_be(page, "/destruction-lists") + + await self.when.user_clicks_button(page, "Destruction list to delete") + await self.then.not_.page_should_contain_text(page, "Lijst verwijderen") diff --git a/backend/src/openarchiefbeheer/utils/tests/gherkin.py b/backend/src/openarchiefbeheer/utils/tests/gherkin.py index da80a88a..bcc8c893 100644 --- a/backend/src/openarchiefbeheer/utils/tests/gherkin.py +++ b/backend/src/openarchiefbeheer/utils/tests/gherkin.py @@ -471,7 +471,12 @@ async def inverted_method(*args, **kwargs): return InvertedThen(self) async def list_should_exist(self, page, name): - return await DestructionList.objects.aget(name=name) + try: + return await DestructionList.objects.aget(name=name) + except DestructionList.DoesNotExist: + raise AssertionError( + f"Destruction list with name '{name}' does not exist." + ) async def list_should_have_assignee(self, page, destruction_list, assignee): @sync_to_async() diff --git a/frontend/src/lib/api/destructionLists.ts b/frontend/src/lib/api/destructionLists.ts index 0d1c0351..23007543 100644 --- a/frontend/src/lib/api/destructionLists.ts +++ b/frontend/src/lib/api/destructionLists.ts @@ -130,6 +130,20 @@ export async function listDestructionLists( return promise; } +/** + * Delete a destruction list. + * @param uuid + * @returns + */ +export async function deleteDestructionList(uuid: string) { + const response = await request("DELETE", `/destruction-lists/${uuid}/`, {}); + if (response.status === 204) { + return null; + } + const promise: Promise = response.json(); + return promise; +} + /** * Update destruction list. * @param uuid @@ -150,7 +164,7 @@ export async function updateDestructionList( } /** - * Mark destruction list as ready to reveiw. + * Mark destruction list as ready to review. * @param uuid */ export async function markDestructionListAsReadyToReview(uuid: string) { @@ -191,7 +205,7 @@ export async function markDestructionListAsFinal( } /** - * Destroy destruction list + * Queue a background process that will delete the cases in the list from the case system. * @param uuid * @returns */ diff --git a/frontend/src/lib/auth/permissions.ts b/frontend/src/lib/auth/permissions.ts index 8b772f71..e0988617 100644 --- a/frontend/src/lib/auth/permissions.ts +++ b/frontend/src/lib/auth/permissions.ts @@ -24,6 +24,21 @@ export const canChangeSettings: PermissionCheck = (user) => { export const canStartDestructionList: PermissionCheck = (user) => user.role.canStartDestruction; +/** + * Returns whether `user` is allowed to delete `destructionList`. + * @param user + * @param destructionList + */ +export const canDeleteDestructionList: DestructionListPermissionCheck = ( + user, + destructionList, +) => { + if (!user.role.canStartDestruction) { + return false; + } + return destructionList.status === "new"; +}; + /** * Returns whether `user` is allowed to mark `destructionList` as ready to review. * @param user diff --git a/frontend/src/pages/destructionlist/detail/DestructionListDetail.action.ts b/frontend/src/pages/destructionlist/detail/DestructionListDetail.action.ts index bc830152..c4126bd6 100644 --- a/frontend/src/pages/destructionlist/detail/DestructionListDetail.action.ts +++ b/frontend/src/pages/destructionlist/detail/DestructionListDetail.action.ts @@ -4,6 +4,7 @@ import { redirect } from "react-router-dom"; import { JsonValue, TypedAction } from "../../../hooks"; import { abort, + deleteDestructionList, destructionListQueueDestruction, markDestructionListAsFinal, markDestructionListAsReadyToReview, @@ -16,6 +17,7 @@ import { import { clearZaakSelection } from "../../../lib/zaakSelection/zaakSelection"; export type UpdateDestructionListAction

= TypedAction< + | "DELETE_LIST" | "QUEUE_DESTRUCTION" | "CANCEL_DESTROY" | "MAKE_FINAL" @@ -36,6 +38,8 @@ export async function destructionListUpdateAction({ const action = data as UpdateDestructionListAction; switch (action.type) { + case "DELETE_LIST": + return await destructionListDeleteAction({ request, params }); case "QUEUE_DESTRUCTION": return await destructionListQueueDestructionAction({ request, params }); case "MAKE_FINAL": @@ -56,6 +60,24 @@ export async function destructionListUpdateAction({ } } +/** + * React Router action (user intents to DELETE THE DESTRUCTION LIST!). + */ +export async function destructionListDeleteAction({ + request, +}: ActionFunctionArgs) { + const { payload } = await request.json(); + try { + await deleteDestructionList(payload.uuid); + } catch (e: unknown) { + if (e instanceof Response) { + return await (e as Response).json(); + } + throw e; + } + return redirect("/"); +} + /** * React Router action (user intents to mark the destruction list as final (assign to archivist)). */ diff --git a/frontend/src/pages/destructionlist/detail/hooks/useSecondaryNavigation.tsx b/frontend/src/pages/destructionlist/detail/hooks/useSecondaryNavigation.tsx index 43abf13e..3aeb48e4 100644 --- a/frontend/src/pages/destructionlist/detail/hooks/useSecondaryNavigation.tsx +++ b/frontend/src/pages/destructionlist/detail/hooks/useSecondaryNavigation.tsx @@ -17,6 +17,7 @@ import { useSubmitAction } from "../../../../hooks"; import { ReviewItemResponse } from "../../../../lib/api/reviewResponse"; import { DestructionListPermissionCheck, + canDeleteDestructionList, canMarkAsReadyToReview, canMarkListAsFinal, canTriggerDestruction, @@ -42,7 +43,7 @@ type MakeFinalFormType = { comment: string; }; -type DestroyFormType = { +type DestructionListNameFormType = { name: string; }; @@ -86,6 +87,55 @@ export function useSecondaryNavigation(): ToolbarItem[] { return toolbarItem; }; + const BUTTON_DELETE_LIST: ToolbarItem = { + children: ( + <> + + Lijst verwijderen + + ), + pad: "h", + variant: "danger", + onClick: () => + formDialog( + "Lijst verwijderen", + `Dit verwijdert de lijst maar niet de zaken die erop staan. Weet u zeker dat u de lijst "${destructionList.name}" wilt verwijderen?`, + [ + { + label: "Type naam van de lijst ter bevestiging", + name: "name", + placeholder: "Naam van de vernietigingslijst", + required: true, + }, + ], + `Lijst verwijderen`, + "Annuleren", + handleDeleteList, + undefined, + undefined, + { + buttonProps: { + variant: "danger", + }, + validate: validateName, + validateOnChange: true, + role: "form", + }, + ), + }; + + /** + * Dispatches action to DELETE THE DESTRUCTION LIST! + */ + const handleDeleteList = async () => { + submitAction({ + type: "DELETE_LIST", + payload: { + uuid: destructionList.uuid, + }, + }); + }; + const BUTTON_READY_TO_REVIEW: ToolbarItem = { children: ( <> @@ -94,6 +144,7 @@ export function useSecondaryNavigation(): ToolbarItem[] { ), pad: "h", + variant: "primary", onClick: () => confirm( "Ter beoordeling indienen", @@ -282,7 +333,7 @@ export function useSecondaryNavigation(): ToolbarItem[] { variant: "danger", pad: "h", onClick: () => - formDialog( + formDialog( "Zaken definitief vernietigen", `U staat op het punt om ${destructionListItems.count} zaken definitief te vernietigen`, [ @@ -295,21 +346,21 @@ export function useSecondaryNavigation(): ToolbarItem[] { ], `${destructionListItems.count} zaken vernietigen`, "Annuleren", - handleDestroy, + handleQueueDestruction, undefined, undefined, { buttonProps: { variant: "danger", }, - validate: validateDestroy, + validate: validateName, validateOnChange: true, role: "form", }, ), }; - const validateDestroy = ({ name }: DestroyFormType) => { + const validateName = ({ name }: DestructionListNameFormType) => { // Name can be undefined at a certain point and will crash the entire page if ( (name as string | undefined)?.toLowerCase() === @@ -325,7 +376,7 @@ export function useSecondaryNavigation(): ToolbarItem[] { /** * Dispatches action to DESTROY ALL ZAKEN ON THE DESTRUCTION LIST! */ - const handleDestroy = async () => { + const handleQueueDestruction = async () => { submitAction({ type: "QUEUE_DESTRUCTION", payload: { @@ -390,7 +441,7 @@ export function useSecondaryNavigation(): ToolbarItem[] { destructionList={destructionList} />, - // Status: "ready_to_delete", badge and spacer + // Status: "ready_to_delete": badge and spacer getPermittedToolbarItem( @@ -420,7 +472,7 @@ export function useSecondaryNavigation(): ToolbarItem[] { (user, destructionList) => destructionList.status !== "new", ), - // Status: "changes_requested", "Opnieuw indienen" + // Status: "changes_requested": "Opnieuw indienen" getPermittedToolbarItem( BUTTON_PROCESS_REVIEW, (user, destructionList) => @@ -428,10 +480,10 @@ export function useSecondaryNavigation(): ToolbarItem[] { destructionList.status === "changes_requested", ), - // Status: "internally_reviewed", "Markeren als definitief" + // Status: "internally_reviewed": "Markeren als definitief" getPermittedToolbarItem(BUTTON_MAKE_FINAL, canMarkListAsFinal), - // Status: "ready_to_delete" "Vernietigen starten"/"Vernietigen herstarten" + // Status: "ready_to_delete": "Vernietigen starten"/"Vernietigen herstarten" getPermittedToolbarItem( BUTTON_DESTROY, (user, destructionList) =>