feat(10.1-02): render header and form partials through an allowlisted node renderer
- partialNodes rebuilds the server node tree with h() under the server's tag, attribute and URL lists - PartialHost owns the skeleton, empty and failure states and keeps nodes visible on refetch - type: partial is a valueless group-labelled field rendered on create and update - ListView shows headerPartial above the list card and refetches it after bulk delete - summer-partial and summer-stats style kit in main.css, documented in the cabana README; dist rebuilt
This commit is contained in:
@@ -39,10 +39,10 @@ const describedBy = computed(() =>
|
||||
|
||||
<template>
|
||||
<div class="flex min-w-0 flex-col gap-1.5" :data-field="field.name">
|
||||
<span v-if="labelsGroup" :id="`${controlId}-label`" class="font-semibold">
|
||||
{{ field.label || field.name }}<span v-if="field.required" class="text-danger" aria-hidden="true"> *</span>
|
||||
<span v-if="labelsGroup && field.label" :id="`${controlId}-label`" class="font-semibold">
|
||||
{{ field.label }}<span v-if="field.required" class="text-danger" aria-hidden="true"> *</span>
|
||||
</span>
|
||||
<label v-else-if="!selfLabelled" :for="controlId" class="font-semibold">
|
||||
<label v-else-if="!selfLabelled && !labelsGroup" :for="controlId" class="font-semibold">
|
||||
{{ field.label || field.name }}<span v-if="field.required" class="text-danger" aria-hidden="true"> *</span>
|
||||
</label>
|
||||
<FieldRenderer
|
||||
|
||||
31
admin/src/components/form/fields/PartialField.vue
Normal file
31
admin/src/components/form/fields/PartialField.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '../../../app/i18n'
|
||||
import PartialHost from '../../partial/PartialHost.vue'
|
||||
import ExtensionFailure from '../../ui/ExtensionFailure.vue'
|
||||
import type { FieldControlProps } from '../control'
|
||||
|
||||
// A `type: partial` form row (D-09; UI-SPEC S2): the controller partial
|
||||
// named by `path`, rendered on create (no record) and update (?id=). It holds
|
||||
// no value. The row's visible label, when declared, names the group. A form
|
||||
// without a controller (a settings page) has no partial route to call.
|
||||
const props = defineProps<FieldControlProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:id="controlId"
|
||||
role="group"
|
||||
data-partial-field
|
||||
:aria-labelledby="props.field.label ? `${controlId}-label` : undefined"
|
||||
:aria-describedby="describedBy || undefined"
|
||||
>
|
||||
<PartialHost
|
||||
v-if="source"
|
||||
variant="field"
|
||||
:source="source"
|
||||
:name="props.field.path || props.field.name"
|
||||
:record-id="recordId ?? null"
|
||||
/>
|
||||
<ExtensionFailure v-else data-partial-failed :text="t('backend::lang.extension.partial_failed')" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -162,7 +162,7 @@ onBeforeUnmount(() => {
|
||||
:id="controlId"
|
||||
role="group"
|
||||
data-widget-host
|
||||
:aria-labelledby="`${controlId}-label`"
|
||||
:aria-labelledby="field.label ? `${controlId}-label` : undefined"
|
||||
:aria-describedby="describedBy || undefined"
|
||||
:aria-busy="status === 'loading' ? 'true' : undefined"
|
||||
class="flex min-h-input items-center"
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
// 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). It holds no form value either, but it renders on
|
||||
// create and update.
|
||||
// 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.
|
||||
import type { Component } from 'vue'
|
||||
import CheckboxField from './fields/CheckboxField.vue'
|
||||
import DropdownField from './fields/DropdownField.vue'
|
||||
import NumberField from './fields/NumberField.vue'
|
||||
import PartialField from './fields/PartialField.vue'
|
||||
import RelationManager from '../relation/RelationManager.vue'
|
||||
import RelationField from './fields/RelationField.vue'
|
||||
import SwitchField from './fields/SwitchField.vue'
|
||||
@@ -37,6 +38,7 @@ const renderers = new Map<string, Component>([
|
||||
['relation', RelationField],
|
||||
[RELATION_MANAGER, RelationManager],
|
||||
['widget', WidgetField],
|
||||
['partial', PartialField],
|
||||
])
|
||||
|
||||
/** Types whose control shows the label itself (toggle cards, relation manager). */
|
||||
@@ -49,13 +51,13 @@ const selfLabelled = new Set<string>(['switch', 'checkbox', RELATION_MANAGER])
|
||||
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'])
|
||||
const valueless = new Set<string>([RELATION_MANAGER, 'widget', 'partial'])
|
||||
|
||||
/**
|
||||
* 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'])
|
||||
const groupLabelledTypes = new Set<string>(['widget', 'partial'])
|
||||
|
||||
export function rendererFor(type: string): Component {
|
||||
return renderers.get(type) ?? UnsupportedField
|
||||
@@ -75,7 +77,10 @@ export function ownsLabel(type: string): boolean {
|
||||
return selfLabelled.has(type)
|
||||
}
|
||||
|
||||
/** Whether a field type's label labels a group (widget) instead of a control. */
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user