Define server error messages to display in FE from the server (#12973)

Currently, when a server query or mutation from the front-end fails, the
error message defined server-side is displayed in a snackbar in the
front-end.
These error messages usually contain technical details that don't belong
to the user interface, such as "ObjectMetadataCollection not found" or
"invalid ENUM value for ...".

**BE**
In addition to the original error message that is still needed (for the
request response, debugging, sentry monitoring etc.), we add a
`displayedErrorMessage` that will be used in the snackbars. It's only
relevant to add it for the messages that will reach the FE (ie. not in
jobs or in rest api for instance) and if it can help the user sort out /
fix things (ie. we do add displayedErrorMessage for "Cannot create
multiple draft versions for the same workflow" or "Cannot delete
[field], please update the label identifier field first", but not
"Object metadata does not exist"), even if in practice in the FE users
should not be able to perform an action that will not work (ie should
not be able to save creation of multiple draft versions of the same
workflows).

**FE**
To ease the usage we replaced enqueueSnackBar with enqueueErrorSnackBar
and enqueueSuccessSnackBar with an api that only requires to pass on the
error.
If no displayedErrorMessage is specified then the default error message
is `An error occured.`
This commit is contained in:
Marie
2025-07-03 14:42:10 +02:00
committed by GitHub
parent 1f1318febf
commit 288f0919db
133 changed files with 1501 additions and 711 deletions

View File

@ -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<void>;
} => {
const { enqueueSnackBar } = useSnackBar();
const { enqueueErrorSnackBar } = useSnackBar();
const [page, setPage] = useState({
pageNumber: 1,
@ -62,8 +61,8 @@ export const useCustomResolver = <
} = useQuery<CustomResolverQueryResult<T>>(query, {
variables: queryVariables,
onError: (error) => {
enqueueSnackBar(error.message || `Error loading ${objectName}`, {
variant: SnackBarVariant.Error,
enqueueErrorSnackBar({
apolloError: error,
});
},
});

View File

@ -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 &&

View File

@ -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);
});

View File

@ -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);
}

View File

@ -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({});
});
});

View File

@ -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' }],
}),
});
});
});

View File

@ -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 };

View File

@ -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,
],

View File

@ -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 } : {}),
});
}

View File

@ -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<Form>) => {
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<Form>) => {
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<Form>) => {
readCaptchaToken,
form,
checkUserExistsQuery,
enqueueSnackBar,
enqueueErrorSnackBar,
setSignInUpStep,
setSignInUpMode,
]);
@ -145,9 +143,9 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
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<Form>) => {
signUpWithCredentialsInWorkspace,
workspaceInviteHash,
workspacePersonalInviteToken,
enqueueSnackBar,
enqueueErrorSnackBar,
buildSearchParamsFromUrlSyncedStates,
isOnAWorkspace,
],

View File

@ -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 });
},
});
};

View File

@ -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);
}
},

View File

@ -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.`,
});
}
};

View File

@ -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);
}

View File

@ -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);

View File

@ -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 <></>;
};

View File

@ -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(() => {

View File

@ -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,
});
},
});

View File

@ -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;
}

View File

@ -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;
}

View File

@ -10,7 +10,6 @@ import { RecordGqlOperationFindDuplicatesResult } from '@/object-record/graphql/
import { useFindDuplicateRecordsQuery } from '@/object-record/hooks/useFindDuplicatesRecordsQuery';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getFindDuplicateRecordsQueryResponseField } from '@/object-record/utils/getFindDuplicateRecordsQueryResponseField';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { logError } from '~/utils/logError';
@ -36,7 +35,7 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
objectNameSingular,
});
const { enqueueSnackBar } = useSnackBar();
const { enqueueErrorSnackBar } = useSnackBar();
const queryResponseField = getFindDuplicateRecordsQueryResponseField(
objectMetadataItem.nameSingular,
@ -59,8 +58,8 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
`useFindDuplicateRecords for "${objectMetadataItem.nameSingular}" error : ` +
error,
);
enqueueSnackBar(`Error finding duplicates:", ${error.message}`, {
variant: SnackBarVariant.Error,
enqueueErrorSnackBar({
apolloError: error,
});
},
},

View File

@ -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 {

View File

@ -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,
});
},
});

View File

