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

Feature/destruction list create pagination #22

Merged
merged 3 commits into from
May 17, 2024
Merged
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion frontend/src/lib/api/auth.ts
Original file line number Diff line number Diff line change
@@ -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,
});
19 changes: 12 additions & 7 deletions frontend/src/lib/api/loginRequired.ts
Original file line number Diff line number Diff line change
@@ -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<T>(
fn: (...args: unknown[]) => Promise<T>,
...args: unknown[]
): () => Promise<T | Response> {
return async () => {
export function loginRequired<T, A extends unknown[]>(
fn: (
loaderFunctionArgs: LoaderFunctionArgs,
handlerCtx: unknown,
...args: A
) => Promise<T>,
...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");
5 changes: 4 additions & 1 deletion frontend/src/lib/api/request.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>,
data?: Record<string, unknown>,
headers?: Record<string, string>,
) {
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, {
10 changes: 7 additions & 3 deletions frontend/src/lib/api/zaken.ts
Original file line number Diff line number Diff line change
@@ -3,15 +3,19 @@ import { request } from "./request";

export type PaginatedZaken = {
count: number;
next: string | null;
previous: string | null;
results: Zaak[];
};

/**
* 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<PaginatedZaken[]> = response.json();
export async function listZaken(
params?: URLSearchParams | Record<string, string>,
) {
const response = await request("GET", "/zaken/", params);
const promise: Promise<PaginatedZaken> = response.json();
return promise;
}
184 changes: 138 additions & 46 deletions frontend/src/pages/destructionlist/DestructionListCreate.tsx
Original file line number Diff line number Diff line change
@@ -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<string>) => {
// 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 (
<List
count={count}
fields={fields}
objectList={objectList}
labelSelect={"Selecteer item"} // FIXME: optional
labelSelectAll={"Selecteer alle items"} // FIXME: optional
pageSize={10}
showPaginator={false} // TODO
selectable={false} // TODO
<ListTemplate
dataGridProps={
// FIXME: Required attrs, alias
{
count: count,
fields: fields,
loading: state === "loading",
objectList: objectList,
pageSize: 100,
showPaginator: true,
selectable: false, // TODO
title: "Vernietigingslijst starten",
boolProps: {
explicit: true,
labelFalse: "Nee", // FIXME: optional
},
filterable: true,
page: Number(searchParams.get("page")) || 1,
onFilter: onFilter,
/*
TODO: Multi page selection flow

We should keep track of both selected and unselected zaken across multiple pages using onSelect (second
parameter indicates selection state). We should store every (un)selected item somewhere (sessionStorage?).

When submitting data we consider both the state for selected and unselected zaken as mutations to the
destruction list items. The zaken not in any of the selection should be left untouched (by both the backend
and the frontend). Submitting data only pushes the changes to the backend state.
*/
// selectionActions: [
// {
// children: "Aanmaken",
// onClick: (...args) => 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<AttributeData>((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),
};
});
}
4 changes: 2 additions & 2 deletions frontend/src/pages/login/Login.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Login
<LoginTemplate
slotPrimaryNavigation={<></>} // FIXME: Shoudl be easier to override
formProps={{
nonFieldErrors,
10 changes: 10 additions & 0 deletions frontend/src/types.d.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown | Expand[string]>;
};

/** Serializer the reverse relation between Besluit-Zaak. */
export interface ZaakBesluit {
/** @format uri */