diff --git a/packages/twenty-front/src/hooks/useCopyToClipboard.tsx b/packages/twenty-front/src/hooks/useCopyToClipboard.tsx index 59776c0e7..588590444 100644 --- a/packages/twenty-front/src/hooks/useCopyToClipboard.tsx +++ b/packages/twenty-front/src/hooks/useCopyToClipboard.tsx @@ -1,4 +1,3 @@ -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useTheme } from '@emotion/react'; import { useLingui } from '@lingui/react/macro'; @@ -6,23 +5,27 @@ import { IconCopy, IconExclamationCircle } from 'twenty-ui/display'; export const useCopyToClipboard = () => { const theme = useTheme(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); const { t } = useLingui(); const copyToClipboard = async (valueAsString: string) => { try { await navigator.clipboard.writeText(valueAsString); - enqueueSnackBar(t`Copied to clipboard`, { - variant: SnackBarVariant.Success, - icon: , - duration: 2000, + enqueueSuccessSnackBar({ + message: t`Copied to clipboard`, + options: { + icon: , + duration: 2000, + }, }); } catch { - enqueueSnackBar(t`Couldn't copy to clipboard`, { - variant: SnackBarVariant.Error, - icon: , - duration: 2000, + enqueueErrorSnackBar({ + message: t`Couldn't copy to clipboard`, + options: { + icon: , + duration: 2000, + }, }); } }; diff --git a/packages/twenty-front/src/modules/activities/hooks/useCustomResolver.ts b/packages/twenty-front/src/modules/activities/hooks/useCustomResolver.ts index ee1e50772..273b85f75 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useCustomResolver.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useCustomResolver.ts @@ -1,14 +1,13 @@ -import { useState } from 'react'; import { DocumentNode, OperationVariables, TypedDocumentNode, useQuery, } from '@apollo/client'; +import { useState } from 'react'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; type CustomResolverQueryResult< @@ -37,7 +36,7 @@ export const useCustomResolver = < isFetchingMore: boolean; fetchMoreRecords: () => Promise; } => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const [page, setPage] = useState({ pageNumber: 1, @@ -62,8 +61,8 @@ export const useCustomResolver = < } = useQuery>(query, { variables: queryVariables, onError: (error) => { - enqueueSnackBar(error.message || `Error loading ${objectName}`, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error, }); }, }); diff --git a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx index 640424baf..fad0b7d57 100644 --- a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx +++ b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx @@ -1,6 +1,5 @@ import { useAuth } from '@/auth/hooks/useAuth'; import { AppPath } from '@/types/AppPath'; -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'; @@ -20,7 +19,7 @@ import { EmailVerificationSent } from '../sign-in-up/components/EmailVerificatio export const VerifyEmailEffect = () => { const { getLoginTokenFromEmailVerificationToken } = useAuth(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar(); const [searchParams] = useSearchParams(); const [isError, setIsError] = useState(false); @@ -39,9 +38,11 @@ export const VerifyEmailEffect = () => { useEffect(() => { const verifyEmailToken = async () => { if (!email || !emailVerificationToken) { - enqueueSnackBar(t`Invalid email verification link.`, { - dedupeKey: 'email-verification-link-dedupe-key', - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: t`Invalid email verification link.`, + options: { + dedupeKey: 'email-verification-link-dedupe-key', + }, }); return navigate(AppPath.SignInUp); } @@ -53,9 +54,11 @@ export const VerifyEmailEffect = () => { email, ); - enqueueSnackBar(t`Email verified.`, { - dedupeKey: 'email-verification-dedupe-key', - variant: SnackBarVariant.Success, + enqueueSuccessSnackBar({ + message: t`Email verified.`, + options: { + dedupeKey: 'email-verification-dedupe-key', + }, }); const workspaceUrl = getWorkspaceUrl(workspaceUrls); @@ -71,14 +74,13 @@ export const VerifyEmailEffect = () => { verifyLoginToken(loginToken.token); } catch (error) { - const message: string = - error instanceof ApolloError - ? error.message - : 'Email verification failed'; - - enqueueSnackBar(t`${message}`, { - dedupeKey: 'email-verification-error-dedupe-key', - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + ...(error instanceof ApolloError + ? { apolloError: error } + : { message: t`Email verification failed` }), + options: { + dedupeKey: 'email-verification-error-dedupe-key', + }, }); if ( error instanceof ApolloError && diff --git a/packages/twenty-front/src/modules/auth/hooks/__tests__/useVerifyLogin.test.ts b/packages/twenty-front/src/modules/auth/hooks/__tests__/useVerifyLogin.test.ts index 4df3160d7..8ff6d1494 100644 --- a/packages/twenty-front/src/modules/auth/hooks/__tests__/useVerifyLogin.test.ts +++ b/packages/twenty-front/src/modules/auth/hooks/__tests__/useVerifyLogin.test.ts @@ -4,14 +4,13 @@ import { renderHook } from '@testing-library/react'; import { RecoilRoot } from 'recoil'; import { AppPath } from '@/types/AppPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useNavigateApp } from '~/hooks/useNavigateApp'; import { useAuth } from '../useAuth'; import { useVerifyLogin } from '../useVerifyLogin'; -import { dynamicActivate } from '~/utils/i18n/dynamicActivate'; import { SOURCE_LOCALE } from 'twenty-shared/translations'; +import { dynamicActivate } from '~/utils/i18n/dynamicActivate'; jest.mock('../useAuth', () => ({ useAuth: jest.fn(), @@ -37,7 +36,7 @@ const renderHooks = () => { describe('useVerifyLogin', () => { const mockGetAuthTokensFromLoginToken = jest.fn(); - const mockEnqueueSnackBar = jest.fn(); + const mockEnqueueErrorSnackBar = jest.fn(); const mockNavigate = jest.fn(); beforeEach(() => { @@ -48,7 +47,7 @@ describe('useVerifyLogin', () => { }); (useSnackBar as jest.Mock).mockReturnValue({ - enqueueSnackBar: mockEnqueueSnackBar, + enqueueErrorSnackBar: mockEnqueueErrorSnackBar, }); (useNavigateApp as jest.Mock).mockReturnValue(mockNavigate); @@ -70,8 +69,8 @@ describe('useVerifyLogin', () => { await result.current.verifyLoginToken('test-token'); - expect(mockEnqueueSnackBar).toHaveBeenCalledWith('Authentication failed', { - variant: SnackBarVariant.Error, + expect(mockEnqueueErrorSnackBar).toHaveBeenCalledWith({ + message: 'Authentication failed', }); expect(mockNavigate).toHaveBeenCalledWith(AppPath.SignInUp); }); diff --git a/packages/twenty-front/src/modules/auth/hooks/useVerifyLogin.ts b/packages/twenty-front/src/modules/auth/hooks/useVerifyLogin.ts index 6862c1753..dcca65900 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useVerifyLogin.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useVerifyLogin.ts @@ -1,5 +1,3 @@ -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; - import { useAuth } from '@/auth/hooks/useAuth'; import { AppPath } from '@/types/AppPath'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; @@ -7,7 +5,7 @@ import { useLingui } from '@lingui/react/macro'; import { useNavigateApp } from '~/hooks/useNavigateApp'; export const useVerifyLogin = () => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const navigate = useNavigateApp(); const { getAuthTokensFromLoginToken } = useAuth(); const { t } = useLingui(); @@ -16,8 +14,8 @@ export const useVerifyLogin = () => { try { await getAuthTokensFromLoginToken(loginToken); } catch (error) { - enqueueSnackBar(t`Authentication failed`, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: t`Authentication failed`, }); navigate(AppPath.SignInUp); } diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useHandleResetPassword.test.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useHandleResetPassword.test.ts index 40fbdfe83..922213e24 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useHandleResetPassword.test.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useHandleResetPassword.test.ts @@ -5,7 +5,6 @@ import { RecoilRoot } from 'recoil'; import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword'; import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { SOURCE_LOCALE } from 'twenty-shared/translations'; import { @@ -36,14 +35,16 @@ const renderHooks = () => { }; describe('useHandleResetPassword', () => { - const enqueueSnackBarMock = jest.fn(); + const enqueueErrorSnackBarMock = jest.fn(); + const enqueueSuccessSnackBarMock = jest.fn(); const emailPasswordResetLinkMock = jest.fn(); beforeEach(() => { jest.clearAllMocks(); (useSnackBar as jest.Mock).mockReturnValue({ - enqueueSnackBar: enqueueSnackBarMock, + enqueueErrorSnackBar: enqueueErrorSnackBarMock, + enqueueSuccessSnackBar: enqueueSuccessSnackBarMock, }); (useEmailPasswordResetLinkMutation as jest.Mock).mockReturnValue([ emailPasswordResetLinkMock, @@ -54,8 +55,8 @@ describe('useHandleResetPassword', () => { const { result } = renderHooks(); await act(() => result.current.handleResetPassword('')()); - expect(enqueueSnackBarMock).toHaveBeenCalledWith('Invalid email', { - variant: SnackBarVariant.Error, + expect(enqueueErrorSnackBarMock).toHaveBeenCalledWith({ + message: 'Invalid email', }); }); @@ -67,10 +68,9 @@ describe('useHandleResetPassword', () => { const { result } = renderHooks(); await act(() => result.current.handleResetPassword('test@example.com')()); - expect(enqueueSnackBarMock).toHaveBeenCalledWith( - 'Password reset link has been sent to the email', - { variant: SnackBarVariant.Success }, - ); + expect(enqueueSuccessSnackBarMock).toHaveBeenCalledWith({ + message: 'Password reset link has been sent to the email', + }); }); it('should show error message if sending reset link fails', async () => { @@ -81,9 +81,7 @@ describe('useHandleResetPassword', () => { const { result } = renderHooks(); await act(() => result.current.handleResetPassword('test@example.com')()); - expect(enqueueSnackBarMock).toHaveBeenCalledWith('There was an issue', { - variant: SnackBarVariant.Error, - }); + expect(enqueueErrorSnackBarMock).toHaveBeenCalledWith({}); }); it('should show error message in case of request error', async () => { @@ -93,8 +91,6 @@ describe('useHandleResetPassword', () => { const { result } = renderHooks(); await act(() => result.current.handleResetPassword('test@example.com')()); - expect(enqueueSnackBarMock).toHaveBeenCalledWith(errorMessage, { - variant: SnackBarVariant.Error, - }); + expect(enqueueErrorSnackBarMock).toHaveBeenCalledWith({}); }); }); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSSO.test.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSSO.test.tsx index d3022d7bf..8a50396ed 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSSO.test.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSSO.test.tsx @@ -2,19 +2,20 @@ import { GET_AUTHORIZATION_URL_FOR_SSO } from '@/auth/graphql/mutations/getAutho import { useSSO } from '@/auth/sign-in-up/hooks/useSSO'; import { useRedirect } from '@/domain-manager/hooks/useRedirect'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { ApolloError } from '@apollo/client'; import { MockedProvider } from '@apollo/client/testing'; -import { MemoryRouter } from 'react-router-dom'; import { renderHook } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar'); jest.mock('@/domain-manager/hooks/useRedirect'); jest.mock('~/generated/graphql'); -const mockEnqueueSnackBar = jest.fn(); +const mockEnqueueErrorSnackBar = jest.fn(); const mockRedirect = jest.fn(); (useSnackBar as jest.Mock).mockReturnValue({ - enqueueSnackBar: mockEnqueueSnackBar, + enqueueErrorSnackBar: mockEnqueueErrorSnackBar, }); (useRedirect as jest.Mock).mockReturnValue({ redirect: mockRedirect, @@ -84,8 +85,10 @@ describe('useSSO', () => { await result.current.redirectToSSOLoginPage(identityProviderId); - expect(mockEnqueueSnackBar).toHaveBeenCalledWith('Error message', { - variant: 'error', + expect(mockEnqueueErrorSnackBar).toHaveBeenCalledWith({ + apolloError: new ApolloError({ + graphQLErrors: [{ message: 'Error message' }], + }), }); }); }); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken.ts index 757b84995..69b3625cd 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken.ts @@ -1,13 +1,13 @@ import { useCallback } from 'react'; import { useOrigin } from '@/domain-manager/hooks/useOrigin'; -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'; import { useResendEmailVerificationTokenMutation } from '~/generated-metadata/graphql'; export const useHandleResendEmailVerificationToken = () => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar(); const [resendEmailVerificationToken, { loading }] = useResendEmailVerificationTokenMutation(); const { origin } = useOrigin(); @@ -16,8 +16,8 @@ export const useHandleResendEmailVerificationToken = () => { (email: string | null) => { return async () => { if (!email) { - enqueueSnackBar(t`Invalid email`, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: t`Invalid email`, }); return; } @@ -31,22 +31,25 @@ export const useHandleResendEmailVerificationToken = () => { }); if (data?.resendEmailVerificationToken?.success === true) { - enqueueSnackBar(t`Email verification link resent!`, { - variant: SnackBarVariant.Success, + enqueueSuccessSnackBar({ + message: t`Email verification link resent!`, }); } else { - enqueueSnackBar(t`There was an issue`, { - variant: SnackBarVariant.Error, - }); + enqueueErrorSnackBar({}); } } catch (error) { - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + ...(error instanceof ApolloError ? { apolloError: error } : {}), }); } }; }, - [enqueueSnackBar, resendEmailVerificationToken, origin], + [ + enqueueErrorSnackBar, + enqueueSuccessSnackBar, + resendEmailVerificationToken, + origin, + ], ); return { handleResendEmailVerificationToken, loading }; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts index b14c244dd..bbb94c694 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts @@ -2,14 +2,14 @@ import { useCallback } from 'react'; import { currentUserState } from '@/auth/states/currentUserState'; import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState'; -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 { useLingui } from '@lingui/react/macro'; import { useRecoilValue } from 'recoil'; import { useEmailPasswordResetLinkMutation } from '~/generated-metadata/graphql'; export const useHandleResetPassword = () => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar(); const [emailPasswordResetLink] = useEmailPasswordResetLinkMutation(); const workspacePublicData = useRecoilValue(workspacePublicDataState); const currentUser = useRecoilValue(currentUserState); @@ -20,15 +20,15 @@ export const useHandleResetPassword = () => { (email = currentUser?.email) => { return async () => { if (!email) { - enqueueSnackBar(t`Invalid email`, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: t`Invalid email`, }); return; } if (!workspacePublicData?.id) { - enqueueSnackBar(t`Invalid workspace`, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: t`Invalid workspace`, }); return; } @@ -39,17 +39,15 @@ export const useHandleResetPassword = () => { }); if (data?.emailPasswordResetLink?.success === true) { - enqueueSnackBar(t`Password reset link has been sent to the email`, { - variant: SnackBarVariant.Success, + enqueueSuccessSnackBar({ + message: t`Password reset link has been sent to the email`, }); } else { - enqueueSnackBar(t`There was an issue`, { - variant: SnackBarVariant.Error, - }); + enqueueErrorSnackBar({}); } } catch (error) { - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + ...(error instanceof ApolloError ? { apolloError: error } : {}), }); } }; @@ -57,7 +55,8 @@ export const useHandleResetPassword = () => { [ currentUser?.email, workspacePublicData?.id, - enqueueSnackBar, + enqueueErrorSnackBar, + enqueueSuccessSnackBar, t, emailPasswordResetLink, ], diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSSO.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSSO.ts index 8db029fc9..f36fc6bfd 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSSO.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSSO.ts @@ -2,16 +2,15 @@ import { GET_AUTHORIZATION_URL_FOR_SSO } from '@/auth/graphql/mutations/getAuthorizationUrlForSSO'; import { useRedirect } from '@/domain-manager/hooks/useRedirect'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { useApolloClient } from '@apollo/client'; +import { ApolloError, useApolloClient } from '@apollo/client'; import { useParams } from 'react-router-dom'; export const useSSO = () => { const apolloClient = useApolloClient(); const workspaceInviteHash = useParams().workspaceInviteHash; - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const { redirect } = useRedirect(); const redirectToSSOLoginPage = async (identityProviderId: string) => { let authorizationUrlForSSOResult; @@ -26,8 +25,8 @@ export const useSSO = () => { }, }); } catch (error: any) { - return enqueueSnackBar(error?.message ?? 'Unknown error', { - variant: SnackBarVariant.Error, + return enqueueErrorSnackBar({ + ...(error instanceof ApolloError ? { apolloError: error } : {}), }); } diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts index 9a70346e4..a2f5d6e33 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts @@ -11,17 +11,17 @@ import { import { SignInUpMode } from '@/auth/types/signInUpMode'; import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken'; import { useBuildSearchParamsFromUrlSyncedStates } from '@/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates'; +import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace'; import { AppPath } from '@/types/AppPath'; -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 { useRecoilState } from 'recoil'; import { buildAppPathWithQueryParams } from '~/utils/buildAppPathWithQueryParams'; import { isMatchingLocation } from '~/utils/isMatchingLocation'; import { useAuth } from '../../hooks/useAuth'; -import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace'; export const useSignInUp = (form: UseFormReturn
) => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState); const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState); @@ -66,9 +66,7 @@ export const useSignInUp = (form: UseFormReturn) => { captchaToken: token, }, onError: (error) => { - enqueueSnackBar(`${error.message}`, { - variant: SnackBarVariant.Error, - }); + enqueueErrorSnackBar({ apolloError: error }); }, onCompleted: (data) => { setSignInUpMode( @@ -83,7 +81,7 @@ export const useSignInUp = (form: UseFormReturn) => { readCaptchaToken, form, checkUserExistsQuery, - enqueueSnackBar, + enqueueErrorSnackBar, setSignInUpStep, setSignInUpMode, ]); @@ -145,9 +143,9 @@ export const useSignInUp = (form: UseFormReturn) => { captchaToken: token, verifyEmailNextPath, }); - } catch (err: any) { - enqueueSnackBar(err?.message, { - variant: SnackBarVariant.Error, + } catch (error: any) { + enqueueErrorSnackBar({ + ...(error instanceof ApolloError ? { apolloError: error } : {}), }); } }, @@ -161,7 +159,7 @@ export const useSignInUp = (form: UseFormReturn) => { signUpWithCredentialsInWorkspace, workspaceInviteHash, workspacePersonalInviteToken, - enqueueSnackBar, + enqueueErrorSnackBar, buildSearchParamsFromUrlSyncedStates, isOnAWorkspace, ], diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignUpInNewWorkspace.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignUpInNewWorkspace.ts index 17690aaaf..94aea9d9c 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignUpInNewWorkspace.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignUpInNewWorkspace.ts @@ -1,13 +1,13 @@ import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; import { AppPath } from '@/types/AppPath'; -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 { useSignUpInNewWorkspaceMutation } from '~/generated-metadata/graphql'; import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; export const useSignUpInNewWorkspace = () => { const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const [signUpInNewWorkspaceMutation] = useSignUpInNewWorkspaceMutation(); @@ -23,10 +23,8 @@ export const useSignUpInNewWorkspace = () => { newTab ? '_blank' : '_self', ); }, - onError: (error: Error) => { - enqueueSnackBar(error.message, { - variant: SnackBarVariant.Error, - }); + onError: (error: ApolloError) => { + enqueueErrorSnackBar({ apolloError: error }); }, }); }; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts index ab524e037..e0f535f95 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts @@ -4,16 +4,16 @@ import { useRecoilValue } from 'recoil'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { AppPath } from '@/types/AppPath'; +import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; import { useGetWorkspaceFromInviteHashQuery } from '~/generated-metadata/graphql'; import { useNavigateApp } from '~/hooks/useNavigateApp'; export const useWorkspaceFromInviteHash = () => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar, enqueueInfoSnackBar } = useSnackBar(); const navigate = useNavigateApp(); const workspaceInviteHash = useParams().workspaceInviteHash; const currentWorkspace = useRecoilValue(currentWorkspaceState); @@ -24,9 +24,7 @@ export const useWorkspaceFromInviteHash = () => { skip: !workspaceInviteHash, variables: { inviteHash: workspaceInviteHash || '' }, onError: (error) => { - enqueueSnackBar(error.message, { - variant: SnackBarVariant.Error, - }); + enqueueErrorSnackBar({ apolloError: error }); navigate(AppPath.Index); }, onCompleted: (data) => { @@ -35,13 +33,14 @@ export const useWorkspaceFromInviteHash = () => { data?.findWorkspaceFromInviteHash && currentWorkspace.id === data.findWorkspaceFromInviteHash.id ) { + const workspaceDisplayName = + data?.findWorkspaceFromInviteHash?.displayName; initiallyLoggedIn && - enqueueSnackBar( - `You already belong to ${data?.findWorkspaceFromInviteHash?.displayName} workspace`, - { - variant: SnackBarVariant.Info, - }, - ); + enqueueInfoSnackBar({ + message: workspaceDisplayName + ? t`You already belong to the workspace ${workspaceDisplayName}` + : t`You already belong to this workspace`, + }); navigate(AppPath.Index); } }, diff --git a/packages/twenty-front/src/modules/billing/components/SettingsBillingSubscriptionInfo.tsx b/packages/twenty-front/src/modules/billing/components/SettingsBillingSubscriptionInfo.tsx index a0920586b..09422e657 100644 --- a/packages/twenty-front/src/modules/billing/components/SettingsBillingSubscriptionInfo.tsx +++ b/packages/twenty-front/src/modules/billing/components/SettingsBillingSubscriptionInfo.tsx @@ -3,7 +3,6 @@ import { SubscriptionInfoRowContainer } from '@/billing/components/SubscriptionI import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { formatMonthlyPrices } from '@/billing/utils/formatMonthlyPrices'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { useModal } from '@/ui/layout/modal/hooks/useModal'; @@ -49,7 +48,7 @@ export const SettingsBillingSubscriptionInfo = () => { const { openModal } = useModal(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); const subscriptionStatus = useSubscriptionStatus(); @@ -134,12 +133,12 @@ export const SettingsBillingSubscriptionInfo = () => { }; setCurrentWorkspace(newCurrentWorkspace); } - enqueueSnackBar(t`Subscription has been switched to Yearly.`, { - variant: SnackBarVariant.Success, + enqueueSuccessSnackBar({ + message: t`Subscription has been switched to Yearly.`, }); } catch (error: any) { - enqueueSnackBar(t`Error while switching subscription to Yearly.`, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: t`Error while switching subscription to Yearly.`, }); } }; @@ -160,16 +159,13 @@ export const SettingsBillingSubscriptionInfo = () => { }; setCurrentWorkspace(newCurrentWorkspace); } - enqueueSnackBar(t`Subscription has been switched to Organization Plan.`, { - variant: SnackBarVariant.Success, + enqueueSuccessSnackBar({ + message: t`Subscription has been switched to Organization Plan.`, }); } catch (error: any) { - enqueueSnackBar( - t`Error while switching subscription to Organization Plan.`, - { - variant: SnackBarVariant.Error, - }, - ); + enqueueErrorSnackBar({ + message: t`Error while switching subscription to Organization Plan.`, + }); } }; diff --git a/packages/twenty-front/src/modules/billing/hooks/useEndSubscriptionTrialPeriod.ts b/packages/twenty-front/src/modules/billing/hooks/useEndSubscriptionTrialPeriod.ts index 473cdf4f3..941e225e7 100644 --- a/packages/twenty-front/src/modules/billing/hooks/useEndSubscriptionTrialPeriod.ts +++ b/packages/twenty-front/src/modules/billing/hooks/useEndSubscriptionTrialPeriod.ts @@ -1,5 +1,4 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { t } from '@lingui/core/macro'; import { useState } from 'react'; @@ -8,7 +7,7 @@ import { isDefined } from 'twenty-shared/utils'; import { useEndSubscriptionTrialPeriodMutation } from '~/generated-metadata/graphql'; export const useEndSubscriptionTrialPeriod = () => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); const [endSubscriptionTrialPeriod] = useEndSubscriptionTrialPeriodMutation(); const [currentWorkspace, setCurrentWorkspace] = useRecoilState( currentWorkspaceState, @@ -25,12 +24,9 @@ export const useEndSubscriptionTrialPeriod = () => { const hasPaymentMethod = endTrialPeriodOutput?.hasPaymentMethod; if (isDefined(hasPaymentMethod) && hasPaymentMethod === false) { - enqueueSnackBar( - t`No payment method found. Please update your billing details.`, - { - variant: SnackBarVariant.Error, - }, - ); + enqueueErrorSnackBar({ + message: t`No payment method found. Please update your billing details.`, + }); return; } @@ -49,16 +45,13 @@ export const useEndSubscriptionTrialPeriod = () => { }); } - enqueueSnackBar(t`Subscription activated.`, { - variant: SnackBarVariant.Success, + enqueueSuccessSnackBar({ + message: t`Subscription activated.`, }); } catch { - enqueueSnackBar( - t`Error while ending trial period. Please contact Twenty team.`, - { - variant: SnackBarVariant.Error, - }, - ); + enqueueErrorSnackBar({ + message: t`Error while ending trial period. Please contact Twenty team.`, + }); } finally { setIsLoading(false); } diff --git a/packages/twenty-front/src/modules/billing/hooks/useHandleCheckoutSession.ts b/packages/twenty-front/src/modules/billing/hooks/useHandleCheckoutSession.ts index 6d9ab61f0..fe9940350 100644 --- a/packages/twenty-front/src/modules/billing/hooks/useHandleCheckoutSession.ts +++ b/packages/twenty-front/src/modules/billing/hooks/useHandleCheckoutSession.ts @@ -1,7 +1,7 @@ import { useRedirect } from '@/domain-manager/hooks/useRedirect'; import { SettingsPath } from '@/types/SettingsPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { t } from '@lingui/core/macro'; import { useState } from 'react'; import { BillingPlanKey, @@ -21,7 +21,7 @@ export const useHandleCheckoutSession = ({ }) => { const { redirect } = useRedirect(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const [checkoutSession] = useCheckoutSessionMutation(); @@ -39,12 +39,9 @@ export const useHandleCheckoutSession = ({ }); setIsSubmitting(false); if (!data?.checkoutSession.url) { - enqueueSnackBar( - 'Checkout session error. Please retry or contact Twenty team', - { - variant: SnackBarVariant.Error, - }, - ); + enqueueErrorSnackBar({ + message: t`Checkout session error. Please retry or contact Twenty team`, + }); return; } redirect(data.checkoutSession.url); diff --git a/packages/twenty-front/src/modules/error-handler/components/ErrorMessageEffect.tsx b/packages/twenty-front/src/modules/error-handler/components/ErrorMessageEffect.tsx index 3147faa7f..0d242c3c1 100644 --- a/packages/twenty-front/src/modules/error-handler/components/ErrorMessageEffect.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/ErrorMessageEffect.tsx @@ -1,26 +1,27 @@ import { useEffect } from 'react'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSearchParams } from 'react-router-dom'; import { isDefined } from 'twenty-shared/utils'; export const ErrorMessageEffect = () => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const [searchParams, setSearchParams] = useSearchParams(); const errorMessage = searchParams.get('errorMessage'); useEffect(() => { if (isDefined(errorMessage)) { - enqueueSnackBar(errorMessage, { - dedupeKey: 'error-message-dedupe-key', - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: errorMessage, + options: { + dedupeKey: 'error-message-dedupe-key', + }, }); const newSearchParams = new URLSearchParams(searchParams); newSearchParams.delete('errorMessage'); setSearchParams(newSearchParams); } - }, [enqueueSnackBar, errorMessage, searchParams, setSearchParams]); + }, [enqueueErrorSnackBar, errorMessage, searchParams, setSearchParams]); return <>; }; diff --git a/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx b/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx index dd172ccf7..d52ec8694 100644 --- a/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx @@ -1,8 +1,6 @@ import { useCallback, useEffect } from 'react'; import { CustomError } from '@/error-handler/CustomError'; -import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import isEmpty from 'lodash.isempty'; import { isDefined } from 'twenty-shared/utils'; @@ -14,29 +12,20 @@ const hasErrorCode = ( }; export const PromiseRejectionEffect = () => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const handlePromiseRejection = useCallback( async (event: PromiseRejectionEvent) => { const error = event.reason; - - if (error instanceof ObjectMetadataItemNotFoundError) { - enqueueSnackBar( - `Error with custom object that cannot be found : ${event.reason}`, - { - variant: SnackBarVariant.Error, - }, - ); - } else { - enqueueSnackBar(`${error.message}`, { - variant: SnackBarVariant.Error, - }); - } - if (error.name === 'ApolloError' && !isEmpty(error.graphQLErrors)) { + enqueueErrorSnackBar({ + apolloError: error, + }); return; // already handled by apolloLink } + enqueueErrorSnackBar({}); + try { const { captureException } = await import('@sentry/react'); captureException(error, (scope) => { @@ -52,7 +41,7 @@ export const PromiseRejectionEffect = () => { console.error('Failed to capture exception with Sentry:', sentryError); } }, - [enqueueSnackBar], + [enqueueErrorSnackBar], ); useEffect(() => { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useFindManyObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useFindManyObjectMetadataItems.ts index e04be1085..175aaabb7 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useFindManyObjectMetadataItems.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useFindManyObjectMetadataItems.ts @@ -1,7 +1,6 @@ import { useQuery } from '@apollo/client'; import { useMemo } from 'react'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { ObjectMetadataItemsQuery, @@ -17,7 +16,7 @@ export const useFindManyObjectMetadataItems = ({ }: { skip?: boolean; } = {}) => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const { data, loading, error, refetch } = useQuery< ObjectMetadataItemsQuery, @@ -26,8 +25,8 @@ export const useFindManyObjectMetadataItems = ({ skip, onError: (error) => { logError('useFindManyObjectMetadataItems error : ' + error); - enqueueSnackBar(`${error.message}`, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error, }); }, }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useBatchCreateManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useBatchCreateManyRecords.ts index f20d9c5fb..416237c11 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useBatchCreateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useBatchCreateManyRecords.ts @@ -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; } diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts index a1bc98f36..76d9b584a 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts @@ -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; } diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts index 60965fe60..c7a43493f 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts @@ -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 = ({ objectNameSingular, }); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const queryResponseField = getFindDuplicateRecordsQueryResponseField( objectMetadataItem.nameSingular, @@ -59,8 +58,8 @@ export const useFindDuplicateRecords = ({ `useFindDuplicateRecords for "${objectMetadataItem.nameSingular}" error : ` + error, ); - enqueueSnackBar(`Error finding duplicates:", ${error.message}`, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error, }); }, }, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useHandleFindManyRecordsError.ts b/packages/twenty-front/src/modules/object-record/hooks/useHandleFindManyRecordsError.ts index b5a89b7eb..2e0e9fc18 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useHandleFindManyRecordsError.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useHandleFindManyRecordsError.ts @@ -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 { diff --git a/packages/twenty-front/src/modules/object-record/hooks/useObjectRecordSearchRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useObjectRecordSearchRecords.ts index 57ac9e244..fceb2d812 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useObjectRecordSearchRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useObjectRecordSearchRecords.ts @@ -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, + }); }, }); diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx index 1f0fef757..f589391ca 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx @@ -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: , - duration: 2000, + enqueueSuccessSnackBar({ + message: t`Link copied to clipboard`, + options: { + icon: , + 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: , - duration: 2000, + enqueueSuccessSnackBar({ + message: t`Link copied to clipboard`, + options: { + icon: , + duration: 2000, + }, }); }} LeftIcon={IconCopy} diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx index 7ec364bdc..b3e4d2c9a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx @@ -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) => { { - enqueueSnackBar(t`Text copied to clipboard`, { - variant: SnackBarVariant.Success, - icon: , - duration: 2000, + enqueueSuccessSnackBar({ + message: t`Text copied to clipboard`, + options: { + icon: , + duration: 2000, + }, }); navigator.clipboard.writeText(copyText); }} diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/PhonesFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/PhonesFieldDisplay.tsx index 1cffcb4d6..c7a37224d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/PhonesFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/PhonesFieldDisplay.tsx @@ -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: , - duration: 2000, + enqueueSuccessSnackBar({ + message: t`Phone number copied to clipboard`, + options: { + icon: , + duration: 2000, + }, }); } catch (err) { - enqueueSnackBar(t`Error copying to clipboard`, { - variant: SnackBarVariant.Error, - icon: , - duration: 2000, + enqueueErrorSnackBar({ + message: t`Error copying to clipboard`, + options: { + icon: , + duration: 2000, + }, }); } }; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts index 8270a1cb9..6e255ba86 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog.ts @@ -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(); - 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, }); } }, diff --git a/packages/twenty-front/src/modules/settings/accounts/hooks/useImapConnectionForm.ts b/packages/twenty-front/src/modules/settings/accounts/hooks/useImapConnectionForm.ts index 5cb8c73a2..b3d544fdf 100644 --- a/packages/twenty-front/src/modules/settings/accounts/hooks/useImapConnectionForm.ts +++ b/packages/twenty-front/src/modules/settings/accounts/hooks/useImapConnectionForm.ts @@ -4,8 +4,8 @@ import { useRecoilValue } from 'recoil'; import { z } from 'zod'; import { SettingsPath } from '@/types/SettingsPath'; -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 { useLingui } from '@lingui/react/macro'; import { ConnectionParameters, @@ -38,7 +38,7 @@ export const useImapConnectionForm = ({ }: UseImapConnectionFormProps = {}) => { const { t } = useLingui(); const navigate = useNavigateSettings(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar(); const currentWorkspace = useRecoilValue(currentWorkspaceState); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); @@ -70,16 +70,12 @@ export const useImapConnectionForm = ({ formValues: ConnectionParameters & { handle: string }, ) => { if (!currentWorkspace?.id) { - enqueueSnackBar('Workspace ID is missing', { - variant: SnackBarVariant.Error, - }); + enqueueErrorSnackBar({}); return; } if (!currentWorkspaceMember?.id) { - enqueueSnackBar('Workspace member ID is missing', { - variant: SnackBarVariant.Error, - }); + enqueueErrorSnackBar({}); return; } @@ -112,19 +108,16 @@ export const useImapConnectionForm = ({ }, }); - enqueueSnackBar( - connectedAccountId + enqueueSuccessSnackBar({ + message: connectedAccountId ? t`IMAP connection successfully updated` : t`IMAP connection successfully created`, - { - variant: SnackBarVariant.Success, - }, - ); + }); navigate(SettingsPath.Accounts); } catch (error) { - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, }); } }; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx index d9dfe4ca6..f3f166612 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx @@ -1,7 +1,6 @@ import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState'; import { SettingsAdminWorkspaceContent } from '@/settings/admin-panel/components/SettingsAdminWorkspaceContent'; import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInput } from '@/ui/input/components/TextInput'; import { TabList } from '@/ui/layout/tab-list/components/TabList'; @@ -40,7 +39,7 @@ const StyledContainer = styled.div` export const SettingsAdminGeneral = () => { const [userIdentifier, setUserIdentifier] = useState(''); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const [activeTabId, setActiveTabId] = useRecoilComponentStateV2( activeTabIdComponentState, @@ -76,8 +75,8 @@ export const SettingsAdminGeneral = () => { }, onError: (error) => { setIsUserLookupLoading(false); - enqueueSnackBar(error.message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error, }); }, }); diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceContent.tsx index a85a098a6..521d7a9e1 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceContent.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminWorkspaceContent.tsx @@ -7,7 +7,6 @@ import { useImpersonationAuth } from '@/settings/admin-panel/hooks/useImpersonat import { useImpersonationRedirect } from '@/settings/admin-panel/hooks/useImpersonationRedirect'; import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState'; import { WorkspaceInfo } from '@/settings/admin-panel/types/WorkspaceInfo'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Table } from '@/ui/layout/table/components/Table'; import { TableBody } from '@/ui/layout/table/components/TableBody'; @@ -57,7 +56,7 @@ export const SettingsAdminWorkspaceContent = ({ activeWorkspace, }: SettingsAdminWorkspaceContentProps) => { const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const [currentUser] = useRecoilState(currentUserState); const currentWorkspace = useRecoilValue(currentWorkspaceState); @@ -74,9 +73,7 @@ export const SettingsAdminWorkspaceContent = ({ const handleImpersonate = async (workspaceId: string) => { if (!userLookupResult?.user.id) { - enqueueSnackBar(t`Please search for a user first`, { - variant: SnackBarVariant.Error, - }); + enqueueErrorSnackBar({ message: t`Please search for a user first` }); return; } @@ -98,8 +95,8 @@ export const SettingsAdminWorkspaceContent = ({ ); }, onError: (error) => { - enqueueSnackBar(`Failed to impersonate user. ${error.message}`, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: `Failed to impersonate user. ${error.message}`, }); }, }).finally(() => { @@ -128,8 +125,8 @@ export const SettingsAdminWorkspaceContent = ({ if (isDefined(previousValue)) { updateFeatureFlagState(workspaceId, featureFlag, previousValue); } - enqueueSnackBar(`Failed to update feature flag. ${error.message}`, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: `Failed to update feature flag. ${error.message}`, }); }, }); diff --git a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigCopyableText.tsx b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigCopyableText.tsx index ef911644a..310315f34 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigCopyableText.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigCopyableText.tsx @@ -1,4 +1,3 @@ -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; @@ -33,15 +32,17 @@ export const SettingsAdminConfigCopyableText = ({ multiline = false, maxRows, }: SettingsAdminConfigCopyableTextProps) => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueSuccessSnackBar } = useSnackBar(); const theme = useTheme(); const { t } = useLingui(); const copyToClipboardDebounced = useDebouncedCallback((value: string) => { navigator.clipboard.writeText(value); - enqueueSnackBar(t`Copied to clipboard!`, { - variant: SnackBarVariant.Success, - icon: , + enqueueSuccessSnackBar({ + message: t`Copied to clipboard!`, + options: { + icon: , + }, }); }, 200); diff --git a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/hooks/useConfigVariableActions.ts b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/hooks/useConfigVariableActions.ts index 7d43dbaa3..c2cda60f8 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/hooks/useConfigVariableActions.ts +++ b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/hooks/useConfigVariableActions.ts @@ -2,7 +2,6 @@ import { useLingui } from '@lingui/react/macro'; import { useClientConfig } from '@/client-config/hooks/useClientConfig'; import { GET_DATABASE_CONFIG_VARIABLE } from '@/settings/admin-panel/config-variables/graphql/queries/getDatabaseConfigVariable'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { ConfigVariableValue } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; @@ -14,7 +13,7 @@ import { export const useConfigVariableActions = (variableName: string) => { const { t } = useLingui(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); const { refetch: refetchClientConfig } = useClientConfig(); const [updateDatabaseConfigVariable] = @@ -68,12 +67,12 @@ export const useConfigVariableActions = (variableName: string) => { await refetchClientConfig(); - enqueueSnackBar(t`Variable updated successfully.`, { - variant: SnackBarVariant.Success, + enqueueSuccessSnackBar({ + message: t`Variable updated successfully.`, }); } catch (error) { - enqueueSnackBar(t`Failed to update variable`, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: t`Failed to update variable`, }); } }; @@ -98,12 +97,12 @@ export const useConfigVariableActions = (variableName: string) => { await refetchClientConfig(); - enqueueSnackBar(t`Variable deleted successfully.`, { - variant: SnackBarVariant.Success, + enqueueSuccessSnackBar({ + message: t`Variable deleted successfully.`, }); } catch (error) { - enqueueSnackBar(t`Failed to remove override`, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: t`Failed to remove override`, }); } }; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerMetricsGraph.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerMetricsGraph.tsx index efad4bd64..a09e90e84 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerMetricsGraph.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerMetricsGraph.tsx @@ -1,6 +1,5 @@ import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard'; import { WorkerMetricsTooltip } from '@/settings/admin-panel/health-status/components/WorkerMetricsTooltip'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; @@ -45,7 +44,7 @@ export const WorkerMetricsGraph = ({ timeRange, }: WorkerMetricsGraphProps) => { const theme = useTheme(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const { loading, data } = useGetQueueMetricsQuery({ variables: { @@ -54,8 +53,8 @@ export const WorkerMetricsGraph = ({ }, fetchPolicy: 'no-cache', onError: (error) => { - enqueueSnackBar(`Error fetching worker metrics: ${error.message}`, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: `Error fetching worker metrics: ${error.message}`, }); }, }); diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx index bd58b84b1..d1e6ced8c 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx @@ -6,8 +6,8 @@ import { settingsDataModelObjectAboutFormSchema, } from '@/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema'; import { SettingsPath } from '@/types/SettingsPath'; -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 { zodResolver } from '@hookform/resolvers/zod'; import { FormProvider, useForm } from 'react-hook-form'; import { useSetRecoilState } from 'recoil'; @@ -23,7 +23,7 @@ export const SettingsUpdateDataModelObjectAboutForm = ({ objectMetadataItem, }: SettingsUpdateDataModelObjectAboutFormProps) => { const navigate = useNavigateSettings(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const setUpdatedObjectNamePlural = useSetRecoilState( updatedObjectNamePluralState, ); @@ -117,15 +117,20 @@ export const SettingsUpdateDataModelObjectAboutForm = ({ console.error(error); if (error instanceof ZodError) { - enqueueSnackBar(error.issues[0].message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: error.issues[0].message, }); return; } - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, - }); + if (error instanceof ApolloError) { + enqueueErrorSnackBar({ + apolloError: error, + }); + return; + } + + enqueueErrorSnackBar({}); }; return ( diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx index 9d0f8ab97..5b4000cfc 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx @@ -7,9 +7,9 @@ import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdat import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getActiveFieldMetadataItems } from '@/object-metadata/utils/getActiveFieldMetadataItems'; import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Select } from '@/ui/input/components/Select'; +import { ApolloError } from '@apollo/client'; import { zodResolver } from '@hookform/resolvers/zod'; import { t } from '@lingui/core/macro'; import { useNavigate } from 'react-router-dom'; @@ -48,7 +48,7 @@ export const SettingsDataModelObjectIdentifiersForm = ({ mode: 'onTouched', resolver: zodResolver(settingsDataModelObjectIdentifiersFormSchema), }); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); const handleSave = async ( @@ -67,12 +67,12 @@ export const SettingsDataModelObjectIdentifiersForm = ({ formConfig.reset(undefined, { keepValues: true }); } catch (error) { if (error instanceof ZodError) { - enqueueSnackBar(error.issues[0].message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: error.issues[0].message, }); } else { - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, }); } } diff --git a/packages/twenty-front/src/modules/settings/developers/components/ApiKeyInput.tsx b/packages/twenty-front/src/modules/settings/developers/components/ApiKeyInput.tsx index e294b52ce..2aae99943 100644 --- a/packages/twenty-front/src/modules/settings/developers/components/ApiKeyInput.tsx +++ b/packages/twenty-front/src/modules/settings/developers/components/ApiKeyInput.tsx @@ -1,13 +1,12 @@ 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 { TextInput } from '@/ui/input/components/TextInput'; import { useLingui } from '@lingui/react/macro'; -import { Button } from 'twenty-ui/input'; import { IconCopy } from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; const StyledContainer = styled.div` display: flex; @@ -25,7 +24,7 @@ export const ApiKeyInput = ({ apiKey }: ApiKeyInputProps) => { const theme = useTheme(); const { t } = useLingui(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueSuccessSnackBar } = useSnackBar(); return ( @@ -35,10 +34,12 @@ export const ApiKeyInput = ({ apiKey }: ApiKeyInputProps) => { Icon={IconCopy} title={t`Copy`} onClick={() => { - enqueueSnackBar(t`API Key copied to clipboard`, { - variant: SnackBarVariant.Success, - icon: , - duration: 2000, + enqueueSuccessSnackBar({ + message: t`API Key copied to clipboard`, + options: { + icon: , + duration: 2000, + }, }); navigator.clipboard.writeText(apiKey); }} diff --git a/packages/twenty-front/src/modules/settings/developers/hooks/__tests__/useWebhookForm.test.tsx b/packages/twenty-front/src/modules/settings/developers/hooks/__tests__/useWebhookForm.test.tsx index f21c346dc..3432239ff 100644 --- a/packages/twenty-front/src/modules/settings/developers/hooks/__tests__/useWebhookForm.test.tsx +++ b/packages/twenty-front/src/modules/settings/developers/hooks/__tests__/useWebhookForm.test.tsx @@ -5,11 +5,13 @@ import { MemoryRouter } from 'react-router-dom'; import { RecoilRoot } from 'recoil'; import { WebhookFormMode } from '@/settings/developers/constants/WebhookFormMode'; +import { ApolloError } from '@apollo/client'; import { useWebhookForm } from '../useWebhookForm'; // Mock dependencies const mockNavigateSettings = jest.fn(); -const mockEnqueueSnackBar = jest.fn(); +const mockEnqueueSuccessSnackBar = jest.fn(); +const mockEnqueueErrorSnackBar = jest.fn(); const mockCreateOneRecord = jest.fn(); const mockUpdateOneRecord = jest.fn(); const mockDeleteOneRecord = jest.fn(); @@ -20,7 +22,8 @@ jest.mock('~/hooks/useNavigateSettings', () => ({ jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar', () => ({ useSnackBar: () => ({ - enqueueSnackBar: mockEnqueueSnackBar, + enqueueSuccessSnackBar: mockEnqueueSuccessSnackBar, + enqueueErrorSnackBar: mockEnqueueErrorSnackBar, }), })); @@ -106,14 +109,15 @@ describe('useWebhookForm', () => { secret: 'test-secret', }); - expect(mockEnqueueSnackBar).toHaveBeenCalledWith( - 'Webhook https://test.com/webhook created successfully', - { variant: 'success' }, - ); + expect(mockEnqueueSuccessSnackBar).toHaveBeenCalledWith({ + message: 'Webhook https://test.com/webhook created successfully', + }); }); it('should handle creation errors', async () => { - const error = new Error('Creation failed'); + const error = new ApolloError({ + graphQLErrors: [{ message: 'Creation failed' }], + }); mockCreateOneRecord.mockRejectedValue(error); const { result } = renderHook( @@ -130,8 +134,8 @@ describe('useWebhookForm', () => { await result.current.handleSave(formData); - expect(mockEnqueueSnackBar).toHaveBeenCalledWith('Creation failed', { - variant: 'error', + expect(mockEnqueueErrorSnackBar).toHaveBeenCalledWith({ + apolloError: error, }); }); @@ -216,7 +220,9 @@ describe('useWebhookForm', () => { }); it('should handle update errors', async () => { - const error = new Error('Update failed'); + const error = new ApolloError({ + graphQLErrors: [{ message: 'Update failed' }], + }); mockUpdateOneRecord.mockRejectedValue(error); const { result } = renderHook( @@ -237,8 +243,8 @@ describe('useWebhookForm', () => { await result.current.handleSave(formData); - expect(mockEnqueueSnackBar).toHaveBeenCalledWith('Update failed', { - variant: 'error', + expect(mockEnqueueErrorSnackBar).toHaveBeenCalledWith({ + apolloError: error, }); }); }); @@ -297,10 +303,9 @@ describe('useWebhookForm', () => { await result.current.deleteWebhook(); expect(mockDeleteOneRecord).toHaveBeenCalledWith(webhookId); - expect(mockEnqueueSnackBar).toHaveBeenCalledWith( - 'Webhook deleted successfully', - { variant: 'success' }, - ); + expect(mockEnqueueSuccessSnackBar).toHaveBeenCalledWith({ + message: 'Webhook deleted successfully', + }); }); it('should handle deletion without webhookId', async () => { @@ -311,14 +316,15 @@ describe('useWebhookForm', () => { await result.current.deleteWebhook(); - expect(mockEnqueueSnackBar).toHaveBeenCalledWith( - 'Webhook ID is required for deletion', - { variant: 'error' }, - ); + expect(mockEnqueueErrorSnackBar).toHaveBeenCalledWith({ + message: 'Webhook ID is required for deletion', + }); }); it('should handle deletion errors', async () => { - const error = new Error('Deletion failed'); + const error = new ApolloError({ + graphQLErrors: [{ message: 'Deletion failed' }], + }); mockDeleteOneRecord.mockRejectedValue(error); const { result } = renderHook( @@ -332,8 +338,8 @@ describe('useWebhookForm', () => { await result.current.deleteWebhook(); - expect(mockEnqueueSnackBar).toHaveBeenCalledWith('Deletion failed', { - variant: 'error', + expect(mockEnqueueErrorSnackBar).toHaveBeenCalledWith({ + apolloError: error, }); }); }); diff --git a/packages/twenty-front/src/modules/settings/developers/hooks/useWebhookForm.ts b/packages/twenty-front/src/modules/settings/developers/hooks/useWebhookForm.ts index fe8c6dd93..fd679aa1c 100644 --- a/packages/twenty-front/src/modules/settings/developers/hooks/useWebhookForm.ts +++ b/packages/twenty-front/src/modules/settings/developers/hooks/useWebhookForm.ts @@ -13,8 +13,9 @@ import { WebhookFormValues, } from '@/settings/developers/validation-schemas/webhookFormSchema'; import { SettingsPath } from '@/types/SettingsPath'; -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'; import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; @@ -28,7 +29,7 @@ type UseWebhookFormProps = { export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => { const navigate = useNavigateSettings(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); const isCreationMode = mode === WebhookFormMode.Create; @@ -134,28 +135,29 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => { ...webhookData, }); - enqueueSnackBar( - `Webhook ${createdWebhook?.targetUrl} created successfully`, - { - variant: SnackBarVariant.Success, - }, - ); + const targetUrl = createdWebhook?.targetUrl + ? `${createdWebhook?.targetUrl}` + : ''; + + enqueueSuccessSnackBar({ + message: t`Webhook ${targetUrl} created successfully`, + }); navigate( createdWebhook ? SettingsPath.WebhookDetail : SettingsPath.Webhooks, createdWebhook ? { webhookId: createdWebhook.id } : undefined, ); } catch (error) { - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, }); } }; const handleUpdate = async (formValues: WebhookFormValues) => { if (!webhookId) { - enqueueSnackBar('Webhook ID is required for updates', { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: t`Webhook ID is required for updates`, }); return; } @@ -177,12 +179,14 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => { formConfig.reset(formValues); - enqueueSnackBar(`Webhook ${webhookData.targetUrl} updated successfully`, { - variant: SnackBarVariant.Success, + const targetUrl = webhookData.targetUrl ? `${webhookData.targetUrl}` : ''; + + enqueueSuccessSnackBar({ + message: t`Webhook ${targetUrl} updated successfully`, }); } catch (error) { - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, }); } }; @@ -222,22 +226,22 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => { const deleteWebhook = async () => { if (!webhookId) { - enqueueSnackBar('Webhook ID is required for deletion', { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: t`Webhook ID is required for deletion`, }); return; } try { await deleteOneWebhook(webhookId); - enqueueSnackBar('Webhook deleted successfully', { - variant: SnackBarVariant.Success, + enqueueSuccessSnackBar({ + message: t`Webhook deleted successfully`, }); navigate(SettingsPath.Webhooks); } catch (error) { - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, }); } }; diff --git a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContent.tsx b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContent.tsx index 8b4497a9e..7a0c59aaa 100644 --- a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContent.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContent.tsx @@ -9,13 +9,14 @@ import { } from '@/settings/integrations/database-connection/utils/editDatabaseConnection'; import { SettingsIntegration } from '@/settings/integrations/types/SettingsIntegration'; import { SettingsPath } from '@/types/SettingsPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; +import { ApolloError } from '@apollo/client'; import { zodResolver } from '@hookform/resolvers/zod'; import { Section } from '@react-email/components'; import pick from 'lodash.pick'; import { FormProvider, useForm } from 'react-hook-form'; +import { H2Title, Info } from 'twenty-ui/display'; import { z } from 'zod'; import { RemoteServer, @@ -24,7 +25,6 @@ import { } from '~/generated-metadata/graphql'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; -import { H2Title, Info } from 'twenty-ui/display'; export const SettingsIntegrationEditDatabaseConnectionContent = ({ connection, @@ -37,7 +37,7 @@ export const SettingsIntegrationEditDatabaseConnectionContent = ({ databaseKey: string; tables: RemoteTable[]; }) => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const navigate = useNavigateSettings(); const editConnectionSchema = getEditionSchemaForForm(databaseKey); @@ -87,8 +87,8 @@ export const SettingsIntegrationEditDatabaseConnectionContent = ({ connectionId: connection?.id, }); } catch (error) { - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, }); } }; diff --git a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx index 41c5365bf..15d96b8b9 100644 --- a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx @@ -12,7 +12,6 @@ import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDr import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState'; import { settingsRolesIsLoadingState } from '@/settings/roles/states/settingsRolesIsLoadingState'; import { SettingsPath } from '@/types/SettingsPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { TabList } from '@/ui/layout/tab-list/components/TabList'; @@ -81,7 +80,7 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { const { loadCurrentUser } = useAuth(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); if (!isDefined(settingsRolesIsLoading)) { return <>; @@ -129,8 +128,8 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { ); if (isDefined(dirtyFields.label) && dirtyFields.label === '') { - enqueueSnackBar(t`Role name cannot be empty`, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: t`Role name cannot be empty`, }); return; } diff --git a/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOIdentitiesProvidersListCard.tsx b/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOIdentitiesProvidersListCard.tsx index 034337f4c..2489976a3 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOIdentitiesProvidersListCard.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOIdentitiesProvidersListCard.tsx @@ -8,8 +8,8 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { SettingsCard } from '@/settings/components/SettingsCard'; import { SettingsSSOIdentitiesProvidersListCardWrapper } from '@/settings/security/components/SSO/SettingsSSOIdentitiesProvidersListCardWrapper'; import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState'; -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 isPropValid from '@emotion/is-prop-valid'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; @@ -26,7 +26,7 @@ const StyledLink = styled(Link, { `; export const SettingsSSOIdentitiesProvidersListCard = () => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const currentWorkspace = useRecoilValue(currentWorkspaceState); @@ -42,9 +42,9 @@ export const SettingsSSOIdentitiesProvidersListCard = () => { onCompleted: (data) => { setSSOIdentitiesProviders(data?.getSSOIdentityProviders ?? []); }, - onError: (error: Error) => { - enqueueSnackBar(error.message, { - variant: SnackBarVariant.Error, + onError: (error: ApolloError) => { + enqueueErrorSnackBar({ + apolloError: error, }); }, }); diff --git a/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOOIDCForm.tsx b/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOOIDCForm.tsx index bb2392866..883b87231 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOOIDCForm.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOOIDCForm.tsx @@ -1,16 +1,15 @@ /* @license Enterprise */ -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInput } from '@/ui/input/components/TextInput'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; import { Controller, useFormContext } from 'react-hook-form'; -import { REACT_APP_SERVER_BASE_URL } from '~/config'; -import { Button } from 'twenty-ui/input'; import { H2Title, IconCopy } from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; +import { REACT_APP_SERVER_BASE_URL } from '~/config'; const StyledInputsContainer = styled.div` display: flex; @@ -37,7 +36,7 @@ const StyledButtonCopy = styled.div` export const SettingsSSOOIDCForm = () => { const { control } = useFormContext(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueSuccessSnackBar } = useSnackBar(); const theme = useTheme(); const { t } = useLingui(); @@ -66,10 +65,12 @@ export const SettingsSSOOIDCForm = () => { Icon={IconCopy} title={t`Copy`} onClick={() => { - enqueueSnackBar(t`Authorized URL copied to clipboard`, { - variant: SnackBarVariant.Success, - icon: , - duration: 2000, + enqueueSuccessSnackBar({ + message: t`Authorized URL copied to clipboard`, + options: { + icon: , + duration: 2000, + }, }); navigator.clipboard.writeText(authorizedUrl); }} @@ -91,10 +92,12 @@ export const SettingsSSOOIDCForm = () => { Icon={IconCopy} title={t`Copy`} onClick={() => { - enqueueSnackBar(t`Redirect Url copied to clipboard`, { - variant: SnackBarVariant.Success, - icon: , - duration: 2000, + enqueueSuccessSnackBar({ + message: t`Redirect Url copied to clipboard`, + options: { + icon: , + duration: 2000, + }, }); navigator.clipboard.writeText(redirectionUrl); }} diff --git a/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx b/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx index 6a2cd950c..b790b0b5c 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSSOSAMLForm.tsx @@ -1,7 +1,6 @@ /* @license Enterprise */ import { parseSAMLMetadataFromXMLFile } from '@/settings/security/utils/parseSAMLMetadataFromXMLFile'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInput } from '@/ui/input/components/TextInput'; import { useTheme } from '@emotion/react'; @@ -9,9 +8,7 @@ import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; import { ChangeEvent, useRef } from 'react'; import { useFormContext } from 'react-hook-form'; -import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { isDefined } from 'twenty-shared/utils'; -import { Button } from 'twenty-ui/input'; import { H2Title, HorizontalSeparator, @@ -20,7 +17,9 @@ import { IconDownload, IconUpload, } from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; +import { REACT_APP_SERVER_BASE_URL } from '~/config'; const StyledUploadFileContainer = styled.div` align-items: center; @@ -56,7 +55,7 @@ const StyledButtonCopy = styled.div` `; export const SettingsSSOSAMLForm = () => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar(); const theme = useTheme(); const { setValue, getValues, watch, trigger } = useFormContext(); const { t } = useLingui(); @@ -67,9 +66,11 @@ export const SettingsSSOSAMLForm = () => { const samlMetadataParsed = parseSAMLMetadataFromXMLFile(text); e.target.value = ''; if (!samlMetadataParsed.success) { - return enqueueSnackBar(t`Invalid File`, { - variant: SnackBarVariant.Error, - duration: 2000, + return enqueueErrorSnackBar({ + message: t`Invalid File`, + options: { + duration: 2000, + }, }); } setValue('ssoURL', samlMetadataParsed.data.ssoUrl); @@ -103,9 +104,11 @@ export const SettingsSSOSAMLForm = () => { `${REACT_APP_SERVER_BASE_URL}/auth/saml/metadata/${getValues('id')}`, ); if (!response.ok) { - return enqueueSnackBar(t`Metadata file generation failed`, { - variant: SnackBarVariant.Error, - duration: 2000, + return enqueueErrorSnackBar({ + message: t`Metadata file generation failed`, + options: { + duration: 2000, + }, }); } const text = await response.text(); @@ -177,10 +180,12 @@ export const SettingsSSOSAMLForm = () => { Icon={IconCopy} title="Copy" onClick={() => { - enqueueSnackBar('ACS Url copied to clipboard', { - variant: SnackBarVariant.Success, - icon: , - duration: 2000, + enqueueSuccessSnackBar({ + message: t`ACS Url copied to clipboard`, + options: { + icon: , + duration: 2000, + }, }); navigator.clipboard.writeText(acsUrl); }} @@ -202,10 +207,12 @@ export const SettingsSSOSAMLForm = () => { Icon={IconCopy} title={t`Copy`} onClick={() => { - enqueueSnackBar(t`Entity ID copied to clipboard`, { - variant: SnackBarVariant.Success, - icon: , - duration: 2000, + enqueueSuccessSnackBar({ + message: t`Entity ID copied to clipboard`, + options: { + icon: , + duration: 2000, + }, }); navigator.clipboard.writeText(entityID); }} diff --git a/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSecuritySSORowDropdownMenu.tsx b/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSecuritySSORowDropdownMenu.tsx index 3fecfe101..7284b4e3b 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSecuritySSORowDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SSO/SettingsSecuritySSORowDropdownMenu.tsx @@ -1,7 +1,6 @@ import { useDeleteSSOIdentityProvider } from '@/settings/security/hooks/useDeleteSSOIdentityProvider'; import { useUpdateSSOIdentityProvider } from '@/settings/security/hooks/useUpdateSSOIdentityProvider'; import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; @@ -24,7 +23,7 @@ export const SettingsSecuritySSORowDropdownMenu = ({ }: SettingsSecuritySSORowDropdownMenuProps) => { const dropdownId = `settings-account-row-${SSOIdp.id}`; - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const { closeDropdown } = useCloseDropdown(); @@ -40,9 +39,11 @@ export const SettingsSecuritySSORowDropdownMenu = ({ identityProviderId, }); if (isDefined(result.errors)) { - enqueueSnackBar(t`Error deleting SSO Identity Provider`, { - variant: SnackBarVariant.Error, - duration: 2000, + enqueueErrorSnackBar({ + message: t`Error deleting SSO Identity Provider`, + options: { + duration: 2000, + }, }); } }; @@ -58,9 +59,11 @@ export const SettingsSecuritySSORowDropdownMenu = ({ : SsoIdentityProviderStatus.Active, }); if (isDefined(result.errors)) { - enqueueSnackBar(t`Error editing SSO Identity Provider`, { - variant: SnackBarVariant.Error, - duration: 2000, + enqueueErrorSnackBar({ + message: t`Error editing SSO Identity Provider`, + options: { + duration: 2000, + }, }); } }; diff --git a/packages/twenty-front/src/modules/settings/security/components/SettingsSecurityAuthProvidersOptionsList.tsx b/packages/twenty-front/src/modules/settings/security/components/SettingsSecurityAuthProvidersOptionsList.tsx index 3d86a7c7f..a78ae8cb0 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SettingsSecurityAuthProvidersOptionsList.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SettingsSecurityAuthProvidersOptionsList.tsx @@ -2,8 +2,8 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { authProvidersState } from '@/client-config/states/authProvidersState'; import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle'; import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState'; -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 styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; import { useRecoilState, useRecoilValue } from 'recoil'; @@ -30,7 +30,7 @@ const StyledSettingsSecurityOptionsList = styled.div` export const SettingsSecurityAuthProvidersOptionsList = () => { const { t } = useLingui(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const SSOIdentitiesProviders = useRecoilValue(SSOIdentitiesProvidersState); const authProviders = useRecoilValue(authProvidersState); @@ -72,12 +72,9 @@ export const SettingsSecurityAuthProvidersOptionsList = () => { allAuthProvidersEnabled.filter((isAuthEnabled) => isAuthEnabled).length <= 1 ) { - return enqueueSnackBar( - t`At least one authentication method must be enabled`, - { - variant: SnackBarVariant.Error, - }, - ); + return enqueueErrorSnackBar({ + message: t`At least one authentication method must be enabled`, + }); } setCurrentWorkspace({ @@ -97,8 +94,8 @@ export const SettingsSecurityAuthProvidersOptionsList = () => { ...currentWorkspace, [key]: !currentWorkspace[key], }); - enqueueSnackBar(err?.message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: err instanceof ApolloError ? err : undefined, }); }); }; @@ -120,8 +117,8 @@ export const SettingsSecurityAuthProvidersOptionsList = () => { isPublicInviteLinkEnabled: value, }); } catch (err: any) { - enqueueSnackBar(err?.message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: err instanceof ApolloError ? err : undefined, }); } }; diff --git a/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard.tsx b/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard.tsx index a03102834..547b86de9 100644 --- a/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard.tsx @@ -7,8 +7,8 @@ import { SettingsListCard } from '@/settings/components/SettingsListCard'; import { SettingsSecurityApprovedAccessDomainRowDropdownMenu } from '@/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainRowDropdownMenu'; import { SettingsSecurityApprovedAccessDomainValidationEffect } from '@/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainValidationEffect'; import { approvedAccessDomainsState } from '@/settings/security/states/ApprovedAccessDomainsState'; -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 styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; import { useRecoilState } from 'recoil'; @@ -22,7 +22,7 @@ const StyledLink = styled(Link)` `; export const SettingsApprovedAccessDomainsListCard = () => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const navigate = useNavigate(); const { t } = useLingui(); @@ -36,8 +36,8 @@ export const SettingsApprovedAccessDomainsListCard = () => { setApprovedAccessDomains(data?.getApprovedAccessDomains ?? []); }, onError: (error: Error) => { - enqueueSnackBar(error.message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, }); }, }); diff --git a/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainRowDropdownMenu.tsx b/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainRowDropdownMenu.tsx index 19a1ea847..71a45f0be 100644 --- a/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainRowDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainRowDropdownMenu.tsx @@ -1,10 +1,10 @@ import { approvedAccessDomainsState } from '@/settings/security/states/ApprovedAccessDomainsState'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; +import { t } from '@lingui/core/macro'; import { UnwrapRecoilValue, useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { IconDotsVertical, IconTrash } from 'twenty-ui/display'; @@ -25,7 +25,7 @@ export const SettingsSecurityApprovedAccessDomainRowDropdownMenu = ({ approvedAccessDomainsState, ); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const { closeDropdown } = useCloseDropdown(); @@ -47,9 +47,11 @@ export const SettingsSecurityApprovedAccessDomainRowDropdownMenu = ({ }, }); if (isDefined(result.errors)) { - enqueueSnackBar('Error deleting approved access domain', { - variant: SnackBarVariant.Error, - duration: 2000, + enqueueErrorSnackBar({ + message: t`Could not delete approved access domain`, + options: { + duration: 2000, + }, }); } }; diff --git a/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainValidationEffect.tsx b/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainValidationEffect.tsx index b6af7f56f..1f41579de 100644 --- a/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainValidationEffect.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainValidationEffect.tsx @@ -1,5 +1,5 @@ -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { t } from '@lingui/core/macro'; import { useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; import { isDefined } from 'twenty-shared/utils'; @@ -8,7 +8,7 @@ import { useValidateApprovedAccessDomainMutation } from '~/generated-metadata/gr export const SettingsSecurityApprovedAccessDomainValidationEffect = () => { const [validateApprovedAccessDomainMutation] = useValidateApprovedAccessDomainMutation(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); const [searchParams] = useSearchParams(); const approvedAccessDomainId = searchParams.get('wtdId'); const validationToken = searchParams.get('validationToken'); @@ -23,15 +23,19 @@ export const SettingsSecurityApprovedAccessDomainValidationEffect = () => { }, }, onCompleted: () => { - enqueueSnackBar('Approved access domain validated', { - dedupeKey: 'approved-access-domain-validation-dedupe-key', - variant: SnackBarVariant.Success, + enqueueSuccessSnackBar({ + message: t`Approved access domain validated`, + options: { + dedupeKey: 'approved-access-domain-validation-dedupe-key', + }, }); }, onError: () => { - enqueueSnackBar('Error validating approved access domain', { - dedupeKey: 'approved-access-domain-validation-error-dedupe-key', - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: t`Error validating approved access domain`, + options: { + dedupeKey: 'approved-access-domain-validation-error-dedupe-key', + }, }); }, }); diff --git a/packages/twenty-front/src/modules/settings/workspace/components/ToggleImpersonate.tsx b/packages/twenty-front/src/modules/settings/workspace/components/ToggleImpersonate.tsx index c88d01eba..f90113ae7 100644 --- a/packages/twenty-front/src/modules/settings/workspace/components/ToggleImpersonate.tsx +++ b/packages/twenty-front/src/modules/settings/workspace/components/ToggleImpersonate.tsx @@ -2,15 +2,15 @@ import { useRecoilState } from 'recoil'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle'; -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'; import { IconLifebuoy } from 'twenty-ui/display'; import { Card } from 'twenty-ui/layout'; import { useUpdateWorkspaceMutation } from '~/generated-metadata/graphql'; export const ToggleImpersonate = () => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const [currentWorkspace, setCurrentWorkspace] = useRecoilState( currentWorkspaceState, @@ -35,8 +35,8 @@ export const ToggleImpersonate = () => { allowImpersonation: value, }); } catch (err: any) { - enqueueSnackBar(err?.message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: err instanceof ApolloError ? err : undefined, }); } }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepper.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepper.tsx index f30b0590c..b4c63fff0 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepper.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepper.tsx @@ -3,7 +3,6 @@ import styled from '@emotion/styled'; import { useCallback, useState } from 'react'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Modal } from '@/ui/layout/modal/components/Modal'; @@ -47,15 +46,15 @@ export const SpreadsheetImportStepper = ({ const [uploadedFile, setUploadedFile] = useState(null); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const handleError = useCallback( (description: string) => { - enqueueSnackBar(description, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: description, }); }, - [enqueueSnackBar], + [enqueueErrorSnackBar], ); const handleBack = useCallback(() => { diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx index d1ad453d2..df6cb2d5b 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx @@ -7,7 +7,6 @@ import { SpreadsheetMaxRecordImportCapacity } from '@/spreadsheet-import/constan import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { useDownloadFakeRecords } from '@/spreadsheet-import/steps/components/UploadStep/hooks/useDownloadFakeRecords'; import { readFileAsync } from '@/spreadsheet-import/utils/readFilesAsync'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Trans, useLingui } from '@lingui/react/macro'; import { MainButton } from 'twenty-ui/input'; @@ -113,7 +112,7 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => { const [loading, setLoading] = useState(false); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const { downloadSample } = useDownloadFakeRecords(); @@ -132,9 +131,11 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => { onDropRejected: (fileRejections) => { setLoading(false); fileRejections.forEach((fileRejection) => { - enqueueSnackBar(`${fileRejection.file.name} upload rejected`, { - detailedMessage: fileRejection.errors[0].message, - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: `${fileRejection.file.name} upload rejected`, + options: { + detailedMessage: fileRejection.errors[0].message, + }, }); }); }, diff --git a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/hooks/useSnackBar.ts b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/hooks/useSnackBar.ts index f55f2b57d..84f253169 100644 --- a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/hooks/useSnackBar.ts +++ b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/hooks/useSnackBar.ts @@ -2,13 +2,17 @@ import { useCallback } from 'react'; import { useRecoilCallback } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarManagerScopeInternalContext } from '@/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext'; import { snackBarInternalScopedState, SnackBarOptions, } from '@/ui/feedback/snack-bar-manager/states/snackBarInternalScopedState'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; +import { ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; +import { getErrorMessageFromApolloError } from '~/utils/get-error-message-from-apollo-error.util'; export const useSnackBar = () => { const scopeId = useAvailableScopeIdOrThrow( @@ -54,16 +58,91 @@ export const useSnackBar = () => { [scopeId], ); - const enqueueSnackBar = useCallback( - (message: string, options?: Omit) => { + const enqueueSuccessSnackBar = useCallback( + ({ + message, + options, + }: { + message: string; + options?: Omit; + }) => { setSnackBarQueue({ id: uuidv4(), message, ...options, + variant: SnackBarVariant.Success, }); }, [setSnackBarQueue], ); - return { handleSnackBarClose, enqueueSnackBar }; + const enqueueInfoSnackBar = useCallback( + ({ + message, + options, + }: { + message: string; + options?: Omit; + }) => { + setSnackBarQueue({ + id: uuidv4(), + message, + ...options, + variant: SnackBarVariant.Info, + }); + }, + [setSnackBarQueue], + ); + + const enqueueWarningSnackBar = useCallback( + ({ + message, + options, + }: { + message: string; + options?: Omit; + }) => { + setSnackBarQueue({ + id: uuidv4(), + message, + ...options, + variant: SnackBarVariant.Warning, + }); + }, + [setSnackBarQueue], + ); + + const enqueueErrorSnackBar = useCallback( + ({ + apolloError, + message, + options, + }: ( + | { apolloError: ApolloError; message?: never } + | { apolloError?: never; message?: string } + ) & { + options?: Omit; + }) => { + const errorMessage = message + ? message + : apolloError + ? getErrorMessageFromApolloError(apolloError) + : t`An error occurred.`; + setSnackBarQueue({ + id: uuidv4(), + message: errorMessage, + ...options, + variant: SnackBarVariant.Error, + }); + }, + [setSnackBarQueue], + ); + + return { + handleSnackBarClose, + enqueueSuccessSnackBar, + enqueueErrorSnackBar, + enqueueInfoSnackBar, + enqueueWarningSnackBar, + }; }; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx index bc73c7be9..d3768f5c3 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdown/internal/MultiWorkspaceDropdownDefaultComponents.tsx @@ -8,7 +8,6 @@ import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUr import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; import { AppPath } from '@/types/AppPath'; import { SettingsPath } from '@/types/SettingsPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; @@ -20,6 +19,7 @@ import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MultiWorkspaceDropdownId'; import { multiWorkspaceDropdownState } from '@/ui/navigation/navigation-drawer/states/multiWorkspaceDropdownState'; import { useColorScheme } from '@/ui/theme/hooks/useColorScheme'; +import { ApolloError } from '@apollo/client'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; import { useRecoilValue, useSetRecoilState } from 'recoil'; @@ -59,7 +59,7 @@ export const MultiWorkspaceDropdownDefaultComponents = () => { const { buildWorkspaceUrl } = useBuildWorkspaceUrl(); const { closeDropdown } = useCloseDropdown(); const { signOut } = useAuth(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const { colorScheme, colorSchemeList } = useColorScheme(); const [signUpInNewWorkspaceMutation] = useSignUpInNewWorkspaceMutation(); @@ -86,9 +86,9 @@ export const MultiWorkspaceDropdownDefaultComponents = () => { '_blank', ); }, - onError: (error: Error) => { - enqueueSnackBar(error.message, { - variant: SnackBarVariant.Error, + onError: (error: ApolloError) => { + enqueueErrorSnackBar({ + apolloError: error, }); }, }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx index 974c4df4c..d058fa466 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx @@ -1,10 +1,10 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { FormRawJsonFieldInput } from '@/object-record/record-field/form-types/components/FormRawJsonFieldInput'; import { getFunctionOutputSchema } from '@/serverless-functions/utils/getFunctionOutputSchema'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Select } from '@/ui/input/components/Select'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; +import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; import { WorkflowWebhookTrigger } from '@/workflow/types/Workflow'; @@ -24,7 +24,6 @@ import { isDefined } from 'twenty-shared/utils'; import { IconCopy, useIcons } from 'twenty-ui/display'; import { useDebouncedCallback } from 'use-debounce'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; -import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; type WorkflowEditTriggerWebhookFormProps = { trigger: WorkflowWebhookTrigger; @@ -50,7 +49,7 @@ export const WorkflowEditTriggerWebhookForm = ({ trigger, triggerOptions, }: WorkflowEditTriggerWebhookFormProps) => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueSuccessSnackBar } = useSnackBar(); const theme = useTheme(); const { t } = useLingui(); const [errorMessages, setErrorMessages] = useState({}); @@ -75,9 +74,11 @@ export const WorkflowEditTriggerWebhookForm = ({ const copyToClipboard = async () => { await navigator.clipboard.writeText(webhookUrl); - enqueueSnackBar(t`Copied to clipboard!`, { - variant: SnackBarVariant.Success, - icon: , + enqueueSuccessSnackBar({ + message: t`Copied to clipboard!`, + options: { + icon: , + }, }); }; diff --git a/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteLink.tsx b/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteLink.tsx index e2c15a8c1..359fd0851 100644 --- a/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteLink.tsx +++ b/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteLink.tsx @@ -1,12 +1,11 @@ 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 { TextInput } from '@/ui/input/components/TextInput'; import { useLingui } from '@lingui/react/macro'; -import { Button } from 'twenty-ui/input'; import { IconCopy, IconLink } from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; const StyledContainer = styled.div` align-items: center; @@ -29,7 +28,7 @@ export const WorkspaceInviteLink = ({ const { t } = useLingui(); const theme = useTheme(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueSuccessSnackBar } = useSnackBar(); return ( @@ -42,10 +41,12 @@ export const WorkspaceInviteLink = ({ accent="blue" title={t`Copy link`} onClick={() => { - enqueueSnackBar(t`Link copied to clipboard`, { - variant: SnackBarVariant.Success, - icon: , - duration: 2000, + enqueueSuccessSnackBar({ + message: t`Link copied to clipboard`, + options: { + icon: , + duration: 2000, + }, }); navigator.clipboard.writeText(inviteLink); }} diff --git a/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteTeam.tsx b/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteTeam.tsx index ca8482e9a..540b6897d 100644 --- a/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteTeam.tsx +++ b/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteTeam.tsx @@ -4,7 +4,6 @@ import { useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { z } from 'zod'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInput } from '@/ui/input/components/TextInput'; import { sanitizeEmailList } from '@/workspace/utils/sanitizeEmailList'; @@ -71,7 +70,7 @@ type FormInput = { export const WorkspaceInviteTeam = () => { const { t } = useLingui(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); const { sendInvitation } = useCreateWorkspaceInvitation(); const { reset, handleSubmit, control, formState, watch } = useForm( @@ -89,21 +88,19 @@ export const WorkspaceInviteTeam = () => { const emailsList = sanitizeEmailList(emails.split(',')); const { data } = await sendInvitation({ emails: emailsList }); if (isDefined(data) && data.sendInvitations.result.length > 0) { - enqueueSnackBar( - `${data.sendInvitations.result.length} invitations sent`, - { - variant: SnackBarVariant.Success, + enqueueSuccessSnackBar({ + message: `${data.sendInvitations.result.length} invitations sent`, + options: { duration: 2000, }, - ); + }); return; } if (isDefined(data) && !data.sendInvitations.success) { - data.sendInvitations.errors.forEach((error) => { - enqueueSnackBar(error, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + options: { duration: 5000, - }); + }, }); } }); diff --git a/packages/twenty-front/src/pages/auth/PasswordReset.tsx b/packages/twenty-front/src/pages/auth/PasswordReset.tsx index 6e9400670..26e122cf4 100644 --- a/packages/twenty-front/src/pages/auth/PasswordReset.tsx +++ b/packages/twenty-front/src/pages/auth/PasswordReset.tsx @@ -7,10 +7,10 @@ import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex'; import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken'; import { AppPath } from '@/types/AppPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { Modal } from '@/ui/layout/modal/components/Modal'; +import { ApolloError } from '@apollo/client'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -78,7 +78,7 @@ const StyledMainButton = styled(MainButton)` export const PasswordReset = () => { const { t } = useLingui(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar(); const workspacePublicData = useRecoilValue(workspacePublicDataState); @@ -108,8 +108,8 @@ export const PasswordReset = () => { }, skip: !passwordResetToken || isTokenValid, onError: (error) => { - enqueueSnackBar(error?.message ?? 'Token Invalid', { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error, }); navigate(AppPath.Index); }, @@ -137,15 +137,15 @@ export const PasswordReset = () => { }); if (!data?.updatePasswordViaResetToken.success) { - enqueueSnackBar(t`There was an error while updating password.`, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: t`There was an error while updating password.`, }); return; } if (isLoggedIn) { - enqueueSnackBar(t`Password has been updated`, { - variant: SnackBarVariant.Success, + enqueueSuccessSnackBar({ + message: t`Password has been updated`, }); navigate(AppPath.Index); return; @@ -161,14 +161,9 @@ export const PasswordReset = () => { navigate(AppPath.Index); } catch (err) { logError(err); - enqueueSnackBar( - err instanceof Error - ? err.message - : t`An error occurred while updating password`, - { - variant: SnackBarVariant.Error, - }, - ); + enqueueErrorSnackBar({ + apolloError: err instanceof ApolloError ? err : undefined, + }); } }; diff --git a/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx b/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx index f204bead6..1975f6243 100644 --- a/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx +++ b/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx @@ -15,12 +15,12 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus'; import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { Modal } from '@/ui/layout/modal/components/Modal'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; +import { ApolloError } from '@apollo/client'; import { Trans, useLingui } from '@lingui/react/macro'; import { isDefined } from 'twenty-shared/utils'; import { H2Title } from 'twenty-ui/display'; @@ -59,7 +59,7 @@ type Form = z.infer; export const CreateProfile = () => { const { t } = useLingui(); const setNextOnboardingStatus = useSetNextOnboardingStatus(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState( currentWorkspaceMemberState, ); @@ -131,15 +131,15 @@ export const CreateProfile = () => { setNextOnboardingStatus(); } catch (error: any) { - enqueueSnackBar(error?.message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, }); } }, [ currentWorkspaceMember?.id, setNextOnboardingStatus, - enqueueSnackBar, + enqueueErrorSnackBar, setCurrentWorkspaceMember, setCurrentUser, updateOneRecord, diff --git a/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx b/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx index 3b8fc8c3d..50958833f 100644 --- a/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx +++ b/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx @@ -13,10 +13,10 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItem'; import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus'; import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { Modal } from '@/ui/layout/modal/components/Modal'; +import { ApolloError } from '@apollo/client'; import { Trans, useLingui } from '@lingui/react/macro'; import { isNonEmptyString } from '@sniptt/guards'; import { motion } from 'framer-motion'; @@ -65,7 +65,7 @@ const StyledPendingCreationLoader = styled(motion.div)` export const CreateWorkspace = () => { const { t } = useLingui(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const setNextOnboardingStatus = useSetNextOnboardingStatus(); const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems(); @@ -127,14 +127,15 @@ export const CreateWorkspace = () => { setNextOnboardingStatus(); } catch (error: any) { setPendingCreationLoaderStep(PendingCreationLoaderStep.None); - enqueueSnackBar(error?.message, { - variant: SnackBarVariant.Error, + + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, }); } }, [ activateWorkspace, - enqueueSnackBar, + enqueueErrorSnackBar, loadCurrentUser, refreshObjectMetadataItems, setNextOnboardingStatus, diff --git a/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx b/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx index 9bfdfe677..81a957339 100644 --- a/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx +++ b/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx @@ -4,7 +4,6 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState'; import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { Modal } from '@/ui/layout/modal/components/Modal'; @@ -65,7 +64,7 @@ type FormInput = z.infer; export const InviteTeam = () => { const { t } = useLingui(); const theme = useTheme(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueSuccessSnackBar } = useSnackBar(); const { sendInvitation } = useCreateWorkspaceInvitation(); const setNextOnboardingStatus = useSetNextOnboardingStatus(); const currentWorkspace = useRecoilValue(currentWorkspaceState); @@ -120,10 +119,12 @@ export const InviteTeam = () => { if (isDefined(currentWorkspace?.inviteHash)) { const inviteLink = `${window.location.origin}/invite/${currentWorkspace?.inviteHash}`; navigator.clipboard.writeText(inviteLink); - enqueueSnackBar(t`Link copied to clipboard`, { - variant: SnackBarVariant.Success, - icon: , - duration: 2000, + enqueueSuccessSnackBar({ + message: t`Link copied to clipboard`, + options: { + icon: , + duration: 2000, + }, }); } }; @@ -143,15 +144,17 @@ export const InviteTeam = () => { throw result.errors; } if (emails.length > 0) { - enqueueSnackBar(t`Invite link sent to email addresses`, { - variant: SnackBarVariant.Success, - duration: 2000, + enqueueSuccessSnackBar({ + message: t`Invite link sent to email addresses`, + options: { + duration: 2000, + }, }); } setNextOnboardingStatus(); }, - [enqueueSnackBar, sendInvitation, setNextOnboardingStatus, t], + [enqueueSuccessSnackBar, sendInvitation, setNextOnboardingStatus, t], ); const handleSkip = async () => { diff --git a/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx b/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx index c7b593652..3a47172f4 100644 --- a/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx @@ -12,7 +12,6 @@ import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPath } from '@/types/SettingsPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInput } from '@/ui/input/components/TextInput'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; @@ -23,6 +22,7 @@ import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { WorkspaceInviteLink } from '@/workspace/components/WorkspaceInviteLink'; import { WorkspaceInviteTeam } from '@/workspace/components/WorkspaceInviteTeam'; +import { ApolloError } from '@apollo/client'; import { formatDistanceToNow } from 'date-fns'; import { isDefined } from 'twenty-shared/utils'; import { @@ -94,7 +94,7 @@ const StyledNoMembers = styled(TableCell)` export const SettingsWorkspaceMembers = () => { const { t } = useLingui(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const theme = useTheme(); const [workspaceMemberToDelete, setWorkspaceMemberToDelete] = useState< string | undefined @@ -127,9 +127,9 @@ export const SettingsWorkspaceMembers = () => { }; useGetWorkspaceInvitationsQuery({ - onError: (error: Error) => { - enqueueSnackBar(error.message, { - variant: SnackBarVariant.Error, + onError: (error: ApolloError) => { + enqueueErrorSnackBar({ + apolloError: error, }); }, onCompleted: (data) => { @@ -140,9 +140,11 @@ export const SettingsWorkspaceMembers = () => { const handleRemoveWorkspaceInvitation = async (appTokenId: string) => { const result = await deleteWorkspaceInvitation({ appTokenId }); if (isDefined(result.errors)) { - enqueueSnackBar(t`Error deleting invitation`, { - variant: SnackBarVariant.Error, - duration: 2000, + enqueueErrorSnackBar({ + message: t`Error deleting invitation`, + options: { + duration: 2000, + }, }); } }; @@ -150,9 +152,11 @@ export const SettingsWorkspaceMembers = () => { const handleResendWorkspaceInvitation = async (appTokenId: string) => { const result = await resendInvitation({ appTokenId }); if (isDefined(result.errors)) { - enqueueSnackBar(t`Error resending invitation`, { - variant: SnackBarVariant.Error, - duration: 2000, + enqueueErrorSnackBar({ + message: t`Error resending invitation`, + options: { + duration: 2000, + }, }); } }; diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx index ebe1837ce..829b76987 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx @@ -11,20 +11,20 @@ import { settingsDataModelObjectAboutFormSchema, } from '@/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema'; import { SettingsPath } from '@/types/SettingsPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { ApolloError } from '@apollo/client'; import { useLingui } from '@lingui/react/macro'; -import { useNavigateSettings } from '~/hooks/useNavigateSettings'; -import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; +import { useState } from 'react'; import { H2Title } from 'twenty-ui/display'; import { Section } from 'twenty-ui/layout'; -import { useState } from 'react'; +import { useNavigateSettings } from '~/hooks/useNavigateSettings'; +import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; export const SettingsNewObject = () => { const { t } = useLingui(); const navigate = useNavigateSettings(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const [isLoading, setIsLoading] = useState(false); const { createOneObjectMetadataItem } = useCreateOneObjectMetadataItem(); @@ -56,8 +56,8 @@ export const SettingsNewObject = () => { } catch (error) { // eslint-disable-next-line no-console console.error(error); - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, }); } finally { setIsLoading(false); diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx index a2df52a27..f5e61c5be 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx @@ -23,9 +23,9 @@ import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/vali import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; import { AppPath } from '@/types/AppPath'; import { SettingsPath } from '@/types/SettingsPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { ApolloError } from '@apollo/client'; import { useLingui } from '@lingui/react/macro'; import { isDefined } from 'twenty-shared/utils'; import { H2Title, IconArchive, IconArchiveOff } from 'twenty-ui/display'; @@ -47,7 +47,7 @@ export const SettingsObjectFieldEdit = () => { const navigateApp = useNavigateApp(); const { t } = useLingui(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const { objectNamePlural = '', fieldName = '' } = useParams(); const { findObjectMetadataItemByNamePlural } = @@ -147,8 +147,8 @@ export const SettingsObjectFieldEdit = () => { }); } } catch (error) { - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, }); } }; diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldConfigure.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldConfigure.tsx index 457d65033..5b31e6dc4 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldConfigure.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldConfigure.tsx @@ -13,7 +13,6 @@ import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/vali import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; import { AppPath } from '@/types/AppPath'; import { SettingsPath } from '@/types/SettingsPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { View } from '@/views/types/View'; @@ -51,7 +50,7 @@ export const SettingsObjectNewFieldConfigure = () => { const fieldType = (searchParams.get('fieldType') as SettingsFieldType) || FieldMetadataType.TEXT; - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const { findActiveObjectMetadataItemByNamePlural } = useFilteredObjectMetadataItems(); @@ -161,14 +160,11 @@ export const SettingsObjectNewFieldConfigure = () => { 'duplicate key value violates unique constraint "IndexOnNameObjectMetadataIdAndWorkspaceIdUnique"', ); - enqueueSnackBar( - isDuplicateFieldNameInObject + enqueueErrorSnackBar({ + message: isDuplicateFieldNameInObject ? t`Please use different names for your source and destination fields` - : (error as Error).message, - { - variant: SnackBarVariant.Error, - }, - ); + : undefined, + }); } }; if (!activeObjectMetadataItem) return null; diff --git a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx index d6406281e..a884dead1 100644 --- a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx +++ b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx @@ -17,7 +17,6 @@ import { ApiKey } from '@/settings/developers/types/api-key/ApiKey'; import { computeNewExpirationDate } from '@/settings/developers/utils/computeNewExpirationDate'; import { formatExpiration } from '@/settings/developers/utils/formatExpiration'; import { SettingsPath } from '@/types/SettingsPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInput } from '@/ui/input/components/TextInput'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; @@ -50,7 +49,7 @@ const REGENERATE_API_KEY_MODAL_ID = 'regenerate-api-key-modal'; export const SettingsDevelopersApiKeyDetail = () => { const { t } = useLingui(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const { openModal } = useModal(); const [isLoading, setIsLoading] = useState(false); @@ -97,9 +96,7 @@ export const SettingsDevelopersApiKeyDetail = () => { navigate(SettingsPath.APIs); } } catch (err) { - enqueueSnackBar(t`Error deleting api key: ${err}`, { - variant: SnackBarVariant.Error, - }); + enqueueErrorSnackBar({ message: t`Error deleting api key.` }); } finally { setIsLoading(false); } @@ -149,8 +146,8 @@ export const SettingsDevelopersApiKeyDetail = () => { } } } catch (err) { - enqueueSnackBar(t`Error regenerating api key: ${err}`, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + message: t`Error regenerating api key.`, }); } finally { setIsLoading(false); diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx index f72b2f1da..7d6664030 100644 --- a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx @@ -17,15 +17,15 @@ import { useIsSettingsIntegrationEnabled } from '@/settings/integrations/hooks/u import { useSettingsIntegrationCategories } from '@/settings/integrations/hooks/useSettingsIntegrationCategories'; import { AppPath } from '@/types/AppPath'; import { SettingsPath } from '@/types/SettingsPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { ApolloError } from '@apollo/client'; +import { H2Title } from 'twenty-ui/display'; +import { Section } from 'twenty-ui/layout'; import { CreateRemoteServerInput } from '~/generated-metadata/graphql'; import { useNavigateApp } from '~/hooks/useNavigateApp'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; -import { H2Title } from 'twenty-ui/display'; -import { Section } from 'twenty-ui/layout'; const createRemoteServerInputPostgresSchema = settingsIntegrationPostgreSQLConnectionFormSchema.transform( @@ -79,7 +79,7 @@ export const SettingsIntegrationNewDatabaseConnection = () => { ); const { createOneDatabaseConnection } = useCreateOneDatabaseConnection(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const isIntegrationEnabled = useIsSettingsIntegrationEnabled(databaseKey); @@ -131,8 +131,8 @@ export const SettingsIntegrationNewDatabaseConnection = () => { connectionId, }); } catch (error) { - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, }); } }; diff --git a/packages/twenty-front/src/pages/settings/security/SettingsSecurityApprovedAccessDomain.tsx b/packages/twenty-front/src/pages/settings/security/SettingsSecurityApprovedAccessDomain.tsx index a20cb3456..b8d3c2d80 100644 --- a/packages/twenty-front/src/pages/settings/security/SettingsSecurityApprovedAccessDomain.tsx +++ b/packages/twenty-front/src/pages/settings/security/SettingsSecurityApprovedAccessDomain.tsx @@ -1,10 +1,10 @@ import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPath } from '@/types/SettingsPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInput } from '@/ui/input/components/TextInput'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { ApolloError } from '@apollo/client'; import { zodResolver } from '@hookform/resolvers/zod'; import { Trans, useLingui } from '@lingui/react/macro'; import { Controller, useForm } from 'react-hook-form'; @@ -20,7 +20,7 @@ export const SettingsSecurityApprovedAccessDomain = () => { const { t } = useLingui(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); const [createApprovedAccessDomain] = useCreateApprovedAccessDomainMutation(); @@ -62,20 +62,20 @@ export const SettingsSecurityApprovedAccessDomain = () => { }, }, onCompleted: () => { - enqueueSnackBar(t`Please check your email for a verification link.`, { - variant: SnackBarVariant.Success, + enqueueSuccessSnackBar({ + message: t`Please check your email for a verification link.`, }); navigate(SettingsPath.Security); }, onError: (error) => { - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, }); }, }); } catch (error) { - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, }); } }; diff --git a/packages/twenty-front/src/pages/settings/security/SettingsSecuritySSOIdentifyProvider.tsx b/packages/twenty-front/src/pages/settings/security/SettingsSecuritySSOIdentifyProvider.tsx index aaafd03c9..b618a6c3a 100644 --- a/packages/twenty-front/src/pages/settings/security/SettingsSecuritySSOIdentifyProvider.tsx +++ b/packages/twenty-front/src/pages/settings/security/SettingsSecuritySSOIdentifyProvider.tsx @@ -7,21 +7,21 @@ import { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/typ import { sSOIdentityProviderDefaultValues } from '@/settings/security/utils/sSOIdentityProviderDefaultValues'; import { SSOIdentitiesProvidersParamsSchema } from '@/settings/security/validation-schemas/SSOIdentityProviderSchema'; import { SettingsPath } from '@/types/SettingsPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { ApolloError } from '@apollo/client'; import { zodResolver } from '@hookform/resolvers/zod'; +import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import pick from 'lodash.pick'; import { FormProvider, useForm } from 'react-hook-form'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; -import { t } from '@lingui/core/macro'; export const SettingsSecuritySSOIdentifyProvider = () => { const navigate = useNavigateSettings(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); const { createSSOIdentityProvider } = useCreateSSOIdentityProvider(); const form = useForm({ @@ -48,8 +48,8 @@ export const SettingsSecuritySSOIdentifyProvider = () => { navigate(SettingsPath.Security); } catch (error) { - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, }); } }; diff --git a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx index 5a4aa4a9a..202600b68 100644 --- a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx +++ b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx @@ -8,17 +8,18 @@ import { usePublishOneServerlessFunction } from '@/settings/serverless-functions import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction'; import { SettingsPath } from '@/types/SettingsPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { TabList } from '@/ui/layout/tab-list/components/TabList'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; +import { ApolloError } from '@apollo/client'; import { useState } from 'react'; import { useParams } from 'react-router-dom'; import { isDefined } from 'twenty-shared/utils'; import { IconCode, IconSettings, IconTestPipe } from 'twenty-ui/display'; import { useDebouncedCallback } from 'use-debounce'; +import { getErrorMessageFromApolloError } from '~/utils/get-error-message-from-apollo-error.util'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; @@ -26,7 +27,7 @@ const SERVERLESS_FUNCTION_DETAIL_ID = 'serverless-function-detail'; export const SettingsServerlessFunctionDetail = () => { const { serverlessFunctionId = '' } = useParams(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar(); const [activeTabId, setActiveTabId] = useRecoilComponentStateV2( activeTabIdComponentState, SERVERLESS_FUNCTION_DETAIL_ID, @@ -88,12 +89,9 @@ export const SettingsServerlessFunctionDetail = () => { })); await handleSave(); } catch (err) { - enqueueSnackBar( - (err as Error)?.message || 'An error occurred while reset function', - { - variant: SnackBarVariant.Error, - }, - ); + enqueueErrorSnackBar({ + apolloError: err instanceof ApolloError ? err : undefined, + }); } }; @@ -102,17 +100,16 @@ export const SettingsServerlessFunctionDetail = () => { await publishOneServerlessFunction({ id: serverlessFunctionId, }); - enqueueSnackBar(`New function version has been published`, { - variant: SnackBarVariant.Success, + enqueueSuccessSnackBar({ + message: `New function version has been published`, }); } catch (err) { - enqueueSnackBar( - (err as Error)?.message || - 'An error occurred while publishing new version', - { - variant: SnackBarVariant.Error, - }, - ); + enqueueErrorSnackBar({ + message: + err instanceof ApolloError + ? getErrorMessageFromApolloError(err) + : 'An error occurred while publishing new version', + }); } }; diff --git a/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomainRecords.tsx b/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomainRecords.tsx index a30b0ce47..8c1ff8242 100644 --- a/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomainRecords.tsx +++ b/packages/twenty-front/src/pages/settings/workspace/SettingsCustomDomainRecords.tsx @@ -1,17 +1,16 @@ -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Table } from '@/ui/layout/table/components/Table'; import { TableBody } from '@/ui/layout/table/components/TableBody'; import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableRow } from '@/ui/layout/table/components/TableRow'; +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; +import { IconCopy } from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; import { useDebouncedCallback } from 'use-debounce'; import { CustomDomainValidRecords } from '~/generated/graphql'; -import { useTheme } from '@emotion/react'; -import { Button } from 'twenty-ui/input'; -import { IconCopy } from 'twenty-ui/display'; const StyledTable = styled(Table)` border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; @@ -46,7 +45,7 @@ export const SettingsCustomDomainRecords = ({ }: { records: CustomDomainValidRecords['records']; }) => { - const { enqueueSnackBar } = useSnackBar(); + const { enqueueSuccessSnackBar } = useSnackBar(); const theme = useTheme(); @@ -54,9 +53,11 @@ export const SettingsCustomDomainRecords = ({ const copyToClipboard = (value: string) => { navigator.clipboard.writeText(value); - enqueueSnackBar(t`Copied to clipboard!`, { - variant: SnackBarVariant.Success, - icon: , + enqueueSuccessSnackBar({ + message: t`Copied to clipboard!`, + options: { + icon: , + }, }); }; diff --git a/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx b/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx index 6328489a2..4cb32cece 100644 --- a/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx +++ b/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx @@ -6,7 +6,6 @@ import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirect import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPath } from '@/types/SettingsPath'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { useModal } from '@/ui/layout/modal/hooks/useModal'; @@ -60,7 +59,7 @@ export const SettingsDomain = () => { }) .required(); - const { enqueueSnackBar } = useSnackBar(); + const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); const [updateWorkspace] = useUpdateWorkspaceMutation(); const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); @@ -105,11 +104,11 @@ export const SettingsDomain = () => { customDomain: customDomain && customDomain.length > 0 ? customDomain : null, }); - enqueueSnackBar(t`Custom domain updated`, { - variant: SnackBarVariant.Success, + enqueueSuccessSnackBar({ + message: t`Custom domain updated`, }); }, - onError: (error) => { + onError: (error: ApolloError) => { if ( error instanceof ApolloError && error.graphQLErrors[0]?.extensions?.code === 'CONFLICT' @@ -119,8 +118,8 @@ export const SettingsDomain = () => { message: t`Subdomain already taken`, }); } - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error, }); }, }); @@ -136,7 +135,7 @@ export const SettingsDomain = () => { subdomain, }, }, - onError: (error) => { + onError: (error: ApolloError) => { if ( error instanceof ApolloError && error.graphQLErrors[0]?.extensions?.code === 'CONFLICT' @@ -147,8 +146,8 @@ export const SettingsDomain = () => { message: t`Subdomain already taken`, }); } - enqueueSnackBar((error as Error).message, { - variant: SnackBarVariant.Error, + enqueueErrorSnackBar({ + apolloError: error, }); }, onCompleted: async () => { @@ -163,8 +162,8 @@ export const SettingsDomain = () => { subdomain, }); - enqueueSnackBar(t`Subdomain updated`, { - variant: SnackBarVariant.Success, + enqueueSuccessSnackBar({ + message: t`Subdomain updated`, }); await redirectToWorkspaceDomain(currentUrl.toString()); @@ -179,14 +178,14 @@ export const SettingsDomain = () => { subdomainValue === currentWorkspace?.subdomain && customDomainValue === currentWorkspace?.customDomain ) { - return enqueueSnackBar(t`No change detected`, { - variant: SnackBarVariant.Error, + return enqueueErrorSnackBar({ + message: t`No change detected`, }); } if (!values || !currentWorkspace) { - return enqueueSnackBar(t`Invalid form values`, { - variant: SnackBarVariant.Error, + return enqueueErrorSnackBar({ + message: t`Invalid form values`, }); } diff --git a/packages/twenty-front/src/utils/get-error-message-from-apollo-error.util.ts b/packages/twenty-front/src/utils/get-error-message-from-apollo-error.util.ts new file mode 100644 index 000000000..9a98e0f69 --- /dev/null +++ b/packages/twenty-front/src/utils/get-error-message-from-apollo-error.util.ts @@ -0,0 +1,14 @@ +import { ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; + +export const getErrorMessageFromApolloError = (error: ApolloError): string => { + if (!isDefined(error.graphQLErrors?.[0]?.extensions?.userFriendlyMessage)) { + return t`An error occurred.`; + } + + return ( + (error.graphQLErrors[0].extensions?.userFriendlyMessage as string) ?? + t`An error occurred.` + ); +}; diff --git a/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain-exception-filter.ts b/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain-exception-filter.ts index 26e23b726..ff77e408c 100644 --- a/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain-exception-filter.ts +++ b/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain-exception-filter.ts @@ -17,7 +17,11 @@ export class ApprovedAccessDomainExceptionFilter implements ExceptionFilter { case ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_VALIDATION_TOKEN_INVALID: case ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VALIDATED: case ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_MUST_BE_A_COMPANY_DOMAIN: - throw new ForbiddenError(exception.message); + throw new ForbiddenError(exception.message, { + extensions: { + userFriendlyMessage: exception.userFriendlyMessage, + }, + }); default: { const _exhaustiveCheck: never = exception.code; diff --git a/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.exception.ts b/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.exception.ts index 1fde10e40..1cbde171e 100644 --- a/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.exception.ts @@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception'; export class ApprovedAccessDomainException extends CustomException { declare code: ApprovedAccessDomainExceptionCode; - constructor(message: string, code: ApprovedAccessDomainExceptionCode) { - super(message, code); + constructor( + message: string, + code: ApprovedAccessDomainExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, + ) { + super(message, code, userFriendlyMessage); } } diff --git a/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts b/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts index 499b395be..1e2bc929a 100644 --- a/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts +++ b/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts @@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import crypto from 'crypto'; +import { t } from '@lingui/core/macro'; import { render } from '@react-email/render'; import { SendApprovedAccessDomainValidation } from 'twenty-emails'; import { APP_LOCALES } from 'twenty-shared/translations'; @@ -18,8 +19,8 @@ import { DomainManagerService } from 'src/engine/core-modules/domain-manager/ser import { EmailService } from 'src/engine/core-modules/email/email.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { isWorkDomain } from 'src/utils/is-work-email'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { isWorkDomain } from 'src/utils/is-work-email'; @Injectable() // eslint-disable-next-line @nx/workspace-inject-workspace-repository @@ -42,6 +43,9 @@ export class ApprovedAccessDomainService { throw new ApprovedAccessDomainException( 'Approved access domain has already been validated', ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VERIFIED, + { + userFriendlyMessage: t`Approved access domain has already been validated`, + }, ); } @@ -49,6 +53,9 @@ export class ApprovedAccessDomainService { throw new ApprovedAccessDomainException( 'Approved access domain does not match email domain', ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_DOES_NOT_MATCH_DOMAIN_EMAIL, + { + userFriendlyMessage: t`Approved access domain does not match email domain`, + }, ); } @@ -118,6 +125,9 @@ export class ApprovedAccessDomainService { throw new ApprovedAccessDomainException( 'Approved access domain has already been validated', ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VALIDATED, + { + userFriendlyMessage: t`Approved access domain has already been validated`, + }, ); } diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts index 055b9df38..4d2163d56 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts @@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception'; export class AuthException extends CustomException { declare code: AuthExceptionCode; - constructor(message: string, code: AuthExceptionCode) { - super(message, code); + constructor( + message: string, + code: AuthExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, + ) { + super(message, code, userFriendlyMessage); } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter.ts index a32d48307..6cba07824 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter.ts @@ -1,5 +1,7 @@ import { Catch, ExceptionFilter } from '@nestjs/common'; +import { t } from '@lingui/core/macro'; + import { AuthException, AuthExceptionCode, @@ -16,26 +18,39 @@ export class AuthGraphqlApiExceptionFilter implements ExceptionFilter { catch(exception: AuthException) { switch (exception.code) { case AuthExceptionCode.CLIENT_NOT_FOUND: - throw new NotFoundError(exception.message); + throw new NotFoundError(exception.message, { + userFriendlyMessage: exception.userFriendlyMessage, + }); case AuthExceptionCode.INVALID_INPUT: - throw new UserInputError(exception.message); + throw new UserInputError(exception.message, { + userFriendlyMessage: exception.userFriendlyMessage, + }); case AuthExceptionCode.FORBIDDEN_EXCEPTION: case AuthExceptionCode.INSUFFICIENT_SCOPES: case AuthExceptionCode.OAUTH_ACCESS_DENIED: case AuthExceptionCode.SSO_AUTH_FAILED: case AuthExceptionCode.USE_SSO_AUTH: case AuthExceptionCode.SIGNUP_DISABLED: - case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED: - case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED: case AuthExceptionCode.MISSING_ENVIRONMENT_VARIABLE: case AuthExceptionCode.INVALID_JWT_TOKEN_TYPE: - throw new ForbiddenError(exception.message); + throw new ForbiddenError(exception.message, { + userFriendlyMessage: exception.userFriendlyMessage, + }); + case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED: + case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED: + throw new ForbiddenError(exception.message, { + userFriendlyMessage: t`Authentication is not enabled with this provider.`, + }); case AuthExceptionCode.EMAIL_NOT_VERIFIED: case AuthExceptionCode.INVALID_DATA: throw new ForbiddenError(exception.message, { subCode: AuthExceptionCode.EMAIL_NOT_VERIFIED, + userFriendlyMessage: t`Email is not verified.`, }); case AuthExceptionCode.UNAUTHENTICATED: + throw new AuthenticationError(exception.message, { + userFriendlyMessage: t`You must be authenticated to perform this action.`, + }); case AuthExceptionCode.USER_NOT_FOUND: case AuthExceptionCode.WORKSPACE_NOT_FOUND: throw new AuthenticationError(exception.message); diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index 15d353a5d..a11e94a19 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -169,6 +169,9 @@ export class AuthService { throw new AuthException( 'Wrong password', AuthExceptionCode.FORBIDDEN_EXCEPTION, + { + userFriendlyMessage: t`Wrong password`, + }, ); } diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts index 366bf131e..40fb44ad4 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts @@ -2,6 +2,7 @@ import { HttpService } from '@nestjs/axios'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { t } from '@lingui/core/macro'; import { TWENTY_ICONS_BASE_URL } from 'twenty-shared/constants'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; import { Repository } from 'typeorm'; @@ -67,6 +68,9 @@ export class SignInUpService { throw new AuthException( 'Email is required', AuthExceptionCode.INVALID_INPUT, + { + userFriendlyMessage: t`Email is required`, + }, ); } @@ -111,6 +115,9 @@ export class SignInUpService { throw new AuthException( 'Password too weak', AuthExceptionCode.INVALID_INPUT, + { + userFriendlyMessage: t`Password too weak`, + }, ); } @@ -130,6 +137,9 @@ export class SignInUpService { throw new AuthException( 'Wrong password', AuthExceptionCode.FORBIDDEN_EXCEPTION, + { + userFriendlyMessage: t`Wrong password`, + }, ); } } @@ -153,6 +163,9 @@ export class SignInUpService { throw new AuthException( 'Email is required', AuthExceptionCode.INVALID_INPUT, + { + userFriendlyMessage: t`Email is required`, + }, ); } @@ -194,6 +207,9 @@ export class SignInUpService { throw new AuthException( 'Workspace is not ready to welcome new members', AuthExceptionCode.FORBIDDEN_EXCEPTION, + { + userFriendlyMessage: t`Workspace is not ready to welcome new members`, + }, ); } @@ -207,6 +223,9 @@ export class SignInUpService { throw new AuthException( 'User is not part of the workspace', AuthExceptionCode.FORBIDDEN_EXCEPTION, + { + userFriendlyMessage: t`User is not part of the workspace`, + }, ); } } @@ -340,6 +359,9 @@ export class SignInUpService { throw new AuthException( 'Email is required', AuthExceptionCode.INVALID_INPUT, + { + userFriendlyMessage: t`Email is required`, + }, ); } diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification-exception-filter.util.ts b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification-exception-filter.util.ts index 218f779a2..09a703ab9 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification-exception-filter.util.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification-exception-filter.util.ts @@ -1,5 +1,7 @@ import { Catch, ExceptionFilter } from '@nestjs/common'; +import { t } from '@lingui/core/macro'; + import { EmailVerificationException, EmailVerificationExceptionCode, @@ -13,17 +15,32 @@ import { export class EmailVerificationExceptionFilter implements ExceptionFilter { catch(exception: EmailVerificationException) { switch (exception.code) { + case EmailVerificationExceptionCode.TOKEN_EXPIRED: + throw new ForbiddenError(exception.message, { + subCode: exception.code, + userFriendlyMessage: t`Request has expired, please try again.`, + }); case EmailVerificationExceptionCode.INVALID_TOKEN: case EmailVerificationExceptionCode.INVALID_APP_TOKEN_TYPE: - case EmailVerificationExceptionCode.TOKEN_EXPIRED: case EmailVerificationExceptionCode.RATE_LIMIT_EXCEEDED: throw new ForbiddenError(exception.message, { subCode: exception.code, }); case EmailVerificationExceptionCode.EMAIL_MISSING: + throw new UserInputError(exception.message, { + subCode: exception.code, + }); case EmailVerificationExceptionCode.EMAIL_ALREADY_VERIFIED: - case EmailVerificationExceptionCode.INVALID_EMAIL: + throw new UserInputError(exception.message, { + subCode: exception.code, + userFriendlyMessage: t`Email already verified.`, + }); case EmailVerificationExceptionCode.EMAIL_VERIFICATION_NOT_REQUIRED: + throw new UserInputError(exception.message, { + subCode: exception.code, + userFriendlyMessage: t`Email verification not required.`, + }); + case EmailVerificationExceptionCode.INVALID_EMAIL: throw new UserInputError(exception.message, { subCode: exception.code, }); diff --git a/packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook.ts b/packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook.ts index ff2f5d304..3b0f60426 100644 --- a/packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook.ts +++ b/packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook.ts @@ -4,6 +4,7 @@ import { OnExecuteDoneHookResultOnNextHook, Plugin, } from '@envelop/core'; +import { t } from '@lingui/core/macro'; import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql'; import { GraphQLContext } from 'src/engine/api/graphql/graphql-config/interfaces/graphql-context.interface'; @@ -24,8 +25,7 @@ import { const DEFAULT_EVENT_ID_KEY = 'exceptionEventId'; const SCHEMA_VERSION_HEADER = 'x-schema-version'; -const SCHEMA_MISMATCH_ERROR = - 'Your workspace has been updated with a new data model. Please refresh the page.'; +const SCHEMA_MISMATCH_ERROR = 'Schema version mismatch.'; type GraphQLErrorHandlerHookOptions = { metricsService: MetricsService; @@ -191,11 +191,22 @@ export const useGraphQLErrorHandlerHook = < const transformedErrors = processedErrors.map((error) => { const graphqlError = error instanceof BaseGraphQLError - ? error + ? { + ...error, + extensions: { + ...error.extensions, + userFriendlyMessage: + error.extensions.userFriendlyMessage ?? + t`An error occurred.`, + }, + } : generateGraphQLErrorFromError(error); if (error.eventId && eventIdKey) { - graphqlError.extensions[eventIdKey] = error.eventId; + graphqlError.extensions = { + ...graphqlError.extensions, + [eventIdKey]: error.eventId, + }; } return graphqlError; @@ -224,7 +235,11 @@ export const useGraphQLErrorHandlerHook = < requestMetadataVersion && requestMetadataVersion !== `${currentMetadataVersion}` ) { - throw new GraphQLError(SCHEMA_MISMATCH_ERROR); + throw new GraphQLError(SCHEMA_MISMATCH_ERROR, { + extensions: { + userFriendlyMessage: t`Your workspace has been updated with a new data model. Please refresh the page.`, + }, + }); } } }, diff --git a/packages/twenty-server/src/engine/core-modules/graphql/utils/generate-graphql-error-from-error.util.ts b/packages/twenty-server/src/engine/core-modules/graphql/utils/generate-graphql-error-from-error.util.ts index ef0434978..8f102d048 100644 --- a/packages/twenty-server/src/engine/core-modules/graphql/utils/generate-graphql-error-from-error.util.ts +++ b/packages/twenty-server/src/engine/core-modules/graphql/utils/generate-graphql-error-from-error.util.ts @@ -1,13 +1,25 @@ +import { t } from '@lingui/core/macro'; + import { BaseGraphQLError, ErrorCode, } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { CustomException } from 'src/utils/custom-exception'; -export const generateGraphQLErrorFromError = (error: Error) => { +export const generateGraphQLErrorFromError = ( + error: Error | CustomException, +) => { const graphqlError = new BaseGraphQLError( error.message, ErrorCode.INTERNAL_SERVER_ERROR, ); + if (error instanceof CustomException) { + graphqlError.extensions.userFriendlyMessage = + error.userFriendlyMessage ?? t`An error occurred.`; + } else { + graphqlError.extensions.userFriendlyMessage = t`An error occurred.`; + } + return graphqlError; }; diff --git a/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts b/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts index ae8d50ed9..81262c1be 100644 --- a/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts +++ b/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts @@ -159,8 +159,9 @@ export class UserInputError extends BaseGraphQLError { } export class NotFoundError extends BaseGraphQLError { - constructor(message: string) { - super(message, ErrorCode.NOT_FOUND); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(message: string, extensions?: Record) { + super(message, ErrorCode.NOT_FOUND, extensions); Object.defineProperty(this, 'name', { value: 'NotFoundError' }); } @@ -175,8 +176,9 @@ export class MethodNotAllowedError extends BaseGraphQLError { } export class ConflictError extends BaseGraphQLError { - constructor(message: string) { - super(message, ErrorCode.CONFLICT); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(message: string, extensions?: Record) { + super(message, ErrorCode.CONFLICT, extensions); Object.defineProperty(this, 'name', { value: 'ConflictError' }); } diff --git a/packages/twenty-server/src/engine/core-modules/record-transformer/record-transformer.exception.ts b/packages/twenty-server/src/engine/core-modules/record-transformer/record-transformer.exception.ts index 2ba94bf4a..a7dfc7acf 100644 --- a/packages/twenty-server/src/engine/core-modules/record-transformer/record-transformer.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/record-transformer/record-transformer.exception.ts @@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception'; export class RecordTransformerException extends CustomException { declare code: RecordTransformerExceptionCode; - constructor(message: string, code: RecordTransformerExceptionCode) { - super(message, code); + constructor( + message: string, + code: RecordTransformerExceptionCode, + userFriendlyMessage?: string, + ) { + super(message, code, userFriendlyMessage); } } diff --git a/packages/twenty-server/src/engine/core-modules/record-transformer/utils/record-transformer-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/core-modules/record-transformer/utils/record-transformer-graphql-api-exception-handler.util.ts index eb5d16859..4597f0b0d 100644 --- a/packages/twenty-server/src/engine/core-modules/record-transformer/utils/record-transformer-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/core-modules/record-transformer/utils/record-transformer-graphql-api-exception-handler.util.ts @@ -17,7 +17,9 @@ export const recordTransformerGraphqlApiExceptionHandler = ( case RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE_AND_COUNTRY_CODE: case RecordTransformerExceptionCode.INVALID_PHONE_CALLING_CODE: case RecordTransformerExceptionCode.INVALID_URL: - throw new UserInputError(error.message); + throw new UserInputError(error.message, { + userFriendlyMessage: error.userFriendlyMessage, + }); default: { assertUnreachable(error.code); } diff --git a/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts b/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts index 38a6d32e8..52f1c6a28 100644 --- a/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts +++ b/packages/twenty-server/src/engine/core-modules/record-transformer/utils/transform-phones-value.util.ts @@ -1,3 +1,4 @@ +import { t } from '@lingui/core/macro'; import { isNonEmptyString } from '@sniptt/guards'; import { CountryCallingCode, @@ -61,6 +62,7 @@ const validatePrimaryPhoneCountryCodeAndCallingCode = ({ throw new RecordTransformerException( `Invalid country code ${countryCode}`, RecordTransformerExceptionCode.INVALID_PHONE_COUNTRY_CODE, + t`Invalid country code ${countryCode}`, ); } @@ -74,6 +76,7 @@ const validatePrimaryPhoneCountryCodeAndCallingCode = ({ throw new RecordTransformerException( `Invalid calling code ${callingCode}`, RecordTransformerExceptionCode.INVALID_PHONE_CALLING_CODE, + t`Invalid calling code ${callingCode}`, ); } @@ -86,6 +89,7 @@ const validatePrimaryPhoneCountryCodeAndCallingCode = ({ throw new RecordTransformerException( `Provided country code and calling code are conflicting`, RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE_AND_COUNTRY_CODE, + t`Provided country code and calling code are conflicting`, ); } }; @@ -106,6 +110,7 @@ const parsePhoneNumberExceptionWrapper = ({ throw new RecordTransformerException( `Provided phone number is invalid ${number}`, RecordTransformerExceptionCode.INVALID_PHONE_NUMBER, + t`Provided phone number is invalid ${number}`, ); } }; @@ -129,6 +134,7 @@ const validateAndInferMetadataFromPrimaryPhoneNumber = ({ throw new RecordTransformerException( 'Provided and inferred country code are conflicting', RecordTransformerExceptionCode.CONFLICTING_PHONE_COUNTRY_CODE, + t`Provided and inferred country code are conflicting`, ); } @@ -140,6 +146,7 @@ const validateAndInferMetadataFromPrimaryPhoneNumber = ({ throw new RecordTransformerException( 'Provided and inferred calling code are conflicting', RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE, + t`Provided and inferred calling code are conflicting`, ); } diff --git a/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter.ts index 83531aede..cf20f77a4 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter.ts @@ -19,9 +19,17 @@ export const handleWorkflowTriggerException = ( case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER: case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS: case WorkflowTriggerExceptionCode.FORBIDDEN: - throw new UserInputError(exception.message); + throw new UserInputError(exception.message, { + extensions: { + userFriendlyMessage: exception.userFriendlyMessage, + }, + }); case WorkflowTriggerExceptionCode.NOT_FOUND: - throw new NotFoundError(exception.message); + throw new NotFoundError(exception.message, { + extensions: { + userFriendlyMessage: exception.userFriendlyMessage, + }, + }); case WorkflowTriggerExceptionCode.INTERNAL_ERROR: throw exception; default: { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.exception.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.exception.ts index a544b183b..d2ac95ef6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.exception.ts @@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception'; export class FieldMetadataException extends CustomException { declare code: FieldMetadataExceptionCode; - constructor(message: string, code: FieldMetadataExceptionCode) { - super(message, code); + constructor( + message: string, + code: FieldMetadataExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, + ) { + super(message, code, userFriendlyMessage); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts index 927181427..8c6cd1ea9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { i18n } from '@lingui/core'; +import { t } from '@lingui/core/macro'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import isEmpty from 'lodash.isempty'; import { APP_LOCALES } from 'twenty-shared/translations'; @@ -363,6 +364,9 @@ export class FieldMetadataService extends TypeOrmQueryService = { validator: (str: T) => boolean; message: string }; +type Validator = { + validator: (str: T) => boolean; + message: string; +}; type FieldMetadataUpdateCreateInput = CreateFieldInput | UpdateFieldInput; @@ -55,6 +59,9 @@ export class FieldMetadataEnumValidationService { throw new FieldMetadataException( message, FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + { + userFriendlyMessage: message, + }, ); } } @@ -80,23 +87,23 @@ export class FieldMetadataEnumValidationService { const validators: Validator[] = [ { validator: (label) => !isDefined(label), - message: 'Option label is required', + message: t`Option label is required`, }, { validator: exceedsDatabaseIdentifierMaximumLength, - message: `Option label "${sanitizedLabel}" exceeds 63 characters`, + message: t`Option label exceeds 63 characters`, }, { validator: beneathDatabaseIdentifierMinimumLength, - message: `Option label "${sanitizedLabel}" is beneath 1 character`, + message: t`Option label "${sanitizedLabel}" is beneath 1 character`, }, { validator: (label) => label.includes(','), - message: 'Label must not contain a comma', + message: t`Label must not contain a comma`, }, { validator: (label) => !isNonEmptyString(label) || label === ' ', - message: 'Label must not be empty', + message: t`Label must not be empty`, }, ]; @@ -109,15 +116,15 @@ export class FieldMetadataEnumValidationService { const validators: Validator[] = [ { validator: (value) => !isDefined(value), - message: 'Option value is required', + message: t`Option value is required`, }, { validator: exceedsDatabaseIdentifierMaximumLength, - message: `Option value "${sanitizedValue}" exceeds 63 characters`, + message: t`Option value exceeds 63 characters`, }, { validator: beneathDatabaseIdentifierMinimumLength, - message: `Option value "${sanitizedValue}" is beneath 1 character`, + message: t`Option value "${sanitizedValue}" is beneath 1 character`, }, { validator: (value) => !isSnakeCaseString(value), diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util.ts index 12e695520..32e3c58ad 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util.ts @@ -18,13 +18,21 @@ export const fieldMetadataGraphqlApiExceptionHandler = (error: Error) => { if (error instanceof FieldMetadataException) { switch (error.code) { case FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND: - throw new NotFoundError(error.message); + throw new NotFoundError(error.message, { + userFriendlyMessage: error.userFriendlyMessage, + }); case FieldMetadataExceptionCode.INVALID_FIELD_INPUT: - throw new UserInputError(error.message); + throw new UserInputError(error.message, { + userFriendlyMessage: error.userFriendlyMessage, + }); case FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED: - throw new ForbiddenError(error.message); + throw new ForbiddenError(error.message, { + userFriendlyMessage: error.userFriendlyMessage, + }); case FieldMetadataExceptionCode.FIELD_ALREADY_EXISTS: - throw new ConflictError(error.message); + throw new ConflictError(error.message, { + userFriendlyMessage: error.userFriendlyMessage, + }); case FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND: case FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR: case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_NOT_ENABLED: diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.exception.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.exception.ts index 4a843ac23..82cb98703 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.exception.ts @@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception'; export class ObjectMetadataException extends CustomException { declare code: ObjectMetadataExceptionCode; - constructor(message: string, code: ObjectMetadataExceptionCode) { - super(message, code); + constructor( + message: string, + code: ObjectMetadataExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, + ) { + super(message, code, userFriendlyMessage); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/__snapshots__/validate-object-metadata-input.util.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/__snapshots__/validate-object-metadata-input.util.spec.ts.snap index ef3323f37..b78c1e3b5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/__snapshots__/validate-object-metadata-input.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/__snapshots__/validate-object-metadata-input.util.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`validateObjectMetadataInputOrThrow should fail when name exceeds maximum length 1`] = `"String "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" exceeds 63 characters limit"`; +exports[`validateObjectMetadataInputOrThrow should fail when name exceeds maximum length 1`] = `"Name is too long: it exceeds the 63 characters limit."`; exports[`validateObjectMetadataInputOrThrow should fail when namePlural has invalid characters 1`] = `"String "μ" is not valid: must start with lowercase letter and contain only alphanumeric letters"`; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util.ts index f130b1ee0..9d8094e4a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util.ts @@ -20,11 +20,15 @@ export const objectMetadataGraphqlApiExceptionHandler = (error: Error) => { case ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND: throw new NotFoundError(error.message); case ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT: - throw new UserInputError(error.message); + throw new UserInputError(error.message, { + userFriendlyMessage: error.userFriendlyMessage, + }); case ObjectMetadataExceptionCode.OBJECT_MUTATION_NOT_ALLOWED: throw new ForbiddenError(error.message); case ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS: - throw new ConflictError(error.message); + throw new ConflictError(error.message, { + userFriendlyMessage: error.userFriendlyMessage, + }); case ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD: throw error; default: { diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util.ts index 6cda29ee1..261191406 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util.ts @@ -29,9 +29,14 @@ export const validateObjectMetadataInputNameOrThrow = (name: string): void => { validateMetadataNameOrThrow(name); } catch (error) { if (error instanceof InvalidMetadataException) { + const errorMessage = error.message; + throw new ObjectMetadataException( - error.message, + errorMessage, ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + { + userFriendlyMessage: errorMessage, + }, ); } @@ -62,6 +67,9 @@ const validateObjectMetadataInputLabelOrThrow = (name: string): void => { throw new ObjectMetadataException( error.message, ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + { + userFriendlyMessage: error.userFriendlyMessage, + }, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.exception.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.exception.ts index c9289e77c..fa2cf01ac 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.exception.ts @@ -23,7 +23,6 @@ export enum PermissionsExceptionCode { CANNOT_UPDATE_SELF_ROLE = 'CANNOT_UPDATE_SELF_ROLE', NO_ROLE_FOUND_FOR_USER_WORKSPACE = 'NO_ROLE_FOUND_FOR_USER_WORKSPACE', INVALID_ARG = 'INVALID_ARG_PERMISSIONS', - PERMISSIONS_V2_NOT_ENABLED = 'PERMISSIONS_V2_NOT_ENABLED', ROLE_LABEL_ALREADY_EXISTS = 'ROLE_LABEL_ALREADY_EXISTS', DEFAULT_ROLE_NOT_FOUND = 'DEFAULT_ROLE_NOT_FOUND', OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND_PERMISSIONS', @@ -53,7 +52,6 @@ export enum PermissionsExceptionMessage { UNKNOWN_REQUIRED_PERMISSION = 'Unknown required permission', CANNOT_UPDATE_SELF_ROLE = 'Cannot update self role', NO_ROLE_FOUND_FOR_USER_WORKSPACE = 'No role found for userWorkspace', - PERMISSIONS_V2_NOT_ENABLED = 'Permissions V2 is not enabled', ROLE_LABEL_ALREADY_EXISTS = 'A role with this label already exists', DEFAULT_ROLE_NOT_FOUND = 'Default role not found', OBJECT_METADATA_NOT_FOUND = 'Object metadata not found', diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util.ts index 109403943..4bf42c72b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util.ts @@ -1,3 +1,5 @@ +import { t } from '@lingui/core/macro'; + import { ForbiddenError, NotFoundError, @@ -13,11 +15,16 @@ export const permissionGraphqlApiExceptionHandler = ( ) => { switch (error.code) { case PermissionsExceptionCode.PERMISSION_DENIED: + throw new ForbiddenError(error.message, { + userFriendlyMessage: 'User does not have permission.', + }); + case PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS: + throw new ForbiddenError(error.message, { + userFriendlyMessage: t`A role with this label already exists.`, + }); case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN: case PermissionsExceptionCode.CANNOT_UPDATE_SELF_ROLE: case PermissionsExceptionCode.CANNOT_DELETE_LAST_ADMIN_USER: - case PermissionsExceptionCode.PERMISSIONS_V2_NOT_ENABLED: - case PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS: case PermissionsExceptionCode.ROLE_NOT_EDITABLE: case PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT: throw new ForbiddenError(error.message); diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/__snapshots__/validate-metadata-name.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/__snapshots__/validate-metadata-name.spec.ts.snap index f0ecb138a..e9e8065f4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/__snapshots__/validate-metadata-name.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/__snapshots__/validate-metadata-name.spec.ts.snap @@ -10,7 +10,7 @@ exports[`validateMetadataNameOrThrow throws error when string has spaces 1`] = ` exports[`validateMetadataNameOrThrow throws error when string is a reserved word 1`] = `"The name "role" is not available"`; -exports[`validateMetadataNameOrThrow throws error when string is above 63 characters 1`] = `"String "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" exceeds 63 characters limit"`; +exports[`validateMetadataNameOrThrow throws error when string is above 63 characters 1`] = `"Name is too long: it exceeds the 63 characters limit."`; exports[`validateMetadataNameOrThrow throws error when string is empty 1`] = `"Input is too short: """`; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/compute-metadata-name-from-label.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/compute-metadata-name-from-label.util.ts index c301092a9..f9708eda2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/compute-metadata-name-from-label.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/compute-metadata-name-from-label.util.ts @@ -1,3 +1,4 @@ +import { t } from '@lingui/core/macro'; import camelCase from 'lodash.camelcase'; import { slugify } from 'transliteration'; import { isDefined } from 'twenty-shared/utils'; @@ -12,6 +13,9 @@ export const computeMetadataNameFromLabel = (label: string): string => { throw new InvalidMetadataException( 'Label is required', InvalidMetadataExceptionCode.LABEL_REQUIRED, + { + userFriendlyMessage: t`Label is required`, + }, ); } @@ -31,6 +35,9 @@ export const computeMetadataNameFromLabel = (label: string): string => { throw new InvalidMetadataException( `Invalid label: "${label}"`, InvalidMetadataExceptionCode.INVALID_LABEL, + { + userFriendlyMessage: t`Invalid label: "${label}"`, + }, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception.ts b/packages/twenty-server/src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception.ts index ca3d30880..4eac9d2f2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception.ts @@ -1,8 +1,12 @@ import { CustomException } from 'src/utils/custom-exception'; export class InvalidMetadataException extends CustomException { - constructor(message: string, code: InvalidMetadataExceptionCode) { - super(message, code); + constructor( + message: string, + code: InvalidMetadataExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, + ) { + super(message, code, userFriendlyMessage); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts index 778f9f7c9..779aeac02 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts @@ -1,3 +1,5 @@ +import { t } from '@lingui/core/macro'; + import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; @@ -43,6 +45,9 @@ export const validateFieldNameAvailabilityOrThrow = ( throw new InvalidMetadataException( `Name "${name}" is not available`, InvalidMetadataExceptionCode.NOT_AVAILABLE, + { + userFriendlyMessage: t`This name is not available.`, + }, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-camel-case.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-camel-case.utils.ts index bd7f8768a..6b2a44260 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-camel-case.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-camel-case.utils.ts @@ -1,3 +1,4 @@ +import { t } from '@lingui/core/macro'; import camelCase from 'lodash.camelcase'; import { @@ -8,7 +9,7 @@ import { export const validateMetadataNameIsCamelCaseOrThrow = (name: string) => { if (name !== camelCase(name)) { throw new InvalidMetadataException( - `${name} should be in camelCase`, + t`${name} should be in camelCase`, InvalidMetadataExceptionCode.NOT_CAMEL_CASE, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts index 5a6e5f596..d75bcf2e0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts @@ -1,3 +1,5 @@ +import { t } from '@lingui/core/macro'; + import { InvalidMetadataException, InvalidMetadataExceptionCode, @@ -71,6 +73,9 @@ export const validateMetadataNameIsNotReservedKeywordOrThrow = ( throw new InvalidMetadataException( `The name "${name}" is not available`, InvalidMetadataExceptionCode.RESERVED_KEYWORD, + { + userFriendlyMessage: t`This name is not available.`, + }, ); } }; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-long.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-long.utils.ts index afd9a20e7..d3486ca9f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-long.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-long.utils.ts @@ -1,3 +1,5 @@ +import { t } from '@lingui/core/macro'; + import { InvalidMetadataException, InvalidMetadataExceptionCode, @@ -7,7 +9,7 @@ import { exceedsDatabaseIdentifierMaximumLength } from 'src/engine/metadata-modu export const validateMetadataNameIsNotTooLongOrThrow = (name: string) => { if (exceedsDatabaseIdentifierMaximumLength(name)) { throw new InvalidMetadataException( - `String "${name}" exceeds 63 characters limit`, + t`Name is too long: it exceeds the 63 characters limit.`, InvalidMetadataExceptionCode.EXCEEDS_MAX_LENGTH, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-short.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-short.utils.ts index db59fe063..5dd27edc4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-short.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-short.utils.ts @@ -1,3 +1,5 @@ +import { t } from '@lingui/core/macro'; + import { InvalidMetadataException, InvalidMetadataExceptionCode, @@ -7,7 +9,7 @@ import { beneathDatabaseIdentifierMinimumLength } from 'src/engine/metadata-modu export const validateMetadataNameIsNotTooShortOrThrow = (name: string) => { if (beneathDatabaseIdentifierMinimumLength(name)) { throw new InvalidMetadataException( - `Input is too short: "${name}"`, + t`Input is too short: "${name}"`, InvalidMetadataExceptionCode.INPUT_TOO_SHORT, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-start-with-lowercase-letter-and-contain-digits-nor-letters.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-start-with-lowercase-letter-and-contain-digits-nor-letters.utils.ts index cc6d93eab..0e60c5f4f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-start-with-lowercase-letter-and-contain-digits-nor-letters.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-start-with-lowercase-letter-and-contain-digits-nor-letters.utils.ts @@ -1,3 +1,5 @@ +import { t } from '@lingui/core/macro'; + import { InvalidMetadataException, InvalidMetadataExceptionCode, @@ -14,7 +16,7 @@ export const validateMetadataNameStartWithLowercaseLetterAndContainDigitsNorLett ) ) { throw new InvalidMetadataException( - `String "${name}" is not valid: must start with lowercase letter and contain only alphanumeric letters`, + t`String "${name}" is not valid: must start with lowercase letter and contain only alphanumeric letters`, InvalidMetadataExceptionCode.INVALID_STRING, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts index fd4f29be8..9c8e0bee9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts @@ -1,3 +1,5 @@ +import { t } from '@lingui/core/macro'; + import { ObjectMetadataException, ObjectMetadataExceptionCode, @@ -30,6 +32,9 @@ export const validatesNoOtherObjectWithSameNameExistsOrThrows = ({ throw new ObjectMetadataException( 'Object already exists', ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS, + { + userFriendlyMessage: t`Object already exists`, + }, ); } }; diff --git a/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-query-validation.exception.ts b/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-query-validation.exception.ts index 7ff0fb35d..4831630fd 100644 --- a/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-query-validation.exception.ts +++ b/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-query-validation.exception.ts @@ -1,8 +1,12 @@ import { CustomException } from 'src/utils/custom-exception'; export class WorkflowQueryValidationException extends CustomException { - constructor(message: string, code: WorkflowQueryValidationExceptionCode) { - super(message, code); + constructor( + message: string, + code: WorkflowQueryValidationExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, + ) { + super(message, code, userFriendlyMessage); } } diff --git a/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-version-step.exception.ts b/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-version-step.exception.ts index 0e414bfd1..2082010c5 100644 --- a/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-version-step.exception.ts +++ b/packages/twenty-server/src/modules/workflow/common/exceptions/workflow-version-step.exception.ts @@ -1,8 +1,12 @@ import { CustomException } from 'src/utils/custom-exception'; export class WorkflowVersionStepException extends CustomException { - constructor(message: string, code: WorkflowVersionStepExceptionCode) { - super(message, code); + constructor( + message: string, + code: WorkflowVersionStepExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, + ) { + super(message, code, userFriendlyMessage); } } export enum WorkflowVersionStepExceptionCode { diff --git a/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-statuses-not-set.ts b/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-statuses-not-set.ts index e16f026c7..a681a2e68 100644 --- a/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-statuses-not-set.ts +++ b/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-statuses-not-set.ts @@ -1,3 +1,5 @@ +import { t } from '@lingui/core/macro'; + import { WorkflowQueryValidationException, WorkflowQueryValidationExceptionCode, @@ -11,6 +13,9 @@ export const assertWorkflowStatusesNotSet = ( throw new WorkflowQueryValidationException( 'Statuses cannot be set manually.', WorkflowQueryValidationExceptionCode.FORBIDDEN, + { + userFriendlyMessage: t`Statuses cannot be set manually.`, + }, ); } }; diff --git a/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-version-has-steps.ts b/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-version-has-steps.ts index e7fe6b4c1..0c4b24b7f 100644 --- a/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-version-has-steps.ts +++ b/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-version-has-steps.ts @@ -1,3 +1,5 @@ +import { t } from '@lingui/core/macro'; + import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; import { @@ -14,6 +16,9 @@ export function assertWorkflowVersionHasSteps( throw new WorkflowTriggerException( 'Workflow version does not contain at least one step', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, + { + userFriendlyMessage: t`Workflow version does not contain at least one step`, + }, ); } } diff --git a/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-version-is-draft.util.ts b/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-version-is-draft.util.ts index 563a3d942..6b0b1917d 100644 --- a/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-version-is-draft.util.ts +++ b/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-version-is-draft.util.ts @@ -1,3 +1,5 @@ +import { t } from '@lingui/core/macro'; + import { WorkflowQueryValidationException, WorkflowQueryValidationExceptionCode, @@ -14,6 +16,9 @@ export const assertWorkflowVersionIsDraft = ( throw new WorkflowQueryValidationException( 'Workflow version is not in draft status', WorkflowQueryValidationExceptionCode.FORBIDDEN, + { + userFriendlyMessage: t`Workflow version is not in draft status`, + }, ); } }; diff --git a/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-version-trigger-is-defined.util.ts b/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-version-trigger-is-defined.util.ts index c102f9ece..3808cd107 100644 --- a/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-version-trigger-is-defined.util.ts +++ b/packages/twenty-server/src/modules/workflow/common/utils/assert-workflow-version-trigger-is-defined.util.ts @@ -1,3 +1,5 @@ +import { t } from '@lingui/core/macro'; + import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { WorkflowTriggerException, @@ -17,6 +19,9 @@ export function assertWorkflowVersionTriggerIsDefined( throw new WorkflowTriggerException( 'Workflow version does not contain trigger', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, + { + userFriendlyMessage: t`Workflow version does not contain trigger`, + }, ); } } diff --git a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-validation.workspace-service.ts b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-validation.workspace-service.ts index 9d005dd2f..c72f677e6 100644 --- a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-validation.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-validation.workspace-service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { t } from '@lingui/core/macro'; import { IsNull, Not } from 'typeorm'; import { @@ -38,6 +39,9 @@ export class WorkflowVersionValidationWorkspaceService { throw new WorkflowQueryValidationException( 'Cannot create workflow version with status other than draft', WorkflowQueryValidationExceptionCode.FORBIDDEN, + { + userFriendlyMessage: t`Cannot create workflow version with status other than draft`, + }, ); } @@ -62,6 +66,9 @@ export class WorkflowVersionValidationWorkspaceService { throw new WorkflowQueryValidationException( 'Cannot create multiple draft versions for the same workflow', WorkflowQueryValidationExceptionCode.FORBIDDEN, + { + userFriendlyMessage: t`Cannot create multiple draft versions for the same workflow`, + }, ); } } @@ -89,6 +96,9 @@ export class WorkflowVersionValidationWorkspaceService { throw new WorkflowQueryValidationException( 'Cannot update workflow version status manually', WorkflowQueryValidationExceptionCode.FORBIDDEN, + { + userFriendlyMessage: t`Cannot update workflow version status manually`, + }, ); } @@ -132,6 +142,9 @@ export class WorkflowVersionValidationWorkspaceService { throw new WorkflowQueryValidationException( 'The initial version of a workflow can not be deleted', WorkflowQueryValidationExceptionCode.FORBIDDEN, + { + userFriendlyMessage: t`The initial version of a workflow can not be deleted`, + }, ); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service.ts index a8e4f713a..dc9152c5e 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { t } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined, isValidUuid } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; @@ -312,6 +313,9 @@ export class WorkflowVersionStepWorkspaceService { throw new WorkflowVersionStepException( 'Step is not a form', WorkflowVersionStepExceptionCode.INVALID, + { + userFriendlyMessage: t`Step is not a form`, + }, ); } diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception.ts index 4a5073b92..80b8550a0 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception.ts @@ -1,8 +1,12 @@ import { CustomException } from 'src/utils/custom-exception'; export class WorkflowStepExecutorException extends CustomException { - constructor(message: string, code: WorkflowStepExecutorExceptionCode) { - super(message, code); + constructor( + message: string, + code: WorkflowStepExecutorExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, + ) { + super(message, code, userFriendlyMessage); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/get-previous-step-output.util.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/get-previous-step-output.util.ts new file mode 100644 index 000000000..46cfbaed6 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/get-previous-step-output.util.ts @@ -0,0 +1,50 @@ +import { t } from '@lingui/core/macro'; + +import { + WorkflowStepExecutorException, + WorkflowStepExecutorExceptionCode, +} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception'; +import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; + +export const getPreviousStepOutput = ( + steps: WorkflowAction[], + currentStepId: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: Record, +) => { + const previousSteps = steps.filter((step) => + step?.nextStepIds?.includes(currentStepId), + ); + + if (previousSteps.length === 0) { + throw new WorkflowStepExecutorException( + 'Filter action must have a previous step', + WorkflowStepExecutorExceptionCode.FAILED_TO_EXECUTE_STEP, + { + userFriendlyMessage: t`Filter action must have a previous step`, + }, + ); + } + + if (previousSteps.length > 1) { + throw new WorkflowStepExecutorException( + 'Filter action must have only one previous step', + WorkflowStepExecutorExceptionCode.FAILED_TO_EXECUTE_STEP, + { + userFriendlyMessage: t`Filter action must have only one previous step`, + }, + ); + } + + const previousStep = previousSteps[0]; + const previousStepOutput = context[previousStep.id]; + + if (!previousStepOutput) { + throw new WorkflowStepExecutorException( + 'Previous step output not found', + WorkflowStepExecutorExceptionCode.FAILED_TO_EXECUTE_STEP, + ); + } + + return previousStepOutput; +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception.ts index 549d25b75..dd909983b 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception.ts @@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception'; export class WorkflowTriggerException extends CustomException { declare code: WorkflowTriggerExceptionCode; - constructor(message: string, code: WorkflowTriggerExceptionCode) { - super(message, code); + constructor( + message: string, + code: WorkflowTriggerExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, + ) { + super(message, code, userFriendlyMessage); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-form-step-is-valid.util.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-form-step-is-valid.util.ts index 4d8e2f453..517390e65 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-form-step-is-valid.util.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-form-step-is-valid.util.ts @@ -1,3 +1,4 @@ +import { t } from '@lingui/core/macro'; import { isNonEmptyString } from '@sniptt/guards'; import { WorkflowFormActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type'; @@ -11,6 +12,9 @@ export function assertFormStepIsValid(settings: WorkflowFormActionSettings) { throw new WorkflowTriggerException( 'No input provided in form step', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`No input provided in form step`, + }, ); } @@ -18,6 +22,9 @@ export function assertFormStepIsValid(settings: WorkflowFormActionSettings) { throw new WorkflowTriggerException( 'Form action must have at least one field', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, + { + userFriendlyMessage: t`Form action must have at least one field`, + }, ); } @@ -29,6 +36,9 @@ export function assertFormStepIsValid(settings: WorkflowFormActionSettings) { throw new WorkflowTriggerException( 'Form action fields must have unique names', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, + { + userFriendlyMessage: t`Form action fields must have unique names`, + }, ); } @@ -41,6 +51,9 @@ export function assertFormStepIsValid(settings: WorkflowFormActionSettings) { throw new WorkflowTriggerException( 'Form action fields must have a defined label and type', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, + { + userFriendlyMessage: t`Form action fields must have a defined label and type`, + }, ); } }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts index 0a61c6c11..e1efb3b32 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts @@ -1,3 +1,5 @@ +import { t } from '@lingui/core/macro'; + import { WorkflowVersionStatus, WorkflowVersionWorkspaceEntity, @@ -33,6 +35,9 @@ export function assertVersionCanBeActivated( throw new WorkflowTriggerException( 'Cannot activate non-draft or non-last-published version', WorkflowTriggerExceptionCode.INVALID_INPUT, + { + userFriendlyMessage: t`Cannot activate non-draft or non-last-published version`, + }, ); } } @@ -42,6 +47,9 @@ function assertVersionIsValid(workflowVersion: WorkflowVersionWorkspaceEntity) { throw new WorkflowTriggerException( 'Workflow version does not contain trigger', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, + { + userFriendlyMessage: t`Workflow version does not contain trigger`, + }, ); } @@ -49,6 +57,9 @@ function assertVersionIsValid(workflowVersion: WorkflowVersionWorkspaceEntity) { throw new WorkflowTriggerException( 'No trigger type provided', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`No trigger type provided`, + }, ); } @@ -56,6 +67,9 @@ function assertVersionIsValid(workflowVersion: WorkflowVersionWorkspaceEntity) { throw new WorkflowTriggerException( 'No steps provided in workflow version', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`No steps provided in workflow version`, + }, ); } @@ -88,6 +102,9 @@ function assertTriggerSettingsAreValid( throw new WorkflowTriggerException( 'Invalid trigger type for enabling workflow trigger', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`Invalid trigger type for enabling workflow trigger`, + }, ); } } @@ -98,6 +115,9 @@ function assertCronTriggerSettingsAreValid(settings: any) { throw new WorkflowTriggerException( 'No setting type provided in cron trigger', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`No setting type provided in cron trigger`, + }, ); } switch (settings.type) { @@ -106,6 +126,9 @@ function assertCronTriggerSettingsAreValid(settings: any) { throw new WorkflowTriggerException( 'No pattern provided in CUSTOM cron trigger', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`No pattern provided in CUSTOM cron trigger`, + }, ); } @@ -117,24 +140,36 @@ function assertCronTriggerSettingsAreValid(settings: any) { throw new WorkflowTriggerException( 'No schedule provided in cron trigger', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`No schedule provided in cron trigger`, + }, ); } if (settings.schedule.day <= 0) { throw new WorkflowTriggerException( 'Invalid day value. Should be integer greater than 1', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`Invalid day value. Should be integer greater than 1`, + }, ); } if (settings.schedule.hour < 0 || settings.schedule.hour > 23) { throw new WorkflowTriggerException( 'Invalid hour value. Should be integer between 0 and 23', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`Invalid hour value. Should be integer between 0 and 23`, + }, ); } if (settings.schedule.minute < 0 || settings.schedule.minute > 59) { throw new WorkflowTriggerException( 'Invalid minute value. Should be integer between 0 and 59', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`Invalid minute value. Should be integer between 0 and 59`, + }, ); } @@ -146,12 +181,18 @@ function assertCronTriggerSettingsAreValid(settings: any) { throw new WorkflowTriggerException( 'No schedule provided in cron trigger', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`Invalid hour value. Should be integer greater than 1`, + }, ); } if (settings.schedule.hour <= 0) { throw new WorkflowTriggerException( 'Invalid hour value. Should be integer greater than 1', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`Invalid hour value. Should be integer greater than 1`, + }, ); } @@ -159,6 +200,9 @@ function assertCronTriggerSettingsAreValid(settings: any) { throw new WorkflowTriggerException( 'Invalid minute value. Should be integer between 0 and 59', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`Invalid minute value. Should be integer between 0 and 59`, + }, ); } @@ -170,6 +214,9 @@ function assertCronTriggerSettingsAreValid(settings: any) { throw new WorkflowTriggerException( 'No schedule provided in cron trigger', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`Invalid minute value. Should be integer greater than 1`, + }, ); } @@ -177,6 +224,9 @@ function assertCronTriggerSettingsAreValid(settings: any) { throw new WorkflowTriggerException( 'Invalid minute value. Should be integer greater than 1', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`Invalid minute value. Should be integer greater than 1`, + }, ); } @@ -187,6 +237,9 @@ function assertCronTriggerSettingsAreValid(settings: any) { throw new WorkflowTriggerException( 'Invalid setting type provided in cron trigger', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`Invalid setting type provided in cron trigger`, + }, ); } } @@ -197,6 +250,9 @@ function assertDatabaseEventTriggerSettingsAreValid(settings: any) { throw new WorkflowTriggerException( 'No event name provided in database event trigger', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`No event name provided in database event trigger`, + }, ); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/compute-cron-pattern-from-schedule.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/compute-cron-pattern-from-schedule.ts index 77c23d991..9bba42b98 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/compute-cron-pattern-from-schedule.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/compute-cron-pattern-from-schedule.ts @@ -1,10 +1,11 @@ +import { t } from '@lingui/core/macro'; import cron from 'cron-validate'; -import { WorkflowCronTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; import { WorkflowTriggerException, WorkflowTriggerExceptionCode, } from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception'; +import { WorkflowCronTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; const validatePattern = (pattern: string) => { const cronValidator = cron(pattern); @@ -13,6 +14,9 @@ const validatePattern = (pattern: string) => { throw new WorkflowTriggerException( `Cron pattern '${pattern}' is invalid`, WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`Cron pattern '${pattern}' is invalid`, + }, ); } }; @@ -51,6 +55,9 @@ export const computeCronPatternFromSchedule = ( throw new WorkflowTriggerException( 'Unsupported cron schedule type', WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + { + userFriendlyMessage: t`Unsupported cron schedule type`, + }, ); } }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts index 638d3929b..964f13e0a 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { t } from '@lingui/core/macro'; import { Repository } from 'typeorm'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @@ -269,6 +270,9 @@ export class WorkflowTriggerWorkspaceService { throw new WorkflowTriggerException( 'Cannot have more than one active workflow version', WorkflowTriggerExceptionCode.FORBIDDEN, + { + userFriendlyMessage: t`Cannot have more than one active workflow version`, + }, ); } @@ -294,6 +298,9 @@ export class WorkflowTriggerWorkspaceService { throw new WorkflowTriggerException( 'Cannot disable non-active workflow version', WorkflowTriggerExceptionCode.FORBIDDEN, + { + userFriendlyMessage: t`Cannot disable non-active workflow version`, + }, ); } diff --git a/packages/twenty-server/src/utils/custom-exception.ts b/packages/twenty-server/src/utils/custom-exception.ts index e12d8840c..a6f6c88cd 100644 --- a/packages/twenty-server/src/utils/custom-exception.ts +++ b/packages/twenty-server/src/utils/custom-exception.ts @@ -1,8 +1,10 @@ export class CustomException extends Error { code: string; + userFriendlyMessage?: string; - constructor(message: string, code: string) { + constructor(message: string, code: string, userFriendlyMessage?: string) { super(message); this.code = code; + this.userFriendlyMessage = userFriendlyMessage; } } diff --git a/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/restore-many-object-records-permissions.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/restore-many-object-records-permissions.integration-spec.ts index 7b387b3a5..1c23f6882 100644 --- a/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/restore-many-object-records-permissions.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/restore-many-object-records-permissions.integration-spec.ts @@ -86,11 +86,8 @@ describe('restoreManyObjectRecordsPermissions', () => { expect(response.body.data).toBeDefined(); expect(response.body.data.restorePeople).toBeDefined(); expect(response.body.data.restorePeople).toHaveLength(2); - expect(response.body.data.restorePeople).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: personId1 }), - expect.objectContaining({ id: personId2 }), - ]), - ); + expect( + response.body.data.restorePeople.map((person: any) => person.id), + ).toEqual(expect.arrayContaining([personId1, personId2])); }); }); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata-related-record.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata-related-record.integration-spec.ts.snap index 3539e5b85..c7e7f1722 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata-related-record.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata-related-record.integration-spec.ts.snap @@ -5,8 +5,10 @@ exports[`update-one-field-metadata-related-record MULTI_SELECT should delete rel { "extensions": { "code": "NOT_FOUND", + "userFriendlyMessage": "An error occurred.", }, "message": "Record not found", + "name": "NotFoundError", }, ] `; @@ -49,6 +51,7 @@ exports[`update-one-field-metadata-related-record MULTI_SELECT should throw erro "extensions": { "code": "INTERNAL_SERVER_ERROR", "exceptionEventId": "mocked-exception-id", + "userFriendlyMessage": "An error occurred.", }, "message": "Unexpected invalid view filter value for filter 20202020-e3b5-4fa7-85aa-9b1950fc7bf5", }, @@ -92,8 +95,10 @@ exports[`update-one-field-metadata-related-record SELECT should delete related v { "extensions": { "code": "NOT_FOUND", + "userFriendlyMessage": "An error occurred.", }, "message": "Record not found", + "name": "NotFoundError", }, ] `; @@ -136,6 +141,7 @@ exports[`update-one-field-metadata-related-record SELECT should throw error if v "extensions": { "code": "INTERNAL_SERVER_ERROR", "exceptionEventId": "mocked-exception-id", + "userFriendlyMessage": "An error occurred.", }, "message": "Unexpected invalid view filter value for filter 20202020-e3b5-4fa7-85aa-9b1950fc7bf5", }, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/create-one-field-metadata-enum.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/create-one-field-metadata-enum.integration-spec.ts.snap index a91cd9d1e..e10d26743 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/create-one-field-metadata-enum.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/create-one-field-metadata-enum.integration-spec.ts.snap @@ -5,8 +5,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Default value for multi-select must be an array", + "name": "UserInputError", }, ] `; @@ -16,8 +18,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Default value for multi-select must be an array", + "name": "UserInputError", }, ] `; @@ -27,8 +31,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Label must not contain a comma", }, "message": "Label must not contain a comma", + "name": "UserInputError", }, ] `; @@ -38,8 +44,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Duplicated option id", }, "message": "Duplicated option id", + "name": "UserInputError", }, ] `; @@ -49,8 +57,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Duplicated option position", }, "message": "Duplicated option position", + "name": "UserInputError", }, ] `; @@ -60,8 +70,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Duplicated option value", }, "message": "Duplicated option value", + "name": "UserInputError", }, ] `; @@ -71,8 +83,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Duplicated option value", }, "message": "Duplicated option value", + "name": "UserInputError", }, ] `; @@ -82,8 +96,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "If defined default value must contain at least one value", }, "message": "If defined default value must contain at least one value", + "name": "UserInputError", }, ] `; @@ -93,8 +109,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Options are required for enum fields", + "name": "UserInputError", }, ] `; @@ -104,8 +122,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Default value for multi-select must be an array", + "name": "UserInputError", }, ] `; @@ -115,8 +135,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -126,8 +148,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label "" is beneath 1 character", }, "message": "Option label "" is beneath 1 character", + "name": "UserInputError", }, ] `; @@ -137,8 +161,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value "" is beneath 1 character", }, "message": "Option value "" is beneath 1 character", + "name": "UserInputError", }, ] `; @@ -148,8 +174,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value should be as quoted string", }, "message": "Default value should be as quoted string", + "name": "UserInputError", }, ] `; @@ -159,8 +187,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -170,8 +200,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "Option 1 and some other things, /"", }, "message": "Value must be in UPPER_CASE and follow snake_case "Option 1 and some other things, /"", + "name": "UserInputError", }, ] `; @@ -181,8 +213,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Default value for multi-select must be an array", + "name": "UserInputError", }, ] `; @@ -192,8 +226,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Default value for multi-select must be an array", + "name": "UserInputError", }, ] `; @@ -203,8 +239,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -215,6 +253,7 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with "extensions": { "code": "INTERNAL_SERVER_ERROR", "exceptionEventId": "mocked-exception-id", + "userFriendlyMessage": "An error occurred.", }, "message": "label.includes is not a function", }, @@ -226,8 +265,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "22222"", }, "message": "Value must be in UPPER_CASE and follow snake_case "22222"", + "name": "UserInputError", }, ] `; @@ -237,8 +278,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is required", }, "message": "Option id is required", + "name": "UserInputError", }, ] `; @@ -248,8 +291,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label is required", }, "message": "Option label is required", + "name": "UserInputError", }, ] `; @@ -259,8 +304,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Options are required for enum fields", + "name": "UserInputError", }, ] `; @@ -270,8 +317,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value is required", }, "message": "Option value is required", + "name": "UserInputError", }, ] `; @@ -281,8 +330,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Default value for multi-select must be an array", + "name": "UserInputError", }, ] `; @@ -292,8 +343,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -303,8 +356,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label "" is beneath 1 character", }, "message": "Option label "" is beneath 1 character", + "name": "UserInputError", }, ] `; @@ -314,8 +369,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value "" is beneath 1 character", }, "message": "Option value "" is beneath 1 character", + "name": "UserInputError", }, ] `; @@ -325,8 +382,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Default value for multi-select must be an array", + "name": "UserInputError", }, ] `; @@ -336,8 +395,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -347,8 +408,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label exceeds 63 characters", }, - "message": "Option label "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" exceeds 63 characters", + "message": "Option label exceeds 63 characters", + "name": "UserInputError", }, ] `; @@ -358,8 +421,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value exceeds 63 characters", }, - "message": "Option value "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" exceeds 63 characters", + "message": "Option value exceeds 63 characters", + "name": "UserInputError", }, ] `; @@ -369,8 +434,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label is required", }, "message": "Option label is required", + "name": "UserInputError", }, ] `; @@ -380,8 +447,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value is required", }, "message": "Option value is required", + "name": "UserInputError", }, ] `; @@ -391,8 +460,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Options are required for enum fields", + "name": "UserInputError", }, ] `; @@ -402,8 +473,10 @@ exports[`Create field metadata MULTI_SELECT tests suite Create should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value "'UNKNOWN_OPTION'" must be one of the option values", }, "message": "Default value "'UNKNOWN_OPTION'" must be one of the option values", + "name": "UserInputError", }, ] `; @@ -413,8 +486,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with an inv { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value should be as quoted string", }, "message": "Default value should be as quoted string", + "name": "UserInputError", }, ] `; @@ -424,8 +499,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with an unk { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value "'OPTION_424242'" must be one of the option values", }, "message": "Default value "'OPTION_424242'" must be one of the option values", + "name": "UserInputError", }, ] `; @@ -435,8 +512,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with comma { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Label must not contain a comma", }, "message": "Label must not contain a comma", + "name": "UserInputError", }, ] `; @@ -446,8 +525,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with duplic { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Duplicated option id", }, "message": "Duplicated option id", + "name": "UserInputError", }, ] `; @@ -457,8 +538,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with duplic { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Duplicated option position", }, "message": "Duplicated option position", + "name": "UserInputError", }, ] `; @@ -468,8 +551,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with duplic { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Duplicated option value", }, "message": "Duplicated option value", + "name": "UserInputError", }, ] `; @@ -479,8 +564,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with duplic { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Duplicated option value", }, "message": "Duplicated option value", + "name": "UserInputError", }, ] `; @@ -490,8 +577,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with empty { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Options are required for enum fields", + "name": "UserInputError", }, ] `; @@ -501,8 +590,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with empty { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value should be as quoted string", }, "message": "Default value should be as quoted string", + "name": "UserInputError", }, ] `; @@ -512,8 +603,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with empty { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -523,8 +616,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with empty { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label "" is beneath 1 character", }, "message": "Option label "" is beneath 1 character", + "name": "UserInputError", }, ] `; @@ -534,8 +629,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with empty { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value "" is beneath 1 character", }, "message": "Option value "" is beneath 1 character", + "name": "UserInputError", }, ] `; @@ -545,8 +642,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with invali { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -556,8 +655,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with invali { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "Option 1 and some other things, /"", }, "message": "Value must be in UPPER_CASE and follow snake_case "Option 1 and some other things, /"", + "name": "UserInputError", }, ] `; @@ -567,8 +668,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with not a { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Default value for multi-select must be a stringified array", + "name": "UserInputError", }, ] `; @@ -578,8 +681,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with not a { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -590,6 +695,7 @@ exports[`Create field metadata SELECT tests suite Create should fail with not a "extensions": { "code": "INTERNAL_SERVER_ERROR", "exceptionEventId": "mocked-exception-id", + "userFriendlyMessage": "An error occurred.", }, "message": "label.includes is not a function", }, @@ -601,8 +707,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with not a { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "22222"", }, "message": "Value must be in UPPER_CASE and follow snake_case "22222"", + "name": "UserInputError", }, ] `; @@ -612,8 +720,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with null i { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is required", }, "message": "Option id is required", + "name": "UserInputError", }, ] `; @@ -623,8 +733,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with null l { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label is required", }, "message": "Option label is required", + "name": "UserInputError", }, ] `; @@ -634,8 +746,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with null o { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Options are required for enum fields", + "name": "UserInputError", }, ] `; @@ -645,8 +759,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with null v { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value is required", }, "message": "Option value is required", + "name": "UserInputError", }, ] `; @@ -656,8 +772,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with only w { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value should be as quoted string", }, "message": "Default value should be as quoted string", + "name": "UserInputError", }, ] `; @@ -667,8 +785,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with only w { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -678,8 +798,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with only w { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label "" is beneath 1 character", }, "message": "Option label "" is beneath 1 character", + "name": "UserInputError", }, ] `; @@ -689,8 +811,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with only w { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value "" is beneath 1 character", }, "message": "Option value "" is beneath 1 character", + "name": "UserInputError", }, ] `; @@ -700,8 +824,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with too lo { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value should be as quoted string", }, "message": "Default value should be as quoted string", + "name": "UserInputError", }, ] `; @@ -711,8 +837,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with too lo { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -722,8 +850,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with too lo { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label exceeds 63 characters", }, - "message": "Option label "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" exceeds 63 characters", + "message": "Option label exceeds 63 characters", + "name": "UserInputError", }, ] `; @@ -733,8 +863,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with too lo { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value exceeds 63 characters", }, - "message": "Option value "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" exceeds 63 characters", + "message": "Option value exceeds 63 characters", + "name": "UserInputError", }, ] `; @@ -744,8 +876,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with undefi { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label is required", }, "message": "Option label is required", + "name": "UserInputError", }, ] `; @@ -755,8 +889,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with undefi { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value is required", }, "message": "Option value is required", + "name": "UserInputError", }, ] `; @@ -766,8 +902,10 @@ exports[`Create field metadata SELECT tests suite Create should fail with undefi { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Options are required for enum fields", + "name": "UserInputError", }, ] -`; +`; \ No newline at end of file diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/update-one-enum-field-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/update-one-enum-field-metadata.integration-spec.ts.snap index 8b6c40329..8fc21ee98 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/update-one-enum-field-metadata.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/update-one-enum-field-metadata.integration-spec.ts.snap @@ -5,8 +5,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Default value for multi-select must be an array", + "name": "UserInputError", }, ] `; @@ -16,8 +18,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Default value for multi-select must be an array", + "name": "UserInputError", }, ] `; @@ -27,8 +31,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Label must not contain a comma", }, "message": "Label must not contain a comma", + "name": "UserInputError", }, ] `; @@ -38,8 +44,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Duplicated option id", }, "message": "Duplicated option id", + "name": "UserInputError", }, ] `; @@ -49,8 +57,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Duplicated option position", }, "message": "Duplicated option position", + "name": "UserInputError", }, ] `; @@ -60,8 +70,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Duplicated option value", }, "message": "Duplicated option value", + "name": "UserInputError", }, ] `; @@ -71,8 +83,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Duplicated option value", }, "message": "Duplicated option value", + "name": "UserInputError", }, ] `; @@ -82,8 +96,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "If defined default value must contain at least one value", }, "message": "If defined default value must contain at least one value", + "name": "UserInputError", }, ] `; @@ -93,8 +109,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Options are required for enum fields", + "name": "UserInputError", }, ] `; @@ -104,8 +122,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Default value for multi-select must be an array", + "name": "UserInputError", }, ] `; @@ -115,8 +135,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -126,8 +148,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label "" is beneath 1 character", }, "message": "Option label "" is beneath 1 character", + "name": "UserInputError", }, ] `; @@ -137,8 +161,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value "" is beneath 1 character", }, "message": "Option value "" is beneath 1 character", + "name": "UserInputError", }, ] `; @@ -148,8 +174,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value should be as quoted string", }, "message": "Default value should be as quoted string", + "name": "UserInputError", }, ] `; @@ -159,8 +187,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -170,8 +200,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "Option 1 and some other things, /"", }, "message": "Value must be in UPPER_CASE and follow snake_case "Option 1 and some other things, /"", + "name": "UserInputError", }, ] `; @@ -181,8 +213,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Default value for multi-select must be an array", + "name": "UserInputError", }, ] `; @@ -192,8 +226,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Default value for multi-select must be an array", + "name": "UserInputError", }, ] `; @@ -203,8 +239,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -215,6 +253,7 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with "extensions": { "code": "INTERNAL_SERVER_ERROR", "exceptionEventId": "mocked-exception-id", + "userFriendlyMessage": "An error occurred.", }, "message": "label.includes is not a function", }, @@ -226,8 +265,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "22222"", }, "message": "Value must be in UPPER_CASE and follow snake_case "22222"", + "name": "UserInputError", }, ] `; @@ -237,8 +278,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is required", }, "message": "Option id is required", + "name": "UserInputError", }, ] `; @@ -248,8 +291,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label is required", }, "message": "Option label is required", + "name": "UserInputError", }, ] `; @@ -259,8 +304,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value is required", }, "message": "Option value is required", + "name": "UserInputError", }, ] `; @@ -270,8 +317,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Default value for multi-select must be an array", + "name": "UserInputError", }, ] `; @@ -281,8 +330,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -292,8 +343,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label "" is beneath 1 character", }, "message": "Option label "" is beneath 1 character", + "name": "UserInputError", }, ] `; @@ -303,8 +356,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value "" is beneath 1 character", }, "message": "Option value "" is beneath 1 character", + "name": "UserInputError", }, ] `; @@ -314,8 +369,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Default value for multi-select must be an array", + "name": "UserInputError", }, ] `; @@ -325,8 +382,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -336,8 +395,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label exceeds 63 characters", }, - "message": "Option label "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" exceeds 63 characters", + "message": "Option label exceeds 63 characters", + "name": "UserInputError", }, ] `; @@ -347,8 +408,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value exceeds 63 characters", }, - "message": "Option value "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" exceeds 63 characters", + "message": "Option value exceeds 63 characters", + "name": "UserInputError", }, ] `; @@ -358,8 +421,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label is required", }, "message": "Option label is required", + "name": "UserInputError", }, ] `; @@ -369,8 +434,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value is required", }, "message": "Option value is required", + "name": "UserInputError", }, ] `; @@ -380,8 +447,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value "'OPTION_42'" must be one of the option values", }, "message": "Default value "'OPTION_42'" must be one of the option values", + "name": "UserInputError", }, ] `; @@ -391,8 +460,10 @@ exports[`Update field metadata MULTI_SELECT tests suite Update should fail with { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value "'UNKNOWN_OPTION'" must be one of the option values", }, "message": "Default value "'UNKNOWN_OPTION'" must be one of the option values", + "name": "UserInputError", }, ] `; @@ -402,8 +473,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with an inv { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value should be as quoted string", }, "message": "Default value should be as quoted string", + "name": "UserInputError", }, ] `; @@ -413,8 +486,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with an unk { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value "'OPTION_424242'" must be one of the option values", }, "message": "Default value "'OPTION_424242'" must be one of the option values", + "name": "UserInputError", }, ] `; @@ -424,8 +499,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with comma { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Label must not contain a comma", }, "message": "Label must not contain a comma", + "name": "UserInputError", }, ] `; @@ -435,8 +512,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with duplic { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Duplicated option id", }, "message": "Duplicated option id", + "name": "UserInputError", }, ] `; @@ -446,8 +525,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with duplic { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Duplicated option position", }, "message": "Duplicated option position", + "name": "UserInputError", }, ] `; @@ -457,8 +538,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with duplic { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Duplicated option value", }, "message": "Duplicated option value", + "name": "UserInputError", }, ] `; @@ -468,8 +551,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with duplic { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Duplicated option value", }, "message": "Duplicated option value", + "name": "UserInputError", }, ] `; @@ -479,8 +564,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with empty { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Options are required for enum fields", + "name": "UserInputError", }, ] `; @@ -490,8 +577,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with empty { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value should be as quoted string", }, "message": "Default value should be as quoted string", + "name": "UserInputError", }, ] `; @@ -501,8 +590,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with empty { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -512,8 +603,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with empty { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label "" is beneath 1 character", }, "message": "Option label "" is beneath 1 character", + "name": "UserInputError", }, ] `; @@ -523,8 +616,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with empty { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value "" is beneath 1 character", }, "message": "Option value "" is beneath 1 character", + "name": "UserInputError", }, ] `; @@ -534,8 +629,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with invali { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -545,8 +642,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with invali { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "Option 1 and some other things, /"", }, "message": "Value must be in UPPER_CASE and follow snake_case "Option 1 and some other things, /"", + "name": "UserInputError", }, ] `; @@ -556,8 +655,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with not a { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Default value for multi-select must be a stringified array", + "name": "UserInputError", }, ] `; @@ -567,8 +668,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with not a { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -579,6 +682,7 @@ exports[`Update field metadata SELECT tests suite Update should fail with not a "extensions": { "code": "INTERNAL_SERVER_ERROR", "exceptionEventId": "mocked-exception-id", + "userFriendlyMessage": "An error occurred.", }, "message": "label.includes is not a function", }, @@ -590,8 +694,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with not a { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "22222"", }, "message": "Value must be in UPPER_CASE and follow snake_case "22222"", + "name": "UserInputError", }, ] `; @@ -601,8 +707,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with null i { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is required", }, "message": "Option id is required", + "name": "UserInputError", }, ] `; @@ -612,8 +720,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with null l { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label is required", }, "message": "Option label is required", + "name": "UserInputError", }, ] `; @@ -623,8 +733,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with null v { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value is required", }, "message": "Option value is required", + "name": "UserInputError", }, ] `; @@ -634,8 +746,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with only w { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value should be as quoted string", }, "message": "Default value should be as quoted string", + "name": "UserInputError", }, ] `; @@ -645,8 +759,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with only w { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -656,8 +772,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with only w { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label "" is beneath 1 character", }, "message": "Option label "" is beneath 1 character", + "name": "UserInputError", }, ] `; @@ -667,8 +785,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with only w { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value "" is beneath 1 character", }, "message": "Option value "" is beneath 1 character", + "name": "UserInputError", }, ] `; @@ -678,8 +798,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with too lo { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value should be as quoted string", }, "message": "Default value should be as quoted string", + "name": "UserInputError", }, ] `; @@ -689,8 +811,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with too lo { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option id is invalid", }, "message": "Option id is invalid", + "name": "UserInputError", }, ] `; @@ -700,8 +824,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with too lo { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label exceeds 63 characters", }, - "message": "Option label "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" exceeds 63 characters", + "message": "Option label exceeds 63 characters", + "name": "UserInputError", }, ] `; @@ -711,8 +837,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with too lo { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value exceeds 63 characters", }, - "message": "Option value "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" exceeds 63 characters", + "message": "Option value exceeds 63 characters", + "name": "UserInputError", }, ] `; @@ -722,8 +850,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with undefi { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option label is required", }, "message": "Option label is required", + "name": "UserInputError", }, ] `; @@ -733,8 +863,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with undefi { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Option value is required", }, "message": "Option value is required", + "name": "UserInputError", }, ] `; @@ -744,8 +876,10 @@ exports[`Update field metadata SELECT tests suite Update should fail with unknow { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value "'OPTION_42'" must be one of the option values", }, "message": "Default value "'OPTION_42'" must be one of the option values", + "name": "UserInputError", }, ] -`; +`; \ No newline at end of file diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/phone/__snapshots__/create-one-field-metadata-phone.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/phone/__snapshots__/create-one-field-metadata-phone.integration-spec.ts.snap index d295a9a76..f3236fd1d 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/phone/__snapshots__/create-one-field-metadata-phone.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/phone/__snapshots__/create-one-field-metadata-phone.integration-spec.ts.snap @@ -5,8 +5,10 @@ exports[`Phone field metadata tests suite It should fail to create primary phone { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Provided country code and calling code are conflicting", }, "message": "Provided country code and calling code are conflicting", + "name": "UserInputError", }, ] `; @@ -16,8 +18,10 @@ exports[`Phone field metadata tests suite It should fail to create primary phone { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Provided country code and calling code are conflicting", }, "message": "Provided country code and calling code are conflicting", + "name": "UserInputError", }, ] `; @@ -27,8 +31,10 @@ exports[`Phone field metadata tests suite It should fail to create primary phone { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Provided and inferred calling code are conflicting", }, "message": "Provided and inferred calling code are conflicting", + "name": "UserInputError", }, ] `; @@ -38,8 +44,10 @@ exports[`Phone field metadata tests suite It should fail to create primary phone { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Provided and inferred calling code are conflicting", }, "message": "Provided and inferred calling code are conflicting", + "name": "UserInputError", }, ] `; @@ -49,8 +57,10 @@ exports[`Phone field metadata tests suite It should fail to create primary phone { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Provided and inferred country code are conflicting", }, "message": "Provided and inferred country code are conflicting", + "name": "UserInputError", }, ] `; @@ -60,8 +70,10 @@ exports[`Phone field metadata tests suite It should fail to create primary phone { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Provided and inferred country code are conflicting", }, "message": "Provided and inferred country code are conflicting", + "name": "UserInputError", }, ] `; @@ -71,8 +83,10 @@ exports[`Phone field metadata tests suite It should fail to create primary phone { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Invalid calling code +999", }, "message": "Invalid calling code +999", + "name": "UserInputError", }, ] `; @@ -82,8 +96,10 @@ exports[`Phone field metadata tests suite It should fail to create primary phone { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Invalid calling code +999", }, "message": "Invalid calling code +999", + "name": "UserInputError", }, ] `; @@ -93,8 +109,10 @@ exports[`Phone field metadata tests suite It should fail to create primary phone { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Invalid country code XX", }, "message": "Invalid country code XX", + "name": "UserInputError", }, ] `; @@ -104,8 +122,10 @@ exports[`Phone field metadata tests suite It should fail to create primary phone { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Invalid country code XX", }, "message": "Invalid country code XX", + "name": "UserInputError", }, ] `; @@ -115,8 +135,10 @@ exports[`Phone field metadata tests suite It should fail to create primary phone { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Provided phone number is invalid not-a-number", }, "message": "Provided phone number is invalid not-a-number", + "name": "UserInputError", }, ] `; @@ -126,8 +148,10 @@ exports[`Phone field metadata tests suite It should fail to create primary phone { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Provided phone number is invalid not-a-number", }, "message": "Provided phone number is invalid not-a-number", + "name": "UserInputError", }, ] `; @@ -137,8 +161,10 @@ exports[`Phone field metadata tests suite It should fail to create primary phone { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Provided phone number is invalid 123456789", }, "message": "Provided phone number is invalid 123456789", + "name": "UserInputError", }, ] `; @@ -148,8 +174,10 @@ exports[`Phone field metadata tests suite It should fail to create primary phone { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Provided phone number is invalid 123456789", }, "message": "Provided phone number is invalid 123456789", + "name": "UserInputError", }, ] `; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata.integration-spec.ts index 6ff472a64..ed5ee282c 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata.integration-spec.ts @@ -181,8 +181,10 @@ describe('updateOne', () => { { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "Default value "'OPTION_2'" must be one of the option values", }, "message": "Default value "'OPTION_2'" must be one of the option values", + "name": "UserInputError", }, ] `); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata.integration-spec.ts.snap index 9a0ffe538..ae731cee6 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata.integration-spec.ts.snap @@ -2,13 +2,13 @@ exports[`Object metadata creation should fail when labelPlural contains only whitespace 1`] = `"Input is too short: """`; -exports[`Object metadata creation should fail when labelPlural exceeds maximum length 1`] = `"String "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" exceeds 63 characters limit"`; +exports[`Object metadata creation should fail when labelPlural exceeds maximum length 1`] = `"Name is too long: it exceeds the 63 characters limit."`; exports[`Object metadata creation should fail when labelPlural is empty 1`] = `"Input is too short: """`; exports[`Object metadata creation should fail when labelSingular contains only whitespace 1`] = `"Input is too short: """`; -exports[`Object metadata creation should fail when labelSingular exceeds maximum length 1`] = `"String "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" exceeds 63 characters limit"`; +exports[`Object metadata creation should fail when labelSingular exceeds maximum length 1`] = `"Name is too long: it exceeds the 63 characters limit."`; exports[`Object metadata creation should fail when labelSingular is empty 1`] = `"Input is too short: """`; @@ -16,7 +16,7 @@ exports[`Object metadata creation should fail when labels are identical 1`] = `" exports[`Object metadata creation should fail when labels with whitespaces result to be identical 1`] = `"The singular and plural labels cannot be the same for an object"`; -exports[`Object metadata creation should fail when name exceeds maximum length 1`] = `"String "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" exceeds 63 characters limit"`; +exports[`Object metadata creation should fail when name exceeds maximum length 1`] = `"Name is too long: it exceeds the 63 characters limit."`; exports[`Object metadata creation should fail when namePlural has invalid characters 1`] = `"String "μ" is not valid: must start with lowercase letter and contain only alphanumeric letters"`; @@ -26,9 +26,9 @@ exports[`Object metadata creation should fail when namePlural is an empty string exports[`Object metadata creation should fail when namePlural is not camelCased 1`] = `"Not_Camel_Case should be in camelCase"`; -exports[`Object metadata creation should fail when nameSingular contains only one char and whitespaces 1`] = `" a a should be in camelCase"`; +exports[`Object metadata creation should fail when nameSingular contains only one char and whitespaces 1`] = `"a a should be in camelCase"`; -exports[`Object metadata creation should fail when nameSingular contains only whitespaces 1`] = `" should be in camelCase"`; +exports[`Object metadata creation should fail when nameSingular contains only whitespaces 1`] = `"should be in camelCase"`; exports[`Object metadata creation should fail when nameSingular has invalid characters 1`] = `"String "μ" is not valid: must start with lowercase letter and contain only alphanumeric letters"`; @@ -40,4 +40,4 @@ exports[`Object metadata creation should fail when nameSingular is not camelCase exports[`Object metadata creation should fail when names are identical 1`] = `"The singular and plural names cannot be the same for an object"`; -exports[`Object metadata creation should fail when names with whitespaces result to be identical 1`] = `" fooBar should be in camelCase"`; +exports[`Object metadata creation should fail when names with whitespaces result to be identical 1`] = `"fooBar should be in camelCase"`; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-field-metadata-relation-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-field-metadata-relation-creation.integration-spec.ts.snap index c51a112e1..2f20e7fbd 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-field-metadata-relation-creation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-field-metadata-relation-creation.integration-spec.ts.snap @@ -5,8 +5,10 @@ exports[`Field metadata relation creation should fail relation when targetFieldL { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Name "collisionfieldlabel" is not available", + "name": "UserInputError", }, ] `; @@ -16,8 +18,10 @@ exports[`Field metadata relation creation should fail relation when targetFieldL { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Invalid label: " "", + "name": "UserInputError", }, ] `; @@ -27,8 +31,10 @@ exports[`Field metadata relation creation should fail relation when targetFieldL { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, - "message": "String "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" exceeds 63 characters limit", + "message": "Name is too long: it exceeds the 63 characters limit.", + "name": "UserInputError", }, ] `; @@ -38,8 +44,10 @@ exports[`Field metadata relation creation should fail relation when targetFieldL { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "Input is too short: """, + "name": "UserInputError", }, ] `; @@ -50,6 +58,7 @@ exports[`Field metadata relation creation should fail relation when targetObject "extensions": { "code": "INTERNAL_SERVER_ERROR", "exceptionEventId": "mocked-exception-id", + "userFriendlyMessage": "An error occurred.", }, "message": "Cannot read properties of undefined (reading 'fieldsById')", }, diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap index f8932e59e..8315d047f 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap @@ -5,8 +5,10 @@ exports[`Object metadata update should fail when labelIdentifier is not a TEXT o { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "labelIdentifierFieldMetadataId validation failed: it must be a TEXT or FULL_NAME field metadata type id", + "name": "UserInputError", }, ] `; @@ -16,8 +18,10 @@ exports[`Object metadata update should fail when labelIdentifier is not a known { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "labelIdentifierFieldMetadataId validation failed: related field metadata not found", + "name": "UserInputError", }, ] `; @@ -27,8 +31,10 @@ exports[`Object metadata update should fail when labelIdentifier is not a uuid 1 { "extensions": { "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, "message": "labelIdentifierFieldMetadataId must be a UUID", + "name": "UserInputError", }, ] `;