Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Overlay Calendar v2 and Troubleshooter v2 #14693

Open
wants to merge 52 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
40b5ad6
feat: add signin with google for overlay calendar user
kart1ka Apr 21, 2024
161f36d
feat: get event title for Overlay Calendar from Google Calendar service
kart1ka Apr 21, 2024
c368c66
feat: Display Event Title for overlay calendar for Google Calendar Se…
kart1ka Apr 21, 2024
6e6854e
feat: Display Event Titles for Troubleshooter
kart1ka Apr 21, 2024
07120a6
fix
kart1ka Apr 22, 2024
bdb712d
Merge branch 'main' into feat/overlay-v2
sean-brydon Apr 22, 2024
473d209
test: Update getCalendarsEvents.test.ts
kart1ka Apr 24, 2024
434e8e4
Merge branch 'main' into feat/overlay-v2
kart1ka Apr 24, 2024
74aa500
Merge branch 'main' into feat/overlay-v2
kart1ka Apr 30, 2024
620ab34
feat: add Microsoft OAuth Provider for Overlay User Sign Up
kart1ka May 1, 2024
d8e3284
feat: fetch event title from office365 calendar
kart1ka May 1, 2024
f0191b2
Merge branch 'main' into feat/overlay-v2
kart1ka May 1, 2024
79df2d0
test
kart1ka May 1, 2024
ad35cfd
Merge branch 'main' into feat/overlay-v2
joeauyeung May 1, 2024
a99c9e9
Merge branch 'main' into feat/overlay-v2
kart1ka May 2, 2024
65bcf69
Merge branch 'main' into feat/overlay-v2
kart1ka May 5, 2024
a361898
add: create public getEventList method on CalendarService class
kart1ka May 5, 2024
096b7d9
refactor: create public getEventList method on google and office365 c…
kart1ka May 5, 2024
256ff31
test
kart1ka May 5, 2024
e38562c
Merge remote-tracking branch 'origin/main' into feat/overlay-v2
kart1ka May 6, 2024
78df9d4
Merge branch 'main' into feat/overlay-v2
kart1ka May 6, 2024
32da2cd
Merge branch 'main' into feat/overlay-v2
joeauyeung May 6, 2024
1851f11
Merge branch 'main' into feat/overlay-v2
Udit-takkar May 7, 2024
405d1cf
Merge branch 'main' into feat/overlay-v2
kart1ka May 8, 2024
53a9951
Merge branch 'main' into feat/overlay-v2
CarinaWolli May 10, 2024
8961569
Merge branch 'main' into feat/overlay-v2
joeauyeung May 10, 2024
ee61117
revert: microsoft identity provider
kart1ka May 12, 2024
5de7e07
fix
kart1ka May 12, 2024
9f92964
Merge branch 'main' into feat/overlay-v2
kart1ka May 12, 2024
ed69f85
deleting microsoft migration files
kart1ka May 12, 2024
bd69a73
Merge branch 'main' into feat/overlay-v2
kart1ka May 15, 2024
63d57e6
Merge branch 'main' into feat/overlay-v2
kart1ka May 21, 2024
28893c4
Merge branch 'main' into feat/overlay-v2
kart1ka May 23, 2024
6140e5a
Merge branch 'main' into feat/overlay-v2
kart1ka May 25, 2024
86f2d65
Merge branch 'main' into feat/overlay-v2
kart1ka Jun 3, 2024
50d209b
Merge branch 'main' into feat/overlay-v2
kart1ka Jun 10, 2024
4b9ec85
Merge branch 'main' into feat/overlay-v2
kart1ka Jun 12, 2024
d67c6ac
Merge branch 'main' into feat/overlay-v2
kart1ka Jun 22, 2024
21f2f60
Merge branch 'main' into feat/overlay-v2
zomars Jun 24, 2024
e10ad6d
Merge branch 'main' into feat/overlay-v2
zomars Jun 25, 2024
da9d12a
Merge branch 'main' into feat/overlay-v2
zomars Jun 25, 2024
ff402b3
Merge branch 'main' into feat/overlay-v2
kart1ka Jun 28, 2024
97a346a
Merge branch 'main' into feat/overlay-v2
kart1ka Jul 1, 2024
09e8a40
fix: primary cal doesn't connect as overlay cal on overlay user login
kart1ka Jul 1, 2024
84a43e8
Merge branch 'main' into feat/overlay-v2
kart1ka Jul 8, 2024
0104319
fix: resolve typeScript error
kart1ka Jul 8, 2024
b725b0e
Merge branch 'main' into feat/overlay-v2
kart1ka Aug 2, 2024
a0c2c16
Merge branch 'main' into feat/overlay-v2
kart1ka Aug 10, 2024
27f87d7
Merge branch 'main' into feat/overlay-v2
kart1ka Aug 14, 2024
06734eb
Merge branch 'main' into feat/overlay-v2
kart1ka Aug 31, 2024
695f373
Merge branch 'main' into feat/overlay-v2
zomars Sep 18, 2024
6adb3d3
Solved conflicts
zomars Sep 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
loading={buttonProps?.isPending}
onClick={(event) => {
// Save cookie key to return url step
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
document.cookie = `return-to=${encodeURIComponent(
window.location.href
)};path=/;max-age=3600;SameSite=Lax`;
buttonProps && buttonProps.onClick && buttonProps?.onClick(event);
}}>
{installed ? t("installed") : t("connect")}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useSearchParams } from "next/navigation";

