From 9c979bb039db4e22207df929a8be652fe52ff3fc Mon Sep 17 00:00:00 2001 From: Sven van de Scheur Date: Tue, 14 May 2024 15:00:09 +0200 Subject: [PATCH 1/3] :wrench: Update eslint config --- frontend/.eslintrc.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 8729cd4a2..375ac591d 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -28,7 +28,9 @@ "react" ], "rules": { - "react/react-in-jsx-scope": "off" + "react/react-in-jsx-scope": "off", + "@typescript-eslint/ban-ts-comment": 2, + "@typescript-eslint/no-explicit-any": 2 }, "settings": { "react": { From d01cb0e7caeb0a5c5dc6a3e19a511e75f79eb513 Mon Sep 17 00:00:00 2001 From: Sven van de Scheur Date: Wed, 15 May 2024 21:01:23 +0200 Subject: [PATCH 2/3] :see_no_evil: Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8009ca9ce..040c2322d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .vscode/ .sass-cache/ *.db +*.DS_Store *.pyc __pycache__ *~ From 8408b4156a97b46fa7eb543227198ef8a2bf6bc4 Mon Sep 17 00:00:00 2001 From: Sven van de Scheur Date: Thu, 16 May 2024 17:05:16 +0200 Subject: [PATCH 3/3] :sparkles: Add filtering and pagination to destruction list create view --- frontend/src/lib/api/auth.ts | 2 +- frontend/src/lib/api/loginRequired.ts | 19 +- frontend/src/lib/api/request.ts | 5 +- frontend/src/lib/api/zaken.ts | 10 +- .../destructionlist/DestructionListCreate.tsx | 184 +++++++++++++----- frontend/src/pages/login/Login.tsx | 4 +- frontend/src/types.d.ts | 10 + 7 files changed, 174 insertions(+), 60 deletions(-) diff --git a/frontend/src/lib/api/auth.ts b/frontend/src/lib/api/auth.ts index ebfb98752..8dda0ee27 100644 --- a/frontend/src/lib/api/auth.ts +++ b/frontend/src/lib/api/auth.ts @@ -6,7 +6,7 @@ import { request } from "./request"; * @param password */ export async function login(username: string, password: string) { - return request("POST", "/auth/login/", { + return request("POST", "/auth/login/", undefined, { username, password, }); diff --git a/frontend/src/lib/api/loginRequired.ts b/frontend/src/lib/api/loginRequired.ts index 5ee9700ac..38a3fb5c1 100644 --- a/frontend/src/lib/api/loginRequired.ts +++ b/frontend/src/lib/api/loginRequired.ts @@ -1,4 +1,5 @@ -import { redirect } from "react-router-dom"; +import { LoaderFunction } from "@remix-run/router/utils"; +import { LoaderFunctionArgs, redirect } from "react-router-dom"; /** * Wraps an async API function with authentication protection. Redirects to the sign-in page if the request fails with a @@ -7,13 +8,17 @@ import { redirect } from "react-router-dom"; * @param args The arguments to be passed to the async API function. * @returns A function that, when called, executes the wrapped async API function with the provided arguments. */ -export function loginRequired( - fn: (...args: unknown[]) => Promise, - ...args: unknown[] -): () => Promise { - return async () => { +export function loginRequired( + fn: ( + loaderFunctionArgs: LoaderFunctionArgs, + handlerCtx: unknown, + ...args: A + ) => Promise, + ...args: A +): LoaderFunction { + return async (loaderFunctionArgs, handlerCtx) => { try { - return await fn(...args); + return await fn(loaderFunctionArgs, handlerCtx, ...args); } catch (e: unknown) { if ((e as Response)?.status === 403) { return redirect("/login"); diff --git a/frontend/src/lib/api/request.ts b/frontend/src/lib/api/request.ts index 9dcaa1568..361ce2b99 100644 --- a/frontend/src/lib/api/request.ts +++ b/frontend/src/lib/api/request.ts @@ -19,17 +19,20 @@ export const API_BASE_URL = `${API_SCHEME}://${API_HOST}:${API_PORT}${API_PATH}` * Makes an actual fetch request to the API, should be used by all other API implementations. * @param method * @param endpoint + * @param params * @param data * @param headers */ export async function request( method: "GET" | "POST", endpoint: string, + params?: URLSearchParams | Record, data?: Record, headers?: Record, ) { + const queryString = params ? new URLSearchParams(params).toString() : ""; + const url = `${API_BASE_URL + endpoint}?${queryString}`; const csrfToken = getCookie("csrftoken"); - const url = API_BASE_URL + endpoint; const abortController = new AbortController(); const response = await fetch(url, { diff --git a/frontend/src/lib/api/zaken.ts b/frontend/src/lib/api/zaken.ts index c849ab320..be0588509 100644 --- a/frontend/src/lib/api/zaken.ts +++ b/frontend/src/lib/api/zaken.ts @@ -3,6 +3,8 @@ import { request } from "./request"; export type PaginatedZaken = { count: number; + next: string | null; + previous: string | null; results: Zaak[]; }; @@ -10,8 +12,10 @@ export type PaginatedZaken = { * Retrieve zaken using the configured ZRC service. For information over the query parameters accepted and the schema of * the response, look at the '/zaken/api/v1/zaken' list endpoint of Open Zaak. */ -export async function listZaken() { - const response = await request("GET", "/zaken/"); - const promise: Promise = response.json(); +export async function listZaken( + params?: URLSearchParams | Record, +) { + const response = await request("GET", "/zaken/", params); + const promise: Promise = response.json(); return promise; } diff --git a/frontend/src/pages/destructionlist/DestructionListCreate.tsx b/frontend/src/pages/destructionlist/DestructionListCreate.tsx index 1e8b83e4a..14276e83a 100644 --- a/frontend/src/pages/destructionlist/DestructionListCreate.tsx +++ b/frontend/src/pages/destructionlist/DestructionListCreate.tsx @@ -1,11 +1,15 @@ import { AttributeData, DataGridProps, - List, + ListTemplate, TypedField, } from "@maykin-ui/admin-ui"; import React from "react"; -import { useLoaderData } from "react-router-dom"; +import { + useLoaderData, + useNavigation, + useSearchParams, +} from "react-router-dom"; import { loginRequired } from "../../lib/api/loginRequired"; import { PaginatedZaken, listZaken } from "../../lib/api/zaken"; @@ -17,7 +21,10 @@ import "./DestructionListCreate.css"; * @param request * TOOD: Requires destruction list lists endpoint. */ -export const destructionListCreateLoader = loginRequired(listZaken); +export const destructionListCreateLoader = loginRequired(({ request }) => { + const searchParams = new URL(request.url).searchParams; + return listZaken(searchParams); +}); export type DestructionListCreateProps = Omit< React.ComponentProps<"main">, @@ -31,63 +38,148 @@ export function DestructionListCreatePage({ ...props }: DestructionListCreateProps) { const { count, results } = useLoaderData() as PaginatedZaken; - const objectList = transformZakenForPresentation(results); - - console.log(results, objectList); + const [searchParams, setSearchParams] = useSearchParams(); + const objectList = results as unknown as AttributeData[]; + const { state } = useNavigation(); const fields: TypedField[] = [ - { name: "identificatie", type: "string" }, - { name: "zaaktype", type: "string" }, - { name: "omschrijving", type: "string" }, - { name: "looptijd", type: "string" }, - { name: "resultaattype", type: "string" }, - { name: "bewaartermijn", type: "string" }, - { name: "vcs", type: "string" }, - { name: "relaties", type: "boolean" }, + { + name: "identificatie", + filterLookup: "identificatie__icontains", + filterValue: searchParams.get("identificatie__icontains") || "", + type: "string", + }, + { + name: "zaaktype", + filterLookup: "zaaktype__omschrijving__icontains", + filterValue: searchParams.get("zaaktype__omschrijving__icontains") || "", + valueLookup: "_expand.zaaktype.omschrijving", + type: "string", + }, + { + name: "omschrijving", + filterLookup: "omschrijving__icontains", + filterValue: searchParams.get("omschrijving__icontains") || "", + type: "string", + }, + { + name: "looptijd", + filterable: false, + valueTransform: (rowData) => { + const zaak = rowData as unknown as Zaak; + const startDate = new Date(zaak.startdatum); + const endDate = zaak.einddatum ? new Date(zaak.einddatum) : new Date(); + return ( + Math.ceil( + (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24), + ) + " dagen" + ); + }, + type: "string", + }, + { + name: "resultaattype", + filterLookup: "resultaat__resultaattype__omschrijving__icontains", + filterValue: + searchParams.get("resultaat__resultaattype__omschrijving__icontains") || + "", + valueLookup: "_expand.resultaat._expand.resultaattype.omschrijving", + type: "string", + }, + { + name: "bewaartermijn", + filterLookup: "resultaat__resultaattype__archiefactietermijn__icontains", + filterValue: + searchParams.get( + "resultaat__resultaattype__archiefactietermijn__icontains", + ) || "", + valueLookup: + "_expand.resultaat._expand.resultaattype.archiefactietermijn", + type: "string", + }, + { + name: "vcs", + filterLookup: "zaaktype__selectielijstprocestype__naam__icontains", + filterValue: + searchParams.get( + "zaaktype__selectielijstprocestype__naam__icontains", + ) || "", + valueLookup: "_expand.zaaktype.selectielijstProcestype.naam", + type: "string", + }, + { + name: "relaties", + filterLookup: "heeft_relaties", + valueTransform: (rowData) => + Boolean((rowData as unknown as Zaak)?.relevanteAndereZaken?.length), + filterValue: searchParams.get("heeft_relaties") || "", + type: "boolean", + options: [ + { value: "true", label: "Ja" }, + { value: "false", label: "Nee" }, + ], + }, ]; + const onFilter = (filterData: AttributeData) => { + // TODO: Fill filter fields with current value + const combinedParams = { + ...Object.fromEntries(searchParams), + ...filterData, + }; + + const activeParams = Object.fromEntries( + Object.entries(combinedParams).filter(([k, v]) => v), + ); + + setSearchParams(activeParams); + }; + return ( - console.log(...args), + // }, + // ], + // TODO: Keep track of selected/unselected state. + onSelectionChange: (...args) => + console.log("onSelectionChange", args), + onPageChange: (page) => + setSearchParams({ + ...Object.fromEntries(searchParams), + page: String(page), + }), } as DataGridProps } - title="Vernietigingslijst starten" {...props} /> ); } - -export function transformZakenForPresentation(zaken: Zaak[]) { - return zaken.map((zaak) => { - const startDate = new Date(zaak.startdatum); - const endDate = zaak.einddatum ? new Date(zaak.einddatum) : new Date(); - const dayDelta = - (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24); - - return { - identificatie: zaak.identificatie || "", - zaaktype: zaak.zaaktype, - omschrijving: zaak.omschrijving || "", - looptijd: String(dayDelta), - resultaattype: "TODO", - bewaartermijn: "TODO", - vcs: "TODO", - relaties: Boolean(zaak?.relevanteAndereZaken?.length || 0), - }; - }); -} diff --git a/frontend/src/pages/login/Login.tsx b/frontend/src/pages/login/Login.tsx index a225deac7..d83cdc8a8 100644 --- a/frontend/src/pages/login/Login.tsx +++ b/frontend/src/pages/login/Login.tsx @@ -1,4 +1,4 @@ -import { Login } from "@maykin-ui/admin-ui"; +import { LoginTemplate } from "@maykin-ui/admin-ui"; import { ActionFunctionArgs } from "@remix-run/router/utils"; import { redirect, useActionData, useSubmit } from "react-router-dom"; @@ -55,7 +55,7 @@ export function LoginPage({ ...props }: LoginProps) { const { nonFieldErrors, ...errors } = formErrors; return ( - } // FIXME: Shoudl be easier to override formProps={{ nonFieldErrors, diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index f65d9ae0c..308a4067c 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -1,5 +1,6 @@ /* eslint-disable */ /* tslint:disable */ + /* * --------------------------------------------------------------- * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## @@ -3062,8 +3063,17 @@ export interface Zaak { startdatumBewaartermijn?: string | null; /** Specificatie van de attribuutsoort van het object, subject of gebeurtenis waarop, vanuit archiveringsoptiek, de zaak betrekking heeft en dat bepalend is voor de start van de archiefactietermijn. */ processobject?: Processobject | null; + + _expand: { + zaaktype: Expand; + resultaat: Expand; + }; } +export type Expand = { + [index: string]: Record; +}; + /** Serializer the reverse relation between Besluit-Zaak. */ export interface ZaakBesluit { /** @format uri */