Files
summercms/admin/src/components/form/FormErrorBanner.vue
Jakub Zych 71073bc8a2 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
2026-10-04 23:53:34 +02:00

53 lines
1.9 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue'
import { CircleAlert } from '@lucide/vue'
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. 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(() =>
Object.entries(props.errors)
.filter(([name]) => !props.fieldNames.includes(name))
.flatMap(([, messages]) => messages),
)
</script>
<template>
<div
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"
>
<CircleAlert :size="20" class="mt-px shrink-0" aria-hidden="true" />
<div class="flex flex-col gap-1">
<p>
<strong>{{ t('backend::lang.form.error_title') }}</strong>
{{ tc('backend::lang.form.error_fields', count) }}
</p>
<p v-for="(text, index) in orphans" :key="index" class="text-[13px]">{{ text }}</p>
</div>
</div>
</template>