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
@@ -12,10 +12,11 @@ import { H2Title, IconArchive, IconPlug, IconRobot } from 'twenty-ui/display';
import { SearchInput } from 'twenty-ui/input';
import { Card, Section } from 'twenty-ui/layout';
import { MenuItemToggle } from 'twenty-ui/navigation';
import { useMutation, useQuery } from '@apollo/client/react';
import {
useCreateDatabaseConfigVariableMutation,
useGetAdminAiModelsQuery,
useSetAdminAiModelEnabledMutation,
CreateDatabaseConfigVariableDocument,
GetAdminAiModelsDocument,
SetAdminAiModelEnabledDocument,
} from '~/generated-metadata/graphql';
import { getModelIcon } from '~/pages/settings/ai/utils/getModelIcon';
import { getModelProviderLabel } from '~/pages/settings/ai/utils/getModelProviderLabel';
@@ -27,9 +28,11 @@ export const SettingsAdminAI = () => {
const [showDeprecated, setShowDeprecated] = useState(false);
const { refetch: refetchClientConfig } = useClientConfig();
const { data } = useGetAdminAiModelsQuery();
const [createConfigVariable] = useCreateDatabaseConfigVariableMutation();
const [setModelEnabled] = useSetAdminAiModelEnabledMutation();
const { data } = useQuery(GetAdminAiModelsDocument);
const [createConfigVariable] = useMutation(
CreateDatabaseConfigVariableDocument,
);
const [setModelEnabled] = useMutation(SetAdminAiModelEnabledDocument);
const autoEnableNewModels =
data?.getAdminAiModels?.autoEnableNewModels ?? true;
@@ -1,10 +1,9 @@
import { FIND_ALL_APPLICATION_REGISTRATIONS } from '@/settings/admin-panel/apps/graphql/queries/findAllApplicationRegistrations';
import { Table } from '@/ui/layout/table/components/Table';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useQuery } from '@apollo/client';
import { useQuery } from '@apollo/client/react';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
@@ -15,7 +14,10 @@ import { SearchInput } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type ApplicationRegistrationFragmentFragment } from '~/generated-metadata/graphql';
import {
type ApplicationRegistrationFragmentFragment,
FindAllApplicationRegistrationsDocument,
} from '~/generated-metadata/graphql';
const StyledTableContainer = styled.div`
margin-top: ${themeCssVariables.spacing[3]};
@@ -30,7 +32,7 @@ const TABLE_GRID = '1fr 1fr 100px 80px';
export const SettingsAdminApps = () => {
const [searchQuery, setSearchQuery] = useState('');
const { data } = useQuery(FIND_ALL_APPLICATION_REGISTRATIONS);
const { data } = useQuery(FindAllApplicationRegistrationsDocument);
const registrations: ApplicationRegistrationFragmentFragment[] =
data?.findAllApplicationRegistrations ?? [];
@@ -11,7 +11,8 @@ import { isNonEmptyString } from '@sniptt/guards';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useState } from 'react';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useUserLookupAdminPanelMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { UserLookupAdminPanelDocument } from '~/generated-metadata/graphql';
import { currentUserState } from '@/auth/states/currentUserState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -52,7 +53,7 @@ export const SettingsAdminGeneral = () => {
);
const [isUserLookupLoading, setIsUserLookupLoading] = useState(false);
const [userLookup] = useUserLookupAdminPanelMutation();
const [userLookup] = useMutation(UserLookupAdminPanelDocument);
const currentUser = useAtomStateValue(currentUserState);
@@ -2,10 +2,11 @@ import { SettingsAdminTableCard } from '@/settings/admin-panel/components/Settin
import { SettingsAdminVersionDisplay } from '@/settings/admin-panel/components/SettingsAdminVersionDisplay';
import { t } from '@lingui/core/macro';
import { IconCircleDot, IconStatusChange } from 'twenty-ui/display';
import { useGetVersionInfoQuery } from '~/generated-metadata/graphql';
import { useQuery } from '@apollo/client/react';
import { GetVersionInfoDocument } from '~/generated-metadata/graphql';
export const SettingsAdminVersionContainer = () => {
const { data, loading } = useGetVersionInfoQuery();
const { data, loading } = useQuery(GetVersionInfoDocument);
const { currentVersion, latestVersion } = data?.versionInfo ?? {};
const versionItems = [
@@ -35,10 +35,11 @@ import { Button, Toggle } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useMutation } from '@apollo/client/react';
import {
type FeatureFlagKey,
useImpersonateMutation,
useUpdateWorkspaceFeatureFlagMutation,
ImpersonateDocument,
UpdateWorkspaceFeatureFlagDocument,
} from '~/generated-metadata/graphql';
type SettingsAdminWorkspaceContentProps = {
@@ -64,11 +65,11 @@ export const SettingsAdminWorkspaceContent = ({
const [currentUser] = useAtomState(currentUserState);
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const [updateFeatureFlag] = useUpdateWorkspaceFeatureFlagMutation();
const [updateFeatureFlag] = useMutation(UpdateWorkspaceFeatureFlagDocument);
const [isImpersonateLoading, setIsImpersonationLoading] = useState(false);
const { executeImpersonationAuth } = useImpersonationAuth();
const { executeImpersonationRedirect } = useImpersonationRedirect();
const [impersonate] = useImpersonateMutation();
const [impersonate] = useMutation(ImpersonateDocument);
const { updateFeatureFlagState } = useFeatureFlagState();
const userLookupResult = useAtomStateValue(userLookupResultState);
@@ -11,9 +11,10 @@ import { t } from '@lingui/core/macro';
import { useMemo, useState } from 'react';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { useQuery } from '@apollo/client/react';
import {
ConfigSource,
useGetConfigVariablesGroupedQuery,
GetConfigVariablesGroupedDocument,
} from '~/generated-metadata/graphql';
import { normalizeSearchText } from '~/utils/normalizeSearchText';
import { ConfigVariableSearchInput } from './ConfigVariableSearchInput';
@@ -30,10 +31,12 @@ const StyledTableContainer = styled.div`
`;
export const SettingsAdminConfigVariables = () => {
const { data: configVariables, loading: configVariablesLoading } =
useGetConfigVariablesGroupedQuery({
const { data: configVariables, loading: configVariablesLoading } = useQuery(
GetConfigVariablesGroupedDocument,
{
fetchPolicy: 'network-only',
});
},
);
const [search, setSearch] = useState('');
const [showHiddenGroupVariables, setShowHiddenGroupVariables] = useAtomState(
@@ -5,10 +5,11 @@ import { GET_DATABASE_CONFIG_VARIABLE } from '@/settings/admin-panel/config-vari
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { type ConfigVariableValue } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useMutation } from '@apollo/client/react';
import {
useCreateDatabaseConfigVariableMutation,
useDeleteDatabaseConfigVariableMutation,
useUpdateDatabaseConfigVariableMutation,
CreateDatabaseConfigVariableDocument,
DeleteDatabaseConfigVariableDocument,
UpdateDatabaseConfigVariableDocument,
} from '~/generated-metadata/graphql';
export const useConfigVariableActions = (variableName: string) => {
@@ -16,12 +17,15 @@ export const useConfigVariableActions = (variableName: string) => {
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const { refetch: refetchClientConfig } = useClientConfig();
const [updateDatabaseConfigVariable] =
useUpdateDatabaseConfigVariableMutation();
const [createDatabaseConfigVariable] =
useCreateDatabaseConfigVariableMutation();
const [deleteDatabaseConfigVariable] =
useDeleteDatabaseConfigVariableMutation();
const [updateDatabaseConfigVariable] = useMutation(
UpdateDatabaseConfigVariableDocument,
);
const [createDatabaseConfigVariable] = useMutation(
CreateDatabaseConfigVariableDocument,
);
const [deleteDatabaseConfigVariable] = useMutation(
DeleteDatabaseConfigVariableDocument,
);
const handleUpdateVariable = async (
value: ConfigVariableValue,
@@ -3,12 +3,16 @@ import { SettingsAdminHealthStatusListCard } from '@/settings/admin-panel/health
import { t } from '@lingui/core/macro';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { useGetSystemHealthStatusQuery } from '~/generated-metadata/graphql';
import { useQuery } from '@apollo/client/react';
import { GetSystemHealthStatusDocument } from '~/generated-metadata/graphql';
export const SettingsAdminHealthStatus = () => {
const { data, loading: loadingHealthStatus } = useGetSystemHealthStatusQuery({
fetchPolicy: 'network-only',
});
const { data, loading: loadingHealthStatus } = useQuery(
GetSystemHealthStatusDocument,
{
fetchPolicy: 'network-only',
},
);
const services = data?.getSystemHealthStatus.services ?? [];
@@ -18,10 +18,11 @@ import { useState } from 'react';
import { IconRefresh, IconTrash } from 'twenty-ui/display';
import { Button, Checkbox } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useQuery } from '@apollo/client/react';
import {
JobState,
type QueueJob,
useGetQueueJobsQuery,
GetQueueJobsDocument,
} from '~/generated-metadata/graphql';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
@@ -96,7 +97,7 @@ export const SettingsAdminQueueJobsTable = ({
const offset = page * LIMIT;
const { data, loading, refetch } = useGetQueueJobsQuery({
const { data, loading, refetch } = useQuery(GetQueueJobsDocument, {
variables: {
queueName,
state: stateFilter,
@@ -1,15 +1,16 @@
import { isDefined } from 'twenty-shared/utils';
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
import { SettingsAdminWorkerMetricsTooltip } from '@/settings/admin-panel/health-status/components/SettingsAdminWorkerMetricsTooltip';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useSnackBarOnQueryError } from '@/apollo/hooks/useSnackBarOnQueryError';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { ResponsiveLine } from '@nivo/line';
import { useContext } from 'react';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { useQuery } from '@apollo/client/react';
import {
QueueMetricsTimeRange,
useGetQueueMetricsQuery,
GetQueueMetricsDocument,
} from '~/generated-metadata/graphql';
const StyledGraphContainer = styled.div`
@@ -48,22 +49,17 @@ export const SettingsAdminWorkerMetricsGraph = ({
timeRange,
}: SettingsAdminWorkerMetricsGraphProps) => {
const { theme } = useContext(ThemeContext);
const { enqueueErrorSnackBar } = useSnackBar();
const { loading, data } = useGetQueueMetricsQuery({
const { loading, data, error } = useQuery(GetQueueMetricsDocument, {
variables: {
queueName,
timeRange,
},
fetchPolicy: 'no-cache',
onError: (error) => {
const errorMessage = error.message;
enqueueErrorSnackBar({
message: t`Error fetching worker metrics: ${errorMessage}`,
});
},
});
useSnackBarOnQueryError(error);
const metricsData = data?.getQueueMetrics?.data || [];
const hasData =
metricsData.length > 0 &&
@@ -1,15 +1,16 @@
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { plural, t } from '@lingui/core/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useDeleteJobsMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { DeleteJobsDocument } from '~/generated-metadata/graphql';
import { getErrorMessageFromApolloError } from '~/utils/get-error-message-from-apollo-error.util';
export const useDeleteJobs = (queueName: string, onSuccess?: () => void) => {
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [isDeleting, setIsDeleting] = useState(false);
const [deleteJobsMutation] = useDeleteJobsMutation();
const [deleteJobsMutation] = useMutation(DeleteJobsDocument);
const deleteJobs = async (jobIds: string[]) => {
setIsDeleting(true);
@@ -66,10 +67,9 @@ export const useDeleteJobs = (queueName: string, onSuccess?: () => void) => {
}
} catch (error) {
enqueueErrorSnackBar({
message:
error instanceof ApolloError
? getErrorMessageFromApolloError(error)
: t`Failed to delete jobs. Please try again later.`,
message: CombinedGraphQLErrors.is(error)
? getErrorMessageFromApolloError(error)
: t`Failed to delete jobs. Please try again later.`,
});
} finally {
setIsDeleting(false);
@@ -1,15 +1,16 @@
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { plural, t } from '@lingui/core/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useRetryJobsMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { RetryJobsDocument } from '~/generated-metadata/graphql';
import { getErrorMessageFromApolloError } from '~/utils/get-error-message-from-apollo-error.util';
export const useRetryJobs = (queueName: string, onSuccess?: () => void) => {
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [isRetrying, setIsRetrying] = useState(false);
const [retryJobsMutation] = useRetryJobsMutation();
const [retryJobsMutation] = useMutation(RetryJobsDocument);
const retryJobs = async (jobIds: string[]) => {
setIsRetrying(true);
@@ -70,10 +71,9 @@ export const useRetryJobs = (queueName: string, onSuccess?: () => void) => {
}
} catch (error) {
enqueueErrorSnackBar({
message:
error instanceof ApolloError
? getErrorMessageFromApolloError(error)
: t`Failed to retry jobs. Please try again later.`,
message: CombinedGraphQLErrors.is(error)
? getErrorMessageFromApolloError(error)
: t`Failed to retry jobs. Please try again later.`,
});
} finally {
setIsRetrying(false);