import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
Expand All @@ -14,12 +16,23 @@ interface IConnectCalendarsProps {

const ConnectedCalendars = (props: IConnectCalendarsProps) => {
const { nextStep } = props;
const searchParams = useSearchParams();
const callbackBookerUrl = searchParams?.get("callbackUrl");
const overlayProvider = searchParams?.get("overlayProvider");
const queryConnectedCalendars = trpc.viewer.connectedCalendars.useQuery({ onboarding: true });
const { t } = useLocale();
const queryIntegrations = trpc.viewer.integrations.useQuery({
variant: "calendar",
onlyInstalled: false,
sortByMostPopular: true,
appId:
callbackBookerUrl && overlayProvider
? overlayProvider === "google"
? "google-calendar"
: overlayProvider === "azure-ad"
? "office365-calendar"
: undefined
: undefined,
});

const firstCalendar = queryConnectedCalendars.data?.connectedCalendars.find(
Expand Down
24 changes: 23 additions & 1 deletion apps/web/lib/getting-started/[[...step]]/getServerSideProps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import prisma from "@calcom/prisma";
import { ssrInit } from "@server/lib/ssr";

export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req } = context;
const { req, query } = context;

const session = await getServerSession({ req });

Expand Down Expand Up @@ -38,13 +38,35 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
},
},
},
credentials: {
select: {
appId: true,
id: true,
},
},
},
});

if (!user) {
throw new Error("User from session not found");
}

if (
query?.step &&
query.step[0] === "connected-calendar" &&
!!(query.callbackUrl && query.overlayProvider)
) {
if (
user.completedOnboarding ||
(user.credentials.length > 0 &&
user.credentials.find(
(credential) => credential.appId === "google-calendar" || credential.appId === "office365-calendar"
))
) {
return { redirect: { permanent: false, destination: query.callbackUrl.toString() } };
}
}

if (user.completedOnboarding) {
return { redirect: { permanent: false, destination: "/event-types" } };
}
Expand Down
22 changes: 17 additions & 5 deletions apps/web/pages/getting-started/[[...step]].tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import Head from "next/head";
import { usePathname, useRouter } from "next/navigation";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { Suspense } from "react";
import { z } from "zod";

