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

@@ -75,8 +75,8 @@ describe('tabs and context', () => {
it('groups fields into tabs with the default tab first and filters by context', async () => {
const { wrapper } = await mountApp('/acme/demo/widgets/1', routes())
expect(tabLabels(wrapper)).toEqual(['Podstawowe', 'Details', 'Parts'])
const [general, details, parts] = wrapper.findAll('[role="tab"]')
expect(tabLabels(wrapper)).toEqual(['Podstawowe', 'Details', 'Members'])
const [general, details, members] = wrapper.findAll('[role="tab"]')
expect(general!.attributes('aria-selected')).toBe('true')
expect(wrapper.find('[role="tabpanel"]').attributes('aria-labelledby')).toBe(general!.attributes('id'))
expect(wrapper.find('#field-name').exists()).toBe(true)
@@ -88,9 +88,10 @@ describe('tabs and context', () => {
expect(wrapper.find('#field-batch').exists()).toBe(false)
expect(wrapper.find('#field-name').exists()).toBe(false)
// The relation manager is not registered yet: it renders the fallback.
await parts!.trigger('click')
expect(wrapper.find('[data-unsupported-field]').text()).toBe('Nieobsługiwany typ pola: relation-manager')
// The relation manager has its own renderer (relation.smoke covers it).
await members!.trigger('click')
expect(wrapper.find('[data-relation-manager]').exists()).toBe(true)
expect(wrapper.find('[data-unsupported-field]').exists()).toBe(false)
})
it('shows create-context fields on create and hides update-only tabs', async () => {