Skip to content

Commit

Permalink
o365 calendar sync (twentyhq#8044)
Browse files Browse the repository at this point in the history
Implemented:

* Account Connect
* Calendar sync via delta ids then requesting single events


I think I would split the messaging part into a second pr - that's a
step more complex then the calendar :)

---------

Co-authored-by: bosiraphael <[email protected]>
  • Loading branch information
brendanlaschke and bosiraphael authored Nov 7, 2024
1 parent 83f3963 commit f9c076d
Show file tree
Hide file tree
Showing 50 changed files with 1,418 additions and 119 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@linaria/core": "^6.2.0",
"@linaria/react": "^6.2.1",
"@mdx-js/react": "^3.0.0",
"@microsoft/microsoft-graph-client": "^3.0.7",
"@nestjs/apollo": "^11.0.5",
"@nestjs/axios": "^3.0.1",
"@nestjs/cli": "^9.0.0",
Expand Down Expand Up @@ -201,6 +202,7 @@
"@graphql-codegen/typescript": "^3.0.4",
"@graphql-codegen/typescript-operations": "^3.0.4",
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
"@microsoft/microsoft-graph-types": "^2.40.0",
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { InformationBanner } from '@/information-banner/components/InformationBanner';
import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect';
import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys';
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { IconRefresh } from 'twenty-ui';

