diff --git a/l10n/en.js b/l10n/en.js index 8dd84a2f4..914e08eab 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -508,8 +508,6 @@ OC.L10N.register( "Organisation or new status is missing": "Organisation or new status is missing", "An error occurred while changing the status": "An error occurred while changing the status", "Search": "Search", - "Search applications and services…": "Search applications and services…", - "Clear search": "Clear search", "Saved views": "Saved views", "Save current filters as view": "Save current filters as view", "GEMMA filters": "GEMMA filters", diff --git a/l10n/en.json b/l10n/en.json index 313f968ea..1126dc6d3 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -507,8 +507,6 @@ "Organisation or new status is missing": "Organisation or new status is missing", "An error occurred while changing the status": "An error occurred while changing the status", "Search": "Search", - "Search applications and services…": "Search applications and services…", - "Clear search": "Clear search", "Saved views": "Saved views", "Save current filters as view": "Save current filters as view", "GEMMA filters": "GEMMA filters", diff --git a/l10n/nl.js b/l10n/nl.js index 3151813d6..da522849f 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -545,8 +545,6 @@ OC.L10N.register( "Group members": "Groepsleden", "Merge organisations": "Organisaties samenvoegen", "Search": "Zoeken", - "Search applications and services…": "Zoek applicaties en diensten…", - "Clear search": "Zoekopdracht wissen", "Saved views": "Opgeslagen weergaven", "Save current filters as view": "Huidige filters opslaan als weergave", "GEMMA filters": "GEMMA-filters", diff --git a/l10n/nl.json b/l10n/nl.json index 680f6f853..34f0d8862 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -544,8 +544,6 @@ "Group members": "Groepsleden", "Merge organisations": "Organisaties samenvoegen", "Search": "Zoeken", - "Search applications and services…": "Zoek applicaties en diensten…", - "Clear search": "Zoekopdracht wissen", "Saved views": "Opgeslagen weergaven", "Save current filters as view": "Huidige filters opslaan als weergave", "GEMMA filters": "GEMMA-filters", diff --git a/openspec/specs/gemma-faceted-search/spec.md b/openspec/specs/gemma-faceted-search/spec.md index ead88fd9d..ee9e18b6c 100644 --- a/openspec/specs/gemma-faceted-search/spec.md +++ b/openspec/specs/gemma-faceted-search/spec.md @@ -137,7 +137,7 @@ The module (`Applications`) and dienst (`Services`) `CnIndexPage`-based index pa - GIVEN a user navigates to the module index page - WHEN the page renders - THEN a facet filter panel MUST be visible showing all four GEMMA dimensions -- AND the existing free-text search box and any `quickFilters` MUST continue to render and function unchanged +- AND the free-text search (the Search tab of the page's Search and columns sidebar, sent by the list as `_search`) and any `quickFilters` MUST continue to render and function #### Scenario: Selecting a facet value updates the object list without a full page reload 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..40c15b9e0 100644 --- a/src/views/FacetedCatalogIndexView.vue +++ b/src/views/FacetedCatalogIndexView.vue @@ -9,8 +9,13 @@ Layout: a `CnFacetSidebar` (from `@conduction/nextcloud-vue`, ADR-012) driven by the `facets` Pinia store on the left, and a self-fetching `CnIndexPage` (register+schema self-fetch mode) on the right. -Why the facet sidebar is NOT `CnIndexPage`'s own embedded -`sidebar.enabled`/`activeFilters` facet machinery: that machinery treats +The free-text search and the column choice are `CnIndexPage`'s own Search +and Columns sidebar (manifest `config.sidebar`): the list sends the term as +`_search` and keeps it in the route, and `onListSearch` hands it to the facet +counts. + +Why the GEMMA facets do NOT go through that sidebar's +`activeFilters` facet machinery: that machinery treats every active-filter key as a LITERAL, directly-filterable schema field and applies it straight to the object-list fetch (`useListView`'s `onFilterChange` → `activeFilters.value[key] = values`, merged verbatim into @@ -49,22 +54,6 @@ generic route-query-to-filter passthrough never sees it (see the
- - - - -