feat(19.6.1-02): per-record locked permission values

- cabana.PermissionRecordLock and the optional PermissionRecordLockProvider
- RecordMeta.PermissionLocks (meta.permissionLocks) on show and save responses, notes localized
- save rule: a record-locked code keeps its stored value, a differing present value is 403
- Roles lock code-declared grants (locked_by_code); Administrators lock every override on your own record (locked_self)
- SPA: FORM_PERMISSION_LOCKS, locked codes never sent, record-locked rows in the permission editor
- regenerated admin OpenAPI and schema.d.ts; docs and README
This commit is contained in:
Jakub Zych
2026-10-22 11:29:25 +02:00
parent e2525d4956
commit 705f94d9e5
22 changed files with 588 additions and 60 deletions

View File

@@ -1,6 +1,6 @@
// Helpers shared by the record form and the settings form: context
// filtering, the save payload, 422 detail mapping and focus.
import type { AdminRecord, ErrorBody, FieldNotice, FormField } from '../../api/types'
import type { AdminRecord, ErrorBody, FieldNotice, FormField, PermissionRecordLock } from '../../api/types'
import { permissionValues } from './control'
import { isMLField, localeRecord } from './mlLocale'
import { isRegisteredField } from './registry'
@@ -48,9 +48,16 @@ export function contextAllows(field: FormField, mode: FormMode): boolean {
* The save body: values keyed by field name for every editable field that
* has a value. Read-only fields and types without a renderer are never sent.
* An empty password on update means "unchanged" and is left out; on create it
* is sent as entered and the server's rules decide (UI-SPEC S7).
* is sent as entered and the server's rules decide (UI-SPEC S7). `locks` are
* the record's permission locks (meta.permissionLocks): their codes are left
* out of each permissioneditor value.
*/
export function editablePayload(fields: FormField[], values: AdminRecord, mode: FormMode = 'create'): AdminRecord {
export function editablePayload(
fields: FormField[],
values: AdminRecord,
mode: FormMode = 'create',
locks?: Readonly<Record<string, readonly PermissionRecordLock[]>>,
): AdminRecord {
const out: AdminRecord = {}
for (const field of fields) {
if (field.readOnly || !isRegisteredField(field)) {
@@ -62,7 +69,8 @@ export function editablePayload(fields: FormField[], values: AdminRecord, mode:
}
if (field.type === 'permissioneditor') {
// Sent whenever it is shown, reduced to offered codes and values.
out[field.name] = permissionValues(field, value)
const locked = locks?.[field.name]
out[field.name] = permissionValues(field, value, locked ? new Set(locked.map((lock) => lock.code)) : undefined)
continue
}
if (isMLField(field)) {