Upgrade Apollo Client to v4 and refactor error handling (#18584)

## Summary
This PR upgrades Apollo Client from v3.10.0 to v4 and refactors error
handling patterns across the codebase to use a new centralized
`useSnackBarOnQueryError` hook.

## Key Changes

- **Dependency Update**: Upgraded `@apollo/client` from `^3.10.0` to
`^3.11.0` in root package.json
- **New Hook**: Added `useSnackBarOnQueryError` hook for centralized
Apollo query error handling with snack bar notifications
- **Error Handling Refactor**: Updated 100+ files to use the new error
handling pattern:
  - Removed direct `ApolloError` imports where no longer needed
- Replaced manual error handling logic with `useSnackBarOnQueryError`
hook
- Simplified error handling in hooks and components across multiple
modules
- **GraphQL Codegen**: Updated codegen configuration files to work with
Apollo Client v3.11.0
- **Type Definitions**: Added TypeScript declaration file for
`apollo-upload-client` module
- **Test Updates**: Updated test files to reflect new error handling
patterns

## Notable Implementation Details

- The new `useSnackBarOnQueryError` hook provides a consistent way to
handle Apollo query errors with automatic snack bar notifications
- Changes span across multiple feature areas: auth, object records,
settings, workflows, billing, and more
- All changes maintain backward compatibility while improving code
maintainability and reducing duplication
- Jest configuration updated to work with the new Apollo Client version

https://claude.ai/code/session_019WGZ6Rd7sEHuBg9sTrXRqJ

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Félix Malfait
2026-03-13 14:59:46 +01:00
committed by GitHub
parent 172bbd01bc
commit b470cb21a1
386 changed files with 3482 additions and 13593 deletions
@@ -8,15 +8,16 @@ 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 { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { type ApolloError } from '@apollo/client';
import { useSnackBarOnQueryError } from '@/apollo/hooks/useSnackBarOnQueryError';
import { styled } from '@linaria/react';
import { useEffect } from 'react';
import { useLingui } from '@lingui/react/macro';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { getSettingsPath } from 'twenty-shared/utils';
import { IconKey } from 'twenty-ui/display';
import { useGetSsoIdentityProvidersQuery } from '~/generated-metadata/graphql';
import { useQuery } from '@apollo/client/react';
import { GetSsoIdentityProvidersDocument } from '~/generated-metadata/graphql';
const StyledLinkContainer = styled.div<{ isDisabled: boolean }>`
pointer-events: ${({ isDisabled }) => (isDisabled ? 'none' : 'auto')};
@@ -27,8 +28,6 @@ const StyledLinkContainer = styled.div<{ isDisabled: boolean }>`
`;
export const SettingsSSOIdentitiesProvidersListCard = () => {
const { enqueueErrorSnackBar } = useSnackBar();
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const { t } = useLingui();
@@ -37,19 +36,23 @@ export const SettingsSSOIdentitiesProvidersListCard = () => {
SSOIdentitiesProvidersState,
);
const { loading } = useGetSsoIdentityProvidersQuery({
const {
loading,
data: ssoData,
error: ssoError,
} = useQuery(GetSsoIdentityProvidersDocument, {
fetchPolicy: 'network-only',
skip: currentWorkspace?.hasValidEnterpriseKey === false,
onCompleted: (data) => {
setSSOIdentitiesProviders(data?.getSSOIdentityProviders ?? []);
},
onError: (error: ApolloError) => {
enqueueErrorSnackBar({
apolloError: error,
});
},
});
useEffect(() => {
if (ssoData) {
setSSOIdentitiesProviders(ssoData?.getSSOIdentityProviders ?? []);
}
}, [ssoData, setSSOIdentitiesProviders]);
useSnackBarOnQueryError(ssoError);
return loading || !SSOIdentitiesProviders.length ? (
<StyledLinkContainer
isDisabled={currentWorkspace?.hasValidEnterpriseKey !== true}
@@ -37,7 +37,7 @@ export const SettingsSecuritySSORowDropdownMenu = ({
const result = await deleteSSOIdentityProvider({
identityProviderId,
});
if (isDefined(result.errors)) {
if (isDefined(result.error)) {
enqueueErrorSnackBar({
message: t`Error deleting SSO Identity Provider`,
options: {
@@ -57,7 +57,7 @@ export const SettingsSecuritySSORowDropdownMenu = ({
? SsoIdentityProviderStatus.Inactive
: SsoIdentityProviderStatus.Active,
});
if (isDefined(result.errors)) {
if (isDefined(result.error)) {
enqueueErrorSnackBar({
message: t`Error editing SSO Identity Provider`,
options: {
@@ -2,16 +2,17 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { capitalize } from 'twenty-shared/utils';
import { IconGoogle, IconMicrosoft, IconPassword } from 'twenty-ui/display';
import { Card } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useMutation } from '@apollo/client/react';
import {
type AuthProviders,
useUpdateWorkspaceMutation,
UpdateWorkspaceDocument,
} from '~/generated-metadata/graphql';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
@@ -32,7 +33,7 @@ export const SettingsSecurityAuthBypassOptionsList = () => {
currentWorkspaceState,
);
const [updateWorkspace] = useUpdateWorkspaceMutation();
const [updateWorkspace] = useMutation(UpdateWorkspaceDocument);
const isValidAuthProvider = (
key: string,
@@ -71,7 +72,7 @@ export const SettingsSecurityAuthBypassOptionsList = () => {
[key]: currentWorkspace[key],
});
enqueueErrorSnackBar({
apolloError: err instanceof ApolloError ? err : undefined,
apolloError: CombinedGraphQLErrors.is(err) ? err : undefined,
});
});
};
@@ -3,7 +3,7 @@ import { authProvidersState } from '@/client-config/states/authProvidersState';
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
@@ -17,9 +17,10 @@ import {
} from 'twenty-ui/display';
import { Card } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useMutation } from '@apollo/client/react';
import {
type AuthProviders,
useUpdateWorkspaceMutation,
UpdateWorkspaceDocument,
} from '~/generated-metadata/graphql';
import { Toggle2FA } from './Toggle2FA';
@@ -42,7 +43,7 @@ export const SettingsSecurityAuthProvidersOptionsList = () => {
currentWorkspaceState,
);
const [updateWorkspace] = useUpdateWorkspaceMutation();
const [updateWorkspace] = useMutation(UpdateWorkspaceDocument);
const isValidAuthProvider = (
key: string,
@@ -99,7 +100,7 @@ export const SettingsSecurityAuthProvidersOptionsList = () => {
[key]: !currentWorkspace[key],
});
enqueueErrorSnackBar({
apolloError: err instanceof ApolloError ? err : undefined,
apolloError: CombinedGraphQLErrors.is(err) ? err : undefined,
});
});
};
@@ -122,7 +123,7 @@ export const SettingsSecurityAuthProvidersOptionsList = () => {
});
} catch (err: any) {
enqueueErrorSnackBar({
apolloError: err instanceof ApolloError ? err : undefined,
apolloError: CombinedGraphQLErrors.is(err) ? err : undefined,
});
}
};
@@ -5,7 +5,7 @@ import { SelectControl } from '@/ui/input/components/SelectControl';
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 { ApolloError } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
@@ -20,7 +20,8 @@ import {
import { type SelectOption } from 'twenty-ui/input';
import { MenuItemMultiSelect } from 'twenty-ui/navigation';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useUpdateWorkspaceMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { UpdateWorkspaceDocument } from '~/generated-metadata/graphql';
const StyledDropdownContainer = styled.div`
display: flex;
@@ -41,7 +42,7 @@ export const SettingsSecurityEditableProfileFields = () => {
const [currentWorkspace, setCurrentWorkspace] = useAtomState(
currentWorkspaceState,
);
const [updateWorkspace] = useUpdateWorkspaceMutation();
const [updateWorkspace] = useMutation(UpdateWorkspaceDocument);
const profileFieldOptions: ProfileFieldOption[] = [
{ value: 'email', label: t`Email`, Icon: IconMail },
@@ -108,7 +109,7 @@ export const SettingsSecurityEditableProfileFields = () => {
prev ? { ...prev, editableProfileFields: previousFields } : prev,
);
enqueueErrorSnackBar({
apolloError: err instanceof ApolloError ? err : undefined,
apolloError: CombinedGraphQLErrors.is(err) ? err : undefined,
});
});
};
@@ -2,10 +2,11 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { IconLifebuoy } from 'twenty-ui/display';
import { useUpdateWorkspaceMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { UpdateWorkspaceDocument } from '~/generated-metadata/graphql';
export const Toggle2FA = () => {
const { enqueueErrorSnackBar } = useSnackBar();
@@ -13,7 +14,7 @@ export const Toggle2FA = () => {
currentWorkspaceState,
);
const [updateWorkspace] = useUpdateWorkspaceMutation();
const [updateWorkspace] = useMutation(UpdateWorkspaceDocument);
const handleChange = async () => {
if (!currentWorkspace?.id) {
@@ -43,7 +44,7 @@ export const Toggle2FA = () => {
isTwoFactorAuthenticationEnforced: !newEnforceValue,
});
enqueueErrorSnackBar({
apolloError: err instanceof ApolloError ? err : undefined,
apolloError: CombinedGraphQLErrors.is(err) ? err : undefined,
message: err?.message,
});
}
@@ -7,15 +7,16 @@ 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 { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { useSnackBarOnQueryError } from '@/apollo/hooks/useSnackBarOnQueryError';
import { styled } from '@linaria/react';
import { useEffect } from 'react';
import { useLingui } from '@lingui/react/macro';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { getSettingsPath } from 'twenty-shared/utils';
import { IconAt, IconMailCog, Status } from 'twenty-ui/display';
import { useGetApprovedAccessDomainsQuery } from '~/generated-metadata/graphql';
import { useQuery } from '@apollo/client/react';
import { GetApprovedAccessDomainsDocument } from '~/generated-metadata/graphql';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
@@ -26,7 +27,6 @@ const StyledLinkContainer = styled.div`
`;
export const SettingsApprovedAccessDomainsListCard = () => {
const { enqueueErrorSnackBar } = useSnackBar();
const navigate = useNavigate();
const { t } = useLingui();
const { localeCatalog } = useAtomStateValue(dateLocaleState);
@@ -35,18 +35,22 @@ export const SettingsApprovedAccessDomainsListCard = () => {
approvedAccessDomainsState,
);
const { loading } = useGetApprovedAccessDomainsQuery({
const {
loading,
data: domainsData,
error: domainsError,
} = useQuery(GetApprovedAccessDomainsDocument, {
fetchPolicy: 'network-only',
onCompleted: (data) => {
setApprovedAccessDomains(data?.getApprovedAccessDomains ?? []);
},
onError: (error: Error) => {
enqueueErrorSnackBar({
apolloError: error instanceof ApolloError ? error : undefined,
});
},
});
useEffect(() => {
if (domainsData) {
setApprovedAccessDomains(domainsData?.getApprovedAccessDomains ?? []);
}
}, [domainsData, setApprovedAccessDomains]);
useSnackBarOnQueryError(domainsError);
const getItemDescription = (createdAt: string) => {
const beautifyPastDateRelative = beautifyPastDateRelativeToNow(
createdAt,
@@ -10,9 +10,10 @@ import { isDefined } from 'twenty-shared/utils';
import { IconDotsVertical, IconTrash } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { useMutation } from '@apollo/client/react';
import {
type ApprovedAccessDomain,
useDeleteApprovedAccessDomainMutation,
DeleteApprovedAccessDomainDocument,
} from '~/generated-metadata/graphql';
type SettingsSecurityApprovedAccessDomainRowDropdownMenuProps = {
@@ -30,7 +31,9 @@ export const SettingsSecurityApprovedAccessDomainRowDropdownMenu = ({
const { closeDropdown } = useCloseDropdown();
const [deleteApprovedAccessDomain] = useDeleteApprovedAccessDomainMutation();
const [deleteApprovedAccessDomain] = useMutation(
DeleteApprovedAccessDomainDocument,
);
const handleDeleteApprovedAccessDomain = async () => {
const result = await deleteApprovedAccessDomain({
@@ -47,7 +50,7 @@ export const SettingsSecurityApprovedAccessDomainRowDropdownMenu = ({
});
},
});
if (isDefined(result.errors)) {
if (isDefined(result.error)) {
enqueueErrorSnackBar({
message: t`Could not delete approved access domain`,
options: {
@@ -5,11 +5,13 @@ import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { isDefined } from 'twenty-shared/utils';
import { useValidateApprovedAccessDomainMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { ValidateApprovedAccessDomainDocument } from '~/generated-metadata/graphql';
export const SettingsSecurityApprovedAccessDomainValidationEffect = () => {
const [validateApprovedAccessDomainMutation] =
useValidateApprovedAccessDomainMutation();
const [validateApprovedAccessDomainMutation] = useMutation(
ValidateApprovedAccessDomainDocument,
);
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [searchParams] = useSearchParams();
const approvedAccessDomainId = searchParams.get('wtdId');
@@ -3,15 +3,26 @@
import { renderHook } from '@testing-library/react';
import { useCreateSSOIdentityProvider } from '@/settings/security/hooks/useCreateSSOIdentityProvider';
import {
CreateOidcIdentityProviderDocument,
CreateSamlIdentityProviderDocument,
} from '~/generated-metadata/graphql';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
const mutationOIDCCallSpy = jest.fn();
const mutationSAMLCallSpy = jest.fn();
jest.mock('~/generated-metadata/graphql', () => ({
...jest.requireActual('~/generated-metadata/graphql'),
useCreateOidcIdentityProviderMutation: () => [mutationOIDCCallSpy],
useCreateSamlIdentityProviderMutation: () => [mutationSAMLCallSpy],
jest.mock('@apollo/client/react', () => ({
...jest.requireActual('@apollo/client/react'),
useMutation: (document: unknown) => {
if (document === CreateOidcIdentityProviderDocument) {
return [mutationOIDCCallSpy];
}
if (document === CreateSamlIdentityProviderDocument) {
return [mutationSAMLCallSpy];
}
return [jest.fn()];
},
}));
const Wrapper = getJestMetadataAndApolloMocksWrapper({
@@ -7,9 +7,9 @@ import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMeta
const mutationDeleteSSOIDPCallSpy = jest.fn();
jest.mock('~/generated-metadata/graphql', () => ({
...jest.requireActual('~/generated-metadata/graphql'),
useDeleteSsoIdentityProviderMutation: () => [mutationDeleteSSOIDPCallSpy],
jest.mock('@apollo/client/react', () => ({
...jest.requireActual('@apollo/client/react'),
useMutation: () => [mutationDeleteSSOIDPCallSpy],
}));
const Wrapper = getJestMetadataAndApolloMocksWrapper({
@@ -8,13 +8,10 @@ import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMeta
const mutationEditSSOIDPCallSpy = jest.fn();
jest.mock('~/generated-metadata/graphql', () => {
const actual = jest.requireActual('~/generated-metadata/graphql');
return {
...actual,
useEditSsoIdentityProviderMutation: () => [mutationEditSSOIDPCallSpy],
};
});
jest.mock('@apollo/client/react', () => ({
...jest.requireActual('@apollo/client/react'),
useMutation: () => [mutationEditSSOIDPCallSpy],
}));
const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
@@ -2,18 +2,21 @@
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { useMutation } from '@apollo/client/react';
import {
type CreateOidcIdentityProviderMutationVariables,
type CreateSamlIdentityProviderMutationVariables,
useCreateOidcIdentityProviderMutation,
useCreateSamlIdentityProviderMutation,
CreateOidcIdentityProviderDocument,
CreateSamlIdentityProviderDocument,
} from '~/generated-metadata/graphql';
export const useCreateSSOIdentityProvider = () => {
const [createOidcIdentityProviderMutation] =
useCreateOidcIdentityProviderMutation();
const [createSamlIdentityProviderMutation] =
useCreateSamlIdentityProviderMutation();
const [createOidcIdentityProviderMutation] = useMutation(
CreateOidcIdentityProviderDocument,
);
const [createSamlIdentityProviderMutation] = useMutation(
CreateSamlIdentityProviderDocument,
);
const setSSOIdentitiesProviders = useSetAtomState(
SSOIdentitiesProvidersState,
@@ -2,14 +2,16 @@
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { useMutation } from '@apollo/client/react';
import {
type DeleteSsoIdentityProviderMutationVariables,
useDeleteSsoIdentityProviderMutation,
DeleteSsoIdentityProviderDocument,
} from '~/generated-metadata/graphql';
export const useDeleteSSOIdentityProvider = () => {
const [deleteSsoIdentityProviderMutation] =
useDeleteSsoIdentityProviderMutation();
const [deleteSsoIdentityProviderMutation] = useMutation(
DeleteSsoIdentityProviderDocument,
);
const setSSOIdentitiesProviders = useSetAtomState(
SSOIdentitiesProvidersState,
@@ -2,14 +2,16 @@
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { useMutation } from '@apollo/client/react';
import {
type EditSsoIdentityProviderMutationVariables,
useEditSsoIdentityProviderMutation,
EditSsoIdentityProviderDocument,
} from '~/generated-metadata/graphql';
export const useUpdateSSOIdentityProvider = () => {
const [editSsoIdentityProviderMutation] =
useEditSsoIdentityProviderMutation();
const [editSsoIdentityProviderMutation] = useMutation(
EditSsoIdentityProviderDocument,
);
const setSSOIdentitiesProviders = useSetAtomState(
SSOIdentitiesProvidersState,