Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions openspec/specs/realtime-updates-ui/spec.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions src/components/cards/OrganisatieCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/components/reviews/ReviewsPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ export default {
* Open the submit-review modal.
*
* @return {void}
* @spec openspec/specs/catalog-ratings/spec.md
*/
openSubmitModal() {
this.showSubmitModal = true
Expand Down
9 changes: 9 additions & 0 deletions src/components/sbom/SbomComponentsPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -243,6 +244,7 @@ export default {
* All kwetsbaarheid records — the vulnerability-match candidate set.
*
* @return {Array<object>} 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 || []
Expand All @@ -263,6 +265,7 @@ export default {
* Display rows for CnDataTable: name/version/purl/licenses plus match badge counts.
*
* @return {Array<object>} 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 }) => {
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -378,6 +384,7 @@ export default {
*
* @param {string} type Object type slug.
* @return {Promise<void>} 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'
Expand All @@ -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]
Expand All @@ -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) {
Expand Down
9 changes: 9 additions & 0 deletions src/composables/useLiveCollections.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>} 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, {
Expand Down
11 changes: 11 additions & 0 deletions src/dialogs/SuiteWizard/Step1Details.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
Expand All @@ -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 }
Expand Down
3 changes: 3 additions & 0 deletions src/dialogs/SuiteWizard/Step2Applications.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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, ... }),
Expand All @@ -89,6 +90,7 @@ export default {
* so re-entering this step (e.g. via "Back") keeps the selection.
*
* @return {Array<object>}
* @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 || []
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/dialogs/SuiteWizard/Step3Confirm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export default {
* The attached applications' names, for the review list.
*
* @return {Array<string>}
* @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)
Expand Down
3 changes: 3 additions & 0 deletions src/dialogs/SuiteWizardDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 [
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions src/modals/SaveFacetViewModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ''
Expand All @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions src/modals/SubmitReviewModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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: '' }
Expand Down
5 changes: 5 additions & 0 deletions src/modals/object/ViewObject.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
1 change: 1 addition & 0 deletions src/services/facets.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions src/store/modules/facets.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand All @@ -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]) {
Expand Down
2 changes: 2 additions & 0 deletions src/store/modules/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const useViewStore = defineStore('view', {
* Fetch all views with current enrichment flags from the backend.
*
* @return {Promise<void>}
* @spec openspec/specs/view-enrichment-api/spec.md#requirement-frontend-must-call-enrichment-api-for-views
*/
async fetchViews() {
this.loading = true
Expand Down Expand Up @@ -102,6 +103,7 @@ export const useViewStore = defineStore('view', {
*
* @param {string} viewId - The view identifier.
* @return {Promise<void>}
* @spec openspec/specs/view-enrichment-api/spec.md#requirement-frontend-must-call-enrichment-api-for-views
*/
async fetchView(viewId) {
this.loading = true
Expand Down
1 change: 1 addition & 0 deletions src/utils/complianceMatrix.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
5 changes: 5 additions & 0 deletions src/utils/portfolioReport.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)'
Expand All @@ -53,6 +54,7 @@ export function quadrantColor(key) {
*
* @param {Record<string, number>} 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 || {})
Expand All @@ -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) {
Expand All @@ -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 : []
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/utils/sbomVulnerabilityMatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 === '') {
Expand Down
Loading
Loading