Skip to content

fix(setup): make the setup wizard, the sample-data step and the case-type menu reachable - #1401

Merged
rubenvdlinde merged 14 commits into
developmentfrom
fix/casetype-index-actions-and-setup-wizard
Aug 28, 2026
Merged

fix(setup): make the setup wizard, the sample-data step and the case-type menu reachable#1401
rubenvdlinde merged 14 commits into
developmentfrom
fix/casetype-index-actions-and-setup-wizard

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Companion to ConductionNL/nextcloud-vue#806. Four things this app declared, wired and shipped, which no user could reach — all found on a clean install of localhost:8080/apps/dossiq.

1. The row-action menu had 15 blank rows

the ... menu with blank space above View/Edit/Copy/Delete

config.actions carried entries CnRowActions cannot draw — a bare string ("edit") and a key-only object ({"key": "edit"}) — which render as full-height, clickable, inert items above the real ones. Six pages: CaseTypes, Partners, WorkflowDefinitions, LhsMatrices, Parafeerroutes, WmsLayers.

Neither shape is how built-ins are enabled (the show*Action toggles do that, and they default on), so all 15 were pure loss — the author got a blank row instead of the action they asked for, and the built-in they meant was already there.

Also drops the six hand-rolled view actions and their showViewAction: false. Each named a detail route the built-in View now resolves to by itself — checked route by route, all six resolve to exactly the page they used to name.

2. The case-type index opened an edit modal

…over a record that has its own detail page. That modal renders caseType's flat scalars only, so a case type's statuses, results, roles and properties — separate related records — were uneditable from the surface claiming to edit them. With the library change, Edit now goes to CaseTypeDetail, which grew its own Edit button.

3. The sample-data step was unreachable

/api/setup/status answers completed: true on a clean install (the required register step auto-satisfies on app-enable) with seed: {done: false}. CnAppRoot read completed as "nothing left to do" and never opened the wizard offering the seed step — the app's only demo-data affordance. Fixed in the library.

This side adds the first tests SetupController has ever had (12), including one that reads the shipped manifest and fails if a declared step goes unreported — an unreported step is invisible to the wizard however unfinished it is.

4. A seed run that created nothing recorded itself as done

seedBezwaarBeroepData() returns success with every counter at zero when its payload is absent — which is its state, the case types having been parked under _caseTypes_disabled in favour of a register.d fragment. So one click reported "Seeded 0 case types, 0 status types, 0 role types (0 skipped)" as a success, marked the step complete, and the wizard never offered it again. It now answers 422 and leaves the step outstanding.

Also

The dwangsom callback signing secret is now asked for. status() computed dwangsom_callback_secret_configured for the enforce-dwangsom-callback-signature spec and nothing in the frontend read it — serialised and dropped on every request, so the incident it exists to prevent was never being prevented. It is now a reported optional setup step; the legacy flag stays for API consumers and is derived from the same value so the two cannot drift.

The walkthrough shows where case types and flows live. Two view-only stops, both allowManualNext with a route-match advance — they point at the surfaces without requiring anyone to create a case type or a flow. manifest.version 1.1.0 → 1.2.0 so returning users get them (sinceVersion is compared against it).

The e2e nav helper now dismisses the setup wizard. It only started appearing once the library learned to tell "reported not done" from "never mentioned", so no spec in this suite had ever had to account for a modal-mask that swallows every click on the app behind it.

Known gap — filed separately

A fresh install still has zero demo cases. dossiq declares 191 seed objects in its register config and 35 of them — 9 schemas, including all 10 demo case objects — are never imported, with no error and a success result. That is an OpenRegister import defect: ConductionNL/openregister#2935.

Also proposed (docs only)

openspec/changes/case-type-one-authoring-surface/ records two measured findings behind the modal complaint:

  • The app has two surfaces claiming to author a case type. The real one is Admin Settings (AdminRoot → CaseTypeAdmin → CaseTypeList/CaseTypeDetail, eight tabs). The in-app pages are a flat duplicate — a consequence of page-topology-cleanup B1 correctly retiring the in-app AdminRoot route per ADR-004, with the replacement rebuilding only the flat half.
  • Nothing can author a CMMN case plan. handlingModel accepts cmmn, CaseModelEngine/CaseModelLoader implement it, the runtime routes exist, the caseModel schema is tagged x-cmmn-equivalent: CasePlanModel — but zero files under src/ mention caseModel, no admin tab covers it, no manifest page binds to it, and there is no CRUD route. getActiveModel() returns null when nothing is published, so choosing cmmn today yields case types the engine has nothing to run.