@ -3,7 +3,6 @@ import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropd
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@ -65,7 +64,7 @@ export const ObjectOptionsDropdownMenuContent = () => {
};
const theme = useTheme();
const { enqueueSnackBar } = useSnackBar();
const { enqueueSuccessSnackBar } = useSnackBar();
const isDefaultView = currentView?.key === 'INDEX';
@ -171,10 +170,12 @@ export const ObjectOptionsDropdownMenuContent = () => {
onEnter={() => {
const currentUrl = window.location.href;
navigator.clipboard.writeText(currentUrl);
enqueueSnackBar('Link copied to clipboard', {
variant: SnackBarVariant.Success,
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
enqueueSuccessSnackBar({
message: t`Link copied to clipboard`,
options: {
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
},
});
}}
>
@ -183,10 +184,12 @@ export const ObjectOptionsDropdownMenuContent = () => {
onClick={() => {
const currentUrl = window.location.href;
navigator.clipboard.writeText(currentUrl);
enqueueSnackBar('Link copied to clipboard', {
variant: SnackBarVariant.Success,
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
enqueueSuccessSnackBar({
message: t`Link copied to clipboard`,
options: {
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
},
});
}}
LeftIcon={IconCopy}

View File

@ -1,7 +1,6 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useLingui } from '@lingui/react/macro';
import { IconCopy } from 'twenty-ui/display';
@ -16,7 +15,7 @@ export type LightCopyIconButtonProps = {
};
export const LightCopyIconButton = ({ copyText }: LightCopyIconButtonProps) => {
const { enqueueSnackBar } = useSnackBar();
const { enqueueSuccessSnackBar } = useSnackBar();
const theme = useTheme();
const { t } = useLingui();
@ -25,10 +24,12 @@ export const LightCopyIconButton = ({ copyText }: LightCopyIconButtonProps) => {
<LightIconButton
Icon={IconCopy}
onClick={() => {
enqueueSnackBar(t`Text copied to clipboard`, {
variant: SnackBarVariant.Success,
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
enqueueSuccessSnackBar({
message: t`Text copied to clipboard`,
options: {
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
},
});
navigator.clipboard.writeText(copyText);
}}

View File

@ -1,6 +1,5 @@
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { usePhonesFieldDisplay } from '@/object-record/record-field/meta-types/hooks/usePhonesFieldDisplay';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { PhonesDisplay } from '@/ui/field/display/components/PhonesDisplay';
import { useLingui } from '@lingui/react/macro';
@ -12,7 +11,7 @@ export const PhonesFieldDisplay = () => {
const { isFocused } = useFieldFocus();
const { enqueueSnackBar } = useSnackBar();
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const { getIcon } = useIcons();
@ -29,16 +28,20 @@ export const PhonesFieldDisplay = () => {
try {
await navigator.clipboard.writeText(phoneNumber);
enqueueSnackBar(t`Phone number copied to clipboard`, {
variant: SnackBarVariant.Success,
icon: <IconCircleCheck size={16} color="green" />,
duration: 2000,
enqueueSuccessSnackBar({
message: t`Phone number copied to clipboard`,
options: {
icon: <IconCircleCheck size={16} color="green" />,
duration: 2000,
},
});
} catch (err) {
enqueueSnackBar(t`Error copying to clipboard`, {
variant: SnackBarVariant.Error,
icon: <IconExclamationCircle size={16} color="red" />,
duration: 2000,
enqueueErrorSnackBar({
message: t`Error copying to clipboard`,
options: {
icon: <IconExclamationCircle size={16} color="red" />,
duration: 2000,
},
});
}
};

View File

@ -8,7 +8,6 @@ import { SpreadsheetImportCreateRecordsBatchSize } from '@/spreadsheet-import/co
import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog';
import { spreadsheetImportCreatedRecordsProgressState } from '@/spreadsheet-import/states/spreadsheetImportCreatedRecordsProgressState';
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useSetRecoilState } from 'recoil';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -17,7 +16,7 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
objectNameSingular: string,
) => {
const { openSpreadsheetImportDialog } = useOpenSpreadsheetImportDialog<any>();
const { enqueueSnackBar } = useSnackBar();
const { enqueueErrorSnackBar } = useSnackBar();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
@ -79,8 +78,8 @@ export const useOpenObjectRecordsSpreadsheetImportDialog = (
upsert: true,
});
} catch (error: any) {
enqueueSnackBar(error?.message || 'Something went wrong', {
variant: SnackBarVariant.Error,
enqueueErrorSnackBar({
apolloError: error,
});
}
},

View File

@ -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,
});
}
};

View File

@ -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,
});
},
});

View File

@ -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}`,
});
},
});

View File

@ -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: <IconCopy size={theme.icon.size.md} />,
enqueueSuccessSnackBar({
message: t`Copied to clipboard!`,
options: {
icon: <IconCopy size={theme.icon.size.md} />,
},
});
}, 200);

View File

@ -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`,
});
}
};

