Skip to content

feat(case): copy a case, start a sub-process, plan a follow-up - #1935

Merged
rubenvdlinde merged 17 commits into
developmentfrom
feat/case-actions-menu
Sep 8, 2026
Merged

feat(case): copy a case, start a sub-process, plan a follow-up#1935
rubenvdlinde merged 17 commits into
developmentfrom
feat/case-actions-menu

Conversation

@rubenvdlinde

@rubenvdlinde rubenvdlinde commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Implements the merged OpenSpec change case-type-authoring-extras (rows A10, A11, A29, A30, A31 of the round 2 placement). One noun: the case type's blueprint — what a status looks like, where a type gets its blueprint from, how types are grouped, what personal data it processes, and how it leaves and enters the system.

Scenario to e2e map

Every @e2e-tagged scenario in the five spec deltas, in tests/e2e/case-type-authoring-extras.spec.ts (13 tests):

Requirement Scenario Test
REQ-CT-01 A coloured status shows on the board a coloured status draws its board column in that colour
REQ-CT-01 A hidden status keeps its cases out of the list a hidden status keeps its cases off the list, and Closed brings them back
REQ-CT-01 (the badge half, on the case page) the case page draws the current status in its status's colour
REQ-CT-02 A child shows its parent's statuses a child that declares nothing shows its parent's four statuses, marked Inherited
REQ-CT-02 A child overrides one deadline a child's own deadline beats its parent's
REQ-CT-02 A cycle is refused a parent that descends from the type is refused, and the message names the cycle
REQ-PDM-01 A folder narrows the index picking a folder narrows the Case types index to that category
REQ-PDM-02 A shared attribute appears on every type an attribute saved without a case type is listed on every type, marked Shared
REQ-AVG-01 The block reads back what you saved the personal data block reads back the categories and the basis
REQ-WIE-01 Export downloads the bundle Export starts a download whose name carries the type's identifier
REQ-WIE-01 Duplicate opens the copy Duplicate lands you on the copy, with the same statuses
REQ-ZV-01 A draft with findings is not published a draft with findings lists them and stays a draft
REQ-ZV-01 A valid draft is published a valid draft is published with its change note, and the version says so

Two scenarios stay @e2e exclude exactly as the deltas wrote them: Import needs an OS file dialog (covered by the controller and service tests), and a verwerkingsactiviteit code cannot be refused until OpenRegister exposes the register as a referenceable schema (task 4.3, still blocked).

Where the design and the code disagree, and why

Five decisions in design.md could not be implemented as written. Each is recorded in tasks.md beside its task.

  • run-action is not a header-action type. The manifest schema enumerates handler, open-modal, open-page, navigate, object-op, export, open-form, refresh, api-call, agent and toggle, and CnActionButtons resolves exactly those. All four actions as designed would have been refused by check:manifest, and had they passed they would have rendered four buttons that dispatch nothing. Export is an api-call with download: true; the other three are dialogs, each for a reason a declarative action cannot meet — Import takes a file, Publish must show its findings before asking for a change note, Duplicate must land on the copy.
  • folderSidebar.source: "facet" does not exist. CnIndexPage resolves register, field, custom and files, and an unknown source falls through to custom — whose folder list is the absent folders array, so the pane renders empty and says nothing. It is source: "field" over category.
  • A filter cannot dot-path into a $ref. status.hiddenInLists is dropped by OpenRegister, which is a default filter that silently lists everything. The case now carries statusHiddenInLists, a materialised x-openregister-calculations entry over @ref.statusType.hiddenInLists — the same shape isFinalStatus already used.
  • The Inherited badge cannot be an object-list column. An object-list fetches OpenRegister itself, so the only question it can ask is caseType = @objectId: a child that inherits its lifecycle would render an empty table. GET /api/case-types/{id}/blueprint merges the chain server-side; case-type-blueprint renders it, and stays out of any tab strip because a type: "custom" widget named as a tab child resolves by registry type and renders an empty panel without logging.
  • Publish could not reuse either existing validate. CaseDefinitionController::validate() validates an uploaded package, and ZgwZtcRulesService::validatePublish() reads the type's own statuses — so a child would be refused with "give it a status" while its page showed four. CaseTypePublishService validates through the resolver.

What the checks found

Five defects that no other check saw, all fixed on the branch:

  • gate-14 route-reachability: appinfo/routes.php still carried caseDefinition#blueprint and caseDefinition#publish after the controller they pointed at was reverted. A route with no target method is a 500 at dispatch.
  • gate-9 semantic-auth: #[NoAdminRequired] over an admin-only body, which is what the change's own task text asked for. Both methods now carry #[AuthorizedAdminSetting(AdminSettings::class)] and the body guard is gone.
  • gate-6 orphan-auth: assertNoCycle() was defined and never called. Wired into CaseTypePublishService::validate(), which is the only write dossiq owns — a case type is saved straight to OpenRegister's object API by the page, so "refused on save" can be met nowhere else. Recorded as a limitation.
  • gate-16 spec-coverage: 44 changed frontend methods with no @spec.
  • ManifestColumnBindingTest: the Versions list bound its Updated column to updated, which workflowTemplate does not declare — a column that renders a dash in every row. It is @self.updated.