It includes the CMMN ↔ ZGW ZTC mapping table, on the framing that CMMN is the core runtime model and ZGW the publication projection.

Verification

  • 12 SetupController tests — 5 of them fail against the pre-fix source, so they are evidence rather than decoration.
  • 2445 unit tests green (control run on the pre-change tree: 2436, same 1 warning, same 5 skips — no regression).
  • lint, phpcs (0 errors, warning count identical to the control), phpmd, psalm and phpstan all clean.
  • Playwright e2e covering all four user-visible fixes, run against a live instance built on nextcloud-vue#806.

Depends on

nextcloud-vue#806 (merged to development) and its beta release. Items 1, 3, 4 and the walkthrough work on the currently-pinned 2.19.0; item 2's edit-routing needs the new library, so this merges together with the version bump.

…type menu reachable

Four things this app declared, wired and shipped, which no user could reach.

1. THE ROW-ACTION MENU HAD 15 BLANK ROWS. `config.actions` carried entries
   that CnRowActions cannot draw — a bare string ("edit") and a key-only
   object ({"key": "edit"}) — which render as full-height, clickable, INERT
   items above the real ones. Six pages were affected: CaseTypes, Partners,
   WorkflowDefinitions, LhsMatrices, Parafeerroutes and WmsLayers. Neither
   shape is how built-ins are enabled (the show*Action toggles do that, and
   they default on), so all 15 were pure loss.

   Also drops the six hand-rolled `view` actions and their
   `showViewAction: false`. Each named a detail route that the built-in View
   now resolves to by itself — verified route-by-route: all six resolve to
   exactly the page they used to name.

2. THE CASE-TYPE INDEX OPENED AN EDIT MODAL over a record that has its own
   detail page. That modal renders the schema's flat scalars only, so a case
   type's statuses, results, roles and properties — separate related records
   — were uneditable from the surface claiming to edit them. With
   @conduction/nextcloud-vue's matching change, Edit now goes to
   CaseTypeDetail, which grew its own Edit button.

3. THE SAMPLE-DATA STEP WAS UNREACHABLE. `/api/setup/status` answers
   `completed: true` on a clean install (the required register step
   auto-satisfies on app-enable) with `seed: {done: false}`. CnAppRoot read
   `completed` as "nothing left to do" and never opened the wizard offering
   the seed step — the app's only demo-data affordance. Fixed in the library;
   the app side adds the first tests SetupController has ever had, including
   one that reads the shipped manifest and fails if a declared step goes
   unreported (an unreported step is invisible to the wizard however
   unfinished it is).

4. A SEED RUN THAT CREATED NOTHING RECORDED ITSELF AS DONE.
   `seedBezwaarBeroepData()` returns success with every counter at zero when
   its payload is absent — which is its state, the case types having been
   parked under `_caseTypes_disabled` in favour of a register.d fragment. So
   one click reported "Seeded 0 case types, 0 status types, 0 role types (0
   skipped)" as a success, marked the step complete, and the wizard never
   offered it again. It now answers 422 and leaves the step outstanding.

Also:

- SURFACES THE DWANGSOM CALLBACK SIGNING SECRET. `status()` computed
  `dwangsom_callback_secret_configured` for the
  enforce-dwangsom-callback-signature spec and NOTHING in the frontend read
  it — it was serialised and dropped on every request, so the incident it
  exists to prevent was never being prevented. It is now also a reported
  optional setup step, so the wizard asks for it; the legacy flag is kept for
  API consumers and derived from the same value so the two cannot drift.

- THE WALKTHROUGH NOW SHOWS WHERE CASE TYPES AND FLOWS LIVE. Two view-only
  stops, both `allowManualNext` with a route-match advance — they point at
  the surfaces without requiring anyone to create a case type or a flow.
  manifest.version 1.1.0 -> 1.2.0 so returning users get the new steps
  (`sinceVersion` is compared against it).

- THE E2E NAV HELPER NOW DISMISSES THE SETUP WIZARD. It only started
  appearing once the library learned to tell "reported not done" from "never
  mentioned", so no spec in this suite had ever had to account for a
  modal-mask that swallows every click on the app behind it.

KNOWN GAP, filed as ConductionNL/openregister#2935: a fresh install still has
zero demo CASES. dossiq declares 191 seed objects in its register config and
35 of them — 9 schemas, including all 10 demo cases — are never imported, with
no error and a success result. That is an OpenRegister import defect, measured
and reported separately.

