Define server error messages to display in FE from the server (#12973)
Currently, when a server query or mutation from the front-end fails, the error message defined server-side is displayed in a snackbar in the front-end. These error messages usually contain technical details that don't belong to the user interface, such as "ObjectMetadataCollection not found" or "invalid ENUM value for ...". **BE** In addition to the original error message that is still needed (for the request response, debugging, sentry monitoring etc.), we add a `displayedErrorMessage` that will be used in the snackbars. It's only relevant to add it for the messages that will reach the FE (ie. not in jobs or in rest api for instance) and if it can help the user sort out / fix things (ie. we do add displayedErrorMessage for "Cannot create multiple draft versions for the same workflow" or "Cannot delete [field], please update the label identifier field first", but not "Object metadata does not exist"), even if in practice in the FE users should not be able to perform an action that will not work (ie should not be able to save creation of multiple draft versions of the same workflows). **FE** To ease the usage we replaced enqueueSnackBar with enqueueErrorSnackBar and enqueueSuccessSnackBar with an api that only requires to pass on the error. If no displayedErrorMessage is specified then the default error message is `An error occured.`
This commit is contained in:
@ -6,7 +6,6 @@ import {
|
||||
} from '@/object-record/hooks/useCreateManyRecords';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { ApolloError } from '@apollo/client';
|
||||
import { t } from '@lingui/core/macro';
|
||||
@ -43,7 +42,7 @@ export const useBatchCreateManyRecords = <
|
||||
objectMetadataNamePlural: objectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const { enqueueWarningSnackBar } = useSnackBar();
|
||||
|
||||
const batchCreateManyRecords = async ({
|
||||
recordsToCreate,
|
||||
@ -84,13 +83,12 @@ export const useBatchCreateManyRecords = <
|
||||
} catch (error) {
|
||||
if (error instanceof ApolloError && error.message.includes('aborted')) {
|
||||
const formattedCreatedRecordsCount = formatNumber(createdRecordsCount);
|
||||
enqueueSnackBar(
|
||||
t`Record creation stopped. ${formattedCreatedRecordsCount} records created.`,
|
||||
{
|
||||
variant: SnackBarVariant.Warning,
|
||||
enqueueWarningSnackBar({
|
||||
message: t`Record creation stopped. ${formattedCreatedRecordsCount} records created.`,
|
||||
options: {
|
||||
duration: 5000,
|
||||
},
|
||||
);
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { ApolloError } from '@apollo/client';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
||||
@ -121,7 +122,7 @@ export const useDeleteOneRecord = ({
|
||||
});
|
||||
},
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
.catch((error: ApolloError) => {
|
||||
if (!shouldHandleOptimisticCache) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ import { RecordGqlOperationFindDuplicatesResult } from '@/object-record/graphql/
|
||||
import { useFindDuplicateRecordsQuery } from '@/object-record/hooks/useFindDuplicatesRecordsQuery';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { getFindDuplicateRecordsQueryResponseField } from '@/object-record/utils/getFindDuplicateRecordsQueryResponseField';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { logError } from '~/utils/logError';
|
||||
|
||||
@ -36,7 +35,7 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
|
||||
const queryResponseField = getFindDuplicateRecordsQueryResponseField(
|
||||
objectMetadataItem.nameSingular,
|
||||
@ -59,8 +58,8 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
`useFindDuplicateRecords for "${objectMetadataItem.nameSingular}" error : ` +
|
||||
error,
|
||||
);
|
||||
enqueueSnackBar(`Error finding duplicates:", ${error.message}`, {
|
||||
variant: SnackBarVariant.Error,
|
||||
enqueueErrorSnackBar({
|
||||
apolloError: error,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { ApolloError } from '@apollo/client';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { logError } from '~/utils/logError';
|
||||
import { useCallback } from 'react';
|
||||
import { logError } from '~/utils/logError';
|
||||
|
||||
export const useHandleFindManyRecordsError = ({
|
||||
handleError,
|
||||
@ -13,7 +12,7 @@ export const useHandleFindManyRecordsError = ({
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
handleError?: (error?: Error) => void;
|
||||
}) => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
|
||||
const handleFindManyRecordsError = useCallback(
|
||||
(error: ApolloError) => {
|
||||
@ -21,12 +20,12 @@ export const useHandleFindManyRecordsError = ({
|
||||
`useFindManyRecords for "${objectMetadataItem.namePlural}" error : ` +
|
||||
error,
|
||||
);
|
||||
enqueueSnackBar(`${error.message}`, {
|
||||
variant: SnackBarVariant.Error,
|
||||
enqueueErrorSnackBar({
|
||||
apolloError: error,
|
||||
});
|
||||
handleError?.(error);
|
||||
},
|
||||
[enqueueSnackBar, handleError, objectMetadataItem.namePlural],
|
||||
[enqueueErrorSnackBar, handleError, objectMetadataItem.namePlural],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@ -3,7 +3,6 @@ import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { WatchQueryFetchPolicy } from '@apollo/client';
|
||||
import { useMemo } from 'react';
|
||||
@ -34,10 +33,9 @@ export const useObjectRecordSearchRecords = ({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const { data, loading, error, previousData } = useSearchQuery({
|
||||
skip:
|
||||
skip ||
|
||||
@ -57,12 +55,9 @@ export const useObjectRecordSearchRecords = ({
|
||||
`useSearchRecords for "${objectMetadataItem.namePlural}" error : ` +
|
||||
error,
|
||||
);
|
||||
enqueueSnackBar(
|
||||
`Error during useSearchRecords for "${objectMetadataItem.namePlural}", ${error.message}`,
|
||||
{
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
enqueueErrorSnackBar({
|
||||
apolloError: error,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropd
|
||||
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
|
||||
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
@ -65,7 +64,7 @@ export const ObjectOptionsDropdownMenuContent = () => {
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const { enqueueSuccessSnackBar } = useSnackBar();
|
||||
|
||||
const isDefaultView = currentView?.key === 'INDEX';
|
||||
|
||||
@ -171,10 +170,12 @@ export const ObjectOptionsDropdownMenuContent = () => {
|
||||
onEnter={() => {
|
||||
const currentUrl = window.location.href;
|
||||
navigator.clipboard.writeText(currentUrl);
|
||||
enqueueSnackBar('Link copied to clipboard', {
|
||||
variant: SnackBarVariant.Success,
|
||||
icon: <IconCopy size={theme.icon.size.md} />,
|
||||
duration: 2000,
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Link copied to clipboard`,
|
||||
options: {
|
||||
icon: <IconCopy size={theme.icon.size.md} />,
|
||||
duration: 2000,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
@ -183,10 +184,12 @@ export const ObjectOptionsDropdownMenuContent = () => {
|
||||
onClick={() => {
|
||||
const currentUrl = window.location.href;
|
||||
navigator.clipboard.writeText(currentUrl);
|
||||
enqueueSnackBar('Link copied to clipboard', {
|
||||
variant: SnackBarVariant.Success,
|
||||
icon: <IconCopy size={theme.icon.size.md} />,
|
||||
duration: 2000,
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Link copied to clipboard`,
|
||||
options: {
|
||||
icon: <IconCopy size={theme.icon.size.md} />,
|
||||
duration: 2000,
|
||||
},
|
||||
});
|
||||
}}
|
||||
LeftIcon={IconCopy}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconCopy } from 'twenty-ui/display';
|
||||
@ -16,7 +15,7 @@ export type LightCopyIconButtonProps = {
|
||||
};
|
||||
|
||||
export const LightCopyIconButton = ({ copyText }: LightCopyIconButtonProps) => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const { enqueueSuccessSnackBar } = useSnackBar();
|
||||
const theme = useTheme();
|
||||
const { t } = useLingui();
|
||||
|
||||
@ -25,10 +24,12 @@ export const LightCopyIconButton = ({ copyText }: LightCopyIconButtonProps) => {
|
||||
<LightIconButton
|
||||
Icon={IconCopy}
|
||||
onClick={() => {
|
||||
enqueueSnackBar(t`Text copied to clipboard`, {
|
||||
variant: SnackBarVariant.Success,
|
||||
icon: <IconCopy size={theme.icon.size.md} />,
|
||||
duration: 2000,
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Text copied to clipboard`,
|
||||
options: {
|
||||
icon: <IconCopy size={theme.icon.size.md} />,
|
||||
duration: 2000,
|
||||
},
|
||||
});
|
||||
navigator.clipboard.writeText(copyText);
|
||||
}}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||
import { usePhonesFieldDisplay } from '@/object-record/record-field/meta-types/hooks/usePhonesFieldDisplay';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { PhonesDisplay } from '@/ui/field/display/components/PhonesDisplay';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
@ -12,7 +11,7 @@ export const PhonesFieldDisplay = () => {
|
||||
|
||||
const { isFocused } = useFieldFocus();
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
@ -29,16 +28,20 @@ export const PhonesFieldDisplay = () => {
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(phoneNumber);
|
||||
enqueueSnackBar(t`Phone number copied to clipboard`, {
|
||||
variant: SnackBarVariant.Success,
|
||||
icon: <IconCircleCheck size={16} color="green" />,
|
||||
duration: 2000,
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Phone number copied to clipboard`,
|
||||
options: {
|
||||
icon: <IconCircleCheck size={16} color="green" />,
|
||||
duration: 2000,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
enqueueSnackBar(t`Error copying to clipboard`, {
|
||||
variant: SnackBarVariant.Error,
|
||||
icon: <IconExclamationCircle size={16} color="red" />,
|
||||
duration: 2000,
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Error copying to clipboard`,
|
||||
options: {
|
||||
icon: <IconExclamationCircle size={16} color="red" />,
|
||||
duration: 2000,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -8,7 +8,6 @@ import { SpreadsheetImportCreateRecordsBatchSize } from '@/spreadsheet-import/co
|
||||
import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog';
|
||||
import { spreadsheetImportCreatedRecordsProgressState } from '@/spreadsheet-import/states/spreadsheetImportCreatedRecordsProgressState';
|
||||
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
@ -17,7 +16,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
||||
objectNameSingular: string,
|
||||
) => {
|
||||
const { openSpreadsheetImportDialog } = useOpenSpreadsheetImportDialog<any>();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
@ -79,8 +78,8 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
|
||||
upsert: true,
|
||||
});
|
||||
} catch (error: any) {
|
||||
enqueueSnackBar(error?.message || 'Something went wrong', {
|
||||
variant: SnackBarVariant.Error,
|
||||
enqueueErrorSnackBar({
|
||||
apolloError: error,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user