feat: SMTP Driver Integration (#12993)
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
141
nx.json
141
nx.json
@ -1,6 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"workspaceLayout": {
|
||||||
|
"appsDir": "packages",
|
||||||
|
"libsDir": "packages"
|
||||||
|
},
|
||||||
"namedInputs": {
|
"namedInputs": {
|
||||||
"default": ["{projectRoot}/**/*"],
|
"default": [
|
||||||
|
"{projectRoot}/**/*"
|
||||||
|
],
|
||||||
"excludeStories": [
|
"excludeStories": [
|
||||||
"default",
|
"default",
|
||||||
"!{projectRoot}/.storybook/*",
|
"!{projectRoot}/.storybook/*",
|
||||||
@ -28,17 +34,26 @@
|
|||||||
"targetDefaults": {
|
"targetDefaults": {
|
||||||
"build": {
|
"build": {
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"inputs": ["^production", "production"],
|
"inputs": [
|
||||||
"dependsOn": ["^build"]
|
"^production",
|
||||||
|
"production"
|
||||||
|
],
|
||||||
|
"dependsOn": [
|
||||||
|
"^build"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"start": {
|
"start": {
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"dependsOn": ["^build"]
|
"dependsOn": [
|
||||||
|
"^build"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"executor": "@nx/eslint:lint",
|
"executor": "@nx/eslint:lint",
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"outputs": ["{options.outputFile}"],
|
"outputs": [
|
||||||
|
"{options.outputFile}"
|
||||||
|
],
|
||||||
"options": {
|
"options": {
|
||||||
"eslintConfig": "{projectRoot}/.eslintrc.cjs",
|
"eslintConfig": "{projectRoot}/.eslintrc.cjs",
|
||||||
"cache": true,
|
"cache": true,
|
||||||
@ -46,10 +61,16 @@
|
|||||||
"ignorePath": "{workspaceRoot}/.gitignore"
|
"ignorePath": "{workspaceRoot}/.gitignore"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"ci": { "cacheStrategy": "content" },
|
"ci": {
|
||||||
"fix": { "fix": true }
|
"cacheStrategy": "content"
|
||||||
},
|
},
|
||||||
"dependsOn": ["^build"]
|
"fix": {
|
||||||
|
"fix": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependsOn": [
|
||||||
|
"^build"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"fmt": {
|
"fmt": {
|
||||||
"executor": "nx:run-commands",
|
"executor": "nx:run-commands",
|
||||||
@ -63,10 +84,16 @@
|
|||||||
"write": false
|
"write": false
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"ci": { "cacheStrategy": "content" },
|
"ci": {
|
||||||
"fix": { "write": true }
|
"cacheStrategy": "content"
|
||||||
},
|
},
|
||||||
"dependsOn": ["^build"]
|
"fix": {
|
||||||
|
"write": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependsOn": [
|
||||||
|
"^build"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"typecheck": {
|
"typecheck": {
|
||||||
"executor": "nx:run-commands",
|
"executor": "nx:run-commands",
|
||||||
@ -76,24 +103,34 @@
|
|||||||
"command": "tsc -b tsconfig.json --incremental"
|
"command": "tsc -b tsconfig.json --incremental"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"watch": { "watch": true }
|
"watch": {
|
||||||
|
"watch": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"dependsOn": ["^build"]
|
"dependsOn": [
|
||||||
|
"^build"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"executor": "@nx/jest:jest",
|
"executor": "@nx/jest:jest",
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": [
|
||||||
|
"^build"
|
||||||
|
],
|
||||||
"inputs": [
|
"inputs": [
|
||||||
"^default",
|
"^default",
|
||||||
"excludeStories",
|
"excludeStories",
|
||||||
"{workspaceRoot}/jest.preset.js"
|
"{workspaceRoot}/jest.preset.js"
|
||||||
],
|
],
|
||||||
"outputs": ["{projectRoot}/coverage"],
|
"outputs": [
|
||||||
|
"{projectRoot}/coverage"
|
||||||
|
],
|
||||||
"options": {
|
"options": {
|
||||||
"jestConfig": "{projectRoot}/jest.config.ts",
|
"jestConfig": "{projectRoot}/jest.config.ts",
|
||||||
"coverage": true,
|
"coverage": true,
|
||||||
"coverageReporters": ["text-summary"],
|
"coverageReporters": [
|
||||||
|
"text-summary"
|
||||||
|
],
|
||||||
"cacheDirectory": "../../.cache/jest/{projectRoot}"
|
"cacheDirectory": "../../.cache/jest/{projectRoot}"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
@ -101,31 +138,49 @@
|
|||||||
"ci": true,
|
"ci": true,
|
||||||
"maxWorkers": 3
|
"maxWorkers": 3
|
||||||
},
|
},
|
||||||
"coverage": { "coverageReporters": ["lcov", "text"] },
|
"coverage": {
|
||||||
"watch": { "watch": true }
|
"coverageReporters": [
|
||||||
|
"lcov",
|
||||||
|
"text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"watch": {
|
||||||
|
"watch": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test:e2e": {
|
"test:e2e": {
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"dependsOn": ["^build"]
|
"dependsOn": [
|
||||||
|
"^build"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"storybook:build": {
|
"storybook:build": {
|
||||||
"executor": "nx:run-commands",
|
"executor": "nx:run-commands",
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"inputs": ["^default", "excludeTests"],
|
"inputs": [
|
||||||
"outputs": ["{projectRoot}/{options.output-dir}"],
|
"^default",
|
||||||
|
"excludeTests"
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
"{projectRoot}/{options.output-dir}"
|
||||||
|
],
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "{projectRoot}",
|
"cwd": "{projectRoot}",
|
||||||
"command": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true storybook build --test",
|
"command": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true storybook build --test",
|
||||||
"output-dir": "storybook-static",
|
"output-dir": "storybook-static",
|
||||||
"config-dir": ".storybook"
|
"config-dir": ".storybook"
|
||||||
},
|
},
|
||||||
"dependsOn": ["^build"]
|
"dependsOn": [
|
||||||
|
"^build"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"storybook:serve:dev": {
|
"storybook:serve:dev": {
|
||||||
"executor": "nx:run-commands",
|
"executor": "nx:run-commands",
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": [
|
||||||
|
"^build"
|
||||||
|
],
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "{projectRoot}",
|
"cwd": "{projectRoot}",
|
||||||
"command": "storybook dev",
|
"command": "storybook dev",
|
||||||
@ -134,7 +189,9 @@
|
|||||||
},
|
},
|
||||||
"storybook:serve:static": {
|
"storybook:serve:static": {
|
||||||
"executor": "nx:run-commands",
|
"executor": "nx:run-commands",
|
||||||
"dependsOn": ["storybook:build"],
|
"dependsOn": [
|
||||||
|
"storybook:build"
|
||||||
|
],
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "{projectRoot}",
|
"cwd": "{projectRoot}",
|
||||||
"command": "npx http-server {args.staticDir} -a={args.host} --port={args.port} --silent={args.silent}",
|
"command": "npx http-server {args.staticDir} -a={args.host} --port={args.port} --silent={args.silent}",
|
||||||
@ -147,8 +204,13 @@
|
|||||||
"storybook:test": {
|
"storybook:test": {
|
||||||
"executor": "nx:run-commands",
|
"executor": "nx:run-commands",
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"inputs": ["^default", "excludeTests"],
|
"inputs": [
|
||||||
"outputs": ["{projectRoot}/coverage/storybook"],
|
"^default",
|
||||||
|
"excludeTests"
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
"{projectRoot}/coverage/storybook"
|
||||||
|
],
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "{projectRoot}",
|
"cwd": "{projectRoot}",
|
||||||
"commands": [
|
"commands": [
|
||||||
@ -164,7 +226,10 @@
|
|||||||
},
|
},
|
||||||
"storybook:test:no-coverage": {
|
"storybook:test:no-coverage": {
|
||||||
"executor": "nx:run-commands",
|
"executor": "nx:run-commands",
|
||||||
"inputs": ["^default", "excludeTests"],
|
"inputs": [
|
||||||
|
"^default",
|
||||||
|
"excludeTests"
|
||||||
|
],
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "{projectRoot}",
|
"cwd": "{projectRoot}",
|
||||||
"commands": [
|
"commands": [
|
||||||
@ -192,7 +257,9 @@
|
|||||||
"checkCoverage": true
|
"checkCoverage": true
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"text": { "reporter": "text" }
|
"text": {
|
||||||
|
"reporter": "text"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"storybook:serve-and-test:static": {
|
"storybook:serve-and-test:static": {
|
||||||
@ -252,12 +319,20 @@
|
|||||||
},
|
},
|
||||||
"@nx/vite:test": {
|
"@nx/vite:test": {
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"inputs": ["default", "^default"]
|
"inputs": [
|
||||||
|
"default",
|
||||||
|
"^default"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"@nx/vite:build": {
|
"@nx/vite:build": {
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": [
|
||||||
"inputs": ["default", "^default"]
|
"^build"
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
"default",
|
||||||
|
"^default"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"installation": {
|
"installation": {
|
||||||
@ -289,7 +364,9 @@
|
|||||||
"tasksRunnerOptions": {
|
"tasksRunnerOptions": {
|
||||||
"default": {
|
"default": {
|
||||||
"options": {
|
"options": {
|
||||||
"cacheableOperations": ["storybook:build"]
|
"cacheableOperations": [
|
||||||
|
"storybook:build"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -435,7 +435,6 @@ export type ConnectionParameters = {
|
|||||||
password: Scalars['String'];
|
password: Scalars['String'];
|
||||||
port: Scalars['Float'];
|
port: Scalars['Float'];
|
||||||
secure?: InputMaybe<Scalars['Boolean']>;
|
secure?: InputMaybe<Scalars['Boolean']>;
|
||||||
username: Scalars['String'];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConnectionParametersOutput = {
|
export type ConnectionParametersOutput = {
|
||||||
@ -444,7 +443,6 @@ export type ConnectionParametersOutput = {
|
|||||||
password: Scalars['String'];
|
password: Scalars['String'];
|
||||||
port: Scalars['Float'];
|
port: Scalars['Float'];
|
||||||
secure?: Maybe<Scalars['Boolean']>;
|
secure?: Maybe<Scalars['Boolean']>;
|
||||||
username: Scalars['String'];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateApiKeyDto = {
|
export type CreateApiKeyDto = {
|
||||||
@ -708,7 +706,7 @@ export enum FeatureFlagKey {
|
|||||||
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
||||||
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
||||||
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||||
IS_IMAP_ENABLED = 'IS_IMAP_ENABLED',
|
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
||||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||||
IS_MORPH_RELATION_ENABLED = 'IS_MORPH_RELATION_ENABLED',
|
IS_MORPH_RELATION_ENABLED = 'IS_MORPH_RELATION_ENABLED',
|
||||||
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
|
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
|
||||||
@ -3270,7 +3268,7 @@ export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
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 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, password: string } | null, SMTP?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, password: string } | null, CALDAV?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, password: string } | null } | null } };
|
||||||
|
|
||||||
export type CreateDatabaseConfigVariableMutationVariables = Exact<{
|
export type CreateDatabaseConfigVariableMutationVariables = Exact<{
|
||||||
key: Scalars['String'];
|
key: Scalars['String'];
|
||||||
@ -6003,21 +6001,18 @@ export const GetConnectedImapSmtpCaldavAccountDocument = gql`
|
|||||||
host
|
host
|
||||||
port
|
port
|
||||||
secure
|
secure
|
||||||
username
|
|
||||||
password
|
password
|
||||||
}
|
}
|
||||||
SMTP {
|
SMTP {
|
||||||
host
|
host
|
||||||
port
|
port
|
||||||
secure
|
secure
|
||||||
username
|
|
||||||
password
|
password
|
||||||
}
|
}
|
||||||
CALDAV {
|
CALDAV {
|
||||||
host
|
host
|
||||||
port
|
port
|
||||||
secure
|
secure
|
||||||
username
|
|
||||||
password
|
password
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -435,7 +435,6 @@ export type ConnectionParameters = {
|
|||||||
password: Scalars['String'];
|
password: Scalars['String'];
|
||||||
port: Scalars['Float'];
|
port: Scalars['Float'];
|
||||||
secure?: InputMaybe<Scalars['Boolean']>;
|
secure?: InputMaybe<Scalars['Boolean']>;
|
||||||
username: Scalars['String'];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConnectionParametersOutput = {
|
export type ConnectionParametersOutput = {
|
||||||
@ -444,7 +443,6 @@ export type ConnectionParametersOutput = {
|
|||||||
password: Scalars['String'];
|
password: Scalars['String'];
|
||||||
port: Scalars['Float'];
|
port: Scalars['Float'];
|
||||||
secure?: Maybe<Scalars['Boolean']>;
|
secure?: Maybe<Scalars['Boolean']>;
|
||||||
username: Scalars['String'];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateApiKeyDto = {
|
export type CreateApiKeyDto = {
|
||||||
@ -672,7 +670,7 @@ export enum FeatureFlagKey {
|
|||||||
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
||||||
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
||||||
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||||
IS_IMAP_ENABLED = 'IS_IMAP_ENABLED',
|
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
||||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||||
IS_MORPH_RELATION_ENABLED = 'IS_MORPH_RELATION_ENABLED',
|
IS_MORPH_RELATION_ENABLED = 'IS_MORPH_RELATION_ENABLED',
|
||||||
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
|
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
|
||||||
|
|||||||
@ -64,19 +64,19 @@ const SettingsNewObject = lazy(() =>
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const SettingsNewImapConnection = lazy(() =>
|
const SettingsNewImapSmtpCaldavConnection = lazy(() =>
|
||||||
import(
|
import(
|
||||||
'@/settings/accounts/components/SettingsAccountsNewImapConnection'
|
'@/settings/accounts/components/SettingsAccountsNewImapSmtpCaldavConnection'
|
||||||
).then((module) => ({
|
).then((module) => ({
|
||||||
default: module.SettingsAccountsNewImapConnection,
|
default: module.SettingsAccountsNewImapSmtpCaldavConnection,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const SettingsEditImapConnection = lazy(() =>
|
const SettingsEditImapSmtpCaldavConnection = lazy(() =>
|
||||||
import(
|
import(
|
||||||
'@/settings/accounts/components/SettingsAccountsEditImapConnection'
|
'@/settings/accounts/components/SettingsAccountsEditImapSmtpCaldavConnection'
|
||||||
).then((module) => ({
|
).then((module) => ({
|
||||||
default: module.SettingsAccountsEditImapConnection,
|
default: module.SettingsAccountsEditImapSmtpCaldavConnection,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -375,12 +375,12 @@ export const SettingsRoutes = ({
|
|||||||
element={<SettingsAccountsEmails />}
|
element={<SettingsAccountsEmails />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={SettingsPath.NewImapConnection}
|
path={SettingsPath.NewImapSmtpCaldavConnection}
|
||||||
element={<SettingsNewImapConnection />}
|
element={<SettingsNewImapSmtpCaldavConnection />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={SettingsPath.EditImapConnection}
|
path={SettingsPath.EditImapSmtpCaldavConnection}
|
||||||
element={<SettingsEditImapConnection />}
|
element={<SettingsEditImapSmtpCaldavConnection />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
element={
|
element={
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/i
|
|||||||
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
|
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
|
||||||
import { isGoogleCalendarEnabledState } from '@/client-config/states/isGoogleCalendarEnabledState';
|
import { isGoogleCalendarEnabledState } from '@/client-config/states/isGoogleCalendarEnabledState';
|
||||||
import { isGoogleMessagingEnabledState } from '@/client-config/states/isGoogleMessagingEnabledState';
|
import { isGoogleMessagingEnabledState } from '@/client-config/states/isGoogleMessagingEnabledState';
|
||||||
|
import { isImapSmtpCaldavEnabledState } from '@/client-config/states/isImapSmtpCaldavEnabledState';
|
||||||
import { isMicrosoftCalendarEnabledState } from '@/client-config/states/isMicrosoftCalendarEnabledState';
|
import { isMicrosoftCalendarEnabledState } from '@/client-config/states/isMicrosoftCalendarEnabledState';
|
||||||
import { isMicrosoftMessagingEnabledState } from '@/client-config/states/isMicrosoftMessagingEnabledState';
|
import { isMicrosoftMessagingEnabledState } from '@/client-config/states/isMicrosoftMessagingEnabledState';
|
||||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||||
@ -92,6 +93,10 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
calendarBookingPageIdState,
|
calendarBookingPageIdState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setIsImapSmtpCaldavEnabled = useSetRecoilState(
|
||||||
|
isImapSmtpCaldavEnabledState,
|
||||||
|
);
|
||||||
|
|
||||||
const { data, loading, error, fetchClientConfig } = useClientConfig();
|
const { data, loading, error, fetchClientConfig } = useClientConfig();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -183,6 +188,7 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
setCalendarBookingPageId(data?.clientConfig?.calendarBookingPageId ?? null);
|
setCalendarBookingPageId(data?.clientConfig?.calendarBookingPageId ?? null);
|
||||||
|
setIsImapSmtpCaldavEnabled(data?.clientConfig?.isImapSmtpCaldavEnabled);
|
||||||
}, [
|
}, [
|
||||||
data,
|
data,
|
||||||
loading,
|
loading,
|
||||||
@ -210,6 +216,7 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
setIsAttachmentPreviewEnabled,
|
setIsAttachmentPreviewEnabled,
|
||||||
setIsConfigVariablesInDbEnabled,
|
setIsConfigVariablesInDbEnabled,
|
||||||
setCalendarBookingPageId,
|
setCalendarBookingPageId,
|
||||||
|
setIsImapSmtpCaldavEnabled,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { createState } from 'twenty-ui/utilities';
|
||||||
|
export const isImapSmtpCaldavEnabledState = createState<boolean>({
|
||||||
|
key: 'isImapSmtpCaldavEnabled',
|
||||||
|
defaultValue: false,
|
||||||
|
});
|
||||||
@ -30,7 +30,7 @@ export type ClientConfig = {
|
|||||||
isMicrosoftCalendarEnabled: boolean;
|
isMicrosoftCalendarEnabled: boolean;
|
||||||
isMicrosoftMessagingEnabled: boolean;
|
isMicrosoftMessagingEnabled: boolean;
|
||||||
isMultiWorkspaceEnabled: boolean;
|
isMultiWorkspaceEnabled: boolean;
|
||||||
isIMAPMessagingEnabled: boolean;
|
isImapSmtpCaldavEnabled: boolean;
|
||||||
publicFeatureFlags: Array<PublicFeatureFlag>;
|
publicFeatureFlags: Array<PublicFeatureFlag>;
|
||||||
sentry: Sentry;
|
sentry: Sentry;
|
||||||
signInPrefilled: boolean;
|
signInPrefilled: boolean;
|
||||||
|
|||||||
@ -4,21 +4,11 @@ import { SettingsPath } from '@/types/SettingsPath';
|
|||||||
|
|
||||||
import { SettingsAccountsConnectedAccountsRowRightContainer } from '@/settings/accounts/components/SettingsAccountsConnectedAccountsRowRightContainer';
|
import { SettingsAccountsConnectedAccountsRowRightContainer } from '@/settings/accounts/components/SettingsAccountsConnectedAccountsRowRightContainer';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import {
|
|
||||||
IconComponent,
|
import { SettingsConnectedAccountIcon } from '@/settings/accounts/components/SettingsConnectedAccountIcon';
|
||||||
IconGoogle,
|
|
||||||
IconMail,
|
|
||||||
IconMicrosoft,
|
|
||||||
} from 'twenty-ui/display';
|
|
||||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||||
import { SettingsListCard } from '../../components/SettingsListCard';
|
import { SettingsListCard } from '../../components/SettingsListCard';
|
||||||
|
|
||||||
const ProviderIcons: { [k: string]: IconComponent } = {
|
|
||||||
google: IconGoogle,
|
|
||||||
microsoft: IconMicrosoft,
|
|
||||||
imap: IconMail,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SettingsAccountsConnectedAccountsListCard = ({
|
export const SettingsAccountsConnectedAccountsListCard = ({
|
||||||
accounts,
|
accounts,
|
||||||
loading,
|
loading,
|
||||||
@ -38,7 +28,7 @@ export const SettingsAccountsConnectedAccountsListCard = ({
|
|||||||
items={accounts}
|
items={accounts}
|
||||||
getItemLabel={(account) => account.handle}
|
getItemLabel={(account) => account.handle}
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
RowIconFn={(row) => ProviderIcons[row.provider]}
|
RowIconFn={(row) => SettingsConnectedAccountIcon({ account: row })}
|
||||||
RowRightComponent={({ item: account }) => (
|
RowRightComponent={({ item: account }) => (
|
||||||
<SettingsAccountsConnectedAccountsRowRightContainer account={account} />
|
<SettingsAccountsConnectedAccountsRowRightContainer account={account} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -0,0 +1,354 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
import { Control, Controller } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Select } from '@/ui/input/components/Select';
|
||||||
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
|
||||||
|
import { ConnectionFormData } from '@/settings/accounts/hooks/useImapSmtpCaldavConnectionForm';
|
||||||
|
import { H2Title } from 'twenty-ui/display';
|
||||||
|
import { Section } from 'twenty-ui/layout';
|
||||||
|
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
|
||||||
|
|
||||||
|
const StyledFormContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(6)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledConnectionSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledSectionHeader = styled.div`
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledSectionTitle = styled.h3`
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledSectionDescription = styled.p`
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
|
margin: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledFieldRow = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(3)};
|
||||||
|
|
||||||
|
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledFieldGroup = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type SettingsAccountsConnectionFormProps = {
|
||||||
|
control: Control<ConnectionFormData>;
|
||||||
|
isEditing: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsAccountsConnectionForm = ({
|
||||||
|
control,
|
||||||
|
isEditing,
|
||||||
|
}: SettingsAccountsConnectionFormProps) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
const getTitle = () => {
|
||||||
|
return isEditing ? t`Edit Email Account` : t`New Email Account`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDescription = () => {
|
||||||
|
if (isEditing) {
|
||||||
|
return t`Update your email account configuration. Configure any combination of IMAP, SMTP, and CalDAV as needed.`;
|
||||||
|
}
|
||||||
|
return t`You can set up any combination of IMAP (receiving emails), SMTP (sending emails), and CalDAV (calendar sync).`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePortChange = (value: string) => Number(value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section>
|
||||||
|
<H2Title title={getTitle()} description={getDescription()} />
|
||||||
|
<StyledFormContainer>
|
||||||
|
<Controller
|
||||||
|
name="handle"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextInput
|
||||||
|
instanceId="email-address-connection-form"
|
||||||
|
label={t`Email Address`}
|
||||||
|
placeholder={t`john.doe@example.com`}
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StyledConnectionSection>
|
||||||
|
<StyledSectionHeader>
|
||||||
|
<StyledSectionTitle>{t`IMAP Configuration`}</StyledSectionTitle>
|
||||||
|
<StyledSectionDescription>
|
||||||
|
{t`Configure IMAP settings to receive and sync your emails.`}
|
||||||
|
<br />
|
||||||
|
{t`Leave blank if you don't need to import emails.`}
|
||||||
|
</StyledSectionDescription>
|
||||||
|
</StyledSectionHeader>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="IMAP.host"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextInput
|
||||||
|
instanceId="imap-host-connection-form"
|
||||||
|
label={t`IMAP Server`}
|
||||||
|
placeholder={t`imap.example.com`}
|
||||||
|
value={field.value || ''}
|
||||||
|
onChange={field.onChange}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="IMAP.password"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextInput
|
||||||
|
instanceId="imap-password-connection-form"
|
||||||
|
label={t`IMAP Password`}
|
||||||
|
placeholder={t`••••••••`}
|
||||||
|
type="password"
|
||||||
|
value={field.value || ''}
|
||||||
|
onChange={field.onChange}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StyledFieldRow>
|
||||||
|
<StyledFieldGroup>
|
||||||
|
<Controller
|
||||||
|
name="IMAP.port"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextInput
|
||||||
|
instanceId="imap-port-connection-form"
|
||||||
|
label={t`IMAP Port`}
|
||||||
|
type="number"
|
||||||
|
placeholder="993"
|
||||||
|
value={field?.value ? field.value : 993}
|
||||||
|
onChange={(value) =>
|
||||||
|
field.onChange(handlePortChange(value))
|
||||||
|
}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledFieldGroup>
|
||||||
|
|
||||||
|
<StyledFieldGroup>
|
||||||
|
<Controller
|
||||||
|
name="IMAP.secure"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
label={t`IMAP Encryption`}
|
||||||
|
options={[
|
||||||
|
{ label: 'SSL/TLS', value: true },
|
||||||
|
{ label: 'None', value: false },
|
||||||
|
]}
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
dropdownId="imap-secure-dropdown"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledFieldGroup>
|
||||||
|
</StyledFieldRow>
|
||||||
|
</StyledConnectionSection>
|
||||||
|
|
||||||
|
<StyledConnectionSection>
|
||||||
|
<StyledSectionHeader>
|
||||||
|
<StyledSectionTitle>{t`SMTP Configuration`}</StyledSectionTitle>
|
||||||
|
<StyledSectionDescription>
|
||||||
|
{t`Configure SMTP settings to send emails from your account.`}
|
||||||
|
<br />
|
||||||
|
{t`Leave blank if you don't need to send emails.`}
|
||||||
|
</StyledSectionDescription>
|
||||||
|
</StyledSectionHeader>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="SMTP.host"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextInput
|
||||||
|
instanceId="smtp-host-connection-form"
|
||||||
|
label={t`SMTP Server`}
|
||||||
|
placeholder={t`smtp.example.com`}
|
||||||
|
value={field.value || ''}
|
||||||
|
onChange={field.onChange}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="SMTP.password"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextInput
|
||||||
|
instanceId="smtp-password-connection-form"
|
||||||
|
label={t`SMTP Password`}
|
||||||
|
placeholder={t`••••••••`}
|
||||||
|
type="password"
|
||||||
|
value={field.value || ''}
|
||||||
|
onChange={field.onChange}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StyledFieldRow>
|
||||||
|
<StyledFieldGroup>
|
||||||
|
<Controller
|
||||||
|
name="SMTP.port"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextInput
|
||||||
|
instanceId="smtp-port-connection-form"
|
||||||
|
label={t`SMTP Port`}
|
||||||
|
type="number"
|
||||||
|
placeholder="587"
|
||||||
|
value={field?.value ? field.value : 587}
|
||||||
|
onChange={(value) =>
|
||||||
|
field.onChange(handlePortChange(value))
|
||||||
|
}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledFieldGroup>
|
||||||
|
|
||||||
|
<StyledFieldGroup>
|
||||||
|
<Controller
|
||||||
|
name="SMTP.secure"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
label={t`SMTP Encryption`}
|
||||||
|
options={[
|
||||||
|
{ label: 'SSL/TLS', value: true },
|
||||||
|
{ label: 'STARTTLS', value: false },
|
||||||
|
]}
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
dropdownId="smtp-secure-dropdown"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledFieldGroup>
|
||||||
|
</StyledFieldRow>
|
||||||
|
</StyledConnectionSection>
|
||||||
|
|
||||||
|
<StyledConnectionSection>
|
||||||
|
<StyledSectionHeader>
|
||||||
|
<StyledSectionTitle>{t`CalDAV Configuration`}</StyledSectionTitle>
|
||||||
|
<StyledSectionDescription>
|
||||||
|
{t`Configure CalDAV settings to sync your calendar events.`}
|
||||||
|
<br />
|
||||||
|
{t`Leave blank if you don't need calendar sync.`}
|
||||||
|
</StyledSectionDescription>
|
||||||
|
</StyledSectionHeader>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="CALDAV.host"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextInput
|
||||||
|
instanceId="caldav-host-connection-form"
|
||||||
|
label={t`CalDAV Server`}
|
||||||
|
placeholder={t`caldav.example.com`}
|
||||||
|
value={field.value || ''}
|
||||||
|
onChange={field.onChange}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="CALDAV.password"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextInput
|
||||||
|
instanceId="caldav-password-connection-form"
|
||||||
|
label={t`CalDAV Password`}
|
||||||
|
placeholder={t`••••••••`}
|
||||||
|
type="password"
|
||||||
|
value={field.value || ''}
|
||||||
|
onChange={field.onChange}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StyledFieldRow>
|
||||||
|
<StyledFieldGroup>
|
||||||
|
<Controller
|
||||||
|
name="CALDAV.port"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextInput
|
||||||
|
instanceId="caldav-port-connection-form"
|
||||||
|
label={t`CalDAV Port`}
|
||||||
|
type="number"
|
||||||
|
placeholder="443"
|
||||||
|
value={field?.value ? field.value : 443}
|
||||||
|
onChange={(value) =>
|
||||||
|
field.onChange(handlePortChange(value))
|
||||||
|
}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledFieldGroup>
|
||||||
|
|
||||||
|
<StyledFieldGroup>
|
||||||
|
<Controller
|
||||||
|
name="CALDAV.secure"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
label={t`CalDAV Encryption`}
|
||||||
|
options={[
|
||||||
|
{ label: 'SSL/TLS', value: true },
|
||||||
|
{ label: 'None', value: false },
|
||||||
|
]}
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
dropdownId="caldav-secure-dropdown"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledFieldGroup>
|
||||||
|
</StyledFieldRow>
|
||||||
|
</StyledConnectionSection>
|
||||||
|
</StyledFormContainer>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,93 +1,93 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { FormProvider } from 'react-hook-form';
|
import { FormProvider } from 'react-hook-form';
|
||||||
import { useParams } from 'react-router-dom';
|
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 { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
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 { Loader } from 'twenty-ui/feedback';
|
||||||
|
|
||||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
|
|
||||||
|
import { NotFound } from '~/pages/not-found/NotFound';
|
||||||
|
import { useImapSmtpCaldavConnectionForm } from '../hooks/useImapSmtpCaldavConnectionForm';
|
||||||
|
import { SettingsAccountsConnectionForm } from './SettingsAccountsConnectionForm';
|
||||||
|
|
||||||
const StyledLoadingContainer = styled.div`
|
const StyledLoadingContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SettingsAccountsEditImapConnection = () => {
|
export const SettingsAccountsEditImapSmtpCaldavConnection = () => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const navigate = useNavigateSettings();
|
const navigate = useNavigateSettings();
|
||||||
const { connectedAccountId } = useParams<{ connectedAccountId: string }>();
|
const { connectedAccountId } = useParams<{ connectedAccountId: string }>();
|
||||||
|
|
||||||
const { connectedAccount, loading: accountLoading } =
|
const {
|
||||||
useConnectedImapSmtpCaldavAccount(connectedAccountId);
|
formMethods,
|
||||||
|
handleSave,
|
||||||
const initialData = {
|
handleSubmit,
|
||||||
handle: connectedAccount?.handle || '',
|
canSave,
|
||||||
host: connectedAccount?.connectionParameters?.IMAP?.host || '',
|
isSubmitting,
|
||||||
port: connectedAccount?.connectionParameters?.IMAP?.port || 993,
|
loading,
|
||||||
secure: connectedAccount?.connectionParameters?.IMAP?.secure ?? true,
|
connectedAccount,
|
||||||
password: connectedAccount?.connectionParameters?.IMAP?.password || '',
|
} = useImapSmtpCaldavConnectionForm({
|
||||||
};
|
|
||||||
|
|
||||||
const { formMethods, handleSave, handleSubmit, canSave, isSubmitting } =
|
|
||||||
useImapConnectionForm({
|
|
||||||
initialData,
|
|
||||||
isEditing: true,
|
isEditing: true,
|
||||||
connectedAccountId,
|
connectedAccountId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { control } = formMethods;
|
const { control } = formMethods;
|
||||||
|
|
||||||
const renderLoadingState = () => (
|
if (loading && !connectedAccount) {
|
||||||
|
return (
|
||||||
<StyledLoadingContainer>
|
<StyledLoadingContainer>
|
||||||
<Loader />
|
<Loader />
|
||||||
</StyledLoadingContainer>
|
</StyledLoadingContainer>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connectedAccount && !loading) {
|
||||||
|
return <NotFound />;
|
||||||
|
}
|
||||||
|
|
||||||
const renderForm = () => (
|
const renderForm = () => (
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
<SubMenuTopBarContainer
|
<SubMenuTopBarContainer
|
||||||
title={t`Edit IMAP Connection`}
|
title={t`Edit Email Account`}
|
||||||
links={[
|
links={[
|
||||||
{
|
{
|
||||||
children: t`Settings`,
|
children: t`Workspace`,
|
||||||
href: getSettingsPath(SettingsPath.Workspace),
|
href: getSettingsPath(SettingsPath.Workspace),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
children: t`Email Connections`,
|
children: t`Accounts`,
|
||||||
href: getSettingsPath(SettingsPath.Accounts),
|
href: getSettingsPath(SettingsPath.Accounts),
|
||||||
},
|
},
|
||||||
{ children: t`Edit IMAP Connection` },
|
{ children: t`Edit Email Account` },
|
||||||
]}
|
]}
|
||||||
actionButton={
|
actionButton={
|
||||||
<SaveAndCancelButtons
|
<SaveAndCancelButtons
|
||||||
isSaveDisabled={!canSave}
|
isSaveDisabled={!canSave}
|
||||||
isCancelDisabled={isSubmitting}
|
isCancelDisabled={isSubmitting}
|
||||||
|
isLoading={loading}
|
||||||
onCancel={() => navigate(SettingsPath.Accounts)}
|
onCancel={() => navigate(SettingsPath.Accounts)}
|
||||||
onSave={handleSubmit(handleSave)}
|
onSave={handleSubmit((data) => handleSave(data))}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SetttingsAccountsImapConnectionForm control={control} isEditing />
|
<SettingsAccountsConnectionForm control={control} isEditing />
|
||||||
</SettingsPageContainer>
|
</SettingsPageContainer>
|
||||||
</SubMenuTopBarContainer>
|
</SubMenuTopBarContainer>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (accountLoading === true) {
|
|
||||||
return renderLoadingState();
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderForm();
|
return renderForm();
|
||||||
};
|
};
|
||||||
@ -9,7 +9,7 @@ import styled from '@emotion/styled';
|
|||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||||
import { IconGoogle, IconMail, IconMicrosoft } from 'twenty-ui/display';
|
import { IconAt, IconGoogle, IconMicrosoft } from 'twenty-ui/display';
|
||||||
import { Button } from 'twenty-ui/input';
|
import { Button } from 'twenty-ui/input';
|
||||||
import { Card, CardContent, CardHeader } from 'twenty-ui/layout';
|
import { Card, CardContent, CardHeader } from 'twenty-ui/layout';
|
||||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||||
@ -51,7 +51,9 @@ export const SettingsAccountsListEmptyStateCard = ({
|
|||||||
isMicrosoftCalendarEnabledState,
|
isMicrosoftCalendarEnabledState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isImapEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_IMAP_ENABLED);
|
const isImapSmtpCaldavFeatureFlagEnabled = useIsFeatureEnabled(
|
||||||
|
FeatureFlagKey.IS_IMAP_SMTP_CALDAV_ENABLED,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
@ -75,12 +77,12 @@ export const SettingsAccountsListEmptyStateCard = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isImapEnabled && (
|
{isImapSmtpCaldavFeatureFlagEnabled && (
|
||||||
<Button
|
<Button
|
||||||
Icon={IconMail}
|
Icon={IconAt}
|
||||||
title={t`Connect with IMAP`}
|
title={t`Connect Email Account`}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
to={getSettingsPath(SettingsPath.NewImapConnection)}
|
to={getSettingsPath(SettingsPath.NewImapSmtpCaldavConnection)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</StyledBody>
|
</StyledBody>
|
||||||
|
|||||||
@ -44,6 +44,9 @@ export const SettingsAccountsMessageChannelsContainer = () => {
|
|||||||
connectedAccountId: {
|
connectedAccountId: {
|
||||||
in: accounts.map((account) => account.id),
|
in: accounts.map((account) => account.id),
|
||||||
},
|
},
|
||||||
|
isSyncEnabled: {
|
||||||
|
eq: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
skip: !accounts.length,
|
skip: !accounts.length,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,21 +1,29 @@
|
|||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { FormProvider } from 'react-hook-form';
|
import { FormProvider } from 'react-hook-form';
|
||||||
|
|
||||||
import { SetttingsAccountsImapConnectionForm } from '@/settings/accounts/components/SetttingsAccountsImapConnectionForm';
|
|
||||||
import { useImapConnectionForm } from '@/settings/accounts/hooks/useImapConnectionForm';
|
|
||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
|
||||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
|
|
||||||
export const SettingsAccountsNewImapConnection = () => {
|
import { SettingsAccountsConnectionForm } from '@/settings/accounts/components/SettingsAccountsConnectionForm';
|
||||||
|
import { useImapSmtpCaldavConnectionForm } from '../hooks/useImapSmtpCaldavConnectionForm';
|
||||||
|
|
||||||
|
export const SettingsAccountsNewImapSmtpCaldavConnection = () => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const navigate = useNavigateSettings();
|
const navigate = useNavigateSettings();
|
||||||
|
|
||||||
const { formMethods, handleSave, handleSubmit, canSave, isSubmitting } =
|
const {
|
||||||
useImapConnectionForm();
|
formMethods,
|
||||||
|
handleSave,
|
||||||
|
handleSubmit,
|
||||||
|
canSave,
|
||||||
|
isSubmitting,
|
||||||
|
loading,
|
||||||
|
} = useImapSmtpCaldavConnectionForm({});
|
||||||
|
|
||||||
const { control } = formMethods;
|
const { control } = formMethods;
|
||||||
|
|
||||||
@ -23,32 +31,30 @@ export const SettingsAccountsNewImapConnection = () => {
|
|||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
<SubMenuTopBarContainer
|
<SubMenuTopBarContainer
|
||||||
title={t`New IMAP Connection`}
|
title={t`New Email Account`}
|
||||||
links={[
|
links={[
|
||||||
{
|
{
|
||||||
children: t`Settings`,
|
children: t`Workspace`,
|
||||||
href: getSettingsPath(SettingsPath.Workspace),
|
href: getSettingsPath(SettingsPath.Workspace),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
children: t`Email Connections`,
|
children: t`Accounts`,
|
||||||
href: getSettingsPath(SettingsPath.Accounts),
|
href: getSettingsPath(SettingsPath.Accounts),
|
||||||
},
|
},
|
||||||
{ children: t`New IMAP Connection` },
|
{ children: t`New Email Account` },
|
||||||
]}
|
]}
|
||||||
actionButton={
|
actionButton={
|
||||||
<SaveAndCancelButtons
|
<SaveAndCancelButtons
|
||||||
isSaveDisabled={!canSave}
|
isSaveDisabled={!canSave}
|
||||||
isCancelDisabled={isSubmitting}
|
isCancelDisabled={isSubmitting}
|
||||||
|
isLoading={loading}
|
||||||
onCancel={() => navigate(SettingsPath.Accounts)}
|
onCancel={() => navigate(SettingsPath.Accounts)}
|
||||||
onSave={handleSubmit(handleSave)}
|
onSave={handleSubmit((data) => handleSave(data))}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SetttingsAccountsImapConnectionForm
|
<SettingsAccountsConnectionForm control={control} isEditing={false} />
|
||||||
control={control}
|
|
||||||
isEditing={false}
|
|
||||||
/>
|
|
||||||
</SettingsPageContainer>
|
</SettingsPageContainer>
|
||||||
</SubMenuTopBarContainer>
|
</SubMenuTopBarContainer>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
@ -10,11 +10,13 @@ import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
|||||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
|
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||||
import {
|
import {
|
||||||
IconCalendarEvent,
|
IconCalendarEvent,
|
||||||
IconDotsVertical,
|
IconDotsVertical,
|
||||||
IconMail,
|
IconMail,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
|
IconSettings,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
} from 'twenty-ui/display';
|
} from 'twenty-ui/display';
|
||||||
import { LightIconButton } from 'twenty-ui/input';
|
import { LightIconButton } from 'twenty-ui/input';
|
||||||
@ -57,6 +59,19 @@ export const SettingsAccountsRowDropdownMenu = ({
|
|||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
<DropdownContent>
|
<DropdownContent>
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer>
|
||||||
|
{account.provider ===
|
||||||
|
ConnectedAccountProvider.IMAP_SMTP_CALDAV && (
|
||||||
|
<MenuItem
|
||||||
|
text={t`Connection settings`}
|
||||||
|
LeftIcon={IconSettings}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(SettingsPath.EditImapSmtpCaldavConnection, {
|
||||||
|
connectedAccountId: account.id,
|
||||||
|
});
|
||||||
|
closeDropdown(dropdownId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
LeftIcon={IconMail}
|
LeftIcon={IconMail}
|
||||||
text={t`Emails settings`}
|
text={t`Emails settings`}
|
||||||
|
|||||||
@ -0,0 +1,83 @@
|
|||||||
|
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import {
|
||||||
|
IconAt,
|
||||||
|
IconCalendarEvent,
|
||||||
|
IconComponent,
|
||||||
|
IconComponentProps,
|
||||||
|
IconGoogle,
|
||||||
|
IconMail,
|
||||||
|
IconMicrosoft,
|
||||||
|
IconSend,
|
||||||
|
} from 'twenty-ui/display';
|
||||||
|
|
||||||
|
const ImapSmtpCaldavIcon = (
|
||||||
|
props: IconComponentProps & { account: ConnectedAccount },
|
||||||
|
) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { account } = props;
|
||||||
|
|
||||||
|
const hasImap = isDefined(account.connectionParameters?.IMAP);
|
||||||
|
const hasSmtp = isDefined(account.connectionParameters?.SMTP);
|
||||||
|
const hasCaldav = isDefined(account.connectionParameters?.CALDAV);
|
||||||
|
|
||||||
|
let IconToShow: IconComponent;
|
||||||
|
|
||||||
|
if (hasImap && hasSmtp && hasCaldav) {
|
||||||
|
IconToShow = IconAt;
|
||||||
|
} else if (hasImap && hasCaldav) {
|
||||||
|
IconToShow = IconAt;
|
||||||
|
} else if (hasImap && hasSmtp) {
|
||||||
|
IconToShow = IconMail;
|
||||||
|
} else if (hasImap) {
|
||||||
|
IconToShow = IconMail;
|
||||||
|
} else if (hasSmtp) {
|
||||||
|
IconToShow = IconSend;
|
||||||
|
} else if (hasCaldav) {
|
||||||
|
IconToShow = IconCalendarEvent;
|
||||||
|
} else {
|
||||||
|
IconToShow = IconMail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconToShow
|
||||||
|
className={props.className}
|
||||||
|
style={props.style}
|
||||||
|
size={props.size}
|
||||||
|
stroke={props.stroke}
|
||||||
|
color={props.color || theme.font.color.primary}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIconForProvider = (account: ConnectedAccount): IconComponent => {
|
||||||
|
switch (account.provider) {
|
||||||
|
case ConnectedAccountProvider.IMAP_SMTP_CALDAV:
|
||||||
|
return (props) => (
|
||||||
|
<ImapSmtpCaldavIcon
|
||||||
|
account={account}
|
||||||
|
className={props.className}
|
||||||
|
style={props.style}
|
||||||
|
size={props.size}
|
||||||
|
stroke={props.stroke}
|
||||||
|
color={props.color}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case ConnectedAccountProvider.GOOGLE:
|
||||||
|
return IconGoogle;
|
||||||
|
case ConnectedAccountProvider.MICROSOFT:
|
||||||
|
return IconMicrosoft;
|
||||||
|
default:
|
||||||
|
return IconMail;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsConnectedAccountIcon = ({
|
||||||
|
account,
|
||||||
|
}: {
|
||||||
|
account: ConnectedAccount;
|
||||||
|
}): IconComponent => {
|
||||||
|
return getIconForProvider(account);
|
||||||
|
};
|
||||||
@ -1,123 +0,0 @@
|
|||||||
import { Control, Controller } from 'react-hook-form';
|
|
||||||
|
|
||||||
import { Select } from '@/ui/input/components/Select';
|
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { useLingui } from '@lingui/react/macro';
|
|
||||||
import { H2Title } from 'twenty-ui/display';
|
|
||||||
import { Section } from 'twenty-ui/layout';
|
|
||||||
import { ConnectionParameters } from '~/generated/graphql';
|
|
||||||
|
|
||||||
const StyledFormContainer = styled.form`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: ${({ theme }) => theme.spacing(4)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
type SetttingsAccountsImapConnectionFormProps = {
|
|
||||||
control: Control<ConnectionParameters & { handle: string }>;
|
|
||||||
isEditing: boolean;
|
|
||||||
defaultValues?: Partial<ConnectionParameters & { handle: string }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SetttingsAccountsImapConnectionForm = ({
|
|
||||||
control,
|
|
||||||
isEditing,
|
|
||||||
defaultValues,
|
|
||||||
}: SetttingsAccountsImapConnectionFormProps) => {
|
|
||||||
const { t } = useLingui();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Section>
|
|
||||||
<H2Title
|
|
||||||
title={t`IMAP Connection Details`}
|
|
||||||
description={
|
|
||||||
isEditing
|
|
||||||
? t`Update your IMAP email account configuration`
|
|
||||||
: t`Configure your IMAP email account`
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<StyledFormContainer>
|
|
||||||
<Controller
|
|
||||||
name="handle"
|
|
||||||
control={control}
|
|
||||||
defaultValue={defaultValues?.handle}
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<TextInput
|
|
||||||
instanceId="email-address-imap-connection-form"
|
|
||||||
label={t`Email Address`}
|
|
||||||
placeholder={t`john.doe@example.com`}
|
|
||||||
value={field.value}
|
|
||||||
onChange={field.onChange}
|
|
||||||
error={fieldState.error?.message}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
name="host"
|
|
||||||
control={control}
|
|
||||||
defaultValue={defaultValues?.host}
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<TextInput
|
|
||||||
instanceId="host-imap-connection-form"
|
|
||||||
label={t`IMAP Server`}
|
|
||||||
placeholder={t`imap.example.com`}
|
|
||||||
value={field.value}
|
|
||||||
onChange={field.onChange}
|
|
||||||
error={fieldState.error?.message}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
name="port"
|
|
||||||
control={control}
|
|
||||||
defaultValue={defaultValues?.port ?? 993}
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<TextInput
|
|
||||||
instanceId="port-imap-connection-form"
|
|
||||||
label={t`IMAP Port`}
|
|
||||||
type="number"
|
|
||||||
placeholder={t`993`}
|
|
||||||
value={field.value.toString()}
|
|
||||||
onChange={(value) => field.onChange(Number(value))}
|
|
||||||
error={fieldState.error?.message}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
name="secure"
|
|
||||||
control={control}
|
|
||||||
defaultValue={defaultValues?.secure}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Select
|
|
||||||
label={t`Encryption`}
|
|
||||||
options={[
|
|
||||||
{ label: 'SSL/TLS', value: true },
|
|
||||||
{ label: 'None', value: false },
|
|
||||||
]}
|
|
||||||
value={field.value}
|
|
||||||
onChange={field.onChange}
|
|
||||||
dropdownId="secure-dropdown"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
name="password"
|
|
||||||
control={control}
|
|
||||||
defaultValue={defaultValues?.password}
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<TextInput
|
|
||||||
instanceId="password-imap-connection-form"
|
|
||||||
label={t`Password`}
|
|
||||||
placeholder={t`••••••••`}
|
|
||||||
type="password"
|
|
||||||
value={field.value}
|
|
||||||
onChange={field.onChange}
|
|
||||||
error={fieldState.error?.message}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</StyledFormContainer>
|
|
||||||
</Section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const ACCOUNT_PROTOCOLS = ['IMAP', 'SMTP', 'CALDAV'] as const;
|
||||||
@ -12,21 +12,18 @@ export const GET_CONNECTED_IMAP_SMTP_CALDAV_ACCOUNT = gql`
|
|||||||
host
|
host
|
||||||
port
|
port
|
||||||
secure
|
secure
|
||||||
username
|
|
||||||
password
|
password
|
||||||
}
|
}
|
||||||
SMTP {
|
SMTP {
|
||||||
host
|
host
|
||||||
port
|
port
|
||||||
secure
|
secure
|
||||||
username
|
|
||||||
password
|
password
|
||||||
}
|
}
|
||||||
CALDAV {
|
CALDAV {
|
||||||
host
|
host
|
||||||
port
|
port
|
||||||
secure
|
secure
|
||||||
username
|
|
||||||
password
|
password
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,238 @@
|
|||||||
|
import { act, renderHook } from '@testing-library/react';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||||
|
import {
|
||||||
|
CalendarChannelVisibility,
|
||||||
|
MessageChannelVisibility,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
|
import { useTriggerProviderReconnect } from '../useTriggerProviderReconnect';
|
||||||
|
|
||||||
|
const mockTriggerApisOAuth = jest.fn();
|
||||||
|
const mockNavigate = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('@/settings/accounts/hooks/useTriggerApiOAuth', () => ({
|
||||||
|
useTriggerApisOAuth: jest.fn().mockImplementation(() => ({
|
||||||
|
triggerApisOAuth: mockTriggerApisOAuth,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/hooks/useNavigateSettings', () => ({
|
||||||
|
useNavigateSettings: jest.fn().mockImplementation(() => mockNavigate),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<RecoilRoot>{children}</RecoilRoot>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('useTriggerProviderReconnect', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('IMAP_SMTP_CALDAV provider', () => {
|
||||||
|
it('should navigate to new connection when no accountId is provided', async () => {
|
||||||
|
const { result } = renderHook(() => useTriggerProviderReconnect(), {
|
||||||
|
wrapper: Wrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.triggerProviderReconnect(
|
||||||
|
ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockNavigate).toHaveBeenCalledWith(
|
||||||
|
SettingsPath.NewImapSmtpCaldavConnection,
|
||||||
|
);
|
||||||
|
expect(mockTriggerApisOAuth).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to edit connection when accountId is provided', async () => {
|
||||||
|
const { result } = renderHook(() => useTriggerProviderReconnect(), {
|
||||||
|
wrapper: Wrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
const accountId = 'test-account-id-123';
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.triggerProviderReconnect(
|
||||||
|
ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||||
|
accountId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockNavigate).toHaveBeenCalledWith(
|
||||||
|
SettingsPath.EditImapSmtpCaldavConnection,
|
||||||
|
{
|
||||||
|
connectedAccountId: accountId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(mockTriggerApisOAuth).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore options parameter for IMAP_SMTP_CALDAV provider', async () => {
|
||||||
|
const { result } = renderHook(() => useTriggerProviderReconnect(), {
|
||||||
|
wrapper: Wrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
const accountId = 'test-account-id-123';
|
||||||
|
const options = {
|
||||||
|
redirectLocation: '/some-path',
|
||||||
|
calendarVisibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.triggerProviderReconnect(
|
||||||
|
ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||||
|
accountId,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockNavigate).toHaveBeenCalledWith(
|
||||||
|
SettingsPath.EditImapSmtpCaldavConnection,
|
||||||
|
{
|
||||||
|
connectedAccountId: accountId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(mockTriggerApisOAuth).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OAuth providers', () => {
|
||||||
|
it('should trigger OAuth for Google provider without options', async () => {
|
||||||
|
const { result } = renderHook(() => useTriggerProviderReconnect(), {
|
||||||
|
wrapper: Wrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.triggerProviderReconnect(
|
||||||
|
ConnectedAccountProvider.GOOGLE,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockTriggerApisOAuth).toHaveBeenCalledWith(
|
||||||
|
ConnectedAccountProvider.GOOGLE,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
expect(mockNavigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger OAuth for Microsoft provider without options', async () => {
|
||||||
|
const { result } = renderHook(() => useTriggerProviderReconnect(), {
|
||||||
|
wrapper: Wrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.triggerProviderReconnect(
|
||||||
|
ConnectedAccountProvider.MICROSOFT,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockTriggerApisOAuth).toHaveBeenCalledWith(
|
||||||
|
ConnectedAccountProvider.MICROSOFT,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
expect(mockNavigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger OAuth for Google provider with options', async () => {
|
||||||
|
const { result } = renderHook(() => useTriggerProviderReconnect(), {
|
||||||
|
wrapper: Wrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
redirectLocation: '/custom-redirect',
|
||||||
|
calendarVisibility: CalendarChannelVisibility.METADATA,
|
||||||
|
messageVisibility: MessageChannelVisibility.SUBJECT,
|
||||||
|
loginHint: 'user@example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.triggerProviderReconnect(
|
||||||
|
ConnectedAccountProvider.GOOGLE,
|
||||||
|
undefined,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockTriggerApisOAuth).toHaveBeenCalledWith(
|
||||||
|
ConnectedAccountProvider.GOOGLE,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
expect(mockNavigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger OAuth for Microsoft provider with options', async () => {
|
||||||
|
const { result } = renderHook(() => useTriggerProviderReconnect(), {
|
||||||
|
wrapper: Wrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
redirectLocation: '/another-redirect',
|
||||||
|
calendarVisibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||||
|
messageVisibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.triggerProviderReconnect(
|
||||||
|
ConnectedAccountProvider.MICROSOFT,
|
||||||
|
'some-account-id',
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockTriggerApisOAuth).toHaveBeenCalledWith(
|
||||||
|
ConnectedAccountProvider.MICROSOFT,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
expect(mockNavigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore accountId parameter for OAuth providers', async () => {
|
||||||
|
const { result } = renderHook(() => useTriggerProviderReconnect(), {
|
||||||
|
wrapper: Wrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.triggerProviderReconnect(
|
||||||
|
ConnectedAccountProvider.GOOGLE,
|
||||||
|
'ignored-account-id',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockTriggerApisOAuth).toHaveBeenCalledWith(
|
||||||
|
ConnectedAccountProvider.GOOGLE,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
expect(mockNavigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('error handling', () => {
|
||||||
|
it('should handle triggerApisOAuth errors gracefully', async () => {
|
||||||
|
const { result } = renderHook(() => useTriggerProviderReconnect(), {
|
||||||
|
wrapper: Wrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = new Error('OAuth failed');
|
||||||
|
mockTriggerApisOAuth.mockRejectedValue(error);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.triggerProviderReconnect(
|
||||||
|
ConnectedAccountProvider.GOOGLE,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}).rejects.toThrow('OAuth failed');
|
||||||
|
|
||||||
|
expect(mockTriggerApisOAuth).toHaveBeenCalledWith(
|
||||||
|
ConnectedAccountProvider.GOOGLE,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,11 +1,21 @@
|
|||||||
import { useGetConnectedImapSmtpCaldavAccountQuery } from '~/generated-metadata/graphql';
|
import {
|
||||||
|
type GetConnectedImapSmtpCaldavAccountQuery,
|
||||||
|
useGetConnectedImapSmtpCaldavAccountQuery,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export type ConnectedImapSmtpCaldavAccount =
|
||||||
|
GetConnectedImapSmtpCaldavAccountQuery['getConnectedImapSmtpCaldavAccount'];
|
||||||
|
|
||||||
export const useConnectedImapSmtpCaldavAccount = (
|
export const useConnectedImapSmtpCaldavAccount = (
|
||||||
connectedAccountId: string | undefined,
|
connectedAccountId: string | undefined,
|
||||||
|
onCompleted?: (data: ConnectedImapSmtpCaldavAccount) => void,
|
||||||
) => {
|
) => {
|
||||||
const { data, loading, error } = useGetConnectedImapSmtpCaldavAccountQuery({
|
const { data, loading, error } = useGetConnectedImapSmtpCaldavAccountQuery({
|
||||||
variables: { id: connectedAccountId ?? '' },
|
variables: { id: connectedAccountId ?? '' },
|
||||||
skip: !connectedAccountId,
|
skip: !connectedAccountId,
|
||||||
|
onCompleted: (data) => {
|
||||||
|
onCompleted?.(data.getConnectedImapSmtpCaldavAccount);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,133 +0,0 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|
||||||
import { ApolloError } from '@apollo/client';
|
|
||||||
import { useLingui } from '@lingui/react/macro';
|
|
||||||
import {
|
|
||||||
ConnectionParameters,
|
|
||||||
useSaveImapSmtpCaldavMutation,
|
|
||||||
} from '~/generated-metadata/graphql';
|
|
||||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
|
||||||
import { currentWorkspaceMemberState } from '~/modules/auth/states/currentWorkspaceMemberState';
|
|
||||||
import { currentWorkspaceState } from '~/modules/auth/states/currentWorkspaceState';
|
|
||||||
|
|
||||||
const imapConnectionFormSchema = z.object({
|
|
||||||
handle: z.string().email('Invalid email address'),
|
|
||||||
host: z.string().min(1, 'IMAP server is required'),
|
|
||||||
port: z.number().int().positive('Port must be a positive number'),
|
|
||||||
secure: z.boolean(),
|
|
||||||
password: z.string().min(1, 'Password is required'),
|
|
||||||
});
|
|
||||||
|
|
||||||
type ImapConnectionFormValues = z.infer<typeof imapConnectionFormSchema>;
|
|
||||||
|
|
||||||
type UseImapConnectionFormProps = {
|
|
||||||
initialData?: ImapConnectionFormValues;
|
|
||||||
isEditing?: boolean;
|
|
||||||
connectedAccountId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useImapConnectionForm = ({
|
|
||||||
initialData,
|
|
||||||
isEditing = false,
|
|
||||||
connectedAccountId,
|
|
||||||
}: UseImapConnectionFormProps = {}) => {
|
|
||||||
const { t } = useLingui();
|
|
||||||
const navigate = useNavigateSettings();
|
|
||||||
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
|
|
||||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
|
||||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
|
||||||
|
|
||||||
const [saveImapConnection, { loading: saveLoading }] =
|
|
||||||
useSaveImapSmtpCaldavMutation();
|
|
||||||
|
|
||||||
const resolver = zodResolver(imapConnectionFormSchema);
|
|
||||||
|
|
||||||
const defaultValues = {
|
|
||||||
handle: initialData?.handle || '',
|
|
||||||
host: initialData?.host || '',
|
|
||||||
port: initialData?.port || 993,
|
|
||||||
secure: initialData?.secure ?? true,
|
|
||||||
password: initialData?.password || '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const formMethods = useForm<ConnectionParameters & { handle: string }>({
|
|
||||||
mode: 'onSubmit',
|
|
||||||
resolver,
|
|
||||||
defaultValues,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { handleSubmit, formState } = formMethods;
|
|
||||||
const { isValid, isSubmitting } = formState;
|
|
||||||
const canSave = isValid && !isSubmitting;
|
|
||||||
const loading = saveLoading;
|
|
||||||
|
|
||||||
const handleSave = async (
|
|
||||||
formValues: ConnectionParameters & { handle: string },
|
|
||||||
) => {
|
|
||||||
if (!currentWorkspace?.id) {
|
|
||||||
enqueueErrorSnackBar({});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentWorkspaceMember?.id) {
|
|
||||||
enqueueErrorSnackBar({});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const variables = {
|
|
||||||
...(isEditing && connectedAccountId ? { id: connectedAccountId } : {}),
|
|
||||||
accountOwnerId: currentWorkspaceMember.id,
|
|
||||||
handle: formValues.handle,
|
|
||||||
host: formValues.host,
|
|
||||||
port: formValues.port,
|
|
||||||
secure: formValues.secure,
|
|
||||||
password: formValues.password,
|
|
||||||
};
|
|
||||||
|
|
||||||
await saveImapConnection({
|
|
||||||
variables: {
|
|
||||||
accountOwnerId: variables.accountOwnerId,
|
|
||||||
handle: variables.handle,
|
|
||||||
accountType: {
|
|
||||||
type: 'IMAP',
|
|
||||||
},
|
|
||||||
connectionParameters: {
|
|
||||||
host: variables.host,
|
|
||||||
port: variables.port,
|
|
||||||
secure: variables.secure,
|
|
||||||
password: variables.password,
|
|
||||||
username: variables.handle,
|
|
||||||
},
|
|
||||||
...(variables.id ? { id: variables.id } : {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
enqueueSuccessSnackBar({
|
|
||||||
message: connectedAccountId
|
|
||||||
? t`IMAP connection successfully updated`
|
|
||||||
: t`IMAP connection successfully created`,
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate(SettingsPath.Accounts);
|
|
||||||
} catch (error) {
|
|
||||||
enqueueErrorSnackBar({
|
|
||||||
apolloError: error instanceof ApolloError ? error : undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
formMethods,
|
|
||||||
handleSave,
|
|
||||||
handleSubmit,
|
|
||||||
canSave,
|
|
||||||
isSubmitting,
|
|
||||||
loading,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -0,0 +1,184 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
import {
|
||||||
|
ConnectionParameters,
|
||||||
|
useSaveImapSmtpCaldavMutation,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||||
|
|
||||||
|
import { ImapSmtpCaldavAccount } from '@/accounts/types/ImapSmtpCaldavAccount';
|
||||||
|
import { ACCOUNT_PROTOCOLS } from '@/settings/accounts/constants/AccountProtocols';
|
||||||
|
import {
|
||||||
|
connectionImapSmtpCalDav,
|
||||||
|
isProtocolConfigured,
|
||||||
|
} from '@/settings/accounts/validation-schemas/connectionImapSmtpCalDav';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import {
|
||||||
|
ConnectedImapSmtpCaldavAccount,
|
||||||
|
useConnectedImapSmtpCaldavAccount,
|
||||||
|
} from './useConnectedImapSmtpCaldavAccount';
|
||||||
|
|
||||||
|
type UseConnectionFormProps = {
|
||||||
|
isEditing?: boolean;
|
||||||
|
connectedAccountId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConnectionFormData = {
|
||||||
|
handle: string;
|
||||||
|
} & ImapSmtpCaldavAccount;
|
||||||
|
|
||||||
|
export const useImapSmtpCaldavConnectionForm = ({
|
||||||
|
isEditing = false,
|
||||||
|
connectedAccountId,
|
||||||
|
}: UseConnectionFormProps = {}) => {
|
||||||
|
const navigate = useNavigateSettings();
|
||||||
|
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||||
|
|
||||||
|
const formMethods = useForm<ConnectionFormData>({
|
||||||
|
mode: 'onSubmit',
|
||||||
|
resolver: zodResolver(connectionImapSmtpCalDav),
|
||||||
|
defaultValues: {
|
||||||
|
handle: '',
|
||||||
|
IMAP: { host: '', port: 993, password: '', secure: true },
|
||||||
|
SMTP: { host: '', port: 587, password: '', secure: true },
|
||||||
|
CALDAV: { host: '', port: 443, password: '', secure: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleSubmit, formState, watch, reset } = formMethods;
|
||||||
|
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
|
||||||
|
const { isSubmitting } = formState;
|
||||||
|
|
||||||
|
const { connectedAccount, loading: accountLoading } =
|
||||||
|
useConnectedImapSmtpCaldavAccount(
|
||||||
|
isEditing ? connectedAccountId : undefined,
|
||||||
|
useCallback(
|
||||||
|
(account: ConnectedImapSmtpCaldavAccount | null) => {
|
||||||
|
if (isDefined(account)) {
|
||||||
|
reset({
|
||||||
|
handle: account.handle || '',
|
||||||
|
IMAP: account.connectionParameters?.IMAP || undefined,
|
||||||
|
SMTP: account.connectionParameters?.SMTP || undefined,
|
||||||
|
CALDAV: account.connectionParameters?.CALDAV || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[reset],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [saveConnection, { loading: saveLoading }] =
|
||||||
|
useSaveImapSmtpCaldavMutation();
|
||||||
|
|
||||||
|
const watchedValues = watch();
|
||||||
|
|
||||||
|
const getConfiguredProtocols = useCallback(
|
||||||
|
(
|
||||||
|
values: ConnectionFormData = watchedValues,
|
||||||
|
): (keyof ImapSmtpCaldavAccount)[] => {
|
||||||
|
return ACCOUNT_PROTOCOLS.filter((protocol) => {
|
||||||
|
const protocolConfig = values[protocol];
|
||||||
|
return (
|
||||||
|
protocolConfig &&
|
||||||
|
isProtocolConfigured(protocolConfig as ConnectionParameters)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[watchedValues],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isValid = useMemo(() => {
|
||||||
|
return (
|
||||||
|
Boolean(watchedValues.handle?.trim()) &&
|
||||||
|
getConfiguredProtocols().length > 0
|
||||||
|
);
|
||||||
|
}, [getConfiguredProtocols, watchedValues.handle]);
|
||||||
|
|
||||||
|
const saveIndividualConnection = useCallback(
|
||||||
|
async (
|
||||||
|
protocol: keyof ImapSmtpCaldavAccount,
|
||||||
|
formValues: ConnectionFormData,
|
||||||
|
): Promise<void> => {
|
||||||
|
if (!currentWorkspaceMember?.id) {
|
||||||
|
throw new Error('Workspace member ID is missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocolConfig = formValues[protocol];
|
||||||
|
if (!protocolConfig) {
|
||||||
|
throw new Error(`${protocol} configuration is missing`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveConnection({
|
||||||
|
variables: {
|
||||||
|
...(isEditing && connectedAccountId
|
||||||
|
? { id: connectedAccountId }
|
||||||
|
: {}),
|
||||||
|
accountOwnerId: currentWorkspaceMember.id,
|
||||||
|
handle: formValues.handle,
|
||||||
|
accountType: {
|
||||||
|
type: protocol,
|
||||||
|
},
|
||||||
|
connectionParameters: protocolConfig,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[saveConnection, isEditing, connectedAccountId, currentWorkspaceMember?.id],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSave = useCallback(
|
||||||
|
async (formValues: ConnectionFormData): Promise<void> => {
|
||||||
|
const configuredProtocols = getConfiguredProtocols(formValues);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
configuredProtocols.map((protocol) =>
|
||||||
|
saveIndividualConnection(protocol, formValues),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const successMessage = isEditing
|
||||||
|
? t`Connection successfully updated`
|
||||||
|
: t`Connection successfully created`;
|
||||||
|
|
||||||
|
enqueueSuccessSnackBar({ message: successMessage });
|
||||||
|
navigate(SettingsPath.Accounts);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'An unexpected error occurred';
|
||||||
|
|
||||||
|
enqueueErrorSnackBar({ message: errorMessage });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
getConfiguredProtocols,
|
||||||
|
saveIndividualConnection,
|
||||||
|
isEditing,
|
||||||
|
enqueueSuccessSnackBar,
|
||||||
|
enqueueErrorSnackBar,
|
||||||
|
navigate,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const canSave = isValid && !isSubmitting;
|
||||||
|
const loading = accountLoading || saveLoading;
|
||||||
|
|
||||||
|
return {
|
||||||
|
formMethods,
|
||||||
|
handleSave,
|
||||||
|
handleSubmit,
|
||||||
|
canSave,
|
||||||
|
isSubmitting,
|
||||||
|
loading,
|
||||||
|
connectedAccount,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -17,11 +17,11 @@ export const useTriggerProviderReconnect = () => {
|
|||||||
) => {
|
) => {
|
||||||
if (provider === ConnectedAccountProvider.IMAP_SMTP_CALDAV) {
|
if (provider === ConnectedAccountProvider.IMAP_SMTP_CALDAV) {
|
||||||
if (!accountId) {
|
if (!accountId) {
|
||||||
navigate(SettingsPath.NewImapConnection);
|
navigate(SettingsPath.NewImapSmtpCaldavConnection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(SettingsPath.EditImapConnection, {
|
navigate(SettingsPath.EditImapSmtpCaldavConnection, {
|
||||||
connectedAccountId: accountId,
|
connectedAccountId: accountId,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -0,0 +1,47 @@
|
|||||||
|
import { ACCOUNT_PROTOCOLS } from '@/settings/accounts/constants/AccountProtocols';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { ConnectionParameters } from '~/generated/graphql';
|
||||||
|
|
||||||
|
const connectionParameters = z
|
||||||
|
.object({
|
||||||
|
host: z.string().default(''),
|
||||||
|
port: z.number().int().nullable().default(null),
|
||||||
|
password: z.string().default(''),
|
||||||
|
secure: z.boolean().default(true),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (Boolean(data.host?.trim()) && Boolean(data.password?.trim())) {
|
||||||
|
return data.port && data.port > 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Port must be a positive number when configuring this protocol',
|
||||||
|
path: ['port'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const connectionImapSmtpCalDav = z
|
||||||
|
.object({
|
||||||
|
handle: z.string().email('Invalid email address'),
|
||||||
|
IMAP: connectionParameters.optional(),
|
||||||
|
SMTP: connectionParameters.optional(),
|
||||||
|
CALDAV: connectionParameters.optional(),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
return ACCOUNT_PROTOCOLS.some((protocol) =>
|
||||||
|
isProtocolConfigured(data[protocol] as ConnectionParameters),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'At least one account type (IMAP, SMTP, or CalDAV) must be completely configured',
|
||||||
|
path: ['handle'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const isProtocolConfigured = (config: ConnectionParameters): boolean => {
|
||||||
|
return Boolean(config?.host?.trim() && config?.password?.trim());
|
||||||
|
};
|
||||||
@ -4,9 +4,9 @@ import { ComponentType } from 'react';
|
|||||||
|
|
||||||
import { SettingsListSkeletonCard } from '@/settings/components/SettingsListSkeletonCard';
|
import { SettingsListSkeletonCard } from '@/settings/components/SettingsListSkeletonCard';
|
||||||
|
|
||||||
import { SettingsListItemCardContent } from './SettingsListItemCardContent';
|
|
||||||
import { Card, CardFooter } from 'twenty-ui/layout';
|
|
||||||
import { IconComponent, IconPlus } from 'twenty-ui/display';
|
import { IconComponent, IconPlus } from 'twenty-ui/display';
|
||||||
|
import { Card, CardFooter } from 'twenty-ui/layout';
|
||||||
|
import { SettingsListItemCardContent } from './SettingsListItemCardContent';
|
||||||
|
|
||||||
const StyledFooter = styled(CardFooter)`
|
const StyledFooter = styled(CardFooter)`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -5,8 +5,8 @@ export enum SettingsPath {
|
|||||||
NewAccount = 'accounts/new',
|
NewAccount = 'accounts/new',
|
||||||
AccountsCalendars = 'accounts/calendars',
|
AccountsCalendars = 'accounts/calendars',
|
||||||
AccountsEmails = 'accounts/emails',
|
AccountsEmails = 'accounts/emails',
|
||||||
NewImapConnection = 'accounts/new-imap-connection',
|
NewImapSmtpCaldavConnection = 'accounts/new-imap-smtp-caldav-connection',
|
||||||
EditImapConnection = 'accounts/edit-imap-connection/:connectedAccountId',
|
EditImapSmtpCaldavConnection = 'accounts/edit-imap-smtp-caldav-connection/:connectedAccountId',
|
||||||
Billing = 'billing',
|
Billing = 'billing',
|
||||||
Objects = 'objects',
|
Objects = 'objects',
|
||||||
ObjectOverview = 'objects/overview',
|
ObjectOverview = 'objects/overview',
|
||||||
|
|||||||
@ -98,7 +98,10 @@ export const WorkflowEditActionSendEmail = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isDefined(scopes) || !hasSendScope(connectedAccount, scopes)) {
|
if (
|
||||||
|
connectedAccount.provider !== ConnectedAccountProvider.IMAP_SMTP_CALDAV &&
|
||||||
|
(!isDefined(scopes) || !hasSendScope(connectedAccount, scopes))
|
||||||
|
) {
|
||||||
await triggerApisOAuth(connectedAccount.provider, {
|
await triggerApisOAuth(connectedAccount.provider, {
|
||||||
redirectLocation: redirectUrl,
|
redirectLocation: redirectUrl,
|
||||||
loginHint: connectedAccount.handle,
|
loginHint: connectedAccount.handle,
|
||||||
@ -180,6 +183,7 @@ export const WorkflowEditActionSendEmail = ({
|
|||||||
provider: true,
|
provider: true,
|
||||||
scopes: true,
|
scopes: true,
|
||||||
accountOwnerId: true,
|
accountOwnerId: true,
|
||||||
|
connectionParameters: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -54,5 +54,5 @@ export const mockedClientConfig: ClientConfig = {
|
|||||||
isGoogleCalendarEnabled: true,
|
isGoogleCalendarEnabled: true,
|
||||||
isAttachmentPreviewEnabled: true,
|
isAttachmentPreviewEnabled: true,
|
||||||
isConfigVariablesInDbEnabled: false,
|
isConfigVariablesInDbEnabled: false,
|
||||||
isIMAPMessagingEnabled: false,
|
isImapSmtpCaldavEnabled: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,7 +14,7 @@ FRONTEND_URL=http://localhost:3001
|
|||||||
# REFRESH_TOKEN_EXPIRES_IN=90d
|
# REFRESH_TOKEN_EXPIRES_IN=90d
|
||||||
# FILE_TOKEN_EXPIRES_IN=1d
|
# FILE_TOKEN_EXPIRES_IN=1d
|
||||||
# MESSAGING_PROVIDER_GMAIL_ENABLED=false
|
# MESSAGING_PROVIDER_GMAIL_ENABLED=false
|
||||||
# MESSAGING_PROVIDER_IMAP_ENABLED=false
|
# IS_IMAP_SMTP_CALDAV_ENABLED=false
|
||||||
# CALENDAR_PROVIDER_GOOGLE_ENABLED=false
|
# CALENDAR_PROVIDER_GOOGLE_ENABLED=false
|
||||||
# MESSAGING_PROVIDER_MICROSOFT_ENABLED=false
|
# MESSAGING_PROVIDER_MICROSOFT_ENABLED=false
|
||||||
# CALENDAR_PROVIDER_MICROSOFT_ENABLED=false
|
# CALENDAR_PROVIDER_MICROSOFT_ENABLED=false
|
||||||
|
|||||||
@ -11,7 +11,7 @@ FRONTEND_URL=http://localhost:3001
|
|||||||
|
|
||||||
AUTH_GOOGLE_ENABLED=false
|
AUTH_GOOGLE_ENABLED=false
|
||||||
MESSAGING_PROVIDER_GMAIL_ENABLED=false
|
MESSAGING_PROVIDER_GMAIL_ENABLED=false
|
||||||
MESSAGING_PROVIDER_IMAP_ENABLED=false
|
IS_IMAP_SMTP_CALDAV_ENABLED=false
|
||||||
CALENDAR_PROVIDER_GOOGLE_ENABLED=false
|
CALENDAR_PROVIDER_GOOGLE_ENABLED=false
|
||||||
MESSAGING_PROVIDER_MICROSOFT_ENABLED=false
|
MESSAGING_PROVIDER_MICROSOFT_ENABLED=false
|
||||||
CALENDAR_PROVIDER_MICROSOFT_ENABLED=false
|
CALENDAR_PROVIDER_MICROSOFT_ENABLED=false
|
||||||
|
|||||||
@ -96,7 +96,7 @@ describe('ClientConfigController', () => {
|
|||||||
isGoogleMessagingEnabled: false,
|
isGoogleMessagingEnabled: false,
|
||||||
isGoogleCalendarEnabled: false,
|
isGoogleCalendarEnabled: false,
|
||||||
isConfigVariablesInDbEnabled: false,
|
isConfigVariablesInDbEnabled: false,
|
||||||
isIMAPMessagingEnabled: false,
|
isImapSmtpCaldavEnabled: false,
|
||||||
calendarBookingPageId: undefined,
|
calendarBookingPageId: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -178,7 +178,7 @@ export class ClientConfig {
|
|||||||
isConfigVariablesInDbEnabled: boolean;
|
isConfigVariablesInDbEnabled: boolean;
|
||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
isIMAPMessagingEnabled: boolean;
|
isImapSmtpCaldavEnabled: boolean;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
calendarBookingPageId?: string;
|
calendarBookingPageId?: string;
|
||||||
|
|||||||
@ -138,8 +138,8 @@ export class ClientConfigService {
|
|||||||
isConfigVariablesInDbEnabled: this.twentyConfigService.get(
|
isConfigVariablesInDbEnabled: this.twentyConfigService.get(
|
||||||
'IS_CONFIG_VARIABLES_IN_DB_ENABLED',
|
'IS_CONFIG_VARIABLES_IN_DB_ENABLED',
|
||||||
),
|
),
|
||||||
isIMAPMessagingEnabled: this.twentyConfigService.get(
|
isImapSmtpCaldavEnabled: this.twentyConfigService.get(
|
||||||
'MESSAGING_PROVIDER_IMAP_ENABLED',
|
'IS_IMAP_SMTP_CALDAV_ENABLED',
|
||||||
),
|
),
|
||||||
calendarBookingPageId: this.twentyConfigService.get(
|
calendarBookingPageId: this.twentyConfigService.get(
|
||||||
'CALENDAR_BOOKING_PAGE_ID',
|
'CALENDAR_BOOKING_PAGE_ID',
|
||||||
|
|||||||
@ -13,12 +13,13 @@ export type PublicFeatureFlag = {
|
|||||||
|
|
||||||
export const PUBLIC_FEATURE_FLAGS: PublicFeatureFlag[] = [
|
export const PUBLIC_FEATURE_FLAGS: PublicFeatureFlag[] = [
|
||||||
{
|
{
|
||||||
key: FeatureFlagKey.IS_IMAP_ENABLED,
|
key: FeatureFlagKey.IS_IMAP_SMTP_CALDAV_ENABLED,
|
||||||
metadata: {
|
metadata: {
|
||||||
label: 'IMAP',
|
label: 'IMAP, SMTP, CalDAV',
|
||||||
description:
|
description:
|
||||||
'Easily add email accounts from any provider that supports IMAP (and soon, send emails with SMTP)',
|
'Easily add email accounts from any provider that supports IMAP, send emails with SMTP (and soon, sync calendars with CalDAV)',
|
||||||
imagePath: 'https://twenty.com/images/lab/is-imap-enabled.png',
|
imagePath:
|
||||||
|
'https://twenty.com/images/lab/is-imap-smtp-caldav-enabled.png',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...(process.env.CLOUDFLARE_API_KEY
|
...(process.env.CLOUDFLARE_API_KEY
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export enum FeatureFlagKey {
|
|||||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
|
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
|
||||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||||
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
||||||
IS_IMAP_ENABLED = 'IS_IMAP_ENABLED',
|
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
||||||
IS_MORPH_RELATION_ENABLED = 'IS_MORPH_RELATION_ENABLED',
|
IS_MORPH_RELATION_ENABLED = 'IS_MORPH_RELATION_ENABLED',
|
||||||
IS_WORKFLOW_FILTERING_ENABLED = 'IS_WORKFLOW_FILTERING_ENABLED',
|
IS_WORKFLOW_FILTERING_ENABLED = 'IS_WORKFLOW_FILTERING_ENABLED',
|
||||||
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
|
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
|
||||||
|
|||||||
@ -14,9 +14,6 @@ export class ConnectionParameters {
|
|||||||
@Field(() => Number)
|
@Field(() => Number)
|
||||||
port: number;
|
port: number;
|
||||||
|
|
||||||
@Field(() => String)
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: This field is stored in plain text in the database.
|
* Note: This field is stored in plain text in the database.
|
||||||
* While encrypting it could provide an extra layer of defense, we have decided not to,
|
* While encrypting it could provide an extra layer of defense, we have decided not to,
|
||||||
@ -37,9 +34,6 @@ export class ConnectionParametersOutput {
|
|||||||
@Field(() => Number)
|
@Field(() => Number)
|
||||||
port: number;
|
port: number;
|
||||||
|
|
||||||
@Field(() => String)
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
import {
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
UseFilters,
|
||||||
|
UseGuards,
|
||||||
|
UsePipes,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||||
@ -39,36 +45,6 @@ export class ImapSmtpCaldavResolver {
|
|||||||
private readonly mailConnectionValidatorService: ImapSmtpCaldavValidatorService,
|
private readonly mailConnectionValidatorService: ImapSmtpCaldavValidatorService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private async checkIfFeatureEnabled(
|
|
||||||
workspaceId: string,
|
|
||||||
accountType: AccountType,
|
|
||||||
): Promise<void> {
|
|
||||||
if (accountType.type === 'IMAP') {
|
|
||||||
const isImapEnabled = await this.featureFlagService.isFeatureEnabled(
|
|
||||||
FeatureFlagKey.IS_IMAP_ENABLED,
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isImapEnabled) {
|
|
||||||
throw new UserInputError(
|
|
||||||
'IMAP feature is not enabled for this workspace',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accountType.type === 'SMTP') {
|
|
||||||
throw new UserInputError(
|
|
||||||
'SMTP feature is not enabled for this workspace',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accountType.type === 'CALDAV') {
|
|
||||||
throw new UserInputError(
|
|
||||||
'CALDAV feature is not enabled for this workspace',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Query(() => ConnectedImapSmtpCaldavAccount)
|
@Query(() => ConnectedImapSmtpCaldavAccount)
|
||||||
@UseGuards(WorkspaceAuthGuard)
|
@UseGuards(WorkspaceAuthGuard)
|
||||||
async getConnectedImapSmtpCaldavAccount(
|
async getConnectedImapSmtpCaldavAccount(
|
||||||
@ -111,7 +87,18 @@ export class ImapSmtpCaldavResolver {
|
|||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
@Args('id', { nullable: true }) id?: string,
|
@Args('id', { nullable: true }) id?: string,
|
||||||
): Promise<ImapSmtpCaldavConnectionSuccess> {
|
): Promise<ImapSmtpCaldavConnectionSuccess> {
|
||||||
await this.checkIfFeatureEnabled(workspace.id, accountType);
|
const isImapSmtpCaldavFeatureFlagEnabled =
|
||||||
|
await this.featureFlagService.isFeatureEnabled(
|
||||||
|
FeatureFlagKey.IS_IMAP_SMTP_CALDAV_ENABLED,
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isImapSmtpCaldavFeatureFlagEnabled) {
|
||||||
|
throw new HttpException(
|
||||||
|
'IMAP, SMTP, CalDAV feature is not enabled for this workspace',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const validatedParams =
|
const validatedParams =
|
||||||
this.mailConnectionValidatorService.validateProtocolConnectionParams(
|
this.mailConnectionValidatorService.validateProtocolConnectionParams(
|
||||||
@ -119,6 +106,7 @@ export class ImapSmtpCaldavResolver {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await this.ImapSmtpCaldavConnectionService.testImapSmtpCaldav(
|
await this.ImapSmtpCaldavConnectionService.testImapSmtpCaldav(
|
||||||
|
handle,
|
||||||
validatedParams,
|
validatedParams,
|
||||||
accountType.type,
|
accountType.type,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -10,7 +10,6 @@ export class ImapSmtpCaldavValidatorService {
|
|||||||
private readonly protocolConnectionSchema = z.object({
|
private readonly protocolConnectionSchema = z.object({
|
||||||
host: z.string().min(1, 'Host is required'),
|
host: z.string().min(1, 'Host is required'),
|
||||||
port: z.number().int().positive('Port must be a positive number'),
|
port: z.number().int().positive('Port must be a positive number'),
|
||||||
username: z.string().min(1, 'Username is required'),
|
|
||||||
password: z.string().min(1, 'Password is required'),
|
password: z.string().min(1, 'Password is required'),
|
||||||
secure: z.boolean().optional(),
|
secure: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
@ -19,7 +18,10 @@ export class ImapSmtpCaldavValidatorService {
|
|||||||
params: ConnectionParameters,
|
params: ConnectionParameters,
|
||||||
): ConnectionParameters {
|
): ConnectionParameters {
|
||||||
if (!params) {
|
if (!params) {
|
||||||
throw new UserInputError('Protocol connection parameters are required');
|
throw new UserInputError('Protocol connection parameters are required', {
|
||||||
|
userFriendlyMessage:
|
||||||
|
'Please provide connection details to configure your email account.',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -32,10 +34,17 @@ export class ImapSmtpCaldavValidatorService {
|
|||||||
|
|
||||||
throw new UserInputError(
|
throw new UserInputError(
|
||||||
`Protocol connection validation failed: ${errorMessages}`,
|
`Protocol connection validation failed: ${errorMessages}`,
|
||||||
|
{
|
||||||
|
userFriendlyMessage:
|
||||||
|
'Please check your connection settings. Make sure the server host, port, and password are correct.',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UserInputError('Protocol connection validation failed');
|
throw new UserInputError('Protocol connection validation failed', {
|
||||||
|
userFriendlyMessage:
|
||||||
|
'There was an issue with your connection settings. Please try again.',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
import { ImapFlow } from 'imapflow';
|
import { ImapFlow } from 'imapflow';
|
||||||
|
import { createTransport } from 'nodemailer';
|
||||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||||
|
|
||||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||||
@ -19,17 +20,16 @@ export class ImapSmtpCaldavService {
|
|||||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async testImapConnection(params: ConnectionParameters): Promise<boolean> {
|
async testImapConnection(
|
||||||
if (!params.host || !params.username || !params.password) {
|
handle: string,
|
||||||
throw new UserInputError('Missing required IMAP connection parameters');
|
params: ConnectionParameters,
|
||||||
}
|
): Promise<boolean> {
|
||||||
|
|
||||||
const client = new ImapFlow({
|
const client = new ImapFlow({
|
||||||
host: params.host,
|
host: params.host,
|
||||||
port: params.port,
|
port: params.port,
|
||||||
secure: params.secure ?? true,
|
secure: params.secure ?? true,
|
||||||
auth: {
|
auth: {
|
||||||
user: params.username,
|
user: handle,
|
||||||
pass: params.password,
|
pass: params.password,
|
||||||
},
|
},
|
||||||
logger: false,
|
logger: false,
|
||||||
@ -57,16 +57,27 @@ export class ImapSmtpCaldavService {
|
|||||||
if (error.authenticationFailed) {
|
if (error.authenticationFailed) {
|
||||||
throw new UserInputError(
|
throw new UserInputError(
|
||||||
'IMAP authentication failed. Please check your credentials.',
|
'IMAP authentication failed. Please check your credentials.',
|
||||||
|
{
|
||||||
|
userFriendlyMessage:
|
||||||
|
"We couldn't log in to your email account. Please check your email address and password, then try again.",
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.code === 'ECONNREFUSED') {
|
if (error.code === 'ECONNREFUSED') {
|
||||||
throw new UserInputError(
|
throw new UserInputError(
|
||||||
`IMAP connection refused. Please verify server and port.`,
|
`IMAP connection refused. Please verify server and port.`,
|
||||||
|
{
|
||||||
|
userFriendlyMessage:
|
||||||
|
"We couldn't connect to your email server. Please check your server settings and try again.",
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UserInputError(`IMAP connection failed: ${error.message}`);
|
throw new UserInputError(`IMAP connection failed: ${error.message}`, {
|
||||||
|
userFriendlyMessage:
|
||||||
|
'We encountered an issue connecting to your email account. Please check your settings and try again.',
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
if (client.authenticated) {
|
if (client.authenticated) {
|
||||||
await client.logout();
|
await client.logout();
|
||||||
@ -74,36 +85,70 @@ export class ImapSmtpCaldavService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async testSmtpConnection(params: ConnectionParameters): Promise<boolean> {
|
async testSmtpConnection(
|
||||||
this.logger.log('SMTP connection testing not yet implemented', params);
|
handle: string,
|
||||||
|
params: ConnectionParameters,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const transport = createTransport({
|
||||||
|
host: params.host,
|
||||||
|
port: params.port,
|
||||||
|
auth: {
|
||||||
|
user: handle,
|
||||||
|
pass: params.password,
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await transport.verify();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`SMTP connection failed: ${error.message}`,
|
||||||
|
error.stack,
|
||||||
|
);
|
||||||
|
throw new UserInputError(`SMTP connection failed: ${error.message}`, {
|
||||||
|
userFriendlyMessage:
|
||||||
|
"We couldn't connect to your outgoing email server. Please check your SMTP settings and try again.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async testCaldavConnection(params: ConnectionParameters): Promise<boolean> {
|
async testCaldavConnection(
|
||||||
|
handle: string,
|
||||||
|
params: ConnectionParameters,
|
||||||
|
): Promise<boolean> {
|
||||||
this.logger.log('CALDAV connection testing not yet implemented', params);
|
this.logger.log('CALDAV connection testing not yet implemented', params);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async testImapSmtpCaldav(
|
async testImapSmtpCaldav(
|
||||||
|
handle: string,
|
||||||
params: ConnectionParameters,
|
params: ConnectionParameters,
|
||||||
accountType: AccountType,
|
accountType: AccountType,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (accountType === 'IMAP') {
|
if (accountType === 'IMAP') {
|
||||||
return this.testImapConnection(params);
|
return this.testImapConnection(handle, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accountType === 'SMTP') {
|
if (accountType === 'SMTP') {
|
||||||
return this.testSmtpConnection(params);
|
return this.testSmtpConnection(handle, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accountType === 'CALDAV') {
|
if (accountType === 'CALDAV') {
|
||||||
return this.testCaldavConnection(params);
|
return this.testCaldavConnection(handle, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UserInputError(
|
throw new UserInputError(
|
||||||
'Invalid account type. Must be one of: IMAP, SMTP, CALDAV',
|
'Invalid account type. Must be one of: IMAP, SMTP, CALDAV',
|
||||||
|
{
|
||||||
|
userFriendlyMessage:
|
||||||
|
'Please select a valid connection type (IMAP, SMTP, or CalDAV) and try again.',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
export type ConnectionParameters = {
|
export type ConnectionParameters = {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
username: string;
|
|
||||||
password: string;
|
password: string;
|
||||||
secure?: boolean;
|
secure?: boolean;
|
||||||
};
|
};
|
||||||
@ -9,7 +8,6 @@ export type ConnectionParameters = {
|
|||||||
export type AccountType = 'IMAP' | 'SMTP' | 'CALDAV';
|
export type AccountType = 'IMAP' | 'SMTP' | 'CALDAV';
|
||||||
|
|
||||||
export type ImapSmtpCaldavParams = {
|
export type ImapSmtpCaldavParams = {
|
||||||
handle: string;
|
|
||||||
IMAP?: ConnectionParameters;
|
IMAP?: ConnectionParameters;
|
||||||
SMTP?: ConnectionParameters;
|
SMTP?: ConnectionParameters;
|
||||||
CALDAV?: ConnectionParameters;
|
CALDAV?: ConnectionParameters;
|
||||||
|
|||||||
@ -148,7 +148,7 @@ export class ConfigVariables {
|
|||||||
description: 'Enable or disable the IMAP messaging integration',
|
description: 'Enable or disable the IMAP messaging integration',
|
||||||
type: ConfigVariableType.BOOLEAN,
|
type: ConfigVariableType.BOOLEAN,
|
||||||
})
|
})
|
||||||
MESSAGING_PROVIDER_IMAP_ENABLED = false;
|
IS_IMAP_SMTP_CALDAV_ENABLED = false;
|
||||||
|
|
||||||
@ConfigVariablesMetadata({
|
@ConfigVariablesMetadata({
|
||||||
group: ConfigVariablesGroup.MicrosoftAuth,
|
group: ConfigVariablesGroup.MicrosoftAuth,
|
||||||
|
|||||||
@ -46,7 +46,7 @@ export const seedFeatureFlags = async (
|
|||||||
value: false,
|
value: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: FeatureFlagKey.IS_IMAP_ENABLED,
|
key: FeatureFlagKey.IS_IMAP_SMTP_CALDAV_ENABLED,
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -88,6 +88,20 @@ export class ImapSmtpCalDavAPIService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let shouldEnableSync = false;
|
||||||
|
|
||||||
|
if (connectedAccount) {
|
||||||
|
const hadOnlySmtp =
|
||||||
|
connectedAccount.connectionParameters?.SMTP &&
|
||||||
|
!connectedAccount.connectionParameters?.IMAP &&
|
||||||
|
!connectedAccount.connectionParameters?.CALDAV;
|
||||||
|
|
||||||
|
const isAddingImapOrCaldav =
|
||||||
|
input.accountType === 'IMAP' || input.accountType === 'CALDAV';
|
||||||
|
|
||||||
|
shouldEnableSync = Boolean(hadOnlySmtp && isAddingImapOrCaldav);
|
||||||
|
}
|
||||||
|
|
||||||
await workspaceDataSource.transaction(async () => {
|
await workspaceDataSource.transaction(async () => {
|
||||||
if (!existingAccountId) {
|
if (!existingAccountId) {
|
||||||
const newConnectedAccount = await connectedAccountRepository.save(
|
const newConnectedAccount = await connectedAccountRepository.save(
|
||||||
@ -129,7 +143,10 @@ export class ImapSmtpCalDavAPIService {
|
|||||||
connectedAccountId: newOrExistingConnectedAccountId,
|
connectedAccountId: newOrExistingConnectedAccountId,
|
||||||
type: MessageChannelType.EMAIL,
|
type: MessageChannelType.EMAIL,
|
||||||
handle,
|
handle,
|
||||||
syncStatus: MessageChannelSyncStatus.ONGOING,
|
isSyncEnabled: shouldEnableSync,
|
||||||
|
syncStatus: shouldEnableSync
|
||||||
|
? MessageChannelSyncStatus.ONGOING
|
||||||
|
: MessageChannelSyncStatus.NOT_SYNCED,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
@ -200,9 +217,12 @@ export class ImapSmtpCalDavAPIService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
syncStage: MessageChannelSyncStage.FULL_MESSAGE_LIST_FETCH_PENDING,
|
syncStage: MessageChannelSyncStage.FULL_MESSAGE_LIST_FETCH_PENDING,
|
||||||
syncStatus: null,
|
syncStatus: shouldEnableSync
|
||||||
|
? MessageChannelSyncStatus.ONGOING
|
||||||
|
: MessageChannelSyncStatus.NOT_SYNCED,
|
||||||
syncCursor: '',
|
syncCursor: '',
|
||||||
syncStageStartedAt: null,
|
syncStageStartedAt: null,
|
||||||
|
isSyncEnabled: shouldEnableSync,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -227,7 +247,10 @@ export class ImapSmtpCalDavAPIService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.twentyConfigService.get('MESSAGING_PROVIDER_IMAP_ENABLED')) {
|
if (!shouldEnableSync) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const messageChannels = await messageChannelRepository.find({
|
const messageChannels = await messageChannelRepository.find({
|
||||||
where: {
|
where: {
|
||||||
connectedAccountId: newOrExistingConnectedAccountId,
|
connectedAccountId: newOrExistingConnectedAccountId,
|
||||||
@ -245,4 +268,3 @@ export class ImapSmtpCalDavAPIService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@ -64,14 +64,14 @@ export class ImapClientProvider {
|
|||||||
await client.connect();
|
await client.connect();
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Connected to IMAP server for ${connectionParameters.handle}`,
|
`Connected to IMAP server for ${connectedAccount.handle}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mailboxes = await client.list();
|
const mailboxes = await client.list();
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Available mailboxes for ${connectionParameters.handle}: ${mailboxes.map((m) => m.path).join(', ')}`,
|
`Available mailboxes for ${connectedAccount.handle}: ${mailboxes.map((m) => m.path).join(', ')}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn(`Failed to list mailboxes: ${error.message}`);
|
this.logger.warn(`Failed to list mailboxes: ${error.message}`);
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { SmtpClientProvider } from './providers/smtp-client.provider';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [SmtpClientProvider],
|
||||||
|
exports: [SmtpClientProvider],
|
||||||
|
})
|
||||||
|
export class MessagingSmtpDriverModule {}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { createTransport, Transporter } from 'nodemailer';
|
||||||
|
|
||||||
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SmtpClientProvider {
|
||||||
|
public async getSmtpClient(
|
||||||
|
connectedAccount: Pick<
|
||||||
|
ConnectedAccountWorkspaceEntity,
|
||||||
|
'connectionParameters' | 'handle'
|
||||||
|
>,
|
||||||
|
): Promise<Transporter> {
|
||||||
|
const smtpParams = connectedAccount.connectionParameters?.SMTP;
|
||||||
|
|
||||||
|
if (!smtpParams) {
|
||||||
|
throw new Error('SMTP settings not configured for this account');
|
||||||
|
}
|
||||||
|
|
||||||
|
const transporter = createTransport({
|
||||||
|
host: smtpParams.host,
|
||||||
|
port: smtpParams.port,
|
||||||
|
auth: {
|
||||||
|
user: connectedAccount.handle,
|
||||||
|
pass: smtpParams.password,
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return transporter;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@ import { MessagingOngoingStaleCronJob } from 'src/modules/messaging/message-impo
|
|||||||
import { MessagingGmailDriverModule } from 'src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module';
|
import { MessagingGmailDriverModule } from 'src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module';
|
||||||
import { MessagingIMAPDriverModule } from 'src/modules/messaging/message-import-manager/drivers/imap/messaging-imap-driver.module';
|
import { MessagingIMAPDriverModule } from 'src/modules/messaging/message-import-manager/drivers/imap/messaging-imap-driver.module';
|
||||||
import { MessagingMicrosoftDriverModule } from 'src/modules/messaging/message-import-manager/drivers/microsoft/messaging-microsoft-driver.module';
|
import { MessagingMicrosoftDriverModule } from 'src/modules/messaging/message-import-manager/drivers/microsoft/messaging-microsoft-driver.module';
|
||||||
|
import { MessagingSmtpDriverModule } from 'src/modules/messaging/message-import-manager/drivers/smtp/messaging-smtp-driver.module';
|
||||||
import { MessagingAddSingleMessageToCacheForImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-add-single-message-to-cache-for-import.job';
|
import { MessagingAddSingleMessageToCacheForImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-add-single-message-to-cache-for-import.job';
|
||||||
import { MessagingCleanCacheJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-clean-cache';
|
import { MessagingCleanCacheJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-clean-cache';
|
||||||
import { MessagingMessageListFetchJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
|
import { MessagingMessageListFetchJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
|
||||||
@ -48,6 +49,7 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess
|
|||||||
MessagingGmailDriverModule,
|
MessagingGmailDriverModule,
|
||||||
MessagingMicrosoftDriverModule,
|
MessagingMicrosoftDriverModule,
|
||||||
MessagingIMAPDriverModule,
|
MessagingIMAPDriverModule,
|
||||||
|
MessagingSmtpDriverModule,
|
||||||
MessagingCommonModule,
|
MessagingCommonModule,
|
||||||
TypeOrmModule.forFeature(
|
TypeOrmModule.forFeature(
|
||||||
[Workspace, DataSourceEntity, ObjectMetadataEntity],
|
[Workspace, DataSourceEntity, ObjectMetadataEntity],
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider';
|
import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider';
|
||||||
import { OAuth2ClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/oauth2-client.provider';
|
import { OAuth2ClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/oauth2-client.provider';
|
||||||
import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
|
import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
|
||||||
|
import { SmtpClientProvider } from 'src/modules/messaging/message-import-manager/drivers/smtp/providers/smtp-client.provider';
|
||||||
import { isAccessTokenRefreshingError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-access-token-refreshing-error.utils';
|
import { isAccessTokenRefreshingError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-access-token-refreshing-error.utils';
|
||||||
import { mimeEncode } from 'src/modules/messaging/message-import-manager/utils/mime-encode.util';
|
import { mimeEncode } from 'src/modules/messaging/message-import-manager/utils/mime-encode.util';
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ export class MessagingSendMessageService {
|
|||||||
private readonly gmailClientProvider: GmailClientProvider,
|
private readonly gmailClientProvider: GmailClientProvider,
|
||||||
private readonly oAuth2ClientProvider: OAuth2ClientProvider,
|
private readonly oAuth2ClientProvider: OAuth2ClientProvider,
|
||||||
private readonly microsoftClientProvider: MicrosoftClientProvider,
|
private readonly microsoftClientProvider: MicrosoftClientProvider,
|
||||||
|
private readonly smtpClientProvider: SmtpClientProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async sendMessage(
|
public async sendMessage(
|
||||||
@ -120,7 +122,16 @@ export class MessagingSendMessageService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ConnectedAccountProvider.IMAP_SMTP_CALDAV: {
|
case ConnectedAccountProvider.IMAP_SMTP_CALDAV: {
|
||||||
throw new Error('IMAP provider does not support sending messages');
|
const smtpClient =
|
||||||
|
await this.smtpClientProvider.getSmtpClient(connectedAccount);
|
||||||
|
|
||||||
|
await smtpClient.sendMail({
|
||||||
|
from: connectedAccount.handle,
|
||||||
|
to: sendMessageInput.to,
|
||||||
|
subject: sendMessageInput.subject,
|
||||||
|
text: sendMessageInput.body,
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
assertUnreachable(
|
assertUnreachable(
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 104 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
Reference in New Issue
Block a user