Verified: 12 SetupController tests (5 fail against the pre-fix source);
2445 unit tests green; phpcs 0 errors (unchanged from the control), phpmd,
psalm and phpstan clean; Playwright e2e covers all four user-visible fixes.
…script tag

Two assertions that could not have passed, and whose failure looked like
missing code rather than a broken instrument.

- `meta[name=requesttoken]` does not exist on NC 34, so the CSRF header went
  out empty and `/api/setup/status` answered 412 {"message":"CSRF check
  failed"} — a refusal a caller checking only for a 2xx reads as data. The
  token is on `OC.requestToken`, with head[data-requesttoken] as fallback.

- `/apps/dossiq/js/dossiq-main.js` answers 200 with the app's HTML shell, not
  the bundle (the real path is /custom_apps/...). Resolving it from the page's
  own <script src> instead of hardcoding it means the test reads the artefact
  the browser actually loaded.
…way to author a CMMN plan

Two findings, both measured rather than assumed.

1. THE APP HAS TWO SURFACES THAT CLAIM TO AUTHOR A CASE TYPE. The real one is
   in Admin Settings — AdminRoot -> CaseTypeAdmin -> CaseTypeList/CaseTypeDetail
   with eight tabs (Statuses, Results, Roles, Properties, Document types,
   Decision types, Sub-case types, Workflow). The in-app /settings/case-types
   pages are plain schema-driven pages over caseType's 32 scalar columns, with
   no composition at all — and until the accompanying fix, the index's row menu
   opened an edit MODAL over those scalars.

   The manifest's own note records how: page-topology-cleanup B1 correctly
   retired the in-app route that mounted AdminRoot through the in-app router
   (ADR-004), and the replacement rebuilt only the flat half.

2. NOTHING CAN AUTHOR A CMMN CASE PLAN. handlingModel accepts 'cmmn';
   CaseModelEngine and CaseModelLoader implement it; the runtime routes exist;
   the caseModel schema is declared and tagged x-cmmn-equivalent:
   CasePlanModel. But zero files under src/ mention caseModel, the eight admin
   tabs do not include it, no manifest page binds to it, and routes.php has no
   CRUD for it. CaseModelLoader::getActiveModel() returns null when nothing is
   published, so choosing 'cmmn' today yields case types the engine has nothing
   to run — silently, null being a normal return.

Includes the CMMN <-> ZGW ZTC mapping the fleet needs to keep straight: CMMN is
the richer runtime model, ZGW the publication contract, so CMMN is the core and
ZGW the projection — and a stage/milestone must be able to name the statusType
it maps to, or the export gets maintained beside the model instead of derived
from it.
…tops added

The l10n gate is doing exactly its job here: every string the wizard and the
walkthrough put on screen is user-facing, and nine of them existed only as
English literals in src/manifest.json.

Dutch is written rather than copied — this app's audience reads it, and
'Zaaktypen' is the term the rest of nl.json already uses for case types
(Flows stays 'Flows', which is also what the existing catalogue does).

Also runs prettier over the new e2e spec, which the format gate caught.
…ugh already is

Making the wizard OPEN is the library fix; it also means its modal-mask
intercepts every click on the app behind it. Before that fix CnAppRoot could
not open the non-gating wizard at all, so no spec in this suite had ever had to
account for it — and navigation.spec.ts clicks sidebar links without dismissing
any overlay:

    <div ... data-testid-modal="cn-wizard-dialog"> subtree intercepts pointer events

Seeding the dismissal marker into the persisted storageState fixes it for the
whole suite in one place, exactly as global-setup already does for the
walkthrough's cn-walkthrough-seen key. A range of setup.version values is
seeded rather than the current one, so a later bump does not silently re-arm
it. Dismissing it in the nav helper (the previous attempt) only covered specs
that route through navTo.

The spec that TESTS the wizard now clears those keys and reloads. Without that
it would assert against a wizard suppressed before it could open — a test that
cannot fail.
… translated label

Two things the full suite surfaced.

1. l10n/*.json is the SOURCE; l10n/*.js is what the browser loads. Adding the
   nine strings to the JSON left en.js and nl.js stale, so they existed in the
   repo and reached no user — the same shape as the 2026-08-24 finding where
   nine apps shipped 8,137 translations no browser ever received.
   `check:l10n-js` catches it; this is `npm run l10n:build`.

2. The spec navigated with navTo(page, 'Case types'), which resolves a nav
   entry by its exact VISIBLE TEXT. On a Dutch-locale instance the sidebar
   reads 'Zaaktypen' and the helper fails by name — as pages.spec.ts did in
   this same run. A spec about case types should not also be asserting which
   language the instance happens to run in, so it resolves the entry by its
   /settings/case-types href instead, and fails by name if no link routes
   there at all.
…lves

CnWalkthrough.resolveTarget() looks a nav-item target up as
`[data-cn-route="<ref>"]`, and CnAppNav sets that attribute from `item.route`.
The step was authored with the MENU id (FlowsMenu), which matches nothing, so
it fell back to a centred anchorless coachmark instead of pointing at the entry.

Easy to miss because every tour step in this fleet that works today targets an
entry whose menu id happens to EQUAL its route (Cases, MyWork). FlowsMenu ->
Flows is the first place they differ.

Verified against the live DOM: [data-cn-route="Cases"] resolves,
[data-cn-route="FlowsMenu"] does not.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 0a819f0

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint ⏭️
stylelint
build ⏭️
check-manifest
check-vue3-compile
test-l10n
format
composer ⏭️ ✅ 106/106
npm ⏭️ ✅ 552/552
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-27 15:15 UTC

Download the full PDF report from the workflow artifacts.

gate-96 (manifest-copy-style) caught it: "Em-dashes and double-dashes are AI
tells. Replace with a period, a comma, or a colon." The Flows stop's body ended
"...read and edit them — nothing to build now."; it now ends with a full stop
and a short sentence, which is what the rule asks for and reads no worse.

The English string is the l10n KEY, so the catalogues are re-keyed in the same
change and the browser .js rebuilt. Leaving the key behind would have made the
string untranslated in every locale while the catalogue still claimed to cover
it. The Dutch value drops its em-dash too, for the same reason the English one
does.

Verified per app with the script that app's own CI runs (test:l10n or
check:l10n), plus check:l10n-js, plus schema validation of the manifest.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 2a1298d

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
check-schema-l10n
check-l10n-js
composer ✅ 106/106
npm ✅ 552/552
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-27 18:42 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 37a88e3

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
check-schema-l10n
check-l10n-js
composer ✅ 106/106
npm ✅ 552/552
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-27 20:34 UTC

Download the full PDF report from the workflow artifacts.

The three walkthrough tests already here prove the steps are declared and
reach the built bundle. None of them proves a user ever sees one.

CnWalkthrough.armStep():

    const el = this.resolveTarget(this.step)
    if (!el) { if (this.step.optional) { this.wt.skip(); return } }

An optional step whose target is absent is skipped outright — no console
error, no stall, nothing on screen. `optional: true` is what stops these two
stops forcing authorship, so the friendly authoring choice is also the one
that fails silently.

The step counter cannot catch it either: skip() advances past the step and
the declared total stays 7, so `/ 7` reads the same whether both stops
render or neither does. That is a test that cannot fail.

So assert what resolveTarget() actually queries: [data-cn-route="<route>"].
Both stops target nav items in the SETTINGS section, and CnAppNav emitted
that attribute on its main/child/footer loops but not the settings one until
nextcloud-vue#811. That gap is how both steps could ship and reach nobody.

Red until package-lock.json pins a library containing #811, which is the
point of the test.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 37a88e3

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
check-schema-l10n
check-l10n-js
composer ✅ 106/106
npm ✅ 552/552
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-27 22:28 UTC

Download the full PDF report from the workflow artifacts.

gate-100 landed on .github main last night and is diff-scoped on
src/manifest.json, so this PR's walkthrough edit pulls the setup
declaration into scope. dossiq opened with `welcome`, which is what the
gate exists to stop: a welcome screen tells you what the app is, the
demo-data offer lets you SEE it, and seeing it is the only question a
first reader actually has.

WHAT MOVED. The step that loads the Bezwaar & Beroep sample data is
promoted from `seed` to `demo-data` and put first, with the greeting
folded into its copy rather than kept as a separate screen. ADR-111 is
explicit that the first step can still say hello.

THE ORDER CHANGE IS NOT FREE. `seed` used to run after `register-check`,
so it could assume the register and schemas existed. First, it cannot:
on a clean install they do not, and seeding into a register that is not
there yet touches zero rows — which the existing guard correctly refuses
to record as done, so the affordance would look broken rather than fail
loudly. runAction() now calls loadConfiguration() before seeding. It is
idempotent, so a repeat run costs nothing.

THE ACTION ID IS A PUBLIC SURFACE. `/api/setup/action/{actionId}` is a
POST route, not an internal label. A wizard someone left half-finished
keeps posting `seed`, so `seed` stays accepted as an alias; without it
the rename becomes a silent 400 that presents as "the button does
nothing". testTheOldSeedActionIdStillRunsTheStep pins that.

Verified locally against the schema CI actually uses: the app vendors
2.25.0 while hydra-gates vendors 2.26.0, and only the latter decides the
gate. gate-22 PASS (0 errors) under 2.26.0, gate-96 rc=0, gate-100 rc=0,
phpunit 13/13, phpcs rc=0, test:l10n and check:l10n-js clean.
…h anchor

The `see-case-types` and `see-flows` stops target nav items in the
SETTINGS section. CnAppNav emitted `data-cn-route` on its main, child and
footer loops but not the settings one, so both stops resolved nothing —
and CnWalkthrough.armStep() SKIPS an optional step whose target is
absent, silently. Both stops would have shipped and reached nobody.

The caret range is not what decides this. `npm ci` installs from
package-lock.json, so the fix only arrives when the LOCK moves. It was
pinned at 2.20.0, which does not contain #811.

2.21.0 verified by unpacking the published tarball rather than trusting
the version string: data-cn-route appears 4 times, one of them at line
277 inside the `v-for="item in settingsItems"` template at 262.

This is what turns the anchor test added in cbd1207 from red to green.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 6f41681

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
check-schema-l10n
check-l10n-js
composer ✅ 106/106
npm ✅ 552/552
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-28 06:25 UTC

Download the full PDF report from the workflow artifacts.

Two things this branch changed, both of which the suite still asserted the
old way.

1. The setup step id moved from `seed` to `demo-data` (ADR-111 rule 4), so
   `status.steps.seed` is now undefined and `after.steps.seed.done` threw
   rather than failing with a readable message. Updated to
   `steps['demo-data']`, and the POST now targets the canonical action id.
   The `seed` alias stays covered by SetupControllerStatusTest.

2. Edit on an index row navigates to the case's detail page instead of
   opening a modal over the list (@conduction/nextcloud-vue 2.21.0). The
   dialog is one click further on, behind the detail page's Edit button.

What each test checks is unchanged: the seeder still must not record a
zero-row run as done, and the title edit still must persist.
dossiq#1408 landed the generated demo dataset while this branch was open.
Both changed `setup.steps`, so they conflicted. Development's shape wins
and my half is withdrawn, because #1408 solves it properly:

- It adds `demo-data` as its OWN step with its own action
  (`install-demo-data`) and its own service, and keeps `seed` — the
  bezwaar/beroep seeder — as a separate step. My change had renamed `seed`
  to `demo-data` to satisfy gate-100, which would have collapsed two
  different datasets into one id.
- So the seed→demo-data rename, its `seed` alias, and the
  loadConfiguration() call I added are all reverted. `seed` runs after
  `register-check` again, so it can assume the register exists.
- `dwangsom-secret` is re-added on top: it is this branch's own step and
  was not in #1408.
- `seed` is restored to the step list. The merge had silently dropped it,
  because my side deleted it (as a rename) and nothing conflicted on the
  deletion.

The action-menu hunks also go development's way. My side REMOVED the
label-less action entries; development instead gave them proper labels and
a route to the detail page. Both close the invisible-row defect, and
development's manifest now validates clean against schema 2.26.0 — 0
non-object actions, 0 missing labels — so the strip is no longer needed.

Test fallout, both real:
- SetupController gained a 4th constructor argument (DemoDataService), so
  the test builder had to supply it or every test died in the constructor.
- The l10n catalogues came from development and lacked this branch's nine
  walkthrough and dwangsom strings; re-derived and translated.

Verified: gate-22 PASS under CI's 2.26.0 schema, gate-96 rc=0, gate-100
rc=0, test:l10n and check:l10n-js rc=0, phpcs rc=0, phpunit 2469 tests /
10931 assertions / 0 failures.
@rubenvdlinde
rubenvdlinde merged commit e917509 into development Aug 28, 2026
42 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/casetype-index-actions-and-setup-wizard branch August 28, 2026 08:56
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 36978e7

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
check-schema-l10n
check-l10n-js
composer ✅ 106/106
npm ✅ 552/552
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-28 09:39 UTC

Download the full PDF report from the workflow artifacts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant