feat(16.2-01): translatable plugin fields, element uploads and save notices in the admin

- MLPluginField wraps PluginField in the MLField locale switcher; the
  element gets the active content locale and that locale's string
- isMLField treats a translatable plugin field as a locale map in the form
  state, FormView adopt and the relation child modal
- summer-upload posts the file with the admin session to the field upload
  route and answers summer-upload-result {id, url} or {id, error}
- noticeLines shows meta.notices under each field as a warning status line
  (text only) and in the saved toast; the stored value is reloaded
- docs/backend/field-types.md documents the full element contract;
  modules/boardwalk/dist rebuilt
This commit is contained in:
2026-10-10 11:25:26 +02:00
parent 8fa92db96d
commit 4adb48a76e
21 changed files with 415 additions and 54 deletions

View File

@@ -1,11 +1,11 @@
// Helpers shared by the record form and the settings form: context
// filtering, the save payload, 422 detail mapping and focus.
import type { AdminRecord, ErrorBody, FormField } from '../../api/types'
import type { AdminRecord, ErrorBody, FieldNotice, FormField } from '../../api/types'
import { permissionValues } from './control'
import { isMLFieldType, localeRecord } from './mlLocale'
import { isMLField, localeRecord } from './mlLocale'
import { isRegisteredField } from './registry'
export { ML_LOCALE_CHANGE, broadcastMLLocale, isMLFieldType, localeRecord } from './mlLocale'
export { ML_LOCALE_CHANGE, broadcastMLLocale, isMLField, isMLFieldType, localeRecord } from './mlLocale'
/** The screen a field is filtered for: the two form modes and the read-only preview (D-11). */
export type FormMode = 'create' | 'update' | 'preview'
@@ -65,7 +65,7 @@ export function editablePayload(fields: FormField[], values: AdminRecord, mode:
out[field.name] = permissionValues(field, value)
continue
}
if (isMLFieldType(field.type)) {
if (isMLField(field)) {
out[field.name] = localeRecord(value)
continue
}
@@ -136,7 +136,7 @@ export function presetUpdates(
isEdited: PresetEdited,
): AdminRecord {
const out: AdminRecord = {}
const sourceML = isMLFieldType(fields.find((field) => field.name === source)?.type)
const sourceML = isMLField(fields.find((field) => field.name === source))
for (const field of fields) {
const preset = field.preset
if (!preset || preset.field !== source || (field.type !== 'text' && field.type !== 'mltext')) {
@@ -184,7 +184,7 @@ export function initialValues(fields: FormField[], enabledLocales: readonly stri
out[field.name] = false
} else if (field.type === 'relation' && field.multiple) {
out[field.name] = []
} else if (isMLFieldType(field.type)) {
} else if (isMLField(field)) {
const seed: Record<string, string> = {}
for (const code of enabledLocales) {
seed[code] = ''
@@ -225,12 +225,12 @@ export function recordDisplayName(
values: AdminRecord,
enabledLocales: readonly string[],
): string {
const first = fields?.find((field) => field.type === 'text' || isMLFieldType(field.type))
const first = fields?.find((field) => field.type === 'text' || isMLField(field))
const value = first ? values[first.name] : undefined
if (typeof value === 'string' || typeof value === 'number') {
return String(value).trim()
}
if (!first || !isMLFieldType(first.type)) {
if (!first || !isMLField(first)) {
return ''
}
const texts = localeRecord(value)
@@ -246,6 +246,23 @@ export function recordDisplayName(
return ''
}
/**
* A save's meta.notices (D-10, 16.2) as field name -> lines shown under the
* field: a notice about one content locale reads "<LOCALE>: message". Absent
* or empty notices give an empty map.
*/
export function noticeLines(notices: readonly FieldNotice[] | null | undefined): Record<string, string[]> {
const out: Record<string, string[]> = {}
for (const notice of notices ?? []) {
if (!notice || typeof notice.message !== 'string' || notice.message === '' || typeof notice.field !== 'string') {
continue
}
const line = notice.locale ? `${notice.locale.toUpperCase()}: ${notice.message}` : notice.message
;(out[notice.field] ??= []).push(line)
}
return out
}
/** D-10 422 details (field -> messages) as a string-list map. */
export function fieldErrors(details: ErrorBody['details'] | undefined): Record<string, string[]> {
const out: Record<string, string[]> = {}