diff --git a/backend/src/openarchiefbeheer/accounts/api/views.py b/backend/src/openarchiefbeheer/accounts/api/views.py index fd8ccc05..95ca761c 100644 --- a/backend/src/openarchiefbeheer/accounts/api/views.py +++ b/backend/src/openarchiefbeheer/accounts/api/views.py @@ -9,6 +9,38 @@ from .serializers import UserSerializer +@extend_schema( + tags=["Users"], + summary=_("Users list"), + description=_("List all the users."), + responses={ + 200: UserSerializer(many=True), + }, +) +class UsersView(ListAPIView): + serializer_class = UserSerializer + + def get_queryset(self) -> QuerySet[User]: + return User.objects.all() + + +@extend_schema( + tags=["Users"], + summary=_("Record managers list"), + description=_( + "List all the users that have the permission to create destruction lists." + ), + responses={ + 200: UserSerializer(many=True), + }, +) +class RecordManagersView(ListAPIView): + serializer_class = UserSerializer + + def get_queryset(self) -> QuerySet[User]: + return User.objects.record_managers() + + @extend_schema( tags=["Users"], summary=_("Main reviewers list"), diff --git a/backend/src/openarchiefbeheer/accounts/managers.py b/backend/src/openarchiefbeheer/accounts/managers.py index 1ad83d19..61d31541 100644 --- a/backend/src/openarchiefbeheer/accounts/managers.py +++ b/backend/src/openarchiefbeheer/accounts/managers.py @@ -4,6 +4,50 @@ class UserQuerySet(QuerySet): + def record_managers(self) -> "UserQuerySet": + """ + Returns a QuerySet of users that have the permission to start a destruction list. + """ + permission = Permission.objects.get(codename="can_start_destruction") + return self._users_with_permission(permission) + + def reviewers(self) -> "UserQuerySet": + """ + Returns a QuerySet of users that have the permission to review a destruction list. + """ + return self.filter( # noqa + Q(groups__permissions__codename="can_review_destruction") + | Q(user_permissions__codename="can_review_destruction") + | Q(groups__permissions__codename="can_review_final_list") + | Q(user_permissions__codename="can_review_final_list") + ).distinct() + + def main_reviewers(self) -> "UserQuerySet": + """ + Returns a QuerySet of users that have the permission to review a destruction list (main reviewers ony). + """ + permission = Permission.objects.get(codename="can_review_destruction") + return self._users_with_permission(permission) + + def co_reviewers(self) -> "UserQuerySet": + """ + Returns a QuerySet of users that have the permission to review a destruction list (co reviewers ony). + """ + permission = Permission.objects.get(codename="can_co_review_destruction") + return self._users_with_permission(permission) + + def archivists(self) -> "UserQuerySet": + """ + Returns a QuerySet of users that have the permission to perform a final review on a destruction list. + """ + permission = Permission.objects.get(codename="can_review_final_list") + return self._users_with_permission(permission) + + def _users_with_permission(self, permission: Permission) -> "UserQuerySet": + return self.filter( # noqa + Q(groups__permissions=permission) | Q(user_permissions=permission) + ).distinct() + def annotate_permissions(self) -> "UserQuerySet": """ Adds `user_permission_codenames` and `group_permission_codenames` as `ArrayField` to the current QuerySet @@ -71,20 +115,3 @@ def create_superuser(self, username, email, password, **extra_fields): raise ValueError("Superuser must have is_superuser=True.") return self._create_user(username, email, password, **extra_fields) - - def _users_with_permission(self, permission: Permission) -> UserQuerySet: - return self.filter( - Q(groups__permissions=permission) | Q(user_permissions=permission) - ).distinct() - - def main_reviewers(self) -> UserQuerySet: - permission = Permission.objects.get(codename="can_review_destruction") - return self._users_with_permission(permission) - - def archivists(self) -> UserQuerySet: - permission = Permission.objects.get(codename="can_review_final_list") - return self._users_with_permission(permission) - - def co_reviewers(self) -> UserQuerySet: - permission = Permission.objects.get(codename="can_co_review_destruction") - return self._users_with_permission(permission) diff --git a/backend/src/openarchiefbeheer/accounts/tests/test_managers.py b/backend/src/openarchiefbeheer/accounts/tests/test_managers.py index ddd52a84..1333e821 100644 --- a/backend/src/openarchiefbeheer/accounts/tests/test_managers.py +++ b/backend/src/openarchiefbeheer/accounts/tests/test_managers.py @@ -9,6 +9,67 @@ class UserQuerySetTests(TestCase): + def test_can_start_destruction_user_permission(self): + user = UserFactory.create() + record_manager = UserFactory.create(post__can_start_destruction=True) + qs = User.objects.record_managers() + self.assertEqual(len(qs), 1) + self.assertIn(record_manager, qs) + self.assertNotIn(user, qs) + + def test_can_start_destruction_group_permission(self): + record_managers = Group.objects.create() + record_managers.permissions.add( + Permission.objects.get( + codename="can_start_destruction", + ) + ) + + user = UserFactory.create() + record_manager = UserFactory.create() + record_manager.groups.add(record_managers) + + qs = User.objects.record_managers() + self.assertEqual(len(qs), 1) + self.assertIn(record_manager, qs) + self.assertNotIn(user, qs) + + def test_can_review_destruction_user_permission(self): + user = UserFactory.create() + reviewer = UserFactory.create(post__can_review_destruction=True) + archivist = UserFactory.create(post__can_review_final_list=True) + qs = User.objects.reviewers() + self.assertEqual(len(qs), 2) + self.assertIn(reviewer, qs) + self.assertIn(archivist, qs) + self.assertNotIn(user, qs) + + def test_can_review_destruction_group_permission(self): + reviewers = Group.objects.create(name="reviewers") + reviewers.permissions.add( + Permission.objects.get( + codename="can_review_destruction", + ) + ) + archivists = Group.objects.create(name="archivists") + archivists.permissions.add( + Permission.objects.get( + codename="can_review_final_list", + ) + ) + + user = UserFactory.create() + reviewer = UserFactory.create() + reviewer.groups.add(reviewers) + archivist = UserFactory.create() + archivist.groups.add(archivists) + + qs = User.objects.reviewers() + self.assertEqual(len(qs), 2) + self.assertIn(reviewer, qs) + self.assertIn(archivist, qs) + self.assertNotIn(user, qs) + def test_annotate_permissions(self): content_type = ContentType.objects.get_for_model(DestructionList) diff --git a/backend/src/openarchiefbeheer/api/tests/test_role_endpoints.py b/backend/src/openarchiefbeheer/api/tests/test_role_endpoints.py index 92b0aa44..37659bee 100644 --- a/backend/src/openarchiefbeheer/api/tests/test_role_endpoints.py +++ b/backend/src/openarchiefbeheer/api/tests/test_role_endpoints.py @@ -11,6 +11,35 @@ def test_user_not_logged_in(self): self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_retrieve_users(self): + admin = UserFactory.create(is_superuser=True) + UserFactory.create_batch(2) + + self.client.force_authenticate(user=admin) + response = self.client.get(reverse("api:users")) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = response.json() + + self.assertEqual(len(data), 3) + + def test_retrieve_record_managers(self): + admin = UserFactory.create(is_superuser=True) + UserFactory.create_batch(2, post__can_start_destruction=True) + UserFactory.create_batch(2, post__can_start_destruction=False) + + self.client.force_authenticate(user=admin) + response = self.client.get(reverse("api:record-managers")) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = response.json() + + self.assertEqual(len(data), 2) + self.assertTrue(data[0]["role"]["canStartDestruction"]) + self.assertTrue(data[1]["role"]["canStartDestruction"]) + def test_retrieve_reviewers(self): admin = UserFactory.create(is_superuser=True) UserFactory.create_batch(2, post__can_review_destruction=True) diff --git a/backend/src/openarchiefbeheer/api/urls.py b/backend/src/openarchiefbeheer/api/urls.py index 7c25cd04..a34910a3 100644 --- a/backend/src/openarchiefbeheer/api/urls.py +++ b/backend/src/openarchiefbeheer/api/urls.py @@ -11,6 +11,8 @@ ArchivistsView, CoReviewersView, MainReviewersView, + RecordManagersView, + UsersView, WhoAmIView, ) from openarchiefbeheer.config.api.views import ArchiveConfigView, OIDCInfoView @@ -114,6 +116,16 @@ "v1/", include( [ + path( + "users/", + UsersView.as_view(), + name="users", + ), + path( + "record-managers/", + RecordManagersView.as_view(), + name="record-managers", + ), path("reviewers/", MainReviewersView.as_view(), name="reviewers"), path("archivists/", ArchivistsView.as_view(), name="archivists"), path("co-reviewers/", CoReviewersView.as_view(), name="co-reviewers"), diff --git a/backend/src/openarchiefbeheer/destruction/api/filtersets.py b/backend/src/openarchiefbeheer/destruction/api/filtersets.py index b56ec5b7..2f696e72 100644 --- a/backend/src/openarchiefbeheer/destruction/api/filtersets.py +++ b/backend/src/openarchiefbeheer/destruction/api/filtersets.py @@ -3,13 +3,15 @@ from django_filters import ( BooleanFilter, + CharFilter, + ChoiceFilter, FilterSet, NumberFilter, OrderingFilter, UUIDFilter, ) -from ..constants import InternalStatus +from ..constants import InternalStatus, ListRole, ListStatus from ..models import ( DestructionList, DestructionListCoReview, @@ -93,15 +95,30 @@ def filter_order_review_ignored( class DestructionListFilterset(FilterSet): + name = CharFilter(lookup_expr="icontains") + status = ChoiceFilter(choices=ListStatus.choices) + author = NumberFilter() + reviewer = NumberFilter( + field_name="assignees__user", + method="filter_reviewer", + ) assignee = NumberFilter( field_name="assignee", help_text="The pk of the user currently assigned to the list.", ) - ordering = OrderingFilter(fields=("created", "created")) + + def filter_reviewer(self, queryset, name, value): + return queryset.filter( + Q(assignees__role=ListRole.main_reviewer) + | Q(assignees__role=ListRole.co_reviewer), + assignees__user=value, + ) + + ordering = OrderingFilter(fields=(("created", "created"), ("name", "name"))) class Meta: model = DestructionList - fields = ("assignee", "ordering") + fields = ("name", "status", "author", "reviewer", "assignee", "ordering") class DestructionListReviewFilterset(FilterSet): diff --git a/backend/src/openarchiefbeheer/destruction/tests/test_filtersets.py b/backend/src/openarchiefbeheer/destruction/tests/test_filtersets.py index a2f53e02..6e665fde 100644 --- a/backend/src/openarchiefbeheer/destruction/tests/test_filtersets.py +++ b/backend/src/openarchiefbeheer/destruction/tests/test_filtersets.py @@ -8,7 +8,9 @@ from openarchiefbeheer.accounts.tests.factories import UserFactory +from ..constants import ListStatus from .factories import ( + DestructionListAssigneeFactory, DestructionListFactory, DestructionListItemFactory, DestructionListItemReviewFactory, @@ -165,6 +167,138 @@ def test_on_method_field(self): class DestructionListEndpoint(APITestCase): + def test_filter_name(self): + DestructionListFactory.create(name="Destruction list A") + DestructionListFactory.create(name="Destruction list B") + + record_manager = UserFactory.create(post__can_start_destruction=True) + self.client.force_authenticate(user=record_manager) + endpoint = furl(reverse("api:destructionlist-list")) + endpoint.args["name"] = "b" + + response = self.client.get( + endpoint.url, + ) + + data = response.json() + + names = [list["name"] for list in data] + + self.assertEqual( + names, + ["Destruction list B"], + ) + + def test_filter_status(self): + DestructionListFactory.create(name="Destruction list A", status=ListStatus.new) + DestructionListFactory.create( + name="Destruction list B", status=ListStatus.ready_to_review + ) + + record_manager = UserFactory.create(post__can_start_destruction=True) + self.client.force_authenticate(user=record_manager) + endpoint = furl(reverse("api:destructionlist-list")) + endpoint.args["status"] = "ready_to_review" + + response = self.client.get( + endpoint.url, + ) + + data = response.json() + + names = [list["name"] for list in data] + + self.assertEqual( + names, + ["Destruction list B"], + ) + + def test_filter_author(self): + user_a = UserFactory.create() + user_b = UserFactory.create() + + DestructionListFactory.create(name="Destruction list A", author=user_a) + DestructionListFactory.create(name="Destruction list B", author=user_b) + + record_manager = UserFactory.create(post__can_start_destruction=True) + self.client.force_authenticate(user=record_manager) + endpoint = furl(reverse("api:destructionlist-list")) + endpoint.args["author"] = user_b.pk + + response = self.client.get( + endpoint.url, + ) + + data = response.json() + + names = [list["name"] for list in data] + + self.assertEqual( + names, + ["Destruction list B"], + ) + + def test_filter_reviewer(self): + user_a = UserFactory.create() + user_b = UserFactory.create() + + destruction_list_a = DestructionListFactory.create( + name="Destruction list A", + ) + DestructionListAssigneeFactory.create( + destruction_list=destruction_list_a, user=user_a + ) + + destruction_list_b = DestructionListFactory.create( + name="Destruction list B", + ) + DestructionListAssigneeFactory.create( + destruction_list=destruction_list_b, user=user_b + ) + + record_manager = UserFactory.create(post__can_start_destruction=True) + self.client.force_authenticate(user=record_manager) + endpoint = furl(reverse("api:destructionlist-list")) + endpoint.args["reviewer"] = user_b.pk + + response = self.client.get( + endpoint.url, + ) + + data = response.json() + + names = [list["name"] for list in data] + + self.assertEqual( + names, + ["Destruction list B"], + ) + + def test_filter_assignee(self): + user_a = UserFactory.create() + user_b = UserFactory.create() + + DestructionListFactory.create(name="Destruction list A", assignee=user_a) + DestructionListFactory.create(name="Destruction list B", assignee=user_b) + + record_manager = UserFactory.create(post__can_start_destruction=True) + self.client.force_authenticate(user=record_manager) + endpoint = furl(reverse("api:destructionlist-list")) + endpoint.args["assignee"] = user_b.pk + + response = self.client.get( + endpoint.url, + ) + + data = response.json() + + names = [list["name"] for list in data] + + self.assertEqual( + names, + ["Destruction list B"], + ) + def test_ordering_on_creation_date(self): with freeze_time("2024-05-02T16:00:00+02:00"): DestructionListFactory.create(name="Destruction list A") diff --git a/frontend/.storybook/mockData.ts b/frontend/.storybook/mockData.ts index 7a330b39..5171a98b 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 74040df9..dabbcceb 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 00000000..a331fc6e --- /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 00000000..9a76c4c8 --- /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 888537a8..0d1c0351 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 00000000..e926380b --- /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 36fecd01..2de8a733 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 00000000..0e0c8cb6 --- /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 b846aed0..587ec8bc 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 09bfae25..eac22ab0 100644 --- a/frontend/src/pages/landing/Landing.tsx +++ b/frontend/src/pages/landing/Landing.tsx @@ -6,13 +6,24 @@ import { P, Solid, Tooltip, + string2Title, } from "@maykin-ui/admin-ui"; +import { useMemo } from "react"; import { useLoaderData, useNavigate, useRevalidator } from "react-router-dom"; import { ProcessingStatusBadge } from "../../components/ProcessingStatusBadge"; +import { + 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 +101,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 +211,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 +231,108 @@ export const Landing = () => { toolbarProps: { items: [ { + icon: , + name: "name", + placeholder: "Zoeken…", + title: "Zoeken", + type: "search", + value: searchParams.get("name") || "", + onBlur: handleFilter, + onKeyUp: (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleFilter(e); + } + }, + }, + { + icon: , + name: "status", + options: useMemo( + () => + 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: useMemo( + () => [ + ...recordManagers.map((rm) => { + return { + label: formatUser(rm, { showUsername: false }), + value: rm.pk, + }; + }), + ], + [recordManagers], + ), + placeholder: "Auteur…", + required: false, + title: "Auteur", + value: searchParams.get("author") || "", + onChange: handleFilter, + }, + { + icon: , + name: "reviewer", + options: useMemo( + () => [ + ...reviewers.map((rm) => { + return { + label: formatUser(rm, { showUsername: false }), + value: rm.pk, + }; + }), + ], + [reviewers], + ), + placeholder: "Beoordelaar…", + required: false, + title: "Beoordelaar", + value: searchParams.get("reviewer") || "", + onChange: handleFilter, + }, + { + icon: , + name: "assignee", + options: useMemo( + () => [ + ...users.map((rm) => { + return { + label: formatUser(rm, { showUsername: false }), + value: rm.pk, + }; + }), + ], + [users], + ), + 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", {