From 47db9666dd986f57f61a3486ceea9a8c53e039e5 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Thu, 22 Oct 2026 11:45:07 +0200 Subject: [PATCH] docs(19.6.1-02): complete SPA trigger and record permission locks plan --- .../19.6.1-02-SUMMARY.md | 284 ++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 .planning/phases/19.6.1-backend-administrators-screen/19.6.1-02-SUMMARY.md diff --git a/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-02-SUMMARY.md b/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-02-SUMMARY.md new file mode 100644 index 00000000..b6183cc3 --- /dev/null +++ b/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-02-SUMMARY.md @@ -0,0 +1,284 @@ +--- +phase: 19.6.1-backend-administrators-screen +plan: 02 +subsystem: admin +tags: [cabana, admin-spa, permissions, fields-yaml, openapi, boardwalk] + +requires: + - phase: 19.6.1-01 + provides: Administrators and Roles controllers, permission editor providers, Registry.permissionOrder, rolePermissions +provides: + - cabana.PermissionRecordLock and the optional cabana.PermissionRecordLockProvider + - RecordMeta.PermissionLocks (meta.permissionLocks) on show, create and update responses, notes localized + - record-lock save rule in applyPermissionValues (absent keeps stored, differing present value is 403) + - fields.yaml `trigger` (cabana.FieldTrigger on FormField.Trigger) with boot checks + - SPA trigger evaluation (trigger.ts), trigger note in FormField, record-locked rows in PermissionEditorField + - regenerated admin/openapi/admin.json, admin/src/api/schema.d.ts and modules/boardwalk/dist +affects: [19.6.1-03 (SPA and Go unit tests for triggers and record locks), any plugin with a permission editor or a fields.yaml that wants trigger] + +actuals: + tokens: 28900 + tasks: 2 + commits: 2 +plan_head_before: e2525d4956e58f693b841324f2837e23fe213d1f +plan_head_after: 7091d09699c30b000a4ae6776ada33560e679252 + +tech-stack: + added: [] + patterns: + - "Per-record display locks travel in the record response meta, never in the form schema" + - "Display rules (trigger, record locks) are applied in the SPA by leaving values out of the save body; the server keeps stored values and stays authoritative" + - "Injection keys (FORM_PERMISSION_LOCKS, FORM_TRIGGER_NOTES) instead of prop threading through FormGrid/FormField/FieldRenderer" + +key-files: + created: + - modules/cabana/field_trigger.go + - modules/cabana/field_trigger_smoke_test.go + - modules/cabana/record_locks_smoke_test.go + - modules/cabana/example_permission_record_locks_test.go + - admin/src/components/form/trigger.ts + modified: + - modules/cabana/field_permission.go + - modules/cabana/relation_field.go + - modules/cabana/crud.go + - modules/cabana/relation_child.go + - modules/cabana/backend_roles_admin.go + - modules/cabana/backend_users_admin.go + - modules/cabana/form_schema.go + - modules/cabana/schema_types.go + - modules/cabana/field_dependent.go + - modules/cabana/field_repeater.go + - modules/cabana/admin_openapi.go + - modules/cabana/backendscreens/models/user/fields.yaml + - modules/cabana/backend_screens_smoke_test.go + - modules/cabana/README.md + - modules/phrasebook/backend/lang/en/lang.yaml + - modules/phrasebook/backend/lang/pl/lang.yaml + - admin/openapi/admin.json + - admin/src/api/schema.d.ts + - admin/src/api/types.ts + - admin/src/components/form/formContext.ts + - admin/src/components/form/control.ts + - admin/src/components/form/formState.ts + - admin/src/components/form/FormField.vue + - admin/src/components/form/fields/PermissionEditorField.vue + - admin/src/components/relation/RelationChildModal.vue + - admin/src/views/FormView.vue + - admin/src/views/PreviewView.vue + - modules/boardwalk/dist + - docs/backend/forms.md + - docs/backend/users-and-permissions.md + +key-decisions: + - "Record-lock notes are filled as phrase keys in projectFullRecord and translated where the show and save paths (and the relation child paths) finish, with s.tr" + - "A record-locked row sets data-locked and data-locked-by=\"record\" only where the lock is shown (editable screens); read-only screens show the lock's value only" + - "The trigger note is looked up by the FormGrid scope path, so a repeater row field never picks up a top-level note; RelationChildModal provides an empty note map" + - "The non-superuser schema check in TestAdministratorsFormSmoke now looks for an is_superuser field rather than the string: the permissions field's trigger names is_superuser by design (UI-SPEC C6)" + +patterns-established: + - "Optional controller capabilities next to PermissionEditorProvider are asked only for stored records and only through cabana's save/show paths" + - "Triggers are compiled per controller form; settingsFormOnly/controllerFormOnly and the repeater row check refuse them elsewhere" + +requirements-completed: [SC-1, SC-2, D-06, D-08, D-16, D-18, D-21, D-22] + +coverage: + - id: D1 + description: "Record-lock save rule: absent locked code keeps stored, differing present value is 403, equal body passes, request-Locked plus record-locked absent code is not a 403, no locks asked on create; meta.permissionLocks present only with a provider" + requirement: "D-21" + verification: + - kind: unit + ref: "modules/cabana/record_locks_smoke_test.go#TestPermissionRecordLocksSmoke" + status: pass + human_judgment: false + - id: D2 + description: "Roles and Administrators saves keep working under the record-lock rule (system role grants, own record)" + requirement: "D-16" + verification: + - kind: integration + ref: "modules/cabana/backend_screens_smoke_test.go#TestRolesSmoke" + status: pass + - kind: integration + ref: "modules/cabana/backend_screens_smoke_test.go#TestAdministratorsFormSmoke" + status: pass + human_judgment: false + - id: D3 + description: "Trigger compile: valid trigger reaches the localized schema; unknown action, unknown condition, extra key, missing key, not a mapping, self reference, unknown controlling field, text controlling field, repeater row are boot errors naming controller, field and trigger; settings and relation forms refuse it; Administrators permissions carries the D-06 trigger" + requirement: "D-06" + verification: + - kind: unit + ref: "modules/cabana/field_trigger_smoke_test.go#TestFieldTriggerSmoke" + status: pass + human_judgment: false + - id: D4 + description: "Generated OpenAPI, schema.d.ts and the committed admin build match fresh generations" + requirement: "D-18" + verification: + - kind: integration + ref: "scripts/check-admin-openapi.sh --check" + status: pass + - kind: integration + ref: "scripts/check-admin-dist.sh" + status: pass + human_judgment: false + - id: D5 + description: "SPA rendering: trigger note, read-only overrides while Superuser is on, record-locked rows with notes, tab removal for hidden fields, live-region announcement" + requirement: "D-21" + verification: [] + human_judgment: true + rationale: "Plan 03 owns the SPA unit tests; the browser behaviour is the Task 2 human-check (verify-work UAT). A throwaway vitest run during execution confirmed the locked row, the payload omission and the trigger note markup, and was not committed." + +duration: 23min +completed: 2026-10-22 +status: complete +--- + +# Phase 19.6.1 Plan 02: SPA support for triggers and per-record permission locks Summary + +**The admin now shows permissions a stored record fixes (a role's plugin-declared grants, an administrator's own overrides) as locked rows with their notes, through a new optional `cabana.PermissionRecordLockProvider` and `meta.permissionLocks`. fields.yaml gains WinterCMS's `trigger`, which the Administrators form uses to make the overrides read-only while Superuser is on. The OpenAPI types and the embedded admin build were regenerated.** + +## Performance + +- **Duration:** ~23 min +- **Started:** 2026-10-22T09:21:00Z +- **Completed:** 2026-10-22T09:44:04Z +- **Tasks:** 2 +- **Files modified:** 38 (1089 insertions, 95 deletions, dist assets included) + +## Accomplishments + +- **Record locks (D-21 for D-16 and D-08).** + - `PermissionRecordLock{Code, Value, Note}` and the optional `PermissionRecordLockProvider` sit next to `PermissionEditorProvider`. + - `projectFullRecord` fills `RecordMeta.PermissionLocks`. Show, create and update responses (and the relation child paths) translate the notes in the request locale. The key is omitted when no lock applies. + - `applyPermissionValues` on update: a record-locked code that is absent from the body keeps its stored value. A present value must equal the stored one, or the save gets a 403 on the field. The code is skipped in the request-Locked comparison, so a system role whose code grant is not in its stored JSON saves without a 403. + - Roles lock every `rolePermissions(code)` grant, in catalog order, at value 1 with `locked_by_code`. Administrators lock every offered code on the editor's own record at its stored override, with `locked_self`. +- **Field trigger (D-21 for D-06).** + - `FieldTrigger{Action, Field, Condition}` on `FormField.Trigger`. + - `compileTrigger` takes exactly action, field and condition. `checkTriggers` runs in `compileForm` through `bootErr`. + - Repeater rows, settings forms and relation forms refuse the key. + - The Administrators `permissions` field declares `trigger: {action: disable, field: is_superuser, condition: checked}`. +- **SPA.** + - `trigger.ts` (`triggerState`, `applyTriggers`); injection keys `FORM_PERMISSION_LOCKS` and `FORM_TRIGGER_NOTES`. + - `permissionValues(field, value, locked?)` and `editablePayload(fields, values, mode, locks?)`. + - FormView: every use of the field list goes through `applyTriggers`, applied with the whole form's values. That covers tabs, panel fields, the banner's field names, the dirty snapshot and the save body. When the active tab disappears, the first remaining tab becomes active. A flip to disabled is announced as "