feat(12.2-04): edit related records in modals and defer relation work on new records
- RelationManager renders create/link/delete/unlink in declared order with one primary, opens the child modal (update or view form) or the pivot modal on row click, and deletes selected children behind a busy confirm - RelationChildModal creates and edits children with its own session key (X-Child-Session-Key) so uploads and dates work inside it - RelationPivotModal edits link details; the picker links one record with its pivot values when the relation has a pivot form - deferrable managers render on the create screen with owner id 0, the form's X-Session-Key and the pending note, and mark the form dirty - the registry resolves RelationManager lazily (child forms close an import cycle); DataTable gains openable rows and a trailing cell - relation lang keys in en and pl; rebuilt boardwalk dist
This commit is contained in:
@@ -158,3 +158,68 @@ describe('relation picker modal', () => {
|
||||
expect(queryOf(requestsTo(calls, 'GET', CANDIDATES).at(-1)!).get('page')).toBe('1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pivot link step (Phase 12.2, D-14)', () => {
|
||||
const pivotSchema = {
|
||||
...relationSchemaFixture.data,
|
||||
pivotForm: [{ name: 'role', label: 'Role', type: 'text' }],
|
||||
}
|
||||
|
||||
async function openPivot(overrides: Record<string, Route> = {}) {
|
||||
const calls = mockApi(routes(overrides))
|
||||
const wrapper = mount(RelationPickerModal, {
|
||||
props: { open: true, source, recordId: 1, relation: 'members', schema: pivotSchema },
|
||||
attachTo: document.body,
|
||||
})
|
||||
await flushPromises()
|
||||
await wait(0)
|
||||
await flushPromises()
|
||||
return { wrapper, calls }
|
||||
}
|
||||
|
||||
const nextButton = () => dialog()!.querySelector<HTMLButtonElement>('[data-action="next"]')!
|
||||
|
||||
it('selects one record, asks for its pivot values and links it with them', async () => {
|
||||
const { wrapper, calls } = await openPivot({ [`POST ${LINK}`]: { body: { data: { linked: 1 }, meta: {} } } })
|
||||
expect(dialog()!.querySelector('[role="listbox"]')!.getAttribute('aria-multiselectable')).toBe('false')
|
||||
expect(nextButton().disabled).toBe(true)
|
||||
await click(options()[0])
|
||||
await click(options()[1])
|
||||
expect(options().filter((option) => option.getAttribute('aria-selected') === 'true')).toHaveLength(1)
|
||||
|
||||
await click(nextButton())
|
||||
expect(dialog()!.querySelector('[data-pivot-step]')).not.toBeNull()
|
||||
const role = dialog()!.querySelector<HTMLInputElement>('#link-members-role')!
|
||||
role.value = 'editor'
|
||||
role.dispatchEvent(new Event('input'))
|
||||
await flushPromises()
|
||||
|
||||
await click(confirmButton())
|
||||
const [post] = requestsTo(calls, 'POST', LINK)
|
||||
expect(await post!.clone().json()).toEqual({ ids: [candidatesFixture.page1.data[1]!.id], pivot: { role: 'editor' } })
|
||||
expect(wrapper.emitted('linked')).toEqual([[1]])
|
||||
})
|
||||
|
||||
it('goes back to the list with the selection kept', async () => {
|
||||
await openPivot()
|
||||
await click(options()[1])
|
||||
await click(nextButton())
|
||||
await click(dialog()!.querySelector('[data-action="back"]'))
|
||||
expect(dialog()!.querySelector('[data-pivot-step]')).toBeNull()
|
||||
expect(options()[1]!.getAttribute('aria-selected')).toBe('true')
|
||||
})
|
||||
|
||||
it('keeps pivot errors in the dialog on a 422', async () => {
|
||||
const { wrapper } = await openPivot({
|
||||
[`POST ${LINK}`]: {
|
||||
status: 422,
|
||||
body: { error: { code: 'validation_failed', message: 'Invalid', details: { role: ['The role is invalid.'] } } },
|
||||
},
|
||||
})
|
||||
await click(options()[0])
|
||||
await click(nextButton())
|
||||
await click(confirmButton())
|
||||
expect(dialog()!.querySelector('[data-pivot-step]')!.textContent).toContain('The role is invalid.')
|
||||
expect(wrapper.emitted('update:open')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user