From de9550d3016459466175adace13a445a3494c640 Mon Sep 17 00:00:00 2001 From: benrejebmoh Date: Tue, 30 Sep 2025 10:44:03 +0200 Subject: [PATCH 1/3] first working version of extendable snackbar --- src/components/app.tsx | 23 ++- .../dialogs/commons/prefilled-name-input.tsx | 4 +- .../contingency-list-creation-dialog.tsx | 9 +- .../criteria-based-edition-dialog.tsx | 13 +- .../explicit-naming-edition-dialog.tsx | 13 +- .../contingency-list-filter-based-dialog.tsx | 23 ++- .../contingency-list-filter-based-form.tsx | 6 +- .../create-study-dialog.tsx | 12 +- .../directory-properties-dialog.tsx | 18 +-- .../composite-modification-dialog.tsx | 11 +- ...spreadsheet-collection-creation-dialog.tsx | 17 +- .../dialogs/use-parameters-dialog.tsx | 10 +- src/components/search/search-bar.tsx | 18 ++- src/components/utils/rest-errors.ts | 150 ++++++++++++++++-- src/translations/en.json | 7 + src/translations/fr.json | 7 + 16 files changed, 250 insertions(+), 91 deletions(-) diff --git a/src/components/app.tsx b/src/components/app.tsx index 21ec528b1..c0e801e06 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -27,7 +27,7 @@ import { UserManagerState, useSnackMessage, } from '@gridsuite/commons-ui'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import { Box } from '@mui/material'; import { selectComputedLanguage, selectEnableDeveloperMode, selectLanguage, selectTheme } from '../redux/actions'; import { ConfigParameters, fetchIdpSettings } from '../utils/rest-api'; @@ -38,9 +38,11 @@ import DirectoryContent from './directory-content'; import DirectoryBreadcrumbs from './directory-breadcrumbs'; import { AppDispatch } from '../redux/store'; import { AppState } from '../redux/types'; +import { snackErrorWithBackendFallback } from './utils/rest-errors'; export default function App() { const { snackError } = useSnackMessage(); + const intl = useIntl(); const user = useSelector((state: AppState) => state.user); @@ -102,15 +104,14 @@ export default function App() { if (eventData.headers?.parameterName) { fetchConfigParameter(APP_NAME, eventData.headers.parameterName) .then((param) => updateParams([param])) - .catch((error) => - snackError({ - messageTxt: error.message, + .catch((error: unknown) => + snackErrorWithBackendFallback(error, snackError, intl, { headerId: 'paramsRetrievingError', }) ); } }, - [updateParams, snackError] + [updateParams, snackError, intl] ); useNotificationsListener(NotificationsUrlKeys.CONFIG, { @@ -154,24 +155,22 @@ export default function App() { if (user !== null) { fetchConfigParameters(COMMON_APP_NAME) .then((params) => updateParams(params)) - .catch((error) => - snackError({ - messageTxt: error.message, + .catch((error: unknown) => + snackErrorWithBackendFallback(error, snackError, intl, { headerId: 'paramsRetrievingError', }) ); fetchConfigParameters(APP_NAME) .then((params) => updateParams(params)) - .catch((error) => - snackError({ - messageTxt: error.message, + .catch((error: unknown) => + snackErrorWithBackendFallback(error, snackError, intl, { headerId: 'paramsRetrievingError', }) ); } return undefined; - }, [user, dispatch, updateParams, snackError]); + }, [user, dispatch, updateParams, snackError, intl]); // We use instead of because flex rules were too complexes or conflicts with MUI grid rules return ( diff --git a/src/components/dialogs/commons/prefilled-name-input.tsx b/src/components/dialogs/commons/prefilled-name-input.tsx index c91063fe2..a3f02c639 100644 --- a/src/components/dialogs/commons/prefilled-name-input.tsx +++ b/src/components/dialogs/commons/prefilled-name-input.tsx @@ -42,8 +42,8 @@ export default function PrefilledNameInput({ label, name, elementType }: Readonl shouldDirty: true, }); }) - .catch((error) => { - handleGenericTxtError(error.message, snackError); + .catch((error: unknown) => { + handleGenericTxtError(error instanceof Error ? error : String(error), snackError); }); } } diff --git a/src/components/dialogs/contingency-list/creation/contingency-list-creation-dialog.tsx b/src/components/dialogs/contingency-list/creation/contingency-list-creation-dialog.tsx index 1599d5807..50e651ba4 100644 --- a/src/components/dialogs/contingency-list/creation/contingency-list-creation-dialog.tsx +++ b/src/components/dialogs/contingency-list/creation/contingency-list-creation-dialog.tsx @@ -17,6 +17,7 @@ import { } from '@gridsuite/commons-ui'; import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; +import { useIntl } from 'react-intl'; import { createContingencyList } from '../../../../utils/rest-api'; import ContingencyListCreationForm from './contingency-list-creation-form'; import { @@ -29,7 +30,7 @@ import { ContingencyListType } from '../../../../utils/elementType'; import { useParameterState } from '../../use-parameters-dialog'; import { AppState } from '../../../../redux/types'; import { getExplicitNamingSchema } from '../explicit-naming/explicit-naming-utils'; -import { handleNotAllowedError } from '../../../utils/rest-errors'; +import { CustomError, handleNotAllowedError, snackErrorWithBackendFallback } from '../../../utils/rest-errors'; const schema = yup.object().shape({ [FieldConstants.NAME]: yup.string().trim().required('nameEmpty'), @@ -59,6 +60,7 @@ export default function ContingencyListCreationDialog({ }: Readonly) { const activeDirectory = useSelector((state: AppState) => state.activeDirectory); const { snackError } = useSnackMessage(); + const intl = useIntl(); const [languageLocal] = useParameterState(PARAM_LANGUAGE); @@ -91,12 +93,11 @@ export default function ContingencyListCreationDialog({ activeDirectory ) .then(() => closeAndClear()) - .catch((error) => { + .catch((error: CustomError) => { if (handleNotAllowedError(error, snackError)) { return; } - snackError({ - messageTxt: error.message, + snackErrorWithBackendFallback(error, snackError, intl, { headerId: 'contingencyListCreationError', headerValues: { name: data[FieldConstants.NAME] }, }); diff --git a/src/components/dialogs/contingency-list/edition/criteria-based/criteria-based-edition-dialog.tsx b/src/components/dialogs/contingency-list/edition/criteria-based/criteria-based-edition-dialog.tsx index 89132b5bd..ec0521f0a 100644 --- a/src/components/dialogs/contingency-list/edition/criteria-based/criteria-based-edition-dialog.tsx +++ b/src/components/dialogs/contingency-list/edition/criteria-based/criteria-based-edition-dialog.tsx @@ -18,6 +18,7 @@ import { import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; import { getContingencyList, saveCriteriaBasedContingencyList } from 'utils/rest-api'; import { useDispatch, useSelector } from 'react-redux'; import { AppState } from '../../../../../redux/types'; @@ -29,6 +30,7 @@ import CriteriaBasedEditionForm from './criteria-based-edition-form'; import { setItemSelectionForCopy } from '../../../../../redux/actions'; import { useParameterState } from '../../../use-parameters-dialog'; import { CriteriaBasedEditionFormData } from '../../../../../utils/rest-api'; +import { snackErrorWithBackendFallback } from '../../../../utils/rest-errors'; const schema = yup.object().shape({ [FieldConstants.NAME]: yup.string().trim().required('nameEmpty'), @@ -61,6 +63,7 @@ export default function CriteriaBasedEditionDialog({ const [languageLocal] = useParameterState(PARAM_LANGUAGE); const [isFetching, setIsFetching] = useState(!!contingencyListId); const { snackError } = useSnackMessage(); + const intl = useIntl(); const itemSelectionForCopy = useSelector((state: AppState) => state.itemSelectionForCopy); const dispatch = useDispatch(); const methods = useForm({ @@ -87,14 +90,13 @@ export default function CriteriaBasedEditionDialog({ }); } }) - .catch((error) => { - snackError({ - messageTxt: error.message, + .catch((error: unknown) => { + snackErrorWithBackendFallback(error, snackError, intl, { headerId: 'cannotRetrieveContingencyList', }); }) .finally(() => setIsFetching(false)); - }, [contingencyListId, contingencyListType, name, reset, snackError, description]); + }, [contingencyListId, contingencyListType, name, reset, snackError, description, intl]); const closeAndClear = () => { reset(getContingencyListEmptyFormData()); @@ -111,8 +113,7 @@ export default function CriteriaBasedEditionDialog({ closeAndClear(); }) .catch((errorMessage) => { - snackError({ - messageTxt: errorMessage, + snackErrorWithBackendFallback(errorMessage, snackError, intl, { headerId: 'contingencyListEditingError', headerValues: { name }, }); diff --git a/src/components/dialogs/contingency-list/edition/explicit-naming/explicit-naming-edition-dialog.tsx b/src/components/dialogs/contingency-list/edition/explicit-naming/explicit-naming-edition-dialog.tsx index a628eb499..427611aa9 100644 --- a/src/components/dialogs/contingency-list/edition/explicit-naming/explicit-naming-edition-dialog.tsx +++ b/src/components/dialogs/contingency-list/edition/explicit-naming/explicit-naming-edition-dialog.tsx @@ -16,6 +16,7 @@ import { import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; import { getContingencyList, saveExplicitNamingContingencyList } from 'utils/rest-api'; import { prepareContingencyListForBackend } from 'components/dialogs/contingency-list-helper'; import { useDispatch, useSelector } from 'react-redux'; @@ -27,6 +28,7 @@ import ExplicitNamingEditionForm from './explicit-naming-edition-form'; import { setItemSelectionForCopy } from '../../../../../redux/actions'; import { AppState } from '../../../../../redux/types'; import { getExplicitNamingEditSchema } from '../../explicit-naming/explicit-naming-utils'; +import { snackErrorWithBackendFallback } from '../../../../utils/rest-errors'; interface ExplicitNamingEditionFormData { [FieldConstants.NAME]: string; @@ -70,6 +72,7 @@ export default function ExplicitNamingEditionDialog({ }: Readonly) { const [isFetching, setIsFetching] = useState(!!contingencyListId); const { snackError } = useSnackMessage(); + const intl = useIntl(); const itemSelectionForCopy = useSelector((state: AppState) => state.itemSelectionForCopy); const dispatch = useDispatch(); const methods = useForm({ @@ -93,14 +96,13 @@ export default function ExplicitNamingEditionDialog({ reset({ ...formData }); } }) - .catch((error) => { - snackError({ - messageTxt: error.message, + .catch((error: unknown) => { + snackErrorWithBackendFallback(error, snackError, intl, { headerId: 'cannotRetrieveContingencyList', }); }) .finally(() => setIsFetching(false)); - }, [contingencyListId, contingencyListType, name, reset, snackError, description]); + }, [contingencyListId, contingencyListType, name, reset, snackError, description, intl]); const closeAndClear = () => { reset(emptyFormData()); @@ -126,8 +128,7 @@ export default function ExplicitNamingEditionDialog({ closeAndClear(); }) .catch((errorMessage) => { - snackError({ - messageTxt: errorMessage, + snackErrorWithBackendFallback(errorMessage, snackError, intl, { headerId: 'contingencyListEditingError', headerValues: { name }, }); diff --git a/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-dialog.tsx b/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-dialog.tsx index a4ecaac55..37ab04bf5 100644 --- a/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-dialog.tsx +++ b/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-dialog.tsx @@ -17,6 +17,7 @@ import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { useSelector } from 'react-redux'; import { useCallback, useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; import { UUID } from 'crypto'; import { ObjectSchema } from 'yup'; import ContingencyListFilterBasedForm from './contingency-list-filter-based-form'; @@ -26,7 +27,7 @@ import { getContingencyList, saveFilterBasedContingencyList, } from '../../../../utils/rest-api'; -import { handleNotAllowedError } from '../../../utils/rest-errors'; +import { CustomError, handleNotAllowedError, snackErrorWithBackendFallback } from '../../../utils/rest-errors'; import { ContingencyListType } from '../../../../utils/elementType'; import { getFilterBasedFormDataFromFetchedElement } from '../contingency-list-utils'; import { FilterBasedContingencyList } from '../../../../utils/contingency-list.type'; @@ -71,6 +72,7 @@ export default function FilterBasedContingencyListDialog({ const activeDirectory = useSelector((state: AppState) => state.activeDirectory); const { snackError } = useSnackMessage(); const [isFetching, setIsFetching] = useState(!!id); + const intl = useIntl(); const methods = useForm({ defaultValues: emptyFormData(), @@ -93,15 +95,14 @@ export default function FilterBasedContingencyListDialog({ ); reset({ ...formData }); }) - .catch((error) => { - snackError({ - messageTxt: error.message, + .catch((error: unknown) => { + snackErrorWithBackendFallback(error, snackError, intl, { headerId: 'cannotRetrieveContingencyList', }); }) .finally(() => setIsFetching(false)); } - }, [id, name, reset, snackError, description]); + }, [id, name, reset, snackError, description, intl]); const closeAndClear = useCallback(() => { reset(emptyFormData()); @@ -126,12 +127,11 @@ export default function FilterBasedContingencyListDialog({ filterBaseContingencyList ) .then(() => closeAndClear()) - .catch((error) => { + .catch((error: CustomError) => { if (handleNotAllowedError(error, snackError)) { return; } - snackError({ - messageTxt: error.message, + snackErrorWithBackendFallback(error, snackError, intl, { headerId: 'contingencyListEditingError', headerValues: { name: data[FieldConstants.NAME] }, }); @@ -144,19 +144,18 @@ export default function FilterBasedContingencyListDialog({ activeDirectory ) .then(() => closeAndClear()) - .catch((error) => { + .catch((error: CustomError) => { if (handleNotAllowedError(error, snackError)) { return; } - snackError({ - messageTxt: error.message, + snackErrorWithBackendFallback(error, snackError, intl, { headerId: 'contingencyListCreationError', headerValues: { name: data[FieldConstants.NAME] }, }); }); } }, - [activeDirectory, closeAndClear, id, snackError] + [activeDirectory, closeAndClear, id, snackError, intl] ); const nameError = errors[FieldConstants.NAME]; diff --git a/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-form.tsx b/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-form.tsx index 3e8f4707e..7d525a056 100644 --- a/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-form.tsx +++ b/src/components/dialogs/contingency-list/filter-based/contingency-list-filter-based-form.tsx @@ -35,6 +35,7 @@ import { FilterAttributes, IdentifiableAttributes, } from '../../../../utils/contingency-list.type'; +import { snackErrorWithBackendFallback } from '../../../utils/rest-errors'; const separator = '/'; const defaultDef: ColDef = { @@ -131,9 +132,8 @@ export default function ContingencyListFilterBasedForm() { } setRowsData(attributes); }) - .catch((error) => - snackError({ - messageTxt: error.message, + .catch((error: unknown) => + snackErrorWithBackendFallback(error, snackError, intl, { headerId: 'cannotComputeContingencyList', }) ) diff --git a/src/components/dialogs/create-study-dialog/create-study-dialog.tsx b/src/components/dialogs/create-study-dialog/create-study-dialog.tsx index cb6a5c442..ba1c414a0 100644 --- a/src/components/dialogs/create-study-dialog/create-study-dialog.tsx +++ b/src/components/dialogs/create-study-dialog/create-study-dialog.tsx @@ -37,7 +37,12 @@ import { getCreateStudyDialogFormDefaultValues, } from './create-study-dialog-utils'; import PrefilledNameInput from '../commons/prefilled-name-input'; -import { handleMaxElementsExceededError, handleNotAllowedError } from '../../utils/rest-errors'; +import { + CustomError, + handleMaxElementsExceededError, + handleNotAllowedError, + snackErrorWithBackendFallback, +} from '../../utils/rest-errors'; import { AppState, UploadingElement } from '../../../redux/types'; const STRING_LIST = 'STRING_LIST'; @@ -210,7 +215,7 @@ export default function CreateStudyDialog({ open, onClose, providedExistingCase dispatch(setActiveDirectory(selectedDirectory?.elementUuid)); onClose(); }) - .catch((error) => { + .catch((error: CustomError) => { dispatch(removeUploadingElement(uploadingStudy)); if (handleMaxElementsExceededError(error, snackError)) { return; @@ -218,8 +223,7 @@ export default function CreateStudyDialog({ open, onClose, providedExistingCase if (handleNotAllowedError(error, snackError)) { return; } - snackError({ - messageTxt: error.message, + snackErrorWithBackendFallback(error, snackError, intl, { headerId: 'studyCreationError', headerValues: { studyName, diff --git a/src/components/dialogs/directory-properties/directory-properties-dialog.tsx b/src/components/dialogs/directory-properties/directory-properties-dialog.tsx index 27068546a..30b48402e 100644 --- a/src/components/dialogs/directory-properties/directory-properties-dialog.tsx +++ b/src/components/dialogs/directory-properties/directory-properties-dialog.tsx @@ -7,7 +7,7 @@ import { useState, useEffect, useCallback } from 'react'; import { Dialog, DialogTitle, DialogContent, DialogActions, Box, Typography, CircularProgress } from '@mui/material'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import { ElementAttributes, useSnackMessage, @@ -26,6 +26,7 @@ import { PermissionDTO, PermissionType, } from '../../../utils/rest-api'; +import { snackErrorWithBackendFallback } from '../../utils/rest-errors'; import { Group, PermissionForm, @@ -46,6 +47,7 @@ interface DirectoryPropertiesDialogProps { function DirectoryPropertiesDialog({ open, onClose, directory }: Readonly) { const { snackError } = useSnackMessage(); + const intl = useIntl(); const [loading, setLoading] = useState(true); const [groups, setGroups] = useState([]); @@ -109,15 +111,14 @@ function DirectoryPropertiesDialog({ open, onClose, directory }: Readonly { if (open && directory) { @@ -150,14 +151,13 @@ function DirectoryPropertiesDialog({ open, onClose, directory }: Readonly { - snackError({ - messageTxt: error.message, + .catch((error: unknown) => { + snackErrorWithBackendFallback(error, snackError, intl, { headerId: 'retrieveCompositeModificationError', }); }) .finally(() => setIsFetching(false)); - }, [compositeModificationId, name, snackError]); + }, [compositeModificationId, name, snackError, intl]); const onSubmit = (formData: FormData) => { const modificationUuids = modifications.map((modification) => modification.uuid); @@ -143,8 +143,7 @@ export default function CompositeModificationDialog({ onClose(); }) .catch((errorMessage) => { - snackError({ - messageTxt: errorMessage, + snackErrorWithBackendFallback(errorMessage, snackError, intl, { headerId: 'compositeModificationEditingError', headerValues: { name }, }); diff --git a/src/components/dialogs/spreadsheet-collection-creation-dialog.tsx b/src/components/dialogs/spreadsheet-collection-creation-dialog.tsx index 40d706c65..dbea91f3f 100644 --- a/src/components/dialogs/spreadsheet-collection-creation-dialog.tsx +++ b/src/components/dialogs/spreadsheet-collection-creation-dialog.tsx @@ -15,10 +15,12 @@ import { type IElementUpdateDialog, useSnackMessage, } from '@gridsuite/commons-ui'; +import { useIntl } from 'react-intl'; import { createSpreadsheetConfigCollectionFromConfigIds, replaceAllSpreadsheetConfigsInCollection, } from '../../utils/rest-api'; +import { snackErrorWithBackendFallback } from '../utils/rest-errors'; export interface CreateSpreadsheetCollectionProps { open: boolean; @@ -34,6 +36,7 @@ function CreateSpreadsheetCollectionDialog({ spreadsheetConfigIds, }: Readonly) { const { snackError, snackInfo } = useSnackMessage(); + const intl = useIntl(); const createCollection = useCallback( (element: IElementCreationDialog) => { @@ -52,15 +55,14 @@ function CreateSpreadsheetCollectionDialog({ }, }); }) - .catch((error) => { + .catch((error: unknown) => { console.error(error); - snackError({ - messageTxt: error.message, + snackErrorWithBackendFallback(error, snackError, intl, { headerId: 'createCollectionError', }); }); }, - [snackError, snackInfo, spreadsheetConfigIds] + [snackError, snackInfo, spreadsheetConfigIds, intl] ); const updateCollection = useCallback( @@ -80,10 +82,9 @@ function CreateSpreadsheetCollectionDialog({ }, }); }) - .catch((error) => { + .catch((error: unknown) => { console.error(error); - snackError({ - messageTxt: error.message, + snackErrorWithBackendFallback(error, snackError, intl, { headerId: 'updateCollectionError', headerValues: { item: element.elementFullPath, @@ -91,7 +92,7 @@ function CreateSpreadsheetCollectionDialog({ }); }); }, - [snackError, snackInfo, spreadsheetConfigIds] + [snackError, snackInfo, spreadsheetConfigIds, intl] ); return ( diff --git a/src/components/dialogs/use-parameters-dialog.tsx b/src/components/dialogs/use-parameters-dialog.tsx index 833a213ee..44eb25730 100644 --- a/src/components/dialogs/use-parameters-dialog.tsx +++ b/src/components/dialogs/use-parameters-dialog.tsx @@ -14,8 +14,10 @@ import { updateConfigParameter, useSnackMessage, } from '@gridsuite/commons-ui'; +import { useIntl } from 'react-intl'; import { AppState } from '../../redux/types'; import { APP_NAME } from '../../utils/config-params'; +import { snackErrorWithBackendFallback } from '../utils/rest-errors'; type ParamName = typeof PARAM_THEME | typeof PARAM_LANGUAGE | typeof PARAM_DEVELOPER_MODE; @@ -23,6 +25,7 @@ export function useParameterState( paramName: TParamName ): [AppState[TParamName], (value: AppState[TParamName]) => void] { const { snackError } = useSnackMessage(); + const intl = useIntl(); const paramGlobalState = useSelector((state: AppState) => state[paramName]); @@ -35,15 +38,14 @@ export function useParameterState( const handleChangeParamLocalState = useCallback( (value: AppState[TParamName]) => { setParamLocalState(value); - updateConfigParameter(APP_NAME, paramName, value.toString()).catch((error) => { + updateConfigParameter(APP_NAME, paramName, value.toString()).catch((error: unknown) => { setParamLocalState(paramGlobalState); - snackError({ - messageTxt: error.message, + snackErrorWithBackendFallback(error, snackError, intl, { headerId: 'paramsChangingError', }); }); }, - [paramName, snackError, setParamLocalState, paramGlobalState] + [paramName, snackError, setParamLocalState, paramGlobalState, intl] ); return [paramLocalState, handleChangeParamLocalState]; diff --git a/src/components/search/search-bar.tsx b/src/components/search/search-bar.tsx index 45e5bb6c9..de2e17062 100644 --- a/src/components/search/search-bar.tsx +++ b/src/components/search/search-bar.tsx @@ -15,6 +15,7 @@ import { useSnackMessage, } from '@gridsuite/commons-ui'; import { useDispatch, useSelector } from 'react-redux'; +import { useIntl } from 'react-intl'; import { TextFieldProps } from '@mui/material'; import { searchElementsInfos } from '../../utils/rest-api'; import { setSearchedElement, setSelectedDirectory, setTreeData } from '../../redux/actions'; @@ -24,6 +25,7 @@ import { AppState, ElementAttributesES, IDirectory, ITreeData } from '../../redu import { SearchBarRenderInput } from './search-bar-render-input'; import { AppDispatch } from '../../redux/store'; import { SearchBarPaperDisplayedElementWarning } from './search-bar-displayed-element-warning'; +import { snackErrorWithBackendFallback } from '../utils/rest-errors'; export interface SearchBarProps { inputRef: RefObject; @@ -32,6 +34,7 @@ export interface SearchBarProps { export function SearchBar({ inputRef }: Readonly) { const dispatch = useDispatch(); const { snackError } = useSnackMessage(); + const intl = useIntl(); const treeData = useSelector((state: AppState) => state.treeData); const treeDataRef = useRef(); const selectedDirectory = useSelector((state: AppState) => state.selectedDirectory); @@ -101,9 +104,8 @@ export function SearchBar({ inputRef }: Readonly) { resources.filter((res) => res.type === ElementType.DIRECTORY) as IDirectory[] ); } - } catch (error: any) { - snackError({ - messageTxt: error.message, + } catch (error: unknown) { + snackErrorWithBackendFallback(error, snackError, intl, { headerId: 'pathRetrievingError', }); } @@ -115,7 +117,15 @@ export function SearchBar({ inputRef }: Readonly) { } } }, - [selectedDirectory?.elementUuid, handleDispatchDirectory, updateMapData, snackError, dispatch, elementsFound] + [ + selectedDirectory?.elementUuid, + handleDispatchDirectory, + updateMapData, + snackError, + dispatch, + elementsFound, + intl, + ] ); const displayComponent = useCallback['PaperComponent']>>( diff --git a/src/components/utils/rest-errors.ts b/src/components/utils/rest-errors.ts index e4767f649..f9350eb28 100644 --- a/src/components/utils/rest-errors.ts +++ b/src/components/utils/rest-errors.ts @@ -11,9 +11,71 @@ import { HTTP_NOT_FOUND, PermissionCheckResult, } from 'utils/UIconstants'; -import { ElementAttributes, UseSnackMessageReturn } from '@gridsuite/commons-ui'; +import { + BackendErrorSnackbarContent, + type BackendErrorSnackbarContentProps, + ElementAttributes, + UseSnackMessageReturn, + createBackendErrorDetails, + extractBackendErrorPayload, + type BackendErrorPayload, + type BackendErrorDetails, + type SnackInputs, +} from '@gridsuite/commons-ui'; import { IntlShape } from 'react-intl'; -import { type Dispatch, SetStateAction } from 'react'; +import { type Dispatch, SetStateAction, createElement } from 'react'; + +const BACKEND_DETAIL_FALLBACK = '-'; + +const formatBackendDetailValue = (value: string): string => (value.trim().length > 0 ? value : BACKEND_DETAIL_FALLBACK); + +const getBackendErrorDetails = (error: unknown): BackendErrorDetails | undefined => { + const backendPayload = extractBackendErrorPayload(error); + if (!backendPayload) { + return undefined; + } + return createBackendErrorDetails(backendPayload); +}; + +interface BackendErrorPresentation { + message: string; + detailsLabel: string; + detailLabels: BackendErrorSnackbarContentProps['detailLabels']; + formattedDetails: BackendErrorDetails; + showDetailsLabel: string; + hideDetailsLabel: string; +} + +const createBackendErrorPresentation = ( + intl: IntlShape, + details: BackendErrorDetails, + firstLine?: string +): BackendErrorPresentation => { + const message = firstLine ?? intl.formatMessage({ id: 'backendError.genericMessage' }); + const detailsLabel = intl.formatMessage({ id: 'backendError.detailsLabel' }); + const serverLabel = intl.formatMessage({ id: 'backendError.serverLabel' }); + const messageLabel = intl.formatMessage({ id: 'backendError.messageLabel' }); + const pathLabel = intl.formatMessage({ id: 'backendError.pathLabel' }); + const showDetailsLabel = intl.formatMessage({ id: 'backendError.showDetails' }); + const hideDetailsLabel = intl.formatMessage({ id: 'backendError.hideDetails' }); + + return { + message, + detailsLabel, + detailLabels: { + service: serverLabel, + message: messageLabel, + path: pathLabel, + }, + formattedDetails: { + service: formatBackendDetailValue(details.service), + message: formatBackendDetailValue(details.message), + path: formatBackendDetailValue(details.path), + }, + showDetailsLabel, + hideDetailsLabel, + }; +}; export interface ErrorMessageByHttpError { [httpCode: string]: string; @@ -21,6 +83,7 @@ export interface ErrorMessageByHttpError { export interface CustomError extends Error { status: number; + backendError?: BackendErrorPayload; } export type SnackError = UseSnackMessageReturn['snackError']; @@ -45,9 +108,76 @@ export const generatePasteErrorMessages = (intl: IntlShape): ErrorMessageByHttpE [HTTP_NOT_FOUND]: intl.formatMessage({ id: 'elementPasteFailed404' }), }); -export const handleGenericTxtError = (error: string, snackError: SnackError) => { +export const handleGenericTxtError = (error: string | Error, snackError: SnackError) => { + const message = typeof error === 'string' ? error : error.message; snackError({ - messageTxt: error, + messageTxt: message, + }); +}; + +export const snackErrorWithBackendFallback = ( + error: unknown, + snackError: SnackError, + intl: IntlShape, + additionalSnack?: Partial +) => { + const backendDetails = getBackendErrorDetails(error); + 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: (_, snackMessage) => + createElement(BackendErrorSnackbarContent, { + message: + typeof snackMessage === 'string' && snackMessage.length > 0 + ? snackMessage + : presentation.message, + detailsLabel: presentation.detailsLabel, + 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 (additionalSnack) { + const { messageTxt: additionalMessageTxt, messageId: additionalMessageId } = additionalSnack; + if (additionalMessageTxt !== undefined || additionalMessageId !== undefined) { + snackError(additionalSnack as SnackInputs); + return; + } + } + + 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, }); }; @@ -122,13 +252,11 @@ export const handleDeleteError = ( return; } - let message = generateGenericPermissionErrorMessages(intl)[error.status]; - if (message) { - snackError({ messageId: message }); - } else { - message = error.message; - handleGenericTxtError(message, snackError); - } + const permissionMessage = generateGenericPermissionErrorMessages(intl)[error.status]; + const message = permissionMessage ?? error.message; + + snackErrorWithBackendFallback(error, snackError, intl, { messageTxt: message }); + // show the error message and don't close the underlying dialog setDeleteError(message); }; diff --git a/src/translations/en.json b/src/translations/en.json index 336a62adc..a4c522f20 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4,6 +4,13 @@ "parameters": "Parameters", "paramsChangingError": "An error occured when changing the parameters", "paramsRetrievingError": "An error occurred while retrieving the parameters", + "backendError.detailsLabel": "Details", + "backendError.serverLabel": "Server", + "backendError.messageLabel": "Message", + "backendError.pathLabel": "Path", + "backendError.showDetails": "Show details", + "backendError.hideDetails": "Hide details", + "backendError.genericMessage": "We were unable to complete your request.", "elementUuid": "Element Uuid", "elementName": "Name", "creator": "Created by", diff --git a/src/translations/fr.json b/src/translations/fr.json index 655f4bb82..ae1dccec6 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -4,6 +4,13 @@ "parameters": "Paramètres", "paramsChangingError": "Une erreur est survenue lors de la modification des paramètres", "paramsRetrievingError": "Une erreur est survenue lors de la récupération des paramètres", + "backendError.detailsLabel": "Détails", + "backendError.serverLabel": "Serveur", + "backendError.messageLabel": "Message", + "backendError.pathLabel": "Chemin", + "backendError.showDetails": "Afficher les détails", + "backendError.hideDetails": "Masquer les détails", + "backendError.genericMessage": "Nous n'avons pas pu finaliser votre requête.", "elementUuid": "Uuid de l'élément", "elementName": "Nom", "creator": "Créé par", From e2a88fd8286252f3298da8d3254f26f9732e5d31 Mon Sep 17 00:00:00 2001 From: benrejebmoh Date: Tue, 30 Sep 2025 15:14:51 +0200 Subject: [PATCH 2/3] close custom snackbar --- src/components/utils/rest-errors.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/utils/rest-errors.ts b/src/components/utils/rest-errors.ts index f9350eb28..a7d39950a 100644 --- a/src/components/utils/rest-errors.ts +++ b/src/components/utils/rest-errors.ts @@ -19,7 +19,6 @@ import { createBackendErrorDetails, extractBackendErrorPayload, type BackendErrorPayload, - type BackendErrorDetails, type SnackInputs, } from '@gridsuite/commons-ui'; import { IntlShape } from 'react-intl'; @@ -29,6 +28,8 @@ const BACKEND_DETAIL_FALLBACK = '-'; const formatBackendDetailValue = (value: string): string => (value.trim().length > 0 ? value : BACKEND_DETAIL_FALLBACK); +type BackendErrorDetails = BackendErrorSnackbarContentProps['details']; + const getBackendErrorDetails = (error: unknown): BackendErrorDetails | undefined => { const backendPayload = extractBackendErrorPayload(error); if (!backendPayload) { @@ -135,8 +136,9 @@ export const snackErrorWithBackendFallback = ( ...(otherSnackProps as SnackInputs), messageTxt: presentation.message, persist: persist ?? true, - content: (_, snackMessage) => + content: (snackbarKey, snackMessage) => createElement(BackendErrorSnackbarContent, { + snackbarKey, message: typeof snackMessage === 'string' && snackMessage.length > 0 ? snackMessage From f21907e406d823eb42dd1655c6b050ae22da3fd2 Mon Sep 17 00:00:00 2001 From: benrejebmoh Date: Tue, 30 Sep 2025 17:34:57 +0200 Subject: [PATCH 3/3] manage snackbar content details by commons-ui --- package-lock.json | 6 +- package.json | 2 +- src/components/utils/rest-errors.ts | 131 +--------------------------- src/translations/en.json | 7 -- src/translations/fr.json | 7 -- 5 files changed, 8 insertions(+), 145 deletions(-) diff --git a/package-lock.json b/package-lock.json index d66aa4525..a014dbe9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@gridsuite/commons-ui": "0.128.0", + "@gridsuite/commons-ui": "file:../commons-ui/gridsuite-commons-ui-0.128.0.tgz", "@hookform/resolvers": "^4.1.3", "@mui/icons-material": "^5.18.0", "@mui/lab": "5.0.0-alpha.175", @@ -3122,8 +3122,8 @@ }, "node_modules/@gridsuite/commons-ui": { "version": "0.128.0", - "resolved": "https://registry.npmjs.org/@gridsuite/commons-ui/-/commons-ui-0.128.0.tgz", - "integrity": "sha512-qWB0mk9y7vq0RLy7eMgL3mgWCtOU6D4BHkUi5YIKFWR3n5fS6iuLJLQco46MD/tA8jUo/kSuCMiJiar2GVOBqw==", + "resolved": "file:../commons-ui/gridsuite-commons-ui-0.128.0.tgz", + "integrity": "sha512-BJdDyEovUefxwQTyQK6rgLc+8VQhWh7YK6XJxUQznpUOTlw0/ZOMI//Lwd7YNoSAhm9E9MCtt/vj81Lzkeyx5Q==", "license": "MPL-2.0", "dependencies": { "@ag-grid-community/locale": "^33.3.2", diff --git a/package.json b/package.json index ea1c60a09..7dc2f800d 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@gridsuite/commons-ui": "0.128.0", + "@gridsuite/commons-ui": "file:../commons-ui/gridsuite-commons-ui-0.128.0.tgz", "@hookform/resolvers": "^4.1.3", "@mui/icons-material": "^5.18.0", "@mui/lab": "5.0.0-alpha.175", diff --git a/src/components/utils/rest-errors.ts b/src/components/utils/rest-errors.ts index a7d39950a..844942f41 100644 --- a/src/components/utils/rest-errors.ts +++ b/src/components/utils/rest-errors.ts @@ -11,72 +11,16 @@ import { HTTP_NOT_FOUND, PermissionCheckResult, } from 'utils/UIconstants'; +import { IntlShape } from 'react-intl'; +import { type Dispatch, SetStateAction } from 'react'; import { - BackendErrorSnackbarContent, - type BackendErrorSnackbarContentProps, ElementAttributes, UseSnackMessageReturn, - createBackendErrorDetails, - extractBackendErrorPayload, + snackErrorWithBackendFallback, type BackendErrorPayload, - type SnackInputs, } from '@gridsuite/commons-ui'; -import { IntlShape } from 'react-intl'; -import { type Dispatch, SetStateAction, createElement } from 'react'; - -const BACKEND_DETAIL_FALLBACK = '-'; - -const formatBackendDetailValue = (value: string): string => (value.trim().length > 0 ? value : BACKEND_DETAIL_FALLBACK); - -type BackendErrorDetails = BackendErrorSnackbarContentProps['details']; -const getBackendErrorDetails = (error: unknown): BackendErrorDetails | undefined => { - const backendPayload = extractBackendErrorPayload(error); - if (!backendPayload) { - return undefined; - } - return createBackendErrorDetails(backendPayload); -}; - -interface BackendErrorPresentation { - message: string; - detailsLabel: string; - detailLabels: BackendErrorSnackbarContentProps['detailLabels']; - formattedDetails: BackendErrorDetails; - showDetailsLabel: string; - hideDetailsLabel: string; -} - -const createBackendErrorPresentation = ( - intl: IntlShape, - details: BackendErrorDetails, - firstLine?: string -): BackendErrorPresentation => { - const message = firstLine ?? intl.formatMessage({ id: 'backendError.genericMessage' }); - const detailsLabel = intl.formatMessage({ id: 'backendError.detailsLabel' }); - const serverLabel = intl.formatMessage({ id: 'backendError.serverLabel' }); - const messageLabel = intl.formatMessage({ id: 'backendError.messageLabel' }); - const pathLabel = intl.formatMessage({ id: 'backendError.pathLabel' }); - const showDetailsLabel = intl.formatMessage({ id: 'backendError.showDetails' }); - const hideDetailsLabel = intl.formatMessage({ id: 'backendError.hideDetails' }); - - return { - message, - detailsLabel, - detailLabels: { - service: serverLabel, - message: messageLabel, - path: pathLabel, - }, - formattedDetails: { - service: formatBackendDetailValue(details.service), - message: formatBackendDetailValue(details.message), - path: formatBackendDetailValue(details.path), - }, - showDetailsLabel, - hideDetailsLabel, - }; -}; +export { snackErrorWithBackendFallback }; export interface ErrorMessageByHttpError { [httpCode: string]: string; @@ -116,73 +60,6 @@ export const handleGenericTxtError = (error: string | Error, snackError: SnackEr }); }; -export const snackErrorWithBackendFallback = ( - error: unknown, - snackError: SnackError, - intl: IntlShape, - additionalSnack?: Partial -) => { - const backendDetails = getBackendErrorDetails(error); - 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, - detailsLabel: presentation.detailsLabel, - 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 (additionalSnack) { - const { messageTxt: additionalMessageTxt, messageId: additionalMessageId } = additionalSnack; - if (additionalMessageTxt !== undefined || additionalMessageId !== undefined) { - snackError(additionalSnack as SnackInputs); - return; - } - } - - 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, - }); -}; - export const handleMaxElementsExceededError = (error: CustomError, snackError: SnackError): boolean => { if (error.status === HTTP_FORBIDDEN && error.message.includes(HTTP_MAX_ELEMENTS_EXCEEDED_MESSAGE)) { const limit = error.message.split(/[: ]+/).pop(); diff --git a/src/translations/en.json b/src/translations/en.json index a4c522f20..336a62adc 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4,13 +4,6 @@ "parameters": "Parameters", "paramsChangingError": "An error occured when changing the parameters", "paramsRetrievingError": "An error occurred while retrieving the parameters", - "backendError.detailsLabel": "Details", - "backendError.serverLabel": "Server", - "backendError.messageLabel": "Message", - "backendError.pathLabel": "Path", - "backendError.showDetails": "Show details", - "backendError.hideDetails": "Hide details", - "backendError.genericMessage": "We were unable to complete your request.", "elementUuid": "Element Uuid", "elementName": "Name", "creator": "Created by", diff --git a/src/translations/fr.json b/src/translations/fr.json index ae1dccec6..655f4bb82 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -4,13 +4,6 @@ "parameters": "Paramètres", "paramsChangingError": "Une erreur est survenue lors de la modification des paramètres", "paramsRetrievingError": "Une erreur est survenue lors de la récupération des paramètres", - "backendError.detailsLabel": "Détails", - "backendError.serverLabel": "Serveur", - "backendError.messageLabel": "Message", - "backendError.pathLabel": "Chemin", - "backendError.showDetails": "Afficher les détails", - "backendError.hideDetails": "Masquer les détails", - "backendError.genericMessage": "Nous n'avons pas pu finaliser votre requête.", "elementUuid": "Uuid de l'élément", "elementName": "Nom", "creator": "Créé par",