Skip to content

fix(decisions): feed every type picker from the registry, and carry the administrators group across the rename - #1109

Merged
rubenvdlinde merged 1 commit into
developmentfrom
fix/decision-type-picker-and-admin-group
Sep 3, 2026
Merged

fix(decisions): feed every type picker from the registry, and carry the administrators group across the rename#1109
rubenvdlinde merged 1 commit into
developmentfrom
fix/decision-type-picker-and-admin-group

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Two defects from the final one-engine acceptance proof.

The Add Decision type picker showed 'No results'

The cross-app create-proposal pickers were wired to the decision_types registry in #1104. Decidiq's own surfaces were not. They still read the stored schema's decisionType enum, and #1099 deliberately emptied that enum. The registry became the only authority, and the built-in dialog never asked it.

Four surfaces carried the defect, and they get two different fixes:

  • Decisions and Motions index pages. Each page's manifest now wires CnIndexPage's form-dialog slot to a new DecisionFormDialog. It renders the same CnFormDialog over the same schema. The one difference: the schema copy driving the form carries the registry vocabulary, fetched through the shared decisionLink.js helpers from fix(approval-routes): advance stages through OR's patch path, and feed the type picker from the registry #1104. Same endpoint, same seeded-13 fallback, same translated labels. A new withDecisionTypeVocabulary() helper in decisionLink.js does the splice, so the wirings cannot drift.
  • AgendaMotionsTab and MotionAmendmentsTab. These render the decision schema too, and their empty required picker blocked the save outright. There the type is the tab's discriminator, not a choice. The field is now excluded from the form and written by onConfirm. This also fixes the motions tab creating rows with no decisionType at all, which fell out of its own decisionType=motion filter on the next refresh.

Known gap: the detail pages' built-in edit dialog (CnDetailPage) has no slot to replace. Fixing that picker needs an nc-vue extension point. Filed here as a note rather than papered over.

The administrators group lived under the old app id

The register's authorization baseline named decidesk-administrators. OpenRegister's GroupProvisioner creates every group an authorization block names, so existing installs hold the admin group, and its hand-granted memberships, under the pre-rename id.

A Nextcloud gid cannot be renamed. IGroupManager offers create and delete only, and delete destroys memberships and shares. The chosen semantics are create-and-migrate, old id kept honored:

  • The baseline now names decidiq-administrators first and keeps decidesk-administrators in the arrays. Dropping the old id would strip access the moment the row re-imports, before any migration ran. On a read-only group backend it would strip access forever.
  • A new MigrateAdminGroup repair step copies the old group's members into the new one. Idempotent. It never deletes the old group and never removes a member. On installs that never had the old group it is a no-op: the register import provisions the new group empty, keeping the documented admin-provisioned, fail-closed posture.
  • The register row version moves to 0.13.0. The register import path skips on version_compare and has no content-diff escape, unlike the schema path, so without the bump the authorization change never reaches existing installs.

Verification

  • vitest: 27 files, 392 tests green, including new specs for withDecisionTypeVocabulary
  • PHPUnit: 1375 tests green, including 6 new MigrateAdminGroup tests
  • eslint, prettier, stylelint scope, manifest Ajv validation, schema-l10n ratchet: clean
  • phpcs, PHPMD (lib plus unusedparams config), Psalm (0 errors), PHPStan: clean
  • hydra gates v1.11.0, diff-scoped against origin/development: 62 of 62 applicable gates green

🤖 Generated with Claude Code

…he administrators group across the rename

Two defects from the final one-engine acceptance proof.

The cross-app create-proposal pickers were wired to the decision_types
registry in #1104, but decidiq's OWN surfaces still read the stored
schema's decisionType enum, which decision-types-as-configuration
(#1099) deliberately emptied. The Add Decision dialog on the Decisions
and Motions index pages therefore showed a required type picker with
'No results'. Those pages now wire CnIndexPage's form-dialog slot to a
DecisionFormDialog that renders the same CnFormDialog over the same
schema, with the vocabulary fetched through the shared decisionLink.js
helpers from #1104 (registry endpoint, seeded-13 fallback, translated
labels) and spliced in client-side by a new withDecisionTypeVocabulary
helper. The sweep found the same class on two more surfaces: the
agenda-item Motions tab and the motion Amendments tab render the
decision schema too, and their empty required picker BLOCKED the save.
There the type is the tab's discriminator rather than a choice, so the
field is excluded from the form and written by onConfirm - which also
fixes the motions tab creating rows with no decisionType at all,
invisible to its own decisionType=motion filter. The detail pages'
built-in edit dialog has no slot to replace and keeps the gap; that
needs an nc-vue extension point.

