From 90c567dc7774ae4a641de443c4094139cf46fba6 Mon Sep 17 00:00:00 2001 From: Sven van de Scheur Date: Fri, 20 Dec 2024 13:49:42 +0100 Subject: [PATCH] :sparkles: #554 - feat: add frontend support for filtering destruction lists on name, status, author, reviewer and assignee --- frontend/.storybook/mockData.ts | 18 ++- frontend/src/hooks/index.ts | 2 + frontend/src/hooks/useRecordManagers.ts | 23 +++ frontend/src/hooks/useUsers.ts | 23 +++ frontend/src/lib/api/destructionLists.ts | 11 +- frontend/src/lib/api/recordManagers.ts | 14 ++ frontend/src/lib/api/reviewers.ts | 5 - frontend/src/lib/api/users.ts | 14 ++ frontend/src/pages/landing/Landing.loader.tsx | 14 +- frontend/src/pages/landing/Landing.tsx | 144 +++++++++++++++--- 10 files changed, 229 insertions(+), 39 deletions(-) create mode 100644 frontend/src/hooks/useRecordManagers.ts create mode 100644 frontend/src/hooks/useUsers.ts create mode 100644 frontend/src/lib/api/recordManagers.ts create mode 100644 frontend/src/lib/api/users.ts diff --git a/frontend/.storybook/mockData.ts b/frontend/.storybook/mockData.ts index d4bb0059c..c4a3be08f 100644 --- a/frontend/.storybook/mockData.ts +++ b/frontend/.storybook/mockData.ts @@ -6,7 +6,11 @@ import { destructionListFactory, } from "../src/fixtures/destructionList"; import { FIXTURE_SELECTIELIJSTKLASSE_CHOICES } from "../src/fixtures/selectieLijstKlasseChoices"; -import { userFactory, usersFactory } from "../src/fixtures/user"; +import { + recordManagerFactory, + userFactory, + usersFactory, +} from "../src/fixtures/user"; import { zaaktypeChoicesFactory } from "../src/fixtures/zaaktypeChoices"; export const MOCKS = { @@ -106,6 +110,18 @@ export const MOCKS = { loginUrl: "http://www.example.com", }, }, + USERS: { + url: "http://localhost:8000/api/v1/users/", + method: "GET", + status: 200, + response: usersFactory(), + }, + RECORD_MANAGERS: { + url: "http://localhost:8000/api/v1/record-managers/", + method: "GET", + status: 200, + response: [recordManagerFactory()], + }, REVIEWERS: { url: "http://localhost:8000/api/v1/reviewers/", method: "GET", diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index 74040df96..dabbcceb4 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -8,10 +8,12 @@ export * from "./useFilter"; export * from "./useLatestReviewResponse"; export * from "./usePage"; export * from "./usePoll"; +export * from "./useRecordManagers"; export * from "./useReviewers"; export * from "./useSelectielijstKlasseChoices"; export * from "./useSort"; export * from "./useSubmitAction"; +export * from "./useUsers"; export * from "./useWhoAmI"; export * from "./useZaakReviewStatusBadges"; export * from "./useZaakReviewStatuses"; diff --git a/frontend/src/hooks/useRecordManagers.ts b/frontend/src/hooks/useRecordManagers.ts new file mode 100644 index 000000000..a331fc6e2 --- /dev/null +++ b/frontend/src/hooks/useRecordManagers.ts @@ -0,0 +1,23 @@ +import { useEffect, useState } from "react"; + +import { User } from "../lib/api/auth"; +import { listRecordManagers } from "../lib/api/recordManagers"; +import { useAlertOnError } from "./useAlertOnError"; + +/** + * Hook resolving recordManagers + */ +export function useRecordManagers(): User[] { + const alertOnError = useAlertOnError( + "Er is een fout opgetreden bij het ophalen van record managers!", + ); + + const [recordManagersState, setRecordManagersState] = useState([]); + useEffect(() => { + listRecordManagers() + .then((r) => setRecordManagersState(r)) + .catch(alertOnError); + }, []); + + return recordManagersState; +} diff --git a/frontend/src/hooks/useUsers.ts b/frontend/src/hooks/useUsers.ts new file mode 100644 index 000000000..9a76c4c8a --- /dev/null +++ b/frontend/src/hooks/useUsers.ts @@ -0,0 +1,23 @@ +import { useEffect, useState } from "react"; + +import { User } from "../lib/api/auth"; +import { listUsers } from "../lib/api/users"; +import { useAlertOnError } from "./useAlertOnError"; + +/** + * Hook resolving users + */ +export function useUsers(): User[] { + const alertOnError = useAlertOnError( + "Er is een fout opgetreden bij het ophalen van gebruikers!", + ); + + const [usersState, setUsersState] = useState([]); + useEffect(() => { + listUsers() + .then((r) => setUsersState(r)) + .catch(alertOnError); + }, []); + + return usersState; +} diff --git a/frontend/src/lib/api/destructionLists.ts b/frontend/src/lib/api/destructionLists.ts index 11f921f31..3e2c9e911 100644 --- a/frontend/src/lib/api/destructionLists.ts +++ b/frontend/src/lib/api/destructionLists.ts @@ -114,7 +114,16 @@ export async function getDestructionList(uuid: string) { * List destruction lists. */ export async function listDestructionLists( - params?: URLSearchParams | { ordering?: string }, + params?: + | URLSearchParams + | { + name: string; + status: DestructionListStatus; + author: number; + reviewer: number; + assignee: number; + ordering?: string; + }, ) { const response = await request("GET", "/destruction-lists/", params); const promise: Promise = response.json(); diff --git a/frontend/src/lib/api/recordManagers.ts b/frontend/src/lib/api/recordManagers.ts new file mode 100644 index 000000000..e926380b2 --- /dev/null +++ b/frontend/src/lib/api/recordManagers.ts @@ -0,0 +1,14 @@ +import { cacheMemo } from "../cache/cache"; +import { User } from "./auth"; +import { request } from "./request"; + +/** + * List all the users that have the permission to review destruction lists. + */ +export async function listRecordManagers() { + return cacheMemo("listRecordManagers", async () => { + const response = await request("GET", "/record-managers/"); + const promise: Promise = response.json(); + return promise; + }); +} diff --git a/frontend/src/lib/api/reviewers.ts b/frontend/src/lib/api/reviewers.ts index 36fecd01e..2de8a733c 100644 --- a/frontend/src/lib/api/reviewers.ts +++ b/frontend/src/lib/api/reviewers.ts @@ -2,11 +2,6 @@ import { cacheMemo } from "../cache/cache"; import { User } from "./auth"; import { request } from "./request"; -export type Assignee = { - user: User; - order: number; -}; - /** * List all the users that have the permission to review destruction lists. */ diff --git a/frontend/src/lib/api/users.ts b/frontend/src/lib/api/users.ts new file mode 100644 index 000000000..0e0c8cb65 --- /dev/null +++ b/frontend/src/lib/api/users.ts @@ -0,0 +1,14 @@ +import { cacheMemo } from "../cache/cache"; +import { User } from "./auth"; +import { request } from "./request"; + +/** + * List all the users that have the permission to review destruction lists. + */ +export async function listUsers() { + return cacheMemo("listUsers", async () => { + const response = await request("GET", "/users/"); + const promise: Promise = response.json(); + return promise; + }); +} diff --git a/frontend/src/pages/landing/Landing.loader.tsx b/frontend/src/pages/landing/Landing.loader.tsx index b846aed00..587ec8bcb 100644 --- a/frontend/src/pages/landing/Landing.loader.tsx +++ b/frontend/src/pages/landing/Landing.loader.tsx @@ -16,9 +16,8 @@ export interface LandingContext { export const landingLoader = loginRequired( async ({ request }): Promise => { const url = new URL(request.url); - const queryParams = url.searchParams; - const orderQuery = queryParams.get("ordering"); - const statusMap = await getStatusMap(orderQuery); + const urlSearchParams = url.searchParams; + const statusMap = await getStatusMap(urlSearchParams); const user = await whoAmI(); return { @@ -28,10 +27,11 @@ export const landingLoader = loginRequired( }, ); -export const getStatusMap = async (orderQuery: string | null) => { - const lists = await listDestructionLists({ - ordering: orderQuery ?? "-created", - }); +export const getStatusMap = async (urlSearchParams: URLSearchParams) => { + if (!urlSearchParams.has("ordering")) { + urlSearchParams.set("ordering", "-created"); + } + const lists = await listDestructionLists(urlSearchParams); return STATUSES.reduce((acc, val) => { const status = val[0] || ""; const destructionLists = lists.filter( diff --git a/frontend/src/pages/landing/Landing.tsx b/frontend/src/pages/landing/Landing.tsx index 09bfae253..c9c72e375 100644 --- a/frontend/src/pages/landing/Landing.tsx +++ b/frontend/src/pages/landing/Landing.tsx @@ -6,13 +6,29 @@ import { P, Solid, Tooltip, + string2Title, } from "@maykin-ui/admin-ui"; -import { useLoaderData, useNavigate, useRevalidator } from "react-router-dom"; +import { + useLoaderData, + useNavigate, + useRevalidator, + useSearchParams, +} from "react-router-dom"; import { ProcessingStatusBadge } from "../../components/ProcessingStatusBadge"; +import { + useCoReviewers, + useCombinedSearchParams, + useRecordManagers, + useReviewers, + useUsers, +} from "../../hooks"; import { usePoll } from "../../hooks/usePoll"; import { User } from "../../lib/api/auth"; -import { DestructionList } from "../../lib/api/destructionLists"; +import { + DESTRUCTION_LIST_STATUSES, + DestructionList, +} from "../../lib/api/destructionLists"; import { ProcessingStatus } from "../../lib/api/processingStatus"; import { canCoReviewDestructionList, @@ -90,12 +106,13 @@ export const Landing = () => { const { statusMap, user } = useLoaderData() as LandingContext; const navigate = useNavigate(); const revalidator = useRevalidator(); + const [searchParams, setSearchParams] = useCombinedSearchParams(); + const recordManagers = useRecordManagers(); + const reviewers = useReviewers(); + const users = useUsers(); usePoll(async () => { - const orderQuery = new URLSearchParams(window.location.search).get( - "ordering", - ); - const _statusMap = await getStatusMap(orderQuery); + const _statusMap = await getStatusMap(searchParams); const equal = JSON.stringify(_statusMap) === JSON.stringify(statusMap); if (!equal) { revalidator.revalidate(); @@ -199,22 +216,15 @@ export const Landing = () => { }), ); - const sortOptions = [ - { label: "Nieuwste eerst", value: "-created" }, - { label: "Oudste eerst", value: "created" }, - ]; - - const selectedSort = - new URLSearchParams(window.location.search).get("ordering") || "-created"; - - const sortedOptions = sortOptions.map((option) => ({ - ...option, - selected: option.value === selectedSort, - })); - - const onChangeSort = (event: React.ChangeEvent) => { - // update the query string - navigate(`?ordering=${event.target.value}`); + /** + * Updates the search params when the user changes a filter/order input. + * @param target + */ + const handleFilter = ({ + target, + }: React.ChangeEvent | React.KeyboardEvent) => { + const { name, value } = target as HTMLInputElement; + setSearchParams({ ...searchParams, [name]: value }); }; return ( @@ -226,11 +236,95 @@ export const Landing = () => { toolbarProps: { items: [ { + icon: , + name: "name", + placeholder: "Zoeken op…", + title: "Zoeken", + type: "search", + value: searchParams.get("name") || "", + onBlur: handleFilter, + onKeyUp: (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleFilter(e); + } + }, + }, + { + icon: , + name: "status", + options: DESTRUCTION_LIST_STATUSES.map((status) => ({ + label: string2Title(STATUS_MAPPING[status]), + value: status, + })), + placeholder: "Status…", + required: false, + title: "Status", + value: searchParams.get("status") || "", + onChange: handleFilter, + }, + { + icon: , + name: "author", + options: [ + ...recordManagers.map((rm) => { + return { + label: formatUser(rm, { showUsername: false }), + value: rm.pk, + }; + }), + ], + placeholder: "Auteur…", + required: false, + title: "Auteur", + value: searchParams.get("author") || "", + onChange: handleFilter, + }, + { + icon: , + name: "reviewer", + options: [ + ...reviewers.map((rm) => { + return { + label: formatUser(rm, { showUsername: false }), + value: rm.pk, + }; + }), + ], + placeholder: "Beoordelaar…", + required: false, + title: "Beoordelaar", + value: searchParams.get("reviewer") || "", + onChange: handleFilter, + }, + { + icon: , + name: "assignee", + options: [ + ...users.map((rm) => { + return { + label: formatUser(rm, { showUsername: false }), + value: rm.pk, + }; + }), + ], + placeholder: "Toegewezen aan…", + required: false, + title: "Toegewezen aan", + value: searchParams.get("assignee") || "", + onChange: handleFilter, + }, + { + icon: , direction: "horizontal", - label: "Sorteren", + name: "ordering", + options: [ + { label: "Nieuwste eerst", value: "-created" }, + { label: "Oudste eerst", value: "created" }, + ], required: true, - options: sortedOptions, - onChange: onChangeSort, + title: "Sorteren", + value: searchParams.get("ordering") || "-created", + onChange: handleFilter, }, "spacer", {