Compare commits

...
Author SHA1 Message Date
Claude e0baef94c9 fix: address PR review comments for Apollo v4 migration
- Add missing await on signIn mutation in handleCredentialsSignIn (Sentry)
- Preserve plain Error messages in useSnackBarOnQueryError instead of
  dropping to generic text
- Handle non-GraphQL errors in catch blocks across auth hooks
  (useHandleResetPassword, useSignUpInNewWorkspace,
  useHandleResendEmailVerificationToken, useSSO) by passing error.message
- Gate Apollo DevTools with IS_DEBUG_MODE in metadata ApolloProvider
- Wrap client.mutate in try/catch in AuthService for Apollo v4 compat
- Handle ApolloError in PromiseRejectionEffect alongside CombinedGraphQLErrors
- Prevent duplicate redirects in useWorkspaceFromInviteHash effect
- Wire billing query error to useSnackBarOnQueryError
- Remove unreachable renewResult.error branch in
  useRequestApplicationTokenRefresh (catch block handles it)

https://claude.ai/code/session_017uafSHYDZYzv7Tp6qnVDbB
2026-03-13 10:56:29 +00:00
Félix Malfait 5051c3c7cc Merge remote-tracking branch 'origin/claude/apollo-client-v3-to-v4-puCGa' into claude/apollo-client-v3-to-v4-TuXEA 2026-03-13 09:37:08 +01:00
Claude 13a5da40a3 fix: memoize useObjectPermissions and remaining query hooks for Apollo v4
Root cause: useObjectPermissions() was calling .reduce() on every render,
creating a new object reference each time. This caused all useMemo
dependency arrays containing objectPermissionsByObjectMetadataId to
invalidate, regenerating query DocumentNodes on every render.

Apollo v4's useQuery uses reference equality (query !== state.query) to
detect query changes. New DocumentNode references cause it to create a
new observable, restarting the query before results arrive — resulting
in infinite loading state on first view load after page refresh.

Fix: memoize the reduce result in useObjectPermissions, and wrap
useFindOneRecordQuery and useFindDuplicatesRecordsQuery with useMemo.

https://claude.ai/code/session_01EA7CMTUTG5zFP132yvUweT
2026-03-13 08:34:28 +00:00
Félix Malfait 41812b6987 Lint 2026-03-13 09:16:09 +01:00
Félix Malfait a4c7fccf62 Merge remote-tracking branch 'origin/claude/apollo-client-v3-to-v4-puCGa' into claude/apollo-client-v3-to-v4-TuXEA 2026-03-13 09:12:25 +01:00
Claude f5b280e7b8 fix: remove invalid addTypename option from InMemoryCache in Apollo v4
Apollo v4 removed addTypename from InMemoryCacheConfig. The option is
not needed because MockLink already normalizes both mock and operation
queries with addTypenameToDocument for matching purposes.

https://claude.ai/code/session_01EA7CMTUTG5zFP132yvUweT
2026-03-13 08:10:55 +00:00
Félix Malfait 6ae3bdaae7 Merge remote-tracking branch 'origin/claude/apollo-client-v3-to-v4-puCGa' into claude/apollo-client-v3-to-v4-TuXEA 2026-03-13 09:06:55 +01:00
Claude 2409152f44 fix: restore addTypename false for MockedProvider in Apollo v4
Apollo v4 removed the addTypename prop from MockedProvider.
Configure addTypename: false on InMemoryCache instead, which
preserves the original v3 behavior where __typename is not added
to mock queries, preventing mock matching failures in storybook tests.

https://claude.ai/code/session_01EA7CMTUTG5zFP132yvUweT
2026-03-13 08:01:58 +00:00
Félix Malfait 387209c288 Fix 2026-03-13 08:49:21 +01:00
Félix Malfait 28f3067911 Fix? 2026-03-12 22:42:43 +01:00
Claude 3ab9d3134c fix: regenerate metadata graphql types with codegen
Properly regenerate the generated-metadata/graphql.ts file using the
typed-document-node codegen plugin against the running server schema,
ensuring all types and documents are complete and correctly ordered.

https://claude.ai/code/session_01EA7CMTUTG5zFP132yvUweT
2026-03-12 20:24:02 +00:00
Claude cad4226a31 style: format files with prettier
https://claude.ai/code/session_01EA7CMTUTG5zFP132yvUweT
2026-03-12 20:14:03 +00:00
Claude 1dc497e1b1 fix: memoize dynamic query generation for Apollo v4 compatibility
Apollo v4's useQuery uses reference equality for the query DocumentNode.
Without memoization, generateFindManyRecordsQuery and similar functions
create new DocumentNode objects on every render, causing the observable
to be recreated before results are delivered - resulting in infinite
loading state on first view load.

Also fix new enterprise settings files from main for Apollo v4 (import
paths, CombinedGraphQLErrors, typed query hooks).

https://claude.ai/code/session_01EA7CMTUTG5zFP132yvUweT
2026-03-12 20:13:24 +00:00
Claude 88a7975d1c merge: resolve conflicts with latest main
https://claude.ai/code/session_01EA7CMTUTG5zFP132yvUweT
2026-03-12 20:03:55 +00:00
Claude 2b259ea44f merge: resolve conflicts with latest main, add EngineComponentKey
- Resolve 4 merge conflicts properly:
  - generated-metadata/graphql.ts: keep typed-document-node format, add
    EngineComponentKey enum and engineComponentKey field to CommandMenuItem
  - useAgentChatData.ts: keep useEffect + useQuery, add useMemo from main
  - useCommandMenuItemFrontComponentCommands.tsx: keep FindManyCommandMenuItemsDocument, add EngineComponentKey
  - CreateWorkspace.tsx: keep result.error (v4 MutateResult API)
- Add TODO to useSnackBarOnQueryError about centralizing error handling

https://claude.ai/code/session_019WGZ6Rd7sEHuBg9sTrXRqJ
2026-03-12 14:13:59 +00:00
Claude b1ed57ecf9 refactor: replace useEffect patterns with useSnackBarOnQueryError utility
Create useSnackBarOnQueryError utility hook to replace ~15 identical
error→snackbar useEffect patterns with one-liners. Remove onCompleted
callbacks from 5 hooks (useFindOneRecord, useGetOneLogicFunction, etc.)
since Apollo v4 no longer supports them on useQuery - callers now use
returned data directly. Net reduction of ~130 lines.

https://claude.ai/code/session_019WGZ6Rd7sEHuBg9sTrXRqJ
2026-03-12 12:53:01 +00:00
Claude 0c6a920bc3 fix: add missing mocks for useFieldMetadataItem test
https://claude.ai/code/session_019WGZ6Rd7sEHuBg9sTrXRqJ
2026-03-12 11:40:17 +00:00
Claude 8a30f5afc5 fix: complete Apollo v4 migration - upgrade deps, fix error handling, fix tests
- Upgrade apollo-link-rest 0.9 → 0.10 (Apollo v4 compatible)
- Upgrade apollo-upload-client 17 → 19 (Apollo v4 compatible)
- Replace createUploadLink with UploadHttpLink
- Replace instanceof CombinedGraphQLErrors with CombinedGraphQLErrors.is()
- Fix test mocks: jest.mock('@apollo/client') → jest.mock('@apollo/client/react')
- Fix inline snapshots: ApolloError → Error (v4 error type changes)
- Fix apollo.factory test for v4 error handling behavior
- Add apollo-upload-client type declarations
- Update jest.config to transform apollo-upload-client .mjs files

https://claude.ai/code/session_019WGZ6Rd7sEHuBg9sTrXRqJ
2026-03-12 11:39:31 +00:00
Claude b48572c89c fix: continue Apollo v4 migration - fix remaining type errors
- Fix untyped REST query data with explicit type parameters
- Fix onCompleted/onError in remaining query hooks
- Fix jest.Mock type assertions
- Fix MutateResult.errors -> .error in NameField
- Fix DeepPartialObject type assertions
- Fix addTypename in InMemoryCacheConfig
- Fix missing gql document constant references
2026-03-12 10:38:29 +00:00
Claude 30d24a26c3 fix: continue Apollo v4 migration - fix view optimistic effects, workflow cache, link types
- Fix Modifier return types by filtering undefined from toReference results
- Fix cache.extract() returning unknown in workflow hooks
- Fix ApolloLink array type compatibility with third-party links
2026-03-12 10:27:10 +00:00
Claude 43be270071 fix: continue Apollo Client v4 migration - fix type errors
- Fix connectToDevTools -> devtools option
- Fix ErrorLike type narrowing in apollo.factory.ts
- Fix ServerError construction in streamingRestLink
- Fix Observable.map -> rxjs pipe(map()) in custom links
- Fix addTypename prop removed from MockedProvider
- Fix FetchMoreQueryOptions -> ObservableQuery.FetchMoreOptions
- Fix duplicate GetChatThreadsDocument import
- Fix mockedApolloClient/mockedApolloCoreClient uri -> HttpLink
- Convert remaining generated hooks to useQuery/useMutation
- Fix useHandleResetPassword test to mock useMutation
2026-03-12 10:18:28 +00:00
Claude af1f8d7477 fix: reapply apollo.factory.ts v4 migration and codegen config
The linter keeps reverting these files. Reapplying the changes.

https://claude.ai/code/session_019WGZ6Rd7sEHuBg9sTrXRqJ
2026-03-12 09:52:55 +00:00
Claude 4649a6b2f5 feat: migrate Apollo Client v3 to v4 (WIP)
- Update @apollo/client from ^3.7.17 to ^4.0.0
- Switch codegen from typescript-react-apollo to typed-document-node
- Update ErrorLink to use CombinedGraphQLErrors.is() pattern
- Replace fromPromise with rxjs from/switchMap
- Move hook imports to @apollo/client/react
- Move error imports to @apollo/client/errors
- Move MockedProvider to @apollo/client/testing/react
- Add local getOperationName utility (removed from Apollo 4)
- Replace ApolloError with CombinedGraphQLErrors
- Remove codemod boilerplate (localState, Defer handler)

