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

@@ -1454,7 +1454,18 @@ export interface paths {
/** List linked relation records */
get: {
parameters: {
query?: never;
query?: {
/** @description Search term over the panel's searchable columns */
search?: string;
/** @description Sort column (a sortable panel column) */
sort?: string;
/** @description Sort direction (asc or desc) */
dir?: string;
/** @description Page */
page?: number;
/** @description Records per page (1-100, default 20) */
per_page?: number;
};
header?: never;
path: {
/** @description Vendor */
@@ -1537,7 +1548,18 @@ export interface paths {
/** List relation candidates */
get: {
parameters: {
query?: never;
query?: {
/** @description Search term over the panel's searchable columns */
search?: string;
/** @description Sort column (a sortable panel column) */
sort?: string;
/** @description Sort direction (asc or desc) */
dir?: string;
/** @description Page */
page?: number;
/** @description Records per page (1-100, default 20) */
per_page?: number;
};
header?: never;
path: {
/** @description Vendor */

View File

@@ -5,6 +5,7 @@ import type { components, paths } from './schema'
type Schemas = components['schemas']
type ControllerPath = paths['/{vendor}/{plugin}/{controller}']
type FieldOptionsPath = paths['/{vendor}/{plugin}/{controller}/fields/{field}/options']
type RelationLinkedPath = paths['/{vendor}/{plugin}/{controller}/{id}/relations/{name}']
export type AdminLoginData = Schemas['cabana.AdminLoginData']
export type AdminLoginRequest = Schemas['cabana.AdminLoginRequest']
@@ -27,6 +28,8 @@ export type RelationSchema = Schemas['cabana.RelationSchema']
export type RelationMessages = Schemas['cabana.RelationMessages']
export type RelationMutationResult = Schemas['cabana.RelationMutationResult']
export type RelationOption = Schemas['cabana.RelationOption']
export type RelationPanel = Schemas['cabana.RelationPanel']
export type RelationColumn = Schemas['cabana.RelationColumn']
export type FilterOption = Schemas['cabana.FilterOption']
export type RecordMeta = Schemas['cabana.RecordMeta']
export type RecordEnvelope = Schemas['cabana.RecordEnvelope']
@@ -46,5 +49,7 @@ export type IDsRequest = Schemas['cabana.AdminIDsRequest']
export type ControllerParams = ControllerPath['get']['parameters']['path']
/** The list query: search, sort, dir, page, per_page and filter[<name>]. */
export type ListQuery = NonNullable<ControllerPath['get']['parameters']['query']>
/** Linked and candidate relation query: search, sort, dir, page, per_page. */
export type RelationQuery = NonNullable<RelationLinkedPath['get']['parameters']['query']>
/** Relation field options query: search, page, per_page. */
export type FieldOptionsQuery = NonNullable<FieldOptionsPath['get']['parameters']['query']>