diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 0b1b4ace05..1c72e53ad9 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -483,6 +483,11 @@ export enum CaptchaDriverType { TURNSTILE = 'TURNSTILE' } +export type ChannelSyncSuccess = { + __typename?: 'ChannelSyncSuccess'; + success: Scalars['Boolean']; +}; + export type CheckUserExistOutput = { __typename?: 'CheckUserExistOutput'; availableWorkspacesCount: Scalars['Float']; @@ -1483,6 +1488,7 @@ export type ImapSmtpCaldavConnectionParameters = { export type ImapSmtpCaldavConnectionSuccess = { __typename?: 'ImapSmtpCaldavConnectionSuccess'; + connectedAccountId: Scalars['String']; success: Scalars['Boolean']; }; @@ -1787,6 +1793,7 @@ export type Mutation = { signUpInWorkspace: SignUpOutput; skipBookOnboardingStep: OnboardingStepSuccess; skipSyncEmailOnboardingStep: OnboardingStepSuccess; + startChannelSync: ChannelSyncSuccess; submitFormStep: Scalars['Boolean']; switchBillingPlan: BillingUpdateOutput; switchSubscriptionInterval: BillingUpdateOutput; @@ -2454,6 +2461,11 @@ export type MutationSignUpInWorkspaceArgs = { }; +export type MutationStartChannelSyncArgs = { + connectedAccountId: Scalars['UUID']; +}; + + export type MutationSubmitFormStepArgs = { input: SubmitFormStepInput; }; @@ -5156,7 +5168,14 @@ export type SaveImapSmtpCaldavAccountMutationVariables = Exact<{ }>; -export type SaveImapSmtpCaldavAccountMutation = { __typename?: 'Mutation', saveImapSmtpCaldavAccount: { __typename?: 'ImapSmtpCaldavConnectionSuccess', success: boolean } }; +export type SaveImapSmtpCaldavAccountMutation = { __typename?: 'Mutation', saveImapSmtpCaldavAccount: { __typename?: 'ImapSmtpCaldavConnectionSuccess', success: boolean, connectedAccountId: string } }; + +export type StartChannelSyncMutationVariables = Exact<{ + connectedAccountId: Scalars['UUID']; +}>; + + +export type StartChannelSyncMutation = { __typename?: 'Mutation', startChannelSync: { __typename?: 'ChannelSyncSuccess', success: boolean } }; export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{ id: Scalars['UUID']; @@ -9442,6 +9461,7 @@ export const SaveImapSmtpCaldavAccountDocument = gql` id: $id ) { success + connectedAccountId } } `; @@ -9474,6 +9494,39 @@ export function useSaveImapSmtpCaldavAccountMutation(baseOptions?: Apollo.Mutati export type SaveImapSmtpCaldavAccountMutationHookResult = ReturnType; export type SaveImapSmtpCaldavAccountMutationResult = Apollo.MutationResult; export type SaveImapSmtpCaldavAccountMutationOptions = Apollo.BaseMutationOptions; +export const StartChannelSyncDocument = gql` + mutation StartChannelSync($connectedAccountId: UUID!) { + startChannelSync(connectedAccountId: $connectedAccountId) { + success + } +} + `; +export type StartChannelSyncMutationFn = Apollo.MutationFunction; + +/** + * __useStartChannelSyncMutation__ + * + * To run a mutation, you first call `useStartChannelSyncMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useStartChannelSyncMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [startChannelSyncMutation, { data, loading, error }] = useStartChannelSyncMutation({ + * variables: { + * connectedAccountId: // value for 'connectedAccountId' + * }, + * }); + */ +export function useStartChannelSyncMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(StartChannelSyncDocument, options); + } +export type StartChannelSyncMutationHookResult = ReturnType; +export type StartChannelSyncMutationResult = Apollo.MutationResult; +export type StartChannelSyncMutationOptions = Apollo.BaseMutationOptions; export const GetConnectedImapSmtpCaldavAccountDocument = gql` query GetConnectedImapSmtpCaldavAccount($id: UUID!) { getConnectedImapSmtpCaldavAccount(id: $id) { diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index a2d05a2513..214a6e15cc 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -483,6 +483,11 @@ export enum CaptchaDriverType { TURNSTILE = 'TURNSTILE' } +export type ChannelSyncSuccess = { + __typename?: 'ChannelSyncSuccess'; + success: Scalars['Boolean']; +}; + export type CheckUserExistOutput = { __typename?: 'CheckUserExistOutput'; availableWorkspacesCount: Scalars['Float']; @@ -1440,6 +1445,7 @@ export type ImapSmtpCaldavConnectionParameters = { export type ImapSmtpCaldavConnectionSuccess = { __typename?: 'ImapSmtpCaldavConnectionSuccess'; + connectedAccountId: Scalars['String']; success: Scalars['Boolean']; }; @@ -1742,6 +1748,7 @@ export type Mutation = { signUpInWorkspace: SignUpOutput; skipBookOnboardingStep: OnboardingStepSuccess; skipSyncEmailOnboardingStep: OnboardingStepSuccess; + startChannelSync: ChannelSyncSuccess; submitFormStep: Scalars['Boolean']; switchBillingPlan: BillingUpdateOutput; switchSubscriptionInterval: BillingUpdateOutput; @@ -2385,6 +2392,11 @@ export type MutationSignUpInWorkspaceArgs = { }; +export type MutationStartChannelSyncArgs = { + connectedAccountId: Scalars['UUID']; +}; + + export type MutationSubmitFormStepArgs = { input: SubmitFormStepInput; }; diff --git a/packages/twenty-front/src/modules/accounts/types/CalendarChannel.ts b/packages/twenty-front/src/modules/accounts/types/CalendarChannel.ts index 2b20c7a086..c0cff271db 100644 --- a/packages/twenty-front/src/modules/accounts/types/CalendarChannel.ts +++ b/packages/twenty-front/src/modules/accounts/types/CalendarChannel.ts @@ -9,6 +9,7 @@ export enum CalendarChannelSyncStatus { } export enum CalendarChannelSyncStage { + PENDING_CONFIGURATION = 'PENDING_CONFIGURATION', FULL_CALENDAR_EVENT_LIST_FETCH_PENDING = 'FULL_CALENDAR_EVENT_LIST_FETCH_PENDING', // WILL BE DEPRECATED PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING = 'PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING', // DEPRECATED CALENDAR_EVENT_LIST_FETCH_PENDING = 'CALENDAR_EVENT_LIST_FETCH_PENDING', diff --git a/packages/twenty-front/src/modules/accounts/types/MessageChannel.ts b/packages/twenty-front/src/modules/accounts/types/MessageChannel.ts index d82c9a1dd4..92fd9a44bd 100644 --- a/packages/twenty-front/src/modules/accounts/types/MessageChannel.ts +++ b/packages/twenty-front/src/modules/accounts/types/MessageChannel.ts @@ -23,6 +23,7 @@ export enum MessageChannelSyncStatus { } export enum MessageChannelSyncStage { + PENDING_CONFIGURATION = 'PENDING_CONFIGURATION', FULL_MESSAGE_LIST_FETCH_PENDING = 'FULL_MESSAGE_LIST_FETCH_PENDING', // WILL BE DEPRECATED PARTIAL_MESSAGE_LIST_FETCH_PENDING = 'PARTIAL_MESSAGE_LIST_FETCH_PENDING', // DEPRECATED MESSAGE_LIST_FETCH_PENDING = 'MESSAGE_LIST_FETCH_PENDING', diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 8840a43ca9..548b8f6a5c 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -37,6 +37,14 @@ const SettingsAccountsEmails = lazy(() => })), ); +const SettingsAccountsConfiguration = lazy(() => + import('~/pages/settings/accounts/SettingsAccountsConfiguration').then( + (module) => ({ + default: module.SettingsAccountsConfiguration, + }), + ), +); + const SettingsNewAccount = lazy(() => import('~/pages/settings/accounts/SettingsNewAccount').then((module) => ({ default: module.SettingsNewAccount, @@ -405,6 +413,10 @@ export const SettingsRoutes = ({ } /> } /> } /> + } + /> } diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx index fd9ca14d3a..ae2b592f8b 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx @@ -1,7 +1,10 @@ import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; -import { type CalendarChannel } from '@/accounts/types/CalendarChannel'; +import { + type CalendarChannel, + CalendarChannelSyncStage, +} from '@/accounts/types/CalendarChannel'; import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -44,6 +47,9 @@ export const SettingsAccountsCalendarChannelsContainer = () => { connectedAccountId: { in: accounts.map((account) => account.id), }, + syncStage: { + neq: CalendarChannelSyncStage.PENDING_CONFIGURATION, + }, }, skip: !accounts.length, }); diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx index 7dba93c97e..5f9244e6d2 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx @@ -2,7 +2,10 @@ import styled from '@emotion/styled'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount'; -import { type MessageChannel } from '@/accounts/types/MessageChannel'; +import { + type MessageChannel, + MessageChannelSyncStage, +} from '@/accounts/types/MessageChannel'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject'; @@ -58,6 +61,9 @@ export const SettingsAccountsMessageChannelsContainer = () => { isSyncEnabled: { eq: true, }, + syncStage: { + neq: MessageChannelSyncStage.PENDING_CONFIGURATION, + }, }, recordGqlFields, onCompleted: (data) => { diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/saveImapSmtpCaldavConnection.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/saveImapSmtpCaldavConnection.ts index f877a80c70..eb0ef11caf 100644 --- a/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/saveImapSmtpCaldavConnection.ts +++ b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/saveImapSmtpCaldavConnection.ts @@ -14,6 +14,7 @@ export const SAVE_IMAP_SMTP_CALDAV_ACCOUNT = gql` id: $id ) { success + connectedAccountId } } `; diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/startChannelSync.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/startChannelSync.ts new file mode 100644 index 0000000000..ff62342d48 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/startChannelSync.ts @@ -0,0 +1,9 @@ +import gql from 'graphql-tag'; + +export const START_CHANNEL_SYNC = gql` + mutation StartChannelSync($connectedAccountId: UUID!) { + startChannelSync(connectedAccountId: $connectedAccountId) { + success + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/accounts/hooks/useImapSmtpCaldavConnectionForm.ts b/packages/twenty-front/src/modules/settings/accounts/hooks/useImapSmtpCaldavConnectionForm.ts index 59ceb83194..d0383d1950 100644 --- a/packages/twenty-front/src/modules/settings/accounts/hooks/useImapSmtpCaldavConnectionForm.ts +++ b/packages/twenty-front/src/modules/settings/accounts/hooks/useImapSmtpCaldavConnectionForm.ts @@ -132,7 +132,7 @@ export const useImapSmtpCaldavConnectionForm = ({ }); try { - await saveConnection({ + const { data } = await saveConnection({ variables: { ...(isEditing && connectedAccountId ? { id: connectedAccountId } @@ -142,13 +142,20 @@ export const useImapSmtpCaldavConnectionForm = ({ connectionParameters, }, }); + if (!isDefined(data)) return; const successMessage = isEditing ? t`Connection successfully updated` : t`Connection successfully created`; enqueueSuccessSnackBar({ message: successMessage }); - navigate(SettingsPath.Accounts); + + const { connectedAccountId: returnedConnectedAccountId } = + data?.saveImapSmtpCaldavAccount || {}; + + navigate(SettingsPath.AccountsConfiguration, { + connectedAccountId: returnedConnectedAccountId, + }); } catch (error) { enqueueErrorSnackBar({ apolloError: error instanceof ApolloError ? error : undefined, diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx index e357c77611..d25236a231 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx @@ -1,4 +1,5 @@ import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount'; +import { MessageChannelSyncStage } from '@/accounts/types/MessageChannel'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject'; @@ -26,14 +27,25 @@ export const SettingsAccounts = () => { shouldOnlyLoadRelationIdentifiers: false, }); - const { records: accounts, loading } = useFindManyRecords({ - objectNameSingular: CoreObjectNameSingular.ConnectedAccount, - filter: { - accountOwnerId: { - eq: currentWorkspaceMember?.id, + const { records: allAccounts, loading } = + useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.ConnectedAccount, + filter: { + accountOwnerId: { + eq: currentWorkspaceMember?.id, + }, }, - }, - recordGqlFields, + recordGqlFields, + }); + + const accountsToShow = allAccounts.filter((account) => { + return ( + account.messageChannels.length === 0 || + account.messageChannels.some( + (channel) => + channel.syncStage !== MessageChannelSyncStage.PENDING_CONFIGURATION, + ) + ); }); return ( @@ -57,7 +69,9 @@ export const SettingsAccounts = () => { title={t`Connected accounts`} description={t`Manage your internet accounts.`} /> - + diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsConfiguration.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsConfiguration.tsx new file mode 100644 index 0000000000..b0b6ed35a9 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsConfiguration.tsx @@ -0,0 +1,111 @@ +import { useLingui } from '@lingui/react/macro'; +import { useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; + +import { type CalendarChannel } from '@/accounts/types/CalendarChannel'; +import { type MessageChannel } from '@/accounts/types/MessageChannel'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { SettingsPath } from 'twenty-shared/types'; +import { getSettingsPath, isDefined } from 'twenty-shared/utils'; +import { useStartChannelSyncMutation } from '~/generated-metadata/graphql'; +import { SettingsAccountsConfigurationStepCalendar } from '~/pages/settings/accounts/SettingsAccountsConfigurationStepCalendar'; +import { SettingsAccountsConfigurationStepEmail } from '~/pages/settings/accounts/SettingsAccountsConfigurationStepEmail'; + +enum SettingsAccountsConfigurationStep { + Email = 'email', + Calendar = 'calendar', +} + +export const SettingsAccountsConfiguration = () => { + const { t } = useLingui(); + const { connectedAccountId } = useParams<{ + connectedAccountId: string; + }>(); + const navigate = useNavigate(); + const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); + const [startChannelSyncMutation, { loading: isSubmitting }] = + useStartChannelSyncMutation(); + + const [currentStep, setCurrentStep] = + useState( + SettingsAccountsConfigurationStep.Email, + ); + + const { records: messageChannels } = useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.MessageChannel, + filter: { + connectedAccountId: { + eq: connectedAccountId, + }, + }, + skip: !connectedAccountId, + }); + + const { records: calendarChannels } = useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.CalendarChannel, + filter: { + connectedAccountId: { + eq: connectedAccountId, + }, + }, + skip: !connectedAccountId, + }); + + const messageChannel = messageChannels[0]; + const calendarChannel = calendarChannels[0]; + + const handleNext = () => { + setCurrentStep(SettingsAccountsConfigurationStep.Calendar); + }; + + const handleAddAccount = async () => { + if (!connectedAccountId) return; + + await startChannelSyncMutation({ + variables: { + connectedAccountId, + }, + onCompleted: () => { + enqueueSuccessSnackBar({ + message: t`Account added successfully. Sync started.`, + }); + navigate(getSettingsPath(SettingsPath.Accounts)); + }, + onError: (error) => { + enqueueErrorSnackBar({ + apolloError: error, + }); + }, + }); + }; + + switch (currentStep) { + case SettingsAccountsConfigurationStep.Email: + if (!isDefined(messageChannel)) { + return null; + } + return ( + + ); + case SettingsAccountsConfigurationStep.Calendar: + if (!isDefined(calendarChannel)) { + return null; + } + return ( + + ); + } +}; diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsConfigurationStepCalendar.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsConfigurationStepCalendar.tsx new file mode 100644 index 0000000000..8a3e00da23 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsConfigurationStepCalendar.tsx @@ -0,0 +1,66 @@ +import { Trans, useLingui } from '@lingui/react/macro'; + +import { type CalendarChannel } from '@/accounts/types/CalendarChannel'; +import { type MessageChannel } from '@/accounts/types/MessageChannel'; +import { SettingsAccountsCalendarChannelDetails } from '@/settings/accounts/components/SettingsAccountsCalendarChannelDetails'; +import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { SettingsPath } from 'twenty-shared/types'; +import { getSettingsPath, isDefined } from 'twenty-shared/utils'; +import { IconPlus } from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; + +type SettingsAccountsConfigurationStepCalendarProps = { + calendarChannel: CalendarChannel; + messageChannel?: MessageChannel; + isSubmitting: boolean; + onAddAccount: () => void; +}; + +export const SettingsAccountsConfigurationStepCalendar = ({ + calendarChannel, + messageChannel, + isSubmitting, + onAddAccount, +}: SettingsAccountsConfigurationStepCalendarProps) => { + const { t } = useLingui(); + + const stepNumber = isDefined(messageChannel) ? 2 : 1; + const stepTitle = t`${stepNumber}. Calendar`; + + return ( + User, + href: getSettingsPath(SettingsPath.ProfilePage), + }, + { + children: Account, + href: getSettingsPath(SettingsPath.Accounts), + }, + { + children: stepTitle, + }, + ]} + actionButton={ +