GrResponseErrorBanner

  • ядро
  • Обратная связь
  • API · 13 props

Берут, когда запрос упал.

import { GrResponseErrorBanner } from '@feugene/granularity/components/GrResponseErrorBanner'

Обзор

Когда брать

  • запрос упал — сеть, 500, 422 с ошибками полей: тип ошибки определяется сам и меняет заголовок;
  • ошибок по полям несколько — они печатаются списком с подписями вместо одной общей фразы;
  • запрос можно повторить — canRetry даёт кнопку рядом с сообщением;
  • нужен статус HTTP — бейдж кода помогает поддержке, не мешая обычному пользователю.

Когда взять другое

НужноБерите
Сообщение не про ответ сервераGrAlert
Короткое уведомление о результатеGrToaster
Ошибка одного поля формыGrFormField
Ошибка внутри диалогаGrConfirmDialog / GrPromptDialog
Данных нет, но это не ошибкаGrEmptyState

Варианты все тона и виды — настоящим компонентом

tone

  • primary
  • neutral
  • success
  • warning
  • danger
  • info
  • slate
  • azure

Матрица живая: наводите и нажимайте. Состояний наведения, фокуса и нажатия здесь нет — они за браузером, а не за пропом.

Playground контролы выводятся из API, а не пишутся руками

Сцена

Загружается…

0 изменено· состояние — в адресе

Примеры 3 у каждого — живое демо и его код

  • Ответ 422, разложенный по полям

    useResponseError принимает сам Response: цепочка парсеров узнаёт формат Laravel, род ошибки становится «validation» с тоном предупреждения, а у каждого поля своя строка. field-labels превращает ключи API в слова — «customer.vat_id» становится «Номером НДС».

    Invoice INV-0418 · customer details
    Applies from the next invoice
    Invoices already sent keep the details they were issued with.
  • Отчёт, который не загрузился

    Упавший fetch классифицируется как «network»: тон опасности и с can-retry кнопка повтора — повтор вполне может сработать. retry возвращает ошибку, и страница выполняет тот же запрос снова.

    Revenue by plan

    The report is built on request.

  • Отказ в формате RFC 7807

    Ответ application/problem+json разбирает свой пресет: заголовок и подробности становятся сообщением. 403 — ошибка «client», повтор ничего не изменит, поэтому баннер не предлагает повторить — только причина и бейдж статуса для поддержки.

    Members · Northwind Traders

    Removing a member signs them out on every device. Invoices they created stay.

    MOMina OrtizOwner · [email protected]
    JWJonas WeberAdmin · [email protected]

API порождается из пакета · разойтись не может

Props

PropTypeпо умолчаниюОписание
tone
"primary" | "neutral" | "success" | "warning" | "danger" | ""primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined
undefinedЖёсткий оверрайд тона. Бьёт `toneByKind`.
textsPartial<ResponseErrorTexts> | undefined{}Частичный override текстов. Мерджится с `DEFAULT_RESPONSE_ERROR_TEXTS`.
toneByKind
Partial<Record<ResponseErrorKind, "primary" | "neutral" | "sPartial<Record<ResponseErrorKind, "primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure">> | undefined
{}Маппинг `kind` → `tone`. Мерджится с `DEFAULT_TONE_BY_KIND`.
showDetailsboolean | undefinedtrueПоказывать ли блок `details`/`fieldErrors` под основным сообщением.
showFieldLabelsboolean | undefinedtrueПрефиксить ли `field:` перед сообщениями по полям.
fieldLabelsRecord<string, string> | undefined{}Человекочитаемые подписи полей (`{ file: 'Файл' }`).
dedupeDetailsboolean | undefinedtrueУдалять дубликаты сообщений между `message` и `details`.
canRetryboolean | undefinedfalseПоказывать ли кнопку «Повторить».
canDismissboolean | undefinedtrueПоказывать ли кнопку «Скрыть».
autoHideKindsResponseErrorKind[] | undefinedDEFAULT_AUTO_HIDE_KINDSКакие `kind` не рендерить вообще (баннер скрывается).
showStatusboolean | undefinedtrueПоказывать ли HTTP-status бейдж.
testIdPrefixstring | undefined"response-error"Префикс `data-testid`.
errorобязательныйResponseErrorInfo | null—Готовая структура ошибки. Если `null` — баннер не рендерится.

События

EventСигнатураОписание
retry(event: "retry", error: ResponseErrorInfo): void—
dismiss(event: "dismiss"): void—

