diff --git a/l10n/en.js b/l10n/en.js index 70c3bfcef..8dd84a2f4 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -1130,7 +1130,9 @@ OC.L10N.register( "The part {part} of the workbook is too large to import: unpacked it is {size}, and the import reads at most {limit} of one part.": "The part {part} of the workbook is too large to import: unpacked it is {size}, and the import reads at most {limit} of one part.", "Delete the sheets the import does not read from a copy of the export (it reads only \"{first}\" and \"{second}\"), and import that copy.": "Delete the sheets the import does not read from a copy of the export (it reads only \"{first}\" and \"{second}\"), and import that copy.", "The workbook holds more than {count} different texts, the most the import reads.": "The workbook holds more than {count} different texts, the most the import reads.", - "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads.": "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads." + "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads.": "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads.", + "In development since": "In development since", + "In use since": "In use since" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/en.json b/l10n/en.json index c1996692c..313f968ea 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -1129,6 +1129,8 @@ "The part {part} of the workbook is too large to import: unpacked it is {size}, and the import reads at most {limit} of one part.": "The part {part} of the workbook is too large to import: unpacked it is {size}, and the import reads at most {limit} of one part.", "Delete the sheets the import does not read from a copy of the export (it reads only \"{first}\" and \"{second}\"), and import that copy.": "Delete the sheets the import does not read from a copy of the export (it reads only \"{first}\" and \"{second}\"), and import that copy.", "The workbook holds more than {count} different texts, the most the import reads.": "The workbook holds more than {count} different texts, the most the import reads.", - "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads.": "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads." + "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads.": "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads.", + "In development since": "In development since", + "In use since": "In use since" } } diff --git a/l10n/nl.js b/l10n/nl.js index b2c3a6257..3151813d6 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -1198,7 +1198,9 @@ OC.L10N.register( "The part {part} of the workbook is too large to import: unpacked it is {size}, and the import reads at most {limit} of one part.": "Het onderdeel {part} van de werkmap is te groot om te importeren: uitgepakt is het {size}, en de import leest van één onderdeel hooguit {limit}.", "Delete the sheets the import does not read from a copy of the export (it reads only \"{first}\" and \"{second}\"), and import that copy.": "Verwijder in een kopie van de export de tabbladen die de import niet leest (hij leest alleen \"{first}\" en \"{second}\"), en importeer die kopie.", "The workbook holds more than {count} different texts, the most the import reads.": "De werkmap bevat meer dan {count} verschillende teksten, het maximum dat de import leest.", - "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads.": "Samen verwijzen de cellen van de werkmap naar meer dan {size} gedeelde tekst, het maximum dat de import leest." + "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads.": "Samen verwijzen de cellen van de werkmap naar meer dan {size} gedeelde tekst, het maximum dat de import leest.", + "In development since": "In ontwikkeling sinds", + "In use since": "In gebruik sinds" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/nl.json b/l10n/nl.json index e226a52ca..680f6f853 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -1197,6 +1197,8 @@ "The part {part} of the workbook is too large to import: unpacked it is {size}, and the import reads at most {limit} of one part.": "Het onderdeel {part} van de werkmap is te groot om te importeren: uitgepakt is het {size}, en de import leest van één onderdeel hooguit {limit}.", "Delete the sheets the import does not read from a copy of the export (it reads only \"{first}\" and \"{second}\"), and import that copy.": "Verwijder in een kopie van de export de tabbladen die de import niet leest (hij leest alleen \"{first}\" en \"{second}\"), en importeer die kopie.", "The workbook holds more than {count} different texts, the most the import reads.": "De werkmap bevat meer dan {count} verschillende teksten, het maximum dat de import leest.", - "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads.": "Samen verwijzen de cellen van de werkmap naar meer dan {size} gedeelde tekst, het maximum dat de import leest." + "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads.": "Samen verwijzen de cellen van de werkmap naar meer dan {size} gedeelde tekst, het maximum dat de import leest.", + "In development since": "In ontwikkeling sinds", + "In use since": "In gebruik sinds" } } diff --git a/src/customComponents.js b/src/customComponents.js index d3c8c0f77..f6ec30d97 100644 --- a/src/customComponents.js +++ b/src/customComponents.js @@ -151,7 +151,7 @@ export default { // object) and `referentiecomponent`/`standaard` are exposed here by display // NAME, not the identifiers the schema stores — feeding them through that // path would break the list. Stays a custom page (wrapping `CnFacetSidebar` - // + a standalone `CnIndexPage` narrowed via `{ id: matchedObjectIds }`) until + // + a standalone `CnIndexPage` narrowed via `{ _ids: matchedObjectIds }`) until // the lib grows a facet-sidebar mode whose counts/narrowing are computed by // an external, non-schema-field aggregation (see FacetedCatalogIndexView.vue). FacetedCatalogIndexView, diff --git a/src/manifest.d/connections.json b/src/manifest.d/connections.json index 2e9072796..075d92be3 100644 --- a/src/manifest.d/connections.json +++ b/src/manifest.d/connections.json @@ -19,7 +19,15 @@ "register": "@resolve:voorzieningen_register", "schema": "connection", "description": "Every connection between applications, and from an application to a national provision.", - "columns": ["name", "type", "status", "moduleA", "moduleB", "nonMunicipalProvision", "dataExchangeDirection"], + "columns": [ + "name", + "type", + "status", + { "key": "moduleA", "label": "From application", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "module", "labelField": "name" } }, + { "key": "moduleB", "label": "To application", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "module", "labelField": "name" } }, + "nonMunicipalProvision", + "dataExchangeDirection" + ], "filterMenu": true, "quickFilters": [ { "label": "All", "filter": {}, "default": true }, diff --git a/src/manifest.d/usages.json b/src/manifest.d/usages.json index 1ff71f97e..66990ad7d 100644 --- a/src/manifest.d/usages.json +++ b/src/manifest.d/usages.json @@ -19,7 +19,15 @@ "register": "@resolve:voorzieningen_register", "schema": "usage", "description": "The applications your organisation uses, with the version it runs, where it stands and who owns it.", - "columns": ["module", "moduleVersion", "status", "businessOwner", "technicalOwner", "timeClassification", "serviceDeskRecordId"], + "columns": [ + { "key": "module", "label": "Application", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "module", "labelField": "name" } }, + { "key": "moduleVersion", "label": "Version", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "moduleVersion", "labelField": "version" } }, + "status", + "businessOwner", + "technicalOwner", + "timeClassification", + "serviceDeskRecordId" + ], "filterMenu": true, "quickFilters": [ { "label": "All", "filter": {}, "default": true }, diff --git a/src/manifest.json b/src/manifest.json index 13080eed0..22b321684 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -421,8 +421,8 @@ { "id": "org-stats-applications", "type": "stats-block", "title": "Applications", "icon": "ChartBar", "content": { "entries": [ { "title": "Applications", "register": "@resolve:voorzieningen_register", "schema": "module", "metric": "count", "filter": { "provider": "@objectId" } } ] } }, { "id": "org-stats-contact-persons", "type": "stats-block", "title": "Contact persons", "icon": "ChartBar", "content": { "entries": [ { "title": "Contact persons", "register": "@resolve:voorzieningen_register", "schema": "contactPerson", "metric": "count", "filter": { "organization": "@objectId" } } ] } }, { "id": "org-diensten", "type": "object-list", "title": "Services", "icon": "HandshakeOutline", "content": { "register": "@resolve:voorzieningen_register", "schema": "catalogService", "filter": { "provider": "@objectId" }, "columns": [ { "key": "type", "label": "Type" } ], "limit": 25, "emptyText": "No services registered for this organisation yet" } }, - { "id": "org-modules", "type": "object-list", "title": "Applications", "icon": "Package", "content": { "register": "@resolve:voorzieningen_register", "schema": "module", "filter": { "provider": "@objectId" }, "columns": [ { "key": "licentietype", "label": "License type" }, { "key": "bbnLevel", "label": "BBN level" } ], "limit": 25, "rowRoute": "ModuleDetail", "emptyText": "No applications registered for this organisation yet" } }, - { "id": "org-usages", "type": "object-list", "title": "Applications in use", "icon": "OfficeBuilding", "content": { "register": "@resolve:voorzieningen_register", "schema": "usage", "filter": { "consumer": "@objectId" }, "columns": [ { "key": "module", "label": "Application" }, { "key": "moduleVersion", "label": "Version" }, { "key": "status", "label": "Status" } ], "limit": 25, "rowRoute": "GebruikDetail", "viewAllRoute": "Gebruik", "viewAllQuery": { "consumer": "@objectId" }, "allowCreate": false, "emptyText": "No applications in use recorded for this organisation yet" } }, + { "id": "org-modules", "type": "object-list", "title": "Applications", "icon": "Package", "content": { "register": "@resolve:voorzieningen_register", "schema": "module", "filter": { "provider": "@objectId" }, "columns": [ { "key": "name", "label": "Name" }, { "key": "licentietype", "label": "License type" }, { "key": "bbnLevel", "label": "BBN level" } ], "limit": 25, "rowRoute": "ModuleDetail", "viewAllRoute": "Modules", "viewAllQuery": { "provider": "@objectId" }, "emptyText": "No applications registered for this organisation yet" } }, + { "id": "org-usages", "type": "object-list", "title": "Applications in use", "icon": "OfficeBuilding", "content": { "register": "@resolve:voorzieningen_register", "schema": "usage", "filter": { "consumer": "@objectId" }, "columns": [ { "key": "module", "label": "Application", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "module", "labelField": "name" } }, { "key": "moduleVersion", "label": "Version", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "moduleVersion", "labelField": "version" } }, { "key": "status", "label": "Status" } ], "limit": 25, "rowRoute": "GebruikDetail", "viewAllRoute": "Gebruik", "viewAllQuery": { "consumer": "@objectId" }, "allowCreate": false, "emptyText": "No applications in use recorded for this organisation yet" } }, { "id": "org-contactpersonen", "type": "object-list", "title": "Contact persons", "icon": "AccountGroup", "content": { "register": "@resolve:voorzieningen_register", "schema": "contactPerson", "filter": { "organization": "@objectId" }, "columns": [ { "key": "role", "label": "Function" }, { "key": "roles", "label": "Roles" } ], "limit": 25, "rowRoute": "ContactpersoonDetail", "emptyText": "No contact persons linked to this organisation yet" } } ], "layout": [ @@ -511,9 +511,9 @@ { "id": "md-related", "type": "related", "title": "Vendor & services", "icon": "LinkVariant" }, { "id": "md-compliance", "type": "object-list", "title": "Compliance claims", "icon": "ClipboardCheckOutline", "content": { "register": "@resolve:voorzieningen_register", "schema": "compliancy", "filter": { "module": "@objectId" }, "columns": [ { "key": "standardVersion", "label": "Standard" }, { "key": "bioMeasure", "label": "BIO measure" }, { "key": "url", "label": "Evidence" } ], "limit": 50, "rowRoute": "KompliantieDetail", "allowCreate": false, "emptyText": "No compliance claims yet" } }, { "id": "md-versions", "type": "object-list", "title": "Application versions", "icon": "SourceBranch", "content": { "register": "@resolve:voorzieningen_register", "schema": "moduleVersion", "filter": { "module": "@objectId" }, "columns": [ { "key": "version", "label": "Version" }, { "key": "status", "label": "Status" } ], "limit": 25, "rowRoute": "ModuleversieDetail", "allowCreate": false, "emptyText": "No versions registered yet" } }, - { "id": "md-usages", "type": "object-list", "title": "Usages", "icon": "OfficeBuilding", "content": { "register": "@resolve:voorzieningen_register", "schema": "usage", "filter": { "module": "@objectId" }, "columns": [ { "key": "consumer", "label": "Organisation" }, { "key": "moduleVersion", "label": "Version" }, { "key": "status", "label": "Status" } ], "limit": 50, "rowRoute": "GebruikDetail", "viewAllRoute": "Gebruik", "viewAllQuery": { "module": "@objectId" }, "addLabel": "Add to our landscape", "formIncludeFields": [ "consumer", "moduleVersion", "status", "businessOwner", "technicalOwner" ], "emptyText": "No organisation registered a usage yet" } }, - { "id": "md-connections-out", "type": "object-list", "title": "Connections from this application", "icon": "LinkVariant", "content": { "register": "@resolve:voorzieningen_register", "schema": "connection", "filter": { "moduleA": "@objectId" }, "columns": [ { "key": "moduleB", "label": "To application" }, { "key": "nonMunicipalProvision", "label": "To national provision" }, { "key": "type", "label": "Type" }, { "key": "status", "label": "Status" } ], "limit": 25, "rowRoute": "KoppelingDetail", "viewAllRoute": "Koppelingen", "viewAllQuery": { "moduleA": "@objectId" }, "allowCreate": false, "emptyText": "No connections start at this application" } }, - { "id": "md-connections-in", "type": "object-list", "title": "Connections to this application", "icon": "LinkVariant", "content": { "register": "@resolve:voorzieningen_register", "schema": "connection", "filter": { "moduleB": "@objectId" }, "columns": [ { "key": "moduleA", "label": "From application" }, { "key": "type", "label": "Type" }, { "key": "status", "label": "Status" } ], "limit": 25, "rowRoute": "KoppelingDetail", "viewAllRoute": "Koppelingen", "viewAllQuery": { "moduleB": "@objectId" }, "allowCreate": false, "emptyText": "No connections end at this application" } }, + { "id": "md-usages", "type": "object-list", "title": "Usages", "icon": "OfficeBuilding", "content": { "register": "@resolve:voorzieningen_register", "schema": "usage", "filter": { "module": "@objectId" }, "columns": [ { "key": "consumer", "label": "Organisation", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "organization", "labelField": "name" } }, { "key": "moduleVersion", "label": "Version", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "moduleVersion", "labelField": "version" } }, { "key": "status", "label": "Status" } ], "limit": 50, "rowRoute": "GebruikDetail", "viewAllRoute": "Gebruik", "viewAllQuery": { "module": "@objectId" }, "addLabel": "Add to our landscape", "formIncludeFields": [ "consumer", "moduleVersion", "status", "businessOwner", "technicalOwner" ], "emptyText": "No organisation registered a usage yet" } }, + { "id": "md-connections-out", "type": "object-list", "title": "Connections from this application", "icon": "LinkVariant", "content": { "register": "@resolve:voorzieningen_register", "schema": "connection", "filter": { "moduleA": "@objectId" }, "columns": [ { "key": "moduleB", "label": "To application", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "module", "labelField": "name" } }, { "key": "nonMunicipalProvision", "label": "To national provision" }, { "key": "type", "label": "Type" }, { "key": "status", "label": "Status" } ], "limit": 25, "rowRoute": "KoppelingDetail", "viewAllRoute": "Koppelingen", "viewAllQuery": { "moduleA": "@objectId" }, "allowCreate": false, "emptyText": "No connections start at this application" } }, + { "id": "md-connections-in", "type": "object-list", "title": "Connections to this application", "icon": "LinkVariant", "content": { "register": "@resolve:voorzieningen_register", "schema": "connection", "filter": { "moduleB": "@objectId" }, "columns": [ { "key": "moduleA", "label": "From application", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "module", "labelField": "name" } }, { "key": "type", "label": "Type" }, { "key": "status", "label": "Status" } ], "limit": 25, "rowRoute": "KoppelingDetail", "viewAllRoute": "Koppelingen", "viewAllQuery": { "moduleB": "@objectId" }, "allowCreate": false, "emptyText": "No connections end at this application" } }, { "id": "md-ai-systems", "type": "object-list", "title": "AI systems", "icon": "RobotOutline", "content": { "register": "@resolve:voorzieningen_register", "schema": "aiSystem", "filter": { "module": "@objectId" }, "columns": [ { "key": "kind", "label": "Kind" }, { "key": "aiActRiskCategory", "label": "AI Act risk category" }, { "key": "status", "label": "Status" } ], "limit": 25, "rowRoute": "AiSystemDetail", "viewAllRoute": "AiSystems", "viewAllQuery": { "module": "@objectId" }, "allowCreate": false, "emptyText": "No AI systems registered for this application" } }, { "id": "md-maintenance", "type": "object-list", "title": "Planned maintenance", "icon": "Calendar", "content": { "register": "@resolve:voorzieningen_register", "schema": "maintenanceWindow", "filter": { "module": "@objectId" }, "sort": { "field": "startsAt", "dir": "asc" }, "columns": [ { "key": "title", "label": "Title" }, { "key": "startsAt", "label": "Starts at" }, { "key": "endsAt", "label": "Ends at" }, { "key": "impact", "label": "Impact" }, { "key": "status", "label": "Status" } ], "limit": 25, "addLabel": "Announce maintenance", "formIncludeFields": [ "title", "description", "moduleVersion", "startsAt", "endsAt", "impact" ], "emptyText": "No maintenance planned on this application" } } ], @@ -616,20 +616,20 @@ "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 { id: matchedObjectIds }. 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 { id: ... } 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": { "register": "@resolve:voorzieningen_register", "schema": "module", "detailRoute": "ModuleDetail", "description": "Browse the application (module) catalogue, filtered by GEMMA architecture dimension.", "columns": [ - "name", - "provider", - "type", - "licentietype", - "publicationDate", - "bbnLevel", - "dpiaStatus" + { "key": "name", "label": "Name" }, + { "key": "provider", "label": "Supplier", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "organization", "labelField": "name" } }, + { "key": "type", "label": "Type" }, + { "key": "licentietype", "label": "License type" }, + { "key": "publicationDate", "label": "Publication date" }, + { "key": "bbnLevel", "label": "BBN level" }, + { "key": "dpiaStatus", "label": "DPIA status" } ], "quickFilters": [ { @@ -756,7 +756,7 @@ { "id": "st-data", "type": "data", "title": "Standard", "icon": "ShieldCheckOutline", "content": { "columns": 2 } }, { "id": "st-files", "type": "integration", "integrationId": "files", "title": "Specification documents", "icon": "FolderOutline" }, { "id": "st-related", "type": "related", "title": "Related", "icon": "LinkVariant" }, - { "id": "st-compliance", "type": "object-list", "title": "Compliance claims for this standard", "icon": "ClipboardCheckOutline", "content": { "register": "@resolve:voorzieningen_register", "schema": "compliancy", "filter": { "standardGemma": "@object.name" }, "columns": [ { "key": "module", "label": "Module" }, { "key": "standardVersion", "label": "Standard version" }, { "key": "url", "label": "Evidence URL" } ], "limit": 50, "rowRoute": "KompliantieDetail", "allowCreate": false, "emptyText": "No compliance claims yet" } } + { "id": "st-compliance", "type": "object-list", "title": "Compliance claims for this standard", "icon": "ClipboardCheckOutline", "content": { "register": "@resolve:voorzieningen_register", "schema": "compliancy", "filter": { "standardGemma": "@object.name" }, "columns": [ { "key": "module", "label": "Module", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "module", "labelField": "name" } }, { "key": "standardVersion", "label": "Standard version" }, { "key": "url", "label": "Evidence URL" } ], "limit": 50, "rowRoute": "KompliantieDetail", "allowCreate": false, "emptyText": "No compliance claims yet" } } ], "layout": [ { "id": "1", "widgetId": "st-data", "gridX": 0, "gridY": 0, "gridWidth": 8, "gridHeight": 4 }, @@ -808,7 +808,7 @@ "_note": "Catalog archetype (a BIO 2.0 measure), mirroring StandaardDetail's shape for the parallel bioMaatregel relation (design.md Decision 1: compliancy links to a standaardversie OR a bioMaatregel, never a third parallel model). Body: measure data 8-wide (code, naam, omschrijving, thema, bioVersie, bbnNiveau, bron). Below, a compliancy object-list shows every compliance claim referencing this measure (compliancy.bioMaatregel = @objectId), mirroring how StandaardDetail's st-compliance widget shows claims for a standard.", "widgets": [ { "id": "bm-data", "type": "data", "title": "BIO measure", "icon": "ShieldCheckOutline", "content": { "columns": 2 } }, - { "id": "bm-compliance", "type": "object-list", "title": "Compliance claims for this measure", "icon": "ClipboardCheckOutline", "content": { "register": "@resolve:voorzieningen_register", "schema": "compliancy", "filter": { "bioMeasure": "@objectId" }, "columns": [ { "key": "module", "label": "Module" }, { "key": "url", "label": "Evidence URL" } ], "limit": 50, "rowRoute": "KompliantieDetail", "allowCreate": false, "emptyText": "No compliance claims yet" } } + { "id": "bm-compliance", "type": "object-list", "title": "Compliance claims for this measure", "icon": "ClipboardCheckOutline", "content": { "register": "@resolve:voorzieningen_register", "schema": "compliancy", "filter": { "bioMeasure": "@objectId" }, "columns": [ { "key": "module", "label": "Module", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "module", "labelField": "name" } }, { "key": "url", "label": "Evidence URL" } ], "limit": 50, "rowRoute": "KompliantieDetail", "allowCreate": false, "emptyText": "No compliance claims yet" } } ], "layout": [ { "id": "1", "widgetId": "bm-data", "gridX": 0, "gridY": 0, "gridWidth": 12, "gridHeight": 4 }, @@ -943,11 +943,11 @@ "register": "@resolve:voorzieningen_register", "schema": "moduleVersion", "columns": [ - "version", - "module", - "dateInDevelopment", - "dateInUse", - "status" + { "key": "version", "label": "Version" }, + { "key": "module", "label": "Application", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "module", "labelField": "name" } }, + { "key": "dateInDevelopment", "label": "In development since" }, + { "key": "dateInUse", "label": "In use since" }, + { "key": "status", "label": "Status" } ], "quickFilters": [ { "label": "All", "filter": {}, "default": true }, diff --git a/src/store/modules/facets.js b/src/store/modules/facets.js index 0f7ddfe3b..a8a322d79 100644 --- a/src/store/modules/facets.js +++ b/src/store/modules/facets.js @@ -35,7 +35,7 @@ import { * `standaardVersies`, … — see design.md). Letting the bare dimension name * leak into `$route.query` would make CnIndexPage attempt an incorrect * direct-field filter (near-guaranteed zero results) IN ADDITION to this - * feature's own `{ id: matchedObjectIds }` narrowing. The `_gf_` prefix + * feature's own `{ _ids: matchedObjectIds }` narrowing. The `_gf_` prefix * keeps GEMMA facet state in the URL (deep-linkable, per spec) while staying * invisible to that generic passthrough (`_`-prefixed keys are reserved/ * skipped there). @@ -158,7 +158,7 @@ export const useFacetStore = defineStore('facets', { /** * The RBAC/filter/search-scoped object id set the last-fetched facet * response describes (`_meta.matchedObjectIds`) — used to narrow the - * schema's own object-list query via `{ id: [...] }` (see + * schema's own object-list query via `{ _ids: [...] }` (see * `FacetService::computeFacetsForRequest()`'s docblock for why an * id-based filter is used instead of re-deriving one from the facet * selection: `domain`/`applicationService` are not module/catalogService diff --git a/src/utils/facetNarrowing.js b/src/utils/facetNarrowing.js new file mode 100644 index 000000000..58b6a9047 --- /dev/null +++ b/src/utils/facetNarrowing.js @@ -0,0 +1,34 @@ +/** + * The `CnIndexPage` filter that narrows a self-fetch object list to the ids + * a facets response matched (gemma-faceted-search). + * + * The filter key is OpenRegister's `_ids` list parameter, NOT a plain `id` + * field filter: the objects endpoint ignores `id` (`id[]=` returned + * the whole list on OpenRegister 2.1.36), so a search or GEMMA facet + * selection narrowed nothing while the facet counts said it did. + * + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-facet-counts-reflect-the-currently-filtered-set-not-the-unfiltered-universe + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-facets-combine-with-free-text-search + */ + +/** + * Id that no real object carries. Sent when the facets matched nothing, so + * the list is empty instead of `CnIndexPage` treating an empty id list as + * "no filter" and showing everything. + */ +export const FACET_NO_MATCH_SENTINEL = '__gemma_facet_no_match__' + +/** + * Build the narrowing filter for a matched id set. + * + * @param {Array} ids The `_meta.matchedObjectIds` of the last facets response. + * @return {{ _ids: Array }} The `CnIndexPage` `filter` prop value. + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-facet-counts-reflect-the-currently-filtered-set-not-the-unfiltered-universe + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-facets-combine-with-free-text-search + */ +export function facetNarrowingFilter(ids) { + const list = Array.isArray(ids) + ? ids.filter((id) => typeof id === 'string' && id !== '') + : [] + return { _ids: list.length > 0 ? list : [FACET_NO_MATCH_SENTINEL] } +} diff --git a/src/utils/facetNarrowing.spec.js b/src/utils/facetNarrowing.spec.js new file mode 100644 index 000000000..06f101c92 --- /dev/null +++ b/src/utils/facetNarrowing.spec.js @@ -0,0 +1,32 @@ +/** + * facetNarrowing.spec.js — the filter that narrows the Applications list to + * the ids a facets response matched uses OpenRegister's `_ids` parameter. + * + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-facets-combine-with-free-text-search + */ +import { + FACET_NO_MATCH_SENTINEL, + facetNarrowingFilter, +} from '../../src/utils/facetNarrowing.js' + +describe('facetNarrowingFilter', () => { + it('sends the matched ids as the _ids list parameter, never as a plain id field filter', () => { + const filter = facetNarrowingFilter(['a1', 'b2']) + expect(filter).toEqual({ _ids: ['a1', 'b2'] }) + expect(filter).not.toHaveProperty('id') + }) + + it('sends the no-match sentinel when nothing matched, so the list is empty rather than unfiltered', () => { + expect(facetNarrowingFilter([])).toEqual({ _ids: [FACET_NO_MATCH_SENTINEL] }) + expect(facetNarrowingFilter(undefined)).toEqual({ + _ids: [FACET_NO_MATCH_SENTINEL], + }) + }) + + it('drops blank entries before deciding whether anything matched', () => { + expect(facetNarrowingFilter(['', null, 'c3'])).toEqual({ _ids: ['c3'] }) + expect(facetNarrowingFilter(['', null])).toEqual({ + _ids: [FACET_NO_MATCH_SENTINEL], + }) + }) +}) diff --git a/src/views/FacetedCatalogIndexView.vue b/src/views/FacetedCatalogIndexView.vue index cac15d0eb..f80deda08 100644 --- a/src/views/FacetedCatalogIndexView.vue +++ b/src/views/FacetedCatalogIndexView.vue @@ -21,7 +21,7 @@ display NAME, not the `referentieComponenten`/`standaardVersies` identifiers the schema actually stores. Feeding GEMMA facet keys through that path would apply an incorrect direct-field filter (near-guaranteed empty results) alongside this feature's own narrowing. This view instead narrows the -self-fetch list via the bounded, RBAC-scoped `{ id: matchedObjectIds }` list +self-fetch list via the bounded, RBAC-scoped `{ _ids: matchedObjectIds }` list `FacetService` already computes (`_meta.matchedObjectIds` — see `lib/Service/FacetService.php::computeFacetsForRequest()`), and keeps GEMMA facet state in the URL under `_gf_`-prefixed query keys so CnIndexPage's own @@ -147,6 +147,7 @@ import FolderStarOutline from 'vue-material-design-icons/FolderStarOutline.vue' import SaveFacetViewModal from '../modals/SaveFacetViewModal.vue' import { useFacetStore } from '../store/modules/facets.js' import { rowDetailLocation } from '../utils/applicationContracts.js' +import { facetNarrowingFilter } from '../utils/facetNarrowing.js' import { buildFacetDimensionSchema } from '../utils/facetSchema.js' /** Dimension key -> translated label, matching `FacetController`'s query params. */ @@ -226,10 +227,10 @@ export default { * with the GEMMA facet narrowing below: `CnIndexPage`'s self-fetch * merges `{ ...base(filter prop), ...activeTab.filter }` (see * `useSelfFetchList.js#fixedFilters`), and this view's own `filter` - * prop only ever carries the single `id` key - * (`{ id: matchedObjectIds }`, see `listFilter` below). A quick + * prop only ever carries the single `_ids` key + * (`{ _ids: matchedObjectIds }`, see `listFilter` below). A quick * filter's keys (`bbnLevel`, `dpiaStatus`, …) never collide with - * `id`, so the merge is additive — both the facet-matched id set AND + * `_ids`, so the merge is additive — both the facet-matched id set AND * the active quick filter's field constraints apply together * (logical AND), never clobbering one another. */ @@ -307,10 +308,11 @@ export default { }, /** - * Bounded `{ id: [...] }` narrowing filter for `CnIndexPage`'s + * Bounded `{ _ids: [...] }` narrowing filter for `CnIndexPage`'s * self-fetch object list, sourced from the last facets response's * `_meta.matchedObjectIds`. Empty object (no narrowing) when no facet - * filter or search term is active. + * filter or search term is active. `facetNarrowingFilter` says why the + * key is `_ids` and what is sent when nothing matched. * * @return {object} The `CnIndexPage` `filter` prop value. * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-facet-counts-reflect-the-currently-filtered-set-not-the-unfiltered-universe @@ -320,11 +322,9 @@ export default { return {} } - const ids = this.facetStore.matchedObjectIdsFor(this.schema) - // A real (if unlikely) id can never collide with this sentinel — - // forces a correct EMPTY list rather than `CnIndexPage` treating - // an empty `id` array as "no filter" (showing everything). - return { id: ids.length > 0 ? ids : ['__gemma_facet_no_match__'] } + return facetNarrowingFilter( + this.facetStore.matchedObjectIdsFor(this.schema), + ) }, /**