feat(10.1-02): mount plugin widget elements and run their actions from the form

- pluginAssets loads controller scripts and stylesheets from {base}/assets/ only, once per URL
- WidgetField mounts the custom element with attributes only and posts summer-action through the typed client
- Only declared fill keys returned by the server are patched; the form turns dirty and nothing saves
- widget is a registered valueless type rendered on create and update, labelled as a group
- backend::lang.extension strings in en and pl; embedded dist rebuilt
This commit is contained in:
Jakub Zych
2026-09-29 02:04:34 +02:00
parent c3b76b1afb
commit 107d820109
19 changed files with 789 additions and 14 deletions

View File

@@ -3,7 +3,9 @@
// 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 turns this seam into the plugin extension point.
// Phase 10.1 adds the plugin extension types: a widget mounts a plugin custom
// element (D-04, D-09). It holds no form value either, but it renders on
// create and update.
import type { Component } from 'vue'
import CheckboxField from './fields/CheckboxField.vue'
import DropdownField from './fields/DropdownField.vue'
@@ -14,6 +16,7 @@ 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
@@ -33,24 +36,34 @@ const renderers = new Map<string, Component>([
['checkbox', CheckboxField],
['relation', RelationField],
[RELATION_MANAGER, RelationManager],
['widget', WidgetField],
])
/** 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 and hold no form value: never rendered on
* create, never part of the save body (D-05, design screen 5).
* Types that need a saved record: never rendered on create (D-05, design
* screen 5).
*/
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'])
/**
* 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'])
export function rendererFor(type: string): Component {
return renderers.get(type) ?? UnsupportedField
}
/** Whether the SPA can edit values of this field type. */
export function isRegistered(type: string): boolean {
return renderers.has(type) && !recordBound.has(type)
return renderers.has(type) && !valueless.has(type)
}
/** Whether a field type renders only on an existing record (relation manager). */
@@ -61,3 +74,8 @@ export function needsRecord(type: string): boolean {
export function ownsLabel(type: string): boolean {
return selfLabelled.has(type)
}
/** Whether a field type's label labels a group (widget) instead of a control. */
export function groupLabelled(type: string): boolean {
return groupLabelledTypes.has(type)
}