diff --git a/backend/src/openarchiefbeheer/destruction/tests/e2e/issues/test_635_filters_reset.py b/backend/src/openarchiefbeheer/destruction/tests/e2e/issues/test_635_filters_reset.py new file mode 100644 index 00000000..d5e94f1f --- /dev/null +++ b/backend/src/openarchiefbeheer/destruction/tests/e2e/issues/test_635_filters_reset.py @@ -0,0 +1,40 @@ +# fmt: off +from django.test import tag + +from openarchiefbeheer.destruction.constants import ListStatus +from openarchiefbeheer.utils.tests.e2e import browser_page +from openarchiefbeheer.utils.tests.gherkin import GherkinLikeTestCase + + +@tag("e2e") +@tag("gh-635") +class Issue635FiltersReset(GherkinLikeTestCase): + # Tests if: + # - Reset button resets query parameters + # - Reset button resets input fields + # - Reset button resets page number to 1 + # - Reset button is not shown when no filters are applied + async def test_scenario_reset_button_works(self): + async with browser_page() as page: + zaken = await self.given.zaken_are_indexed(amount=500) + record_manager = await self.given.record_manager_exists() + + await self.given.list_exists( + name="Destruction list to reset filters for", + status=ListStatus.ready_to_review, + zaken=zaken, + ) + + await self.when.user_logs_in(page, record_manager) + await self.then.path_should_be(page, "/destruction-lists") + await self.when.user_clicks_button(page, "Destruction list to reset filters for") + await self.then.url_should_contain_text(page, "destruction-lists/") + initial_url_with_page = page.url + "?page=1" + await self.when.user_clicks_button(page, "Volgende") + await self.then.url_should_contain_text(page, "page=2") + await self.then.page_should_not_contain_text(page, "Filters wissen") + await self.when.user_filters_zaken(page, "omschrijving", "some text") + await self.then.url_should_contain_text(page, "omschrijving__icontains=") + await self.when.user_clicks_button(page, "Filters wissen") + await self.then.input_field_should_be_empty(page, "Omschrijving") + await self.then.url_should_be(page, initial_url_with_page) diff --git a/backend/src/openarchiefbeheer/utils/tests/gherkin.py b/backend/src/openarchiefbeheer/utils/tests/gherkin.py index 22178c5d..47522cfa 100644 --- a/backend/src/openarchiefbeheer/utils/tests/gherkin.py +++ b/backend/src/openarchiefbeheer/utils/tests/gherkin.py @@ -723,6 +723,14 @@ async def page_should_contain_text(self, page, text, timeout=None): element = page.locator(f"text={text}") await expect(element.nth(0)).to_be_visible(timeout=timeout) + async def page_should_not_contain_text(self, page, text, timeout=None): + if timeout is None: + timeout = 500 if self.is_inverted else 10000 + + # Check if the text is not present within the timeout + element = page.locator(f"text={text}") + await expect(element).to_have_count(0, timeout=timeout) + async def page_should_contain_element_with_title( self, page, title, timeout=5000 ): @@ -801,3 +809,7 @@ async def this_number_of_zaken_should_be_visible(self, page, number): rows = await locator.locator("tbody").locator("tr").all() self.testcase.assertEqual(len(rows), number) + + async def input_field_should_be_empty(self, page, placeholder): + locator = page.get_by_placeholder(placeholder) + await expect(locator).to_have_value("") diff --git a/frontend/src/hooks/useFields.ts b/frontend/src/hooks/useFields.ts index f8b154de..f5803b21 100644 --- a/frontend/src/hooks/useFields.ts +++ b/frontend/src/hooks/useFields.ts @@ -45,6 +45,8 @@ export function useFields( ( filterData: Partial>, ) => FilterTransformReturnType, + Record, + () => void, ] { const [fieldSelectionState, setFieldSelectionState] = useState(); @@ -53,7 +55,7 @@ export function useFields( setFieldSelectionState(fieldSelection), ); }, []); - const [searchParams] = useSearchParams(); + const [searchParams, setSearchParams] = useSearchParams(); const selectielijstKlasseChoices = useSelectielijstKlasseChoices(); const zaaktypeChoices = useZaaktypeChoices( destructionList, @@ -62,8 +64,9 @@ export function useFields( ); // The raw, unfiltered configuration of the available base fields. + // Both filterLookup AND filterLookups will be used for clearing filters. // NOTE: This get filtered by `getActiveFields()`. - const fields: TypedField[] = [ + const fields: (TypedField & { filterLookups?: string[] })[] = [ { name: "identificatie", filterLookup: "identificatie__icontains", @@ -96,6 +99,7 @@ export function useFields( { name: "startdatum", type: "daterange", + filterLookups: ["startdatum__gte", "startdatum__lte"], filterValue: searchParams.get("startdatum__gte") && searchParams.get("startdatum__lte") @@ -108,6 +112,7 @@ export function useFields( { name: "einddatum", type: "daterange", + filterLookups: ["einddatum__gte", "einddatum__lte"], filterValue: searchParams.get("einddatum__gte") && searchParams.get("einddatum__lte") ? `${searchParams.get("einddatum__gte")}/${searchParams.get("einddatum__lte")}` @@ -162,6 +167,7 @@ export function useFields( name: "archiefactiedatum", type: "daterange", width: "130px", + filterLookups: ["archiefactiedatum__gte", "archiefactiedatum__lte"], filterValue: searchParams.get("archiefactiedatum__gte") && searchParams.get("archiefactiedatum__lte") @@ -206,6 +212,17 @@ export function useFields( ...(extraFields || []), ]; + const filterLookupValues = [ + ...new Set( + fields + .flatMap((field) => [ + field.filterLookup, + ...(field.filterLookups || []), + ]) + .filter(Boolean), + ), + ]; + const getActiveFields = useCallback(() => { return fields.map((field) => { const isActiveFromStorage = @@ -214,10 +231,38 @@ export function useFields( typeof isActiveFromStorage === "undefined" ? field.active !== false : isActiveFromStorage; - return { ...field, active: isActive } as TypedField; + return { ...field, active: isActive }; }); }, [fields, fieldSelectionState]); + /** + * Function to reset all the filters + * It will concat all the `filterLookup` and `filterLookups` values from the `fields` array and remove them from the searchParams + */ + const resetFilters = () => { + const newSearchParams = new URLSearchParams(searchParams); + filterLookupValues.forEach((filterLookup) => { + if (!filterLookup) return; + newSearchParams.delete(filterLookup); + }); + setSearchParams(newSearchParams); + }; + + /** + * A function to return the current active filters + */ + const getActiveFilters = () => { + const activeFilters: Record = {}; + filterLookupValues.forEach((filterLookup) => { + if (!filterLookup) return; + const value = searchParams.get(filterLookup); + if (value) { + activeFilters[filterLookup] = value; + } + }); + return activeFilters; + }; + /** * Gets called when the fields selection is changed. * Pass this to `filterTransform` of a DataGrid component. @@ -267,5 +312,11 @@ export function useFields( }; }; - return [getActiveFields(), setFields, filterTransform]; + return [ + getActiveFields(), + setFields, + filterTransform, + getActiveFilters(), + resetFilters, + ]; } diff --git a/frontend/src/pages/destructionlist/abstract/BaseListView.tsx b/frontend/src/pages/destructionlist/abstract/BaseListView.tsx index cd7a3e9f..122d755a 100644 --- a/frontend/src/pages/destructionlist/abstract/BaseListView.tsx +++ b/frontend/src/pages/destructionlist/abstract/BaseListView.tsx @@ -100,11 +100,8 @@ export function BaseListView({ })) as unknown as T[]; // Fields. - const [fields, setFields, filterTransform] = useFields( - destructionList, - review, - extraFields, - ); + const [fields, setFields, filterTransform, activeFilters, resetFilters] = + useFields(destructionList, review, extraFields); type FilterTransformData = ReturnType; // Filter. @@ -162,7 +159,7 @@ export function BaseListView({ : { ...props, disabled: selectable && !hasSelection }, ); const fixedItems = disabled - ? ([ + ? [ { children: ( <> @@ -174,9 +171,27 @@ export function BaseListView({ wrap: false, onClick: handleClearZaakSelection, }, - ] as ButtonProps[]) + ] : []; - return [...dynamicItems, ...fixedItems]; + if (!Object.keys(activeFilters).length) { + return [...dynamicItems, ...fixedItems]; + } + + return [ + ...dynamicItems, + ...fixedItems, + { + children: ( + <> + + Filters wissen + + ), + variant: "warning", + wrap: false, + onClick: resetFilters, + }, + ]; }, [selectable, hasSelection, selectedZakenOnPage, selectionActions]); return (