feat(10-04): search, link and unlink related records through the relation manager
- relation-manager registered in the field registry; renders only on an existing record, never on create, and is never part of the save body - RelationManager: relation schema label and comment, debounced search, selectable linked list (DataTable relation variant), toolbar buttons in declared order, confirmed unlink with plural messages and toasts - RelationPickerModal: Reka Dialog (aria-modal, focus trap, Esc) over the candidates endpoint five per page, selection kept across pages, Dodaj (N) POSTs link, focus returns to the opener - admin OpenAPI documents search, sort, dir, page and per_page on the linked and candidate relation routes so the SPA sends them typed - neutral acme.demo.widgets members fixtures and relation smoke tests
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
// 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
|
||||
// arrives with its own component in a later plan. Phase 10.1 turns this seam
|
||||
// into the plugin extension point.
|
||||
// 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.
|
||||
import type { Component } from 'vue'
|
||||
import type { ControllerParams, FormField, RelationOption } from '../../api/types'
|
||||
import CheckboxField from './fields/CheckboxField.vue'
|
||||
import DropdownField from './fields/DropdownField.vue'
|
||||
import NumberField from './fields/NumberField.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'
|
||||
@@ -27,6 +29,8 @@ export interface FieldControlProps {
|
||||
labels?: RelationOption[]
|
||||
/** Controller whose option endpoints serve this form; null for settings. */
|
||||
source?: ControllerParams | null
|
||||
/** Id of the record being edited; null on create and on settings pages. */
|
||||
recordId?: number | null
|
||||
}
|
||||
|
||||
const renderers = new Map<string, Component>([
|
||||
@@ -37,10 +41,17 @@ const renderers = new Map<string, Component>([
|
||||
['switch', SwitchField],
|
||||
['checkbox', CheckboxField],
|
||||
['relation', RelationField],
|
||||
['relation-manager', RelationManager],
|
||||
])
|
||||
|
||||
/** Types whose control shows the label itself (toggle cards). */
|
||||
const selfLabelled = new Set<string>(['switch', 'checkbox'])
|
||||
/** 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).
|
||||
*/
|
||||
const recordBound = new Set<string>(['relation-manager'])
|
||||
|
||||
export function rendererFor(type: string): Component {
|
||||
return renderers.get(type) ?? UnsupportedField
|
||||
@@ -48,7 +59,12 @@ export function rendererFor(type: string): Component {
|
||||
|
||||
/** Whether the SPA can edit values of this field type. */
|
||||
export function isRegistered(type: string): boolean {
|
||||
return renderers.has(type)
|
||||
return renderers.has(type) && !recordBound.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 {
|
||||
|
||||
Reference in New Issue
Block a user