feat(12.1-02): password and form-only fields, rules per operation and preset

- pact.FormVirtualFields lists form fields that are not model columns: never
  bound, filled or projected; their values reach the Form hooks through
  cabana.VirtualFieldsFromContext when the field's context allows the operation
- type: password is a masked field that must be listed as virtual
- pact.FormRules supplies the rule set per operation and replaces the model's
  Rules() for admin saves; a rule on a virtual field sees the submitted value
- preset on a text field follows another text field on the create form
- SPA: PasswordField, preset handling in FormView, empty password left out of
  an update
- README, docs, OpenAPI document, TS types and dist updated
This commit is contained in:
Jakub Zych
2026-10-05 10:35:08 +02:00
parent a65c670574
commit a1c6bb1ce6
42 changed files with 1284 additions and 49 deletions

View File

@@ -23,6 +23,7 @@ import {
focusField,
initialValues,
panelDomId,
presetValue,
snapshot,
tabDomId,
tabOf,
@@ -162,15 +163,17 @@ const dirty = computed(
!loading.value &&
(pendingChanges.value > 0 ||
activeUploads.value > 0 ||
snapshot(editablePayload(fields.value, values.value)) !== saved.value),
snapshot(editablePayload(fields.value, values.value, mode)) !== saved.value),
)
function adopt(record: RecordEnvelope | undefined): void {
if (record) {
// A record response never carries a password, so every password field
// is empty again after a save (UI-SPEC S7).
values.value = { ...record.data }
labels.value = record.meta.labels ?? {}
}
saved.value = snapshot(editablePayload(fields.value, values.value))
saved.value = snapshot(editablePayload(fields.value, values.value, mode))
}
async function load(): Promise<void> {
@@ -197,8 +200,22 @@ async function load(): Promise<void> {
loading.value = false
}
// Preset fields (fields.yaml `preset`, UI-SPEC S7): on create, a text field
// follows its source field until the administrator edits it by hand; that
// stops it for as long as the form is open. On update nothing follows.
const edited = new Set<string>()
function update(name: string, value: unknown): void {
values.value = { ...values.value, [name]: value }
const next = { ...values.value, [name]: value }
edited.add(name)
if (mode === 'create') {
for (const field of fields.value) {
if (field.preset?.field === name && field.type === 'text' && !edited.has(field.name)) {
next[field.name] = presetValue(field.preset.type, value)
}
}
}
values.value = next
if (errors.value[name]) {
const next = { ...errors.value }
delete next[name]
@@ -255,7 +272,7 @@ async function save(): Promise<RecordEnvelope | null> {
busy.value = true
forbidden.value = null
try {
const body = editablePayload(fields.value, values.value)
const body = editablePayload(fields.value, values.value, mode)
const result =
recordId === null
? await api.POST('/{vendor}/{plugin}/{controller}', { params: { path, header: sessionHeader }, body })