From 616d58bc7e4e756255ccfcd441ad494b266024c4 Mon Sep 17 00:00:00 2001 From: neo773 <62795688+neo773@users.noreply.github.com> Date: Fri, 19 Jun 2026 05:29:35 +0530 Subject: [PATCH] messaging: gmail folder backfill (#21753) demo https://github.com/user-attachments/assets/a157cee1-a8fa-4050-af1b-c31a83fb75da /closes #17095 Review in cubic --- .../src/metadata/generated/schema.graphql | 3 +- .../src/metadata/generated/schema.ts | 5 +- .../src/generated-metadata/graphql.ts | 11 ++-- .../modules/accounts/types/MessageFolder.ts | 3 + .../SettingsMessageFoldersTreeItem.tsx | 7 +++ .../computeFolderIdsForSyncToggle.test.ts | 2 + .../computeMessageFolderTree.test.ts | 2 + .../graphql/mutations/updateMessageFolders.ts | 1 + .../graphql/queries/getMyMessageFolders.ts | 1 + .../useUpdateMessageFoldersSyncStatus.ts | 2 + ...t-to-message-folder-pending-sync-action.ts | 25 ++++++++ .../instance-commands.constant.ts | 2 + .../dtos/update-message-folder.input.ts | 13 +---- .../message-folder-metadata.service.ts | 58 +++++++++++++++++-- .../resolvers/message-folder.resolver.ts | 2 +- .../gmail-get-message-list.service.ts | 2 +- .../messaging-import-manager.module.ts | 6 +- ...essaging-import-folder-messages.service.ts | 49 ++++++++++++++++ .../messaging-message-list-fetch.service.ts | 27 +++++---- ...essaging-process-folder-actions.service.ts | 57 +++++++++++++----- .../types/MessageFolderPendingSyncAction.ts | 1 + 21 files changed, 223 insertions(+), 56 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-15/2-15-instance-command-fast-1781714499016-add-folder-import-to-message-folder-pending-sync-action.ts create mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-folder-messages.service.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 9166dc2536..b74535d330 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -2943,6 +2943,7 @@ type MessageFolder { enum MessageFolderPendingSyncAction { FOLDER_DELETION + FOLDER_IMPORT NONE } @@ -4374,7 +4375,7 @@ input UpdateMessageFolderInput { } input UpdateMessageFolderInputUpdates { - isSynced: Boolean + isSynced: Boolean! } input UpdateMessageFoldersInput { diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 287a94ba77..4e505130ea 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -2596,7 +2596,7 @@ export interface MessageFolder { __typename: 'MessageFolder' } -export type MessageFolderPendingSyncAction = 'FOLDER_DELETION' | 'NONE' +export type MessageFolderPendingSyncAction = 'FOLDER_DELETION' | 'FOLDER_IMPORT' | 'NONE' export interface CollectionHash { collectionName: AllMetadataName @@ -6467,7 +6467,7 @@ id: Scalars['UUID']} export interface UpdateMessageFolderInput {id: Scalars['UUID'],update: UpdateMessageFolderInputUpdates} -export interface UpdateMessageFolderInputUpdates {isSynced?: (Scalars['Boolean'] | null)} +export interface UpdateMessageFolderInputUpdates {isSynced: Scalars['Boolean']} export interface UpdateMessageFoldersInput {ids: Scalars['UUID'][],update: UpdateMessageFolderInputUpdates} @@ -9197,6 +9197,7 @@ export const enumCalendarChannelContactAutoCreationPolicy = { export const enumMessageFolderPendingSyncAction = { FOLDER_DELETION: 'FOLDER_DELETION' as const, + FOLDER_IMPORT: 'FOLDER_IMPORT' as const, NONE: 'NONE' as const } diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 7b78f95170..7e93128138 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -2380,6 +2380,7 @@ export enum MessageFolderImportPolicy { export enum MessageFolderPendingSyncAction { FOLDER_DELETION = 'FOLDER_DELETION', + FOLDER_IMPORT = 'FOLDER_IMPORT', NONE = 'NONE' } @@ -5345,7 +5346,7 @@ export type UpdateMessageFolderInput = { }; export type UpdateMessageFolderInputUpdates = { - isSynced?: InputMaybe; + isSynced: Scalars['Boolean']['input']; }; export type UpdateMessageFoldersInput = { @@ -7545,7 +7546,7 @@ export type UpdateMessageFoldersMutationVariables = Exact<{ }>; -export type UpdateMessageFoldersMutation = { __typename?: 'Mutation', updateMessageFolders: Array<{ __typename?: 'MessageFolder', id: string, isSynced: boolean }> }; +export type UpdateMessageFoldersMutation = { __typename?: 'Mutation', updateMessageFolders: Array<{ __typename?: 'MessageFolder', id: string, isSynced: boolean, pendingSyncAction: MessageFolderPendingSyncAction }> }; export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{ id: Scalars['UUID']['input']; @@ -7578,7 +7579,7 @@ export type MyMessageFoldersQueryVariables = Exact<{ }>; -export type MyMessageFoldersQuery = { __typename?: 'Query', myMessageFolders: Array<{ __typename?: 'MessageFolder', id: string, name?: string | null, isSynced: boolean, isSentFolder: boolean, parentFolderId?: string | null, externalId?: string | null, messageChannelId: string, createdAt: string, updatedAt: string }> }; +export type MyMessageFoldersQuery = { __typename?: 'Query', myMessageFolders: Array<{ __typename?: 'MessageFolder', id: string, name?: string | null, isSynced: boolean, isSentFolder: boolean, parentFolderId?: string | null, externalId?: string | null, pendingSyncAction: MessageFolderPendingSyncAction, messageChannelId: string, createdAt: string, updatedAt: string }> }; export type ApplicationRegistrationFragmentFragment = { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, logoUrl?: string | null, oAuthClientId: string, oAuthRedirectUris: Array, oAuthScopes: Array, sourceType: ApplicationRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, isListed: boolean, isFeatured: boolean, isPreInstalled: boolean, isConfigured: boolean, ownerWorkspaceId?: string | null, createdAt: string, updatedAt: string }; @@ -8779,12 +8780,12 @@ export const StartChannelSyncDocument = {"kind":"Document","definitions":[{"kind export const UpdateCalendarChannelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCalendarChannel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateCalendarChannelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateCalendarChannel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}}]}}]}}]} as unknown as DocumentNode; export const UpdateMessageChannelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageChannel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageChannelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageChannel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"excludeNonProfessionalEmails"}},{"kind":"Field","name":{"kind":"Name","value":"excludeGroupEmails"}},{"kind":"Field","name":{"kind":"Name","value":"messageFolderImportPolicy"}}]}}]}}]} as unknown as DocumentNode; export const UpdateMessageFolderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageFolder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageFolderInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageFolder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}}]}}]}}]} as unknown as DocumentNode; -export const UpdateMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageFoldersInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}}]}}]}}]} as unknown as DocumentNode; +export const UpdateMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageFoldersInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}},{"kind":"Field","name":{"kind":"Name","value":"pendingSyncAction"}}]}}]}}]} as unknown as DocumentNode; export const GetConnectedImapSmtpCaldavAccountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetConnectedImapSmtpCaldavAccount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getConnectedImapSmtpCaldavAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"connectionParameters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"IMAP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"SMTP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}}]}},{"kind":"Field","name":{"kind":"Name","value":"CALDAV"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const MyCalendarChannelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyCalendarChannels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myCalendarChannels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"syncStage"}},{"kind":"Field","name":{"kind":"Name","value":"syncStageStartedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"isSyncEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; export const MyConnectedAccountsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyConnectedAccounts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myConnectedAccounts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"authFailedAt"}},{"kind":"Field","name":{"kind":"Name","value":"archivedAt"}},{"kind":"Field","name":{"kind":"Name","value":"scopes"}},{"kind":"Field","name":{"kind":"Name","value":"handleAliases"}},{"kind":"Field","name":{"kind":"Name","value":"lastSignedInAt"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"connectionProviderId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"lastCredentialsRefreshedAt"}},{"kind":"Field","name":{"kind":"Name","value":"connectionParameters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"IMAP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"SMTP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"CALDAV"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; export const MyMessageChannelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyMessageChannels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myMessageChannels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"messageFolderImportPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"excludeNonProfessionalEmails"}},{"kind":"Field","name":{"kind":"Name","value":"excludeGroupEmails"}},{"kind":"Field","name":{"kind":"Name","value":"isSyncEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"syncStage"}},{"kind":"Field","name":{"kind":"Name","value":"syncStageStartedAt"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccount"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; -export const MyMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"messageChannelId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"messageChannelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"messageChannelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}},{"kind":"Field","name":{"kind":"Name","value":"isSentFolder"}},{"kind":"Field","name":{"kind":"Name","value":"parentFolderId"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"messageChannelId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; +export const MyMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"messageChannelId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"messageChannelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"messageChannelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}},{"kind":"Field","name":{"kind":"Name","value":"isSentFolder"}},{"kind":"Field","name":{"kind":"Name","value":"parentFolderId"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"pendingSyncAction"}},{"kind":"Field","name":{"kind":"Name","value":"messageChannelId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; export const DeleteApplicationRegistrationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteApplicationRegistration"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteApplicationRegistration"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode; export const RotateApplicationRegistrationClientSecretDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RotateApplicationRegistrationClientSecret"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rotateApplicationRegistrationClientSecret"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"clientSecret"}}]}}]}}]} as unknown as DocumentNode; export const TransferApplicationRegistrationOwnershipDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TransferApplicationRegistrationOwnership"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"targetWorkspaceSubdomain"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"transferApplicationRegistrationOwnership"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"targetWorkspaceSubdomain"},"value":{"kind":"Variable","name":{"kind":"Name","value":"targetWorkspaceSubdomain"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/accounts/types/MessageFolder.ts b/packages/twenty-front/src/modules/accounts/types/MessageFolder.ts index 2fcc332829..dbc9441b4a 100644 --- a/packages/twenty-front/src/modules/accounts/types/MessageFolder.ts +++ b/packages/twenty-front/src/modules/accounts/types/MessageFolder.ts @@ -1,3 +1,5 @@ +import { type MessageFolderPendingSyncAction } from 'twenty-shared/types'; + export type MessageFolder = { id: string; name: string; @@ -5,6 +7,7 @@ export type MessageFolder = { isSentFolder: boolean; parentFolderId: string | null; externalId: string | null; + pendingSyncAction: MessageFolderPendingSyncAction; messageChannelId: string; createdAt: string; updatedAt: string; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsMessageFoldersTreeItem.tsx b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsMessageFoldersTreeItem.tsx index 5216bcf7ad..f39ea373da 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsMessageFoldersTreeItem.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsMessageFoldersTreeItem.tsx @@ -8,6 +8,8 @@ import { isFolderTreePartiallySelected } from '@/settings/accounts/components/me import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; import { useState } from 'react'; +import { MessageFolderPendingSyncAction } from 'twenty-shared/types'; +import { Status } from 'twenty-ui/data-display'; import { IconChevronDown, IconChevronUp } from 'twenty-ui/icon'; import { Checkbox, CheckboxSize } from 'twenty-ui/input'; import { themeCssVariables } from 'twenty-ui/theme-constants'; @@ -176,6 +178,11 @@ export const SettingsMessageFoldersTreeItem = ({ + {folder.pendingSyncAction === + MessageFolderPendingSyncAction.FOLDER_IMPORT && ( + + )} + {hasChildren && ( <> {childCount} diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeFolderIdsForSyncToggle.test.ts b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeFolderIdsForSyncToggle.test.ts index 2b78704826..ead8f67432 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeFolderIdsForSyncToggle.test.ts +++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeFolderIdsForSyncToggle.test.ts @@ -1,5 +1,6 @@ import { type MessageFolder } from '@/accounts/types/MessageFolder'; import { computeFolderIdsForSyncToggle } from '@/settings/accounts/components/message-folders/utils/computeFolderIdsForSyncToggle'; +import { MessageFolderPendingSyncAction } from 'twenty-shared/types'; describe('computeFolderIdsForSyncToggle', () => { const createFolder = ({ @@ -22,6 +23,7 @@ describe('computeFolderIdsForSyncToggle', () => { externalId: externalId || id, isSentFolder: false, isSynced, + pendingSyncAction: MessageFolderPendingSyncAction.NONE, messageChannelId: 'channel-1', createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z', diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts index 03f564d997..d29a858719 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts +++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts @@ -1,5 +1,6 @@ import { type MessageFolder } from '@/accounts/types/MessageFolder'; import { computeMessageFolderTree } from '@/settings/accounts/components/message-folders/utils/computeMessageFolderTree'; +import { MessageFolderPendingSyncAction } from 'twenty-shared/types'; describe('computeMessageFolderTree', () => { const createFolder = ( @@ -15,6 +16,7 @@ describe('computeMessageFolderTree', () => { externalId: externalId || id, isSentFolder: false, isSynced: false, + pendingSyncAction: MessageFolderPendingSyncAction.NONE, messageChannelId: '20202020-1c25-4d02-bf25-6aeccf7ea419', createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z', diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageFolders.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageFolders.ts index f1327626bf..33b54dd02c 100644 --- a/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageFolders.ts +++ b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageFolders.ts @@ -5,6 +5,7 @@ export const UPDATE_MESSAGE_FOLDERS = gql` updateMessageFolders(input: $input) { id isSynced + pendingSyncAction } } `; diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyMessageFolders.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyMessageFolders.ts index 029d0c1f2d..c6647b24ea 100644 --- a/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyMessageFolders.ts +++ b/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyMessageFolders.ts @@ -9,6 +9,7 @@ export const GET_MY_MESSAGE_FOLDERS = gql` isSentFolder parentFolderId externalId + pendingSyncAction messageChannelId createdAt updatedAt diff --git a/packages/twenty-front/src/modules/settings/accounts/hooks/useUpdateMessageFoldersSyncStatus.ts b/packages/twenty-front/src/modules/settings/accounts/hooks/useUpdateMessageFoldersSyncStatus.ts index ab4652c658..1b3c7ba842 100644 --- a/packages/twenty-front/src/modules/settings/accounts/hooks/useUpdateMessageFoldersSyncStatus.ts +++ b/packages/twenty-front/src/modules/settings/accounts/hooks/useUpdateMessageFoldersSyncStatus.ts @@ -2,6 +2,7 @@ import { useCallback } from 'react'; import { UPDATE_MESSAGE_FOLDERS } from '@/settings/accounts/graphql/mutations/updateMessageFolders'; import { useApolloClient } from '@apollo/client/react'; +import { MessageFolderPendingSyncAction } from 'twenty-shared/types'; type UpdateMessageFoldersSyncStatusArgs = { messageFolderIds: string[]; @@ -33,6 +34,7 @@ export const useUpdateMessageFoldersSyncStatus = () => { __typename: 'MessageFolder', id, isSynced, + pendingSyncAction: MessageFolderPendingSyncAction.NONE, })), }, }); diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-15/2-15-instance-command-fast-1781714499016-add-folder-import-to-message-folder-pending-sync-action.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-15/2-15-instance-command-fast-1781714499016-add-folder-import-to-message-folder-pending-sync-action.ts new file mode 100644 index 0000000000..9fe85335df --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-15/2-15-instance-command-fast-1781714499016-add-folder-import-to-message-folder-pending-sync-action.ts @@ -0,0 +1,25 @@ +import { QueryRunner } from 'typeorm'; + +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface'; + +@RegisteredInstanceCommand('2.15.0', 1781714499016) +export class AddFolderImportToMessageFolderPendingSyncActionFastInstanceCommand implements FastInstanceCommand { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TYPE "core"."messageFolder_pendingsyncaction_enum" RENAME TO "messageFolder_pendingsyncaction_enum_old"'); + await queryRunner.query('CREATE TYPE "core"."messageFolder_pendingsyncaction_enum" AS ENUM(\'FOLDER_DELETION\', \'FOLDER_IMPORT\', \'NONE\')'); + await queryRunner.query('ALTER TABLE "core"."messageFolder" ALTER COLUMN "pendingSyncAction" DROP DEFAULT'); + await queryRunner.query('ALTER TABLE "core"."messageFolder" ALTER COLUMN "pendingSyncAction" TYPE "core"."messageFolder_pendingsyncaction_enum" USING "pendingSyncAction"::"text"::"core"."messageFolder_pendingsyncaction_enum"'); + await queryRunner.query('ALTER TABLE "core"."messageFolder" ALTER COLUMN "pendingSyncAction" SET DEFAULT \'NONE\''); + await queryRunner.query('DROP TYPE "core"."messageFolder_pendingsyncaction_enum_old"'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('CREATE TYPE "core"."messageFolder_pendingsyncaction_enum_old" AS ENUM(\'FOLDER_DELETION\', \'NONE\')'); + await queryRunner.query('ALTER TABLE "core"."messageFolder" ALTER COLUMN "pendingSyncAction" DROP DEFAULT'); + await queryRunner.query('ALTER TABLE "core"."messageFolder" ALTER COLUMN "pendingSyncAction" TYPE "core"."messageFolder_pendingsyncaction_enum_old" USING (CASE WHEN "pendingSyncAction"::"text" = \'FOLDER_IMPORT\' THEN \'NONE\' ELSE "pendingSyncAction"::"text" END)::"core"."messageFolder_pendingsyncaction_enum_old"'); + await queryRunner.query('ALTER TABLE "core"."messageFolder" ALTER COLUMN "pendingSyncAction" SET DEFAULT \'NONE\''); + await queryRunner.query('DROP TYPE "core"."messageFolder_pendingsyncaction_enum"'); + await queryRunner.query('ALTER TYPE "core"."messageFolder_pendingsyncaction_enum_old" RENAME TO "messageFolder_pendingsyncaction_enum"'); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts index dcf29fcdce..7aed346e28 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts @@ -73,6 +73,7 @@ import { AddLogicFunctionExecutionModeFastInstanceCommand } from 'src/database/c import { EncryptNonSecretApplicationVariableSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-slow-1798400000000-encrypt-non-secret-application-variable'; import { MigrateAiModelPreferencesSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-slow-1799000010000-migrate-ai-model-preferences'; import { AddHasPaymentMethodToBillingCustomerFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-15/2-15-instance-command-fast-1781280240009-add-has-payment-method-to-billing-customer'; +import { AddFolderImportToMessageFolderPendingSyncActionFastInstanceCommand } from './2-15/2-15-instance-command-fast-1781714499016-add-folder-import-to-message-folder-pending-sync-action'; export const INSTANCE_COMMANDS = [ AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand, @@ -148,4 +149,5 @@ export const INSTANCE_COMMANDS = [ SetTableWidgetViewsVisibilityToWorkspaceSlowInstanceCommand, AddIsSystemSideEffectFastInstanceCommand, BackfillConnectionSecuritySlowInstanceCommand, + AddFolderImportToMessageFolderPendingSyncActionFastInstanceCommand, ]; diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/update-message-folder.input.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/update-message-folder.input.ts index 596806dfc5..4dfdb255a2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/update-message-folder.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/update-message-folder.input.ts @@ -1,22 +1,15 @@ import { Field, InputType } from '@nestjs/graphql'; import { Type } from 'class-transformer'; -import { - IsBoolean, - IsNotEmpty, - IsOptional, - IsUUID, - ValidateNested, -} from 'class-validator'; +import { IsBoolean, IsNotEmpty, IsUUID, ValidateNested } from 'class-validator'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @InputType() export class UpdateMessageFolderInputUpdates { - @IsOptional() @IsBoolean() - @Field({ nullable: true }) - isSynced?: boolean; + @Field() + isSynced: boolean; } @InputType() diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder-metadata.service.ts index 1933b95925..8d6e98174e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder-metadata.service.ts @@ -3,7 +3,10 @@ import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; -import { MessageFolderPendingSyncAction } from 'twenty-shared/types'; +import { + ConnectedAccountProvider, + MessageFolderPendingSyncAction, +} from 'twenty-shared/types'; import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service'; import { MessageFolderDTO } from 'src/engine/metadata-modules/message-folder/dtos/message-folder.dto'; @@ -183,7 +186,7 @@ export class MessageFolderMetadataService { return this.repository.findOneOrFail({ where: { id, workspaceId } }); } - async updateMany({ + async setSyncStatus({ ids, workspaceId, data, @@ -192,10 +195,53 @@ export class MessageFolderMetadataService { workspaceId: string; data: Partial; }): Promise { - await this.repository.update( - { id: In(ids), workspaceId }, - data as Record, - ); + await this.repository.manager.transaction(async (manager) => { + if (!data.isSynced) { + await manager.update( + MessageFolderEntity, + { id: In(ids), workspaceId }, + { isSynced: false }, + ); + await manager.update( + MessageFolderEntity, + { + id: In(ids), + workspaceId, + pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_IMPORT, + }, + { pendingSyncAction: MessageFolderPendingSyncAction.NONE }, + ); + + return; + } + + const folderIdsToBackfill = ( + await manager.find(MessageFolderEntity, { + where: { + id: In(ids), + workspaceId, + isSynced: false, + messageChannel: { + connectedAccount: { provider: ConnectedAccountProvider.GOOGLE }, + }, + }, + }) + ).map((folder) => folder.id); + + await manager.update( + MessageFolderEntity, + { id: In(ids), workspaceId }, + { isSynced: true }, + ); + + if (folderIdsToBackfill.length > 0) { + await manager.update( + MessageFolderEntity, + { id: In(folderIdsToBackfill), workspaceId }, + { pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_IMPORT }, + ); + } + }); return this.repository.find({ where: { id: In(ids), workspaceId } }); } diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/resolvers/message-folder.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/resolvers/message-folder.resolver.ts index e0660cc6f6..6958c7e512 100644 --- a/packages/twenty-server/src/engine/metadata-modules/message-folder/resolvers/message-folder.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/resolvers/message-folder.resolver.ts @@ -86,7 +86,7 @@ export class MessageFolderResolver { ), ); - return this.messageFolderMetadataService.updateMany({ + return this.messageFolderMetadataService.setSyncStatus({ ids: input.ids, workspaceId: workspace.id, data: input.update, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.ts index 17de4c99ed..ea6a83aa50 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.ts @@ -28,7 +28,7 @@ export class GmailGetMessageListService { private readonly gmailMessageListFetchErrorHandler: GmailMessageListFetchErrorHandler, ) {} - private async getMessageListWithoutCursor( + async getMessageListWithoutCursor( connectedAccount: Pick< ConnectedAccountEntity, 'provider' | 'id' | 'handle' diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts index a39f91e7c0..693806ffb8 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts @@ -18,7 +18,6 @@ import { RefreshTokensManagerModule } from 'src/modules/connected-account/refres import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; import { MessagingMessageCleanerModule } from 'src/modules/messaging/message-cleaner/messaging-message-cleaner.module'; import { MessagingFolderSyncManagerModule } from 'src/modules/messaging/message-folder-manager/messaging-folder-sync-manager.module'; -import { MessagingSingleMessageImportCommand } from 'src/modules/messaging/message-import-manager/commands/messaging-single-message-import.command'; import { MessagingTriggerMessageListFetchCommand } from 'src/modules/messaging/message-import-manager/commands/messaging-trigger-message-list-fetch.command'; import { MessagingMessageListFetchCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-message-list-fetch.cron.command'; import { MessagingMessagesImportCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-messages-import.cron.command'; @@ -34,7 +33,6 @@ import { MessagingInboundEmailDriverModule } from 'src/modules/messaging/message import { InboundEmailImportService } from 'src/modules/messaging/message-import-manager/drivers/inbound-email/services/inbound-email-import.service'; import { MessagingMicrosoftDriverModule } from 'src/modules/messaging/message-import-manager/drivers/microsoft/messaging-microsoft-driver.module'; import { MessagingSmtpDriverModule } from 'src/modules/messaging/message-import-manager/drivers/smtp/messaging-smtp-driver.module'; -import { MessagingAddSingleMessageToCacheForImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-add-single-message-to-cache-for-import.job'; import { MessagingCleanCacheJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-clean-cache'; import { MessagingInboundEmailImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-inbound-email-import.job'; import { MessagingMessageListFetchJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job'; @@ -44,6 +42,7 @@ import { MessagingRelaunchFailedMessageChannelJob } from 'src/modules/messaging/ import { MessagingCursorService } from 'src/modules/messaging/message-import-manager/services/messaging-cursor.service'; import { MessagingDeleteFolderMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-delete-folder-messages.service'; import { MessagingDeleteGroupEmailMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-delete-group-email-messages.service'; +import { MessagingImportFolderMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-import-folder-messages.service'; import { MessagingGetMessageListService } from 'src/modules/messaging/message-import-manager/services/messaging-get-message-list.service'; import { MessagingGetMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-get-messages.service'; import { MessageImportExceptionHandlerService } from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service'; @@ -92,7 +91,6 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess MessagingMessagesImportCronCommand, MessagingOngoingStaleCronCommand, MessagingRelaunchFailedMessageChannelsCronCommand, - MessagingSingleMessageImportCommand, MessagingTriggerMessageListFetchCommand, MessagingMessageListFetchJob, MessagingMessagesImportJob, @@ -102,7 +100,6 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess MessagingMessagesImportCronJob, MessagingOngoingStaleCronJob, MessagingRelaunchFailedMessageChannelsCronJob, - MessagingAddSingleMessageToCacheForImportJob, MessagingCleanCacheJob, MessagingInboundEmailImportJob, MessagingMessageService, @@ -117,6 +114,7 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess MessagingProcessFolderActionsService, MessagingProcessGroupEmailActionsService, MessagingDeleteFolderMessagesService, + MessagingImportFolderMessagesService, MessagingDeleteGroupEmailMessagesService, InboundEmailImportService, ], diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-folder-messages.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-folder-messages.service.ts new file mode 100644 index 0000000000..ffccee41ce --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-folder-messages.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@nestjs/common'; + +import { + ConnectedAccountProvider, + MessageFolderImportPolicy, +} from 'twenty-shared/types'; + +import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity'; +import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity'; +import { GmailGetMessageListService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service'; + +@Injectable() +export class MessagingImportFolderMessagesService { + constructor( + private readonly gmailGetMessageListService: GmailGetMessageListService, + ) {} + + async getFolderMessageIdsToImport( + messageChannel: MessageChannelEntity, + messageFolder: MessageFolderEntity, + ): Promise { + switch (messageChannel.connectedAccount.provider) { + case ConnectedAccountProvider.GOOGLE: { + const foldersScopedToImportedFolder = messageChannel.messageFolders.map( + (folder) => ({ + name: folder.name, + externalId: folder.externalId, + parentFolderId: folder.parentFolderId, + isSynced: folder.id === messageFolder.id, + }), + ); + + const [messageList] = + await this.gmailGetMessageListService.getMessageListWithoutCursor( + messageChannel.connectedAccount, + foldersScopedToImportedFolder, + { + messageFolderImportPolicy: + MessageFolderImportPolicy.SELECTED_FOLDERS, + }, + ); + + return messageList?.messageExternalIds ?? []; + } + default: + return []; + } + } +} diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service.ts index 46a03a7105..48d1ab64bb 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service.ts @@ -27,7 +27,10 @@ import { MessageImportSyncStep, } from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service'; import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service'; -import { MessagingProcessFolderActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service'; +import { + MessagingProcessFolderActionsService, + type ProcessFolderActionsResult, +} from 'src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service'; import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service'; import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity'; @@ -68,7 +71,7 @@ export class MessagingMessageListFetchService { workspaceId, ); - const pendingFolderActionsProcessed = + const processedfolderActionsResult = await this.processPendingFolderActions(messageChannel, workspaceId); await this.messageChannelSyncStatusService.markAsMessagesListFetchOngoing( @@ -81,7 +84,8 @@ export class MessagingMessageListFetchService { ); const freshMessageChannel = - pendingGroupEmailActionsProcessed || pendingFolderActionsProcessed + pendingGroupEmailActionsProcessed || + isDefined(processedfolderActionsResult) ? await this.messageChannelRepository.findOne({ where: { id: messageChannel.id, @@ -120,9 +124,12 @@ export class MessagingMessageListFetchService { `messages-to-import:${workspaceId}:${freshMessageChannel.id}`, ); - const messageExternalIds = messageLists.flatMap( - (messageList) => messageList.messageExternalIds, - ); + const messageExternalIds = [ + ...messageLists.flatMap( + (messageList) => messageList.messageExternalIds, + ), + ...(processedfolderActionsResult?.messageExternalIdsToImport ?? []), + ]; const messageExternalIdsToDelete = messageLists.flatMap( (messageList) => messageList.messageExternalIdsToDelete, @@ -312,7 +319,7 @@ export class MessagingMessageListFetchService { private async processPendingFolderActions( messageChannel: MessageChannelEntity, workspaceId: string, - ): Promise { + ): Promise { const foldersWithPendingActions = messageChannel.messageFolders.filter( (folder) => isDefined(folder.pendingSyncAction) && @@ -320,20 +327,18 @@ export class MessagingMessageListFetchService { ); if (foldersWithPendingActions.length === 0) { - return false; + return null; } this.logger.log( `messageChannelId: ${messageChannel.id} Processing pending folder actions before message list fetch`, ); - await this.messagingProcessFolderActionsService.processFolderActions( + return this.messagingProcessFolderActionsService.processFolderActions( messageChannel, foldersWithPendingActions, workspaceId, ); - - return true; } private async computeFullSyncMessageChannelMessageAssociationsToDelete( diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service.ts index 3ca45eb6d6..bfbf3f5765 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service.ts @@ -10,6 +10,11 @@ import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspac import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { MessageFolderPendingSyncAction } from 'twenty-shared/types'; import { MessagingDeleteFolderMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-delete-folder-messages.service'; +import { MessagingImportFolderMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-import-folder-messages.service'; + +export type ProcessFolderActionsResult = { + messageExternalIdsToImport: string[]; +}; @Injectable() export class MessagingProcessFolderActionsService { @@ -22,13 +27,14 @@ export class MessagingProcessFolderActionsService { @InjectRepository(MessageFolderEntity) private readonly messageFolderRepository: Repository, private readonly messagingDeleteFolderMessagesService: MessagingDeleteFolderMessagesService, + private readonly messagingImportFolderMessagesService: MessagingImportFolderMessagesService, ) {} async processFolderActions( messageChannel: MessageChannelEntity, messageFolders: MessageFolderEntity[], workspaceId: string, - ): Promise { + ): Promise { const foldersWithPendingActions = messageFolders.filter( (folder) => isDefined(folder.pendingSyncAction) && @@ -36,13 +42,14 @@ export class MessagingProcessFolderActionsService { ); if (foldersWithPendingActions.length === 0) { - return; + return { messageExternalIdsToImport: [] }; } this.logger.log( `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Processing ${foldersWithPendingActions.length} folders with pending actions`, ); + const messageExternalIdsToImport: string[] = []; const folderIdsToDelete: string[] = []; const processedFolderIds: string[] = []; const failedFolderIds: Array<{ folderId: string; error: Error }> = []; @@ -53,21 +60,37 @@ export class MessagingProcessFolderActionsService { `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${folder.id} - Processing folder action: ${folder.pendingSyncAction}`, ); - if ( - folder.pendingSyncAction === - MessageFolderPendingSyncAction.FOLDER_DELETION - ) { - await this.messagingDeleteFolderMessagesService.deleteFolderMessages( - workspaceId, - messageChannel, - folder, - ); + switch (folder.pendingSyncAction) { + case MessageFolderPendingSyncAction.FOLDER_DELETION: { + await this.messagingDeleteFolderMessagesService.deleteFolderMessages( + workspaceId, + messageChannel, + folder, + ); - folderIdsToDelete.push(folder.id); + folderIdsToDelete.push(folder.id); - this.logger.debug( - `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${folder.id} - Completed FOLDER_DELETION action`, - ); + this.logger.debug( + `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${folder.id} - Completed FOLDER_DELETION action`, + ); + break; + } + case MessageFolderPendingSyncAction.FOLDER_IMPORT: { + const folderMessageExternalIdsToImport = + await this.messagingImportFolderMessagesService.getFolderMessageIdsToImport( + messageChannel, + folder, + ); + + messageExternalIdsToImport.push( + ...folderMessageExternalIdsToImport, + ); + + this.logger.debug( + `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${folder.id} - Completed FOLDER_IMPORT action`, + ); + break; + } } processedFolderIds.push(folder.id); @@ -117,5 +140,9 @@ export class MessagingProcessFolderActionsService { { lite: true }, ); } + + return { + messageExternalIdsToImport: [...new Set(messageExternalIdsToImport)], + }; } } diff --git a/packages/twenty-shared/src/types/MessageFolderPendingSyncAction.ts b/packages/twenty-shared/src/types/MessageFolderPendingSyncAction.ts index 9e24eeba8e..f3c9e7396d 100644 --- a/packages/twenty-shared/src/types/MessageFolderPendingSyncAction.ts +++ b/packages/twenty-shared/src/types/MessageFolderPendingSyncAction.ts @@ -1,4 +1,5 @@ export enum MessageFolderPendingSyncAction { FOLDER_DELETION = 'FOLDER_DELETION', + FOLDER_IMPORT = 'FOLDER_IMPORT', NONE = 'NONE', }