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:
Jakub Zych
2026-09-27 17:18:14 +02:00
parent 6e1b6dd5bf
commit f4e97cccad
26 changed files with 1509 additions and 33 deletions

View File

@@ -11,12 +11,17 @@ defineProps<{
errors: Record<string, string[]>
labels?: RecordMeta['labels']
source?: ControllerParams | null
recordId?: number | null
idPrefix?: string
}>()
const emit = defineEmits<{ update: [name: string, value: unknown] }>()
function spanClass(span: string | undefined): string {
switch (span) {
function spanClass(field: FormFieldSchema): string {
// The relation manager always takes the whole row (design screen 5).
if (field.type === 'relation-manager') {
return 'col-span-full'
}
switch (field.span) {
case 'left':
return 'min-[600px]:col-start-1'
case 'right':
@@ -35,12 +40,13 @@ function spanClass(span: string | undefined): string {
<FormField
v-for="field in fields"
:key="field.name"
:class="spanClass(field.span)"
:class="spanClass(field)"
:field="field"
:model-value="values[field.name]"
:errors="errors[field.name]"
:labels="labels?.[field.name]"
:source="source"
:record-id="recordId"
:id-prefix="idPrefix"
@update:model-value="(value: unknown) => emit('update', field.name, value)"
/>