Skip to content

Commit 8e88d71

Browse files
committed
chore(openspec): archive suite-wizard
proposal, design, delta spec (7 requirements) and tasks for the suite wizard, archived after implementation and verification; delta applied onto openspec/specs/suite-wizard/spec.md (new capability, 7 requirements added).
1 parent 4efddc3 commit 8e88d71

8 files changed

Lines changed: 481 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: conduction
2+
created: 2026-07-24
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Context Brief: suite-wizard
2+
3+
## What
4+
A guided wizard to register an application **suite** (e.g. "Centric Leefomgeving") and its member applications in one pass, plus the suite index/detail surfaces to view them. Closes softwarecatalog#372.
5+
6+
## Why (evidence)
7+
- VNG Softwarecatalogus issue **#242** — the retired "product" concept should be replaced by a suite wizard; the datamodel object already exists in the incumbent.
8+
- Corroborated by a broader UX theme: **21 wizard-labelled VNG issues** (guided flows are one of the top user-wish clusters).
9+
10+
## Current state — the schema ALREADY EXISTS and is unused
11+
`suite` is defined in `lib/Settings/softwarecatalogus_register.json` with:
12+
`naam`, `beschrijvingKort`, `beschrijvingLang`, `logo`, `website`, `contactpersoon`, `applicaties` (related-object array).
13+
`suite_schema` is already wired in `voorzieningen_config`. There is **no** suite Vue view, no wizard, no controller — this is purely additive UI over an existing schema. Verify the exact property shapes yourself before building.
14+
15+
## Scope
16+
IN:
17+
- A multi-step wizard (create suite → attach member applications → confirm) reachable from a nav entry and/or the Applications page.
18+
- Suite index page + suite detail page (members listed, links through to each module).
19+
- Attaching **existing** modules to a suite, and showing suite membership on the module detail page.
20+
- i18n (EN keys + nl + en_US), unit tests, docs page.
21+
22+
OUT: creating brand-new modules from inside the wizard (attach existing only — keep the change small); suite-level contracts/licensing; migrating the legacy "product" concept.
23+
24+
## Design constraints
25+
- **Register changes go in a NEW `lib/Settings/register.d/suite-wizard.json` FRAGMENT — never edit the monolith.** Per ADR-037 (`lib/Settings/register.d/README.md`) each change ships its own fragment. This is not just convention: the import version is computed from `info.version` + a hash of the `register.d/*.json` fragments, so **a monolith edit is a silent no-op on every installed instance** (see softwarecatalog#391). If `suite` needs no schema change at all, add no fragment.
26+
- ADR-001: all data via OpenRegister, no custom tables. ADR-008 Controller→Service if any backend is needed (prefer none — the frontend can use the object store directly).
27+
- ADR-012: use `@conduction/nextcloud-vue` components (CnIndexPage / CnFormDialog / modal-in-its-own-file); do NOT hand-roll a stepper if the library offers one. `NcSelect` needs an `inputLabel` prop (a11y gate). Modals/dialogs MUST live in their own file under `src/modals/` or `src/dialogs/`.
28+
- 🔑 When registering an object type in the store, register by **schema SLUG** against `voorzieningenConfig.register` — the pattern `useSelfFetchList.js` uses. Do NOT resolve via `voorzieningen_config.<x>_schema`; several of those keys are never populated (that exact mistake made the portfolio-report org picker dead — sc#392).
29+
- Spec deltas: `### Requirement: <name>` headers; MUST/SHALL on the requirement's FIRST physical line; no angle brackets in requirement bodies; `#### Scenario:` GIVEN/WHEN/THEN per MUST/SHALL.
30+
- `@spec` anchors → canonical `openspec/specs/<capability>/spec.md#requirement-<kebab>`, NEVER a change dir (archive moves it).
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Design: suite-wizard
2+
3+
## Context
4+
`suite` is a fully-specified OpenRegister schema (`lib/Settings/softwarecatalogus_register.json`) with `naam`, `beschrijvingKort` (both required), `beschrijvingLang`, `logo`, `website`, `contactpersoon` (single related-object → `contactpersoon`) and `applicaties` (array of related-object → `module`). It has zero Vue surface. The change is purely additive frontend UI over this existing schema; ADR-001/ADR-008 push implementation toward "frontend talks to OpenRegister directly", and grounding confirmed no backend controller is needed.
5+
6+
## Goals / Non-Goals
7+
**Goals:** a guided 3-step wizard to create a suite and attach existing modules; a suite index + detail page; suite membership visible from a module's own detail page.
8+
**Non-Goals:** creating new modules from the wizard; suite-level contracts/licensing; migrating the legacy "product" concept; any PHP controller/service (no backend code is added).
9+
10+
## Decisions
11+
12+
### Decision 1: No register fragment
13+
The `suite` schema already carries every field the wizard needs (verified by reading `lib/Settings/softwarecatalogus_register.json` directly — `naam`, `beschrijvingKort`, `beschrijvingLang`, `logo`, `website`, `contactpersoon`, `applicaties`). Per ADR-037, a fragment is only added when the schema itself changes. Since it does not, `lib/Settings/register.d/` gets no new file for this change.
14+
15+
### Decision 2: `CnWizardDialog`, not a hand-rolled stepper
16+
`@conduction/nextcloud-vue` ships `CnWizardDialog` (multi-step modal: progress indicator, per-step slot, `validate` hook, `setResult`/`setError` result phase) — exactly the shape the brief asks for, and already used in this exact way by `openbuild/src/dialogs/CreateApplicationWizard.vue`. Building a custom stepper would violate ADR-012 (use the library's components; don't hand-roll what it already offers). The wizard lives at `src/dialogs/SuiteWizardDialog.vue` (it wraps `CnWizardDialog`, which wraps `NcDialog` — a dialog, not an `NcModal`, so per the modal-isolation rule it belongs under `src/dialogs/`, matching the OpenBuild precedent) with one sub-component per step under `src/dialogs/SuiteWizard/`.
17+
18+
### Decision 3: Register `suite` and `module` object types by schema slug, not via `voorzieningen_config.<x>_schema`
19+
Mirrors `PortfolioReport.vue`'s documented fix for the sc#392 dead-picker bug: `<type>_schema` keys in the settings config blob are only populated for a handful of types (module/compliancy/moduleVersie/sbomComponent) and `suite` is not one of them. The wizard calls `objectStore.registerObjectType(slug, slug, voorzieningenConfig.register, { registerSlug: 'voorzieningen', schemaSlug: slug })` for both `suite` and `module` before it fetches or saves — OpenRegister's `/api/objects/{register}/{schemaSlugOrId}` accepts a schema slug interchangeably with a numeric id, so this works with no dependency on the config blob ever holding a `suite_schema`/`module_schema` key. The Suites index page itself needs no such call: passing `register="voorzieningen"` `schema="suite"` straight to `CnIndexPage` triggers the library's own self-fetch path (`CnIndexPage/useSelfFetchList.js`), which performs the identical registration internally.
20+
21+
### Decision 4: The wizard writes plain UUID references, matching the `related-object` convention
22+
`CnFormDialog`'s reference-field handling (`onReferenceSelected`) stores a related-object value as the referenced object's id string, not a nested object (`formData[field.key] = String(value)`); the same schema declares `objectConfiguration.handling: "related-object"` identically for both single-object fields (e.g. `module.aanbieder`) and array-of-object fields (e.g. `suite.applicaties`) — there is no separate embedding convention for arrays. The wizard therefore submits `applicaties` as a plain array of module UUID strings.
23+
24+
### Decision 5: No ModuleDetail manifest change is needed for "suite membership on the module detail page"
25+
`ModuleDetail` already carries a `type: "related"` widget (`md-related`). `CnRelatedObjectsWidget`'s self-fetch mode calls OpenRegister's `/uses` + `/used` sub-resources and merges them into one "Objects" tab. `ObjectsController::used()` (openregister `lib/Controller/ObjectsController.php`) is a **generic, backend-tracked, bidirectional** relation index ("B → A means B references A") — it is not scoped to fields a specific app declared or expects; any object that stores another object's id anywhere in a `related-object`-handled property is discoverable from the referenced object's `/used` response. Once a suite's `applicaties` array contains a module's id, that module's existing `md-related` widget surfaces the suite with zero widget-config changes, exactly as `KwetsbaarheidDetail`'s reverse-linked "Affected applications" panel already relies on the same mechanism for `kwetsbaarheid.modules`. Adding a bespoke widget would duplicate functionality the platform already provides and risk drifting from it.
26+
27+
### Decision 6: Suite detail page mirrors the `ContactpersoonDetail`/`ModuleDetail` archetype
28+
`SuiteDetail` gets a `data` widget (8-wide) with the suite's own scalar/reference fields (`naam`, `beschrijvingKort`, `beschrijvingLang`, `website`, `logo`, `contactpersoon`) and a `related` widget (4-wide) that surfaces both the forward `contactpersoon` reference and the forward `applicaties` array (via the same `/uses`+`/used` mechanism as Decision 5) with click-through navigation to each module's `ModuleDetail` page. A suite does not communicate, so per the comms hard-rule no Emails/Meetings widgets are placed. Audit trail stays a sidebar tab.
29+
30+
### Decision 7: Wizard requires at least one attached application to advance past step 2
31+
The suite schema itself does not mark `applicaties` as required, but a suite with zero members defeats the feature's purpose (grouping applications). The wizard's `validate` step-hook blocks advancing from the `applications` step until at least one module is selected, surfaced as a `CnWizardDialog` validation-error banner — this keeps the requirement enforced in the UI without touching the schema's own `required` array (which stays scoped to what OpenRegister itself must reject).
32+
33+
### Decision 8: Pure logic extracted to `src/utils/suiteWizard.js`
34+
Payload construction (`buildSuitePayload`) and step-validity checks (`isDetailsStepValid`, `isApplicationsStepValid`) are pure functions in their own module, unit-tested directly — mirroring the existing `src/utils/translationBadge.js` + co-located `.spec.js` convention (Jest) already used throughout this app for exactly this "extract the pure logic, test it directly, keep the `.vue` thin" shape. `npx vitest run` re-runs the existing `tests/vitest/**` suite as a regression check.
35+
36+
## Risks / Trade-offs
37+
- [Risk] Decision 5 (no ModuleDetail widget change) relies on OpenRegister's generic relation index actually covering array-of-object `related-object` fields identically to scalar ones. → Mitigation: confirmed by reading `ObjectsController::used()`/`getObjectUsedBy()` directly (generic, not field-scoped) and by the precedent of `KwetsbaarheidDetail`'s existing reverse-linked panel over an array field (`kwetsbaarheid.modules`) using the same widget type.
38+
- [Risk] Requiring ≥1 application at wizard-submit time (Decision 7) is stricter than the schema. → Mitigation: this is a UI-only gate on the wizard's own guided flow; a suite can still be edited afterwards (via the generic edit form) to have zero or additional applications — the schema's own validation is untouched.
39+
40+
## Migration Plan
41+
Not applicable — no schema, data, or backend change. New Vue files + manifest additions are additive; nothing is removed or renamed. Rollback is a plain code revert.
42+
43+
## Open Questions
44+
None.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Proposal: suite-wizard
2+
3+
## Summary
4+
Add a guided wizard to register an application **suite** (e.g. "Centric Leefomgeving") together with its existing member applications in one pass, plus a suite index page and suite detail page so the resulting suites and their membership are browsable, and surface suite membership on the module (application) detail page. Closes softwarecatalog#372.
5+
6+
## Motivation
7+
VNG Softwarecatalogus issue #242 flags that the retired "product" concept in the incumbent system should be replaced by a suite grouping. The `suite` schema already exists in `lib/Settings/softwarecatalogus_register.json` (naam, beschrijvingKort, beschrijvingLang, logo, website, contactpersoon, applicaties) but has no Vue surface at all — no index, no detail, no way to create one. Municipalities buying a bundled product (e.g. a "leefomgeving" suite covering several separately-licensed applications) currently have no way to record that grouping, and the broader UX research (21 wizard-labelled VNG issues) shows guided flows are one of the top user-wish clusters. This change ships the missing UI over the existing schema.
8+
9+
## Affected Projects
10+
- [x] Project: `softwarecatalog` — new suite wizard, suite index/detail pages, module-detail suite membership widget, nav entry
11+
12+
## Scope
13+
14+
### In Scope
15+
- A multi-step wizard (create suite → attach existing member applications → confirm) reachable from a nav entry.
16+
- Suite index page (`type: index`) listing existing suites.
17+
- Suite detail page (`type: detail`) showing suite data and its member applications, linking through to each module's detail page.
18+
- Attaching **existing** `module` objects to a suite's `applicaties` array field.
19+
- Showing suite membership (which suite(s) a module belongs to) on the `ModuleDetail` page.
20+
- i18n: English keys plus `l10n/nl.js`/`.json` and `l10n/en_US.js`/`.json` translations.
21+
- Unit tests for new wizard/store logic.
22+
- A docs page for the new feature.
23+
24+
### Out of Scope
25+
- Creating brand-new modules from inside the wizard — the wizard only attaches modules that already exist in the catalogue.
26+
- Suite-level contracts or licensing.
27+
- Migrating or importing the legacy "product" concept from the incumbent system.
28+
29+
## Approach
30+
Reuse the existing `suite` OpenRegister schema unchanged (no fragment needed — verified during grounding that `naam`, `beschrijvingKort`, `beschrijvingLang`, `logo`, `website`, `contactpersoon` and `applicaties` already cover the wizard's needs). Add manifest-driven `Suites` (index) and `SuiteDetail` (detail) pages following the existing `Organisaties`/`OrganisatieDetail` and `Contactpersonen`/`ContactpersoonDetail` conventions. Build the wizard as a dedicated multi-step modal component under `src/modals/`, registering the `suite` and `module` object types by schema slug against the voorzieningen register id (the `useSelfFetchList.js` pattern already proven in `PortfolioReport.vue`), never via the unreliable `voorzieningen_config.<x>_schema` keys. Add a small `object-list`/`related`-style widget to `ModuleDetail` surfacing the suite(s) that reference this module in their `applicaties` array.
31+
32+
## New Dependencies
33+
None.
34+
35+
## Impact
36+
- `src/manifest.json`: new nav entry, new `Suites`/`SuiteDetail` pages, extended `ModuleDetail` page config.
37+
- `src/modals/`: new `SuiteWizardModal.vue` (or step sub-components) orchestrating the three-step flow.
38+
- `src/customComponents.js`: registered only if a custom component is required beyond manifest-driven pages/widgets.
39+
- `l10n/`: new EN/NL translation keys.
40+
- `docs/features/`: new suite-wizard feature doc with screenshots.
41+
42+
## Cross-Project Dependencies
43+
None — this is a self-contained softwarecatalog frontend change over an existing OpenRegister schema; no backend controller or other app is touched.
44+
45+
## Risks
46+
47+
### Risk 1: Editing the register monolith by mistake
48+
**Severity:** Medium — **Mitigation:** No schema change is planned (the `suite` schema already has everything needed); if a later iteration needs a schema tweak, it MUST land in a new `lib/Settings/register.d/suite-wizard.json` fragment per ADR-037, never in `softwarecatalogus_register.json` directly, since a monolith edit is a silent no-op on installed instances (softwarecatalog#391).
49+
50+
### Risk 2: Resolving the module/suite object type via the wrong config key
51+
**Severity:** Low — **Mitigation:** Register object types by schema slug against `voorzieningenConfig.register`, mirroring the proven `PortfolioReport.vue` pattern, instead of `voorzieningen_config.<x>_schema` (several of those keys are never populated — the exact mistake that shipped a dead org picker, sc#392).
52+
53+
## Rollback Strategy
54+
Revert the commit(s) on `wip/suite-wizard`. No schema/data migration is introduced (the `suite` schema and any existing `suite` objects are untouched), so rollback is a pure code revert with no data cleanup required.
55+
56+
## Open Questions
57+
None.

0 commit comments

Comments
 (0)