From 8b962a7ec788f863e92f459eb109a27d7e82f22a Mon Sep 17 00:00:00 2001 From: benrejebmoh Date: Tue, 30 Sep 2025 10:43:38 +0200 Subject: [PATCH 1/3] first working version of extendable snackbar --- src/components/index.ts | 1 + src/components/menus/custom-nested-menu.tsx | 7 +- .../snackbarProvider/SnackbarProvider.tsx | 13 +-- .../snackbars/BackendErrorSnackbarContent.tsx | 87 +++++++++++++++++++ src/components/snackbars/index.ts | 1 + src/hooks/useSnackMessage.ts | 2 +- src/services/utils.ts | 44 ++++++++-- src/utils/backendErrors.ts | 72 +++++++++++++++ src/utils/index.ts | 1 + 9 files changed, 212 insertions(+), 16 deletions(-) create mode 100644 src/components/snackbars/BackendErrorSnackbarContent.tsx create mode 100644 src/components/snackbars/index.ts create mode 100644 src/utils/backendErrors.ts diff --git a/src/components/index.ts b/src/components/index.ts index e5fecb8e4..b37b9f6b4 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -20,6 +20,7 @@ export * from './grid'; export * from './inputs'; export * from './multipleSelectionDialog'; export * from './overflowableText'; +export * from './snackbars'; export * from './snackbarProvider'; export * from './topBar'; export * from './treeViewFinder'; diff --git a/src/components/menus/custom-nested-menu.tsx b/src/components/menus/custom-nested-menu.tsx index 68e37721e..c8d1cdf2d 100644 --- a/src/components/menus/custom-nested-menu.tsx +++ b/src/components/menus/custom-nested-menu.tsx @@ -4,7 +4,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { PropsWithChildren, useState } from 'react'; +import { useState } from 'react'; import { NestedMenuItem, NestedMenuItemProps } from 'mui-nested-menu'; import { Box, MenuItem, type MenuItemProps } from '@mui/material'; import { mergeSx, type SxStyle, type MuiStyles } from '../../utils/styles'; @@ -24,9 +24,10 @@ const styles = { }, } as const satisfies MuiStyles; -interface CustomNestedMenuItemProps extends PropsWithChildren, Omit { +type CustomNestedMenuItemProps = Omit & { + children?: NestedMenuItemProps['children']; sx?: SxStyle; -} +}; export function CustomNestedMenuItem({ sx, children, ...other }: Readonly) { const [subMenuActive, setSubMenuActive] = useState(false); diff --git a/src/components/snackbarProvider/SnackbarProvider.tsx b/src/components/snackbarProvider/SnackbarProvider.tsx index 736fb7c50..b9d91a41b 100644 --- a/src/components/snackbarProvider/SnackbarProvider.tsx +++ b/src/components/snackbarProvider/SnackbarProvider.tsx @@ -5,10 +5,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { useRef } from 'react'; import { IconButton, styled } from '@mui/material'; import { Clear as ClearIcon } from '@mui/icons-material'; -import { type SnackbarKey, SnackbarProvider as OrigSnackbarProvider, type SnackbarProviderProps } from 'notistack'; +import { + type SnackbarKey, + SnackbarProvider as OrigSnackbarProvider, + type SnackbarProviderProps, + closeSnackbar as closeSnackbarFromNotistack, +} from 'notistack'; import type { MuiStyles } from '../../utils/styles'; const StyledOrigSnackbarProvider = styled(OrigSnackbarProvider)(() => ({ @@ -29,11 +33,9 @@ const styles = { /* A wrapper around notistack's SnackbarProvider that provides defaults props */ export function SnackbarProvider(props: SnackbarProviderProps) { - const ref = useRef(null); - const action = (key: SnackbarKey) => ( ref.current?.closeSnackbar(key)} + onClick={() => closeSnackbarFromNotistack(key)} aria-label="clear-snack" size="small" sx={styles.buttonColor} @@ -44,7 +46,6 @@ export function SnackbarProvider(props: SnackbarProviderProps) { return ( ; + details: BackendErrorDetails; + showDetailsLabel: string; + hideDetailsLabel: string; +} + +const detailOrder: BackendErrorDetailKey[] = ['service', 'message', 'path']; + +export const BackendErrorSnackbarContent = forwardRef( + ({ message, detailsLabel, detailLabels, details, showDetailsLabel, hideDetailsLabel }, ref) => { + const theme = useTheme(); + const [isExpanded, setIsExpanded] = useState(false); + + const renderedDetails = useMemo( + () => + detailOrder + .map((key) => ({ key, label: detailLabels[key], value: details[key] })) + .filter((detail) => detail.label), + [detailLabels, details] + ); + + return ( + + + {message} + + {renderedDetails.length > 0 ? ( + <> + + + + + {detailsLabel} + + {renderedDetails.map(({ key, label, value }) => ( + + {label}: {value} + + ))} + + + + ) : null} + + } + /> + ); + } +); + +BackendErrorSnackbarContent.displayName = 'BackendErrorSnackbarContent'; diff --git a/src/components/snackbars/index.ts b/src/components/snackbars/index.ts new file mode 100644 index 000000000..37a93eb8f --- /dev/null +++ b/src/components/snackbars/index.ts @@ -0,0 +1 @@ +export * from './BackendErrorSnackbarContent'; diff --git a/src/hooks/useSnackMessage.ts b/src/hooks/useSnackMessage.ts index e71166d02..7879acd93 100644 --- a/src/hooks/useSnackMessage.ts +++ b/src/hooks/useSnackMessage.ts @@ -10,7 +10,7 @@ import { BaseVariant, OptionsObject, closeSnackbar as closeSnackbarFromNotistack import { IntlShape } from 'react-intl'; import { useIntlRef } from './useIntlRef'; -interface SnackInputs extends Omit { +export interface SnackInputs extends Omit { messageTxt?: string; messageId?: string; messageValues?: Record; diff --git a/src/services/utils.ts b/src/services/utils.ts index 042e7f9ef..8f1a51efd 100644 --- a/src/services/utils.ts +++ b/src/services/utils.ts @@ -6,6 +6,11 @@ */ import { getUserToken } from '../redux/commonStore'; +import { + isBackendErrorLike, + normalizeBackendErrorPayload, + type HttpErrorWithBackendDetails, +} from '../utils/backendErrors'; const parseError = (text: string) => { try { @@ -26,18 +31,45 @@ const prepareRequest = (init: RequestInit | undefined, token?: string) => { return initCopy; }; +const isRecord = (value: unknown): value is Record => typeof value === 'object' && value !== null; + const handleError = (response: Response) => { return response.text().then((text: string) => { const errorName = 'HttpResponseError : '; - const errorJson = parseError(text); - let customError: Error & { status?: number }; - if (errorJson && errorJson.status && errorJson.error && errorJson.message) { + const errorJson = parseError(text) as unknown; + let customError: HttpErrorWithBackendDetails; + + if (isBackendErrorLike(errorJson)) { + const backendError = normalizeBackendErrorPayload(errorJson); + const status = backendError.status ?? response.status; + const jsonRecord = errorJson as Record; + const errorLabel = + typeof jsonRecord.error === 'string' + ? (jsonRecord.error as string) + : (backendError.errorCode ?? response.statusText); + const message = + backendError.message ?? + (typeof jsonRecord.message === 'string' ? (jsonRecord.message as string) : text); customError = new Error( - `${errorName + errorJson.status} ${errorJson.error}, message : ${errorJson.message}` - ); + `${errorName + status} ${errorLabel}, message : ${message}` + ) as HttpErrorWithBackendDetails; + customError.status = status; + customError.backendError = backendError; + } else if ( + isRecord(errorJson) && + typeof errorJson.status === 'number' && + (typeof errorJson.error === 'string' || typeof errorJson.message === 'string') + ) { + const errorLabel = typeof errorJson.error === 'string' ? errorJson.error : response.statusText; + const message = typeof errorJson.message === 'string' ? errorJson.message : text; + customError = new Error( + `${errorName + errorJson.status} ${errorLabel}, message : ${message}` + ) as HttpErrorWithBackendDetails; customError.status = errorJson.status; } else { - customError = new Error(`${errorName + response.status} ${response.statusText}, message : ${text}`); + customError = new Error( + `${errorName + response.status} ${response.statusText}, message : ${text}` + ) as HttpErrorWithBackendDetails; customError.status = response.status; } throw customError; diff --git a/src/utils/backendErrors.ts b/src/utils/backendErrors.ts new file mode 100644 index 000000000..e62883519 --- /dev/null +++ b/src/utils/backendErrors.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +export interface BackendErrorPayload { + service?: string; + errorCode?: string; + message?: string; + status?: number; + timestamp?: string; + path?: string; + correlationId?: string; +} + +export interface HttpErrorWithBackendDetails extends Error { + status?: number; + backendError?: BackendErrorPayload; +} + +type BackendErrorRaw = Record; + +const backendErrorKeys = ['service', 'errorCode', 'message', 'status', 'timestamp', 'path', 'correlationId']; + +const isRecord = (value: unknown): value is BackendErrorRaw => typeof value === 'object' && value !== null; + +export const isBackendErrorLike = (value: unknown): value is BackendErrorRaw => { + if (!isRecord(value)) { + return false; + } + return backendErrorKeys.some((key) => key in value); +}; + +const parseString = (value: unknown): string | undefined => + typeof value === 'string' && value.trim().length > 0 ? value : undefined; + +const parseNumber = (value: unknown): number | undefined => (typeof value === 'number' ? value : undefined); + +export const normalizeBackendErrorPayload = (value: BackendErrorRaw): BackendErrorPayload => ({ + service: parseString(value.service), + errorCode: parseString(value.errorCode), + message: parseString(value.message), + status: parseNumber(value.status), + timestamp: parseString(value.timestamp), + path: parseString(value.path), + correlationId: parseString(value.correlationId), +}); + +export type BackendErrorDetails = Record<'service' | 'message' | 'path', string>; + +const sanitizeDetail = (value?: string): string => (typeof value === 'string' ? value : ''); + +export const createBackendErrorDetails = (backendError: BackendErrorPayload): BackendErrorDetails => ({ + service: sanitizeDetail(backendError.service), + message: sanitizeDetail(backendError.message), + path: sanitizeDetail(backendError.path), +}); + +export const extractBackendErrorPayload = (error: unknown): BackendErrorPayload | undefined => { + if (!error) { + return undefined; + } + if (isRecord(error) && isBackendErrorLike(error.backendError)) { + return normalizeBackendErrorPayload(error.backendError as BackendErrorRaw); + } + if (isBackendErrorLike(error)) { + return normalizeBackendErrorPayload(error); + } + return undefined; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 89c23fe97..603dc8ff9 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -8,6 +8,7 @@ export * from './algos'; export * from './constants'; export * from './conversionUtils'; export * from './functions'; +export * from './backendErrors'; export * from './langs'; export * from './mapper'; export * from './constants/notificationsProvider'; From 119685816bfc9d921807ab653d08b64b47f277ed Mon Sep 17 00:00:00 2001 From: benrejebmoh Date: Tue, 30 Sep 2025 15:14:29 +0200 Subject: [PATCH 2/3] close custom error snackbar --- .../snackbars/BackendErrorSnackbarContent.tsx | 173 ++++++++++++------ src/components/snackbars/index.ts | 7 + 2 files changed, 121 insertions(+), 59 deletions(-) diff --git a/src/components/snackbars/BackendErrorSnackbarContent.tsx b/src/components/snackbars/BackendErrorSnackbarContent.tsx index 1e080f541..68cffc889 100644 --- a/src/components/snackbars/BackendErrorSnackbarContent.tsx +++ b/src/components/snackbars/BackendErrorSnackbarContent.tsx @@ -1,85 +1,140 @@ /** - * Copyright (c) 2025, RTE (http://www.rte-france.com) + * Copyright (c) 2024, RTE (http://www.rte-france.com) * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { forwardRef, useMemo, useState } from 'react'; -import { Button, Collapse, SnackbarContent, Stack, Typography, useTheme } from '@mui/material'; -import { type BackendErrorDetails } from '../../utils/backendErrors'; -type BackendErrorDetailKey = keyof BackendErrorDetails; +import { Close, ExpandLess, ExpandMore } from '@mui/icons-material'; +import { Button, Collapse, IconButton, Stack, Typography, styled } from '@mui/material'; +import { useSnackbar, type SnackbarKey } from 'notistack'; +import { forwardRef, useCallback, useId, useMemo, useState } from 'react'; + +interface BackendErrorDetails { + service: string; + message: string; + path: string; +} + +export type BackendErrorDetailLabels = Record; export interface BackendErrorSnackbarContentProps { message: string; detailsLabel: string; - detailLabels: Record; + detailLabels: BackendErrorDetailLabels; details: BackendErrorDetails; showDetailsLabel: string; hideDetailsLabel: string; + snackbarKey?: SnackbarKey; } -const detailOrder: BackendErrorDetailKey[] = ['service', 'message', 'path']; +const Root = styled(Stack)(({ theme }) => ({ + width: '100%', + color: theme.palette.common.white, + backgroundColor: theme.palette.error.main, + borderRadius: theme.shape.borderRadius, + padding: theme.spacing(1.5), + boxShadow: theme.shadows[6], +})); + +const Header = styled(Stack)(({ theme }) => ({ + width: '100%', + columnGap: theme.spacing(1), +})); + +const ToggleButton = styled(Button)(({ theme }) => ({ + alignSelf: 'flex-start', + padding: 0, + minWidth: 0, + textTransform: 'none', + color: 'inherit', + fontWeight: theme.typography.fontWeightMedium, + '&:hover': { + backgroundColor: 'transparent', + }, +})); + +const DetailsList = styled(Stack)(() => ({ + width: '100%', +})); + +const DetailRow = styled(Stack)(({ theme }) => ({ + flexDirection: 'row', + columnGap: theme.spacing(1), +})); export const BackendErrorSnackbarContent = forwardRef( - ({ message, detailsLabel, detailLabels, details, showDetailsLabel, hideDetailsLabel }, ref) => { - const theme = useTheme(); + ({ message, detailsLabel, detailLabels, details, showDetailsLabel, hideDetailsLabel, snackbarKey }, ref) => { + const { closeSnackbar } = useSnackbar(); const [isExpanded, setIsExpanded] = useState(false); + const detailsId = useId(); - const renderedDetails = useMemo( - () => - detailOrder - .map((key) => ({ key, label: detailLabels[key], value: details[key] })) - .filter((detail) => detail.label), - [detailLabels, details] - ); + const detailEntries = useMemo(() => { + return (Object.keys(detailLabels) as Array).map((key) => ({ + key, + label: detailLabels[key], + value: details[key], + })); + }, [detailLabels, details]); + + const toggleDetails = useCallback(() => { + setIsExpanded((prev) => !prev); + }, []); + + const handleClose = useCallback(() => { + closeSnackbar(snackbarKey); + }, [closeSnackbar, snackbarKey]); return ( - - - {message} + +
+ + {message} + + + + +
+ : } + aria-expanded={isExpanded} + aria-controls={detailsId} + > + {isExpanded ? hideDetailsLabel : showDetailsLabel} + + + + + {detailsLabel} - {renderedDetails.length > 0 ? ( - <> - - - - - {detailsLabel} - - {renderedDetails.map(({ key, label, value }) => ( - - {label}: {value} - - ))} - - - - ) : null} - - } - /> + {label} +
+ + {value} + + + ))} + + + ); } ); diff --git a/src/components/snackbars/index.ts b/src/components/snackbars/index.ts index 37a93eb8f..669f587a7 100644 --- a/src/components/snackbars/index.ts +++ b/src/components/snackbars/index.ts @@ -1 +1,8 @@ +/** + * Copyright (c) 2024, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + export * from './BackendErrorSnackbarContent'; From dfc41b6534ce87c02abc6bac6b690fff077e7590 Mon Sep 17 00:00:00 2001 From: benrejebmoh Date: Tue, 30 Sep 2025 17:34:51 +0200 Subject: [PATCH 3/3] manage snackbar content details by commons-ui --- .../snackbars/BackendErrorSnackbarContent.tsx | 8 +- src/translations/en/parameters.ts | 6 + src/translations/fr/parameters.ts | 6 + src/utils/backendErrors.ts | 189 ++++++++++++++---- 4 files changed, 169 insertions(+), 40 deletions(-) diff --git a/src/components/snackbars/BackendErrorSnackbarContent.tsx b/src/components/snackbars/BackendErrorSnackbarContent.tsx index 68cffc889..dad82204a 100644 --- a/src/components/snackbars/BackendErrorSnackbarContent.tsx +++ b/src/components/snackbars/BackendErrorSnackbarContent.tsx @@ -20,7 +20,6 @@ export type BackendErrorDetailLabels = Record export interface BackendErrorSnackbarContentProps { message: string; - detailsLabel: string; detailLabels: BackendErrorDetailLabels; details: BackendErrorDetails; showDetailsLabel: string; @@ -64,7 +63,7 @@ const DetailRow = styled(Stack)(({ theme }) => ({ })); export const BackendErrorSnackbarContent = forwardRef( - ({ message, detailsLabel, detailLabels, details, showDetailsLabel, hideDetailsLabel, snackbarKey }, ref) => { + ({ message, detailLabels, details, showDetailsLabel, hideDetailsLabel, snackbarKey }, ref) => { const { closeSnackbar } = useSnackbar(); const [isExpanded, setIsExpanded] = useState(false); const detailsId = useId(); @@ -96,7 +95,7 @@ export const BackendErrorSnackbarContent = forwardRef : } aria-expanded={isExpanded} aria-controls={detailsId} @@ -105,9 +104,6 @@ export const BackendErrorSnackbarContent = forwardRef - - {detailsLabel} - {detailEntries.map(({ key, label, value }) => ( => typeof value === 'object' && value !== null; + export interface BackendErrorPayload { service?: string; errorCode?: string; @@ -16,57 +32,162 @@ export interface BackendErrorPayload { } export interface HttpErrorWithBackendDetails extends Error { - status?: number; + status: number; backendError?: BackendErrorPayload; } -type BackendErrorRaw = Record; +export const isBackendErrorLike = (value: unknown): value is BackendErrorPayload => { + if (!isRecord(value)) { + return false; + } + return ( + 'service' in value || + 'errorCode' in value || + 'message' in value || + 'status' in value || + 'timestamp' in value || + 'path' in value || + 'correlationId' in value + ); +}; -const backendErrorKeys = ['service', 'errorCode', 'message', 'status', 'timestamp', 'path', 'correlationId']; +export const normalizeBackendErrorPayload = (payload: BackendErrorPayload): BackendErrorPayload => { + const record = payload as BackendErrorPayload & { server?: unknown }; + return { + service: typeof record.service === 'string' ? record.service : undefined, + errorCode: typeof record.errorCode === 'string' ? record.errorCode : undefined, + message: typeof record.message === 'string' ? record.message : undefined, + status: typeof record.status === 'number' ? record.status : undefined, + timestamp: typeof record.timestamp === 'string' ? record.timestamp : undefined, + path: typeof record.path === 'string' ? record.path : undefined, + correlationId: typeof record.correlationId === 'string' ? record.correlationId : undefined, + }; +}; -const isRecord = (value: unknown): value is BackendErrorRaw => typeof value === 'object' && value !== null; +const toBackendErrorPayload = (value: unknown): BackendErrorPayload | undefined => { + if (!isBackendErrorLike(value)) { + return undefined; + } + return normalizeBackendErrorPayload(value); +}; -export const isBackendErrorLike = (value: unknown): value is BackendErrorRaw => { - if (!isRecord(value)) { - return false; +const hasBackendError = (value: unknown): value is { backendError?: unknown } => + isRecord(value) && 'backendError' in value; + +export const extractBackendErrorPayload = (error: unknown): BackendErrorPayload | undefined => { + if (hasBackendError(error) && error.backendError) { + return toBackendErrorPayload(error.backendError); } - return backendErrorKeys.some((key) => key in value); + return toBackendErrorPayload(error); }; -const parseString = (value: unknown): string | undefined => - typeof value === 'string' && value.trim().length > 0 ? value : undefined; +export const createBackendErrorDetails = ( + payload: BackendErrorPayload +): BackendErrorSnackbarContentProps['details'] => ({ + service: typeof payload.service === 'string' ? payload.service : '', + message: typeof payload.message === 'string' ? payload.message : '', + path: typeof payload.path === 'string' ? payload.path : '', +}); -const parseNumber = (value: unknown): number | undefined => (typeof value === 'number' ? value : undefined); +const formatBackendDetailValue = (value: string): string => (value.trim().length > 0 ? value : BACKEND_DETAIL_FALLBACK); -export const normalizeBackendErrorPayload = (value: BackendErrorRaw): BackendErrorPayload => ({ - service: parseString(value.service), - errorCode: parseString(value.errorCode), - message: parseString(value.message), - status: parseNumber(value.status), - timestamp: parseString(value.timestamp), - path: parseString(value.path), - correlationId: parseString(value.correlationId), +const formatBackendErrorDetails = (details: BackendErrorDetails): BackendErrorDetails => ({ + service: formatBackendDetailValue(details.service), + message: formatBackendDetailValue(details.message), + path: formatBackendDetailValue(details.path), }); -export type BackendErrorDetails = Record<'service' | 'message' | 'path', string>; +const createBackendErrorDetailLabels = (intl: IntlShape): BackendErrorDetailLabels => ({ + service: intl.formatMessage({ id: 'serverLabel' }), + message: intl.formatMessage({ id: 'messageLabel' }), + path: intl.formatMessage({ id: 'pathLabel' }), +}); -const sanitizeDetail = (value?: string): string => (typeof value === 'string' ? value : ''); +interface BackendErrorPresentation { + message: string; + detailLabels: BackendErrorDetailLabels; + formattedDetails: BackendErrorDetails; + showDetailsLabel: string; + hideDetailsLabel: string; +} -export const createBackendErrorDetails = (backendError: BackendErrorPayload): BackendErrorDetails => ({ - service: sanitizeDetail(backendError.service), - message: sanitizeDetail(backendError.message), - path: sanitizeDetail(backendError.path), +const createBackendErrorPresentation = ( + intl: IntlShape, + details: BackendErrorDetails, + firstLine?: string +): BackendErrorPresentation => ({ + message: firstLine ?? intl.formatMessage({ id: 'genericMessage' }), + detailLabels: createBackendErrorDetailLabels(intl), + formattedDetails: formatBackendErrorDetails(details), + showDetailsLabel: intl.formatMessage({ id: 'showDetails' }), + hideDetailsLabel: intl.formatMessage({ id: 'hideDetails' }), }); -export const extractBackendErrorPayload = (error: unknown): BackendErrorPayload | undefined => { - if (!error) { - return undefined; - } - if (isRecord(error) && isBackendErrorLike(error.backendError)) { - return normalizeBackendErrorPayload(error.backendError as BackendErrorRaw); +export const snackErrorWithBackendFallback = ( + error: unknown, + snackError: UseSnackMessageReturn['snackError'], + intl: IntlShape, + additionalSnack?: Partial +) => { + const backendPayload = extractBackendErrorPayload(error); + const backendDetails = backendPayload ? createBackendErrorDetails(backendPayload) : undefined; + + if (backendDetails) { + const { headerId, headerTxt, headerValues, persist, messageId, messageTxt, messageValues, ...rest } = + additionalSnack ?? {}; + const otherSnackProps: Partial = rest ? { ...(rest as Partial) } : {}; + + const firstLine = messageTxt ?? (messageId ? intl.formatMessage({ id: messageId }, messageValues) : undefined); + + const presentation = createBackendErrorPresentation(intl, backendDetails, firstLine); + + const snackInputs: SnackInputs = { + ...(otherSnackProps as SnackInputs), + messageTxt: presentation.message, + persist: persist ?? true, + content: (snackbarKey, snackMessage) => + createElement(BackendErrorSnackbarContent, { + snackbarKey, + message: + typeof snackMessage === 'string' && snackMessage.length > 0 + ? snackMessage + : presentation.message, + detailLabels: presentation.detailLabels, + details: presentation.formattedDetails, + showDetailsLabel: presentation.showDetailsLabel, + hideDetailsLabel: presentation.hideDetailsLabel, + }), + }; + + if (headerId !== undefined) { + snackInputs.headerId = headerId; + } + if (headerTxt !== undefined) { + snackInputs.headerTxt = headerTxt; + } + if (headerValues !== undefined) { + snackInputs.headerValues = headerValues; + } + + snackError(snackInputs); + return; } - if (isBackendErrorLike(error)) { - return normalizeBackendErrorPayload(error); + + if (additionalSnack) { + const { messageTxt: additionalMessageTxt, messageId: additionalMessageId } = additionalSnack; + if (additionalMessageTxt !== undefined || additionalMessageId !== undefined) { + snackError(additionalSnack as SnackInputs); + return; + } } - return undefined; + + const message = error instanceof Error ? error.message : String(error); + const restSnackInputs: Partial = additionalSnack ? { ...additionalSnack } : {}; + delete restSnackInputs.messageId; + delete restSnackInputs.messageTxt; + delete restSnackInputs.messageValues; + snackError({ + ...(restSnackInputs as SnackInputs), + messageTxt: message, + }); };