From 76f69efb437d6d5b658e7a826cc8750a0aee2904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Sat, 13 Jun 2026 15:52:13 +0200 Subject: [PATCH] Keep synced messages and events when removing a workspace member (#21443) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context Removing a workspace member deletes their connected accounts, which cascades into deleting every message and calendar event those accounts synced. For a CRM, losing the email history of departed teammates is a big deal. ## What this does Connected accounts are now kept and reassigned instead of deleted when a member is removed: - Ownership moves to the acting user (whoever removed the member). When members remove themselves (leave workspace, account deletion), it falls back to the oldest admin. - OAuth tokens are revoked, credentials wiped, message/calendar channels get `isSyncEnabled = false`, and the account is stamped with a new `archivedAt` column (fast instance command included). - Synced messages, threads and calendar events stay in the workspace. Channel visibility settings keep applying as before, since channels and associations survive. - The reassigned account appears in the new owner's Settings → Accounts, where it can still be deleted (with its data) like any other account. The transfer happens synchronously during removal, while the member's userWorkspace row still exists. This also removes `DeleteWorkspaceMemberConnectedAccountsCleanupJob` and its listener: the async job had to reconstruct the account-owner link from rows the removal flow had just deleted, which was race-prone (see 2181fb541e). Archived accounts are excluded from the workflow send-email default account resolution, and both removal confirmation modals now mention what happens to synced data. --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- .../src/metadata/generated/schema.graphql | 65 ++--- .../src/metadata/generated/schema.ts | 196 ++++++------- .../src/metadata/generated/types.ts | 261 +++++++++--------- .../src/generated-metadata/graphql.ts | 5 +- packages/twenty-front/src/locales/en.po | 20 +- .../accounts/types/ConnectedAccount.ts | 3 + .../__stories__/CommandMenu.stories.tsx | 4 + ...untsConnectedAccountsRowRightContainer.tsx | 14 + .../graphql/queries/getMyConnectedAccounts.ts | 1 + .../profile/components/DeleteAccount.tsx | 2 +- .../WorkflowEditActionEmailBase.stories.tsx | 1 + .../SettingsAccountsCalendars.stories.tsx | 1 + .../SettingsAccountsEmails.stories.tsx | 1 + .../members/SettingsWorkspaceMember.tsx | 2 +- ...00-add-archived-at-to-connected-account.ts | 21 ++ .../instance-commands.constant.ts | 2 + .../email-tool/email-composer.service.ts | 4 +- .../user/services/user.service.spec.ts | 15 +- .../user/services/user.service.ts | 72 +++++ .../engine/core-modules/user/user.module.ts | 2 + .../engine/core-modules/user/user.resolver.ts | 1 + .../connected-account-metadata.service.ts | 57 +++- .../dtos/connected-account.dto.ts | 7 + .../entities/connected-account.entity.ts | 3 + .../connected-account.module.ts | 9 +- ...workspace-member-connected-accounts.job.ts | 67 ----- ...ected-account-workspace-member.listener.ts | 47 ---- 27 files changed, 485 insertions(+), 398 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-13/2-13-instance-command-fast-1781171103000-add-archived-at-to-connected-account.ts delete mode 100644 packages/twenty-server/src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job.ts delete mode 100644 packages/twenty-server/src/modules/connected-account/listeners/connected-account-workspace-member.listener.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 15e2ee8667..d6fc69b75b 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -2246,6 +2246,39 @@ type LogicFunctionLogs { logs: String! } +type PublicConnectionParametersOutput { + host: String! + port: Float! + username: String + secure: Boolean +} + +type PublicImapSmtpCaldavConnectionParameters { + IMAP: PublicConnectionParametersOutput + SMTP: PublicConnectionParametersOutput + CALDAV: PublicConnectionParametersOutput +} + +type ConnectedAccountPublicDTO { + id: UUID! + handle: String! + provider: String! + lastCredentialsRefreshedAt: DateTime + authFailedAt: DateTime + archivedAt: DateTime + handleAliases: [String!] + scopes: [String!] + lastSignedInAt: DateTime + userWorkspaceId: UUID! + connectionProviderId: UUID + applicationId: UUID + name: String + visibility: String! + createdAt: DateTime! + updatedAt: DateTime! + connectionParameters: PublicImapSmtpCaldavConnectionParameters +} + type DeleteTwoFactorAuthenticationMethod { """Boolean that confirms query was dispatched""" success: Boolean! @@ -2428,38 +2461,6 @@ type PlaceDetailsResult { location: Location } -type PublicConnectionParametersOutput { - host: String! - port: Float! - username: String - secure: Boolean -} - -type PublicImapSmtpCaldavConnectionParameters { - IMAP: PublicConnectionParametersOutput - SMTP: PublicConnectionParametersOutput - CALDAV: PublicConnectionParametersOutput -} - -type ConnectedAccountPublicDTO { - id: UUID! - handle: String! - provider: String! - lastCredentialsRefreshedAt: DateTime - authFailedAt: DateTime - handleAliases: [String!] - scopes: [String!] - lastSignedInAt: DateTime - userWorkspaceId: UUID! - connectionProviderId: UUID - applicationId: UUID - name: String - visibility: String! - createdAt: DateTime! - updatedAt: DateTime! - connectionParameters: PublicImapSmtpCaldavConnectionParameters -} - type ImapSmtpCaldavPublicConnectionParams { host: String! port: Float! diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 6258afc4dc..7fafbd8ffc 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -1890,6 +1890,42 @@ export interface LogicFunctionLogs { __typename: 'LogicFunctionLogs' } +export interface PublicConnectionParametersOutput { + host: Scalars['String'] + port: Scalars['Float'] + username?: Scalars['String'] + secure?: Scalars['Boolean'] + __typename: 'PublicConnectionParametersOutput' +} + +export interface PublicImapSmtpCaldavConnectionParameters { + IMAP?: PublicConnectionParametersOutput + SMTP?: PublicConnectionParametersOutput + CALDAV?: PublicConnectionParametersOutput + __typename: 'PublicImapSmtpCaldavConnectionParameters' +} + +export interface ConnectedAccountPublicDTO { + id: Scalars['UUID'] + handle: Scalars['String'] + provider: Scalars['String'] + lastCredentialsRefreshedAt?: Scalars['DateTime'] + authFailedAt?: Scalars['DateTime'] + archivedAt?: Scalars['DateTime'] + handleAliases?: Scalars['String'][] + scopes?: Scalars['String'][] + lastSignedInAt?: Scalars['DateTime'] + userWorkspaceId: Scalars['UUID'] + connectionProviderId?: Scalars['UUID'] + applicationId?: Scalars['UUID'] + name?: Scalars['String'] + visibility: Scalars['String'] + createdAt: Scalars['DateTime'] + updatedAt: Scalars['DateTime'] + connectionParameters?: PublicImapSmtpCaldavConnectionParameters + __typename: 'ConnectedAccountPublicDTO' +} + export interface DeleteTwoFactorAuthenticationMethod { /** Boolean that confirms query was dispatched */ success: Scalars['Boolean'] @@ -2104,41 +2140,6 @@ export interface PlaceDetailsResult { __typename: 'PlaceDetailsResult' } -export interface PublicConnectionParametersOutput { - host: Scalars['String'] - port: Scalars['Float'] - username?: Scalars['String'] - secure?: Scalars['Boolean'] - __typename: 'PublicConnectionParametersOutput' -} - -export interface PublicImapSmtpCaldavConnectionParameters { - IMAP?: PublicConnectionParametersOutput - SMTP?: PublicConnectionParametersOutput - CALDAV?: PublicConnectionParametersOutput - __typename: 'PublicImapSmtpCaldavConnectionParameters' -} - -export interface ConnectedAccountPublicDTO { - id: Scalars['UUID'] - handle: Scalars['String'] - provider: Scalars['String'] - lastCredentialsRefreshedAt?: Scalars['DateTime'] - authFailedAt?: Scalars['DateTime'] - handleAliases?: Scalars['String'][] - scopes?: Scalars['String'][] - lastSignedInAt?: Scalars['DateTime'] - userWorkspaceId: Scalars['UUID'] - connectionProviderId?: Scalars['UUID'] - applicationId?: Scalars['UUID'] - name?: Scalars['String'] - visibility: Scalars['String'] - createdAt: Scalars['DateTime'] - updatedAt: Scalars['DateTime'] - connectionParameters?: PublicImapSmtpCaldavConnectionParameters - __typename: 'ConnectedAccountPublicDTO' -} - export interface ImapSmtpCaldavPublicConnectionParams { host: Scalars['String'] port: Scalars['Float'] @@ -4894,6 +4895,45 @@ export interface LogicFunctionLogsGenqlSelection{ __scalar?: boolean | number } +export interface PublicConnectionParametersOutputGenqlSelection{ + host?: boolean | number + port?: boolean | number + username?: boolean | number + secure?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + +export interface PublicImapSmtpCaldavConnectionParametersGenqlSelection{ + IMAP?: PublicConnectionParametersOutputGenqlSelection + SMTP?: PublicConnectionParametersOutputGenqlSelection + CALDAV?: PublicConnectionParametersOutputGenqlSelection + __typename?: boolean | number + __scalar?: boolean | number +} + +export interface ConnectedAccountPublicDTOGenqlSelection{ + id?: boolean | number + handle?: boolean | number + provider?: boolean | number + lastCredentialsRefreshedAt?: boolean | number + authFailedAt?: boolean | number + archivedAt?: boolean | number + handleAliases?: boolean | number + scopes?: boolean | number + lastSignedInAt?: boolean | number + userWorkspaceId?: boolean | number + connectionProviderId?: boolean | number + applicationId?: boolean | number + name?: boolean | number + visibility?: boolean | number + createdAt?: boolean | number + updatedAt?: boolean | number + connectionParameters?: PublicImapSmtpCaldavConnectionParametersGenqlSelection + __typename?: boolean | number + __scalar?: boolean | number +} + export interface DeleteTwoFactorAuthenticationMethodGenqlSelection{ /** Boolean that confirms query was dispatched */ success?: boolean | number @@ -5140,44 +5180,6 @@ export interface PlaceDetailsResultGenqlSelection{ __scalar?: boolean | number } -export interface PublicConnectionParametersOutputGenqlSelection{ - host?: boolean | number - port?: boolean | number - username?: boolean | number - secure?: boolean | number - __typename?: boolean | number - __scalar?: boolean | number -} - -export interface PublicImapSmtpCaldavConnectionParametersGenqlSelection{ - IMAP?: PublicConnectionParametersOutputGenqlSelection - SMTP?: PublicConnectionParametersOutputGenqlSelection - CALDAV?: PublicConnectionParametersOutputGenqlSelection - __typename?: boolean | number - __scalar?: boolean | number -} - -export interface ConnectedAccountPublicDTOGenqlSelection{ - id?: boolean | number - handle?: boolean | number - provider?: boolean | number - lastCredentialsRefreshedAt?: boolean | number - authFailedAt?: boolean | number - handleAliases?: boolean | number - scopes?: boolean | number - lastSignedInAt?: boolean | number - userWorkspaceId?: boolean | number - connectionProviderId?: boolean | number - applicationId?: boolean | number - name?: boolean | number - visibility?: boolean | number - createdAt?: boolean | number - updatedAt?: boolean | number - connectionParameters?: PublicImapSmtpCaldavConnectionParametersGenqlSelection - __typename?: boolean | number - __scalar?: boolean | number -} - export interface ImapSmtpCaldavPublicConnectionParamsGenqlSelection{ host?: boolean | number port?: boolean | number @@ -7721,6 +7723,30 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null + const PublicConnectionParametersOutput_possibleTypes: string[] = ['PublicConnectionParametersOutput'] + export const isPublicConnectionParametersOutput = (obj?: { __typename?: any } | null): obj is PublicConnectionParametersOutput => { + if (!obj?.__typename) throw new Error('__typename is missing in "isPublicConnectionParametersOutput"') + return PublicConnectionParametersOutput_possibleTypes.includes(obj.__typename) + } + + + + const PublicImapSmtpCaldavConnectionParameters_possibleTypes: string[] = ['PublicImapSmtpCaldavConnectionParameters'] + export const isPublicImapSmtpCaldavConnectionParameters = (obj?: { __typename?: any } | null): obj is PublicImapSmtpCaldavConnectionParameters => { + if (!obj?.__typename) throw new Error('__typename is missing in "isPublicImapSmtpCaldavConnectionParameters"') + return PublicImapSmtpCaldavConnectionParameters_possibleTypes.includes(obj.__typename) + } + + + + const ConnectedAccountPublicDTO_possibleTypes: string[] = ['ConnectedAccountPublicDTO'] + export const isConnectedAccountPublicDTO = (obj?: { __typename?: any } | null): obj is ConnectedAccountPublicDTO => { + if (!obj?.__typename) throw new Error('__typename is missing in "isConnectedAccountPublicDTO"') + return ConnectedAccountPublicDTO_possibleTypes.includes(obj.__typename) + } + + + const DeleteTwoFactorAuthenticationMethod_possibleTypes: string[] = ['DeleteTwoFactorAuthenticationMethod'] export const isDeleteTwoFactorAuthenticationMethod = (obj?: { __typename?: any } | null): obj is DeleteTwoFactorAuthenticationMethod => { if (!obj?.__typename) throw new Error('__typename is missing in "isDeleteTwoFactorAuthenticationMethod"') @@ -7977,30 +8003,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null - const PublicConnectionParametersOutput_possibleTypes: string[] = ['PublicConnectionParametersOutput'] - export const isPublicConnectionParametersOutput = (obj?: { __typename?: any } | null): obj is PublicConnectionParametersOutput => { - if (!obj?.__typename) throw new Error('__typename is missing in "isPublicConnectionParametersOutput"') - return PublicConnectionParametersOutput_possibleTypes.includes(obj.__typename) - } - - - - const PublicImapSmtpCaldavConnectionParameters_possibleTypes: string[] = ['PublicImapSmtpCaldavConnectionParameters'] - export const isPublicImapSmtpCaldavConnectionParameters = (obj?: { __typename?: any } | null): obj is PublicImapSmtpCaldavConnectionParameters => { - if (!obj?.__typename) throw new Error('__typename is missing in "isPublicImapSmtpCaldavConnectionParameters"') - return PublicImapSmtpCaldavConnectionParameters_possibleTypes.includes(obj.__typename) - } - - - - const ConnectedAccountPublicDTO_possibleTypes: string[] = ['ConnectedAccountPublicDTO'] - export const isConnectedAccountPublicDTO = (obj?: { __typename?: any } | null): obj is ConnectedAccountPublicDTO => { - if (!obj?.__typename) throw new Error('__typename is missing in "isConnectedAccountPublicDTO"') - return ConnectedAccountPublicDTO_possibleTypes.includes(obj.__typename) - } - - - const ImapSmtpCaldavPublicConnectionParams_possibleTypes: string[] = ['ImapSmtpCaldavPublicConnectionParams'] export const isImapSmtpCaldavPublicConnectionParams = (obj?: { __typename?: any } | null): obj is ImapSmtpCaldavPublicConnectionParams => { if (!obj?.__typename) throw new Error('__typename is missing in "isImapSmtpCaldavPublicConnectionParams"') diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index 04d06603bd..ec1de81611 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -4376,6 +4376,93 @@ export default { 1 ] }, + "PublicConnectionParametersOutput": { + "host": [ + 1 + ], + "port": [ + 11 + ], + "username": [ + 1 + ], + "secure": [ + 6 + ], + "__typename": [ + 1 + ] + }, + "PublicImapSmtpCaldavConnectionParameters": { + "IMAP": [ + 236 + ], + "SMTP": [ + 236 + ], + "CALDAV": [ + 236 + ], + "__typename": [ + 1 + ] + }, + "ConnectedAccountPublicDTO": { + "id": [ + 3 + ], + "handle": [ + 1 + ], + "provider": [ + 1 + ], + "lastCredentialsRefreshedAt": [ + 4 + ], + "authFailedAt": [ + 4 + ], + "archivedAt": [ + 4 + ], + "handleAliases": [ + 1 + ], + "scopes": [ + 1 + ], + "lastSignedInAt": [ + 4 + ], + "userWorkspaceId": [ + 3 + ], + "connectionProviderId": [ + 3 + ], + "applicationId": [ + 3 + ], + "name": [ + 1 + ], + "visibility": [ + 1 + ], + "createdAt": [ + 4 + ], + "updatedAt": [ + 4 + ], + "connectionParameters": [ + 237 + ], + "__typename": [ + 1 + ] + }, "DeleteTwoFactorAuthenticationMethod": { "success": [ 6 @@ -4421,7 +4508,7 @@ export default { }, "AvailableWorkspacesAndAccessTokens": { "tokens": [ - 240 + 243 ], "availableWorkspaces": [ 228 @@ -4476,7 +4563,7 @@ export default { 32 ], "workspace": [ - 245 + 248 ], "__typename": [ 1 @@ -4525,7 +4612,7 @@ export default { }, "AuthTokens": { "tokens": [ - 240 + 243 ], "__typename": [ 1 @@ -4566,7 +4653,7 @@ export default { 32 ], "workspace": [ - 245 + 248 ], "__typename": [ 1 @@ -4588,7 +4675,7 @@ export default { 1 ], "dailyUsage": [ - 256 + 259 ], "__typename": [ 1 @@ -4605,7 +4692,7 @@ export default { 197 ], "timeSeries": [ - 256 + 259 ], "periodStart": [ 4 @@ -4614,7 +4701,7 @@ export default { 4 ], "userDailyUsage": [ - 257 + 260 ], "__typename": [ 1 @@ -4779,90 +4866,6 @@ export default { 1 ], "location": [ - 266 - ], - "__typename": [ - 1 - ] - }, - "PublicConnectionParametersOutput": { - "host": [ - 1 - ], - "port": [ - 11 - ], - "username": [ - 1 - ], - "secure": [ - 6 - ], - "__typename": [ - 1 - ] - }, - "PublicImapSmtpCaldavConnectionParameters": { - "IMAP": [ - 268 - ], - "SMTP": [ - 268 - ], - "CALDAV": [ - 268 - ], - "__typename": [ - 1 - ] - }, - "ConnectedAccountPublicDTO": { - "id": [ - 3 - ], - "handle": [ - 1 - ], - "provider": [ - 1 - ], - "lastCredentialsRefreshedAt": [ - 4 - ], - "authFailedAt": [ - 4 - ], - "handleAliases": [ - 1 - ], - "scopes": [ - 1 - ], - "lastSignedInAt": [ - 4 - ], - "userWorkspaceId": [ - 3 - ], - "connectionProviderId": [ - 3 - ], - "applicationId": [ - 3 - ], - "name": [ - 1 - ], - "visibility": [ - 1 - ], - "createdAt": [ - 4 - ], - "updatedAt": [ - 4 - ], - "connectionParameters": [ 269 ], "__typename": [ @@ -5666,7 +5669,7 @@ export default { 4 ], "connectedAccount": [ - 270 + 238 ], "__typename": [ 1 @@ -6243,7 +6246,7 @@ export default { } ], "myConnectedAccounts": [ - 270 + 238 ], "myCalendarChannels": [ 304, @@ -6331,7 +6334,7 @@ export default { } ], "checkUserExists": [ - 253, + 256, { "email": [ 1, @@ -6343,7 +6346,7 @@ export default { } ], "checkWorkspaceInviteHashIsValid": [ - 254, + 257, { "inviteHash": [ 1, @@ -6361,7 +6364,7 @@ export default { } ], "validatePasswordResetToken": [ - 248, + 251, { "passwordResetToken": [ 1, @@ -6512,7 +6515,7 @@ export default { } ], "getAutoCompleteAddress": [ - 265, + 268, { "address": [ 1, @@ -6531,7 +6534,7 @@ export default { } ], "getAddressDetails": [ - 267, + 270, { "placeId": [ 1, @@ -6544,7 +6547,7 @@ export default { } ], "getUsageAnalytics": [ - 258, + 261, { "input": [ 337 @@ -6552,13 +6555,13 @@ export default { } ], "findManyPublicDomains": [ - 264 + 267 ], "findManyMarketplaceApps": [ - 262 + 265 ], "findMarketplaceAppDetail": [ - 263, + 266, { "universalIdentifier": [ 1, @@ -7883,7 +7886,7 @@ export default { } ], "deleteConnectedAccount": [ - 270, + 238, { "id": [ 3, @@ -8056,7 +8059,7 @@ export default { } ], "getAuthorizationUrlForSSO": [ - 243, + 246, { "input": [ 451, @@ -8065,7 +8068,7 @@ export default { } ], "getLoginTokenFromCredentials": [ - 252, + 255, { "email": [ 1, @@ -8091,7 +8094,7 @@ export default { } ], "signIn": [ - 241, + 244, { "email": [ 1, @@ -8113,7 +8116,7 @@ export default { } ], "verifyEmailAndGetLoginToken": [ - 249, + 252, { "emailVerificationToken": [ 1, @@ -8133,7 +8136,7 @@ export default { } ], "verifyEmailAndGetWorkspaceAgnosticToken": [ - 241, + 244, { "emailVerificationToken": [ 1, @@ -8149,7 +8152,7 @@ export default { } ], "getAuthTokensFromOTP": [ - 251, + 254, { "otp": [ 1, @@ -8169,7 +8172,7 @@ export default { } ], "signUp": [ - 241, + 244, { "email": [ 1, @@ -8191,7 +8194,7 @@ export default { } ], "signUpInWorkspace": [ - 246, + 249, { "email": [ 1, @@ -8222,13 +8225,13 @@ export default { } ], "signUpInNewWorkspace": [ - 246 + 249 ], "generateTransientToken": [ - 247 + 250 ], "getAuthTokensFromLoginToken": [ - 251, + 254, { "loginToken": [ 1, @@ -8241,7 +8244,7 @@ export default { } ], "authorizeApp": [ - 239, + 242, { "clientId": [ 1, @@ -8263,7 +8266,7 @@ export default { } ], "renewToken": [ - 251, + 254, { "appToken": [ 1, @@ -8272,7 +8275,7 @@ export default { } ], "generateApiKeyToken": [ - 250, + 253, { "apiKeyId": [ 3, @@ -8288,7 +8291,7 @@ export default { 32 ], "emailPasswordResetLink": [ - 242, + 245, { "email": [ 1, @@ -8300,7 +8303,7 @@ export default { } ], "updatePasswordViaResetToken": [ - 244, + 247, { "passwordResetToken": [ 1, @@ -8401,7 +8404,7 @@ export default { } ], "initiateOTPProvisioning": [ - 237, + 240, { "loginToken": [ 1, @@ -8414,10 +8417,10 @@ export default { } ], "initiateOTPProvisioningForAuthenticatedUser": [ - 237 + 240 ], "deleteTwoFactorAuthenticationMethod": [ - 236, + 239, { "twoFactorAuthenticationMethodId": [ 3, @@ -8426,7 +8429,7 @@ export default { } ], "verifyTwoFactorAuthenticationMethodForAuthenticatedUser": [ - 238, + 241, { "otp": [ 1, @@ -8606,7 +8609,7 @@ export default { } ], "impersonate": [ - 255, + 258, { "userId": [ 3, @@ -8662,7 +8665,7 @@ export default { } ], "createPublicDomain": [ - 264, + 267, { "domain": [ 1, @@ -8674,7 +8677,7 @@ export default { } ], "updatePublicDomain": [ - 264, + 267, { "domain": [ 1, @@ -8740,7 +8743,7 @@ export default { 6 ], "createDevelopmentApplication": [ - 259, + 262, { "universalIdentifier": [ 1, @@ -8762,7 +8765,7 @@ export default { } ], "syncApplication": [ - 260, + 263, { "manifest": [ 15, @@ -8774,7 +8777,7 @@ export default { } ], "uploadApplicationFile": [ - 261, + 264, { "file": [ 345, diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 0aafa1b997..21859de1f1 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -935,6 +935,7 @@ export type CommandMenuItemPayload = ObjectMetadataCommandMenuItemPayload | Path export type ConnectedAccountPublicDto = { __typename?: 'ConnectedAccountPublicDTO'; applicationId?: Maybe; + archivedAt?: Maybe; authFailedAt?: Maybe; connectionParameters?: Maybe; connectionProviderId?: Maybe; @@ -7338,7 +7339,7 @@ export type MyCalendarChannelsQuery = { __typename?: 'Query', myCalendarChannels export type MyConnectedAccountsQueryVariables = Exact<{ [key: string]: never; }>; -export type MyConnectedAccountsQuery = { __typename?: 'Query', myConnectedAccounts: Array<{ __typename?: 'ConnectedAccountPublicDTO', id: string, handle: string, provider: string, authFailedAt?: string | null, scopes?: Array | null, handleAliases?: Array | null, lastSignedInAt?: string | null, userWorkspaceId: string, connectionProviderId?: string | null, name?: string | null, visibility: string, lastCredentialsRefreshedAt?: string | null, createdAt: string, updatedAt: string, connectionParameters?: { __typename?: 'PublicImapSmtpCaldavConnectionParameters', IMAP?: { __typename?: 'PublicConnectionParametersOutput', host: string, port: number, secure?: boolean | null, username?: string | null } | null, SMTP?: { __typename?: 'PublicConnectionParametersOutput', host: string, port: number, secure?: boolean | null, username?: string | null } | null, CALDAV?: { __typename?: 'PublicConnectionParametersOutput', host: string, username?: string | null } | null } | null }> }; +export type MyConnectedAccountsQuery = { __typename?: 'Query', myConnectedAccounts: Array<{ __typename?: 'ConnectedAccountPublicDTO', id: string, handle: string, provider: string, authFailedAt?: string | null, archivedAt?: string | null, scopes?: Array | null, handleAliases?: Array | null, lastSignedInAt?: string | null, userWorkspaceId: string, connectionProviderId?: string | null, name?: string | null, visibility: string, lastCredentialsRefreshedAt?: string | null, createdAt: string, updatedAt: string, connectionParameters?: { __typename?: 'PublicImapSmtpCaldavConnectionParameters', IMAP?: { __typename?: 'PublicConnectionParametersOutput', host: string, port: number, secure?: boolean | null, username?: string | null } | null, SMTP?: { __typename?: 'PublicConnectionParametersOutput', host: string, port: number, secure?: boolean | null, username?: string | null } | null, CALDAV?: { __typename?: 'PublicConnectionParametersOutput', host: string, username?: string | null } | null } | null }> }; export type MyMessageChannelsQueryVariables = Exact<{ connectedAccountId?: InputMaybe; @@ -8528,7 +8529,7 @@ export const UpdateMessageFolderDocument = {"kind":"Document","definitions":[{"k 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 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":"secure"}},{"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":"secure"}}]}},{"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":"secure"}},{"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":"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":"secure"}},{"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":"secure"}},{"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 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":"secure"}},{"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":"secure"}},{"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 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; diff --git a/packages/twenty-front/src/locales/en.po b/packages/twenty-front/src/locales/en.po index 1d0cb4a8cf..67ecf6b406 100644 --- a/packages/twenty-front/src/locales/en.po +++ b/packages/twenty-front/src/locales/en.po @@ -15891,6 +15891,11 @@ msgstr "This action can return up to {maxRecordsFormatted} records." msgid "This action cannot be undone." msgstr "This action cannot be undone." +#. js-lingui-id: M0WyJi +#: src/pages/settings/members/SettingsWorkspaceMember.tsx +msgid "This action cannot be undone. This member will be removed and unassigned from their records. Their synced emails and calendars will stop syncing and be reassigned to you." +msgstr "This action cannot be undone. This member will be removed and unassigned from their records. Their synced emails and calendars will stop syncing and be reassigned to you." + #. js-lingui-id: Ox1hE7 #: src/modules/settings/profile/components/DeleteAccount.tsx msgid "" @@ -15910,21 +15915,16 @@ msgstr "This action cannot be undone. This will permanently delete your agent.<0 msgid "This action cannot be undone. This will permanently delete your entire workspace. <0/> Please type in your email to confirm." msgstr "This action cannot be undone. This will permanently delete your entire workspace. <0/> Please type in your email to confirm." -#. js-lingui-id: fTuKde -#: src/pages/settings/members/SettingsWorkspaceMember.tsx -msgid "This action cannot be undone. This will permanently remove this member from this workspace and remove them from all their assignments." -msgstr "This action cannot be undone. This will permanently remove this member from this workspace and remove them from all their assignments." - -#. js-lingui-id: Ik52hF -#: src/modules/settings/profile/components/DeleteAccount.tsx -msgid "This action cannot be undone. This will permanently remove your membership from this workspace." -msgstr "This action cannot be undone. This will permanently remove your membership from this workspace." - #. js-lingui-id: o5vUsG #: src/modules/settings/two-factor-authentication/components/DeleteTwoFactorAuthenticationMethod.tsx msgid "This action cannot be undone. This will permanently reset your two factor authentication method. <0/> Please type in your email to confirm." msgstr "This action cannot be undone. This will permanently reset your two factor authentication method. <0/> Please type in your email to confirm." +#. js-lingui-id: kVRKMS +#: src/modules/settings/profile/components/DeleteAccount.tsx +msgid "This action cannot be undone. Your membership will be removed; synced emails and calendars stay with the workspace." +msgstr "This action cannot be undone. Your membership will be removed; synced emails and calendars stay with the workspace." + #. js-lingui-id: XxxrMw #. placeholder {2}: favoritesEdit.navigationMenuItemCount #: src/modules/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDnd.tsx diff --git a/packages/twenty-front/src/modules/accounts/types/ConnectedAccount.ts b/packages/twenty-front/src/modules/accounts/types/ConnectedAccount.ts index d6dbac7ac8..38c1f27d82 100644 --- a/packages/twenty-front/src/modules/accounts/types/ConnectedAccount.ts +++ b/packages/twenty-front/src/modules/accounts/types/ConnectedAccount.ts @@ -8,6 +8,9 @@ export type ConnectedAccount = { handle: string; provider: ConnectedAccountProvider; authFailedAt: string | null; + // Set when the account was frozen after its owner left the workspace: + // synced data is kept but the account is read-only. + archivedAt: string | null; scopes: string[] | null; handleAliases: string[] | null; lastSignedInAt: string | null; diff --git a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx index 9500cf8928..1995d9ff80 100644 --- a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx @@ -274,6 +274,10 @@ export const SubPageNavigation: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); + // Let the side panel finish opening before interacting, otherwise the + // click can land mid-transition and the sub-page navigation is dropped. + await sleep(openTimeout); + const objectButton = await canvas.findByText('Object'); expect(objectButton).toBeVisible(); diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectedAccountsRowRightContainer.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectedAccountsRowRightContainer.tsx index 9e2b1b3c0b..49bf21ab2b 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectedAccountsRowRightContainer.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectedAccountsRowRightContainer.tsx @@ -4,6 +4,7 @@ import { SyncStatus } from '@/settings/accounts/constants/SyncStatus'; import { computeSyncStatus } from '@/settings/accounts/utils/computeSyncStatus'; import { styled } from '@linaria/react'; import { t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; import { Status } from 'twenty-ui-deprecated/display'; import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants'; @@ -21,8 +22,21 @@ export const SettingsAccountsConnectedAccountsRowRightContainer = ({ const messageChannel = account.messageChannels[0]; const calendarChannel = account.calendarChannels[0]; + const isArchived = isDefined(account.archivedAt); + const status = computeSyncStatus(messageChannel, calendarChannel); + // Archived accounts are frozen (owner left the workspace): their synced data + // is kept but sync is disabled, so the live sync status is no longer relevant. + if (isArchived) { + return ( + + + + + ); + } + return ( {status === SyncStatus.FAILED && ( diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyConnectedAccounts.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyConnectedAccounts.ts index 6037928092..8677192bc1 100644 --- a/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyConnectedAccounts.ts +++ b/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyConnectedAccounts.ts @@ -7,6 +7,7 @@ export const GET_MY_CONNECTED_ACCOUNTS = gql` handle provider authFailedAt + archivedAt scopes handleAliases lastSignedInAt diff --git a/packages/twenty-front/src/modules/settings/profile/components/DeleteAccount.tsx b/packages/twenty-front/src/modules/settings/profile/components/DeleteAccount.tsx index e78a80668d..3766178e92 100644 --- a/packages/twenty-front/src/modules/settings/profile/components/DeleteAccount.tsx +++ b/packages/twenty-front/src/modules/settings/profile/components/DeleteAccount.tsx @@ -101,7 +101,7 @@ export const DeleteAccount = () => { title={t`Leave workspace`} subtitle={ <> - {t`This action cannot be undone. This will permanently remove your membership from this workspace.`} + {t`This action cannot be undone. Your membership will be removed; synced emails and calendars stay with the workspace.`}
{t`Please type in your email to confirm.`} diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionEmailBase.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionEmailBase.stories.tsx index 7b1ffc29a9..5b7849b3e1 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionEmailBase.stories.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionEmailBase.stories.tsx @@ -26,6 +26,7 @@ const mockedConnectedAccounts = [ handle: 'tim@apple.dev', provider: 'google', authFailedAt: null, + archivedAt: null, scopes: ['email', 'calendar'], handleAliases: '', lastSignedInAt: null, diff --git a/packages/twenty-front/src/pages/settings/accounts/__stories__/SettingsAccountsCalendars.stories.tsx b/packages/twenty-front/src/pages/settings/accounts/__stories__/SettingsAccountsCalendars.stories.tsx index 209cb396d9..6a850fa083 100644 --- a/packages/twenty-front/src/pages/settings/accounts/__stories__/SettingsAccountsCalendars.stories.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/__stories__/SettingsAccountsCalendars.stories.tsx @@ -41,6 +41,7 @@ export const TwoConnectedAccounts: Story = { handle: 'test.test@gmail.com', provider: 'google', authFailedAt: null, + archivedAt: null, scopes: ['calendar'], handleAliases: '', lastSignedInAt: null, diff --git a/packages/twenty-front/src/pages/settings/accounts/__stories__/SettingsAccountsEmails.stories.tsx b/packages/twenty-front/src/pages/settings/accounts/__stories__/SettingsAccountsEmails.stories.tsx index e34f9581d0..39078501f6 100644 --- a/packages/twenty-front/src/pages/settings/accounts/__stories__/SettingsAccountsEmails.stories.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/__stories__/SettingsAccountsEmails.stories.tsx @@ -42,6 +42,7 @@ export const TwoConnectedAccounts: Story = { handle: 'test.test@gmail.com', provider: 'google', authFailedAt: null, + archivedAt: null, scopes: ['email'], handleAliases: '', lastSignedInAt: null, diff --git a/packages/twenty-front/src/pages/settings/members/SettingsWorkspaceMember.tsx b/packages/twenty-front/src/pages/settings/members/SettingsWorkspaceMember.tsx index 3858d65819..77f80a5b2c 100644 --- a/packages/twenty-front/src/pages/settings/members/SettingsWorkspaceMember.tsx +++ b/packages/twenty-front/src/pages/settings/members/SettingsWorkspaceMember.tsx @@ -241,7 +241,7 @@ export const SettingsWorkspaceMember = () => { { + await queryRunner.query( + 'ALTER TABLE "core"."connectedAccount" ADD COLUMN IF NOT EXISTS "archivedAt" timestamptz', + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE "core"."connectedAccount" DROP COLUMN IF EXISTS "archivedAt"', + ); + } +} 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 be7dbb8d26..53ea2bfe34 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 @@ -60,6 +60,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 { DropIsCustomFromObjectAndFieldMetadataFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-12/2-12-instance-command-fast-1780579070012-drop-is-custom-from-object-and-field-metadata'; +import { AddArchivedAtToConnectedAccountFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-13/2-13-instance-command-fast-1781171103000-add-archived-at-to-connected-account'; import { DropEmailingDomainDriverColumnFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-11/2-11-instance-command-fast-1780926908000-drop-emailing-domain-driver-column'; import { ViewOverridableEntityFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-12/2-12-instance-command-fast-1781114009075-view-overridable-entity'; import { RenameIsUiReadOnlyToIsUiEditableFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-13/2-13-instance-command-fast-1781277453604-rename-is-ui-read-only-to-is-ui-editable'; @@ -129,6 +130,7 @@ export const INSTANCE_COMMANDS = [ DropIsCustomFromObjectAndFieldMetadataFastInstanceCommand, DropEmailingDomainDriverColumnFastInstanceCommand, ViewOverridableEntityFastInstanceCommand, + AddArchivedAtToConnectedAccountFastInstanceCommand, RenameIsUiReadOnlyToIsUiEditableFastInstanceCommand, BackfillNonUiCreatableStandardSystemObjectsSlowInstanceCommand, CommandMenuItemOverridableEntityFastInstanceCommand, diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-composer.service.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-composer.service.ts index 29f3f90cd8..fae761eada 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-composer.service.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-composer.service.ts @@ -11,7 +11,7 @@ import { FileFolder, } from 'twenty-shared/types'; import { isDefined, isValidUuid } from 'twenty-shared/utils'; -import { In, LessThanOrEqual, type Repository } from 'typeorm'; +import { In, IsNull, LessThanOrEqual, type Repository } from 'typeorm'; import { z } from 'zod'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; @@ -97,7 +97,7 @@ export class EmailComposerService { return this.globalWorkspaceOrmManager.executeInWorkspaceContext( async () => { const allAccounts = await this.connectedAccountRepository.find({ - where: { workspaceId }, + where: { workspaceId, archivedAt: IsNull() }, }); if (!allAccounts || allAccounts.length === 0) { diff --git a/packages/twenty-server/src/engine/core-modules/user/services/user.service.spec.ts b/packages/twenty-server/src/engine/core-modules/user/services/user.service.spec.ts index 8216400198..4fb88bc619 100644 --- a/packages/twenty-server/src/engine/core-modules/user/services/user.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/user/services/user.service.spec.ts @@ -11,7 +11,7 @@ import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspac import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; -import { type UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; +import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { WorkspaceMemberTranspiler } from 'src/engine/core-modules/user/services/workspace-member-transpiler.service'; @@ -23,6 +23,7 @@ import { PermissionsExceptionCode, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service'; +import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; @@ -96,6 +97,18 @@ describe('UserService', () => { validateUserWorkspaceIsNotUniqueAdminOrThrow: jest.fn(), }, }, + { + provide: getRepositoryToken(UserWorkspaceEntity), + useValue: { + find: jest.fn().mockResolvedValue([]), + }, + }, + { + provide: ConnectedAccountMetadataService, + useValue: { + transferOwnership: jest.fn(), + }, + }, { provide: UserWorkspaceService, useValue: { diff --git a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts index d69e47e574..69d2406d29 100644 --- a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts @@ -44,9 +44,11 @@ import { PermissionsExceptionCode, PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; +import { STANDARD_ROLE } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-role.constant'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; // oxlint-disable-next-line twenty/inject-workspace-repository @@ -54,6 +56,9 @@ export class UserService extends TypeOrmQueryService { constructor( @InjectRepository(UserEntity) private readonly userRepository: Repository, + @InjectRepository(UserWorkspaceEntity) + private readonly userWorkspaceRepository: Repository, + private readonly connectedAccountMetadataService: ConnectedAccountMetadataService, private readonly workspaceDomainsService: WorkspaceDomainsService, private readonly emailVerificationService: EmailVerificationService, private readonly workspaceService: WorkspaceService, @@ -295,9 +300,11 @@ export class UserService extends TypeOrmQueryService { async deleteUserWorkspaceAndPotentiallyDeleteUser({ userId, workspaceId, + actingUserWorkspaceId, }: { userId: string; workspaceId: string; + actingUserWorkspaceId?: string; }) { const user = await this.userRepository.findOne({ where: { @@ -318,6 +325,7 @@ export class UserService extends TypeOrmQueryService { await this.removeUserFromWorkspaceAndPotentiallyDeleteWorkspace( userWorkspace, + actingUserWorkspaceId, ); if (user.userWorkspaces.length === 1) { @@ -330,6 +338,7 @@ export class UserService extends TypeOrmQueryService { async removeUserFromWorkspaceAndPotentiallyDeleteWorkspace( userWorkspace: UserWorkspaceEntity, + actingUserWorkspaceId?: string, ) { const workspaceId = userWorkspace.workspaceId; const authContext = buildSystemAuthContext(workspaceId); @@ -390,6 +399,20 @@ export class UserService extends TypeOrmQueryService { assert(workspaceMember, 'WorkspaceMember not found'); + const custodianUserWorkspaceId = + await this.resolveConnectedAccountsCustodianUserWorkspaceId({ + removedUserWorkspace: userWorkspace, + actingUserWorkspaceId, + }); + + if (isDefined(custodianUserWorkspaceId)) { + await this.connectedAccountMetadataService.transferOwnership({ + fromUserWorkspaceId: userWorkspaceId, + toUserWorkspaceId: custodianUserWorkspaceId, + workspaceId, + }); + } + await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { const workspaceMemberRepository = await this.globalWorkspaceOrmManager.getRepository( @@ -408,6 +431,55 @@ export class UserService extends TypeOrmQueryService { }); } + private async resolveConnectedAccountsCustodianUserWorkspaceId({ + removedUserWorkspace, + actingUserWorkspaceId, + }: { + removedUserWorkspace: UserWorkspaceEntity; + actingUserWorkspaceId?: string; + }): Promise { + const otherUserWorkspaces = await this.userWorkspaceRepository.find({ + where: { + workspaceId: removedUserWorkspace.workspaceId, + id: Not(removedUserWorkspace.id), + }, + order: { createdAt: 'ASC' }, + }); + + if (otherUserWorkspaces.length === 0) { + return undefined; + } + + const actingUserWorkspace = otherUserWorkspaces.find( + (otherUserWorkspace) => otherUserWorkspace.id === actingUserWorkspaceId, + ); + + if (isDefined(actingUserWorkspace)) { + return actingUserWorkspace.id; + } + + const rolesByUserWorkspaceId = + await this.userRoleService.getRolesByUserWorkspaces({ + userWorkspaceIds: otherUserWorkspaces.map( + (otherUserWorkspace) => otherUserWorkspace.id, + ), + workspaceId: removedUserWorkspace.workspaceId, + }); + + const oldestAdminUserWorkspace = otherUserWorkspaces.find( + (otherUserWorkspace) => + rolesByUserWorkspaceId + .get(otherUserWorkspace.id) + ?.some( + (role) => + role.universalIdentifier === + STANDARD_ROLE.admin.universalIdentifier, + ), + ); + + return (oldestAdminUserWorkspace ?? otherUserWorkspaces[0]).id; + } + async hasUserAccessToWorkspaceOrThrow(userId: string, workspaceId: string) { const user = await this.userRepository.findOne({ where: { diff --git a/packages/twenty-server/src/engine/core-modules/user/user.module.ts b/packages/twenty-server/src/engine/core-modules/user/user.module.ts index 293737a96e..08768da2a0 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.module.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.module.ts @@ -22,6 +22,7 @@ import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars import { UserEntity } from 'src/engine/core-modules/user/user.entity'; import { UserResolver } from 'src/engine/core-modules/user/user.resolver'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; +import { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.module'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; @@ -48,6 +49,7 @@ import { UserService } from './services/user.service'; UserVarsModule, UserWorkspaceModule, UserRoleModule, + ConnectedAccountMetadataModule, FeatureFlagModule, PermissionsModule, EmailVerificationModule, diff --git a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts index be97416b41..428bebecf1 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts @@ -443,6 +443,7 @@ export class UserResolver { return this.userService.deleteUserWorkspaceAndPotentiallyDeleteUser({ userId: workspaceMemberToDelete.userId, workspaceId: workspace.id, + actingUserWorkspaceId: userWorkspaceId, }); } diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/connected-account-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/connected-account-metadata.service.ts index fb5378a0d6..8548756c64 100644 --- a/packages/twenty-server/src/engine/metadata-modules/connected-account/connected-account-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/connected-account-metadata.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { In, IsNull, Repository } from 'typeorm'; import { AppOAuthRevokeService } from 'src/engine/core-modules/application/connection-provider/refresh/services/app-oauth-revoke.service'; import { CALENDAR_CHANNEL_DELETED_EVENT } from 'src/engine/metadata-modules/calendar-channel/constants/calendar-channel-deleted.constant'; @@ -161,6 +161,61 @@ export class ConnectedAccountMetadataService { return this.repository.findOneOrFail({ where: { id, workspaceId } }); } + async transferOwnership({ + fromUserWorkspaceId, + toUserWorkspaceId, + workspaceId, + }: { + fromUserWorkspaceId: string; + toUserWorkspaceId: string; + workspaceId: string; + }): Promise { + const connectedAccounts = await this.repository.find({ + where: { userWorkspaceId: fromUserWorkspaceId, workspaceId }, + }); + + if (connectedAccounts.length === 0) { + return; + } + + const connectedAccountIds = connectedAccounts.map((account) => account.id); + + await this.repository.manager.transaction(async (entityManager) => { + await entityManager.update( + ConnectedAccountEntity, + { id: In(connectedAccountIds), workspaceId }, + { + userWorkspaceId: toUserWorkspaceId, + accessToken: null, + refreshToken: null, + connectionParameters: null, + }, + ); + + await entityManager.update( + ConnectedAccountEntity, + { id: In(connectedAccountIds), workspaceId, archivedAt: IsNull() }, + { archivedAt: new Date() }, + ); + + await entityManager.update( + MessageChannelEntity, + { connectedAccountId: In(connectedAccountIds), workspaceId }, + { isSyncEnabled: false }, + ); + + await entityManager.update( + CalendarChannelEntity, + { connectedAccountId: In(connectedAccountIds), workspaceId }, + { isSyncEnabled: false }, + ); + }); + + for (const connectedAccount of connectedAccounts) { + await this.appOAuthRevokeService.revokeIfApp(connectedAccount); + } + } + async delete({ id, workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/connected-account.dto.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/connected-account.dto.ts index 1a65b3020c..cd63f78d8b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/connected-account.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/connected-account.dto.ts @@ -45,6 +45,13 @@ export class ConnectedAccountDTO { @Field(() => Date, { nullable: true }) authFailedAt: Date | null; + // Set when the account is frozen after its owner is removed from the + // workspace: synced data is kept but the account is read-only. + @IsDateString() + @IsOptional() + @Field(() => Date, { nullable: true }) + archivedAt: Date | null; + @IsArray() @IsOptional() @Field(() => [String], { nullable: true }) diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/entities/connected-account.entity.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/entities/connected-account.entity.ts index 1db836661d..0640b7135f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/connected-account/entities/connected-account.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/entities/connected-account.entity.ts @@ -65,6 +65,9 @@ export class ConnectedAccountEntity extends WorkspaceRelatedEntity { @Column({ type: 'timestamptz', nullable: true }) authFailedAt: Date | null; + @Column({ type: 'timestamptz', nullable: true }) + archivedAt: Date | null; + @Column({ type: 'varchar', array: true, nullable: true }) handleAliases: string[] | null; diff --git a/packages/twenty-server/src/modules/connected-account/connected-account.module.ts b/packages/twenty-server/src/modules/connected-account/connected-account.module.ts index 125d37c891..0a34f61bb6 100644 --- a/packages/twenty-server/src/modules/connected-account/connected-account.module.ts +++ b/packages/twenty-server/src/modules/connected-account/connected-account.module.ts @@ -4,8 +4,6 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module'; import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; -import { DeleteWorkspaceMemberConnectedAccountsCleanupJob } from 'src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job'; -import { ConnectedAccountWorkspaceMemberListener } from 'src/modules/connected-account/listeners/connected-account-workspace-member.listener'; import { ConnectedAccountListener } from 'src/modules/connected-account/listeners/connected-account.listener'; import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service'; @@ -14,12 +12,7 @@ import { AccountsToReconnectService } from 'src/modules/connected-account/servic TypeOrmModule.forFeature([ConnectedAccountEntity, UserWorkspaceEntity]), UserVarsModule, ], - providers: [ - AccountsToReconnectService, - ConnectedAccountListener, - DeleteWorkspaceMemberConnectedAccountsCleanupJob, - ConnectedAccountWorkspaceMemberListener, - ], + providers: [AccountsToReconnectService, ConnectedAccountListener], exports: [AccountsToReconnectService], }) export class ConnectedAccountModule {} diff --git a/packages/twenty-server/src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job.ts b/packages/twenty-server/src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job.ts deleted file mode 100644 index fcd9780643..0000000000 --- a/packages/twenty-server/src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { InjectRepository } from '@nestjs/typeorm'; - -import { type Repository } from 'typeorm'; - -import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; -import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; -import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; -import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; -import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; - -export type DeleteWorkspaceMemberConnectedAccountsCleanupJobData = { - workspaceId: string; - workspaceMemberId: string; -}; - -@Processor(MessageQueue.deleteCascadeQueue) -export class DeleteWorkspaceMemberConnectedAccountsCleanupJob { - constructor( - private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, - @InjectRepository(ConnectedAccountEntity) - private readonly connectedAccountRepository: Repository, - @InjectRepository(UserWorkspaceEntity) - private readonly userWorkspaceRepository: Repository, - ) {} - - @Process(DeleteWorkspaceMemberConnectedAccountsCleanupJob.name) - async handle( - data: DeleteWorkspaceMemberConnectedAccountsCleanupJobData, - ): Promise { - const { workspaceId, workspaceMemberId } = data; - - const authContext = buildSystemAuthContext(workspaceId); - - await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => { - const workspaceMemberRepo = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); - - const member = await workspaceMemberRepo.findOne({ - where: { id: workspaceMemberId }, - }); - - if (!member) { - return; - } - - const userWorkspace = await this.userWorkspaceRepository.findOne({ - where: { userId: member.userId, workspaceId }, - }); - - if (!userWorkspace) { - return; - } - - await this.connectedAccountRepository.delete({ - userWorkspaceId: userWorkspace.id, - workspaceId, - }); - }, authContext); - } -} diff --git a/packages/twenty-server/src/modules/connected-account/listeners/connected-account-workspace-member.listener.ts b/packages/twenty-server/src/modules/connected-account/listeners/connected-account-workspace-member.listener.ts deleted file mode 100644 index 6186d3417a..0000000000 --- a/packages/twenty-server/src/modules/connected-account/listeners/connected-account-workspace-member.listener.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { - type ObjectRecordDeleteEvent, - type ObjectRecordDestroyEvent, -} from 'twenty-shared/database-events'; - -import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; -import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; -import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; -import { - DeleteWorkspaceMemberConnectedAccountsCleanupJob, - type DeleteWorkspaceMemberConnectedAccountsCleanupJobData, -} from 'src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; - -@Injectable() -export class ConnectedAccountWorkspaceMemberListener { - constructor( - @InjectMessageQueue(MessageQueue.deleteCascadeQueue) - private readonly messageQueueService: MessageQueueService, - ) {} - - @OnDatabaseBatchEvent('workspaceMember', DatabaseEventAction.DESTROYED) - @OnDatabaseBatchEvent('workspaceMember', DatabaseEventAction.DELETED) - async handleWorkspaceMemberRemovalEvent( - payload: WorkspaceEventBatch< - | ObjectRecordDeleteEvent - | ObjectRecordDestroyEvent - >, - ) { - await Promise.all( - payload.events.map((eventPayload) => - this.messageQueueService.add( - DeleteWorkspaceMemberConnectedAccountsCleanupJob.name, - { - workspaceId: payload.workspaceId, - workspaceMemberId: eventPayload.recordId, - }, - ), - ), - ); - } -}