https://claude.ai/code/session_019WGZ6Rd7sEHuBg9sTrXRqJ
2026-03-12 09:47:18 +00:00
383 changed files with 3368 additions and 13432 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"private": true,
"dependencies": {
"@apollo/client": "^3.7.17",
"@apollo/client": "^4.0.0",
"@floating-ui/react": "^0.24.3",
"@linaria/core": "^6.2.0",
"@linaria/react": "^6.2.1",
+1 -4
View File
@@ -45,13 +45,10 @@ module.exports = {
plugins: [
'typescript',
'typescript-operations',
'typescript-react-apollo',
'typed-document-node',
],
config: {
skipTypename: false,
withHooks: true,
withHOC: false,
withComponent: false,
scalars: {
DateTime: 'string',
UUID: 'string',
+1 -4
View File
@@ -21,13 +21,10 @@ module.exports = {
plugins: [
'typescript',
'typescript-operations',
'typescript-react-apollo',
'typed-document-node',
],
config: {
skipTypename: false,
withHooks: true,
withHOC: false,
withComponent: false,
scalars: {
DateTime: 'string',
},
+3 -3
View File
@@ -24,12 +24,12 @@ const jestConfig = {
testEnvironmentOptions: {},
transformIgnorePatterns: [
'/node_modules/(?!(twenty-ui)/.*)',
'../../node_modules/(?!(twenty-ui)/.*)',
'/node_modules/(?!(twenty-ui|apollo-upload-client|extract-files|is-plain-obj)/.*)',
'../../node_modules/(?!(twenty-ui|apollo-upload-client|extract-files|is-plain-obj)/.*)',
'../../twenty-ui/',
],
transform: {
'^.+\\.(ts|js|tsx|jsx)$': [
'^.+\\.(ts|js|tsx|jsx|mjs)$': [
'@swc/jest',
{
jsc: {
+3 -4
View File
@@ -30,7 +30,7 @@
},
"dependencies": {
"@ai-sdk/react": "3.0.99",
"@apollo/client": "^3.7.17",
"@apollo/client": "^4.0.0",
"@blocknote/mantine": "^0.47.1",
"@blocknote/react": "^0.47.1",
"@blocknote/xl-docx-exporter": "^0.47.1",
@@ -77,8 +77,8 @@
"@types/marked": "^6.0.0",
"@xyflow/react": "^12.4.2",
"ai": "6.0.97",
"apollo-link-rest": "^0.9.0",
"apollo-upload-client": "^17.0.0",
"apollo-link-rest": "^0.10.0-rc.2",
"apollo-upload-client": "^19.0.0",
"buffer": "^6.0.3",
"cron-parser": "5.1.1",
"date-fns": "^2.30.0",
@@ -126,7 +126,6 @@
"@lingui/vite-plugin": "^5.1.2",
"@playwright/test": "^1.56.1",
"@tiptap/suggestion": "3.4.2",
"@types/apollo-upload-client": "^17.0.2",
"@types/file-saver": "^2.0.7",
"@types/js-cookie": "^3.0.3",
"@types/json-logic-js": "^2",
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
import { getOperationName } from '@apollo/client/utilities';
import { getOperationName } from '~/utils/getOperationName';
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { HttpResponse, graphql, http } from 'msw';
import { expect, within } from 'storybook/test';
@@ -1,4 +1,4 @@
import { getOperationName } from '@apollo/client/utilities';
import { getOperationName } from '~/utils/getOperationName';
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { HttpResponse, graphql } from 'msw';
@@ -5,18 +5,18 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useApolloClient } from '@apollo/client';
import { useApolloClient, useMutation } from '@apollo/client/react';
import { t } from '@lingui/core/macro';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import {
FeatureFlagKey,
FieldMetadataType,
useUploadFilesFieldFileMutation,
UploadFilesFieldFileDocument,
} from '~/generated-metadata/graphql';
export const useUploadAttachmentFile = () => {
const apolloClient = useApolloClient();
const [uploadFilesFieldFile] = useUploadFilesFieldFileMutation({
const [uploadFilesFieldFile] = useMutation(UploadFilesFieldFileDocument, {
client: apolloClient,
});
const isAttachmentMigrated = useIsFeatureEnabled(
@@ -1,5 +1,5 @@
import { gql, InMemoryCache } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing/react';
import { act, renderHook } from '@testing-library/react';
import { type ReactNode } from 'react';
import { Provider as JotaiProvider } from 'jotai';
@@ -2,14 +2,14 @@ import {
type DocumentNode,
type OperationVariables,
type TypedDocumentNode,
useQuery,
} from '@apollo/client';
import { useQuery } from '@apollo/client/react';
import { useState } from 'react';
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useSnackBarOnQueryError } from '@/apollo/hooks/useSnackBarOnQueryError';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
type CustomResolverQueryResult<
T extends {
@@ -37,7 +37,6 @@ export const useCustomResolver = <
isFetchingMore: boolean;
fetchMoreRecords: () => Promise<void>;
} => {
const { enqueueErrorSnackBar } = useSnackBar();
const apolloCoreClient = useApolloCoreClient();
const [page, setPage] = useState({
@@ -63,16 +62,14 @@ export const useCustomResolver = <
data,
loading: firstQueryLoading,
fetchMore,
error,
} = useQuery<CustomResolverQueryResult<T>>(query, {
client: apolloCoreClient,
variables: queryVariables,
onError: (error) => {
enqueueErrorSnackBar({
apolloError: error,
});
},
});
useSnackBarOnQueryError(error);
const fetchMoreRecords = async () => {
if (page.hasNextPage && !isFetchingMore && !firstQueryLoading) {
setIsFetchingMore(true);
@@ -10,8 +10,9 @@ import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { UserContext } from '@/users/contexts/UserContext';
import { useContext } from 'react';
import { useContext, useEffect } from 'react';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import {
@@ -118,28 +119,33 @@ export const EventCardCalendarEvent = ({
displayName: true,
},
},
onCompleted: (data) => {
upsertRecordsInStore({ partialRecords: [data] });
},
});
useEffect(() => {
if (calendarEvent) {
upsertRecordsInStore({ partialRecords: [calendarEvent] });
}
}, [calendarEvent, upsertRecordsInStore]);
const { timeZone } = useContext(UserContext);
if (isDefined(error)) {
const shouldHideMessageContent = error.graphQLErrors.some(
(e) => e.extensions?.code === 'FORBIDDEN',
);
if (CombinedGraphQLErrors.is(error)) {
const shouldHideMessageContent = error.errors.some(
(e) => e.extensions?.code === 'FORBIDDEN',
);
if (shouldHideMessageContent) {
return <CalendarEventNotSharedContent />;
}
if (shouldHideMessageContent) {
return <CalendarEventNotSharedContent />;
}
const shouldHandleNotFound = error.graphQLErrors.some(
(e) => e.extensions?.code === 'NOT_FOUND',
);
const shouldHandleNotFound = error.errors.some(
(e) => e.extensions?.code === 'NOT_FOUND',
);
if (shouldHandleNotFound) {
return <div>{t`Calendar event not found`}</div>;
if (shouldHandleNotFound) {
return <div>{t`Calendar event not found`}</div>;
}
}
return <div>{t`Error loading calendar event`}</div>;
@@ -8,7 +8,9 @@ import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { Trans, useLingui } from '@lingui/react/macro';
import { useEffect } from 'react';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { isDefined } from 'twenty-shared/utils';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -81,30 +83,37 @@ export const EventCardMessage = ({
handle: true,
},
},
onCompleted: (data) => {
upsertRecordsInStore({ partialRecords: [data] });
},
});
if (isDefined(error)) {
const shouldHideMessageContent = error.graphQLErrors.some(
(e) => e.extensions?.code === 'FORBIDDEN',
);
if (shouldHideMessageContent) {
return <EventCardMessageForbidden notSharedByFullName={authorFullName} />;
useEffect(() => {
if (message) {
upsertRecordsInStore({ partialRecords: [message] });
}
}, [message, upsertRecordsInStore]);
const shouldHandleNotFound = error.graphQLErrors.some(
(e) => e.extensions?.code === 'NOT_FOUND',
);
if (shouldHandleNotFound) {
return (
<div>
<Trans>Message not found</Trans>
</div>
if (isDefined(error)) {
if (CombinedGraphQLErrors.is(error)) {
const shouldHideMessageContent = error.errors.some(
(e) => e.extensions?.code === 'FORBIDDEN',
);
if (shouldHideMessageContent) {
return (
<EventCardMessageForbidden notSharedByFullName={authorFullName} />
);
}
const shouldHandleNotFound = error.errors.some(
(e) => e.extensions?.code === 'NOT_FOUND',
);
if (shouldHandleNotFound) {
return (
<div>
<Trans>Message not found</Trans>
</div>
);
}
}
return (
@@ -7,11 +7,12 @@ import {
isDefined,
} from 'twenty-shared/utils';
import { type WorkflowAttachment } from 'twenty-shared/workflow';
import { useUploadWorkflowFileMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { UploadWorkflowFileDocument } from '~/generated-metadata/graphql';
import { logError } from '~/utils/logError';
export const useUploadWorkflowFile = () => {
const [uploadWorkflowFileMutation] = useUploadWorkflowFileMutation();
const [uploadWorkflowFileMutation] = useMutation(UploadWorkflowFileDocument);
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const uploadWorkflowFile = async (
@@ -8,10 +8,11 @@ import { useState } from 'react';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { IconSparkles } from 'twenty-ui/display';
import { useQuery } from '@apollo/client/react';
import {
PermissionFlagType,
SubscriptionStatus,
useBillingPortalSessionQuery,
BillingPortalSessionDocument,
} from '~/generated-metadata/graphql';
export const AIChatCreditsExhaustedMessage = () => {
@@ -26,12 +27,14 @@ export const AIChatCreditsExhaustedMessage = () => {
const { [PermissionFlagType.WORKSPACE]: hasPermissionToManageBilling } =
usePermissionFlagMap();
const { data: billingPortalData, loading: isBillingPortalLoading } =
useBillingPortalSessionQuery({
const { data: billingPortalData, loading: isBillingPortalLoading } = useQuery(
BillingPortalSessionDocument,
{
variables: {
returnUrlPath: getSettingsPath(SettingsPath.Billing),
},
});
},
);
const openBillingPortal = () => {
if (
@@ -2,16 +2,16 @@ import { agentChatSelectedFilesState } from '@/ai/states/agentChatSelectedFilesS
import { agentChatUploadedFilesState } from '@/ai/states/agentChatUploadedFilesState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useApolloClient } from '@apollo/client';
import { useApolloClient, useMutation } from '@apollo/client/react';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { type AgentChatFileUIPart } from '@/ai/types/agent-chat-file-ui-part.type';
import { useUploadAiChatFileMutation } from '~/generated-metadata/graphql';
import { UploadAiChatFileDocument } from '~/generated-metadata/graphql';
export const useAIChatFileUpload = () => {
const apolloClient = useApolloClient();
const [uploadAiChatFile] = useUploadAiChatFileMutation({
const [uploadAiChatFile] = useMutation(UploadAiChatFileDocument, {
client: apolloClient,
});
const { t } = useLingui();
@@ -1,5 +1,5 @@
import { AGENT_CHAT_SEND_MESSAGE_EVENT_NAME } from '@/ai/constants/AgentChatSendMessageEventName';
import { useApolloClient } from '@apollo/client';
import { useApolloClient } from '@apollo/client/react';
import { useGetBrowsingContext } from '@/ai/hooks/useBrowsingContext';
import { agentChatSelectedFilesState } from '@/ai/states/agentChatSelectedFilesState';
@@ -1,5 +1,6 @@
import { useApolloClient } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { useApolloClient, useMutation, useQuery } from '@apollo/client/react';
import { getOperationName } from '~/utils/getOperationName';
import { useCallback, useEffect, useMemo } from 'react';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
@@ -24,13 +25,11 @@ import { mapDBMessagesToUIMessages } from '@/ai/utils/mapDBMessagesToUIMessages'
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { useCallback, useMemo } from 'react';
import {
type GetChatThreadsQuery,
GetChatThreadsDocument,
useCreateChatThreadMutation,
useGetChatMessagesQuery,
useGetChatThreadsQuery,
CreateChatThreadDocument,
GetChatMessagesDocument,
} from '~/generated-metadata/graphql';
export const useAgentChatData = () => {
@@ -54,7 +53,7 @@ export const useAgentChatData = () => {
const { scrollToBottom } = useAgentChatScrollToBottom();
const [createChatThread] = useCreateChatThreadMutation({
const [createChatThread] = useMutation(CreateChatThreadDocument, {
onCompleted: (data) => {
if (store.get(isCreatingForFirstSendState.atom)) {
store.set(isCreatingForFirstSendState.atom, false);
@@ -137,64 +136,80 @@ export const useAgentChatData = () => {
],
});
const { loading: threadsLoading } = useGetChatThreadsQuery({
variables: { paging: { first: CHAT_THREADS_PAGE_SIZE } },
skip: isDefined(currentAIChatThread),
onCompleted: (data) => {
const threads = data.chatThreads.edges.map((edge) => edge.node);
if (threads.length > 0) {
const firstThread = threads[0];
const newDraft =
store.get(agentChatDraftsByThreadIdState.atom)[firstThread.id] ?? '';
setCurrentAIChatThread(firstThread.id);
setAgentChatInput(newDraft);
setCurrentAIChatThreadTitle(firstThread.title ?? null);
const hasUsageData =
(firstThread.conversationSize ?? 0) > 0 &&
isDefined(firstThread.contextWindowTokens);
setAgentChatUsage(
hasUsageData
? {
lastMessage: null,
conversationSize: firstThread.conversationSize ?? 0,
contextWindowTokens: firstThread.contextWindowTokens ?? 0,
inputTokens: firstThread.totalInputTokens,
outputTokens: firstThread.totalOutputTokens,
inputCredits: firstThread.totalInputCredits,
outputCredits: firstThread.totalOutputCredits,
}
: null,
);
} else {
store.set(hasTriggeredCreateForDraftState.atom, false);
setCurrentAIChatThread(AGENT_CHAT_NEW_THREAD_DRAFT_KEY);
setAgentChatInput(
store.get(agentChatDraftsByThreadIdState.atom)[
AGENT_CHAT_NEW_THREAD_DRAFT_KEY
] ?? '',
);
setCurrentAIChatThreadTitle(null);
setAgentChatUsage(null);
}
const { loading: threadsLoading, data: threadsData } = useQuery(
GetChatThreadsDocument,
{
variables: { paging: { first: CHAT_THREADS_PAGE_SIZE } },
skip: isDefined(currentAIChatThread),
},
});
);
useEffect(() => {
if (!threadsData) return;
const threads = threadsData.chatThreads.edges.map((edge) => edge.node);
if (threads.length > 0) {
const firstThread = threads[0];
const newDraft =
store.get(agentChatDraftsByThreadIdState.atom)[firstThread.id] ?? '';
setCurrentAIChatThread(firstThread.id);
setAgentChatInput(newDraft);
setCurrentAIChatThreadTitle(firstThread.title ?? null);
const hasUsageData =
(firstThread.conversationSize ?? 0) > 0 &&
isDefined(firstThread.contextWindowTokens);
setAgentChatUsage(
hasUsageData
? {
lastMessage: null,
conversationSize: firstThread.conversationSize ?? 0,
contextWindowTokens: firstThread.contextWindowTokens ?? 0,
inputTokens: firstThread.totalInputTokens,
outputTokens: firstThread.totalOutputTokens,
inputCredits: firstThread.totalInputCredits,
outputCredits: firstThread.totalOutputCredits,
}
: null,
);
} else {
store.set(hasTriggeredCreateForDraftState.atom, false);
setCurrentAIChatThread(AGENT_CHAT_NEW_THREAD_DRAFT_KEY);
setAgentChatInput(
store.get(agentChatDraftsByThreadIdState.atom)[
AGENT_CHAT_NEW_THREAD_DRAFT_KEY
] ?? '',
);
setCurrentAIChatThreadTitle(null);
setAgentChatUsage(null);
}
}, [
threadsData,
store,
setCurrentAIChatThread,
setAgentChatInput,
setCurrentAIChatThreadTitle,
setAgentChatUsage,
]);
const isNewThread = useMemo(
() => currentAIChatThread === AGENT_CHAT_NEW_THREAD_DRAFT_KEY,
[currentAIChatThread],
);
const { loading: messagesLoading, data } = useGetChatMessagesQuery({
const { loading: messagesLoading, data } = useQuery(GetChatMessagesDocument, {
variables: { threadId: currentAIChatThread! },
skip: !isDefined(currentAIChatThread) || isNewThread,
onCompleted: () => {
});
useEffect(() => {
if (data) {
store.set(skipMessagesSkeletonUntilLoadedState.atom, false);
scrollToBottom();
},
});
}
}, [data, store, scrollToBottom]);
const ensureThreadForDraft = useCallback(() => {
const current = store.get(currentAIChatThreadState.atom);
@@ -5,21 +5,25 @@ import { isDefined } from 'twenty-shared/utils';
import { CHAT_THREADS_PAGE_SIZE } from '@/ai/constants/ChatThreads';
import { useGetChatThreadsQuery } from '~/generated-metadata/graphql';
import { useQuery } from '@apollo/client/react';
import { GetChatThreadsDocument } from '~/generated-metadata/graphql';
const FETCH_MORE_ROOT_MARGIN = '200px';
export const useChatThreads = () => {
const [shouldFetchMore, setShouldFetchMore] = useState(false);
const { data, loading, fetchMore } = useGetChatThreadsQuery({
const { data, loading, fetchMore } = useQuery(GetChatThreadsDocument, {
variables: {
paging: { first: CHAT_THREADS_PAGE_SIZE },
},
onCompleted: () => {
setShouldFetchMore(false);
},
});
useEffect(() => {
if (data) {
setShouldFetchMore(false);
}
}, [data]);
const edges = data?.chatThreads?.edges ?? [];
const threads = edges.map((edge) => edge.node);
const pageInfo = data?.chatThreads?.pageInfo;
@@ -1,5 +1,5 @@
import { GET_TOOL_INDEX } from '@/ai/graphql/queries/getToolIndex';
import { useQuery } from '@apollo/client';
import { useQuery } from '@apollo/client/react';
type ToolIndexEntry = {
name: string;
@@ -2,7 +2,6 @@ import { useProcessNewMessageStreamIncrement } from '@/ai/hooks/useProcessNewMes
import { agentChatMessageComponentFamilyState } from '@/ai/states/agentChatMessageComponentFamilyState';
import { useAtomComponentFamilyStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateCallbackState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { cloneDeep } from '@apollo/client/utilities';
import { useCallback } from 'react';
import { type ExtendedUIMessage } from 'twenty-shared/ai';
import { isDefined } from 'twenty-shared/utils';
@@ -38,7 +37,7 @@ export const useProcessIncrementalStreamMessages = () => {
continue;
}
const clonedMessage = cloneDeep(updatedMessage);
const clonedMessage = structuredClone(updatedMessage);
jotaiStore.set(
agentChatMessageFamilyCallbackState(updatedMessage.id),
@@ -1,5 +1,6 @@
import { gql } from '@apollo/client';
import { MockedProvider, type MockedResponse } from '@apollo/client/testing';
import { type MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing/react';
import { act, renderHook, waitFor } from '@testing-library/react';
import { type ReactNode } from 'react';
import {
@@ -94,9 +95,7 @@ const mocks: MockedResponse[] = [
];
const Wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={mocks} addTypename={false}>
{children}
</MockedProvider>
<MockedProvider mocks={mocks}>{children}</MockedProvider>
);
describe('useEventTracker', () => {
@@ -1,9 +1,10 @@
import { useCallback } from 'react';
import { v4 } from 'uuid';
import { useMutation } from '@apollo/client/react';
import {
AnalyticsType,
type MutationTrackAnalyticsArgs,
useTrackAnalyticsMutation,
TrackAnalyticsDocument,
} from '~/generated-metadata/graphql';
export const ANALYTICS_COOKIE_NAME = 'analyticsCookie';
@@ -25,7 +26,7 @@ export const setSessionId = (domain?: string): void => {
};
export const useEventTracker = () => {
const [createEventMutation] = useTrackAnalyticsMutation();
const [createEventMutation] = useMutation(TrackAnalyticsDocument);
return useCallback(
(
@@ -1,4 +1,4 @@
import { ApolloProvider as ApolloProviderBase } from '@apollo/client';
import { ApolloProvider as ApolloProviderBase } from '@apollo/client/react';
import { useApolloFactory } from '@/apollo/hooks/useApolloFactory';
import { createCaptchaRefreshLink } from '@/apollo/utils/captchaRefreshLink';
@@ -12,7 +12,7 @@ export const ApolloProvider = ({ children }: React.PropsWithChildren) => {
const apolloClient = useApolloFactory({
uri: `${REACT_APP_SERVER_BASE_URL}/metadata`,
connectToDevTools: true, // should this be default , ie dependant on IS_DEBUG_MODE?
devtools: { enabled: process.env.IS_DEBUG_MODE === 'true' },
extraLinks: [captchaRefreshLink],
});
@@ -1,4 +1,5 @@
import { ApolloError, gql } from '@apollo/client';
import { gql } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { act, renderHook } from '@testing-library/react';
import fetchMock, { enableFetchMocks } from 'jest-fetch-mock';
import { MemoryRouter, useLocation } from 'react-router-dom';
@@ -87,8 +88,10 @@ describe('useApolloFactory', () => {
});
});
} catch (error) {
expect(error).toBeInstanceOf(ApolloError);
expect((error as ApolloError).message).toBe('Error message not found.');
expect(error).toBeInstanceOf(CombinedGraphQLErrors);
expect((error as CombinedGraphQLErrors).message).toBe(
'Error message not found.',
);
expect(mockNavigate).toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith('/welcome');
@@ -1,4 +1,4 @@
import { InMemoryCache, type NormalizedCacheObject } from '@apollo/client';
import { InMemoryCache } from '@apollo/client';
import { useMemo, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
@@ -21,9 +21,9 @@ import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
export const useApolloFactory = (options: Partial<Options> = {}) => {
// oxlint-disable-next-line twenty/no-state-useref
const apolloRef = useRef<ApolloFactory<NormalizedCacheObject> | null>(null);
const apolloRef = useRef<ApolloFactory | null>(null);
const navigate = useNavigate();
const setTokenPair = useSetAtomState(tokenPairState);
@@ -58,7 +58,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
fetchPolicy: 'cache-and-network',
},
},
connectToDevTools: process.env.IS_DEBUG_MODE === 'true',
devtools: { enabled: process.env.IS_DEBUG_MODE === 'true' },
currentWorkspaceMember: currentWorkspaceMember,
currentWorkspace: currentWorkspace,
appVersion,
@@ -0,0 +1,30 @@
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useEffect } from 'react';
// Utility hook to show an error snackbar when a query returns an error.
// Replaces the removed onError callback from useQuery in Apollo v4.
//
// TODO: Consider moving generic error→snackbar handling into the centralized
// Apollo error link (apollo.factory.ts) which already handles auth errors,
// Sentry, retries, etc. This would eliminate the need for per-component
// useEffect calls. Keep this hook only for call sites that need a custom
// error message (e.g. useWebhookForm, SettingsAdminWorkerMetricsGraph).
export const useSnackBarOnQueryError = (
error: Error | CombinedGraphQLErrors | undefined,
message?: string,
) => {
const { enqueueErrorSnackBar } = useSnackBar();
useEffect(() => {
if (!error) return;
enqueueErrorSnackBar(
message
? { message }
: CombinedGraphQLErrors.is(error)
? { apolloError: error }
: { message: error.message },
);
}, [error, enqueueErrorSnackBar, message]);
};
@@ -1,8 +1,8 @@
import { type Reference } from '@apollo/client';
import {
type ReadFieldFunction,
type ToReferenceFunction,
} from '@apollo/client/cache/core/types/common';
import { type FieldFunctionOptions } from '@apollo/client/cache';
type ReadFieldFunction = FieldFunctionOptions['readField'];
type ToReferenceFunction = FieldFunctionOptions['toReference'];
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
@@ -21,7 +21,7 @@ describe('triggerUpdateGroupByQueriesOptimisticEffect', () => {
const mockModify = jest.fn();
const mockCache = {
modify: mockModify,
} as unknown as ApolloCache<unknown>;
} as unknown as ApolloCache;
triggerUpdateGroupByQueriesOptimisticEffect({
cache: mockCache,
@@ -43,7 +43,7 @@ describe('triggerUpdateGroupByQueriesOptimisticEffect', () => {
const mockModify = jest.fn();
const mockCache = {
modify: mockModify,
} as unknown as ApolloCache<unknown>;
} as unknown as ApolloCache;
triggerUpdateGroupByQueriesOptimisticEffect({
cache: mockCache,
@@ -60,7 +60,7 @@ describe('triggerUpdateGroupByQueriesOptimisticEffect', () => {
const mockModify = jest.fn();
const mockCache = {
modify: mockModify,
} as unknown as ApolloCache<unknown>;
} as unknown as ApolloCache;
triggerUpdateGroupByQueriesOptimisticEffect({
cache: mockCache,
@@ -77,7 +77,7 @@ describe('triggerUpdateGroupByQueriesOptimisticEffect', () => {
const mockModify = jest.fn();
const mockCache = {
modify: mockModify,
} as unknown as ApolloCache<unknown>;
} as unknown as ApolloCache;
triggerUpdateGroupByQueriesOptimisticEffect({
cache: mockCache,
@@ -1,6 +1,5 @@
import { normalizeGroupByDimensionValue } from '@/apollo/optimistic-effect/group-by/utils/normalizeGroupByDimensionValue';
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { isArray } from '@apollo/client/utilities';
import { isDefined } from 'twenty-shared/utils';
export const doesRecordBelongToGroup = (
@@ -14,7 +13,7 @@ export const doesRecordBelongToGroup = (
return true;
}
if (isArray(groupByConfig)) {
if (Array.isArray(groupByConfig)) {
const groupByFieldNames = groupByConfig.map(
(groupByField) => Object.keys(groupByField)[0],
);
@@ -4,10 +4,10 @@ import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefE
import { createCacheEdgeWithRecordRef } from '@/object-record/cache/utils/createCacheEdgeWithRecordRef';
import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
import {
type ReadFieldFunction,
type ToReferenceFunction,
} from '@apollo/client/cache/core/types/common';
import { type FieldFunctionOptions } from '@apollo/client/cache';
type ReadFieldFunction = FieldFunctionOptions['readField'];
type ToReferenceFunction = FieldFunctionOptions['toReference'];
import { isDefined } from 'twenty-shared/utils';
type ProcessGroupByConnectionWithRecordsArgs = {
@@ -15,7 +15,7 @@ import { isDefined } from 'twenty-shared/utils';
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
type TriggerUpdateGroupByQueriesOptimisticEffectArgs = {
cache: ApolloCache<unknown>;
cache: ApolloCache;
objectMetadataItem: ObjectMetadataItem;
operation: 'create' | 'update' | 'delete';
records: RecordGqlNode[];
@@ -1,5 +1,7 @@
import { type Reference, type StoreObject } from '@apollo/client';
import { type ReadFieldFunction } from '@apollo/client/cache/core/types/common';
import { type FieldFunctionOptions } from '@apollo/client/cache';
type ReadFieldFunction = FieldFunctionOptions['readField'];
import { isNonEmptyString } from '@sniptt/guards';
import { type RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
@@ -20,7 +20,7 @@ export const triggerAttachRelationOptimisticEffect = ({
objectMetadataItems,
objectPermissionsByObjectMetadataId,
}: {
cache: ApolloCache<unknown>;
cache: ApolloCache;
sourceObjectNameSingular: string;
sourceRecordId: string;
targetObjectMetadataItem: Pick<
@@ -25,7 +25,7 @@ import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
then we'll be able to uncomment the code below so the cached lists are updated coherently with the variables.
*/
type TriggerCreateRecordsOptimisticEffectArgs = {
cache: ApolloCache<object>;
cache: ApolloCache;
objectMetadataItem: ObjectMetadataItem;
recordsToCreate: RecordGqlNode[];
objectMetadataItems: ObjectMetadataItem[];
@@ -22,7 +22,7 @@ export const triggerDestroyRecordsOptimisticEffect = ({
upsertRecordsInStore,
objectPermissionsByObjectMetadataId,
}: {
cache: ApolloCache<unknown>;
cache: ApolloCache;
objectMetadataItem: ObjectMetadataItem;
recordsToDestroy: RecordGqlNode[];
objectMetadataItems: ObjectMetadataItem[];
@@ -18,7 +18,7 @@ export const triggerDetachRelationOptimisticEffect = ({
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
}: {
cache: ApolloCache<unknown>;
cache: ApolloCache;
sourceObjectNameSingular: string;
sourceRecordId: string;
targetObjectMetadataItem: Pick<
@@ -25,7 +25,7 @@ export const triggerUpdateRecordOptimisticEffect = ({
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
}: {
cache: ApolloCache<unknown>;
cache: ApolloCache;
objectMetadataItem: ObjectMetadataItem;
currentRecord: RecordGqlNode;
updatedRecord: RecordGqlNode;
@@ -26,7 +26,7 @@ export const triggerUpdateRecordOptimisticEffectByBatch = ({
objectPermissionsByObjectMetadataId,
upsertRecordsInStore,
}: {
cache: ApolloCache<unknown>;
cache: ApolloCache;
objectMetadataItem: ObjectMetadataItem;
currentRecords: RecordGqlNode[];
updatedRecords: RecordGqlNode[];
@@ -27,7 +27,7 @@ import {
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
type TriggerUpdateRelationsOptimisticEffectArgs = {
cache: ApolloCache<unknown>;
cache: ApolloCache;
sourceObjectMetadataItem: ObjectMetadataItem;
currentSourceRecord: RecordGqlNode | null;
updatedSourceRecord: RecordGqlNode | null;
@@ -106,7 +106,7 @@ const triggerUpdateRelationOptimisticEffect = ({
currentSourceRecord: RecordGqlNode | null;
objectMetadataItems: ObjectMetadataItem[];
sourceObjectMetadataItem: ObjectMetadataItem;
cache: ApolloCache<unknown>;
cache: ApolloCache;
isDeletion: boolean;
upsertRecordsInStore: (props: { partialRecords: ObjectRecord[] }) => void;
objectPermissionsByObjectMetadataId: Record<
@@ -262,7 +262,7 @@ const triggerUpdateMorphRelationOptimisticEffect = ({
currentSourceRecord: RecordGqlNode | null;
objectMetadataItems: ObjectMetadataItem[];
sourceObjectMetadataItem: ObjectMetadataItem;
cache: ApolloCache<unknown>;
cache: ApolloCache;
isDeletion: boolean;
upsertRecordsInStore: (props: { partialRecords: ObjectRecord[] }) => void;
objectPermissionsByObjectMetadataId: Record<
@@ -1,4 +1,5 @@
import { ApolloError, gql, InMemoryCache } from '@apollo/client';
import { gql, InMemoryCache } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import fetchMock, { enableFetchMocks } from 'jest-fetch-mock';
import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel';
@@ -81,7 +82,7 @@ const mockWorkspace = {
workspaceCustomApplicationId: CUSTOM_WORKSPACE_APPLICATION_MOCK.id,
};
const createMockOptions = (): Options<any> => ({
const createMockOptions = (): Options => ({
uri: 'http://localhost:3000',
currentWorkspaceMember: mockWorkspaceMember,
currentWorkspace: mockWorkspace,
@@ -148,8 +149,8 @@ describe('ApolloFactory', () => {
try {
await makeRequest();
} catch (error) {
expect(error).toBeInstanceOf(ApolloError);
expect((error as ApolloError).message).toBe('Unauthorized');
expect(error).toBeInstanceOf(CombinedGraphQLErrors);
expect((error as CombinedGraphQLErrors).message).toBe('Unauthorized');
expect(mockOnError).toHaveBeenCalledWith(errors);
}
}, 10000);
@@ -174,8 +175,10 @@ describe('ApolloFactory', () => {
try {
await makeRequest();
} catch (error) {
expect(error).toBeInstanceOf(ApolloError);
expect((error as ApolloError).message).toBe('Error message not found.');
expect(error).toBeInstanceOf(CombinedGraphQLErrors);
expect((error as CombinedGraphQLErrors).message).toBe(
'Error message not found.',
);
expect(mockOnError).toHaveBeenCalledWith(errors);
}
}, 10000);
@@ -198,22 +201,20 @@ describe('ApolloFactory', () => {
try {
await makeRequest();
} catch (error) {
expect(error).toBeInstanceOf(ApolloError);
expect((error as ApolloError).message).toBe('Unknown error');
expect(error).toBeInstanceOf(CombinedGraphQLErrors);
expect((error as CombinedGraphQLErrors).message).toBe('Unknown error');
expect(mockOnError).toHaveBeenCalledWith(errors);
}
}, 10000);
it('should call renewToken when encountering any error', async () => {
const mockError = { message: 'Unknown error' };
fetchMock.mockReject(() => Promise.reject(mockError));
fetchMock.mockReject(() => Promise.reject({ message: 'Unknown error' }));
try {
await makeRequest();
} catch (error) {
expect(error).toBeInstanceOf(ApolloError);
expect((error as ApolloError).message).toBe('Unknown error');
expect(mockOnNetworkError).toHaveBeenCalledWith(mockError);
expect(error).toBeDefined();
expect(mockOnNetworkError).toHaveBeenCalled();
}
}, 10000);
@@ -238,9 +239,9 @@ describe('ApolloFactory', () => {
it('should call onPayloadTooLarge when encountering a 413 error', async () => {
fetchMock.mockResponse(() =>
Promise.reject({
statusCode: 413,
message: 'Payload Too Large',
Promise.resolve({
status: 413,
body: 'Payload Too Large',
}),
);
@@ -1,19 +1,15 @@
import { ApolloClient, ApolloLink, type ErrorLike } from '@apollo/client';
import {
ApolloClient,
type ApolloClientOptions,
ApolloLink,
type FetchResult,
fromPromise,
type Observable,
type Operation,
type ServerError,
CombinedGraphQLErrors,
ServerError,
type ServerParseError,
} from '@apollo/client';
} from '@apollo/client/errors';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
import { ErrorLink } from '@apollo/client/link/error';
import { RetryLink } from '@apollo/client/link/retry';
import { from, switchMap } from 'rxjs';
import { RestLink } from 'apollo-link-rest';
import { createUploadLink } from 'apollo-upload-client';
import UploadHttpLink from 'apollo-upload-client/UploadHttpLink.mjs';
import { renewToken } from '@/auth/services/AuthService';
import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
@@ -47,7 +43,12 @@ const logger = loggerLink(() => 'Twenty');
// deduplicate into a single renewal request.
let renewalPromise: Promise<void> | null = null;
export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
export interface Options {
uri: string;
cache: ApolloClient.Options['cache'];
defaultOptions?: ApolloClient.Options['defaultOptions'];
headers?: Record<string, string>;
devtools?: { enabled?: boolean };
onError?: (err: readonly GraphQLFormattedError[] | undefined) => void;
onNetworkError?: (err: Error | ServerParseError | ServerError) => void;
onTokenPairChange?: (tokenPair: AuthTokenPair) => void;
@@ -61,15 +62,19 @@ export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
appVersion?: string;
}
export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
private client: ApolloClient<TCacheShape>;
export class ApolloFactory implements ApolloManager {
private client: ApolloClient;
private currentWorkspaceMember: CurrentWorkspaceMember | null = null;
private currentWorkspace: CurrentWorkspace | null = null;
private appVersion?: string;
constructor(opts: Options<TCacheShape>) {
constructor(opts: Options) {
const {
uri,
cache,
defaultOptions,
headers: optionHeaders,
devtools,
onError: onErrorCb,
onNetworkError,
onTokenPairChange,
@@ -81,7 +86,6 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
extraLinks,
isDebugMode,
appVersion,
...options
} = opts;
this.currentWorkspaceMember = currentWorkspaceMember;
@@ -89,7 +93,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
this.appVersion = appVersion;
const buildApolloLink = (): ApolloLink => {
const uploadLink = createUploadLink({
const uploadLink = new UploadHttpLink({
uri,
});
@@ -110,7 +114,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
return {
headers: {
...headers,
...options.headers,
...optionHeaders,
'x-locale': locale,
},
};
@@ -121,7 +125,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
return {
headers: {
...headers,
...options.headers,
...optionHeaders,
authorization: token ? `Bearer ${token}` : '',
'x-locale': locale,
...(this.currentWorkspace?.metadataVersion && {
@@ -153,8 +157,8 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
});
const handleTokenRenewal = (
operation: Operation,
forward: (operation: Operation) => Observable<FetchResult>,
operation: ApolloLink.Operation,
forward: ApolloLink.ForwardFunction,
) => {
if (!renewalPromise) {
// Always renew through /metadata since the RenewToken is only exposed there
@@ -181,7 +185,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
});
}
return fromPromise(renewalPromise).flatMap(() => forward(operation));
return from(renewalPromise).pipe(switchMap(() => forward(operation)));
};
const sendToSentry = ({
@@ -189,7 +193,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
operation,
}: {
graphQLError: GraphQLFormattedError;
operation: Operation;
operation: ApolloLink.Operation;
}) => {
if (isDebugMode === true) {
logDebug(
@@ -242,99 +246,104 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
});
};
const errorLink = onError(
({ graphQLErrors, networkError, forward, operation }) => {
if (isDefined(graphQLErrors)) {
onErrorCb?.(graphQLErrors);
for (const graphQLError of graphQLErrors) {
if (graphQLError.message === 'Unauthorized') {
// oxlint-disable-next-line no-console
console.log('Unauthorized, triggering token renewal');
return handleTokenRenewal(operation, forward);
}
switch (graphQLError?.extensions?.code) {
case 'APP_VERSION_MISMATCH': {
onAppVersionMismatch?.(
(graphQLError.extensions?.userFriendlyMessage as string) ||
t`Your app version is out of date. Please refresh the page.`,
);
return;
}
case 'UNAUTHENTICATED': {
// oxlint-disable-next-line no-console
console.log('UNAUTHENTICATED, triggering token renewal');
return handleTokenRenewal(operation, forward);
}
case 'NOT_FOUND':
case 'BAD_USER_INPUT':
case 'FORBIDDEN':
case 'CONFLICT':
case 'METADATA_VALIDATION_FAILED': {
return;
}
case 'USER_INPUT_ERROR': {
if (graphQLError.extensions?.isExpected === true) {
return;
}
sendToSentry({ graphQLError, operation });
return;
}
case 'INTERNAL_SERVER_ERROR': {
return; // already caught in BE
}
default:
sendToSentry({ graphQLError, operation });
}
}
}
if (isDefined(networkError)) {
if (
this.isRestOperation(operation) &&
this.isAuthenticationError(networkError as ServerError)
) {
const errorLink = new ErrorLink(({ error, operation, forward }) => {
if (CombinedGraphQLErrors.is(error)) {
onErrorCb?.(error.errors);
for (const graphQLError of error.errors) {
if (graphQLError.message === 'Unauthorized') {
// oxlint-disable-next-line no-console
console.log(
'Authentication error, triggering token renewal from errorLink',
);
console.log('Unauthorized, triggering token renewal');
return handleTokenRenewal(operation, forward);
}
if (this.isPayloadTooLargeError(networkError as ServerError)) {
onPayloadTooLarge?.(t`Uploaded content is too large.`);
return;
switch (graphQLError?.extensions?.code) {
case 'APP_VERSION_MISMATCH': {
onAppVersionMismatch?.(
(graphQLError.extensions?.userFriendlyMessage as string) ||
t`Your app version is out of date. Please refresh the page.`,
);
return;
}
case 'UNAUTHENTICATED': {
// oxlint-disable-next-line no-console
console.log('UNAUTHENTICATED, triggering token renewal');
return handleTokenRenewal(operation, forward);
}
case 'NOT_FOUND':
case 'BAD_USER_INPUT':
case 'FORBIDDEN':
case 'CONFLICT':
case 'METADATA_VALIDATION_FAILED': {
return;
}
case 'USER_INPUT_ERROR': {
if (graphQLError.extensions?.isExpected === true) {
return;
}
sendToSentry({ graphQLError, operation });
return;
}
case 'INTERNAL_SERVER_ERROR': {
return; // already caught in BE
}
default:
sendToSentry({ graphQLError, operation });
}
if (isDebugMode === true) {
logDebug(`[Network error]: ${networkError}`);
}
onNetworkError?.(networkError);
}
},
);
} else if (ServerError.is(error)) {
if (
this.isRestOperation(operation) &&
this.isAuthenticationError(error)
) {
// oxlint-disable-next-line no-console
console.log(
'Authentication error, triggering token renewal from errorLink',
);
return handleTokenRenewal(operation, forward);
}
return ApolloLink.from(
[
errorLink,
authLink,
...(extraLinks || []),
isDebugMode ? logger : null,
retryLink,
streamingRestLink,
restLink,
uploadLink,
].filter(isDefined),
);
if (this.isPayloadTooLargeError(error)) {
onPayloadTooLarge?.(t`Uploaded content is too large.`);
return;
}
if (isDebugMode === true) {
logDebug(`[Network error]: ${error}`);
}
onNetworkError?.(error);
} else if (isDefined(error)) {
if (isDebugMode === true) {
logDebug(`[Network error]: ${error}`);
}
onNetworkError?.(error as Error);
}
});
// Type assertion needed because third-party link packages (apollo-link-rest,
// apollo-upload-client) reference their own @apollo/client ApolloLink type
const links = [
errorLink,
authLink,
...(extraLinks || []),
...(isDebugMode ? [logger] : []),
retryLink,
streamingRestLink,
restLink,
uploadLink,
] as ApolloLink[];
return ApolloLink.from(links);
};
this.client = new ApolloClient({
...options,
cache,
link: buildApolloLink(),
defaultOptions,
devtools,
});
}
private isRestOperation(operation: Operation): boolean {
private isRestOperation(operation: ApolloLink.Operation): boolean {
return operation.query.definitions.some(
(def: DefinitionNode) =>
def.kind === 'OperationDefinition' &&
@@ -350,12 +359,12 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
);
}
private isAuthenticationError(error: ServerError): boolean {
return error.statusCode === 401;
private isAuthenticationError(error: ErrorLike): boolean {
return ServerError.is(error) && error.statusCode === 401;
}
private isPayloadTooLargeError(error: ServerError): boolean {
return error.statusCode === 413;
private isPayloadTooLargeError(error: ErrorLike): boolean {
return ServerError.is(error) && error.statusCode === 413;
}
updateWorkspaceMember(workspaceMember: CurrentWorkspaceMember | null) {
@@ -2,7 +2,7 @@ import { type ApolloClient } from '@apollo/client';
import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
export interface ApolloManager<TCacheShape> {
getClient(): ApolloClient<TCacheShape>;
export interface ApolloManager {
getClient(): ApolloClient;
updateWorkspaceMember(workspaceMember: CurrentWorkspaceMember | null): void;
}
@@ -25,7 +25,7 @@ describe('StreamingRestLink', () => {
`,
variables: {},
getContext: () => ({}),
} as Operation;
} as unknown as Operation;
const result = streamingLink.request(operation, mockForward);
@@ -50,7 +50,7 @@ describe('StreamingRestLink', () => {
operationName: 'StreamTest',
extensions: {},
setContext: jest.fn(),
} as Operation;
} as unknown as Operation;
const mockResponse = {
ok: true,
@@ -95,7 +95,7 @@ describe('StreamingRestLink', () => {
`,
variables: {},
getContext: () => ({}),
} as Operation;
} as unknown as Operation;
(global.fetch as jest.Mock).mockRejectedValue(new Error('Network error'));
@@ -122,7 +122,7 @@ describe('StreamingRestLink', () => {
`,
variables: {},
getContext: () => ({}),
} as Operation;
} as unknown as Operation;
const mockResponse = { ok: false, status: 404 };
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
@@ -160,7 +160,7 @@ describe('StreamingRestLink', () => {
`,
variables: {},
getContext: () => ({}),
} as Operation;
} as unknown as Operation;
const directive = (streamingLink as any).extractStreamDirective(
operation,
@@ -183,7 +183,7 @@ describe('StreamingRestLink', () => {
`,
variables: {},
getContext: () => ({}),
} as Operation;
} as unknown as Operation;
const directive = (streamingLink as any).extractStreamDirective(
operation,
@@ -1,4 +1,5 @@
import { ApolloLink } from '@apollo/client';
import { map } from 'rxjs';
export const createCaptchaRefreshLink = (
requestFreshCaptchaToken: () => void,
@@ -8,12 +9,14 @@ export const createCaptchaRefreshLink = (
const hasCaptchaToken = variables != null && 'captchaToken' in variables;
return forward(operation).map((response) => {
if (hasCaptchaToken) {
requestFreshCaptchaToken();
}
return forward(operation).pipe(
map((response) => {
if (hasCaptchaToken) {
requestFreshCaptchaToken();
}
return response;
});
return response;
}),
);
});
};
@@ -1,4 +1,5 @@
import { ApolloLink, gql, type Operation } from '@apollo/client';
import { map } from 'rxjs';
import { logDebug } from '~/utils/logDebug';
import { logError } from '~/utils/logError';
@@ -11,6 +12,10 @@ const getGroup = (collapsed: boolean) =>
: console.group.bind(console);
const parseQuery = (queryString: string) => {
if (!queryString.trim()) {
return ['Generic', ''];
}
const queryObj = gql`
${queryString}
`;
@@ -51,57 +56,59 @@ export const loggerLink = (getSchemaName: (operation: Operation) => string) =>
return forward(operation);
}
return forward(operation).map((result) => {
const time = Date.now() - operation.getContext().start;
const errors = result.errors ?? result.data?.[queryName]?.errors;
const hasError = Boolean(errors);
return forward(operation).pipe(
map((result) => {
const time = Date.now() - operation.getContext().start;
const errors = result.errors ?? result.data?.[queryName]?.errors;
const hasError = Boolean(errors);
try {
const titleArgs = formatTitle(
operationType,
schemaName,
queryName,
time,
);
try {
const titleArgs = formatTitle(
operationType,
schemaName,
queryName,
time,
);
getGroup(!hasError)(...titleArgs);
getGroup(!hasError)(...titleArgs);
if (isDefined(errors)) {
errors.forEach((err: any) => {
logDebug(
`%c${err.message}`,
// oxlint-disable-next-line twenty/no-hardcoded-colors
'color: #F51818; font-weight: lighter',
);
});
if (isDefined(errors)) {
errors.forEach((err: any) => {
logDebug(
`%c${err.message}`,
// oxlint-disable-next-line twenty/no-hardcoded-colors
'color: #F51818; font-weight: lighter',
);
});
}
logDebug('HEADERS: ', headers);
if (Object.keys(variables).length !== 0) {
logDebug('VARIABLES', variables);
}
logDebug('QUERY', query);
if (isDefined(result.data)) {
logDebug('RESULT', result.data);
}
if (isDefined(errors)) {
logDebug('ERRORS', errors);
}
console.groupEnd();
} catch {
// this may happen if console group is not supported
logDebug(
`${operationType} ${schemaName}::${queryName} (in ${time} ms)`,
);
if (isDefined(errors)) {
logError(errors);
}
}
logDebug('HEADERS: ', headers);
if (Object.keys(variables).length !== 0) {
logDebug('VARIABLES', variables);
}
logDebug('QUERY', query);
if (isDefined(result.data)) {
logDebug('RESULT', result.data);
}
if (isDefined(errors)) {
logDebug('ERRORS', errors);
}
console.groupEnd();
} catch {
// this may happen if console group is not supported
logDebug(
`${operationType} ${schemaName}::${queryName} (in ${time} ms)`,
);
if (isDefined(errors)) {
logError(errors);
}
}
return result;
});
return result;
}),
);
});
@@ -2,9 +2,9 @@ import {
ApolloLink,
Observable,
type Operation,
type ServerError,
} from '@apollo/client/core';
import { type FetchResult } from '@apollo/client/link/core';
type FetchResult,
} from '@apollo/client';
import { ServerError } from '@apollo/client/errors';
import { type ArgumentNode, type DirectiveNode } from 'graphql';
import { isDefined } from 'twenty-shared/utils';
@@ -62,13 +62,10 @@ export class StreamingRestLink extends ApolloLink {
fetch(url, requestConfig)
.then(async (response) => {
if (!response.ok) {
const networkError = new Error(
`HTTP error! status: ${response.status}`,
) as ServerError;
networkError.statusCode = response.status;
throw networkError;
throw new ServerError(`HTTP error! status: ${response.status}`, {
response,
bodyText: '',
});
}
if (!response.body) {
@@ -1,6 +1,6 @@
import { useAuth } from '@/auth/hooks/useAuth';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { AppPath } from 'twenty-shared/types';
import { verifyEmailRedirectPathState } from '@/app/states/verifyEmailRedirectPathState';
@@ -95,7 +95,7 @@ export const VerifyEmailEffect = () => {
await verifyLoginToken(loginToken.token);
} catch (error) {
enqueueErrorSnackBar({
...(error instanceof ApolloError
...(CombinedGraphQLErrors.is(error)
? { apolloError: error }
: { message: t`Email verification failed` }),
options: {
@@ -103,9 +103,8 @@ export const VerifyEmailEffect = () => {
},
});
if (
error instanceof ApolloError &&
error.graphQLErrors[0].extensions?.subCode ===
'EMAIL_ALREADY_VERIFIED'
CombinedGraphQLErrors.is(error) &&
error.errors[0].extensions?.subCode === 'EMAIL_ALREADY_VERIFIED'
) {
navigate(AppPath.SignInUp);
}
@@ -5,8 +5,8 @@ import { supportChatState } from '@/client-config/states/supportChatState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useApolloClient } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';
import { useApolloClient } from '@apollo/client/react';
import { MockedProvider } from '@apollo/client/testing/react';
import { type ReactNode, act } from 'react';
import { MemoryRouter } from 'react-router-dom';
@@ -73,7 +73,7 @@ jest.mock('@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain', () => ({
}));
const Wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={Object.values(mocks)} addTypename={false}>
<MockedProvider mocks={Object.values(mocks)}>
<MemoryRouter>
<SnackBarComponentInstanceContext.Provider
value={{ instanceId: 'test-instance-id' }}
@@ -1,20 +1,25 @@
import { ApolloError, useApolloClient } from '@apollo/client';
import {
useApolloClient,
useLazyQuery,
useMutation,
} from '@apollo/client/react';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useCallback } from 'react';
import { AppPath } from 'twenty-shared/types';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import {
useCheckUserExistsLazyQuery,
useGetAuthTokensFromLoginTokenMutation,
useGetAuthTokensFromOtpMutation,
useGetLoginTokenFromCredentialsMutation,
useSignInMutation,
useSignUpInWorkspaceMutation,
useSignUpMutation,
useVerifyEmailAndGetLoginTokenMutation,
useVerifyEmailAndGetWorkspaceAgnosticTokenMutation,
type AuthToken,
type AuthTokenPair,
CheckUserExistsDocument,
GetAuthTokensFromLoginTokenDocument,
GetAuthTokensFromOtpDocument,
GetLoginTokenFromCredentialsDocument,
SignInDocument,
SignUpInWorkspaceDocument,
SignUpDocument,
VerifyEmailAndGetLoginTokenDocument,
VerifyEmailAndGetWorkspaceAgnosticTokenDocument,
} from '~/generated-metadata/graphql';
import { tokenPairState } from '@/auth/states/tokenPairState';
@@ -91,25 +96,30 @@ export const useAuth = () => {
const { redirect } = useRedirect();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const [getLoginTokenFromCredentials] =
useGetLoginTokenFromCredentialsMutation();
const [signIn] = useSignInMutation();
const [signUp] = useSignUpMutation();
const [signUpInWorkspace] = useSignUpInWorkspaceMutation();
const [getAuthTokensFromLoginToken] =
useGetAuthTokensFromLoginTokenMutation();
const [verifyEmailAndGetLoginToken] =
useVerifyEmailAndGetLoginTokenMutation();
const [verifyEmailAndGetWorkspaceAgnosticToken] =
useVerifyEmailAndGetWorkspaceAgnosticTokenMutation();
const [getAuthTokensFromOtp] = useGetAuthTokensFromOtpMutation();
const [getLoginTokenFromCredentials] = useMutation(
GetLoginTokenFromCredentialsDocument,
);
const [signIn] = useMutation(SignInDocument);
const [signUp] = useMutation(SignUpDocument);
const [signUpInWorkspace] = useMutation(SignUpInWorkspaceDocument);
const [getAuthTokensFromLoginToken] = useMutation(
GetAuthTokensFromLoginTokenDocument,
);
const [verifyEmailAndGetLoginToken] = useMutation(
VerifyEmailAndGetLoginTokenDocument,
);
const [verifyEmailAndGetWorkspaceAgnosticToken] = useMutation(
VerifyEmailAndGetWorkspaceAgnosticTokenDocument,
);
const [getAuthTokensFromOtp] = useMutation(GetAuthTokensFromOtpDocument);
const workspacePublicData = useAtomStateValue(workspacePublicDataState);
const { setLastAuthenticateWorkspaceDomain } =
useLastAuthenticatedWorkspaceDomain();
const [checkUserExistsQuery, { data: checkUserExistsData }] =
useCheckUserExistsLazyQuery();
const [checkUserExistsQuery, { data: checkUserExistsData }] = useLazyQuery(
CheckUserExistsDocument,
);
const client = useApolloClient();
@@ -188,8 +198,8 @@ export const useAuth = () => {
origin,
},
});
if (isDefined(getLoginTokenResult.errors)) {
throw getLoginTokenResult.errors;
if (isDefined(getLoginTokenResult.error)) {
throw getLoginTokenResult.error;
}
if (!getLoginTokenResult.data?.getLoginTokenFromCredentials) {
@@ -200,8 +210,8 @@ export const useAuth = () => {
} catch (error) {
// TODO: Get intellisense for graphql error extensions code (codegen?)
if (
error instanceof ApolloError &&
error.graphQLErrors[0]?.extensions?.subCode === 'EMAIL_NOT_VERIFIED'
CombinedGraphQLErrors.is(error) &&
error.errors[0]?.extensions?.subCode === 'EMAIL_NOT_VERIFIED'
) {
setSearchParams({ email });
setSignInUpStep(SignInUpStep.EmailVerification);
@@ -228,8 +238,8 @@ export const useAuth = () => {
},
});
if (isDefined(loginTokenResult.errors)) {
throw loginTokenResult.errors;
if (isDefined(loginTokenResult.error)) {
throw loginTokenResult.error;
}
if (!loginTokenResult.data?.verifyEmailAndGetLoginToken) {
@@ -247,7 +257,7 @@ export const useAuth = () => {
email: string,
captchaToken?: string,
) => {
const { data, errors } = await verifyEmailAndGetWorkspaceAgnosticToken({
const { data, error } = await verifyEmailAndGetWorkspaceAgnosticToken({
variables: {
email,
emailVerificationToken,
@@ -255,8 +265,8 @@ export const useAuth = () => {
},
});
if (isDefined(errors)) {
throw errors;
if (isDefined(error)) {
throw error;
}
if (!data?.verifyEmailAndGetWorkspaceAgnosticToken) {
@@ -315,8 +325,8 @@ export const useAuth = () => {
},
});
if (isDefined(getAuthTokensResult.errors)) {
throw getAuthTokensResult.errors;
if (isDefined(getAuthTokensResult.error)) {
throw getAuthTokensResult.error;
}
if (!getAuthTokensResult.data?.getAuthTokensFromLoginToken) {
@@ -328,8 +338,8 @@ export const useAuth = () => {
);
} catch (error) {
if (
error instanceof ApolloError &&
error.graphQLErrors[0]?.extensions?.subCode ===
CombinedGraphQLErrors.is(error) &&
error.errors[0]?.extensions?.subCode ===
'TWO_FACTOR_AUTHENTICATION_PROVISION_REQUIRED'
) {
handleSetLoginToken(loginToken);
@@ -338,8 +348,8 @@ export const useAuth = () => {
}
if (
error instanceof ApolloError &&
error.graphQLErrors[0]?.extensions?.subCode ===
CombinedGraphQLErrors.is(error) &&
error.errors[0]?.extensions?.subCode ===
'TWO_FACTOR_AUTHENTICATION_VERIFICATION_REQUIRED'
) {
handleSetLoginToken(loginToken);
@@ -360,7 +370,7 @@ export const useAuth = () => {
const handleCredentialsSignIn = useCallback(
async (email: string, password: string, captchaToken?: string) => {
signIn({
await signIn({
variables: { email, password, captchaToken },
onCompleted: async (data) => {
handleSetAuthTokens(data.signIn.tokens);
@@ -394,8 +404,8 @@ export const useAuth = () => {
},
onError: (error) => {
if (
error instanceof ApolloError &&
error.graphQLErrors[0]?.extensions?.subCode === 'EMAIL_NOT_VERIFIED'
CombinedGraphQLErrors.is(error) &&
error.errors[0]?.extensions?.subCode === 'EMAIL_NOT_VERIFIED'
) {
setSearchParams({ email });
setSignInUpStep(SignInUpStep.EmailVerification);
@@ -427,8 +437,8 @@ export const useAuth = () => {
},
});
if (isDefined(signUpResult.errors)) {
throw signUpResult.errors;
if (isDefined(signUpResult.error)) {
throw signUpResult.error;
}
if (isEmailVerificationRequired) {
@@ -510,8 +520,8 @@ export const useAuth = () => {
},
});
if (isDefined(signUpInWorkspaceResult.errors)) {
throw signUpInWorkspaceResult.errors;
if (isDefined(signUpInWorkspaceResult.error)) {
throw signUpInWorkspaceResult.error;
}
if (!signUpInWorkspaceResult.data?.signUpInWorkspace) {
@@ -632,8 +642,8 @@ export const useAuth = () => {
},
});
if (isDefined(getAuthTokensFromOtpResult.errors)) {
throw getAuthTokensFromOtpResult.errors;
if (isDefined(getAuthTokensFromOtpResult.error)) {
throw getAuthTokensFromOtpResult.error;
}
if (!getAuthTokensFromOtpResult.data?.getAuthTokensFromOTP) {
@@ -3,11 +3,9 @@ import {
ApolloLink,
HttpLink,
InMemoryCache,
type UriFunction,
} from '@apollo/client';
import { loggerLink } from '@/apollo/utils/loggerLink';
import { isDefined } from 'twenty-shared/utils';
import {
type AuthTokenPair,
RenewTokenDocument,
@@ -19,7 +17,7 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
const logger = loggerLink(() => 'Twenty-Refresh');
const renewTokenMutation = async (
uri: string | UriFunction | undefined,
uri: string | undefined,
refreshToken: string,
) => {
const httpLink = new HttpLink({ uri });
@@ -30,18 +28,24 @@ const renewTokenMutation = async (
cache: new InMemoryCache({}),
});
const { data, errors } = await client.mutate<
RenewTokenMutation,
RenewTokenMutationVariables
>({
mutation: RenewTokenDocument,
variables: {
appToken: refreshToken,
},
fetchPolicy: 'network-only',
});
let data: RenewTokenMutation | null | undefined;
try {
const result = await client.mutate<
RenewTokenMutation,
RenewTokenMutationVariables
>({
mutation: RenewTokenDocument,
variables: {
appToken: refreshToken,
},
fetchPolicy: 'network-only',
});
data = result.data;
} catch {
throw new Error('Something went wrong during token renewal');
}
if (isDefined(errors) || isUndefinedOrNull(data)) {
if (isUndefinedOrNull(data)) {
throw new Error('Something went wrong during token renewal');
}
@@ -49,7 +53,7 @@ const renewTokenMutation = async (
};
export const renewToken = async (
uri: string | UriFunction | undefined,
uri: string | undefined,
tokenPair: AuthTokenPair | undefined | null,
) => {
if (!tokenPair) {
@@ -9,15 +9,13 @@ import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import {
type PublicWorkspaceData,
useEmailPasswordResetLinkMutation,
} from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { type PublicWorkspaceData } from '~/generated-metadata/graphql';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
// Mocks
jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar');
jest.mock('~/generated-metadata/graphql');
jest.mock('@apollo/client/react');
dynamicActivate(SOURCE_LOCALE);
@@ -63,7 +61,7 @@ describe('useHandleResetPassword', () => {
enqueueErrorSnackBar: enqueueErrorSnackBarMock,
enqueueSuccessSnackBar: enqueueSuccessSnackBarMock,
});
(useEmailPasswordResetLinkMutation as jest.Mock).mockReturnValue([
(useMutation as unknown as jest.Mock).mockReturnValue([
emailPasswordResetLinkMock,
]);
});
@@ -2,8 +2,8 @@ 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 { CombinedGraphQLErrors } from '@apollo/client/errors';
import { MockedProvider } from '@apollo/client/testing/react';
import { renderHook } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
@@ -55,9 +55,7 @@ const apolloMocks = [
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter>
<MockedProvider mocks={apolloMocks} addTypename={false}>
{children}
</MockedProvider>
<MockedProvider mocks={apolloMocks}>{children}</MockedProvider>
</MemoryRouter>
);
@@ -86,8 +84,8 @@ describe('useSSO', () => {
await result.current.redirectToSSOLoginPage(identityProviderId);
expect(mockEnqueueErrorSnackBar).toHaveBeenCalledWith({
apolloError: new ApolloError({
graphQLErrors: [{ message: 'Error message' }],
apolloError: new CombinedGraphQLErrors({
errors: [{ message: 'Error message' }],
}),
});
});
@@ -2,14 +2,16 @@ import { useCallback } from 'react';
import { useOrigin } from '@/domain-manager/hooks/useOrigin';
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 { useResendEmailVerificationTokenMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { ResendEmailVerificationTokenDocument } from '~/generated-metadata/graphql';
export const useHandleResendEmailVerificationToken = () => {
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
const [resendEmailVerificationToken, { loading }] =
useResendEmailVerificationTokenMutation();
const [resendEmailVerificationToken, { loading }] = useMutation(
ResendEmailVerificationTokenDocument,
);
const { origin } = useOrigin();
const handleResendEmailVerificationToken = useCallback(
@@ -38,9 +40,11 @@ export const useHandleResendEmailVerificationToken = () => {
enqueueErrorSnackBar({});
}
} catch (error) {
enqueueErrorSnackBar({
...(error instanceof ApolloError ? { apolloError: error } : {}),
});
enqueueErrorSnackBar(
CombinedGraphQLErrors.is(error)
? { apolloError: error }
: { message: error instanceof Error ? error.message : undefined },
);
}
};
},
@@ -3,14 +3,15 @@ import { useCallback } from 'react';
import { currentUserState } from '@/auth/states/currentUserState';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useLingui } from '@lingui/react/macro';
import { useEmailPasswordResetLinkMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { EmailPasswordResetLinkDocument } from '~/generated-metadata/graphql';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const useHandleResetPassword = () => {
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
const [emailPasswordResetLink] = useEmailPasswordResetLinkMutation();
const [emailPasswordResetLink] = useMutation(EmailPasswordResetLinkDocument);
const workspacePublicData = useAtomStateValue(workspacePublicDataState);
const currentUser = useAtomStateValue(currentUserState);
@@ -41,9 +42,11 @@ export const useHandleResetPassword = () => {
enqueueErrorSnackBar({});
}
} catch (error) {
enqueueErrorSnackBar({
...(error instanceof ApolloError ? { apolloError: error } : {}),
});
enqueueErrorSnackBar(
CombinedGraphQLErrors.is(error)
? { apolloError: error }
: { message: error instanceof Error ? error.message : undefined },
);
}
};
},
@@ -1,10 +1,11 @@
/* @license Enterprise */
import { GET_AUTHORIZATION_URL_FOR_SSO } from '@/auth/graphql/mutations/getAuthorizationUrlForSSO';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError, useApolloClient } from '@apollo/client';
import { useApolloClient } from '@apollo/client/react';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useParams } from 'react-router-dom';
import { GetAuthorizationUrlForSsoDocument } from '~/generated-metadata/graphql';
export const useSSO = () => {
const apolloClient = useApolloClient();
@@ -16,7 +17,7 @@ export const useSSO = () => {
let authorizationUrlForSSOResult;
try {
authorizationUrlForSSOResult = await apolloClient.mutate({
mutation: GET_AUTHORIZATION_URL_FOR_SSO,
mutation: GetAuthorizationUrlForSsoDocument,
variables: {
input: {
identityProviderId,
@@ -24,16 +25,21 @@ export const useSSO = () => {
},
},
});
} catch (error: any) {
return enqueueErrorSnackBar({
...(error instanceof ApolloError ? { apolloError: error } : {}),
});
} catch (error: unknown) {
return enqueueErrorSnackBar(
CombinedGraphQLErrors.is(error)
? { apolloError: error }
: { message: error instanceof Error ? error.message : undefined },
);
}
redirect(
const authorizationURL =
authorizationUrlForSSOResult.data?.getAuthorizationUrlForSSO
.authorizationURL,
);
?.authorizationURL;
if (authorizationURL) {
redirect(authorizationURL);
}
};
return {
@@ -16,7 +16,7 @@ import { useCaptcha } from '@/client-config/hooks/useCaptcha';
import { useBuildSearchParamsFromUrlSyncedStates } from '@/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { isErrorLike } from '@apollo/client/errors';
import { useLingui } from '@lingui/react/macro';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -178,9 +178,9 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
captchaToken: token,
verifyEmailRedirectPath,
});
} catch (error: any) {
} catch (error: unknown) {
enqueueErrorSnackBar({
...(error instanceof ApolloError ? { apolloError: error } : {}),
...(isErrorLike(error) ? { apolloError: error } : {}),
});
}
},
@@ -1,8 +1,9 @@
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { AppPath } from 'twenty-shared/types';
import { useSignUpInNewWorkspaceMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { SignUpInNewWorkspaceDocument } from '~/generated-metadata/graphql';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { useLingui } from '@lingui/react/macro';
@@ -12,7 +13,9 @@ export const useSignUpInNewWorkspace = () => {
const { enqueueErrorSnackBar } = useSnackBar();
const { t } = useLingui();
const [signUpInNewWorkspaceMutation] = useSignUpInNewWorkspaceMutation();
const [signUpInNewWorkspaceMutation] = useMutation(
SignUpInNewWorkspaceDocument,
);
const createWorkspace = async ({ newTab } = { newTab: true }) => {
try {
@@ -27,11 +30,16 @@ export const useSignUpInNewWorkspace = () => {
newTab ? '_blank' : '_self',
);
} catch (error) {
enqueueErrorSnackBar({
...(error instanceof ApolloError
enqueueErrorSnackBar(
CombinedGraphQLErrors.is(error)
? { apolloError: error }
: { message: t`Workspace creation failed` }),
});
: {
message:
error instanceof Error
? error.message
: t`Workspace creation failed`,
},
);
}
};
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
@@ -9,7 +9,8 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { t } from '@lingui/core/macro';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useGetWorkspaceFromInviteHashQuery } from '~/generated-metadata/graphql';
import { useQuery } from '@apollo/client/react';
import { GetWorkspaceFromInviteHashDocument } from '~/generated-metadata/graphql';
import { useNavigateApp } from '~/hooks/useNavigateApp';
export const useWorkspaceFromInviteHash = () => {
@@ -18,33 +19,52 @@ export const useWorkspaceFromInviteHash = () => {
const workspaceInviteHash = useParams().workspaceInviteHash;
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const [initiallyLoggedIn] = useState(isDefined(currentWorkspace));
const [hasRedirected, setHasRedirected] = useState(false);
const { data: workspaceFromInviteHash, loading } =
useGetWorkspaceFromInviteHashQuery({
skip: !workspaceInviteHash,
variables: { inviteHash: workspaceInviteHash || '' },
onError: (error) => {
enqueueErrorSnackBar({ apolloError: error });
navigate(AppPath.Index);
},
onCompleted: (data) => {
if (
isDefined(currentWorkspace) &&
isDefined(data?.findWorkspaceFromInviteHash) &&
currentWorkspace.id === data.findWorkspaceFromInviteHash.id
) {
const workspaceDisplayName =
data?.findWorkspaceFromInviteHash?.displayName;
initiallyLoggedIn &&
enqueueInfoSnackBar({
message: workspaceDisplayName
? t`You already belong to the workspace ${workspaceDisplayName}`
: t`You already belong to this workspace`,
});
navigate(AppPath.Index);
}
},
});
const {
data: workspaceFromInviteHash,
loading,
error,
} = useQuery(GetWorkspaceFromInviteHashDocument, {
skip: !workspaceInviteHash,
variables: { inviteHash: workspaceInviteHash || '' },
});
useEffect(() => {
if (error) {
enqueueErrorSnackBar({ apolloError: error });
navigate(AppPath.Index);
}
}, [error, enqueueErrorSnackBar, navigate]);
useEffect(() => {
if (!workspaceFromInviteHash || hasRedirected) return;
const inviteWorkspace = workspaceFromInviteHash.findWorkspaceFromInviteHash;
if (
isDefined(currentWorkspace) &&
isDefined(inviteWorkspace) &&
currentWorkspace.id === inviteWorkspace.id
) {
setHasRedirected(true);
const workspaceDisplayName = inviteWorkspace.displayName;
initiallyLoggedIn &&
enqueueInfoSnackBar({
message: workspaceDisplayName
? t`You already belong to the workspace ${workspaceDisplayName}`
: t`You already belong to this workspace`,
});
navigate(AppPath.Index);
}
}, [
workspaceFromInviteHash,
currentWorkspace,
hasRedirected,
initiallyLoggedIn,
enqueueInfoSnackBar,
navigate,
]);
return {
workspace: workspaceFromInviteHash?.findWorkspaceFromInviteHash,
workspaceInviteHash,
@@ -10,9 +10,10 @@ import { isDefined } from 'twenty-shared/utils';
import { H2Title, IconCircleX, IconCreditCard } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { useQuery } from '@apollo/client/react';
import {
SubscriptionStatus,
useBillingPortalSessionQuery,
BillingPortalSessionDocument,
} from '~/generated-metadata/graphql';
import { useGetWorkflowNodeExecutionUsage } from '@/billing/hooks/useGetWorkflowNodeExecutionUsage';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -37,7 +38,7 @@ export const SettingsBillingContent = () => {
isDefined(subscriptionStatus) &&
subscriptionStatus !== SubscriptionStatus.Canceled;
const { data, loading } = useBillingPortalSessionQuery({
const { data, loading } = useQuery(BillingPortalSessionDocument, {
variables: {
returnUrlPath: '/settings/billing',
},
@@ -46,17 +46,18 @@ import {
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useMutation } from '@apollo/client/react';
import {
BillingPlanKey,
BillingProductKey,
PermissionFlagType,
SubscriptionInterval,
useCancelSwitchBillingIntervalMutation,
useCancelSwitchBillingPlanMutation,
useCancelSwitchMeteredPriceMutation,
useSwitchBillingPlanMutation,
useSwitchSubscriptionIntervalMutation,
SubscriptionStatus,
CancelSwitchBillingIntervalDocument,
CancelSwitchBillingPlanDocument,
CancelSwitchMeteredPriceDocument,
SwitchBillingPlanDocument,
SwitchSubscriptionIntervalDocument,
} from '~/generated-metadata/graphql';
import { beautifyExactDate } from '~/utils/date-utils';
@@ -133,17 +134,23 @@ export const SettingsBillingSubscriptionInfo = ({
getBeautifiedRenewDate,
} = useBillingWording();
const [switchSubscriptionIntervalMutation] =
useSwitchSubscriptionIntervalMutation();
const [switchSubscriptionIntervalMutation] = useMutation(
SwitchSubscriptionIntervalDocument,
);
const [switchBillingPlan] = useSwitchBillingPlanMutation();
const [switchBillingPlan] = useMutation(SwitchBillingPlanDocument);
const [cancelSwitchBillingInterval] =
useCancelSwitchBillingIntervalMutation();
const [cancelSwitchBillingInterval] = useMutation(
CancelSwitchBillingIntervalDocument,
);
const [cancelSwitchBillingPlan] = useCancelSwitchBillingPlanMutation();
const [cancelSwitchBillingPlan] = useMutation(
CancelSwitchBillingPlanDocument,
);
const [cancelSwitchMeteredPrice] = useCancelSwitchMeteredPriceMutation();
const [cancelSwitchMeteredPrice] = useMutation(
CancelSwitchMeteredPriceDocument,
);
const setCurrentWorkspace = useSetAtomState(currentWorkspaceState);
@@ -19,9 +19,10 @@ import { findOrThrow, isDefined } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useMutation } from '@apollo/client/react';
import {
useSetMeteredSubscriptionPriceMutation,
SubscriptionInterval,
SetMeteredSubscriptionPriceDocument,
} from '~/generated-metadata/graphql';
const StyledRow = styled.div`
@@ -76,8 +77,9 @@ export const MeteredPriceSelector = ({
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [setMeteredSubscriptionPrice, { loading: isUpdating }] =
useSetMeteredSubscriptionPriceMutation();
const [setMeteredSubscriptionPrice, { loading: isUpdating }] = useMutation(
SetMeteredSubscriptionPriceDocument,
);
const options = [...meteredBillingPrices]
.sort((a, b) => a.tiers[0].flatAmount - b.tiers[0].flatAmount)
@@ -5,11 +5,14 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useEndSubscriptionTrialPeriodMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { EndSubscriptionTrialPeriodDocument } from '~/generated-metadata/graphql';
export const useEndSubscriptionTrialPeriod = () => {
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [endSubscriptionTrialPeriod] = useEndSubscriptionTrialPeriodMutation();
const [endSubscriptionTrialPeriod] = useMutation(
EndSubscriptionTrialPeriodDocument,
);
const [currentWorkspace, setCurrentWorkspace] = useAtomState(
currentWorkspaceState,
);
@@ -1,11 +1,12 @@
import { useQuery } from '@apollo/client/react';
import {
BillingProductKey,
useGetMeteredProductsUsageQuery,
GetMeteredProductsUsageDocument,
} from '~/generated-metadata/graphql';
import { findOrThrow } from 'twenty-shared/utils';
export const useGetWorkflowNodeExecutionUsage = () => {
const { data, loading, refetch } = useGetMeteredProductsUsageQuery();
const { data, loading, refetch } = useQuery(GetMeteredProductsUsageDocument);
const refetchMeteredProductsUsage = () => {
refetch();
@@ -2,10 +2,11 @@ import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { useMutation } from '@apollo/client/react';
import {
type BillingPlanKey,
type SubscriptionInterval,
useCheckoutSessionMutation,
CheckoutSessionDocument,
} from '~/generated-metadata/graphql';
export const useHandleCheckoutSession = ({
@@ -23,7 +24,7 @@ export const useHandleCheckoutSession = ({
const { enqueueErrorSnackBar } = useSnackBar();
const [checkoutSession] = useCheckoutSessionMutation();
const [checkoutSession] = useMutation(CheckoutSessionDocument);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -1,8 +1,9 @@
import { useListPlansQuery } from '~/generated-metadata/graphql';
import { useQuery } from '@apollo/client/react';
import { ListPlansDocument } from '~/generated-metadata/graphql';
import { isDefined } from 'twenty-shared/utils';
export const usePlans = () => {
const { data, loading, error } = useListPlansQuery();
const { data, loading, error } = useQuery(ListPlansDocument);
const isPlansLoaded = isDefined(data?.listPlans);
@@ -20,11 +20,12 @@ import { type IconComponent, useIcons } from 'twenty-ui/display';
import { type HeadlessFrontComponentMountContext } from '@/front-components/states/mountedHeadlessFrontComponentMapsState';
import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon';
import { useQuery } from '@apollo/client/react';
import {
CommandMenuItemAvailabilityType,
type CommandMenuItemFieldsFragment,
type EngineComponentKey,
useFindManyCommandMenuItemsQuery,
FindManyCommandMenuItemsDocument,
} from '~/generated-metadata/graphql';
type CommandMenuItemWithFrontComponent = CommandMenuItemFieldsFragment & {
@@ -202,7 +203,7 @@ export const useCommandMenuItemFrontComponentCommands = (
}
: undefined;
const { data } = useFindManyCommandMenuItemsQuery();
const { data } = useQuery(FindManyCommandMenuItemsDocument);
const allItems = data?.commandMenuItems ?? [];
@@ -9,7 +9,8 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
import { useDuplicateDashboardMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { DuplicateDashboardDocument } from '~/generated-metadata/graphql';
export const useDuplicateDashboard = () => {
const { objectMetadataItem } = useObjectMetadataItem({
@@ -24,7 +25,7 @@ export const useDuplicateDashboard = () => {
objectMetadataItem,
});
const [mutate] = useDuplicateDashboardMutation();
const [mutate] = useMutation(DuplicateDashboardDocument);
const duplicateDashboard = async (dashboardId: string) => {
const result = await mutate({
@@ -8,8 +8,11 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { workspaceAuthBypassProvidersState } from '@/workspace/states/workspaceAuthBypassProvidersState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useGetPublicWorkspaceDataByDomainQuery } from '~/generated-metadata/graphql';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useQuery } from '@apollo/client/react';
import { GetPublicWorkspaceDataByDomainDocument } from '~/generated-metadata/graphql';
export const useGetPublicWorkspaceDataByDomain = () => {
const { isDefaultDomain } = useIsCurrentLocationOnDefaultDomain();
@@ -28,15 +31,21 @@ export const useGetPublicWorkspaceDataByDomain = () => {
const setWorkspacePublicData = useSetAtomState(workspacePublicDataState);
const clientConfigApiStatus = useAtomStateValue(clientConfigApiStatusState);
const { loading, data, error } = useGetPublicWorkspaceDataByDomainQuery({
variables: {
origin,
const { loading, data, error } = useQuery(
GetPublicWorkspaceDataByDomainDocument,
{
variables: {
origin,
},
skip:
!clientConfigApiStatus.isSaved ||
(isMultiWorkspaceEnabled && isDefaultDomain) ||
isDefined(workspacePublicData),
},
skip:
!clientConfigApiStatus.isSaved ||
(isMultiWorkspaceEnabled && isDefaultDomain) ||
isDefined(workspacePublicData),
onCompleted: (data) => {
);
useEffect(() => {
if (data) {
setWorkspaceAuthProviders(
data.getPublicWorkspaceDataByDomain.authProviders,
);
@@ -44,21 +53,31 @@ export const useGetPublicWorkspaceDataByDomain = () => {
data.getPublicWorkspaceDataByDomain.authBypassProviders ?? null,
);
setWorkspacePublicData(data.getPublicWorkspaceDataByDomain);
},
onError: (error) => {
// Only redirect to default domain if it's a workspace not found error
const isWorkspaceNotFoundError = error.graphQLErrors?.some(
(graphQLError) => graphQLError.extensions?.code === 'NOT_FOUND',
);
}
}, [
data,
setWorkspaceAuthProviders,
setWorkspaceAuthBypassProviders,
setWorkspacePublicData,
]);
if (isWorkspaceNotFoundError) {
redirectToDefaultDomain();
} else {
// oxlint-disable-next-line no-console
console.error(error);
useEffect(() => {
if (error) {
// Only redirect to default domain if it's a workspace not found error
if (CombinedGraphQLErrors.is(error)) {
const isWorkspaceNotFoundError = error.errors?.some(
(graphQLError) => graphQLError.extensions?.code === 'NOT_FOUND',
);
if (isWorkspaceNotFoundError) {
redirectToDefaultDomain();
return;
}
}
},
});
// oxlint-disable-next-line no-console
console.error(error);
}
}, [error, redirectToDefaultDomain]);
return {
loading,
@@ -1,7 +1,8 @@
import { useCallback, useEffect } from 'react';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import isEmpty from 'lodash.isempty';
import { ApolloError } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { isDefined, type CustomError } from 'twenty-shared/utils';
const hasErrorCode = (
@@ -16,7 +17,7 @@ export const PromiseRejectionEffect = () => {
const handlePromiseRejection = useCallback(
async (event: PromiseRejectionEvent) => {
const error = event.reason;
if (error.name === 'ApolloError' && !isEmpty(error.graphQLErrors)) {
if (CombinedGraphQLErrors.is(error) || error instanceof ApolloError) {
enqueueErrorSnackBar({
apolloError: error,
});
@@ -24,11 +25,13 @@ export const PromiseRejectionEffect = () => {
}
const isAbortError =
error.networkError?.name === 'AbortError' ||
error.name === 'AbortError';
error?.networkError?.name === 'AbortError' ||
error?.name === 'AbortError';
if (!isAbortError) {
enqueueErrorSnackBar({});
enqueueErrorSnackBar(
error instanceof Error ? { message: error.message } : {},
);
}
try {
@@ -4,7 +4,8 @@ import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
import { useCreateNavigationMenuItemMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
@@ -20,10 +21,12 @@ export const useCreateFavorite = () => {
objectNameSingular: CoreObjectNameSingular.Favorite,
});
const [createNavigationMenuItemMutation] =
useCreateNavigationMenuItemMutation({
const [createNavigationMenuItemMutation] = useMutation(
CreateNavigationMenuItemDocument,
{
refetchQueries: ['FindManyNavigationMenuItems'],
});
},
);
const createFavorite = async (
targetRecord: ObjectRecord,
@@ -2,7 +2,8 @@ import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hoo
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { isDefined } from 'twenty-shared/utils';
import { useCreateNavigationMenuItemMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql';
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
import { usePrefetchedFavoritesFoldersData } from './usePrefetchedFavoritesFoldersData';
@@ -15,10 +16,12 @@ export const useCreateFavoriteFolder = () => {
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
const { navigationMenuItems } = usePrefetchedNavigationMenuItemsData();
const [createNavigationMenuItemMutation] =
useCreateNavigationMenuItemMutation({
const [createNavigationMenuItemMutation] = useMutation(
CreateNavigationMenuItemDocument,
{
refetchQueries: ['FindManyNavigationMenuItems'],
});
},
);
const createNewFavoriteFolder = async (name: string): Promise<void> => {
if (!name || !currentWorkspaceMemberId) {
@@ -6,12 +6,13 @@ import { getFrontComponentUrl } from '@/front-components/utils/getFrontComponent
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { t } from '@lingui/core/macro';
import { useCallback, useContext } from 'react';
import { useCallback, useContext, useEffect } from 'react';
import { FrontComponentRenderer as SharedFrontComponentRenderer } from 'twenty-sdk/front-component-renderer';
import { isDefined } from 'twenty-shared/utils';
import { ThemeContext } from 'twenty-ui/theme-constants';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useFindOneFrontComponentQuery } from '~/generated-metadata/graphql';
import { useQuery } from '@apollo/client/react';
import { FindOneFrontComponentDocument } from '~/generated-metadata/graphql';
type FrontComponentRendererProps = {
frontComponentId: string;
@@ -46,17 +47,25 @@ export const FrontComponentRenderer = ({
[enqueueErrorSnackBar],
);
const { data, loading } = useFindOneFrontComponentQuery({
const { data, loading, error } = useQuery(FindOneFrontComponentDocument, {
variables: { id: frontComponentId },
onError: handleError,
onCompleted: (completedData) => {
const tokenPair = completedData.frontComponent?.applicationTokenPair;
});
useEffect(() => {
if (error) {
handleError(error);
}
}, [error, handleError]);
useEffect(() => {
if (data) {
const tokenPair = data.frontComponent?.applicationTokenPair;
if (isDefined(tokenPair)) {
setFrontComponentApplicationTokenPair(tokenPair);
}
},
});
}
}, [data, setFrontComponentApplicationTokenPair]);
useOnFrontComponentUpdated({
frontComponentId,
@@ -1,8 +1,9 @@
import { ApolloError, useApolloClient } from '@apollo/client';
import { useApolloClient } from '@apollo/client/react';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { type GraphQLFormattedError } from 'graphql';
import { useCallback } from 'react';
import { useStore } from 'jotai';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
import { isDefined } from 'twenty-shared/utils';
import { frontComponentApplicationTokenPairComponentState } from '@/front-components/states/frontComponentApplicationTokenPairComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
@@ -76,6 +77,8 @@ export const useRequestApplicationTokenRefresh = ({
// If the refresh token itself is expired, fall back to refetching
// the front component which issues a fresh token pair server-side.
try {
// With the default errorPolicy ('none'), client.mutate() rejects on
// GraphQL/network errors, so error handling happens in the catch block.
const renewResult = await apolloClient.mutate<
RenewApplicationTokenMutation,
RenewApplicationTokenMutationVariables
@@ -87,20 +90,6 @@ export const useRequestApplicationTokenRefresh = ({
},
});
if (isNonEmptyArray(renewResult.errors)) {
if (
hasApplicationRefreshTokenInvalidOrExpiredSubCode(renewResult.errors)
) {
return await refetchFrontComponentForNewTokenPair();
}
const errorMessage = renewResult.errors
.map((error) => error.message)
.join(', ');
throw new Error(`Token renewal failed: ${errorMessage}`);
}
const renewedTokenPair = renewResult.data?.renewApplicationToken;
if (!isDefined(renewedTokenPair)) {
@@ -112,8 +101,8 @@ export const useRequestApplicationTokenRefresh = ({
return renewedTokenPair.applicationAccessToken.token;
} catch (error) {
if (
error instanceof ApolloError &&
hasApplicationRefreshTokenInvalidOrExpiredSubCode(error.graphQLErrors)
CombinedGraphQLErrors.is(error) &&
hasApplicationRefreshTokenInvalidOrExpiredSubCode(error.errors)
) {
return await refetchFrontComponentForNewTokenPair();
}
@@ -1,5 +1,5 @@
import { type MetadataOperationBrowserEventDetail } from '@/browser-event/types/MetadataOperationBrowserEventDetail';
import { useApolloClient } from '@apollo/client';
import { useApolloClient } from '@apollo/client/react';
import { isDefined } from 'twenty-shared/utils';
import {
FindOneFrontComponentDocument,
@@ -1,4 +1,5 @@
import { MockedProvider, type MockedResponse } from '@apollo/client/testing';
import { type MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing/react';
import { renderHook, waitFor } from '@testing-library/react';
import { type ReactNode } from 'react';
@@ -32,9 +33,7 @@ const mockPlaceDetails: PlaceDetailsResult = {
const createWrapper = (mocks: MockedResponse[]) => {
return ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={mocks} addTypename={false}>
{children}
</MockedProvider>
<MockedProvider mocks={mocks}>{children}</MockedProvider>
);
};
@@ -6,7 +6,7 @@ import {
type PlaceAutocompleteResult,
type PlaceDetailsResult,
} from '@/geo-map/types/placeApi';
import { useApolloClient } from '@apollo/client';
import { useApolloClient } from '@apollo/client/react';
export const useGetPlaceApiData = () => {
const apolloClient = useApolloClient();
@@ -16,7 +16,9 @@ export const useGetPlaceApiData = () => {
country?: string,
isFieldCity?: boolean,
): Promise<PlaceAutocompleteResult[] | undefined> => {
const { data } = await apolloClient.query({
const { data } = await apolloClient.query<{
getAutoCompleteAddress: PlaceAutocompleteResult[];
}>({
query: GET_AUTOCOMPLETE_QUERY,
variables: { address, token, country, isFieldCity: isFieldCity ?? false },
fetchPolicy: 'no-cache',
@@ -28,7 +30,9 @@ export const useGetPlaceApiData = () => {
placeId: string,
token: string,
): Promise<PlaceDetailsResult | undefined> => {
const { data } = await apolloClient.query({
const { data } = await apolloClient.query<{
getAddressDetails: PlaceDetailsResult;
}>({
query: GET_PLACE_DETAILS_QUERY,
variables: { placeId, token },
fetchPolicy: 'no-cache',
@@ -1,23 +1,27 @@
import { useSnackBarOnQueryError } from '@/apollo/hooks/useSnackBarOnQueryError';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { InformationBanner } from '@/information-banner/components/InformationBanner';
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
import { t } from '@lingui/core/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { useQuery } from '@apollo/client/react';
import {
PermissionFlagType,
useBillingPortalSessionQuery,
BillingPortalSessionDocument,
} from '~/generated-metadata/graphql';
export const InformationBannerBillingSubscriptionPaused = () => {
const { redirect } = useRedirect();
const { data, loading } = useBillingPortalSessionQuery({
const { data, loading, error } = useQuery(BillingPortalSessionDocument, {
variables: {
returnUrlPath: getSettingsPath(SettingsPath.Billing),
},
});
useSnackBarOnQueryError(error);
const {
[PermissionFlagType.WORKSPACE]: hasPermissionToUpdateBillingDetails,
} = usePermissionFlagMap();
@@ -4,15 +4,16 @@ import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMa
import { t } from '@lingui/core/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { useQuery } from '@apollo/client/react';
import {
PermissionFlagType,
useBillingPortalSessionQuery,
BillingPortalSessionDocument,
} from '~/generated-metadata/graphql';
export const InformationBannerFailPaymentInfo = () => {
const { redirect } = useRedirect();
const { data, loading } = useBillingPortalSessionQuery({
const { data, loading } = useQuery(BillingPortalSessionDocument, {
variables: {
returnUrlPath: getSettingsPath(SettingsPath.Billing),
},
@@ -1,4 +1,4 @@
import { useMutation } from '@apollo/client';
import { useMutation } from '@apollo/client/react';
import { DISMISS_RECONNECT_ACCOUNT_BANNER } from '@/information-banner/graphql/mutations/dismissReconnectAccountBanner';
import { informationBannerIsOpenComponentState } from '@/information-banner/states/informationBannerIsOpenComponentState';
@@ -37,7 +37,7 @@ describe('useLogicFunctionUpdateFormState', () => {
const { formValues } = result.current;
expect(formValues).toEqual({
name: '',
name: 'name',
description: '',
sourceHandlerCode: '',
isTool: false,
@@ -2,7 +2,7 @@ import { EXECUTE_ONE_LOGIC_FUNCTION } from '@/logic-functions/graphql/mutations/
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
import { logicFunctionTestDataFamilyState } from '@/workflow/workflow-steps/workflow-actions/code-action/states/logicFunctionTestDataFamilyState';
import { useMutation } from '@apollo/client';
import { useMutation } from '@apollo/client/react';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { LogicFunctionExecutionStatus } from '~/generated-metadata/graphql';
@@ -1,4 +1,4 @@
import { useQuery } from '@apollo/client';
import { useQuery } from '@apollo/client/react';
import { FIND_MANY_AVAILABLE_PACKAGES } from '@/logic-functions/graphql/queries/findManyAvailablePackages';
import {
type FindManyAvailablePackagesQuery,
@@ -1,5 +1,5 @@
import { GET_LOGIC_FUNCTION_SOURCE_CODE } from '@/logic-functions/graphql/queries/getLogicFunctionSourceCode';
import { useQuery } from '@apollo/client';
import { useQuery } from '@apollo/client/react';
import {
type GetLogicFunctionSourceCodeQuery,
type GetLogicFunctionSourceCodeQueryVariables,
@@ -1,17 +1,12 @@
import { FIND_ONE_LOGIC_FUNCTION } from '@/logic-functions/graphql/queries/findOneLogicFunction';
import { useQuery } from '@apollo/client';
import { useQuery } from '@apollo/client/react';
import {
type FindOneLogicFunctionQuery,
type FindOneLogicFunctionQueryVariables,
type LogicFunctionIdInput,
} from '~/generated-metadata/graphql';
export const useGetOneLogicFunction = ({
id,
onCompleted,
}: LogicFunctionIdInput & {
onCompleted?: (data: FindOneLogicFunctionQuery) => void;
}) => {
export const useGetOneLogicFunction = ({ id }: LogicFunctionIdInput) => {
const { data, loading } = useQuery<
FindOneLogicFunctionQuery,
FindOneLogicFunctionQueryVariables
@@ -19,8 +14,8 @@ export const useGetOneLogicFunction = ({
variables: {
input: { id },
},
onCompleted,
});
return {
logicFunction: data?.findOneLogicFunction || null,
loading,
@@ -1,10 +1,7 @@
import { useGetOneLogicFunction } from '@/logic-functions/hooks/useGetOneLogicFunction';
import { type Dispatch, type SetStateAction, useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
type FindOneLogicFunctionQuery,
type LogicFunction,
} from '~/generated-metadata/graphql';
import { type LogicFunction } from '~/generated-metadata/graphql';
import { useGetLogicFunctionSourceCode } from '@/logic-functions/hooks/useGetLogicFunctionSourceCode';
import { DEFAULT_TOOL_INPUT_SCHEMA } from 'twenty-shared/logic-function';
@@ -48,22 +45,22 @@ export const useLogicFunctionUpdateFormState = ({
const { logicFunction, loading: logicFunctionLoading } =
useGetOneLogicFunction({
id: logicFunctionId,
onCompleted: (data: FindOneLogicFunctionQuery) => {
const fn = data?.findOneLogicFunction;
if (isDefined(fn)) {
setFormValues((prevState) => ({
...prevState,
name: fn.name || '',
description: fn.description || '',
isTool: fn.isTool ?? false,
timeoutSeconds: fn.timeoutSeconds ?? 300,
toolInputSchema: fn.toolInputSchema || DEFAULT_TOOL_INPUT_SCHEMA,
}));
}
},
});
useEffect(() => {
if (isDefined(logicFunction)) {
setFormValues((prevState) => ({
...prevState,
name: logicFunction.name || '',
description: logicFunction.description || '',
isTool: logicFunction.isTool ?? false,
timeoutSeconds: logicFunction.timeoutSeconds ?? 300,
toolInputSchema:
logicFunction.toolInputSchema || DEFAULT_TOOL_INPUT_SCHEMA,
}));
}
}, [logicFunction]);
useEffect(() => {
if (isDefined(sourceHandlerCode)) {
setFormValues((prev) => ({
@@ -8,8 +8,9 @@ import { CREATE_ONE_LOGIC_FUNCTION } from '@/logic-functions/graphql/mutations/c
import { DELETE_ONE_LOGIC_FUNCTION } from '@/logic-functions/graphql/mutations/deleteOneLogicFunction';
import { FIND_MANY_LOGIC_FUNCTIONS } from '@/logic-functions/graphql/queries/findManyLogicFunctions';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError, useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { useMutation } from '@apollo/client/react';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { getOperationName } from '~/utils/getOperationName';
import { t } from '@lingui/core/macro';
import { CrudOperationType } from 'twenty-shared/types';
import {
@@ -60,7 +61,7 @@ export const usePersistLogicFunction = () => {
response: result,
};
} catch (error) {
if (error instanceof ApolloError) {
if (CombinedGraphQLErrors.is(error)) {
handleMetadataError(error, {
primaryMetadataName: 'logicFunction',
operationType: CrudOperationType.CREATE,
@@ -96,7 +97,7 @@ export const usePersistLogicFunction = () => {
response: result,
};
} catch (error) {
if (error instanceof ApolloError) {
if (CombinedGraphQLErrors.is(error)) {
handleMetadataError(error, {
primaryMetadataName: 'logicFunction',
operationType: CrudOperationType.UPDATE,
@@ -140,7 +141,7 @@ export const usePersistLogicFunction = () => {
response: result,
};
} catch (error) {
if (error instanceof ApolloError) {
if (CombinedGraphQLErrors.is(error)) {
handleMetadataError(error, {
primaryMetadataName: 'logicFunction',
operationType: CrudOperationType.DELETE,
@@ -1,8 +1,11 @@
import { useInstallApp } from '~/modules/marketplace/hooks/useInstallApp';
import { useInstallMarketplaceAppMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { InstallMarketplaceAppDocument } from '~/generated-metadata/graphql';
export const useInstallMarketplaceApp = () => {
const [installMarketplaceAppMutation] = useInstallMarketplaceAppMutation();
const [installMarketplaceAppMutation] = useMutation(
InstallMarketplaceAppDocument,
);
return useInstallApp<{
universalIdentifier: string;
@@ -1,6 +1,7 @@
import { useQuery } from '@apollo/client/react';
import {
useFindManyMarketplaceAppsQuery,
type MarketplaceApp,
FindManyMarketplaceAppsDocument,
} from '~/generated-metadata/graphql';
export type MarketplaceAppWithContentCounts = MarketplaceApp & {
@@ -13,7 +14,7 @@ export type MarketplaceAppWithContentCounts = MarketplaceApp & {
};
export const useMarketplaceApps = () => {
const { data, loading, error } = useFindManyMarketplaceAppsQuery();
const { data, loading, error } = useQuery(FindManyMarketplaceAppsDocument);
const marketplaceApps: MarketplaceAppWithContentCounts[] =
data?.findManyMarketplaceApps.map((app) => {
@@ -2,11 +2,12 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useUpgradeApplicationMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { UpgradeApplicationDocument } from '~/generated-metadata/graphql';
export const useUpgradeApplication = () => {
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
const [upgradeApplicationMutation] = useUpgradeApplicationMutation();
const [upgradeApplicationMutation] = useMutation(UpgradeApplicationDocument);
const [isUpgrading, setIsUpgrading] = useState(false);
const upgrade = async (params: {
@@ -1,4 +1,4 @@
import { type ApolloError } from '@apollo/client';
import { type CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { classifyMetadataError } from '@/metadata-error-handler/utils/classifyMetadataError';
@@ -48,7 +48,7 @@ export const useMetadataErrorHandler = () => {
} as const satisfies Record<AllMetadataName, string>;
const handleMetadataError = (
error: ApolloError,
error: CombinedGraphQLErrors,
options: {
primaryMetadataName: AllMetadataName;
operationType: CrudOperationType;
@@ -1,4 +1,4 @@
import { type ApolloError } from '@apollo/client';
import { type CombinedGraphQLErrors } from '@apollo/client/errors';
import {
type AllMetadataName,
type MetadataValidationErrorResponse,
@@ -7,7 +7,7 @@ import {
import { isDefined } from 'twenty-shared/utils';
export type MetadataErrorClassification =
| { type: 'v1'; error: ApolloError }
| { type: 'v1'; error: CombinedGraphQLErrors }
| {
type: 'v2-validation';
extensions: MetadataValidationErrorResponse;
@@ -39,14 +39,14 @@ const isMetadataInternalError = (
};
type ClassifyMetadataErrorArgs = {
error: ApolloError;
error: CombinedGraphQLErrors;
primaryMetadataName: AllMetadataName;
};
export const classifyMetadataError = ({
error,
primaryMetadataName,
}: ClassifyMetadataErrorArgs): MetadataErrorClassification => {
const extensions = error.graphQLErrors?.[0]?.extensions;
const extensions = error.errors?.[0]?.extensions;
if (!isDefined(extensions)) {
return { type: 'v1', error };
@@ -4,9 +4,10 @@ import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavi
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
import { useLazyQuery } from '@apollo/client/react';
import {
AllMetadataName,
useFindManyNavigationMenuItemsLazyQuery,
FindManyNavigationMenuItemsDocument,
} from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
@@ -16,8 +17,12 @@ export const NavigationMenuItemSSEEffect = () => {
const store = useStore();
const { updateDraft, applyChanges } = useMetadataStore();
const [findManyNavigationMenuItemsLazy] =
useFindManyNavigationMenuItemsLazyQuery();
const [findManyNavigationMenuItemsLazy] = useLazyQuery(
FindManyNavigationMenuItemsDocument,
{
fetchPolicy: 'network-only',
},
);
useListenToEventsForQuery({
queryId,
@@ -30,9 +35,7 @@ export const NavigationMenuItemSSEEffect = () => {
useListenToMetadataOperationBrowserEvent({
metadataName: AllMetadataName.navigationMenuItem,
onMetadataOperationBrowserEvent: async () => {
const result = await findManyNavigationMenuItemsLazy({
fetchPolicy: 'network-only',
});
const result = await findManyNavigationMenuItemsLazy();
if (!isDefined(result.data?.navigationMenuItems)) {
return;
@@ -5,9 +5,10 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
import { useStore } from 'jotai';
import { useLazyQuery } from '@apollo/client/react';
import {
AllMetadataName,
useFindManyNavigationMenuItemsLazyQuery,
FindManyNavigationMenuItemsDocument,
} from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
@@ -19,8 +20,12 @@ export const ObjectMetadataItemSSEEffect = () => {
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { updateDraft, applyChanges } = useMetadataStore();
const [findManyNavigationMenuItemsLazy] =
useFindManyNavigationMenuItemsLazyQuery();
const [findManyNavigationMenuItemsLazy] = useLazyQuery(
FindManyNavigationMenuItemsDocument,
{
fetchPolicy: 'network-only',
},
);
useListenToEventsForQuery({
queryId,
@@ -39,9 +44,7 @@ export const ObjectMetadataItemSSEEffect = () => {
updateDraft('objectMetadataItems', loadedObjects);
applyChanges();
const navigationMenuItemsResult = await findManyNavigationMenuItemsLazy({
fetchPolicy: 'network-only',
});
const navigationMenuItemsResult = await findManyNavigationMenuItemsLazy();
const existingNavigationMenuItems = store.get(
prefetchNavigationMenuItemsState.atom,
@@ -18,9 +18,10 @@ import { useCallback, useEffect, useState } from 'react';
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
import { type ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useQuery } from '@apollo/client/react';
import {
useGetCurrentUserQuery,
type WorkspaceMember,
GetCurrentUserDocument,
} from '~/generated-metadata/graphql';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
@@ -66,10 +67,12 @@ export const UserMetadataProviderInitialEffect = () => {
const shouldSkipUserQuery = !isLoggedIn || isDefined(currentUser);
const { data: userQueryData, loading: userQueryLoading } =
useGetCurrentUserQuery({
const { data: userQueryData, loading: userQueryLoading } = useQuery(
GetCurrentUserDocument,
{
skip: shouldSkipUserQuery,
});
},
);
useEffect(() => {
if (isInitialized) {
@@ -2,22 +2,24 @@ import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { useSetIndexViews } from '@/metadata-store/hooks/useSetIndexViews';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useLazyQuery } from '@apollo/client/react';
import {
useFindAllCoreViewsLazyQuery,
ViewType,
FindAllCoreViewsDocument,
} from '~/generated-metadata/graphql';
const INDEX_VIEW_TYPES = [ViewType.TABLE, ViewType.KANBAN, ViewType.CALENDAR];
export const useFetchAndLoadIndexViews = () => {
const [findAllCoreViews] = useFindAllCoreViewsLazyQuery();
const [findAllCoreViews] = useLazyQuery(FindAllCoreViewsDocument, {
fetchPolicy: 'network-only',
});
const { updateDraft, applyChanges } = useMetadataStore();
const { setIndexViews } = useSetIndexViews();
const fetchAndLoadIndexViews = useCallback(async () => {
const result = await findAllCoreViews({
variables: { viewTypes: INDEX_VIEW_TYPES },
fetchPolicy: 'network-only',
});
if (isDefined(result.data?.getCoreViews)) {
@@ -1,5 +1,6 @@
import { isDefined } from 'twenty-shared/utils';
import { useCreateNavigationMenuItemMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql';
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
@@ -11,10 +12,12 @@ export const useCreateNavigationMenuItem = () => {
usePrefetchedNavigationMenuItemsData();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const [createNavigationMenuItemMutation] =
useCreateNavigationMenuItemMutation({
const [createNavigationMenuItemMutation] = useMutation(
CreateNavigationMenuItemDocument,
{
refetchQueries: ['FindManyNavigationMenuItems'],
});
},
);
const createNavigationMenuItem = async (
targetRecord: ObjectRecord,
@@ -1,5 +1,6 @@
import { isDefined } from 'twenty-shared/utils';
import { useCreateNavigationMenuItemMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql';
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
@@ -7,10 +8,12 @@ export const useCreateNavigationMenuItemFolder = () => {
const { navigationMenuItems, currentWorkspaceMemberId } =
usePrefetchedNavigationMenuItemsData();
const [createNavigationMenuItemMutation] =
useCreateNavigationMenuItemMutation({
const [createNavigationMenuItemMutation] = useMutation(
CreateNavigationMenuItemDocument,
{
refetchQueries: ['FindManyNavigationMenuItems'],
});
},
);
const createNewNavigationMenuItemFolder = async (
name: string,
@@ -1,10 +1,13 @@
import { useDeleteNavigationMenuItemMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { DeleteNavigationMenuItemDocument } from '~/generated-metadata/graphql';
export const useDeleteNavigationMenuItem = () => {
const [deleteNavigationMenuItemMutation] =
useDeleteNavigationMenuItemMutation({
const [deleteNavigationMenuItemMutation] = useMutation(
DeleteNavigationMenuItemDocument,
{
refetchQueries: ['FindManyNavigationMenuItems'],
});
},
);
const deleteNavigationMenuItem = async (id: string) => {
await deleteNavigationMenuItemMutation({

Some files were not shown because too many files have changed in this diff Show More