Fix Snackbar crashing the whole app (#15774)
This PR fixes a crash of the app when a Snackbar tries to render an object. <img width="3002" height="754" alt="image" src="https://github.com/user-attachments/assets/988f97eb-5c8a-44f8-ac8e-e2953b872bac" /> What happened : the backend sent a MessageDescriptor in `extensions.userFriendlyMessage` while it should have sent a translated string. But it is a problem that the Snackbar can end up rendering objects and crashing the whole app because it is a the highest level in the tree. So this PR hardens many points to avoid future bugs : - Sanitize what is rendered by the Snackbar to avoid any crash - Translate any MessageDescriptor object that could end up in the frontend - Fix the places in the backend where a MessageDescriptor wasn't translated and sent directly to the frontend in `use-graphql-error-handler.hook.ts` Fixes https://github.com/twentyhq/twenty/issues/15685 Fixes https://github.com/twentyhq/core-team-issues/issues/1869
This commit is contained in:
+12
-5
@@ -1,3 +1,4 @@
|
||||
import { sanitizeMessageToRenderInSnackbar } from '@/ui/feedback/snack-bar-manager/utils/sanitizeMessageToRenderInSnackbar';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
@@ -196,13 +197,19 @@ export const SnackBar = ({
|
||||
}
|
||||
};
|
||||
|
||||
const sanitizedMessage = sanitizeMessageToRenderInSnackbar(message);
|
||||
const sanitizedDetailedMessage =
|
||||
sanitizeMessageToRenderInSnackbar(detailedMessage);
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
aria-live={role === 'alert' ? 'assertive' : 'polite'}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
title={message || defaultAriaLabelByVariant[variant]}
|
||||
{...{ className, id, role, variant }}
|
||||
title={sanitizedMessage ?? defaultAriaLabelByVariant[variant]}
|
||||
className={className}
|
||||
id={id}
|
||||
role={role}
|
||||
data-globally-prevent-click-outside
|
||||
>
|
||||
<StyledProgressBar
|
||||
@@ -211,7 +218,7 @@ export const SnackBar = ({
|
||||
/>
|
||||
<StyledHeader>
|
||||
<StyledIcon>{icon}</StyledIcon>
|
||||
<StyledMessage>{message}</StyledMessage>
|
||||
<StyledMessage>{sanitizedMessage ?? ''}</StyledMessage>
|
||||
<StyledActions>
|
||||
{!!onCancel && <LightButton title={t`Cancel`} onClick={onCancel} />}
|
||||
|
||||
@@ -220,8 +227,8 @@ export const SnackBar = ({
|
||||
)}
|
||||
</StyledActions>
|
||||
</StyledHeader>
|
||||
{detailedMessage && (
|
||||
<StyledDescription>{detailedMessage}</StyledDescription>
|
||||
{isDefined(sanitizedDetailedMessage) && (
|
||||
<StyledDescription>{sanitizedDetailedMessage}</StyledDescription>
|
||||
)}
|
||||
{link && <StyledLink to={link.href}>{link.text}</StyledLink>}
|
||||
</StyledContainer>
|
||||
|
||||
@@ -142,6 +142,7 @@ export const useSnackBar = () => {
|
||||
: apolloError
|
||||
? getErrorMessageFromApolloError(apolloError)
|
||||
: t`An error occurred.`;
|
||||
|
||||
setSnackBarQueue({
|
||||
id: uuidv4(),
|
||||
message: errorMessage,
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const sanitizeMessageToRenderInSnackbar = (
|
||||
messageToRenderInSnackbar: any,
|
||||
) => {
|
||||
if (!isDefined(messageToRenderInSnackbar)) {
|
||||
return null;
|
||||
} else if (
|
||||
typeof messageToRenderInSnackbar === 'string' ||
|
||||
typeof messageToRenderInSnackbar === 'string' ||
|
||||
typeof messageToRenderInSnackbar === 'boolean'
|
||||
) {
|
||||
return `${messageToRenderInSnackbar}`;
|
||||
} else if (typeof messageToRenderInSnackbar === 'object') {
|
||||
try {
|
||||
return JSON.stringify(messageToRenderInSnackbar);
|
||||
} catch {
|
||||
return t`Cannot display message`;
|
||||
}
|
||||
} else {
|
||||
return t`Cannot display message`;
|
||||
}
|
||||
};
|
||||
@@ -1,14 +1,20 @@
|
||||
import { type ApolloError } from '@apollo/client';
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type Nullable } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const getErrorMessageFromApolloError = (error: ApolloError): string => {
|
||||
if (!isDefined(error.graphQLErrors?.[0]?.extensions?.userFriendlyMessage)) {
|
||||
const userFriendlyMessage = error.graphQLErrors?.[0]?.extensions
|
||||
?.userFriendlyMessage as Nullable<MessageDescriptor | string>;
|
||||
|
||||
if (!isDefined(userFriendlyMessage)) {
|
||||
return t`An error occurred.`;
|
||||
}
|
||||
|
||||
return (
|
||||
(error.graphQLErrors[0].extensions?.userFriendlyMessage as string) ??
|
||||
t`An error occurred.`
|
||||
);
|
||||
if (typeof userFriendlyMessage === 'object' && 'id' in userFriendlyMessage) {
|
||||
return t(userFriendlyMessage);
|
||||
}
|
||||
|
||||
return userFriendlyMessage;
|
||||
};
|
||||
|
||||
+10
-2
@@ -248,6 +248,9 @@ export const useGraphQLErrorHandlerHook = <
|
||||
onValidate: ({ context, validateFn, params: { documentAST, schema } }) => {
|
||||
const errors = validateFn(schema, documentAST);
|
||||
|
||||
const userLocale = context.req.locale ?? SOURCE_LOCALE;
|
||||
const i18n = options.i18nService.getI18nInstance(userLocale);
|
||||
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
const headers = context.req.headers;
|
||||
const currentMetadataVersion = context.req.workspaceMetadataVersion;
|
||||
@@ -267,9 +270,12 @@ export const useGraphQLErrorHandlerHook = <
|
||||
options.metricsService.incrementCounter({
|
||||
key: MetricsKeys.SchemaVersionMismatch,
|
||||
});
|
||||
|
||||
throw new GraphQLError(SCHEMA_MISMATCH_ERROR, {
|
||||
extensions: {
|
||||
userFriendlyMessage: msg`Your workspace has been updated with a new data model. Please refresh the page.`,
|
||||
userFriendlyMessage: i18n._(
|
||||
msg`Your workspace has been updated with a new data model. Please refresh the page.`,
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -297,7 +303,9 @@ export const useGraphQLErrorHandlerHook = <
|
||||
throw new GraphQLError(APP_VERSION_MISMATCH_ERROR, {
|
||||
extensions: {
|
||||
code: APP_VERSION_MISMATCH_CODE,
|
||||
userFriendlyMessage: msg`Your app version is out of date. Please refresh the page to continue.`,
|
||||
userFriendlyMessage: i18n._(
|
||||
msg`Your app version is out of date. Please refresh the page to continue.`,
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user