Known limitations, recorded in tasks.md

case.status carries an x-relation-filter and case.caseType an x-openregister-prefill; both are OpenRegister's, both read the child's own rows, and neither can express a chain. So the status picker on a new case of a child type offers only the child's own statuses, and the case form does not pick up a shared attribute. effectiveCaseType() inherits initialStatus so every PHP reader is right; the picker is filed as an OpenRegister request.

Four findings from a sibling change, checked against this one

  • run-action is not a header-action type. Not used here: all three entries are open-modal, and npm run check:manifest passes.
  • An open-modal action forwards props verbatim. Handled: none of the three passes props at all, and each dialog reads the case from the route.
  • A folder sidebar source of facet does not exist. No sidebar source is touched here.
  • A filter cannot dot-path into a $ref. No filter is added here, and the one cross-object read this change needs is exactly a materialised calculation: case.hasStartableFlows over @ref.caseType.startableFlows.

Checks, by exit code

Run locally on this branch, AFTER merging development in (four l10n files conflicted, both sides having appended entries at the tail of the translations object; resolved as the union and the .js catalogues regenerated). E2E was NOT run: it runs on the development push, and a pull_request check cannot prove an E2E fix.

Check Exit
npm run lint 0
npx vitest run (72 files, 820 tests) 0
npm run check:manifest 0
node tests/l10n/check-l10n.js 0
node scripts/check-schema-l10n.js 0
npm run check:l10n-js 0
npm run format 0
composer lint 0
composer phpcs 0
phpmd, swept per lib/ directory 0, no findings
composer psalm 0
composer phpstan 0
./vendor/bin/phpunit (3262 tests) 0
HYDRA_GATE_BASE_REF=origin/development run-hydra-gates.sh 0 (82 of 82 applicable gates ran; two pre-existing advisory warnings, non-blocking)

composer check:strict was run as its separate legs: as one command it exceeds a 300s budget. phpmd was swept per directory because printing nothing is its OOM signature, so a whole-lib/ pass that says nothing proves nothing.

🤖 Generated with Claude Code

Conduction Release Bot added 10 commits September 8, 2026 14:23
CaseCopyService carries the type, requester, confidentiality, priority,
intake channel and properties, relates the copy back to its source and
links the source documents by reference when asked. The number, deadline,
result, status history, decisions and publications are never copied, and
the status is left to the case type prefill so there is one write path
for it.
CaseActionsController carries copy, startable-flows, plan and planned.
Every method is NoAdminRequired and asks CaseAccessGuard before the
service: mutation access for the two writes, read access for the two
reads. CaseFlowActions holds the flow half and resolves every OpenRegister
class by name, because dossiq declares no app dependency on it.

A flow is a row in oc_openregister_flows, not a register object, so
startableFlows holds plain uuids rather than a $ref: a $ref addresses a
schema in a register and there is no flow schema to name.
The entry is open-modal onto CaseCopyDialog, not the handler type the
design named: a handler action resolves against effectiveManifest.actions,
a JSON map that cannot hold a function, and dossiq declares no such key,
so the entry would warn to the console and do nothing. The id is
copy-case because copy is a reserved id CnActionsBar drops.

Not CnCopyDialog either: 2.41.0 offers three naming patterns over a fixed
name and carries no slots, so there is nowhere to put Include documents
and no way to type a title that is not one of the three.
caseType gains startableFlows and the case gains hasStartableFlows, a
materialised boolean over it, because the Start entry must be hidden on a
type that lists none and an actions local visibleWhen can only see the
case record. Both schema versions are bumped: OpenRegister fast-skips a
schema whose version did not move, so the properties would be stored in
this file and absent from every install.

The list is plain strings and not a $ref: a flow is a row in
oc_openregister_flows, not an object in a register.
The dialog lists what the case type allows and posts the chosen flow to
OpenRegisters own run endpoint with the case as subject {uuid, register,
schema}, the three keys FlowRunRow reads, so the run lands in the Flow
runs widget beside it and dossiq stores no copy of a run.

The entry is hidden by the cases materialised hasStartableFlows. The
dialog still says so when the list comes back empty, because that gate is
a save-time value and a case type edited since the last case save has not
been recomputed yet.
The plan writes one scheduled flow: a schedule trigger pinned to the date
with an explicit runAs, feeding dossiqs createSubCase node. It is written
disabled, published, then enabled, because a run is refused unless a
published sound version exists.

