From 8345ac236b9263a0d766c4b6d7fe79f0ce7b1404 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 9 Aug 2026 22:28:17 +0200 Subject: [PATCH] docs(spec): trace 64 frontend methods to their spec, and sync the spec seven tags already pointed at MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gate-16 spec-coverage 64 -> PASS, measured with hydra-gates 651e5c5 at the CI scope (--scope-to-diff --base origin/beta). gate-46 spec-anchor-existence stays PASS, so every anchor added here resolves to a heading that exists. 64 changed frontend methods across 24 files carried no @spec. Each now names the requirement it serves — facet views and the facet store to gemma-faceted-search, the SBOM panel to sbom-import, the suite wizard to suite-wizard, the portfolio helpers to portfolio-rationalization-time, the view store to view-enrichment-api, the review modals to catalog-ratings, and so on. No tag was added without reading the method and the requirement it points at. openspec/specs/realtime-updates-ui/ is NEW here, and that is the real find. Change `adopt-live-updates-ui` declares "Affected specs: realtime-updates-ui (new)" and shipped src/composables/useLiveCollections.js plus its seven consumers — but the delta was never synced into openspec/specs/. SEVEN @spec tags across six source files have been pointing at a spec that does not exist. gate-46 never caught it because gate-46 validates ANCHORS (#fragment), not bare file targets; an audit of every `@spec openspec/specs//spec.md` in src/ found this one and only this one dangling. The delta is promoted verbatim — its requirement, its three scenarios and its author's reason-bearing `@e2e exclude` are unchanged; the only edits are the title line and the delta's "## ADDED Requirements" heading becoming "## Requirements", plus a Purpose recording where it came from. I did not author that exclusion. I nearly made this worse: the first draft of this change copied the dangling `@spec openspec/specs/realtime-updates-ui/spec.md` onto useLiveCollections() itself. Checking the target existed before trusting it is what turned a propagated broken reference into a fixed one. Can-fail: reverting the 22 annotated files takes gate-16 from PASS back to 51; the run before any of this work reported 64 on the same package. vitest 215/215. gate-46 PASS. No other gate count moved. --- openspec/specs/realtime-updates-ui/spec.md | 43 +++++++++++++++++++ src/components/cards/OrganisatieCard.vue | 1 + src/components/reviews/ReviewsPanel.vue | 1 + src/components/sbom/SbomComponentsPanel.vue | 9 ++++ src/composables/useLiveCollections.js | 9 ++++ src/dialogs/SuiteWizard/Step1Details.vue | 11 +++++ src/dialogs/SuiteWizard/Step2Applications.vue | 3 ++ src/dialogs/SuiteWizard/Step3Confirm.vue | 1 + src/dialogs/SuiteWizardDialog.vue | 3 ++ src/modals/SaveFacetViewModal.vue | 14 ++++++ src/modals/SubmitReviewModal.vue | 2 + src/modals/object/ViewObject.vue | 5 +++ src/services/facets.js | 1 + src/store/modules/facets.js | 2 + src/store/modules/view.js | 2 + src/utils/complianceMatrix.js | 1 + src/utils/portfolioReport.js | 5 +++ src/utils/sbomVulnerabilityMatch.js | 1 + src/views/ComplianceMatrixView.vue | 9 ++++ src/views/Dashboard.vue | 1 + src/views/FacetedCatalogIndexView.vue | 26 ++++++++++- src/views/gemmaviews/GemmaViewIndex.vue | 3 ++ src/views/organisaties/PortfolioReport.vue | 4 ++ .../settings/sections/EolSyncSettings.vue | 10 +++++ src/views/suites/SuitesIndexView.vue | 2 + 25 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 openspec/specs/realtime-updates-ui/spec.md diff --git a/openspec/specs/realtime-updates-ui/spec.md b/openspec/specs/realtime-updates-ui/spec.md new file mode 100644 index 00000000..892625ee --- /dev/null +++ b/openspec/specs/realtime-updates-ui/spec.md @@ -0,0 +1,43 @@ +# realtime-updates-ui Specification + +## Purpose +Live-update subscription behaviour for Softwarecatalog views that render from the +`createObjectStore`-based object store. Synced from change `adopt-live-updates-ui`, +which shipped `src/composables/useLiveCollections.js` and its seven consumers but +was never synced into `openspec/specs/`. + +## Requirements + +### Requirement: Store-rendered views MUST subscribe to live updates for their scope + +Views that render from Softwarecatalog's `createObjectStore`-based object store MUST subscribe to +live updates for the data they display: collection-scoped views subscribe to +`or-collection-{register-slug}-{schema-slug}` per rendered object type, object-scoped views +subscribe to `or-object-{uuid}`. Subscriptions MUST be re-scoped when the viewed scope +changes and released when the view is destroyed. Events are refetch HINTS only: views MUST +refetch through their existing fetch paths and MUST NOT patch rendered state from an event +payload. + +@e2e exclude Requires a second concurrent authenticated session plus a notify_push (or poll-tick) round-trip; covered by the shared library's transport tests and manual two-browser verification. + +#### Scenario: Module view refreshes when a rendered collection changes elsewhere + +- **GIVEN** a module view (vulnerabilities, compliance matrix, license posture, lifecycle + roadmap, or organisaties index) is open +- **WHEN** another user creates, updates or deletes an object of a type the view renders +- **THEN** the view receives the `or-collection-{register}-{schema}` hint, the plugin + re-runs `fetchCollection` with the last-used params, and the view's `getCollection`-backed + computeds re-render the fresh data without a manual refresh + +#### Scenario: Subscription waits for lazy type registration + +- **GIVEN** a module view mounts before its `loadData()` has registered its object types +- **WHEN** the registration lands in the store's `objectTypeRegistry` +- **THEN** the reactive `enabled` gate flips and the subscription attaches — no subscribe + call is attempted against an unregistered type + +#### Scenario: Subscription released on destroy + +- **GIVEN** live subscriptions are active for a module view +- **WHEN** the user navigates away and the component scope is disposed +- **THEN** every subscription is released via the composable's scope-bound lifecycle diff --git a/src/components/cards/OrganisatieCard.vue b/src/components/cards/OrganisatieCard.vue index 24c67606..abfa5355 100644 --- a/src/components/cards/OrganisatieCard.vue +++ b/src/components/cards/OrganisatieCard.vue @@ -240,6 +240,7 @@ export default { * a custom cardComponent like this one must emit it explicitly — * without it, clicking an organisation card was a no-op. * @return {void} + * @spec openspec/specs/fe-organizations/spec.md */ handleCardClick() { this.$emit('click', this.item) diff --git a/src/components/reviews/ReviewsPanel.vue b/src/components/reviews/ReviewsPanel.vue index 942b084e..6a40db14 100644 --- a/src/components/reviews/ReviewsPanel.vue +++ b/src/components/reviews/ReviewsPanel.vue @@ -176,6 +176,7 @@ export default { * Open the submit-review modal. * * @return {void} + * @spec openspec/specs/catalog-ratings/spec.md */ openSubmitModal() { this.showSubmitModal = true diff --git a/src/components/sbom/SbomComponentsPanel.vue b/src/components/sbom/SbomComponentsPanel.vue index 512db221..635e6fd7 100644 --- a/src/components/sbom/SbomComponentsPanel.vue +++ b/src/components/sbom/SbomComponentsPanel.vue @@ -206,6 +206,7 @@ export default { * The moduleVersie's raw data bag. * * @return {object} The property bag. + * @spec openspec/specs/sbom-import/spec.md#requirement-imported-components-persist-as-openregister-objects-scoped-to-a-moduleversie */ moduleVersieData() { if (!this.moduleVersie) { @@ -243,6 +244,7 @@ export default { * All kwetsbaarheid records — the vulnerability-match candidate set. * * @return {Array} The kwetsbaarheid records. + * @spec openspec/specs/sbom-import/spec.md#requirement-components-are-matched-against-existing-kwetsbaarheden-without-external-calls */ kwetsbaarheden() { return objectStore.getCollection('kwetsbaarheid')?.results || [] @@ -263,6 +265,7 @@ export default { * Display rows for CnDataTable: name/version/purl/licenses plus match badge counts. * * @return {Array} The table rows. + * @spec openspec/specs/sbom-import/spec.md#requirement-the-module-version-detail-page-shows-imported-components-with-summary-counts */ rows() { return this.matches.rows.map(({ component, confirmed, possible }) => { @@ -283,6 +286,7 @@ export default { * Total imported component count. * * @return {number} The count. + * @spec openspec/specs/sbom-import/spec.md#requirement-the-module-version-detail-page-shows-imported-components-with-summary-counts */ totalComponents() { return this.components.length @@ -292,6 +296,7 @@ export default { * Distinct, non-empty license count across the imported set. * * @return {number} The count. + * @spec openspec/specs/sbom-import/spec.md#requirement-the-module-version-detail-page-shows-imported-components-with-summary-counts */ distinctLicenseCount() { const set = new Set() @@ -313,6 +318,7 @@ export default { * deduplicated) across the whole component list. * * @return {number} The count. + * @spec openspec/specs/sbom-import/spec.md#requirement-components-are-matched-against-existing-kwetsbaarheden-without-external-calls */ matchedVulnerabilityCount() { return this.matches.matchedVulnerabilityCount @@ -378,6 +384,7 @@ export default { * * @param {string} type Object type slug. * @return {Promise} Resolves once fetched. + * @spec openspec/specs/sbom-import/spec.md#requirement-the-module-version-detail-page-shows-imported-components-with-summary-counts */ async fetchType(type) { if (typeof objectStore.registerObjectType === 'function' @@ -402,6 +409,7 @@ export default { * * @param {Event} event The file input change event. * @return {void} + * @spec openspec/specs/sbom-import/spec.md#requirement-uploaded-sbom-files-are-bounded-in-size-and-json-only */ handleFileSelect(event) { const file = event.target.files[0] @@ -417,6 +425,7 @@ export default { * * @param {number} bytes The size in bytes. * @return {string} A human-readable size. + * @spec openspec/specs/sbom-import/spec.md#requirement-uploaded-sbom-files-are-bounded-in-size-and-json-only */ formatFileSize(bytes) { if (!bytes) { diff --git a/src/composables/useLiveCollections.js b/src/composables/useLiveCollections.js index 8cda92c5..f67a1e20 100644 --- a/src/composables/useLiveCollections.js +++ b/src/composables/useLiveCollections.js @@ -26,6 +26,15 @@ import { computed } from 'vue' import { useObjectSubscription } from '@conduction/nextcloud-vue' +/** + * Subscribe a component to live updates for a static list of object types. + * + * @param {object} objectStore The app's createObjectStore-based store instance. + * @param {Array} types Object type slugs to subscribe to (static list). + * @return {void} + * + * @spec openspec/specs/realtime-updates-ui/spec.md + */ export function useLiveCollections(objectStore, types) { for (const type of types) { useObjectSubscription(objectStore, type, null, { diff --git a/src/dialogs/SuiteWizard/Step1Details.vue b/src/dialogs/SuiteWizard/Step1Details.vue index 822948d1..78fbef06 100644 --- a/src/dialogs/SuiteWizard/Step1Details.vue +++ b/src/dialogs/SuiteWizard/Step1Details.vue @@ -64,11 +64,21 @@ export default { * `onField` so `_step1Valid` recomputes on every keystroke. * * @return {string} The current long description. + * @spec openspec/specs/suite-wizard/spec.md#requirement-the-wizard-shall-guide-suite-creation-through-details-application-attachment-and-confirmation-steps */ beschrijvingLangModel: { + /** + * @return {string} The current long description. + * @spec openspec/specs/suite-wizard/spec.md#requirement-the-wizard-shall-guide-suite-creation-through-details-application-attachment-and-confirmation-steps + */ get() { return this.payload.beschrijvingLang || '' }, + /** + * @param {string} value The new long description. + * @return {void} + * @spec openspec/specs/suite-wizard/spec.md#requirement-the-wizard-shall-guide-suite-creation-through-details-application-attachment-and-confirmation-steps + */ set(value) { this.onField('beschrijvingLang', value) }, @@ -86,6 +96,7 @@ export default { * @param {string} key The stepData field to update. * @param {string} value The new value. * @return {void} + * @spec openspec/specs/suite-wizard/spec.md#requirement-the-wizard-must-require-at-least-one-attached-application-before-advancing-past-the-applications-step */ onField(key, value) { const merged = { ...this.payload, [key]: value } diff --git a/src/dialogs/SuiteWizard/Step2Applications.vue b/src/dialogs/SuiteWizard/Step2Applications.vue index 6bfa4535..f77bd5ad 100644 --- a/src/dialogs/SuiteWizard/Step2Applications.vue +++ b/src/dialogs/SuiteWizard/Step2Applications.vue @@ -73,6 +73,7 @@ export default { * Existing modules, mapped to `NcSelect` option shape. * * @return {Array<{uuid: string, label: string, raw: object}>} + * @spec openspec/specs/suite-wizard/spec.md#requirement-the-wizard-shall-guide-suite-creation-through-details-application-attachment-and-confirmation-steps */ applicationOptions() { // `getCollection()` returns the paginated ENVELOPE ({ results, ... }), @@ -89,6 +90,7 @@ export default { * so re-entering this step (e.g. via "Back") keeps the selection. * * @return {Array} + * @spec openspec/specs/suite-wizard/spec.md#requirement-the-wizard-must-require-at-least-one-attached-application-before-advancing-past-the-applications-step */ selected() { const applications = this.payload.applications || [] @@ -149,6 +151,7 @@ export default { * * @param {Array<{raw: object}>} options The selected NcSelect options. * @return {void} + * @spec openspec/specs/suite-wizard/spec.md#requirement-the-wizard-must-require-at-least-one-attached-application-before-advancing-past-the-applications-step */ onSelectionChange(options) { const applications = (options || []).map((option) => option.raw) diff --git a/src/dialogs/SuiteWizard/Step3Confirm.vue b/src/dialogs/SuiteWizard/Step3Confirm.vue index a8dfab9b..9a36a7e5 100644 --- a/src/dialogs/SuiteWizard/Step3Confirm.vue +++ b/src/dialogs/SuiteWizard/Step3Confirm.vue @@ -57,6 +57,7 @@ export default { * The attached applications' names, for the review list. * * @return {Array} + * @spec openspec/specs/suite-wizard/spec.md#requirement-submitting-the-wizard-shall-create-a-suite-object-with-the-attached-applications */ applicationNames() { return summarizeApplications(this.payload.applications) diff --git a/src/dialogs/SuiteWizardDialog.vue b/src/dialogs/SuiteWizardDialog.vue index c4debd4b..0489fb16 100644 --- a/src/dialogs/SuiteWizardDialog.vue +++ b/src/dialogs/SuiteWizardDialog.vue @@ -81,6 +81,7 @@ export default { * Seed values for the wizard's shared stepData. * * @return {object} + * @spec openspec/specs/suite-wizard/spec.md#requirement-the-wizard-shall-guide-suite-creation-through-details-application-attachment-and-confirmation-steps */ defaults() { return { @@ -98,6 +99,7 @@ export default { * (attach existing only), confirm. * * @return {Array<{id: string, label: string}>} + * @spec openspec/specs/suite-wizard/spec.md#requirement-the-wizard-shall-guide-suite-creation-through-details-application-attachment-and-confirmation-steps */ wizardSteps() { return [ @@ -191,6 +193,7 @@ export default { * reopens fresh. * * @return {void} + * @spec openspec/specs/suite-wizard/spec.md#requirement-the-wizard-shall-guide-suite-creation-through-details-application-attachment-and-confirmation-steps */ onClose() { this.$emit('update:show', false) diff --git a/src/modals/SaveFacetViewModal.vue b/src/modals/SaveFacetViewModal.vue index 4431e0db..3dd95216 100644 --- a/src/modals/SaveFacetViewModal.vue +++ b/src/modals/SaveFacetViewModal.vue @@ -80,6 +80,14 @@ export default { }, watch: { + /** + * Clear the name field each time the modal is (re)opened, so a + * previous attempt's text never leaks into the next save. + * + * @param {boolean} value Whether the modal became visible. + * @return {void} + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-a-facet-selection-can-be-saved-as-a-view + */ show(value) { if (value === true) { this.name = '' @@ -89,14 +97,20 @@ export default { methods: { /** + * Dismiss the modal without saving. + * * @return {void} + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-a-facet-selection-can-be-saved-as-a-view */ closeModal() { this.$emit('close') }, /** + * Emit the trimmed view name, refusing an empty one. + * * @return {void} + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-a-facet-selection-can-be-saved-as-a-view */ save() { const trimmed = this.name.trim() diff --git a/src/modals/SubmitReviewModal.vue b/src/modals/SubmitReviewModal.vue index fafac45a..3f3ac852 100644 --- a/src/modals/SubmitReviewModal.vue +++ b/src/modals/SubmitReviewModal.vue @@ -161,6 +161,7 @@ export default { * Close the modal and reset its state. * * @return {void} + * @spec openspec/specs/catalog-ratings/spec.md */ closeModal() { this.resetForm() @@ -171,6 +172,7 @@ export default { * Reset the form back to its initial state. * * @return {void} + * @spec openspec/specs/catalog-ratings/spec.md */ resetForm() { this.formData = { naam: '', beschrijvingLang: '' } diff --git a/src/modals/object/ViewObject.vue b/src/modals/object/ViewObject.vue index 72fc3d84..09b50c45 100644 --- a/src/modals/object/ViewObject.vue +++ b/src/modals/object/ViewObject.vue @@ -1478,6 +1478,11 @@ export default { * @spec openspec/specs/fe-object-modals/spec.md */ selectedProperty: { + /** + * @param {string|null} newKey The property now in edit mode, if any. + * @return {void} + * @spec openspec/specs/fe-object-modals/spec.md + */ handler(newKey) { this.destroyMarkdownEditors() diff --git a/src/services/facets.js b/src/services/facets.js index ef39220a..464d9017 100644 --- a/src/services/facets.js +++ b/src/services/facets.js @@ -31,6 +31,7 @@ export const FACET_DIMENSIONS = ['referentiecomponent', 'standaard', 'applicatie * @param {string} [options.search] Free-text query. * @param {string} [options.organization] Organisation override. * @return {URLSearchParams} The query parameters. + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-facet-aggregation-endpoint-returns-gemma-dimension-counts */ export function buildFacetQueryParams({ filters = {}, search = '', organization = '' } = {}) { const params = new URLSearchParams() diff --git a/src/store/modules/facets.js b/src/store/modules/facets.js index 98ab763a..1032fabf 100644 --- a/src/store/modules/facets.js +++ b/src/store/modules/facets.js @@ -200,6 +200,7 @@ export const useFacetStore = defineStore('facets', { * @param {string} dimension The facet dimension key. * @param {Array|string|null} values The new selection for that dimension. * @return {void} + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-filter-state-is-url-encoded-and-deep-linkable */ setFilter(schema, dimension, values) { if (!this[schema]) { @@ -224,6 +225,7 @@ export const useFacetStore = defineStore('facets', { * @param {string} schema `module` or `dienst`. * @param {string} value The search term. * @return {void} + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-facets-combine-with-free-text-search */ setSearch(schema, value) { if (!this[schema]) { diff --git a/src/store/modules/view.js b/src/store/modules/view.js index f4f1dbea..fa047485 100644 --- a/src/store/modules/view.js +++ b/src/store/modules/view.js @@ -65,6 +65,7 @@ export const useViewStore = defineStore('view', { * Fetch all views with current enrichment flags from the backend. * * @return {Promise} + * @spec openspec/specs/view-enrichment-api/spec.md#requirement-frontend-must-call-enrichment-api-for-views */ async fetchViews() { this.loading = true @@ -102,6 +103,7 @@ export const useViewStore = defineStore('view', { * * @param {string} viewId - The view identifier. * @return {Promise} + * @spec openspec/specs/view-enrichment-api/spec.md#requirement-frontend-must-call-enrichment-api-for-views */ async fetchView(viewId) { this.loading = true diff --git a/src/utils/complianceMatrix.js b/src/utils/complianceMatrix.js index 15d54328..dc508aff 100644 --- a/src/utils/complianceMatrix.js +++ b/src/utils/complianceMatrix.js @@ -133,6 +133,7 @@ export function hasEvidence(record) { * * @param {object} record A record (OR object or plain data). * @return {object} The property bag. + * @spec openspec/specs/module-compliance-assessment/spec.md */ export function dataOf(record) { if (!record || typeof record !== 'object') { diff --git a/src/utils/portfolioReport.js b/src/utils/portfolioReport.js index 40b29328..4ba0937a 100644 --- a/src/utils/portfolioReport.js +++ b/src/utils/portfolioReport.js @@ -42,6 +42,7 @@ export const QUADRANT_COLORS = { * * @param {string} key A QUADRANT_ORDER key. * @return {string} A CSS custom-property reference. + * @spec openspec/specs/portfolio-rationalization-time/spec.md#requirement-portfolio-rationalization-report-aggregates-per-organisation */ export function quadrantColor(key) { return QUADRANT_COLORS[key] || 'var(--color-primary-element)' @@ -53,6 +54,7 @@ export function quadrantColor(key) { * * @param {Record} cloudTransition Hosting model → count. * @return {string} Display label, or an em dash when empty. + * @spec openspec/specs/portfolio-rationalization-time/spec.md#requirement-portfolio-rationalization-report-aggregates-per-organisation */ export function cloudTransitionLabel(cloudTransition) { const entries = Object.entries(cloudTransition || {}) @@ -70,6 +72,7 @@ export function cloudTransitionLabel(cloudTransition) { * * @param {number} amount The amount. * @return {string} Currency label, or an em dash when zero/falsy. + * @spec openspec/specs/portfolio-rationalization-time/spec.md#requirement-portfolio-rationalization-report-aggregates-per-organisation */ export function formatCurrency(amount) { if (!amount) { @@ -89,6 +92,7 @@ export function formatCurrency(amount) { * * @param {Array<{quadrant: string}>} rows Report rows (each carries a `quadrant` key). * @return {Array<{key: string, rows: Array}>} Grouped rows, one entry per QUADRANT_ORDER key. + * @spec openspec/specs/portfolio-rationalization-time/spec.md#requirement-portfolio-rationalization-report-aggregates-per-organisation */ export function groupRowsByQuadrant(rows) { const list = Array.isArray(rows) ? rows : [] @@ -106,6 +110,7 @@ export function groupRowsByQuadrant(rows) { * @param {string} baseUrl The report endpoint's base URL (no query string). * @param {string} organisationUuid The selected organisation's uuid. * @return {string} The CSV export URL, or '' when `organisationUuid` is empty. + * @spec openspec/specs/portfolio-rationalization-time/spec.md#requirement-csv-export-of-the-portfolio-report */ export function buildCsvExportUrl(baseUrl, organisationUuid) { if (!organisationUuid) { diff --git a/src/utils/sbomVulnerabilityMatch.js b/src/utils/sbomVulnerabilityMatch.js index 79c2d959..2bdead54 100644 --- a/src/utils/sbomVulnerabilityMatch.js +++ b/src/utils/sbomVulnerabilityMatch.js @@ -60,6 +60,7 @@ function dataOf(record) { * * @param {string} purl A Package URL. * @return {string} The package name segment, lowercased. + * @spec openspec/specs/sbom-import/spec.md#requirement-components-are-matched-against-existing-kwetsbaarheden-without-external-calls */ export function purlPackageName(purl) { if (typeof purl !== 'string' || purl === '') { diff --git a/src/views/ComplianceMatrixView.vue b/src/views/ComplianceMatrixView.vue index adf4fe18..601ce3c6 100644 --- a/src/views/ComplianceMatrixView.vue +++ b/src/views/ComplianceMatrixView.vue @@ -343,9 +343,18 @@ export default { * @spec openspec/specs/bio-compliance-assessment/spec.md */ selectedColumns: { + /** + * @return {Array} Selected NcSelect options for the active scope. + * @spec openspec/specs/bio-compliance-assessment/spec.md + */ get() { return this.columnSource === COLUMN_SOURCE.BIO_MAATREGEL ? this.selectedBioMeasures : this.selectedStandards }, + /** + * @param {Array} value The new selection for the active scope. + * @return {void} + * @spec openspec/specs/bio-compliance-assessment/spec.md + */ set(value) { if (this.columnSource === COLUMN_SOURCE.BIO_MAATREGEL) { this.selectedBioMeasures = value diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue index 7338cdbe..dba03f92 100644 --- a/src/views/Dashboard.vue +++ b/src/views/Dashboard.vue @@ -393,6 +393,7 @@ export default { * * @param {string} slug - OR schema slug (e.g. 'organisatie', 'standaard'). * @return {void} + * @spec openspec/specs/fe-shell-navigation/spec.md */ navigateToSchema(slug) { const slugToSelected = { diff --git a/src/views/FacetedCatalogIndexView.vue b/src/views/FacetedCatalogIndexView.vue index 0f0777cf..139d31bf 100644 --- a/src/views/FacetedCatalogIndexView.vue +++ b/src/views/FacetedCatalogIndexView.vue @@ -223,6 +223,12 @@ export default { }, }, + /** + * Expose the facet Pinia store to the options API. + * + * @return {{facetStore: object}} The store bindings. + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-facet-sidebar-ui-on-the-module-and-dienst-index-pages + */ setup() { const facetStore = useFacetStore() return { facetStore } @@ -244,6 +250,7 @@ export default { * `options` whenever both are present. * * @return {Array} The filter definitions. + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-facet-sidebar-ui-on-the-module-and-dienst-index-pages */ facetDimensionFilters() { return Object.keys(DIMENSION_LABELS).map((key) => ({ @@ -254,7 +261,10 @@ export default { })) }, - /** @return {string} The current free-text search term for this schema. */ + /** + * @return {string} The current free-text search term for this schema. + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-facets-combine-with-free-text-search + */ searchValue() { return this.facetStore[this.schema].search }, @@ -266,6 +276,7 @@ export default { * filter or search term is active. * * @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 */ listFilter() { if (!this.facetStore.hasActiveFilterOrSearchFor(this.schema)) { @@ -287,6 +298,7 @@ export default { * `listFilter` — whenever the narrowing id set changes. * * @return {string} A key that changes exactly when `listFilter` does. + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-facet-sidebar-ui-on-the-module-and-dienst-index-pages */ indexPageKey() { return `${this.schema}-${JSON.stringify(this.listFilter)}` @@ -297,6 +309,12 @@ export default { this.facetStore.setFiltersFromQuery(this.schema, this.$route.query) }, + /** + * Fetch the initial facet counts and this schema's saved views. + * + * @return {void} + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-a-facet-selection-can-be-saved-as-a-view + */ mounted() { this.refetch() this.facetStore.fetchSavedViews(this.schema) @@ -312,6 +330,7 @@ export default { * already live in the store). * * @return {Promise} + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-facet-counts-reflect-the-currently-filtered-set-not-the-unfiltered-universe */ async refetch() { await this.facetStore.fetchFacets(this.schema) @@ -344,6 +363,7 @@ export default { * * @param {{key: string, values: Array}} payload The filter change. * @return {void} + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-filter-state-is-url-encoded-and-deep-linkable */ onFacetFilterChange({ key, values }) { this.facetStore.setFilter(this.schema, key, values) @@ -352,7 +372,10 @@ export default { }, /** + * Clear every active facet filter for this schema and refetch. + * * @return {void} + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-filter-state-is-url-encoded-and-deep-linkable */ onClearAllFacets() { this.facetStore.clearFilters(this.schema) @@ -365,6 +388,7 @@ export default { * * @param {string} value The new search term. * @return {void} + * @spec openspec/specs/gemma-faceted-search/spec.md#requirement-facets-combine-with-free-text-search */ onSearchInput(value) { this.facetStore.setSearch(this.schema, value) diff --git a/src/views/gemmaviews/GemmaViewIndex.vue b/src/views/gemmaviews/GemmaViewIndex.vue index 6c15fb8e..c96ad643 100644 --- a/src/views/gemmaviews/GemmaViewIndex.vue +++ b/src/views/gemmaviews/GemmaViewIndex.vue @@ -157,6 +157,7 @@ export default { * * @param {boolean} value - New toggle state. * @return {void} + * @spec openspec/specs/view-enrichment-api/spec.md#requirement-frontend-filter-toggles-must-map-to-backend-enrichment-parameters */ onProductsToggle(value) { this.viewStore.setIncludeProducts(value) @@ -168,6 +169,7 @@ export default { * * @param {object} view - View object from the API. * @return {boolean} True when at least one node has deelnamesGebruik. + * @spec openspec/specs/view-enrichment-api/spec.md#requirement-enrichment-api-must-return-standard-viewnode-format */ hasDeelnamesData(view) { const nodes = view.viewNodes ?? [] @@ -179,6 +181,7 @@ export default { * * @param {object} view - View object from the API. * @return {boolean} True when at least one node has products. + * @spec openspec/specs/view-enrichment-api/spec.md#requirement-enrichment-api-must-return-standard-viewnode-format */ hasProductsData(view) { const nodes = view.viewNodes ?? [] diff --git a/src/views/organisaties/PortfolioReport.vue b/src/views/organisaties/PortfolioReport.vue index d7705000..797aebb4 100644 --- a/src/views/organisaties/PortfolioReport.vue +++ b/src/views/organisaties/PortfolioReport.vue @@ -309,6 +309,7 @@ export default { /** * Quadrant chart x-axis categories (translated labels). * @return {Array} Categories. + * @spec openspec/specs/portfolio-rationalization-time/spec.md#requirement-portfolio-rationalization-report-aggregates-per-organisation */ chartCategories() { return this.quadrantSummary.map((row) => this.quadrantLabel(row.key)) @@ -319,6 +320,7 @@ export default { * CnChartWidget's `colorMap` contract, which is keyed by the resolved * category label, not the raw quadrant key). * @return {Record} Label → CSS colour. + * @spec openspec/specs/portfolio-rationalization-time/spec.md#requirement-portfolio-rationalization-report-aggregates-per-organisation */ chartColorMap() { const map = {} @@ -380,6 +382,7 @@ export default { * with no dependency on `organisatie_schema` ever being set. * * @return {Promise} + * @spec openspec/specs/portfolio-rationalization-time/spec.md#requirement-report-and-csv-export-are-scoped-to-the-requesters-authorised-organisations */ async loadOrganisations() { try { @@ -463,6 +466,7 @@ export default { * — matches `LifecycleRoadmapView.phaseLabel()`'s convention. * @param {string} key A QUADRANT_ORDER key. * @return {string} Display label. + * @spec openspec/specs/portfolio-rationalization-time/spec.md#requirement-portfolio-rationalization-report-aggregates-per-organisation */ quadrantLabel(key) { const map = { diff --git a/src/views/settings/sections/EolSyncSettings.vue b/src/views/settings/sections/EolSyncSettings.vue index 57f8efb3..5e8102f2 100644 --- a/src/views/settings/sections/EolSyncSettings.vue +++ b/src/views/settings/sections/EolSyncSettings.vue @@ -180,9 +180,18 @@ export default defineComponent({ * @spec openspec/specs/eol-feed-integration/spec.md#requirement-eol-sync-runs-on-a-schedule-with-a-manual-trigger */ intervalMinutesInput: { + /** + * @return {string} The interval in minutes, as a string for NcTextField. + * @spec openspec/specs/eol-feed-integration/spec.md#requirement-eol-sync-runs-on-a-schedule-with-a-manual-trigger + */ get() { return String(Math.max(1, Math.round((this.config.intervalSeconds || 86400) / 60))) }, + /** + * @param {string} value The new interval in minutes. + * @return {void} + * @spec openspec/specs/eol-feed-integration/spec.md#requirement-eol-sync-runs-on-a-schedule-with-a-manual-trigger + */ set(value) { const minutes = parseInt(value, 10) if (Number.isFinite(minutes) && minutes > 0) { @@ -196,6 +205,7 @@ export default defineComponent({ * feature has never run. * * @return {string} The formatted timestamp. + * @spec openspec/specs/eol-feed-integration/spec.md#requirement-eol-sync-runs-on-a-schedule-with-a-manual-trigger */ formattedLastRunAt() { if (!this.status.lastRunAt) { diff --git a/src/views/suites/SuitesIndexView.vue b/src/views/suites/SuitesIndexView.vue index fd62af68..ffbd6b3c 100644 --- a/src/views/suites/SuitesIndexView.vue +++ b/src/views/suites/SuitesIndexView.vue @@ -74,6 +74,7 @@ export default { * * @param {object} row The clicked suite row. * @return {void} + * @spec openspec/specs/suite-wizard/spec.md#requirement-the-suite-index-page-shall-list-existing-suites */ onRowOpen(row) { const id = row && (row.id || row.uuid || row['@self']?.id) @@ -87,6 +88,7 @@ export default { * * @param {string} suiteId The newly-created suite's id. * @return {void} + * @spec openspec/specs/suite-wizard/spec.md#requirement-submitting-the-wizard-shall-create-a-suite-object-with-the-attached-applications */ onSuiteCreated(suiteId) { this.showWizard = false