feat(12.1-01): cabana.ForbiddenError answers a refused write with 403

- hooks and bulk, record, toolbar and widget actions may return it
- 403 forbidden with the localized message and field details; the write's
  transaction is rolled back; other errors stay the opaque 500
- form shows a refused save as a persistent banner and keeps the values;
  a refused delete is a toast
- smoke tests, OpenAPI notes, dist, README, docs
This commit is contained in:
Jakub Zych
2026-10-04 23:53:34 +02:00
parent 61d5fc72ad
commit 71073bc8a2
23 changed files with 486 additions and 48 deletions

View File

@@ -192,7 +192,9 @@ describe('edit a record', () => {
it('shows a danger toast with the envelope message for other errors', async () => {
const { wrapper } = await mountApp('/acme/demo/widgets/1', {
...formRoutes,
[`PUT ${RECORD}`]: { status: 403, body: { error: { code: 'forbidden', message: 'Forbidden.', details: {} } } },
// Not a 422 and not a 403: a refused save (403) is the forbidden banner
// (Phase 12.1, UI-SPEC S6), covered in actions.smoke.test.ts.
[`PUT ${RECORD}`]: { status: 409, body: { error: { code: 'conflict', message: 'The record changed.', details: {} } } },
})
await wrapper.find('[data-action="save"]').trigger('click')
@@ -200,8 +202,9 @@ describe('edit a record', () => {
const toast = wrapper.find('[data-tone="danger"]')
expect(toast.attributes('role')).toBe('alert')
expect(toast.text()).toContain('Forbidden.')
expect(toast.text()).toContain('The record changed.')
expect(wrapper.find('[data-error-banner]').exists()).toBe(false)
expect(wrapper.find('[data-forbidden-banner]').exists()).toBe(false)
})
it('renders an unknown field type as the unsupported box instead of breaking the form', async () => {