Типы

Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.

Импорт
import type { UseResponseErrorOptions, UseResponseErrorReturn, ResponseErrorParserPresets } from '@feugene/granularity/components/GrResponseErrorBanner'

Файлdist/types/components/GrResponseErrorBanner/useResponseError.d.ts

UseResponseErrorOptionstype

Опции для useResponseError. Все поля опциональны и поддерживают **реактивные геттеры** (() => value) — чтобы пробрасывать пропсы родителя без потери реактивности.

type UseResponseErrorOptions = {
    /** Цепочка парсеров. По умолчанию — `defaultResponseErrorParsers`. */
    parsers?: () => ResponseErrorParser[] | undefined;
UseResponseErrorReturntype

Публичный возвращаемый API.

type UseResponseErrorReturn = {
    /** Текущее состояние ошибки. `null` — баннер скрыт. */
    currentError: Ref<ResponseErrorInfo | null>;
    /** `currentError.value !== null`. */
    isVisible: ComputedRef<boolean>;
    /** Последний «сырой» вход (для retry/логов). */
    lastRaw: Ref<unknown>;
    /**
     * Главное API: классифицирует raw, сохраняет в state, возвращает info
     * (или `null`, если `kind` в `autoHideKinds`). Возвращает Promise,
     * т.к. нормализатор поддерживает `Response.json()` (async).
     */
    setRaw: (raw: unknown, meta?: Record<string, unknown>) => Promise<ResponseErrorInfo | null>;
useResponseErrorfunction

Stateful-композабл вокруг createResponseErrorClassifier. Хранит currentError, инкапсулирует setRaw/retry/dismiss. Подходит для типичного сценария «поймал ошибку запроса → показал баннер → ретрай/скрыть».

function useResponseError(options?: UseResponseErrorOptions): UseResponseErrorReturn;

Файлdist/types/components/GrResponseErrorBanner/parsers/createResponseErrorClassifier.d.ts

coreResponseErrorParsersconst

Универсальное «ядро» парсеров — транспортно-независимый набор, безопасный для любого бэкенда (Laravel / Symfony / Go / …). Не делает формат-специфичных допущений: разбирает только отмену, сетевые ошибки, HTTP-статус и «плоское» сообщение из тела. Используется как дефолт в useDialogService. Порядок: abort → network → httpStatus → plainMessage.

const coreResponseErrorParsers: ResponseErrorParser[];
ResponseErrorParserPresetstype

Именованные пресеты-строительные блоки для удобной композиции цепочки парсеров без ручного импорта каждого из них. - core — универсальное ядро (см. coreResponseErrorParsers). - all — полная дефолтная цепочка (defaultResponseErrorParsers). - остальные — серверо-специфичные парсеры, подключаемые осознанно.

type ResponseErrorParserPresets = {
    core: ResponseErrorParser[];
    all: ResponseErrorParser[];
    laravel: ResponseErrorParser;
    problemDetails: ResponseErrorParser;
    jsonApi: ResponseErrorParser;
    fileValidation: ResponseErrorParser;
};
extendDefaultParsersfunction

Добавляет пользовательские парсеры в начало дефолтной цепочки.

function extendDefaultParsers(extra: ResponseErrorParser[]): ResponseErrorParser[];
CreateResponseErrorClassifierOptionstype
type CreateResponseErrorClassifierOptions = {
    parsers?: ResponseErrorParser[];
    texts?: Partial<ResponseErrorTexts>;
    /** Имя поля основного сообщения в body (для `plainMessageParser`). */
    messageKey?: string;
};
createResponseErrorClassifierfunction

Фабрика классификатора. Возвращает функцию (raw, meta?) => Promise<ResponseErrorInfo>, которая: 1. нормализует вход через normalizeError, 2. строит ResponseErrorContext, 3. прогоняет цепочку парсеров (с поддержкой stop), 4. собирает финальный ResponseErrorInfo с фолбэками из texts.

function createResponseErrorClassifier(options?: CreateResponseErrorClassifierOptions): (raw: unknown, meta?: Record<string, unknown>) => Promise<ResponseErrorInfo>;
export declare function resolveResponseErrorTitle(kind: ResponseErrorKind, texts: ResponseErrorTexts): string;

Файлdist/types/components/GrResponseErrorBanner/parsers/normalizeError.d.ts

normalizeErrorfunction

Универсально приводит «сырой» вход к {@link NormalizedRawError}. Поддерживаемые источники: - **axios** — AxiosError с response.status/response.data/response.headers, либо request без response + code === 'ERR_NETWORK', либо code === 'ERR_CANCELED'. - **fetch Response** — объект Response с ok=false. Тело читается через clone().json() (с фолбэком на .text()); поэтому функция асинхронная. - **fetch TypeError** — TypeError('Failed to fetch') ⇒ isNetwork=true. - **{ response: Response, body: unknown }** — если потребитель уже прочитал тело сам. - **XMLHttpRequest** / { status, responseText, response } — пара полей. - **AbortError / DOMException** ⇒ isAbort=true. - **Error/строка/произвольный объект** — кладётся в body/raw как есть. Цель — отделить «откуда пришла ошибка» от «как её классифицировать»: парсеры работают только с нормализованным ResponseErrorContext, не зная о транспорте.

function normalizeError(raw: unknown): Promise<NormalizedRawError>;

Файлdist/types/components/GrResponseErrorBanner/responseError.types.d.ts

ResponseErrorKindtype

Категория ошибки. Жёстко разделены по семантике (см. DEFAULT_TONE_BY_KIND). - network — транспортная проблема: нет интернета, DNS, CORS-preflight, fetch TypeError, axios ERR_NETWORK. **Ответа от сервера нет.** - aborted — запрос отменён клиентом (AbortController, axios CanceledError). - validation — ошибка валидации формы/файлов; есть структурированные поля или статус 400/422. UX: «исправь поле и повтори». - client — прочие 4xx (auth/forbidden/not-found/conflict/rate-limit) — проблема запроса, retry обычно не помогает. - server — 5xx, проблема на сервере; retry разумен. - unknown — ничего не сматчилось, fallback.

type ResponseErrorKind = 'network' | 'aborted' | 'validation' | 'client' | 'server' | 'unknown';
ResponseErrorTonetype

Визуальный тон баннера — та же шкала, что у всех компонентов пакета.

type ResponseErrorTone = GrTone;
ResponseErrorFieldErrortype

Ошибка по конкретному полю (Laravel-style / JSON:API pointer / RFC 7807 errors).

type ResponseErrorFieldError = {
    /** Имя поля. Для Laravel — ключ из `errors`. Для JSON:API — последний сегмент `source.pointer`. */
    field: string;
    /** Сообщения, относящиеся к этому полю. */
    messages: string[];
};
ResponseErrorInfotype

Готовая к показу структура. Это то, что баннер принимает в error-проп.

type ResponseErrorInfo = {
    kind: ResponseErrorKind;
    /** HTTP-статус, если известен. */
    status?: number;
    /** Основной заголовок-сообщение для пользователя. */
    message: string;
    /** Плоский список дополнительных сообщений под `message`. */
    details?: string[];
    /** Структурированные ошибки по полям (если сервер вернул JSON-объект `errors`). */
    fieldErrors?: ResponseErrorFieldError[];
    /** Оригинальный объект ошибки/ответа (для логов/retry). */
    raw: unknown;
    /** Произвольный контекст, переданный потребителем (например, `{ files }`). */
    meta?: Record<string, unknown>;
    /**
     * `message` не пришёл ни от одного парсера, и классификатор подставил свой
     * текст по `kind`. Флаг нужен баннеру: только такое сообщение он вправе
     * заменить переводом. Опознание фолбэка сравнением строк давало ложное
     * срабатывание на сервере, вернувшем дословно тот же текст.
     */
    isFallbackMessage?: boolean;
};
NormalizedRawErrortype

Нормализованный «сырой» результат запроса. Заполняется normalizeError единообразно для axios / fetch / xhr / голого Error / строки.

type NormalizedRawError = {
    /** Оригинал, как пришёл от потребителя. */
    raw: unknown;
    /** HTTP-статус (если был ответ). */
    status?: number;
    /** Уже распарсенное тело ответа (JSON-объект, строка). */
    body?: unknown;
    /** Заголовки ответа (lower-case keys). */
    headers?: Record<string, string>;
    /** Признак отмены (`AbortError` / axios `isCancel`). */
    isAbort?: boolean;
    /** Признак транспортной ошибки без ответа. */
    isNetwork?: boolean;
};
ResponseErrorContexttype

Контекст, который классификатор передаёт каждому парсеру. raw — исходный объект (для duck-typing). status/body/headers/isAbort/isNetwork — нормализованные поля. meta — произвольный пользовательский контекст (например, { files, formId }). texts — мёрджнутые тексты, парсер может использовать дефолты как fallback.

type ResponseErrorContext = NormalizedRawError & {
    meta: Record<string, unknown>;
    texts: ResponseErrorTexts;
};
ResponseErrorParserResulttype

Результат работы парсера. Любое поле опционально — классификатор сливает результаты по очереди (последний непустой выигрывает). stop: true — прервать цепочку, никакие следующие парсеры не вызовутся.

type ResponseErrorParserResult = {
    kind?: ResponseErrorKind;
    status?: number;
    message?: string;
    details?: string[];
    fieldErrors?: ResponseErrorFieldError[];
    stop?: boolean;
};
ResponseErrorParsertype

Сигнатура парсера.

type ResponseErrorParser = (ctx: ResponseErrorContext) => ResponseErrorParserResult | null | undefined;
/**
 * Тексты-фолбэки для каждого `kind` + лейблы кнопок.
 * Все строки переопределяются через проп `texts` (частичный мердж).
 */
export type ResponseErrorTexts = {
ResponseErrorTextstype

Тексты-фолбэки для каждого kind + лейблы кнопок. Все строки переопределяются через проп texts (частичный мердж).

type ResponseErrorTexts = {
    networkTitle: string;
    networkMessage: string;
    abortedTitle: string;
    abortedMessage: string;
    validationTitle: string;
    validationMessage: string;
    clientTitle: string;
    clientMessage: string;
    serverTitle: string;
    serverMessage: string;
    unknownTitle: string;
    unknownMessage: string;
    /** Подпись бейджа HTTP-статуса. `{status}` подставляется компонентом. */
    statusLabel: string;
    retryLabel: string;
    dismissLabel: string;
};

Файлdist/types/components/GrResponseErrorBanner/GrFormErrorBanner.vue.d.ts

GrFormErrorBannerPropsinterface
interface GrFormErrorBannerProps {
    error: ResponseErrorInfo | null;
    fieldLabels?: Record<string, string>;
    texts?: Partial<ResponseErrorTexts>;
    tone?: ResponseErrorTone;
    toneByKind?: Partial<Record<ResponseErrorKind, ResponseErrorTone>>;
    canRetry?: boolean;
    canDismiss?: boolean;
    autoHideKinds?: ResponseErrorKind[];
    testIdPrefix?: string;
}
GrFormErrorBannerEmitsinterface
interface GrFormErrorBannerEmits {
    (e: 'retry', error: ResponseErrorInfo): void;
    (e: 'dismiss'): void;
}

Файлdist/types/components/GrResponseErrorBanner/GrResponseErrorBanner.vue.d.ts

GrResponseErrorBannerPropsinterface
interface GrResponseErrorBannerProps {
    /** Готовая структура ошибки. Если `null` — баннер не рендерится. */
    error: ResponseErrorInfo | null;
    /** Частичный override текстов. Мерджится с `DEFAULT_RESPONSE_ERROR_TEXTS`. */
    texts?: Partial<ResponseErrorTexts>;
    /** Жёсткий оверрайд тона. Бьёт `toneByKind`. */
    tone?: ResponseErrorTone;
    /** Маппинг `kind` → `tone`. Мерджится с `DEFAULT_TONE_BY_KIND`. */
    toneByKind?: Partial<Record<ResponseErrorKind, ResponseErrorTone>>;
    /** Показывать ли блок `details`/`fieldErrors` под основным сообщением. */
    showDetails?: boolean;
    /** Префиксить ли `field:` перед сообщениями по полям. */
    showFieldLabels?: boolean;
    /** Человекочитаемые подписи полей (`{ file: 'Файл' }`). */
    fieldLabels?: Record<string, string>;
    /** Удалять дубликаты сообщений между `message` и `details`. */
    dedupeDetails?: boolean;
    /** Показывать ли кнопку «Повторить». */
    canRetry?: boolean;
    /** Показывать ли кнопку «Скрыть». */
    canDismiss?: boolean;
    /** Какие `kind` не рендерить вообще (баннер скрывается). */
    autoHideKinds?: ResponseErrorKind[];
    /** Показывать ли HTTP-status бейдж. */
    showStatus?: boolean;
    /** Префикс `data-testid`. */
    testIdPrefix?: string;
}
GrResponseErrorBannerEmitsinterface
interface GrResponseErrorBannerEmits {
    /** Нажата «Повторить» (`canRetry`); приходит текущая ошибка — повторить запрос, что её дал. */
    (e: 'retry', error: ResponseErrorInfo): void;
    /** Нажат крестик (`canDismiss`). Сам баннер не прячется: обнулить `error` — забота экрана. */
    (e: 'dismiss'): void;
}

Файлdist/types/components/GrResponseErrorBanner/GrUploadErrorBanner.vue.d.ts

GrUploadErrorBannerPropsinterface
interface GrUploadErrorBannerProps {
    error: ResponseErrorInfo | null;
    /** Файлы, которые пытались загрузить — пробрасываются в `retry` payload. */
    files?: File[];
    texts?: Partial<ResponseErrorTexts>;
    tone?: ResponseErrorTone;
    toneByKind?: Partial<Record<ResponseErrorKind, ResponseErrorTone>>;
    canRetry?: boolean;
    canDismiss?: boolean;
    autoHideKinds?: ResponseErrorKind[];
    testIdPrefix?: string;
}
GrUploadErrorBannerEmitsinterface
interface GrUploadErrorBannerEmits {
    (e: 'retry', payload: {
        error: ResponseErrorInfo;
        files: File[];
    }): void;
    (e: 'dismiss'): void;
}

Использование из прозы библиотеки

Три слоя
  1. normalizeError — приводит axios / fetch Response / XMLHttpRequest / голый Error / строку к общему виду: статус, разобранное тело, заголовки, признаки отмены и сетевой ошибки. Тело Response читается клоном, поэтому потребитель может прочитать его сам;
  2. цепочка парсеров — от нормализованного контекста к ResponseErrorInfo (kind, message, details, fieldErrors). Порядок значим, парсер может остановить цепочку (stop);
  3. баннер — показ: тон по kind, тексты из локали, дедупликация деталей.
Цепочка парсеров

По умолчанию — универсальное ядро (coreResponseErrorParsers): abort, сеть, HTTP-статус, plain-сообщение. Оно работает на транспортном уровне и не делает допущений о формате тела, поэтому не срабатывает ложно на «почти-Laravel» ответах.

Серверо-специфичные парсеры подключаются осознанно:

ts
const { setRaw } = useResponseError({
  parsers: () => extendDefaultParsers([myParser]),
})

// или через builder в опциях диалогов и баннера
errorParsers: presets => [...presets.core, presets.laravel]

Готовые пресеты: laravel, problemDetails (RFC 7807), jsonApi, fileValidation (клиентская проверка файлов из GrFileUpload).

Тексты, i18n и флаг фолбэка

Тексты берутся из локали (gr.responseError.*), поверх — проп texts. Подпись бейджа статуса — тот же механизм: statusLabel со вставкой {status}.

Когда ни один парсер не дал сообщения, его подставляет классификатор и помечает isFallbackMessage: true. Баннер заменяет переводом только такое сообщение. Опознание фолбэка сравнением строк, которое здесь было раньше, выбрасывало ответ сервера, если тот дословно совпал с дефолтом (а "Network error." сервер вернуть вполне может).

Отсюда контракт парсера: message заполняется только тем, что нашлось в ответе. Общий текст по kind — работа классификатора, и парсеру не надо его подставлять; иначе сообщение приедет без флага, и перевести его будет уже нельзя. Парсеры, знающие только тип ошибки (httpStatus, abort, network), сообщение не заполняют вовсе.

Сообщение транспортной ошибки (Request failed with status 500 у axios) сообщением сервера не считается: оно берётся, только если ответа не было вовсе. Иначе пользователь видел бы техническую фразу на английском вместо переведённого текста.

Тон и доступность

kind → тон по умолчанию: validation/client — warning, network/ server/unknown — danger, aborted — info. Перебивается точечно (toneByKind) или целиком (tone).

Роль наследуется от GrAlert: warning и danger объявляются как role="alert" (перебивают речь диктора), остальные — role="status" (ждут паузы). Отдельной настройки у баннера нет намеренно — она есть у GrAlert.

autoHideKinds прячет баннер целиком для перечисленных kind: типовой случай — тихо проглатывать aborted.

Пресеты

GrFormErrorBanner и GrUploadErrorBanner — тонкие обёртки с преднастройками: формам не нужен повтор и нужны подписи полей, загрузке нужен повтор и контекст файлов (его пресет отдаёт в retry как { error, files } — базовый баннер отдаёт саму ошибку).

Своей разметки они не добавляют и живут в папке базового компонента осознанно: отдельная единица гранулярности не даст ничего — ни своего CSS, ни своего safelist, — а стоить будет двух лишних entry в сборке. Импортируются из корня пакета.