Files
summercms/admin/src/components/form/registry.ts
Jakub Zych a1c6bb1ce6 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
2026-10-05 10:35:08 +02:00

105 lines
4.7 KiB
TypeScript

// Field renderer registry (D-05). A form field's `type` selects the control
// component; any type without a renderer gets UnsupportedField, the design's
// dashed box, so an unknown type never breaks the form. The relation manager
// is registered like any control but holds no form value: it edits its
// relation through its own endpoints and renders only on an existing record.
// Phase 10.1 adds the plugin extension types: a widget mounts a plugin custom
// element (D-04, D-09) and a partial renders a server template (D-17). They
// hold no form value either, but they render on create and update.
// Phase 12.2 adds the fileupload control: it edits its files through the file
// routes against the form's session key (D-02, D-03), so it holds no form
// value either and renders on create and update. The datepicker control is
// a plain value field: its string value is part of the save body (D-18).
// Phase 12.1 adds the password control: a plain value field whose value the
// server never sends back, so it is empty on load and after every save.
import type { Component } from 'vue'
import CheckboxField from './fields/CheckboxField.vue'
import DatepickerField from './fields/DatepickerField.vue'
import DropdownField from './fields/DropdownField.vue'
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 RelationManager from '../relation/RelationManager.vue'
import RelationField from './fields/RelationField.vue'
import SwitchField from './fields/SwitchField.vue'
import TextField from './fields/TextField.vue'
import TextareaField from './fields/TextareaField.vue'
import UnsupportedField from './fields/UnsupportedField.vue'
import WidgetField from './fields/WidgetField.vue'
// The control helpers live in ./control so the field components never import
// this module: registry -> field -> registry would be an import cycle whose
// evaluation order decides whether a renderer is registered at all.
export { controlAttributes, controlClass, toggleOn, toggleValue } from './control'
export type { FieldControlProps } from './control'
/** The relation manager's field type (design screen 5). */
export const RELATION_MANAGER = 'relation-manager'
// The relation manager renders child forms (FormGrid -> FormField -> this
// registry), so it sits on an import cycle with this module. It is looked up
// when a field renders, never while this module evaluates: whichever module
// of the cycle loads first, the binding is complete by then.
const lazyRenderers = new Map<string, () => Component>([[RELATION_MANAGER, () => RelationManager]])
const renderers = new Map<string, Component>([
['text', TextField],
['textarea', TextareaField],
['number', NumberField],
['dropdown', DropdownField],
['switch', SwitchField],
['checkbox', CheckboxField],
['relation', RelationField],
['widget', WidgetField],
['partial', PartialField],
['fileupload', FileuploadField],
['datepicker', DatepickerField],
['password', PasswordField],
])
/** Types whose control shows the label itself (toggle cards, relation manager). */
const selfLabelled = new Set<string>(['switch', 'checkbox', RELATION_MANAGER])
/**
* Types that need a saved record: never rendered on create (D-05, design
* screen 5), except a relation manager whose field is deferrable (Phase
* 12.2, D-03): FormView keeps it and it works on owner id 0.
*/
const recordBound = new Set<string>([RELATION_MANAGER])
/** Types that hold no form value: never part of the save body (D-09). */
const valueless = new Set<string>([RELATION_MANAGER, 'widget', 'partial', 'fileupload'])
/**
* Types whose control is a group rather than one focusable element: the
* visible label is a span the group points at, not a label for an input.
*/
const groupLabelledTypes = new Set<string>(['widget', 'partial', 'fileupload'])
export function rendererFor(type: string): Component {
return renderers.get(type) ?? lazyRenderers.get(type)?.() ?? UnsupportedField
}
/** Whether the SPA can edit values of this field type. */
export function isRegistered(type: string): boolean {
return (renderers.has(type) || lazyRenderers.has(type)) && !valueless.has(type)
}
/** Whether a field type renders only on an existing record (relation manager). */
export function needsRecord(type: string): boolean {
return recordBound.has(type)
}
export function ownsLabel(type: string): boolean {
return selfLabelled.has(type)
}
/**
* Whether a field type's label names a group (widget, partial) instead of a
* control. Such a row shows no label row when the field declares no label.
*/
export function groupLabelled(type: string): boolean {
return groupLabelledTypes.has(type)
}