The engine has no one-shot trigger and five cron fields cannot say once,
so PlannedFollowUpSweepJob switches a planned flow off after it has
fired. Left alone, a follow-up planned for October 2026 would also open a
case in October 2027 with nothing reporting it.
A planned follow-up is a scheduled flow and not a case, so the related
widget cannot list it: it reads related objects. CasePlannedWidget wraps
the librarys own CnRelatedObjectsWidget and hands it the planned rows as
an extraSections group, so the built-in related content is untouched.

The widget declares its own registry TYPE rather than type custom: a
custom widget resolves through the pages widget-<id> slot, which
CnDetailPage renders per layout grid item only, and a tab child has no
grid item. CnTabsWidget resolves a tab child by type and renders nothing,
silently, when no key answers.
Copy with and without documents, the Start list following the case type,
Start hidden on a type that allows none, and a planned follow-up on the
Related cases tab.

Two scenarios are re-annotated @e2e exclude rather than written as tests
that cannot fail. A reader cannot copy: Playwright signs in as admin and
cannot take a lesser role, and PHPUnit asserts the guard runs before the
service. A run appears on the case: running a flow needs an adopted,
published and enabled flow, which a fresh install deliberately has not
got, so arranging one would make it a test of the adoption path.
PlannedFollowUpDocument is split out of CaseFlowActions: the flow a
planned follow-up is written as, and read back from, is pure, which drops
the class complexity under the phpmd threshold and makes the two rules
that matter testable without an instance.

PHPStan caught a real one. CARRIED and NEVER_COPIED are provably
disjoint, so the runtime strip between them was dead code. The ban is now
a rule about the constants, asserted by the unit test both against the
written payload and as a disjointness check.

The date is parsed without DateTimeImmutable::createFromFormat, whose
static call is a phpmd finding, and the shape check it would do is done
in full anyway.
Comment thread tests/vitest/caseActionsMenu.spec.js Fixed
The only conflict is the tail of the l10n catalogues: development added
keys where this branch did, in both en.json and nl.json. Resolved by
taking developments file and re-adding this branchs 25 keys onto it, then
regenerating en.js and nl.js with the build script rather than by hand.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 83a69e9

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 ✅ 541/541
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-08 13:19 UTC

Download the full PDF report from the workflow artifacts.

Clean. It brings 2ed1e85, which declares @uses on the four test classes
whose eight risky results this PR inherited: they name StatusTypeLookup
and TransitionSpecReader, both from developments own checklist-per-status
work and neither touched here. The failure arrived because a pull_request
check tests the branch merged into the base, not the branch.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 222f313

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 ✅ 541/541
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-08 13:27 UTC

Download the full PDF report from the workflow artifacts.

… a regex

CodeQL flagged the key being escaped into a pattern as incomplete
sanitisation, correctly: escaping a callers string into a regex is a shape
that is wrong more often than it is right, and it bought nothing here. The
entry is a fixed two-line shape in a file this test reads whole, so
slicing forward from the key answers the same question with no pattern at
all, and it slices to the closing brace so a neighbouring entrys kind can
never answer for this one.

Two negative cases are added beside it: the helper decides seven
assertions, so it is asserted to be capable of saying no.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ bc7008a

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 ✅ 541/541
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-08 13:35 UTC

Download the full PDF report from the workflow artifacts.

Conduction Release Bot and others added 4 commits September 8, 2026 15:46
Three conflicts, all "both sides added an entry beside each other", all
resolved by keeping both: an icon import, three registry dialogs, and a
materialised boolean on the case.

The versions are the load-bearing part. Development moved caseType to
1.5.0 and case to 1.18.0 for its own statusHiddenInLists, so this branch
goes to 1.6.0 and 1.19.0: OpenRegister fast-skips a schema whose version
did not move, and a merge that kept 1.18.0 would have shipped
hasStartableFlows into a file nothing re-reads.

A sibling vitest pinned the case version to exactly 1.18.0, which fails on
the next change that legitimately bumps the same schema. It now asserts
at least 1.18.0, which is the rule it meant.
Two conflicts. The registry import is both sides adding one, kept both.
The manifest is development adding subtitleField, breadcrumbs and two
notes ABOVE the page note this branch extended: their keys are kept and
their _note dropped, because this branchs _note is theirs verbatim plus a
paragraph on the three new actions. Verified: every sentence of theirs is
present in the merged note.
One conflict, the CaseDetail page note: development replaced the audit
history sentence with a longer one about the version-history tab moving
to the timeline, in the same string this branch appended a paragraph to.
Resolved as the union, developments note plus this branchs paragraph on
the three new actions, so neither side loses a sentence.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 2cc8a00

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 ✅ 541/541
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-08 14:22 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit f060b12 into development Sep 8, 2026
48 of 49 checks passed
@rubenvdlinde
rubenvdlinde deleted the feat/case-actions-menu branch September 8, 2026 14:24
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.

2 participants