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

@@ -5,8 +5,13 @@ import { t, tc } from '../../app/i18n'
// 422 banner (design screen 4): "Nie udało się zapisać. Popraw N pola…".
// Messages of keys that are not form fields are listed here, so no server
// message is lost.
const props = defineProps<{ errors: Record<string, string[]>; fieldNames: string[] }>()
// message is lost. With `forbidden` (UI-SPEC S6: a save the server refused
// with 403) the same geometry shows that text instead; it stays until the
// next save attempt, and field messages still render on their fields.
const props = withDefaults(
defineProps<{ errors: Record<string, string[]>; fieldNames: string[]; forbidden?: string | null }>(),
{ forbidden: null },
)
const count = computed(() => Object.keys(props.errors).length)
const orphans = computed(() =>
@@ -18,7 +23,19 @@ const orphans = computed(() =>
<template>
<div
v-if="count > 0"
v-if="forbidden"
role="alert"
data-forbidden-banner
class="flex items-start gap-3 rounded-inner bg-danger-soft px-[18px] py-3.5 text-danger"
>
<CircleAlert :size="20" class="mt-px shrink-0" aria-hidden="true" />
<div class="flex min-w-0 flex-col gap-1">
<p class="[overflow-wrap:anywhere]">{{ forbidden }}</p>
<p v-for="(text, index) in orphans" :key="index" class="text-[13px] [overflow-wrap:anywhere]">{{ text }}</p>
</div>
</div>
<div
v-else-if="count > 0"
role="alert"
data-error-banner
class="flex items-start gap-3 rounded-inner bg-danger-soft px-[18px] py-3.5 text-danger"