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:
@@ -150,3 +150,77 @@ describe('relation manager (D-05, design screen 5)', () => {
|
||||
expect(wrapper.find('[data-action="unlink"]').attributes('disabled')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('relation manager child editing (Phase 12.2, D-11, D-12)', () => {
|
||||
const editable = {
|
||||
...relationSchemaFixture,
|
||||
data: {
|
||||
...relationSchemaFixture.data,
|
||||
view: { ...relationSchemaFixture.data.view, toolbarButtons: ['create', 'update', 'delete', 'link'] },
|
||||
manageForm: [{ name: 'name', label: 'Name', type: 'text' }],
|
||||
},
|
||||
}
|
||||
const RECORDS = `${LINKED}/records`
|
||||
const DELETE = `${LINKED}/delete`
|
||||
|
||||
it('renders the toolbar in order with create as the only primary button', async () => {
|
||||
const { wrapper } = await mountManager({ [`GET ${SCHEMA}`]: { body: editable } })
|
||||
const actions = wrapper.findAll('header [data-action]').map((button) => button.attributes('data-action'))
|
||||
expect(actions).toEqual(['create', 'delete', 'link'])
|
||||
expect(wrapper.find('[data-action="link"]').classes()).not.toContain('bg-primary')
|
||||
expect(wrapper.find('[data-action="create"]').classes()).toContain('bg-primary')
|
||||
})
|
||||
|
||||
it('opens the child modal in update mode from the row button and saves the child', async () => {
|
||||
const first = linkedFixture.all.data[0]!
|
||||
const { calls } = await mountManager({
|
||||
[`GET ${SCHEMA}`]: { body: editable },
|
||||
[`GET ${RECORDS}/${first.id}`]: { body: { data: { ...first }, meta: { labels: {} } } },
|
||||
[`PUT ${RECORDS}/${first.id}`]: { body: { data: { ...first }, meta: { labels: {} } } },
|
||||
})
|
||||
;(document.body.querySelector('[data-row-open]') as HTMLButtonElement).click()
|
||||
await flushPromises()
|
||||
const modal = document.body.querySelector<HTMLElement>('[data-relation-child-modal]')!
|
||||
expect(modal.dataset.mode).toBe('update')
|
||||
const input = modal.querySelector<HTMLInputElement>('#child-members-name')!
|
||||
expect(input.value).toBe(String(first.name))
|
||||
input.value = 'Renamed'
|
||||
input.dispatchEvent(new Event('input'))
|
||||
await flushPromises()
|
||||
;(modal.querySelector('[data-action="save-child"]') as HTMLButtonElement).click()
|
||||
await flushPromises()
|
||||
const [put] = requestsTo(calls, 'PUT', `${RECORDS}/${first.id}`)
|
||||
expect(await put!.clone().json()).toEqual({ name: 'Renamed' })
|
||||
expect(put!.headers.get('X-Child-Session-Key')).toMatch(/^[A-Za-z0-9_-]{43}$/)
|
||||
expect(document.body.querySelector('[data-relation-child-modal]')).toBeNull()
|
||||
})
|
||||
|
||||
it('closes, toasts and reloads when the child is gone', async () => {
|
||||
const first = linkedFixture.all.data[0]!
|
||||
const { calls } = await mountManager({
|
||||
[`GET ${SCHEMA}`]: { body: editable },
|
||||
[`GET ${RECORDS}/${first.id}`]: failure(404, 'Not found'),
|
||||
})
|
||||
;(document.body.querySelector('[data-row-open]') as HTMLButtonElement).click()
|
||||
await flushPromises()
|
||||
expect(document.body.querySelector('[data-relation-child-modal]')).toBeNull()
|
||||
expect(toasts.value[0]).toMatchObject({ text: 'backend::lang.relation.child_gone', tone: 'danger' })
|
||||
expect(requestsTo(calls, 'GET', LINKED)).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('deletes the selected children after a confirmation and keeps them on failure', async () => {
|
||||
const { wrapper, calls } = await mountManager({
|
||||
[`GET ${SCHEMA}`]: { body: editable },
|
||||
[`POST ${DELETE}`]: failure(422, 'Refused'),
|
||||
})
|
||||
expect(wrapper.find('[data-action="delete"]').attributes('disabled')).toBeDefined()
|
||||
await wrapper.find('tbody [role="checkbox"]').trigger('click')
|
||||
await wrapper.find('[data-action="delete"]').trigger('click')
|
||||
await flushPromises()
|
||||
;(document.body.querySelector('[data-confirm-dialog] [data-action="confirm"]') as HTMLElement).click()
|
||||
await flushPromises()
|
||||
expect(requestsTo(calls, 'POST', DELETE)).toHaveLength(1)
|
||||
expect(toasts.value[0]).toMatchObject({ text: 'Refused', tone: 'danger' })
|
||||
expect(wrapper.find('tbody [role="checkbox"]').attributes('aria-checked')).toBe('true')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user