Expand Down Expand Up @@ -48,7 +48,7 @@ const stepRouteSchema = z.object({
const OnboardingPage = () => {
const pathname = usePathname();
const params = useParamsWithFallback();

const searchParams = useSearchParams();
const router = useRouter();
const [user] = trpc.viewer.me.useSuspenseQuery();
const { t } = useLocale();
Expand All @@ -60,6 +60,8 @@ const OnboardingPage = () => {

const currentStep = result.success ? result.data.step[0] : INITIAL_STEP;
const from = result.success ? result.data.from : "";
const callbackBookerUrl = searchParams?.get("callbackUrl");
const isOverlayUser = currentStep === "connected-calendar" && !!callbackBookerUrl;
const headers = [
{
title: `${t("welcome_to_cal_header", { appName: APP_NAME })}`,
Expand Down Expand Up @@ -136,14 +138,24 @@ const OnboardingPage = () => {
</p>
))}
</header>
<Steps maxSteps={steps.length} currentStep={currentStepIndex + 1} navigateToStep={goToIndex} />
{!isOverlayUser && (
<Steps
maxSteps={steps.length}
currentStep={currentStepIndex + 1}
navigateToStep={goToIndex}
/>
)}
</div>
<StepCard>
<Suspense fallback={<Icon name="loader" />}>
{currentStep === "user-settings" && (
<UserSettings nextStep={() => goToIndex(1)} hideUsername={from === "signup"} />
)}
{currentStep === "connected-calendar" && <ConnectedCalendars nextStep={() => goToIndex(2)} />}
{currentStep === "connected-calendar" && (
<ConnectedCalendars
nextStep={isOverlayUser ? () => router.push(callbackBookerUrl) : () => goToIndex(2)}
/>
)}

{currentStep === "connected-video" && <ConnectedVideoStep nextStep={() => goToIndex(3)} />}

Expand All @@ -157,7 +169,7 @@ const OnboardingPage = () => {
</Suspense>
</StepCard>

{headers[currentStepIndex]?.skipText && (
{!isOverlayUser && headers[currentStepIndex]?.skipText && (
<div className="flex w-full flex-row justify-center">
<Button
color="minimal"
Expand Down
1 change: 1 addition & 0 deletions apps/web/public/microsoft-icon.svg
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The icon provided here is for demonstrative purposes only. Feel free to replace it with a more fitting one. Thanks!"

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
188 changes: 134 additions & 54 deletions packages/app-store/googlecalendar/lib/CalendarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
Calendar,
CalendarEvent,
EventBusyDate,
EventBusyData,
IntegrationCalendar,
NewCalendarEventType,
} from "@calcom/types/Calendar";
Expand Down Expand Up @@ -514,6 +515,128 @@ export default class GoogleCalendarService implements Calendar {
}
}

async getCalIds(selectedCalendars: IntegrationCalendar[]): Promise<string[]> {
const selectedCalendarIds = selectedCalendars
.filter((e) => e.integration === this.integrationName)
.map((e) => e.externalId);
if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
// Only calendars of other integrations selected
return [];
}
if (selectedCalendarIds.length !== 0) return selectedCalendarIds;
const calendar = await this.authedCalendar();
const cals = await calendar.calendarList.list({ fields: "items(id)" });
if (!cals.data.items) return [];
return cals.data.items.reduce((c, cal) => (cal.id ? [...c, cal.id] : c), [] as string[]);
}

// fetches free-busy/events data with date range check
async fetchCalendarDataWithDateRangeCheck(
dateFrom: string,
dateTo: string,
calsIds: string[],
getEventsOrFreeBusyData: (args: {
timeMin: string;
timeMax: string;
items: { id: string }[];
}) => Promise<EventBusyDate[] | null> | Promise<EventBusyData[]>
) {
const originalStartDate = dayjs(dateFrom);
const originalEndDate = dayjs(dateTo);
const diff = originalEndDate.diff(originalStartDate, "days");
if (diff <= 90) {
const data = await getEventsOrFreeBusyData({
timeMin: dateFrom,
timeMax: dateTo,
items: calsIds.map((id) => ({ id })),
});

if (!data) throw new Error("No response from google calendar");

return data;
} else {
const busyData = [];

const loopsNumber = Math.ceil(diff / 90);

let startDate = originalStartDate;
let endDate = originalStartDate.add(90, "days");

for (let i = 0; i < loopsNumber; i++) {
if (endDate.isAfter(originalEndDate)) endDate = originalEndDate;

busyData.push(
...((await getEventsOrFreeBusyData({
timeMin: startDate.format(),
timeMax: endDate.format(),
items: calsIds.map((id) => ({ id })),
})) || [])
);

startDate = endDate.add(1, "minutes");
endDate = startDate.add(90, "days");
}
return busyData;
}
}

async getEventList(
dateFrom: string,
dateTo: string,
selectedCalendars: IntegrationCalendar[]
): Promise<EventBusyData[]> {
try {
const calsIds = await this.getCalIds(selectedCalendars);
if (calsIds.length === 0) return [];

const fetchEventData = async (args: {
timeMin: string;
timeMax: string;
items: { id: string }[];
}): Promise<EventBusyData[]> => {
const calendar = await this.authedCalendar();
const { timeMin, timeMax, items } = args;
const events = await Promise.all(
items.map(async (item) => {
const { json: eventData } = await this.oAuthManagerInstance.request(
async () =>
new AxiosLikeResponseToFetchResponse(
await calendar.events.list({
calendarId: item.id,
timeMin: timeMin,
timeMax: timeMax,
fields: "items(summary,start/dateTime, end/dateTime)",
})
)
);

if (!eventData.items || eventData.items?.length === 0) return [];

return eventData.items.map((event) => {
const busyData: EventBusyData = {
start: event.start?.dateTime || "",
end: event.end?.dateTime || "",
title: event.summary || "",
};
return busyData;
});
})
);
if (events.length === 0) return [];
return events.flat();
};

const data = await this.fetchCalendarDataWithDateRangeCheck(dateFrom, dateTo, calsIds, fetchEventData);
return data;
} catch (error) {
this.log.error(
"There was an error getting availability from google calendar: ",
safeStringify({ error, selectedCalendars })
);
throw error;
}
}

async getCacheOrFetchAvailability(args: {
timeMin: string;
timeMax: string;
Expand Down Expand Up @@ -599,62 +722,19 @@ export default class GoogleCalendarService implements Calendar {
dateTo: string,
selectedCalendars: IntegrationCalendar[]
): Promise<EventBusyDate[]> {
this.log.debug("Getting availability", safeStringify({ dateFrom, dateTo, selectedCalendars }));
const calendar = await this.authedCalendar();
const selectedCalendarIds = selectedCalendars
.filter((e) => e.integration === this.integrationName)
.map((e) => e.externalId);
if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
// Only calendars of other integrations selected
return [];
}
async function getCalIds() {
if (selectedCalendarIds.length !== 0) return selectedCalendarIds;
const cals = await calendar.calendarList.list({ fields: "items(id)" });
if (!cals.data.items) return [];
return cals.data.items.reduce((c, cal) => (cal.id ? [...c, cal.id] : c), [] as string[]);
}
this.log.debug("Getting availability");

try {
const calsIds = await getCalIds();
const originalStartDate = dayjs(dateFrom);
const originalEndDate = dayjs(dateTo);
const diff = originalEndDate.diff(originalStartDate, "days");

// /freebusy from google api only allows a date range of 90 days
if (diff <= 90) {
const freeBusyData = await this.getCacheOrFetchAvailability({
timeMin: dateFrom,
timeMax: dateTo,
items: calsIds.map((id) => ({ id })),
});
if (!freeBusyData) throw new Error("No response from google calendar");

return freeBusyData;
} else {
const busyData = [];

const loopsNumber = Math.ceil(diff / 90);

let startDate = originalStartDate;
let endDate = originalStartDate.add(90, "days");

for (let i = 0; i < loopsNumber; i++) {
if (endDate.isAfter(originalEndDate)) endDate = originalEndDate;

busyData.push(
...((await this.getCacheOrFetchAvailability({
timeMin: startDate.format(),
timeMax: endDate.format(),
items: calsIds.map((id) => ({ id })),
})) || [])
);

startDate = endDate.add(1, "minutes");
endDate = startDate.add(90, "days");
}
return busyData;
}
const calsIds = await this.getCalIds(selectedCalendars);
if (calsIds.length === 0) return [];

const data = await this.fetchCalendarDataWithDateRangeCheck(
dateFrom,
dateTo,
calsIds,
this.getCacheOrFetchAvailability.bind(this)
);
return data;
} catch (error) {
this.log.error(
"There was an error getting availability from google calendar: ",
Expand Down
Loading
Loading