Skip to content

Commit ac04a96

Browse files
🐛 #701 - fix: fix a bug that prevented users from adding additional zaaktypen to a destruction list
1 parent 81b0261 commit ac04a96

File tree

12 files changed

+225
-68
lines changed

12 files changed

+225
-68
lines changed

backend/src/openarchiefbeheer/destruction/tests/e2e/features/test_feature_list_create.py

+2
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ def create_data():
109109
}
110110
},
111111
)
112+
113+
# This zaaktype should not be shown as its only zaak gets assigned to a destruction list.
112114
zaak = ZaakFactory.create(
113115
identificatie="ZAAK-111-2",
114116
post___expand={

frontend/src/hooks/useFields.tsx

+50-10
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,20 @@ type FilterTransformReturnType<T> = Record<
3838

3939
/**
4040
* Hook resolving the base fields for lists.
41+
* s
42+
* @param [destructionList] - The destruction list to return fields for.
43+
* @param [review] - The destruction list to return fields for.
44+
* @param [extraFields] - Additional fields to add.
45+
* @param [restrictFilterChoices="list"] - Allows restricting choices in filters
46+
* to either:
47+
* - The destruction list or review (`list`).
48+
* - To zaken not already assigned to a destruction list (`unassigned`).
4149
*/
4250
export function useFields<T extends Zaak = Zaak>(
4351
destructionList?: DestructionList,
4452
review?: Review,
4553
extraFields?: TypedField<T>[],
54+
restrictFilterChoices: "list" | "unassigned" | false = "list",
4655
): [
4756
TypedField<T>[],
4857
(fields: TypedField<T>[]) => void,
@@ -61,11 +70,27 @@ export function useFields<T extends Zaak = Zaak>(
6170
}, []);
6271
const [searchParams, setSearchParams] = useSearchParams();
6372
const selectielijstKlasseChoices = useSelectielijstKlasseChoices();
64-
const zaaktypeChoices = useZaaktypeChoices(
65-
destructionList,
66-
review,
67-
searchParams,
68-
);
73+
74+
// Fetch the zaaktype choices, if `restrictFilterChoices==="list"`: only zaaktype
75+
// choices of zaken belonging to either the destruction list or review are loaded.
76+
// If `restrictFilterChoices==="unassigned"`: only zaaktype choices belonging to
77+
// zaken not assigned to any destruction list are loaded. If `restrictFilterChoices==="false"`:
78+
// all zaaktype choices are loaded.
79+
const zaaktypeParams = new URLSearchParams(searchParams);
80+
zaaktypeParams.delete("zaaktype");
81+
82+
if (restrictFilterChoices === "list") {
83+
if (destructionList) {
84+
zaaktypeParams.set("inDestructionList", destructionList.uuid);
85+
}
86+
if (review?.pk) {
87+
zaaktypeParams.set("inReview", review.pk.toString());
88+
}
89+
}
90+
if (restrictFilterChoices === "unassigned") {
91+
zaaktypeParams.set("not_in_destruction_list", "true");
92+
}
93+
const zaaktypeChoices = useZaaktypeChoices(zaaktypeParams, false, null);
6994

7095
// The raw, unfiltered configuration of the available base fields.
7196
// Both filterLookup AND filterLookups will be used for clearing filters.
@@ -82,11 +107,26 @@ export function useFields<T extends Zaak = Zaak>(
82107
name: "zaaktype",
83108
filterLookup: "zaaktype",
84109
filterValue: searchParams.get("zaaktype") || "",
85-
valueTransform: (v: ExpandZaak) =>
86-
zaaktypeChoices.find(
87-
(c) => c.value === v._expand?.zaaktype?.identificatie,
88-
)?.label || <Placeholder />,
89-
options: zaaktypeChoices,
110+
valueTransform: (zaak) => {
111+
const expandZaak = zaak as T & {
112+
_expand: { zaaktype: { identificatie: string } };
113+
};
114+
const zaaktype = expandZaak._expand.zaaktype.identificatie;
115+
116+
// Zaaktype choices not yet loaded.
117+
if (zaaktypeChoices === null) {
118+
return <Placeholder />;
119+
}
120+
121+
// Find label by zaaktype choice.
122+
const zaaktypeChoice = zaaktypeChoices.find(
123+
({ value }) => value === zaaktype,
124+
);
125+
126+
// Return label, or null.
127+
return zaaktypeChoice?.label || zaaktype;
128+
},
129+
options: zaaktypeChoices || [],
90130
type: "string",
91131
width: "150px",
92132
},
+19-20
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,40 @@
11
import { Option } from "@maykin-ui/admin-ui";
22
import { useEffect, useState } from "react";
33

4-
import { DestructionList } from "../lib/api/destructionLists";
54
import { listZaaktypeChoices } from "../lib/api/private";
6-
import { Review } from "../lib/api/review";
5+
import { params2CacheKey } from "../lib/format/params";
76
import { useAlertOnError } from "./useAlertOnError";
87

98
/**
109
* Hook resolving zaaktype choices
11-
* @param destructionList
12-
* @param review
13-
* @param searchParams
10+
* @param params
1411
* @param external
12+
* @param initalData
1513
*/
16-
export function useZaaktypeChoices(
17-
destructionList?: DestructionList,
18-
review?: Review,
19-
searchParams?: URLSearchParams,
14+
export function useZaaktypeChoices<T = Option[]>(
15+
params?: Parameters<typeof listZaaktypeChoices>[0],
2016
external = false,
21-
): Option[] {
17+
initalData?: T,
18+
): T | Option[] {
19+
const initial = typeof initalData === "undefined" ? [] : initalData;
2220
const alertOnError = useAlertOnError(
2321
"Er is een fout opgetreden bij het ophalen van zaaktypen!",
2422
);
2523

26-
const [zaaktypeChoicesState, setZaaktypeChoicesState] = useState<Option[]>(
27-
[],
28-
);
24+
const [zaaktypeChoicesState, setZaaktypeChoicesState] = useState<
25+
T | Option[]
26+
>(initial);
27+
2928
useEffect(() => {
30-
listZaaktypeChoices(
31-
destructionList?.uuid,
32-
review?.pk,
33-
searchParams,
34-
external,
35-
)
29+
setZaaktypeChoicesState(initial);
30+
const abortController = new AbortController();
31+
32+
listZaaktypeChoices(params, external, abortController.signal)
3633
.then((z) => setZaaktypeChoicesState(z))
3734
.catch(alertOnError);
38-
}, [destructionList?.uuid, review?.pk, searchParams?.toString()]);
35+
36+
return () => abortController.abort();
37+
}, [params2CacheKey(params || {}), external]);
3938

4039
return zaaktypeChoicesState;
4140
}

frontend/src/lib/api/private.ts

+31-28
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ import { Option } from "@maykin-ui/admin-ui";
22

33
import { Zaak } from "../../types";
44
import { cacheMemo } from "../cache/cache";
5-
import { DestructionList } from "./destructionLists";
5+
import { params2CacheKey, params2Object } from "../format/params";
66
import { request } from "./request";
7-
import { Review } from "./review";
87

98
/**
109
* Retrieve informatieobjecttypen from Open Zaak and return a value and a label per informatieobjecttype. The label is
@@ -98,52 +97,56 @@ export async function listSelectielijstKlasseChoices(
9897
}
9998

10099
/**
101-
* Retrieve zaaktypen from Open Zaak and return a value and a label per zaaktype. The label is the 'omschrijving' field
102-
* an the value is the URL. The response is cached for 15 minutes.
100+
* Retrieve zaaktypen from Open Zaak and return a value and a label per zaaktype.
101+
* The label is the 'omschrijving' field, and the value is the URL. The response is cached for 15 minutes.
102+
* @param [params] - Additional search parameters for filtering (this keeps filters in sync with objects on page).
103+
* @param [external=false] - Fetch zaaktypen from ZRC Service (Open Zaak) (slower/can't be combined with other filtering options).
104+
* @param signal - Abort signal, should be called in cleanup function in React `useEffect()` hooks.
105+
* @returns {Promise<Option[]>} A promise resolving to an array of options with `value` and `label`.
103106
*/
104107
export async function listZaaktypeChoices(
105-
destructionListUuid?: DestructionList["uuid"],
106-
reviewPk?: Review["pk"],
107-
searchParams?: URLSearchParams,
108+
params?:
109+
| URLSearchParams
110+
| {
111+
inReview: string;
112+
inDestructionList: string;
113+
notInDestructionList: boolean;
114+
},
108115
external = false,
116+
signal?: AbortSignal,
109117
) {
110-
const params = [destructionListUuid, reviewPk, searchParams]
111-
.filter((param) => !!param)
112-
.map((param) => String(param));
118+
const cacheParams = params2CacheKey(params || {});
113119

114120
return cacheMemo(
115121
"listZaaktypeChoices",
116122
async () => {
117-
if (!searchParams) searchParams = new URLSearchParams();
118-
119-
const params = new URLSearchParams({
120-
...Object.fromEntries(searchParams),
121-
});
122-
123-
if (reviewPk) {
124-
params.set("inReview", String(reviewPk));
125-
} else if (destructionListUuid) {
126-
params.set("inDestructionList", destructionListUuid);
127-
} else {
128-
params.set("notInDestructionList", "true");
129-
}
130-
123+
const data = params2Object(params || {});
131124
let response;
125+
132126
if (external) {
133127
response = await request(
134128
"GET",
135129
"/_external-zaaktypen-choices/",
136-
params,
130+
data,
131+
undefined,
132+
undefined,
133+
signal,
137134
);
138135
} else {
139-
const filters = Object.fromEntries(params.entries());
140-
response = await request("POST", "/_zaaktypen-choices/", {}, filters);
136+
response = await request(
137+
"POST",
138+
"/_zaaktypen-choices/",
139+
{},
140+
data,
141+
undefined,
142+
signal,
143+
);
141144
}
142145

143146
const promise: Promise<Option[]> = response.json();
144147

145148
return promise;
146149
},
147-
external ? [...params, "external"] : params,
150+
external ? [cacheParams, "external"] : [cacheParams],
148151
);
149152
}

frontend/src/lib/format/error.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { collectErrors } from "./error";
22

3-
describe("collectErrors", () => {
3+
describe("collectErrors()", () => {
44
test("should return an array with a single string when the input is a string", () => {
55
const errors = "This is an error";
66
const result = collectErrors(errors);
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
entries2CacheKey,
3+
params2CacheKey,
4+
params2Entries,
5+
params2Object,
6+
} from "./params";
7+
8+
describe("params2Object()", () => {
9+
test("should return correct object when URLSearchParams instance is passed", () => {
10+
const obj = params2Object(new URLSearchParams("?foo=bar"));
11+
expect(obj).toEqual({ foo: "bar" });
12+
});
13+
14+
test("should return correct object when object is passed", () => {
15+
const obj = params2Object({ foo: true, bar: 5 });
16+
expect(obj).toEqual({ foo: "true", bar: "5" });
17+
});
18+
});
19+
20+
describe("params2Entries()", () => {
21+
test("should return correct entries when URLSearchParams instance is passed", () => {
22+
const obj = params2Entries(new URLSearchParams("?foo=bar&bar=baz"));
23+
expect(obj).toEqual([
24+
["foo", "bar"],
25+
["bar", "baz"],
26+
]);
27+
});
28+
29+
test("should return correct entries when object is passed", () => {
30+
const obj = params2Entries({ foo: true, bar: 5 });
31+
expect(obj).toEqual([
32+
["foo", "true"],
33+
["bar", "5"],
34+
]);
35+
});
36+
});
37+
38+
describe("params2CacheKey()", () => {
39+
test("should return correct string when URLSearchParams instance is passed", () => {
40+
const obj = params2CacheKey(new URLSearchParams("?foo=bar&bar=baz"));
41+
expect(obj).toEqual("bar=baz:foo=bar");
42+
});
43+
44+
test("should return correct object when object is passed", () => {
45+
const obj = params2CacheKey({ foo: true, bar: 5 });
46+
expect(obj).toEqual("bar=5:foo=true");
47+
});
48+
});
49+
50+
describe("entries2CacheKey()", () => {
51+
test("should return correct string when entries are passed", () => {
52+
const obj = entries2CacheKey([
53+
["foo", "bar"],
54+
["bar", "baz"],
55+
]);
56+
expect(obj).toEqual("bar=baz:foo=bar");
57+
});
58+
});

frontend/src/lib/format/params.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Converts URLSearchParams/Record to object.
3+
* @param params - URLSearchParams or an object.
4+
*/
5+
export function params2Object(
6+
params: URLSearchParams | Record<string | number, unknown>,
7+
): Record<string, string> {
8+
const entries = params2Entries(params);
9+
return Object.fromEntries(entries);
10+
}
11+
/**
12+
* Converts URLSearchParams/Record to cache key string.
13+
* @param params - URLSearchParams or an object.
14+
*/
15+
export function params2CacheKey(
16+
params: URLSearchParams | Record<string | number, unknown>,
17+
): string {
18+
const entries = params2Entries(params);
19+
return entries2CacheKey(entries);
20+
}
21+
22+
/**
23+
* Converts URLSearchParams/Record to entries array.
24+
* @param params - URLSearchParams or an object.
25+
*/
26+
export function params2Entries(
27+
params: URLSearchParams | Record<string | number, unknown>,
28+
): [string, string][] {
29+
return params instanceof URLSearchParams
30+
? [...params.entries()]
31+
: Object.entries(params || {}).map(([k, v]) => [String(k), String(v)]);
32+
}
33+
/**
34+
* Converts entries array to cache key string;
35+
* @param entries - An array of `[string, string]` entries.
36+
*/
37+
export function entries2CacheKey(entries: [string, string][]): string {
38+
return entries
39+
.map((entry) => entry.join("="))
40+
.sort()
41+
.join(":");
42+
}

frontend/src/lib/format/user.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { User } from "../api/auth";
22
import { formatUser } from "./user";
33

4-
describe("formatUser", () => {
4+
describe("formatUser()", () => {
55
test("should return an empty string if the user is null or undefined", () => {
66
// @ts-expect-error - testing untyped behavior.
77
expect(formatUser(null)).toBe("");

frontend/src/pages/destructionlist/abstract/BaseListView.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ export type BaseListViewProps<T extends Zaak = Zaak> = React.PropsWithChildren<{
5454
filterSelectionZaken?: ZaakSelectionZaakFilter;
5555
getSelectionDetail?: ZaakSelectionDetailGetter;
5656

57+
/**
58+
* Allows restricting choices in filters to either:
59+
* - The destruction list or review (`list`).
60+
* - To zaken not already assigned to a destruction list (`unassigned`).
61+
*/
62+
restrictFilterChoices?: false | "list" | "unassigned";
63+
5764
dataGridProps?: Partial<DataGridProps<T>>;
5865

5966
enableUseZaakSelection?: boolean;
@@ -81,6 +88,7 @@ export function BaseListView<T extends Zaak = Zaak>({
8188
extraFields,
8289
filterSelectionZaken,
8390
getSelectionDetail,
91+
restrictFilterChoices,
8492

8593
dataGridProps,
8694

@@ -102,7 +110,7 @@ export function BaseListView<T extends Zaak = Zaak>({
102110

103111
// Fields.
104112
const [fields, setFields, filterTransform, activeFilters, resetFilters] =
105-
useFields<T>(destructionList, review, extraFields);
113+
useFields<T>(destructionList, review, extraFields, restrictFilterChoices);
106114
type FilterTransformData = ReturnType<typeof filterTransform>;
107115

108116
// Filter.

0 commit comments

Comments
 (0)