Skip to content

Commit

Permalink
Merge pull request #39 from maykinmedia/feature/multi-page-selection
Browse files Browse the repository at this point in the history
✨ Add multi page selection to destruction list create page
  • Loading branch information
svenvandescheur authored May 17, 2024
2 parents 596187c + b669703 commit 4593d23
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 23 deletions.
135 changes: 135 additions & 0 deletions frontend/src/lib/zaakSelection/zaakSelection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { isPrimitive } from "@maykin-ui/admin-ui";

import { Zaak } from "../../types";

export type ZaakSelection = {
/**
* A `Zaak.url` mapped to a `boolean`.
* - `true`: The zaak is added to the selection.
* - `false`: The zaak is removed from the selection.
*/
[index: string]: boolean;
};

/**
* Adds `zaken` to zaak selection identified by key.
* Note: only the `url` of selected `zaken` are stored.
* Note: This function is async to accommodate possible future refactors.
* @param key A key identifying the selection
* @param zaken An array containing either `Zaak.url` or `Zaak` objects
*/
export async function addToZaakSelection(
key: string,
zaken: string[] | Zaak[],
) {
await _mutateZaakSelection(key, zaken, true);
}

/**
* Removes `zaken` from zaak selection identified by key.
* Note: only the `url` of selected `zaken` are stored.
* Note: This function is async to accommodate possible future refactors.
* @param key A key identifying the selection
* @param zaken An array containing either `Zaak.url` or `Zaak` objects
*/
export async function removeFromZaakSelection(
key: string,
zaken: string[] | Zaak[],
) {
await _mutateZaakSelection(key, zaken, false);
}

/**
* Gets the zaak selection.
* Note: only the `url` of selected `zaken` are stored.
* Note: This function is async to accommodate possible future refactors.
* @param key A key identifying the selection
*/
export async function getZaakSelection(key: string) {
const computedKey = _getComputedKey(key);
const json = sessionStorage.getItem(computedKey) || "{}";
return JSON.parse(json) as ZaakSelection;
}

/**
* Sets zaak selection cache.
* Note: only the `url` of selected `zaken` are stored.
* Note: This function is async to accommodate possible future refactors.
* @param key A key identifying the selection
* @param zaakSelection
*/
export async function setZaakSelection(
key: string,
zaakSelection: ZaakSelection,
) {
const computedKey = _getComputedKey(key);
const json = JSON.stringify(zaakSelection);
sessionStorage.setItem(computedKey, json);
}

/**
* Returns whether zaak is selected.
* @param key A key identifying the selection
* @param zaak Either a `Zaak.url` or `Zaak` object.
*/
export async function isZaakSelected(key: string, zaak: string | Zaak) {
const zaakSelection = await getZaakSelection(key);
const url = _getZaakUrl(zaak);
return zaakSelection[url];
}

/**
* Mutates the zaak selection
* Note: only the `url` of selected `zaken` are stored.
* Note: This function is async to accommodate possible future refactors.
* @param key A key identifying the selection
* @param zaken An array containing either `Zaak.url` or `Zaak` objects
* @param selected Indicating whether the selection should be added (`true) or removed (`false).
*/
export async function _mutateZaakSelection(
key: string,
zaken: string[] | Zaak[],
selected: boolean,
) {
const currentZaakSelection = await getZaakSelection(key);
const urls = _getZaakUrls(zaken);

const zaakSelectionOverrides = urls.reduce<ZaakSelection>(
(partialZaakSelection, url) => ({
...partialZaakSelection,
[url]: selected,
}),
{},
);

const combinedZaakSelection = {
...currentZaakSelection,
...zaakSelectionOverrides,
};

await setZaakSelection(key, combinedZaakSelection);
}

/**
* Computes the prefixed cache key.
* @param key A key identifying the selection
*/
function _getComputedKey(key: string): string {
return `oab.lib.zaakSelection.${key}`;
}

/**
* Returns the urls based on an `Array` of `string`s or `Zaak` objects.
* @param zaken An array containing either `Zaak.url` or `Zaak` objects
*/
function _getZaakUrls(zaken: Array<string | Zaak>) {
return zaken.map(_getZaakUrl);
}