View File

@ -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}`,
});
},
});

View File

@ -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 (

View File

@ -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,
});
}
}

View File

@ -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 (
<StyledContainer>
<StyledLinkContainer>
@ -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: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
enqueueSuccessSnackBar({
message: t`API Key copied to clipboard`,
options: {
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
},
});
navigator.clipboard.writeText(apiKey);
}}

View File

@ -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,
});
});
});

View File

@ -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,
});
}
};

View File

@ -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,
});
}
};

View File

@ -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;
}

View File

@ -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,
});
},
});

View File

@ -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: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
enqueueSuccessSnackBar({
message: t`Authorized URL copied to clipboard`,
options: {
icon: <IconCopy size={theme.icon.size.md} />,
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: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
enqueueSuccessSnackBar({
message: t`Redirect Url copied to clipboard`,
options: {
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
},
});
navigator.clipboard.writeText(redirectionUrl);
}}

View File

@ -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: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
enqueueSuccessSnackBar({
message: t`ACS Url copied to clipboard`,
options: {
icon: <IconCopy size={theme.icon.size.md} />,
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: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
enqueueSuccessSnackBar({
message: t`Entity ID copied to clipboard`,
options: {
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
},
});
navigator.clipboard.writeText(entityID);
}}

View File

@ -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,
},
});
}
};

View File

@ -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,
});
}
};

View File

@ -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,
});
},
});

View File

@ -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,
},
});
}
};

View File

@ -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',
},
});
},
});

View File

@ -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,
});
}
};

View File

@ -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<File | null>(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(() => {

View File

@ -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,
},
});
});
},

View File

@ -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<SnackBarOptions, 'message' | 'id'>) => {
const enqueueSuccessSnackBar = useCallback(
({
message,
options,
}: {
message: string;
options?: Omit<SnackBarOptions, 'message' | 'id'>;
}) => {
setSnackBarQueue({
id: uuidv4(),
message,
...options,
variant: SnackBarVariant.Success,
});
},
[setSnackBarQueue],
);
return { handleSnackBarClose, enqueueSnackBar };
const enqueueInfoSnackBar = useCallback(
({
message,
options,
}: {
message: string;
options?: Omit<SnackBarOptions, 'message' | 'id'>;
}) => {
setSnackBarQueue({
id: uuidv4(),
message,
...options,
variant: SnackBarVariant.Info,
});
},
[setSnackBarQueue],
);
const enqueueWarningSnackBar = useCallback(
({
message,
options,
}: {
message: string;
options?: Omit<SnackBarOptions, 'message' | 'id'>;
}) => {
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<SnackBarOptions, 'message' | 'id'>;
}) => {
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,
};
};

View File

@ -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,
});
},
});

View File

@ -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<FormErrorMessages>({});
@ -75,9 +74,11 @@ export const WorkflowEditTriggerWebhookForm = ({
const copyToClipboard = async () => {
await navigator.clipboard.writeText(webhookUrl);
enqueueSnackBar(t`Copied to clipboard!`, {
variant: SnackBarVariant.Success,
icon: <IconCopy size={theme.icon.size.md} />,
enqueueSuccessSnackBar({
message: t`Copied to clipboard!`,
options: {
icon: <IconCopy size={theme.icon.size.md} />,
},
});
};

View File

@ -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 (
<StyledContainer data-chromatic="ignore">
@ -42,10 +41,12 @@ export const WorkspaceInviteLink = ({
accent="blue"
title={t`Copy link`}
onClick={() => {
enqueueSnackBar(t`Link copied to clipboard`, {
variant: SnackBarVariant.Success,
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
enqueueSuccessSnackBar({
message: t`Link copied to clipboard`,
options: {
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
},
});
navigator.clipboard.writeText(inviteLink);
}}

View File

@ -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<FormInput>(
@ -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,
});
},
});
}
});