o365 calendar sync (#8044)

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 <raphael.bosi@gmail.com>
This commit is contained in:
brendanlaschke
2024-11-07 18:13:22 +01:00
committed by GitHub
parent 83f3963bfb
commit f9c076df31
50 changed files with 1417 additions and 118 deletions

View File

@ -1,7 +1,7 @@
import { InformationBanner } from '@/information-banner/components/InformationBanner';
import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect';
import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys';
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { IconRefresh } from 'twenty-ui';
export const InformationBannerReconnectAccountEmailAliases = () => {
@ -9,7 +9,7 @@ export const InformationBannerReconnectAccountEmailAliases = () => {
InformationBannerKeys.ACCOUNTS_TO_RECONNECT_EMAIL_ALIASES,
);
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
const { triggerApisOAuth } = useTriggerApisOAuth();
if (!accountToReconnect) {
return null;
@ -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)}
/>
);
};

View File

@ -1,7 +1,7 @@
import { InformationBanner } from '@/information-banner/components/InformationBanner';
import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect';
import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys';
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { IconRefresh } from 'twenty-ui';
export const InformationBannerReconnectAccountInsufficientPermissions = () => {
@ -9,7 +9,7 @@ export const InformationBannerReconnectAccountInsufficientPermissions = () => {
InformationBannerKeys.ACCOUNTS_TO_RECONNECT_INSUFFICIENT_PERMISSIONS,
);
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
const { triggerApisOAuth } = useTriggerApisOAuth();
if (!accountToReconnect) {
return null;
@ -21,7 +21,7 @@ export const InformationBannerReconnectAccountInsufficientPermissions = () => {
reconnect for updates:`}
buttonTitle="Reconnect"
buttonIcon={IconRefresh}
buttonOnClick={() => triggerGoogleApisOAuth()}
buttonOnClick={() => triggerApisOAuth(accountToReconnect.provider)}
/>
);
};

View File

@ -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';
@ -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,
@ -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} />
)}

View File

@ -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;
@ -12,6 +19,7 @@ const StyledHeader = styled(CardHeader)`
const StyledBody = styled(CardContent)`
display: flex;
justify-content: center;
gap: ${({ theme }) => theme.spacing(2)};
`;
type SettingsAccountsListEmptyStateCardProps = {
@ -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>
@ -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>
);

View File

@ -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';
@ -35,8 +35,7 @@ export const SettingsAccountsRowDropdownMenu = ({
const { destroyOneRecord } = useDestroyOneRecord({
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
});
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
const { triggerApisOAuth } = useTriggerApisOAuth();
return (
<Dropdown
@ -71,7 +70,7 @@ export const SettingsAccountsRowDropdownMenu = ({
LeftIcon={IconRefresh}
text="Reconnect"
onClick={() => {
triggerGoogleApisOAuth();
triggerApisOAuth(account.provider);
closeDropdown();
}}
/>

View File

@ -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();
@ -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 };
};

View File

@ -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';
@ -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}`;
@ -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,
});
@ -183,7 +184,7 @@ export const WorkflowEditActionFormSendEmail = (
options={connectedAccountOptions}
callToActionButton={{
onClick: () =>
triggerGoogleApisOAuth({ redirectLocation: redirectUrl }),
triggerApisOAuth('google', { redirectLocation: redirectUrl }),
Icon: IconPlus,
text: 'Add account',
}}

View File

@ -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';