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
@@ -6,7 +6,8 @@ import { useDebouncedCallback } from 'use-debounce';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useUpdateApiKeyMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { UpdateApiKeyDocument } from '~/generated-metadata/graphql';
const StyledComboInputContainer = styled.div`
display: flex;
@@ -29,7 +30,7 @@ export const ApiKeyNameInput = ({
disabled,
onNameUpdate,
}: ApiKeyNameInputProps) => {
const [updateApiKey] = useUpdateApiKeyMutation();
const [updateApiKey] = useMutation(UpdateApiKeyDocument);
// TODO: Enhance this with react-web-hook-form (https://www.react-hook-form.com)
// oxlint-disable-next-line react-hooks/exhaustive-deps
@@ -8,14 +8,15 @@ import { Trans } from '@lingui/react/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useGetApiKeysQuery } from '~/generated-metadata/graphql';
import { useQuery } from '@apollo/client/react';
import { GetApiKeysDocument } from '~/generated-metadata/graphql';
const StyledTableBodyContainer = styled.div`
border-bottom: 1px solid ${themeCssVariables.border.color.light};
`;
export const SettingsApiKeysTable = () => {
const { data: apiKeysData } = useGetApiKeysQuery();
const { data: apiKeysData } = useQuery(GetApiKeysDocument);
const apiKeys = apiKeysData?.apiKeys;
@@ -8,7 +8,8 @@ import { TableRow } from '@/ui/layout/table/components/TableRow';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useGetWebhooksQuery } from '~/generated-metadata/graphql';
import { useQuery } from '@apollo/client/react';
import { GetWebhooksDocument } from '~/generated-metadata/graphql';
const StyledTableBodyContainer = styled.div`
border-bottom: 1px solid ${themeCssVariables.border.color.light};
@@ -17,7 +18,7 @@ const StyledTableBodyContainer = styled.div`
`;
export const SettingsWebhooksTable = () => {
const { data: webhooksData } = useGetWebhooksQuery();
const { data: webhooksData } = useQuery(GetWebhooksDocument);
const webhooks = webhooksData?.webhooks;
@@ -1,5 +1,6 @@
import { MockedProvider } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing/react';
import { act, renderHook } from '@testing-library/react';
import { GraphQLError } from 'graphql';
import { type ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
@@ -123,7 +124,7 @@ const Wrapper = ({
children: ReactNode;
mocks?: any[];
}) => (
<MockedProvider mocks={mocks} addTypename={false}>
<MockedProvider mocks={mocks}>
<MemoryRouter>{children}</MemoryRouter>
</MockedProvider>
);
@@ -190,7 +191,9 @@ describe('useWebhookForm', () => {
},
},
},
error: new Error('Creation failed'),
result: {
errors: [new GraphQLError('Creation failed')],
},
};
const mocks = [errorMock];
@@ -332,7 +335,9 @@ describe('useWebhookForm', () => {
},
},
},
error: new Error('Update failed'),
result: {
errors: [new GraphQLError('Update failed')],
},
};
const mocks = [getWebhookMock, updateErrorMock];
@@ -463,7 +468,9 @@ describe('useWebhookForm', () => {
id: webhookId,
},
},
error: new Error('Deletion failed'),
result: {
errors: [new GraphQLError('Deletion failed')],
},
};
const mocks = [createGetWebhookMock(webhookId), errorMock];
@@ -1,4 +1,5 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { WebhookFormMode } from '@/settings/developers/constants/WebhookFormMode';
@@ -12,15 +13,17 @@ import {
webhookFormSchema,
type WebhookFormValues,
} from '@/settings/developers/validation-schemas/webhookFormSchema';
import { useSnackBarOnQueryError } from '@/apollo/hooks/useSnackBarOnQueryError';
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 { SettingsPath } from 'twenty-shared/types';
import { useMutation, useQuery } from '@apollo/client/react';
import {
useCreateWebhookMutation,
useDeleteWebhookMutation,
useGetWebhookQuery,
useUpdateWebhookMutation,
CreateWebhookDocument,
DeleteWebhookDocument,
GetWebhookDocument,
UpdateWebhookDocument,
} from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
@@ -42,9 +45,9 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
const isCreationMode = mode === WebhookFormMode.Create;
const [createWebhook] = useCreateWebhookMutation();
const [updateWebhook] = useUpdateWebhookMutation();
const [deleteWebhook] = useDeleteWebhookMutation();
const [createWebhook] = useMutation(CreateWebhookDocument);
const [updateWebhook] = useMutation(UpdateWebhookDocument);
const [deleteWebhook] = useMutation(DeleteWebhookDocument);
const formConfig = useForm<WebhookFormValues>({
mode: isCreationMode ? 'onSubmit' : 'onTouched',
@@ -52,13 +55,20 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
defaultValues: DEFAULT_FORM_VALUES,
});
const { loading, error } = useGetWebhookQuery({
const {
loading,
error,
data: webhookData,
} = useQuery(GetWebhookDocument, {
skip: isCreationMode || !webhookId,
variables: {
id: webhookId || '',
},
onCompleted: (data) => {
const webhook = data.webhook;
});
useEffect(() => {
if (webhookData) {
const webhook = webhookData.webhook;
if (!webhook) return;
const baseOperations = webhook?.operations?.length
@@ -72,13 +82,10 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
operations,
secret: webhook.secret || '',
});
},
onError: () => {
enqueueErrorSnackBar({
message: t`Failed to load webhook`,
});
},
});
}
}, [webhookData, formConfig]);
useSnackBarOnQueryError(error, t`Failed to load webhook`);
const { isDirty, isValid, isSubmitting } = formConfig.formState;
const canSave = isCreationMode
@@ -105,7 +112,7 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
);
} catch (error) {
enqueueErrorSnackBar({
apolloError: error instanceof ApolloError ? error : undefined,
apolloError: CombinedGraphQLErrors.is(error) ? error : undefined,
});
}
};
@@ -136,7 +143,7 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
});
} catch (error) {
enqueueErrorSnackBar({
apolloError: error instanceof ApolloError ? error : undefined,
apolloError: CombinedGraphQLErrors.is(error) ? error : undefined,
});
}
};
@@ -193,7 +200,7 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
navigate(SettingsPath.ApiWebhooks);
} catch (error) {
enqueueErrorSnackBar({
apolloError: error instanceof ApolloError ? error : undefined,
apolloError: CombinedGraphQLErrors.is(error) ? error : undefined,
});
}
};