export const InformationBannerReconnectAccountEmailAliases = () => {
const { accountToReconnect } = useAccountToReconnect(
InformationBannerKeys.ACCOUNTS_TO_RECONNECT_EMAIL_ALIASES,
);

const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
const { triggerApisOAuth } = useTriggerApisOAuth();

if (!accountToReconnect) {
return null;
Expand All @@ -20,7 +20,7 @@ export const InformationBannerReconnectAccountEmailAliases = () => {
message={`Please reconnect your mailbox ${accountToReconnect?.handle} to update your email aliases:`}
buttonTitle="Reconnect"
buttonIcon={IconRefresh}
buttonOnClick={() => triggerGoogleApisOAuth()}
buttonOnClick={() => triggerApisOAuth(accountToReconnect.provider)}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { InformationBanner } from '@/information-banner/components/InformationBanner';
import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect';
import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys';
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { IconRefresh } from 'twenty-ui';

export const InformationBannerReconnectAccountInsufficientPermissions = () => {
const { accountToReconnect } = useAccountToReconnect(
InformationBannerKeys.ACCOUNTS_TO_RECONNECT_INSUFFICIENT_PERMISSIONS,
);

const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
const { triggerApisOAuth } = useTriggerApisOAuth();

if (!accountToReconnect) {
return null;
Expand All @@ -21,7 +21,7 @@ export const InformationBannerReconnectAccountInsufficientPermissions = () => {
reconnect for updates:`}
buttonTitle="Reconnect"
buttonIcon={IconRefresh}
buttonOnClick={() => triggerGoogleApisOAuth()}
buttonOnClick={() => triggerApisOAuth(accountToReconnect.provider)}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useNavigate } from 'react-router-dom';
import { IconGoogle } from 'twenty-ui';
import { IconComponent, IconGoogle, IconMicrosoft } from 'twenty-ui';

import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { SettingsAccountsListEmptyStateCard } from '@/settings/accounts/components/SettingsAccountsListEmptyStateCard';
Expand All @@ -9,6 +9,11 @@ import { SettingsPath } from '@/types/SettingsPath';
import { SettingsAccountsConnectedAccountsRowRightContainer } from '@/settings/accounts/components/SettingsAccountsConnectedAccountsRowRightContainer';
import { SettingsListCard } from '../../components/SettingsListCard';

const ProviderIcons: { [k: string]: IconComponent } = {
google: IconGoogle,
microsoft: IconMicrosoft,
};

export const SettingsAccountsConnectedAccountsListCard = ({
accounts,
loading,
Expand All @@ -27,7 +32,7 @@ export const SettingsAccountsConnectedAccountsListCard = ({
items={accounts}
getItemLabel={(account) => account.handle}
isLoading={loading}
RowIcon={IconGoogle}
RowIconFn={(row) => ProviderIcons[row.provider]}
RowRightComponent={({ item: account }) => (
<SettingsAccountsConnectedAccountsRowRightContainer account={account} />
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import styled from '@emotion/styled';
import { Button, Card, CardContent, CardHeader, IconGoogle } from 'twenty-ui';

import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
import {
Button,
Card,
CardContent,
CardHeader,
IconGoogle,
IconMicrosoft,
} from 'twenty-ui';

const StyledHeader = styled(CardHeader)`
align-items: center;
Expand All @@ -12,6 +19,7 @@ const StyledHeader = styled(CardHeader)`
const StyledBody = styled(CardContent)`
display: flex;
justify-content: center;
gap: ${({ theme }) => theme.spacing(2)};
`;

type SettingsAccountsListEmptyStateCardProps = {
Expand All @@ -21,11 +29,10 @@ type SettingsAccountsListEmptyStateCardProps = {
export const SettingsAccountsListEmptyStateCard = ({
label,
}: SettingsAccountsListEmptyStateCardProps) => {
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();

const handleOnClick = async () => {
await triggerGoogleApisOAuth();
};
const { triggerApisOAuth } = useTriggerApisOAuth();
const isMicrosoftSyncEnabled = useIsFeatureEnabled(
'IS_MICROSOFT_SYNC_ENABLED',
);

return (
<Card>
Expand All @@ -35,8 +42,16 @@ export const SettingsAccountsListEmptyStateCard = ({
Icon={IconGoogle}
title="Connect with Google"
variant="secondary"
onClick={handleOnClick}
onClick={() => triggerApisOAuth('google')}
/>
{isMicrosoftSyncEnabled && (
<Button
Icon={IconMicrosoft}
title="Connect with Microsoft"
variant="secondary"
onClick={() => triggerApisOAuth('microsoft')}
/>
)}
</StyledBody>
</Card>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
Expand All @@ -35,8 +35,7 @@ export const SettingsAccountsRowDropdownMenu = ({
const { destroyOneRecord } = useDestroyOneRecord({
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
});

const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
const { triggerApisOAuth } = useTriggerApisOAuth();

return (
<Dropdown
Expand Down Expand Up @@ -71,7 +70,7 @@ export const SettingsAccountsRowDropdownMenu = ({
LeftIcon={IconRefresh}
text="Reconnect"
onClick={() => {
triggerGoogleApisOAuth();
triggerApisOAuth(account.provider);
closeDropdown();
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,35 @@ import {
useGenerateTransientTokenMutation,
} from '~/generated/graphql';

export const useTriggerGoogleApisOAuth = () => {
const getProviderUrl = (provider: string) => {
switch (provider) {
case 'google':
return 'google-apis';
case 'microsoft':
return 'microsoft-apis';
default:
throw new Error(`Provider ${provider} is not supported`);
}
};

export const useTriggerApisOAuth = () => {
const [generateTransientToken] = useGenerateTransientTokenMutation();

const triggerGoogleApisOAuth = useCallback(
async ({
redirectLocation,
messageVisibility,
calendarVisibility,
loginHint,
}: {
redirectLocation?: AppPath | string;
messageVisibility?: MessageChannelVisibility;
calendarVisibility?: CalendarChannelVisibility;
loginHint?: string;
} = {}) => {
const triggerApisOAuth = useCallback(
async (
provider: string,
{
redirectLocation,
messageVisibility,
calendarVisibility,
loginHint,
}: {
redirectLocation?: AppPath | string;
messageVisibility?: MessageChannelVisibility;
calendarVisibility?: CalendarChannelVisibility;
loginHint?: string;
} = {},
) => {
const authServerUrl = REACT_APP_SERVER_BASE_URL;

const transientToken = await generateTransientToken();
Expand All @@ -46,10 +60,10 @@ export const useTriggerGoogleApisOAuth = () => {

params += loginHint ? `&loginHint=${loginHint}` : '';

window.location.href = `${authServerUrl}/auth/google-apis?${params}`;
window.location.href = `${authServerUrl}/auth/${getProviderUrl(provider)}?${params}`;
},
[generateTransientToken],
);

return { triggerGoogleApisOAuth };
return { triggerApisOAuth };
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { GMAIL_SEND_SCOPE } from '@/accounts/constants/GmailSendScope';
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { Select, SelectOption } from '@/ui/input/components/Select';
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
import { VariableTagInput } from '@/workflow/search-variables/components/VariableTagInput';
Expand Down Expand Up @@ -38,7 +38,8 @@ export const WorkflowEditActionFormSendEmail = (
) => {
const theme = useTheme();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
const { triggerApisOAuth } = useTriggerApisOAuth();

const workflowId = useRecoilValue(workflowIdState);
const redirectUrl = `/object/workflow/${workflowId}`;

Expand Down Expand Up @@ -66,7 +67,7 @@ export const WorkflowEditActionFormSendEmail = (
!isDefined(scopes) ||
!isDefined(scopes.find((scope) => scope === GMAIL_SEND_SCOPE))
) {
await triggerGoogleApisOAuth({
await triggerApisOAuth('google', {
redirectLocation: redirectUrl,
loginHint: connectedAccount.handle,
});
Expand Down Expand Up @@ -183,7 +184,7 @@ export const WorkflowEditActionFormSendEmail = (
options={connectedAccountOptions}
callToActionButton={{
onClick: () =>
triggerGoogleApisOAuth({ redirectLocation: redirectUrl }),
triggerApisOAuth('google', { redirectLocation: redirectUrl }),
Icon: IconPlus,
text: 'Add account',
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export type FeatureFlagKey =
| 'IS_SSO_ENABLED'
| 'IS_UNIQUE_INDEXES_ENABLED'
| 'IS_ARRAY_AND_JSON_FILTER_ENABLED'
| 'IS_MICROSOFT_SYNC_ENABLED'
| 'IS_ADVANCED_FILTERS_ENABLED';
6 changes: 3 additions & 3 deletions packages/twenty-front/src/pages/onboarding/SyncEmails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import { Title } from '@/auth/components/Title';
import { currentUserState } from '@/auth/states/currentUserState';
import { OnboardingSyncEmailsSettingsCard } from '@/onboarding/components/OnboardingSyncEmailsSettingsCard';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';

import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { AppPath } from '@/types/AppPath';
import {
CalendarChannelVisibility,
Expand All @@ -38,7 +38,7 @@ const StyledActionLinkContainer = styled.div`

export const SyncEmails = () => {
const theme = useTheme();
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
const { triggerApisOAuth } = useTriggerApisOAuth();
const setNextOnboardingStatus = useSetNextOnboardingStatus();
const currentUser = useRecoilValue(currentUserState);
const [visibility, setVisibility] = useState<MessageChannelVisibility>(
Expand All @@ -53,7 +53,7 @@ export const SyncEmails = () => {
? CalendarChannelVisibility.ShareEverything
: CalendarChannelVisibility.Metadata;

await triggerGoogleApisOAuth({
await triggerApisOAuth('google', {
redirectLocation: AppPath.Index,
messageVisibility: visibility,
calendarVisibility: calendarChannelVisibility,
Expand Down
1 change: 1 addition & 0 deletions packages/twenty-server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access
# AUTH_MICROSOFT_TENANT_ID=replace_me_with_azure_tenant_id
# AUTH_MICROSOFT_CLIENT_SECRET=replace_me_with_azure_client_secret
# AUTH_MICROSOFT_CALLBACK_URL=http://localhost:3000/auth/microsoft/redirect
# AUTH_MICROSOFT_APIS_CALLBACK_URL=http://localhost:3000/auth/microsoft-apis/get-access-token
# AUTH_GOOGLE_ENABLED=false
# AUTH_GOOGLE_CLIENT_ID=replace_me_with_google_client_id
# AUTH_GOOGLE_CLIENT_SECRET=replace_me_with_google_client_secret
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: false,
},
{
key: FeatureFlagKey.IsMicrosoftSyncEnabled,
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IsAdvancedFiltersEnabled,
workspaceId: workspaceId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { AppTokenService } from 'src/engine/core-modules/app-token/services/app-token.service';
import { GoogleAPIsAuthController } from 'src/engine/core-modules/auth/controllers/google-apis-auth.controller';
import { GoogleAuthController } from 'src/engine/core-modules/auth/controllers/google-auth.controller';
import { MicrosoftAPIsAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller';
import { MicrosoftAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-auth.controller';
import { SSOAuthController } from 'src/engine/core-modules/auth/controllers/sso-auth.controller';
import { VerifyAuthController } from 'src/engine/core-modules/auth/controllers/verify-auth.controller';
import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service';
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
Expand Down Expand Up @@ -80,6 +82,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
GoogleAuthController,
MicrosoftAuthController,
GoogleAPIsAuthController,
MicrosoftAPIsAuthController,
VerifyAuthController,
SSOAuthController,
],
Expand All @@ -90,6 +93,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
SamlAuthStrategy,
AuthResolver,
GoogleAPIsService,
MicrosoftAPIsService,
AppTokenService,
AccessTokenService,
LoginTokenService,
Expand Down
Loading

0 comments on commit f9c076d

Please sign in to comment.