/**
* Returns the url based on a `string` or `Zaak` object.
* @param zaak Either a `Zaak.url` or `Zaak` object.
*/
function _getZaakUrl(zaak: string | Zaak) {
return isPrimitive(zaak) ? zaak : (zaak.url as string);
}
78 changes: 55 additions & 23 deletions frontend/src/pages/destructionlist/DestructionListCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,20 @@ import {
import { loginRequired } from "../../lib/api/loginRequired";
import { ZaaktypeChoice, listZaaktypeChoices } from "../../lib/api/private";
import { PaginatedZaken, listZaken } from "../../lib/api/zaken";
import {
addToZaakSelection,
isZaakSelected,
removeFromZaakSelection,
} from "../../lib/zaakSelection/zaakSelection";
import { Zaak } from "../../types";
import "./DestructionListCreate.css";

/** We need a key to store the zaak selection to, however we don't have a destruction list name yet. */
const DESTRUCTION_LIST_CREATE_KEY = "tempDestructionList";

export type DestructionListCreateContext = {
zaken: PaginatedZaken;
selectedZaken: Zaak[];
zaaktypeChoices: ZaaktypeChoice[];
};

Expand All @@ -30,9 +39,21 @@ export const destructionListCreateLoader = loginRequired(
async ({ request }) => {
const searchParams = new URL(request.url).searchParams;
searchParams.set("not_in_destruction_list", "true");

// Get zaken and zaaktypen.
const zaken = await listZaken(searchParams);
const zaaktypeChoices = await listZaaktypeChoices();
return { zaken, zaaktypeChoices };

// Get zaak selection.
const isZaakSelectedPromises = zaken.results.map((zaak) =>
isZaakSelected(DESTRUCTION_LIST_CREATE_KEY, zaak),
);
const isZaakSelectedResults = await Promise.all(isZaakSelectedPromises);
const selectedZaken = zaken.results.filter(
(_, index) => isZaakSelectedResults[index],
);

return { zaken, selectedZaken, zaaktypeChoices };
},
);

Expand All @@ -47,7 +68,7 @@ export type DestructionListCreateProps = Omit<
export function DestructionListCreatePage({
...props
}: DestructionListCreateProps) {
const { zaken, zaaktypeChoices } =
const { zaken, selectedZaken, zaaktypeChoices } =
useLoaderData() as DestructionListCreateContext;
const [searchParams, setSearchParams] = useSearchParams();
const objectList = zaken.results as unknown as AttributeData[];
Expand Down Expand Up @@ -133,8 +154,11 @@ export function DestructionListCreatePage({
},
];

/**
* Gets called when a filter value is change.
* @param filterData
*/
const onFilter = (filterData: AttributeData<string>) => {
// TODO: Fill filter fields with current value
const combinedParams = {
...Object.fromEntries(searchParams),
...filterData,
Expand All @@ -147,6 +171,28 @@ export function DestructionListCreatePage({
setSearchParams(activeParams);
};

/**
* Gets called when the selection is changed.
* @param attributeData
* @param selected
*/
const onSelect = async (
attributeData: AttributeData[],
selected: boolean,
) => {
selected
? await addToZaakSelection(
DESTRUCTION_LIST_CREATE_KEY,
attributeData as unknown as Zaak[],
)
: await removeFromZaakSelection(
DESTRUCTION_LIST_CREATE_KEY,
attributeData.length
? (attributeData as unknown as Zaak[])
: zaken.results,
);
};

return (
<ListTemplate
dataGridProps={
Expand All @@ -157,33 +203,19 @@ export function DestructionListCreatePage({
objectList: objectList,
pageSize: 100,
showPaginator: true,
selectable: false, // TODO
selectable: true,
selected: selectedZaken as unknown as AttributeData[],
labelSelect: `Zaak {identificatie} toevoegen aan selectie`,
labelSelectAll:
"Alle {count} zaken op deze pagina toevoegen aan selectie",
title: "Vernietigingslijst starten",
boolProps: {
explicit: true,
},
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),
onSelect: onSelect,
onPageChange: (page) =>
setSearchParams({
...Object.fromEntries(searchParams),
Expand Down

0 comments on commit 4593d23

Please sign in to comment.