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 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
34 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
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
@@ -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,15 @@ interface IConnectCalendarsProps {

const ConnectedCalendars = (props: IConnectCalendarsProps) => {
const { nextStep } = props;
const searchParams = useSearchParams();
const callbackBookerUrl = searchParams?.get("callbackUrl");
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 ? "google-calendar" : undefined,
});

const firstCalendar = queryConnectedCalendars.data?.connectedCalendars.find(
Expand Down
18 changes: 17 additions & 1 deletion apps/web/lib/getting-started/[[...step]]/getServerSideProps.tsx
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,29 @@ 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) {
if (
user.completedOnboarding ||
(user.credentials.length > 0 &&
user.credentials.find((credential) => credential.appId === "google-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
@@ -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
71 changes: 62 additions & 9 deletions packages/app-store/googlecalendar/lib/CalendarService.ts
Expand Up @@ -17,6 +17,7 @@ import type {
Calendar,
CalendarEvent,
EventBusyDate,
EventBusyData,
IntegrationCalendar,
NewCalendarEventType,
} from "@calcom/types/Calendar";
Expand Down Expand Up @@ -468,6 +469,38 @@ export default class GoogleCalendarService implements Calendar {
}
}

async getEventList(args: {
timeMin: string;
timeMax: string;
items: { id: string }[];
}): Promise<EventBusyData[] | null> {
const calendar = await this.authedCalendar();
const { timeMin, timeMax, items } = args;
const events = await Promise.all(
items.map(async (item) => {
const { data } = await calendar.events.list({
calendarId: item.id,
timeMin: timeMin,
timeMax: timeMax,
});
kart1ka marked this conversation as resolved.
Show resolved Hide resolved

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

return data.items.map((event) => {
const busyData: EventBusyData = {
start: event.start?.dateTime || "",
end: event.end?.dateTime || "",
title: event.summary || "",
};
return busyData;
});
})
);

if (events.length === 0) return null;
return events.flat();
}

async getCacheOrFetchAvailability(args: {
timeMin: string;
timeMax: string;
Expand Down Expand Up @@ -546,8 +579,9 @@ export default class GoogleCalendarService implements Calendar {
async getAvailability(
dateFrom: string,
dateTo: string,
selectedCalendars: IntegrationCalendar[]
): Promise<EventBusyDate[]> {
selectedCalendars: IntegrationCalendar[],
isOverlayUser?: boolean
): Promise<EventBusyData[]> {
const calendar = await this.authedCalendar();
const selectedCalendarIds = selectedCalendars
.filter((e) => e.integration === this.integrationName)
Expand All @@ -571,6 +605,15 @@ export default class GoogleCalendarService implements Calendar {

// /freebusy from google api only allows a date range of 90 days
if (diff <= 90) {
if (isOverlayUser) {
const eventsListData = await this.getEventList({
timeMin: dateFrom,
timeMax: dateTo,
items: calsIds.map((id) => ({ id })),
});
if (!eventsListData) throw new Error("No response from google calendar");
return eventsListData;
}
const freeBusyData = await this.getCacheOrFetchAvailability({
timeMin: dateFrom,
timeMax: dateTo,
Expand All @@ -590,13 +633,23 @@ export default class GoogleCalendarService implements Calendar {
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 })),
})) || [])
);
if (isOverlayUser) {
busyData.push(
...((await this.getEventList({
timeMin: startDate.format(),
timeMax: endDate.format(),
items: calsIds.map((id) => ({ id })),
})) || [])
);
} else {
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");
Expand Down
8 changes: 5 additions & 3 deletions packages/core/CalendarManager.ts
Expand Up @@ -13,6 +13,7 @@ import { performance } from "@calcom/lib/server/perfObserver";
import type {
CalendarEvent,
EventBusyDate,
EventBusyData,
IntegrationCalendar,
NewCalendarEventType,
} from "@calcom/types/Calendar";
Expand Down Expand Up @@ -203,16 +204,17 @@ export const getBusyCalendarTimes = async (
withCredentials: CredentialPayload[],
dateFrom: string,
dateTo: string,
selectedCalendars: SelectedCalendar[]
selectedCalendars: SelectedCalendar[],
isOverlayUser?: boolean
) => {
let results: EventBusyDate[][] = [];
let results: EventBusyData[][] = [];
// const months = getMonths(dateFrom, dateTo);
try {
// Subtract 11 hours from the start date to avoid problems in UTC- time zones.
const startDate = dayjs(dateFrom).subtract(11, "hours").format();
// Add 14 hours from the start date to avoid problems in UTC+ time zones.
const endDate = dayjs(dateTo).endOf("month").add(14, "hours").format();
results = await getCalendarsEvents(withCredentials, startDate, endDate, selectedCalendars);
results = await getCalendarsEvents(withCredentials, startDate, endDate, selectedCalendars, isOverlayUser);
} catch (e) {
log.warn(safeStringify(e));
}
Expand Down
5 changes: 4 additions & 1 deletion packages/core/getBusyTimes.ts
Expand Up @@ -39,6 +39,7 @@ export async function getBusyTimes(params: {
};
})[]
| null;
isOverlayUser?: boolean;
}) {
const {
credentials,
Expand All @@ -54,6 +55,7 @@ export async function getBusyTimes(params: {
seatedEvent,
rescheduleUid,
duration,
isOverlayUser,
} = params;

logger.silly(
Expand Down Expand Up @@ -198,7 +200,8 @@ export async function getBusyTimes(params: {
credentials,
startTime,
endTime,
selectedCalendars
selectedCalendars,
isOverlayUser
);
const endConnectedCalendarsGet = performance.now();
logger.debug(
Expand Down
15 changes: 11 additions & 4 deletions packages/core/getCalendarsEvents.ts
Expand Up @@ -5,16 +5,17 @@ import logger from "@calcom/lib/logger";
import { getPiiFreeCredential, getPiiFreeSelectedCalendar } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify";
import { performance } from "@calcom/lib/server/perfObserver";
import type { EventBusyDate } from "@calcom/types/Calendar";
import type { EventBusyData } from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";

const log = logger.getSubLogger({ prefix: ["getCalendarsEvents"] });
const getCalendarsEvents = async (
withCredentials: CredentialPayload[],
dateFrom: string,
dateTo: string,
selectedCalendars: SelectedCalendar[]
): Promise<EventBusyDate[][]> => {
selectedCalendars: SelectedCalendar[],
isOverlayUser?: boolean
): Promise<EventBusyData[][]> => {
const calendarCredentials = withCredentials
.filter((credential) => credential.type.endsWith("_calendar"))
// filter out invalid credentials - these won't work.
Expand Down Expand Up @@ -44,7 +45,13 @@ const getCalendarsEvents = async (
selectedCalendars: passedSelectedCalendars.map(getPiiFreeSelectedCalendar),
})
);
const eventBusyDates = await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars);
let eventBusyDates = [];
// we are getting event titles from only google calendar service for now
if (type === "google_calendar") {
eventBusyDates = await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars, isOverlayUser);
} else {
eventBusyDates = await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

We can simplify this and still pass isOverlayUser to all calendars. They won't have the logic to handle this anyways.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am now simply calling getEventList instead of getAvailability whenever we need to get event data with title.

performance.mark("eventBusyDatesEnd");
performance.measure(
`[getAvailability for ${selectedCalendarIds.join(", ")}][$1]'`,
Expand Down
4 changes: 3 additions & 1 deletion packages/core/getUserAvailability.ts
Expand Up @@ -182,7 +182,8 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
};
})[];
busyTimesFromLimitsBookings: EventBusyDetails[];
}
},
isOverlayUser?: boolean
) {
const {
username,
Expand Down Expand Up @@ -257,6 +258,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
rescheduleUid: initialData?.rescheduleUid || null,
duration,
currentBookings: initialData?.currentBookings,
isOverlayUser,
});

const detailedBusyTimes: EventBusyDetails[] = [
Expand Down
Expand Up @@ -67,7 +67,7 @@ export const LargeCalendar = ({
id,
start: dayjs(event.start).toDate(),
end: dayjs(event.end).toDate(),
title: "Busy",
title: event.title ?? "Busy",
options: {
status: "ACCEPTED",
},
Expand Down
Expand Up @@ -14,7 +14,7 @@ type OverlayCalendarProps = Pick<
> & {
handleClickNoCalendar: () => void;
hasSession: boolean;
handleClickContinue: () => void;
handleClickContinue: (provider: "calcom" | "google") => void;
handleSwitchStateChange: (state: boolean) => void;
};

Expand Down