From 288f0919dbf2f5215d81f742b9f5ed7a35ddb13f Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Thu, 3 Jul 2025 14:42:10 +0200 Subject: [PATCH] Define server error messages to display in FE from the server (#12973) Currently, when a server query or mutation from the front-end fails, the error message defined server-side is displayed in a snackbar in the front-end. These error messages usually contain technical details that don't belong to the user interface, such as "ObjectMetadataCollection not found" or "invalid ENUM value for ...". **BE** In addition to the original error message that is still needed (for the request response, debugging, sentry monitoring etc.), we add a `displayedErrorMessage` that will be used in the snackbars. It's only relevant to add it for the messages that will reach the FE (ie. not in jobs or in rest api for instance) and if it can help the user sort out / fix things (ie. we do add displayedErrorMessage for "Cannot create multiple draft versions for the same workflow" or "Cannot delete [field], please update the label identifier field first", but not "Object metadata does not exist"), even if in practice in the FE users should not be able to perform an action that will not work (ie should not be able to save creation of multiple draft versions of the same workflows). **FE** To ease the usage we replaced enqueueSnackBar with enqueueErrorSnackBar and enqueueSuccessSnackBar with an api that only requires to pass on the error. If no displayedErrorMessage is specified then the default error message is `An error occured.` --- .../src/hooks/useCopyToClipboard.tsx | 23 +-- .../activities/hooks/useCustomResolver.ts | 9 +- .../auth/components/VerifyEmailEffect.tsx | 34 ++-- .../hooks/__tests__/useVerifyLogin.test.ts | 11 +- .../src/modules/auth/hooks/useVerifyLogin.ts | 8 +- .../__tests__/useHandleResetPassword.test.ts | 26 ++- .../hooks/__tests__/useSSO.test.tsx | 13 +- .../useHandleResendEmailVerificationToken.ts | 27 ++-- .../hooks/useHandleResetPassword.ts | 27 ++-- .../modules/auth/sign-in-up/hooks/useSSO.ts | 9 +- .../auth/sign-in-up/hooks/useSignInUp.ts | 20 ++- .../hooks/useSignUpInNewWorkspace.ts | 10 +- .../hooks/useWorkspaceFromInviteHash.ts | 21 ++- .../SettingsBillingSubscriptionInfo.tsx | 24 ++- .../hooks/useEndSubscriptionTrialPeriod.ts | 25 ++- .../billing/hooks/useHandleCheckoutSession.ts | 13 +- .../components/ErrorMessageEffect.tsx | 13 +- .../components/PromiseRejectionEffect.tsx | 25 +-- .../hooks/useFindManyObjectMetadataItems.ts | 7 +- .../hooks/useBatchCreateManyRecords.ts | 12 +- .../object-record/hooks/useDeleteOneRecord.ts | 3 +- .../hooks/useFindDuplicateRecords.ts | 7 +- .../hooks/useHandleFindManyRecordsError.ts | 11 +- .../hooks/useObjectRecordSearchRecords.ts | 13 +- .../ObjectOptionsDropdownMenuContent.tsx | 23 +-- .../components/LightCopyIconButton.tsx | 13 +- .../display/components/PhonesFieldDisplay.tsx | 23 +-- ...penObjectRecordsSpreadsheetImportDialog.ts | 7 +- .../accounts/hooks/useImapConnectionForm.ts | 25 ++- .../components/SettingsAdminGeneral.tsx | 7 +- .../SettingsAdminWorkspaceContent.tsx | 15 +- .../SettingsAdminConfigCopyableText.tsx | 11 +- .../hooks/useConfigVariableActions.ts | 19 ++- .../components/WorkerMetricsGraph.tsx | 7 +- ...SettingsUpdateDataModelObjectAboutForm.tsx | 19 ++- ...SettingsDataModelObjectIdentifiersForm.tsx | 12 +- .../developers/components/ApiKeyInput.tsx | 15 +- .../hooks/__tests__/useWebhookForm.test.tsx | 52 +++--- .../developers/hooks/useWebhookForm.ts | 48 +++--- ...tegrationEditDatabaseConnectionContent.tsx | 10 +- .../roles/role/components/SettingsRole.tsx | 7 +- ...SettingsSSOIdentitiesProvidersListCard.tsx | 10 +- .../components/SSO/SettingsSSOOIDCForm.tsx | 27 ++-- .../components/SSO/SettingsSSOSAMLForm.tsx | 43 ++--- .../SettingsSecuritySSORowDropdownMenu.tsx | 19 ++- ...ttingsSecurityAuthProvidersOptionsList.tsx | 21 ++- .../SettingsApprovedAccessDomainsListCard.tsx | 8 +- ...ityApprovedAccessDomainRowDropdownMenu.tsx | 12 +- ...tyApprovedAccessDomainValidationEffect.tsx | 20 ++- .../components/ToggleImpersonate.tsx | 8 +- .../components/SpreadsheetImportStepper.tsx | 9 +- .../UploadStep/components/DropZone.tsx | 11 +- .../snack-bar-manager/hooks/useSnackBar.ts | 85 +++++++++- ...ultiWorkspaceDropdownDefaultComponents.tsx | 10 +- .../WorkflowEditTriggerWebhookForm.tsx | 13 +- .../components/WorkspaceInviteLink.tsx | 15 +- .../components/WorkspaceInviteTeam.tsx | 19 +-- .../src/pages/auth/PasswordReset.tsx | 27 ++-- .../src/pages/onboarding/CreateProfile.tsx | 10 +- .../src/pages/onboarding/CreateWorkspace.tsx | 11 +- .../src/pages/onboarding/InviteTeam.tsx | 23 +-- .../settings/SettingsWorkspaceMembers.tsx | 26 +-- .../settings/data-model/SettingsNewObject.tsx | 14 +- .../data-model/SettingsObjectFieldEdit.tsx | 8 +- .../SettingsObjectNewFieldConfigure.tsx | 14 +- .../SettingsDevelopersApiKeyDetail.tsx | 11 +- ...ttingsIntegrationNewDatabaseConnection.tsx | 12 +- .../SettingsSecurityApprovedAccessDomain.tsx | 16 +- .../SettingsSecuritySSOIdentifyProvider.tsx | 10 +- .../SettingsServerlessFunctionDetail.tsx | 31 ++-- .../workspace/SettingsCustomDomainRecords.tsx | 17 +- .../settings/workspace/SettingsDomain.tsx | 31 ++-- ...et-error-message-from-apollo-error.util.ts | 14 ++ ...approved-access-domain-exception-filter.ts | 6 +- .../approved-access-domain.exception.ts | 8 +- .../approved-access-domain.service.ts | 12 +- .../core-modules/auth/auth.exception.ts | 8 +- .../auth-graphql-api-exception.filter.ts | 25 ++- .../auth/services/auth.service.ts | 3 + .../auth/services/sign-in-up.service.ts | 22 +++ ...mail-verification-exception-filter.util.ts | 21 ++- .../hooks/use-graphql-error-handler.hook.ts | 25 ++- .../generate-graphql-error-from-error.util.ts | 14 +- .../graphql/utils/graphql-errors.util.ts | 10 +- .../record-transformer.exception.ts | 8 +- ...rmer-graphql-api-exception-handler.util.ts | 4 +- .../utils/transform-phones-value.util.ts | 7 + ...ow-trigger-graphql-api-exception.filter.ts | 12 +- .../field-metadata.exception.ts | 8 +- .../field-metadata/field-metadata.service.ts | 7 + .../field-metadata-enum-validation.service.ts | 25 +-- ...data-graphql-api-exception-handler.util.ts | 16 +- .../object-metadata.exception.ts | 8 +- ...te-object-metadata-input.util.spec.ts.snap | 2 +- ...data-graphql-api-exception-handler.util.ts | 8 +- .../validate-object-metadata-input.util.ts | 10 +- .../permissions/permissions.exception.ts | 2 - ...sion-graphql-api-exception-handler.util.ts | 11 +- .../validate-metadata-name.spec.ts.snap | 2 +- .../compute-metadata-name-from-label.util.ts | 7 + .../exceptions/invalid-metadata.exception.ts | 8 +- .../validate-field-name-availability.utils.ts | 5 + ...idate-metadata-name-is-camel-case.utils.ts | 3 +- ...e-metadata-name-is-not-reserved-keyword.ts | 5 + ...ate-metadata-name-is-not-too-long.utils.ts | 4 +- ...te-metadata-name-is-not-too-short.utils.ts | 4 +- ...er-and-contain-digits-nor-letters.utils.ts | 4 +- ...ect-with-same-name-exists-or-throw.util.ts | 5 + .../workflow-query-validation.exception.ts | 8 +- .../workflow-version-step.exception.ts | 8 +- .../utils/assert-workflow-statuses-not-set.ts | 5 + .../assert-workflow-version-has-steps.ts | 5 + .../assert-workflow-version-is-draft.util.ts | 5 + ...orkflow-version-trigger-is-defined.util.ts | 5 + ...ow-version-validation.workspace-service.ts | 13 ++ ...workflow-version-step.workspace-service.ts | 4 + .../workflow-step-executor.exception.ts | 8 +- .../utils/get-previous-step-output.util.ts | 50 ++++++ .../exceptions/workflow-trigger.exception.ts | 8 +- .../utils/assert-form-step-is-valid.util.ts | 13 ++ .../assert-version-can-be-activated.util.ts | 56 +++++++ .../compute-cron-pattern-from-schedule.ts | 9 +- .../workflow-trigger.workspace-service.ts | 7 + .../src/utils/custom-exception.ts | 4 +- ...ct-records-permissions.integration-spec.ts | 9 +- ...ta-related-record.integration-spec.ts.snap | 6 + ...eld-metadata-enum.integration-spec.ts.snap | 148 +++++++++++++++++- ...um-field-metadata.integration-spec.ts.snap | 144 ++++++++++++++++- ...ld-metadata-phone.integration-spec.ts.snap | 28 ++++ ...ate-one-field-metadata.integration-spec.ts | 2 + ...e-object-metadata.integration-spec.ts.snap | 12 +- ...relation-creation.integration-spec.ts.snap | 11 +- ...e-object-metadata.integration-spec.ts.snap | 6 + 133 files changed, 1501 insertions(+), 711 deletions(-) create mode 100644 packages/twenty-front/src/utils/get-error-message-from-apollo-error.util.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/get-previous-step-output.util.ts 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", }, ] `;