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:
@@ -20,6 +20,7 @@ import {
|
||||
FORM_FEEDBACK,
|
||||
FORM_LOCALE,
|
||||
FORM_PATCH,
|
||||
FORM_PERMISSION_LOCKS,
|
||||
FORM_SESSION,
|
||||
FORM_VALUES,
|
||||
createUploadGate,
|
||||
@@ -78,6 +79,9 @@ const recordId = mode === 'update' ? Number(route.params.id) : null
|
||||
const schema = ref<FormView | null>(null)
|
||||
const values = ref<AdminRecord>({})
|
||||
const labels = ref<RecordMeta['labels']>({})
|
||||
// The record's permission locks (meta.permissionLocks, D-21): codes the
|
||||
// stored record fixes; they are shown locked and never sent.
|
||||
const permissionLocks = ref<NonNullable<RecordMeta['permissionLocks']>>({})
|
||||
/** The controller's record title (meta.title, pact.FormRecordTitle); '' when it names none. */
|
||||
const recordTitle = ref('')
|
||||
const errors = ref<Record<string, string[]>>({})
|
||||
@@ -205,7 +209,7 @@ const dirty = computed(
|
||||
!loading.value &&
|
||||
(pendingChanges.value > 0 ||
|
||||
activeUploads.value > 0 ||
|
||||
snapshot(editablePayload(fields.value, values.value, mode)) !== saved.value),
|
||||
snapshot(editablePayload(fields.value, values.value, mode, permissionLocks.value)) !== saved.value),
|
||||
)
|
||||
|
||||
function adopt(record: RecordEnvelope | undefined): void {
|
||||
@@ -221,9 +225,10 @@ function adopt(record: RecordEnvelope | undefined): void {
|
||||
}
|
||||
values.value = next
|
||||
labels.value = record.meta.labels ?? {}
|
||||
permissionLocks.value = record.meta.permissionLocks ?? {}
|
||||
recordTitle.value = record.meta.title ?? ''
|
||||
}
|
||||
saved.value = snapshot(editablePayload(fields.value, values.value, mode))
|
||||
saved.value = snapshot(editablePayload(fields.value, values.value, mode, permissionLocks.value))
|
||||
}
|
||||
|
||||
async function load(): Promise<void> {
|
||||
@@ -316,6 +321,7 @@ provide(FORM_PATCH, update)
|
||||
provide(FORM_LOCALE, computed(() => schema.value?.meta.locale || currentLocale.value))
|
||||
provide(FORM_ENABLED_LOCALES, computed(() => schemaEnabledLocales(schema.value?.meta)))
|
||||
provide(FORM_ASSETS, () => assetsReady)
|
||||
provide(FORM_PERMISSION_LOCKS, computed(() => permissionLocks.value))
|
||||
// The form's single polite live region (dependent dropdown clears, repeater
|
||||
// moves) and the feedback nested controls read: errors and notices by row
|
||||
// path, the confirm dialog (Phase 17, D-07, D-08).
|
||||
@@ -395,7 +401,7 @@ async function save(): Promise<RecordEnvelope | null> {
|
||||
forbidden.value = null
|
||||
notices.value = {}
|
||||
try {
|
||||
const body = editablePayload(fields.value, values.value, mode)
|
||||
const body = editablePayload(fields.value, values.value, mode, permissionLocks.value)
|
||||
const result =
|
||||
recordId === null
|
||||
? await api.POST('/{vendor}/{plugin}/{controller}', { params: { path, header: sessionHeader }, body })
|
||||
|
||||
@@ -9,7 +9,7 @@ import { controllerIdFromParams, controllerPath } from '../app/controllerRoutes'
|
||||
import { message, t } from '../app/i18n'
|
||||
import { loadControllerAssets } from '../app/pluginAssets'
|
||||
import { newSessionKey } from '../app/sessionKey'
|
||||
import { FIELD_SCOPE, FORM_SESSION, createUploadGate } from '../components/form/formContext'
|
||||
import { FIELD_SCOPE, FORM_PERMISSION_LOCKS, FORM_SESSION, createUploadGate } from '../components/form/formContext'
|
||||
import FormTabs from '../components/form/FormTabs.vue'
|
||||
import PreviewField from '../components/form/PreviewField.vue'
|
||||
import RecordActions from '../components/form/RecordActions.vue'
|
||||
@@ -54,6 +54,9 @@ const recordPath = `${listPath}/${recordId}`
|
||||
const schema = ref<FormView | null>(null)
|
||||
const values = ref<AdminRecord>({})
|
||||
const labels = ref<RecordMeta['labels']>({})
|
||||
// The record's permission locks (meta.permissionLocks): the preview shows
|
||||
// their values, without icon or note (read-only screen).
|
||||
const permissionLocks = ref<NonNullable<RecordMeta['permissionLocks']>>({})
|
||||
/** The controller's record title (meta.title, pact.FormRecordTitle); '' when it names none. */
|
||||
const recordTitle = ref('')
|
||||
const actions = ref<RecordAction[]>([])
|
||||
@@ -150,6 +153,7 @@ async function loadRecord(): Promise<boolean> {
|
||||
}
|
||||
values.value = { ...result.data.data }
|
||||
labels.value = result.data.meta.labels ?? {}
|
||||
permissionLocks.value = result.data.meta.permissionLocks ?? {}
|
||||
recordTitle.value = result.data.meta.title ?? ''
|
||||
actions.value = result.data.meta.actions ?? []
|
||||
return true
|
||||
@@ -212,6 +216,7 @@ provide(FIELD_SCOPE, {
|
||||
values: computed(() => values.value),
|
||||
path: (name: string) => name,
|
||||
})
|
||||
provide(FORM_PERMISSION_LOCKS, computed(() => permissionLocks.value))
|
||||
provide(FORM_SESSION, {
|
||||
key: sessionKey,
|
||||
recordId,
|
||||
|
||||
Reference in New Issue
Block a user