From b9460014d8a308c03e87be584ae3ab75dff7f77e Mon Sep 17 00:00:00 2001 From: WilcoLouwerse Date: Mon, 12 Oct 2026 13:37:20 +0200 Subject: [PATCH 1/2] fix(ui): search the Applications and Services lists server side and give them the Search and Columns sidebar A free-text search on the faceted Applications page narrowed the list by every id the facets matched, sent as _ids[] in the query string. From about 170 matches the request line was too long, the server answered HTTP 414 and the list showed an error on every search after that. The page now has the Search and Columns sidebar the other index pages have. Its search is the list's own _search, which OpenRegister pages server side; the facet counts follow the same term. Only a GEMMA facet selection still narrows the list by the matched ids. The Services page uses the same view and gets the same sidebar, as its toolbar search box is gone. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/manifest.json | 4 +- src/utils/facetNarrowing.js | 30 +++++++ src/utils/facetNarrowing.spec.js | 26 ++++++ src/views/FacetedCatalogIndexView.vue | 122 +++++++++++++------------- 4 files changed, 121 insertions(+), 61 deletions(-) diff --git a/src/manifest.json b/src/manifest.json index 22b321684..825de7293 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -616,12 +616,13 @@ "type": "custom", "title": "Applications", "component": "FacetedCatalogIndexView", - "_note": "Faceted index page (gemma-faceted-search): CnFacetSidebar (live GEMMA-dimension counts from FacetService) + a standalone CnIndexPage narrowed via { _ids: matchedObjectIds } (OpenRegister's list parameter; the objects endpoint ignores a plain id filter). Columns are objects: `provider` is a reference, so it renders through the fkResolve cell widget (uuid → organisation name) and carries its own label — object-typed properties get no schema-generated column, so a bare key showed the raw uuid under the header 'provider'. Stays custom rather than type:index — see customComponents.js's FacetedCatalogIndexView entry for why CnIndexPage's own embedded facet sidebar cannot drive this narrowing. bio-compliance-assessment: bbnLevel/dpiaStatus columns + BBN/DPIA quickFilters reconciled onto this page after gemma-faceted-search (#382) merged — see FacetedCatalogIndexView.vue's quickFilters prop docblock for how the quick filter's field constraints compose with the facet { _ids: ... } narrowing (disjoint keys, additive AND, no clobber).", + "_note": "Faceted index page (gemma-faceted-search): CnFacetSidebar (live GEMMA-dimension counts from FacetService) + a standalone CnIndexPage narrowed via { _ids: matchedObjectIds } (OpenRegister's list parameter; the objects endpoint ignores a plain id filter). Columns are objects: `provider` is a reference, so it renders through the fkResolve cell widget (uuid → organisation name) and carries its own label — object-typed properties get no schema-generated column, so a bare key showed the raw uuid under the header 'provider'. Stays custom rather than type:index — see customComponents.js's FacetedCatalogIndexView entry for why CnIndexPage's own embedded facet sidebar cannot drive this narrowing. bio-compliance-assessment: bbnLevel/dpiaStatus columns + BBN/DPIA quickFilters reconciled onto this page after gemma-faceted-search (#382) merged — see FacetedCatalogIndexView.vue's quickFilters prop docblock for how the quick filter's field constraints compose with the facet { _ids: ... } narrowing (disjoint keys, additive AND, no clobber). config.sidebar gives the page the Search and Columns sidebar of the other index pages; its search is the page's free-text search, sent by the list itself as _search, so only a GEMMA facet selection narrows by _ids (a search narrowed by _ids put every match in the URL and failed with HTTP 414 from about 170 matches).", "config": { "register": "@resolve:voorzieningen_register", "schema": "module", "detailRoute": "ModuleDetail", "description": "Browse the application (module) catalogue, filtered by GEMMA architecture dimension.", + "sidebar": { "enabled": true, "showMetadata": true }, "columns": [ { "key": "name", "label": "Name" }, { "key": "provider", "label": "Supplier", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "organization", "labelField": "name" } }, @@ -677,6 +678,7 @@ "register": "@resolve:voorzieningen_register", "schema": "catalogService", "description": "Browse the service (dienst) catalogue, filtered by GEMMA architecture dimension.", + "sidebar": { "enabled": true, "showMetadata": true }, "columns": [ "name", "provider", diff --git a/src/utils/facetNarrowing.js b/src/utils/facetNarrowing.js index 58b6a9047..f37b4678a 100644 --- a/src/utils/facetNarrowing.js +++ b/src/utils/facetNarrowing.js @@ -32,3 +32,33 @@ export function facetNarrowingFilter(ids) { : [] return { _ids: list.length > 0 ? list : [FACET_NO_MATCH_SENTINEL] } } + +/** + * Whether any GEMMA facet dimension has a selected value. + * + * @param {object} activeFilters The facet store's `{ dimension: [values] }` map. + * @return {boolean} True when at least one dimension has a value. + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-facets-combine-with-free-text-search + */ +export function hasFacetSelection(activeFilters) { + return Object.values(activeFilters ?? {}).some( + (values) => Array.isArray(values) && values.length > 0, + ) +} + +/** + * The `CnIndexPage` `filter` prop of a faceted list. + * + * Only a GEMMA facet selection narrows by id. A free-text search alone goes + * through the list's own `_search`, which OpenRegister pages server side: + * narrowing it by the matched ids put every match in the URL, and from about + * 170 matches the request line was too long (HTTP 414) and the list failed. + * + * @param {object} activeFilters The facet store's `{ dimension: [values] }` map. + * @param {Array} ids The `_meta.matchedObjectIds` of the last facets response. + * @return {object} `{}` without a facet selection, else `{ _ids: [...] }`. + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-facets-combine-with-free-text-search + */ +export function listNarrowingFilter(activeFilters, ids) { + return hasFacetSelection(activeFilters) ? facetNarrowingFilter(ids) : {} +} diff --git a/src/utils/facetNarrowing.spec.js b/src/utils/facetNarrowing.spec.js index 06f101c92..92775598b 100644 --- a/src/utils/facetNarrowing.spec.js +++ b/src/utils/facetNarrowing.spec.js @@ -7,6 +7,8 @@ import { FACET_NO_MATCH_SENTINEL, facetNarrowingFilter, + hasFacetSelection, + listNarrowingFilter, } from '../../src/utils/facetNarrowing.js' describe('facetNarrowingFilter', () => { @@ -30,3 +32,27 @@ describe('facetNarrowingFilter', () => { }) }) }) + +describe('listNarrowingFilter', () => { + it("does not narrow by id without a facet selection, so a search stays on the list's own _search", () => { + const many = Array.from({ length: 1200 }, (_, i) => `id-${i}`) + expect(listNarrowingFilter({}, many)).toEqual({}) + expect(listNarrowingFilter({ domain: [] }, many)).toEqual({}) + expect(listNarrowingFilter(undefined, many)).toEqual({}) + }) + + it('narrows by the matched ids once a facet dimension has a value', () => { + expect(listNarrowingFilter({ domain: ['Burgerzaken'] }, ['a1'])).toEqual({ + _ids: ['a1'], + }) + expect(listNarrowingFilter({ standard: ['StUF'] }, [])).toEqual({ + _ids: [FACET_NO_MATCH_SENTINEL], + }) + }) + + it('tells a selection from an empty dimension', () => { + expect(hasFacetSelection({ domain: [], standard: ['StUF'] })).toBe(true) + expect(hasFacetSelection({ domain: [] })).toBe(false) + expect(hasFacetSelection(null)).toBe(false) + }) +}) diff --git a/src/views/FacetedCatalogIndexView.vue b/src/views/FacetedCatalogIndexView.vue index f80deda08..97694580c 100644 --- a/src/views/FacetedCatalogIndexView.vue +++ b/src/views/FacetedCatalogIndexView.vue @@ -49,22 +49,6 @@ generic route-query-to-filter passthrough never sees it (see the
- - - - -