diff --git a/package.json b/package.json index b0414a544..07b2cfcd5 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "graphql-yoga": "^4.0.4", "hex-rgb": "^5.0.0", "iframe-resizer-react": "^1.1.0", + "imapflow": "^1.0.186", "immer": "^10.0.2", "jest-mock-extended": "^3.0.4", "js-cookie": "^3.0.5", @@ -133,6 +134,7 @@ "lodash.snakecase": "^4.1.1", "lodash.upperfirst": "^4.3.1", "luxon": "^3.3.0", + "mailparser": "^3.7.3", "microdiff": "^1.3.2", "moize": "^6.1.6", "nest-commander": "^3.12.0", @@ -251,6 +253,7 @@ "@types/file-saver": "^2.0.7", "@types/graphql-fields": "^1.3.6", "@types/graphql-upload": "^8.0.12", + "@types/imapflow": "^1.0.21", "@types/js-cookie": "^3.0.3", "@types/js-levenshtein": "^1.1.3", "@types/lodash.camelcase": "^4.3.7", @@ -269,6 +272,7 @@ "@types/lodash.snakecase": "^4.1.7", "@types/lodash.upperfirst": "^4.3.7", "@types/luxon": "^3.3.0", + "@types/mailparser": "^3.4.6", "@types/ms": "^0.7.31", "@types/node": "^22.0.0", "@types/passport-google-oauth20": "^2.0.11", diff --git a/packages/twenty-front/codegen.cjs b/packages/twenty-front/codegen.cjs index 1c2ecc86e..80b74d6a2 100644 --- a/packages/twenty-front/codegen.cjs +++ b/packages/twenty-front/codegen.cjs @@ -9,6 +9,7 @@ module.exports = { '!./src/modules/object-metadata/**', '!./src/modules/object-record/**', '!./src/modules/settings/serverless-functions/**', + '!./src/modules/settings/accounts/hooks/**', './src/modules/**/*.tsx', './src/modules/**/*.ts', '!./src/**/*.test.tsx', diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index adfedc373..57be9e923 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -30,6 +30,10 @@ export type Scalars = { Upload: { input: any; output: any; } }; +export type AccountType = { + type: Scalars['String']['input']; +}; + export type ActivateWorkspaceInput = { displayName?: InputMaybe; }; @@ -412,6 +416,32 @@ export type ConfigVariablesOutput = { groups: Array; }; +export type ConnectedImapSmtpCaldavAccount = { + __typename?: 'ConnectedImapSmtpCaldavAccount'; + accountOwnerId: Scalars['String']['output']; + connectionParameters?: Maybe; + handle: Scalars['String']['output']; + id: Scalars['String']['output']; + provider: Scalars['String']['output']; +}; + +export type ConnectionParameters = { + host: Scalars['String']['input']; + password: Scalars['String']['input']; + port: Scalars['Float']['input']; + secure?: InputMaybe; + username: Scalars['String']['input']; +}; + +export type ConnectionParametersOutput = { + __typename?: 'ConnectionParametersOutput'; + host: Scalars['String']['output']; + password: Scalars['String']['output']; + port: Scalars['Float']['output']; + secure?: Maybe; + username: Scalars['String']['output']; +}; + export type CreateAgentInput = { description?: InputMaybe; modelId: Scalars['String']['input']; @@ -662,6 +692,7 @@ export type FeatureFlagDto = { export enum FeatureFlagKey { IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED', IS_AI_ENABLED = 'IS_AI_ENABLED', + IS_IMAP_ENABLED = 'IS_IMAP_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED', IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED', @@ -819,6 +850,18 @@ export enum IdentityProviderType { SAML = 'SAML' } +export type ImapSmtpCaldavConnectionParameters = { + __typename?: 'ImapSmtpCaldavConnectionParameters'; + CALDAV?: Maybe; + IMAP?: Maybe; + SMTP?: Maybe; +}; + +export type ImapSmtpCaldavConnectionSuccess = { + __typename?: 'ImapSmtpCaldavConnectionSuccess'; + success: Scalars['Boolean']['output']; +}; + export type ImpersonateOutput = { __typename?: 'ImpersonateOutput'; loginToken: AuthToken; @@ -1010,6 +1053,7 @@ export type Mutation = { resendEmailVerificationToken: ResendEmailVerificationTokenOutput; resendWorkspaceInvitation: SendInvitationsOutput; runWorkflowVersion: WorkflowRun; + saveImapSmtpCaldav: ImapSmtpCaldavConnectionSuccess; sendInvitations: SendInvitationsOutput; signIn: AvailableWorkspacesAndAccessTokensOutput; signUp: AvailableWorkspacesAndAccessTokensOutput; @@ -1294,6 +1338,15 @@ export type MutationRunWorkflowVersionArgs = { }; +export type MutationSaveImapSmtpCaldavArgs = { + accountOwnerId: Scalars['String']['input']; + accountType: AccountType; + connectionParameters: ConnectionParameters; + handle: Scalars['String']['input']; + id?: InputMaybe; +}; + + export type MutationSendInvitationsArgs = { emails: Array; }; @@ -1696,6 +1749,7 @@ export type Query = { getApprovedAccessDomains: Array; getAvailablePackages: Scalars['JSON']['output']; getConfigVariablesGrouped: ConfigVariablesOutput; + getConnectedImapSmtpCaldavAccount: ConnectedImapSmtpCaldavAccount; getDatabaseConfigVariable: ConfigVariable; getIndicatorHealthStatus: AdminPanelHealthServiceData; getMeteredProductsUsage: Array; @@ -1783,6 +1837,11 @@ export type QueryGetAvailablePackagesArgs = { }; +export type QueryGetConnectedImapSmtpCaldavAccountArgs = { + id: Scalars['String']['input']; +}; + + export type QueryGetDatabaseConfigVariableArgs = { key: Scalars['String']['input']; }; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 095db291d..53e98e9f6 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -22,6 +22,10 @@ export type Scalars = { Upload: any; }; +export type AccountType = { + type: Scalars['String']; +}; + export type ActivateWorkspaceInput = { displayName?: InputMaybe; }; @@ -404,6 +408,32 @@ export type ConfigVariablesOutput = { groups: Array; }; +export type ConnectedImapSmtpCaldavAccount = { + __typename?: 'ConnectedImapSmtpCaldavAccount'; + accountOwnerId: Scalars['String']; + connectionParameters?: Maybe; + handle: Scalars['String']; + id: Scalars['String']; + provider: Scalars['String']; +}; + +export type ConnectionParameters = { + host: Scalars['String']; + password: Scalars['String']; + port: Scalars['Float']; + secure?: InputMaybe; + username: Scalars['String']; +}; + +export type ConnectionParametersOutput = { + __typename?: 'ConnectionParametersOutput'; + host: Scalars['String']; + password: Scalars['String']; + port: Scalars['Float']; + secure?: Maybe; + username: Scalars['String']; +}; + export type CreateAgentInput = { description?: InputMaybe; modelId: Scalars['String']; @@ -618,6 +648,7 @@ export type FeatureFlagDto = { export enum FeatureFlagKey { IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED', IS_AI_ENABLED = 'IS_AI_ENABLED', + IS_IMAP_ENABLED = 'IS_IMAP_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED', IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED', @@ -768,6 +799,18 @@ export enum IdentityProviderType { SAML = 'SAML' } +export type ImapSmtpCaldavConnectionParameters = { + __typename?: 'ImapSmtpCaldavConnectionParameters'; + CALDAV?: Maybe; + IMAP?: Maybe; + SMTP?: Maybe; +}; + +export type ImapSmtpCaldavConnectionSuccess = { + __typename?: 'ImapSmtpCaldavConnectionSuccess'; + success: Scalars['Boolean']; +}; + export type ImpersonateOutput = { __typename?: 'ImpersonateOutput'; loginToken: AuthToken; @@ -957,6 +1000,7 @@ export type Mutation = { resendEmailVerificationToken: ResendEmailVerificationTokenOutput; resendWorkspaceInvitation: SendInvitationsOutput; runWorkflowVersion: WorkflowRun; + saveImapSmtpCaldav: ImapSmtpCaldavConnectionSuccess; sendInvitations: SendInvitationsOutput; signIn: AvailableWorkspacesAndAccessTokensOutput; signUp: AvailableWorkspacesAndAccessTokensOutput; @@ -1217,6 +1261,15 @@ export type MutationRunWorkflowVersionArgs = { }; +export type MutationSaveImapSmtpCaldavArgs = { + accountOwnerId: Scalars['String']; + accountType: AccountType; + connectionParameters: ConnectionParameters; + handle: Scalars['String']; + id?: InputMaybe; +}; + + export type MutationSendInvitationsArgs = { emails: Array; }; @@ -1596,6 +1649,7 @@ export type Query = { getApprovedAccessDomains: Array; getAvailablePackages: Scalars['JSON']; getConfigVariablesGrouped: ConfigVariablesOutput; + getConnectedImapSmtpCaldavAccount: ConnectedImapSmtpCaldavAccount; getDatabaseConfigVariable: ConfigVariable; getIndicatorHealthStatus: AdminPanelHealthServiceData; getMeteredProductsUsage: Array; @@ -1657,6 +1711,11 @@ export type QueryGetAvailablePackagesArgs = { }; +export type QueryGetConnectedImapSmtpCaldavAccountArgs = { + id: Scalars['String']; +}; + + export type QueryGetDatabaseConfigVariableArgs = { key: Scalars['String']; }; @@ -2816,6 +2875,24 @@ export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string] export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } }; +export type SaveImapSmtpCaldavMutationVariables = Exact<{ + accountOwnerId: Scalars['String']; + handle: Scalars['String']; + accountType: AccountType; + connectionParameters: ConnectionParameters; + id?: InputMaybe; +}>; + + +export type SaveImapSmtpCaldavMutation = { __typename?: 'Mutation', saveImapSmtpCaldav: { __typename?: 'ImapSmtpCaldavConnectionSuccess', success: boolean } }; + +export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{ + id: Scalars['String']; +}>; + + +export type GetConnectedImapSmtpCaldavAccountQuery = { __typename?: 'Query', getConnectedImapSmtpCaldavAccount: { __typename?: 'ConnectedImapSmtpCaldavAccount', id: string, handle: string, provider: string, accountOwnerId: string, connectionParameters?: { __typename?: 'ImapSmtpCaldavConnectionParameters', IMAP?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, username: string, password: string } | null, SMTP?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, username: string, password: string } | null, CALDAV?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, username: string, password: string } | null } | null } }; + export type CreateDatabaseConfigVariableMutationVariables = Exact<{ key: Scalars['String']; value: Scalars['JSON']; @@ -4885,6 +4962,110 @@ export function useSkipSyncEmailOnboardingStepMutation(baseOptions?: Apollo.Muta export type SkipSyncEmailOnboardingStepMutationHookResult = ReturnType; export type SkipSyncEmailOnboardingStepMutationResult = Apollo.MutationResult; export type SkipSyncEmailOnboardingStepMutationOptions = Apollo.BaseMutationOptions; +export const SaveImapSmtpCaldavDocument = gql` + mutation SaveImapSmtpCaldav($accountOwnerId: String!, $handle: String!, $accountType: AccountType!, $connectionParameters: ConnectionParameters!, $id: String) { + saveImapSmtpCaldav( + accountOwnerId: $accountOwnerId + handle: $handle + accountType: $accountType + connectionParameters: $connectionParameters + id: $id + ) { + success + } +} + `; +export type SaveImapSmtpCaldavMutationFn = Apollo.MutationFunction; + +/** + * __useSaveImapSmtpCaldavMutation__ + * + * To run a mutation, you first call `useSaveImapSmtpCaldavMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useSaveImapSmtpCaldavMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [saveImapSmtpCaldavMutation, { data, loading, error }] = useSaveImapSmtpCaldavMutation({ + * variables: { + * accountOwnerId: // value for 'accountOwnerId' + * handle: // value for 'handle' + * accountType: // value for 'accountType' + * connectionParameters: // value for 'connectionParameters' + * id: // value for 'id' + * }, + * }); + */ +export function useSaveImapSmtpCaldavMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(SaveImapSmtpCaldavDocument, options); + } +export type SaveImapSmtpCaldavMutationHookResult = ReturnType; +export type SaveImapSmtpCaldavMutationResult = Apollo.MutationResult; +export type SaveImapSmtpCaldavMutationOptions = Apollo.BaseMutationOptions; +export const GetConnectedImapSmtpCaldavAccountDocument = gql` + query GetConnectedImapSmtpCaldavAccount($id: String!) { + getConnectedImapSmtpCaldavAccount(id: $id) { + id + handle + provider + accountOwnerId + connectionParameters { + IMAP { + host + port + secure + username + password + } + SMTP { + host + port + secure + username + password + } + CALDAV { + host + port + secure + username + password + } + } + } +} + `; + +/** + * __useGetConnectedImapSmtpCaldavAccountQuery__ + * + * To run a query within a React component, call `useGetConnectedImapSmtpCaldavAccountQuery` and pass it any options that fit your needs. + * When your component renders, `useGetConnectedImapSmtpCaldavAccountQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetConnectedImapSmtpCaldavAccountQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useGetConnectedImapSmtpCaldavAccountQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetConnectedImapSmtpCaldavAccountDocument, options); + } +export function useGetConnectedImapSmtpCaldavAccountLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetConnectedImapSmtpCaldavAccountDocument, options); + } +export type GetConnectedImapSmtpCaldavAccountQueryHookResult = ReturnType; +export type GetConnectedImapSmtpCaldavAccountLazyQueryHookResult = ReturnType; +export type GetConnectedImapSmtpCaldavAccountQueryResult = Apollo.QueryResult; export const CreateDatabaseConfigVariableDocument = gql` mutation CreateDatabaseConfigVariable($key: String!, $value: JSON!) { createDatabaseConfigVariable(key: $key, value: $value) diff --git a/packages/twenty-front/src/modules/accounts/types/ConnectedAccount.ts b/packages/twenty-front/src/modules/accounts/types/ConnectedAccount.ts index e496b0c2e..edae9f347 100644 --- a/packages/twenty-front/src/modules/accounts/types/ConnectedAccount.ts +++ b/packages/twenty-front/src/modules/accounts/types/ConnectedAccount.ts @@ -1,6 +1,7 @@ import { CalendarChannel } from '@/accounts/types/CalendarChannel'; -import { MessageChannel } from './MessageChannel'; +import { ImapSmtpCaldavAccount } from '@/accounts/types/ImapSmtpCaldavAccount'; import { ConnectedAccountProvider } from 'twenty-shared/types'; +import { MessageChannel } from './MessageChannel'; export type ConnectedAccount = { id: string; @@ -14,5 +15,6 @@ export type ConnectedAccount = { messageChannels: MessageChannel[]; calendarChannels: CalendarChannel[]; scopes: string[] | null; + connectionParameters?: ImapSmtpCaldavAccount; __typename: 'ConnectedAccount'; }; diff --git a/packages/twenty-front/src/modules/accounts/types/ImapSmtpCaldavAccount.ts b/packages/twenty-front/src/modules/accounts/types/ImapSmtpCaldavAccount.ts new file mode 100644 index 000000000..3c91a5321 --- /dev/null +++ b/packages/twenty-front/src/modules/accounts/types/ImapSmtpCaldavAccount.ts @@ -0,0 +1,7 @@ +import { ConnectionParameters } from '~/generated/graphql'; + +export type ImapSmtpCaldavAccount = { + IMAP?: ConnectionParameters; + SMTP?: ConnectionParameters; + CALDAV?: ConnectionParameters; +}; diff --git a/packages/twenty-front/src/modules/accounts/types/MessageChannel.ts b/packages/twenty-front/src/modules/accounts/types/MessageChannel.ts index 00fdbfeec..e39a58c52 100644 --- a/packages/twenty-front/src/modules/accounts/types/MessageChannel.ts +++ b/packages/twenty-front/src/modules/accounts/types/MessageChannel.ts @@ -1,5 +1,6 @@ -import { MessageChannelVisibility } from '~/generated/graphql'; +import { ImapSmtpCaldavAccount } from '@/accounts/types/ImapSmtpCaldavAccount'; import { ConnectedAccountProvider } from 'twenty-shared/types'; +import { MessageChannelVisibility } from '~/generated/graphql'; export enum MessageChannelContactAutoCreationPolicy { SENT_AND_RECEIVED = 'SENT_AND_RECEIVED', @@ -40,6 +41,7 @@ export type MessageChannel = { connectedAccount?: { id: string; provider: ConnectedAccountProvider; + connectionParameters?: ImapSmtpCaldavAccount; }; __typename: 'MessageChannel'; }; diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 83a9b601b..874cb6aba 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -64,6 +64,22 @@ const SettingsNewObject = lazy(() => })), ); +const SettingsNewImapConnection = lazy(() => + import( + '@/settings/accounts/components/SettingsAccountsNewImapConnection' + ).then((module) => ({ + default: module.SettingsAccountsNewImapConnection, + })), +); + +const SettingsEditImapConnection = lazy(() => + import( + '@/settings/accounts/components/SettingsAccountsEditImapConnection' + ).then((module) => ({ + default: module.SettingsAccountsEditImapConnection, + })), +); + const SettingsObjectDetailPage = lazy(() => import('~/pages/settings/data-model/SettingsObjectDetailPage').then( (module) => ({ @@ -358,6 +374,14 @@ export const SettingsRoutes = ({ path={SettingsPath.AccountsEmails} element={} /> + } + /> + } + /> ; sentry: Sentry; signInPrefilled: boolean; diff --git a/packages/twenty-front/src/modules/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage.tsx b/packages/twenty-front/src/modules/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage.tsx index 492406f18..7c04b7e64 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage.tsx @@ -12,8 +12,9 @@ import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; import { ConnectedAccountProvider } from 'twenty-shared/types'; -import { Button } from 'twenty-ui/input'; +import { isDefined } from 'twenty-shared/utils'; import { IconArrowBackUp } from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; const StyledWrapper = styled.div` display: flex; @@ -41,6 +42,12 @@ const StyledButtonContainer = styled.div<{ isMobile: boolean }>` box-sizing: border-box; `; +const ALLOWED_REPLY_PROVIDERS = [ + ConnectedAccountProvider.GOOGLE, + ConnectedAccountProvider.MICROSOFT, + ConnectedAccountProvider.IMAP_SMTP_CALDAV, +]; + export const CommandMenuMessageThreadPage = () => { const setMessageThread = useSetRecoilComponentStateV2( messageThreadComponentState, @@ -58,6 +65,7 @@ export const CommandMenuMessageThreadPage = () => { messageChannelLoading, connectedAccountProvider, lastMessageExternalId, + connectedAccountConnectionParameters, } = useEmailThreadInCommandMenu(); useEffect(() => { @@ -83,10 +91,14 @@ export const CommandMenuMessageThreadPage = () => { return ( connectedAccountHandle && connectedAccountProvider && + ALLOWED_REPLY_PROVIDERS.includes(connectedAccountProvider) && + (connectedAccountProvider !== ConnectedAccountProvider.IMAP_SMTP_CALDAV || + isDefined(connectedAccountConnectionParameters?.SMTP)) && lastMessage && messageThreadExternalId != null ); }, [ + connectedAccountConnectionParameters, connectedAccountHandle, connectedAccountProvider, lastMessage, @@ -108,6 +120,8 @@ export const CommandMenuMessageThreadPage = () => { url = `https://mail.google.com/mail/?authuser=${connectedAccountHandle}#all/${messageThreadExternalId}`; window.open(url, '_blank'); break; + case ConnectedAccountProvider.IMAP_SMTP_CALDAV: + throw new Error('Account provider not supported'); case null: throw new Error('Account provider not provided'); default: diff --git a/packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/useEmailThreadInCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/useEmailThreadInCommandMenu.ts index fbeda7be9..45988d815 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/useEmailThreadInCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/useEmailThreadInCommandMenu.ts @@ -139,6 +139,7 @@ export const useEmailThreadInCommandMenu = () => { connectedAccount: { id: true, provider: true, + connectionParameters: true, }, }, skip: !lastMessageChannelId, @@ -175,12 +176,16 @@ export const useEmailThreadInCommandMenu = () => { ? messageChannelData[0]?.connectedAccount : null; const connectedAccountProvider = connectedAccount?.provider ?? null; + const connectedAccountConnectionParameters = + connectedAccount?.connectionParameters; + return { thread, messages: messagesWithSender, messageThreadExternalId, connectedAccountHandle, connectedAccountProvider, + connectedAccountConnectionParameters, threadLoading: messagesLoading, messageChannelLoading, lastMessageExternalId, diff --git a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases.tsx b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases.tsx index 1ce7f5dcf..032781449 100644 --- a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases.tsx @@ -1,7 +1,7 @@ import { InformationBanner } from '@/information-banner/components/InformationBanner'; import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect'; import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys'; -import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth'; +import { useTriggerProviderReconnect } from '@/settings/accounts/hooks/useTriggerProviderReconnect'; import { IconRefresh } from 'twenty-ui/display'; export const InformationBannerReconnectAccountEmailAliases = () => { @@ -9,7 +9,7 @@ export const InformationBannerReconnectAccountEmailAliases = () => { InformationBannerKeys.ACCOUNTS_TO_RECONNECT_EMAIL_ALIASES, ); - const { triggerApisOAuth } = useTriggerApisOAuth(); + const { triggerProviderReconnect } = useTriggerProviderReconnect(); if (!accountToReconnect) { return null; @@ -17,10 +17,15 @@ export const InformationBannerReconnectAccountEmailAliases = () => { return ( triggerApisOAuth(accountToReconnect.provider)} + buttonOnClick={() => + triggerProviderReconnect( + accountToReconnect.provider, + accountToReconnect.id, + ) + } /> ); }; diff --git a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx index c1da9376f..d20745779 100644 --- a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx @@ -1,7 +1,7 @@ import { InformationBanner } from '@/information-banner/components/InformationBanner'; import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect'; import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys'; -import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth'; +import { useTriggerProviderReconnect } from '@/settings/accounts/hooks/useTriggerProviderReconnect'; import { IconRefresh } from 'twenty-ui/display'; export const InformationBannerReconnectAccountInsufficientPermissions = () => { @@ -9,7 +9,7 @@ export const InformationBannerReconnectAccountInsufficientPermissions = () => { InformationBannerKeys.ACCOUNTS_TO_RECONNECT_INSUFFICIENT_PERMISSIONS, ); - const { triggerApisOAuth } = useTriggerApisOAuth(); + const { triggerProviderReconnect } = useTriggerProviderReconnect(); if (!accountToReconnect) { return null; @@ -17,11 +17,16 @@ export const InformationBannerReconnectAccountInsufficientPermissions = () => { return ( triggerApisOAuth(accountToReconnect.provider)} + buttonOnClick={() => + triggerProviderReconnect( + accountToReconnect.provider, + accountToReconnect.id, + ) + } /> ); }; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectedAccountsListCard.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectedAccountsListCard.tsx index bc4ae82f1..a4603b1c2 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectedAccountsListCard.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsConnectedAccountsListCard.tsx @@ -3,14 +3,20 @@ import { SettingsAccountsListEmptyStateCard } from '@/settings/accounts/componen import { SettingsPath } from '@/types/SettingsPath'; import { SettingsAccountsConnectedAccountsRowRightContainer } from '@/settings/accounts/components/SettingsAccountsConnectedAccountsRowRightContainer'; +import { useLingui } from '@lingui/react/macro'; +import { + IconComponent, + IconGoogle, + IconMail, + IconMicrosoft, +} from 'twenty-ui/display'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { SettingsListCard } from '../../components/SettingsListCard'; -import { useLingui } from '@lingui/react/macro'; -import { IconComponent, IconGoogle, IconMicrosoft } from 'twenty-ui/display'; const ProviderIcons: { [k: string]: IconComponent } = { google: IconGoogle, microsoft: IconMicrosoft, + imap: IconMail, }; export const SettingsAccountsConnectedAccountsListCard = ({ diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEditImapConnection.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEditImapConnection.tsx new file mode 100644 index 000000000..e2557d9f9 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEditImapConnection.tsx @@ -0,0 +1,93 @@ +import { FormProvider } from 'react-hook-form'; +import { useParams } from 'react-router-dom'; + +import { SetttingsAccountsImapConnectionForm } from '@/settings/accounts/components/SetttingsAccountsImapConnectionForm'; +import { useConnectedImapSmtpCaldavAccount } from '@/settings/accounts/hooks/useConnectedImapSmtpCaldavAccount'; +import { useImapConnectionForm } from '@/settings/accounts/hooks/useImapConnectionForm'; +import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; +import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; +import { SettingsPath } from '@/types/SettingsPath'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; +import { Loader } from 'twenty-ui/feedback'; +import { useNavigateSettings } from '~/hooks/useNavigateSettings'; +import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; + +const StyledLoadingContainer = styled.div` + align-items: center; + display: flex; + height: 200px; + justify-content: center; + width: 100%; +`; + +export const SettingsAccountsEditImapConnection = () => { + const { t } = useLingui(); + const navigate = useNavigateSettings(); + const { connectedAccountId } = useParams<{ connectedAccountId: string }>(); + + const { connectedAccount, loading: accountLoading } = + useConnectedImapSmtpCaldavAccount(connectedAccountId); + + const initialData = { + handle: connectedAccount?.handle || '', + host: connectedAccount?.connectionParameters?.IMAP?.host || '', + port: connectedAccount?.connectionParameters?.IMAP?.port || 993, + secure: connectedAccount?.connectionParameters?.IMAP?.secure ?? true, + password: connectedAccount?.connectionParameters?.IMAP?.password || '', + }; + + const { formMethods, handleSave, handleSubmit, canSave, isSubmitting } = + useImapConnectionForm({ + initialData, + isEditing: true, + connectedAccountId, + }); + + const { control } = formMethods; + + const renderLoadingState = () => ( + + + + ); + + const renderForm = () => ( + // eslint-disable-next-line react/jsx-props-no-spreading + + navigate(SettingsPath.Accounts)} + onSave={handleSubmit(handleSave)} + /> + } + > + + + + + + ); + + if (accountLoading === true) { + return renderLoadingState(); + } + + return renderForm(); +}; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx index e2b36ba6d..a0e948bae 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx @@ -3,13 +3,17 @@ import { isGoogleMessagingEnabledState } from '@/client-config/states/isGoogleMe import { isMicrosoftCalendarEnabledState } from '@/client-config/states/isMicrosoftCalendarEnabledState'; import { isMicrosoftMessagingEnabledState } from '@/client-config/states/isMicrosoftMessagingEnabledState'; import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth'; +import { SettingsPath } from '@/types/SettingsPath'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; import { useRecoilValue } from 'recoil'; import { ConnectedAccountProvider } from 'twenty-shared/types'; +import { IconGoogle, IconMail, IconMicrosoft } from 'twenty-ui/display'; import { Button } from 'twenty-ui/input'; import { Card, CardContent, CardHeader } from 'twenty-ui/layout'; -import { IconGoogle, IconMicrosoft } from 'twenty-ui/display'; +import { FeatureFlagKey } from '~/generated-metadata/graphql'; +import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; const StyledHeader = styled(CardHeader)` align-items: center; @@ -47,6 +51,8 @@ export const SettingsAccountsListEmptyStateCard = ({ isMicrosoftCalendarEnabledState, ); + const isImapEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_IMAP_ENABLED); + return ( {label || t`No connected account`} @@ -68,6 +74,15 @@ export const SettingsAccountsListEmptyStateCard = ({ onClick={() => triggerApisOAuth(ConnectedAccountProvider.MICROSOFT)} /> )} + + {isImapEnabled && ( +