feat(16.2-01): host plugin field type elements in the admin SPA

- loadFieldTypes fetches GET /field-types after sign-in, loads each script
  once and adds the styles as global links activateStyles never disables
- PluginField creates the type's element, sets the value property and the
  contract attributes, takes summer-change as the form value and never
  writes back a value the element just emitted
- rendererForField and isRegisteredField route any field with an element
  to PluginField in forms, previews and the save payload
- rebuilt modules/boardwalk/dist
This commit is contained in:
2026-10-10 11:10:41 +02:00
parent 717d46ec3d
commit 83fa2f7b2c
16 changed files with 471 additions and 30 deletions

View File

@@ -17,7 +17,12 @@
// The ML controls (mltext, mltextarea, mlmarkdown) are thin shells over one
// locale-switching wrapper, MLField, around the text, textarea and markdown
// controls; each holds a locale -> text map, labelled as a group.
// Phase 16.2 adds plugin field types (D-13): a field whose schema carries an
// `element` renders PluginField whatever its type alias, holds a form value
// and is labelled as a group. Look such fields up with rendererForField and
// isRegisteredField, which know the field, not only its type.
import type { Component } from 'vue'
import type { FormField } from '../../api/types'
import CheckboxField from './fields/CheckboxField.vue'
import DatepickerField from './fields/DatepickerField.vue'
import DropdownField from './fields/DropdownField.vue'
@@ -25,6 +30,7 @@ import FileuploadField from './fields/FileuploadField.vue'
import NumberField from './fields/NumberField.vue'
import PartialField from './fields/PartialField.vue'
import PasswordField from './fields/PasswordField.vue'
import PluginField from './fields/PluginField.vue'
import PermissionEditorField from './fields/PermissionEditorField.vue'
import RelationManager from '../relation/RelationManager.vue'
import RelationField from './fields/RelationField.vue'
@@ -101,6 +107,24 @@ export function isRegistered(type: string): boolean {
return (renderers.has(type) || lazyRenderers.has(type)) && !valueless.has(type)
}
/** Whether a field's type is registered by a plugin: its schema names an element. */
export function isPluginField(field: Pick<FormField, 'element'>): boolean {
return typeof field.element === 'string' && field.element !== ''
}
/** The control of one field: a plugin field type's host, else rendererFor its type. */
export function rendererForField(field: FormField): Component {
if (isPluginField(field)) {
return PluginField
}
return rendererFor(field.type)
}
/** Whether the SPA edits this field's value: a plugin field type always does. */
export function isRegisteredField(field: FormField): boolean {
return isPluginField(field) || isRegistered(field.type)
}
/** Whether a field type renders only on an existing record (relation manager). */
export function needsRecord(type: string): boolean {
return recordBound.has(type)