The register's authorization baseline named decidesk-administrators,
the pre-rename app id, and OpenRegister's GroupProvisioner creates
every group an authorization block names - so existing installs hold
the admin group, and its hand-granted memberships, under the old id.
A Nextcloud gid cannot be renamed, so the fix is create-and-migrate:
the baseline now names decidiq-administrators first while keeping the
old id honored (dropping it would strip access the moment the row
re-imports, before any migration ran), and a MigrateAdminGroup repair
step copies the old group's members into the new one. Idempotent,
never deletes the old group, never removes a member, and a no-op on
installs that never had the old group - those keep the documented
admin-provisioned fail-closed posture. The register row version moves
to 0.13.0 because the register import path skips on version_compare
with no content-diff escape, unlike the schema path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidiq @ 798fb47

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-nav-ceiling
test-l10n
format
check-l10n-js
check-schema-l10n
composer ✅ 104/104
npm ✅ 537/537
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-03 08:41 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 094b95e into development Sep 3, 2026
49 checks passed
rubenvdlinde added a commit that referenced this pull request Sep 3, 2026
…1111)

#1109 fixed the Decisions and Motions INDEX pickers through CnIndexPage's
form-dialog slot and filed the rest as a known gap: CnDetailPage had no
such slot, so every detail page that renders the Decision schema kept
showing an empty required decisionType picker in its built-in Edit
dialog. #1099 emptied properties.decisionType.enum on purpose, and the
detail pages had nowhere to splice the vocabulary back in.

nextcloud-vue#944 added the slot, deliberately with the same name and the
same scope as CnIndexPage's, so one replacement serves both page types.
This takes 2.34.0 and wires FOUR surfaces, not the one the gap named:
DecisionDetail, MotionDetail, AmendmentDetail and DecisionIntegrations.
The last is easy to miss and renders the same form: a `type: "detail"`
page with a `config.schema` gets its Edit affordance from CnPageRenderer
unconditionally, so the picker was empty there too.

All four reuse #1109's DecisionFormDialog and its
withDecisionTypeVocabulary() helper rather than a second copy of the
vocabulary logic, so the six wirings cannot drift.

One behavioural change in that dialog. CnDetailPage's slot `confirm`
resolves to `{ success: true, data }` / `{ error }` because a replacement
holds the only ref to its own CnFormDialog: the dialog raises `loading`
on submit, only setResult() lowers it, and `no-close` is bound to
`loading`. A failed edit leaves the form open, so dropping the result
strands the user in a modal that can neither retry nor close.
settleFormDialogResult() hands the result back and treats "resolved to
nothing" as normal, which is what CnIndexPage's confirm still does, so
the same component keeps working on the index pages.

Tests (tests/vitest/decisionDetailFormDialog.spec.js, 15):
- every detail page bound to the decision schema is wired, asserted as an
  EQUALITY on the page set so a fifth one cannot ship unwired
- the picker's negative control is the shipped schema itself: stored
  decisionType carries no enum and is required, so the built-in dialog
  renders zero options; passing it through the helper yields the registry
  vocabulary, and the store's schema is not mutated
- settleFormDialogResult closes on success, unlocks on failure, and stays
  out of the way for a confirm that resolves to nothing or a dialog that
  unmounted mid-save

Verified locally: vitest 28 files / 407 tests green, eslint 0 errors,
prettier clean, stylelint 0 errors, manifest Ajv PASS, schema-l10n and
l10n parity at baseline, hydra gates v1.11.0 diff-scoped green on the
diff (gate-16 and gate-69 findings are pre-existing and untouched here).

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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