Add calendar source to busy blocks (#3074)

* Add calendar source to busy blocks

* Update troubleshoot.tsx

* Type fixes

* Adds sources to availablity

# Conflicts:
#	apps/web/pages/availability/troubleshoot.tsx

* Update troubleshoot v1 and v2 with source element

* Type fixes

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Alan <alannnc@gmail.com>
pull/4261/head^2
Omar López 2022-09-07 13:28:43 -06:00 committed by GitHub
parent 7da272f67a
commit 34408c5593
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 98 additions and 80 deletions

View File

@ -2,6 +2,7 @@
* @deprecated modifications to this file should be v2 only
* Use `/apps/web/pages/v2/availability/troubleshoot.tsx` instead
*/
import type { IBusySlot } from "pages/v2/availability/troubleshoot";
import { useState } from "react";
import dayjs from "@calcom/dayjs";
@ -30,6 +31,7 @@ const AvailabilityView = ({ user }: { user: User }) => {
username: user.username!,
dateFrom: selectedDate.startOf("day").utc().format(),
dateTo: selectedDate.endOf("day").utc().format(),
withSource: true,
},
],
{
@ -59,23 +61,28 @@ const AvailabilityView = ({ user }: { user: User }) => {
{isLoading ? (
<Loader />
) : data && data.busy.length > 0 ? (
data.busy.map((slot) => (
<div key={slot.start} className="overflow-hidden rounded-sm bg-neutral-100">
<div className="px-4 py-5 text-black sm:p-6">
{t("calendar_shows_busy_between")}{" "}
<span className="font-medium text-neutral-800" title={slot.start}>
{dayjs(slot.start).format("HH:mm")}
</span>{" "}
{t("and")}{" "}
<span className="font-medium text-neutral-800" title={slot.end}>
{dayjs(slot.end).format("HH:mm")}
</span>{" "}
{t("on")} {dayjs(slot.start).format("D")}{" "}
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
{slot.title && ` - (${slot.title})`}
data.busy
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
.map((slot: IBusySlot) => (
<div
key={`${slot.start}-${slot.title ?? "untitled"}`}
className="overflow-hidden rounded-sm bg-neutral-100">
<div className="px-4 py-5 text-black sm:p-6">
{t("calendar_shows_busy_between")}{" "}
<span className="font-medium text-neutral-800" title={slot.start}>
{dayjs(slot.start).format("HH:mm")}
</span>{" "}
{t("and")}{" "}
<span className="font-medium text-neutral-800" title={slot.end}>
{dayjs(slot.end).format("HH:mm")}
</span>{" "}
{t("on")} {dayjs(slot.start).format("D")}{" "}
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
{slot.title && ` - (${slot.title})`}
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
</div>
</div>
</div>
))
))
) : (
<div className="overflow-hidden rounded-sm bg-neutral-100">
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useState } from "react";
import dayjs, { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import Shell from "@calcom/ui/Shell";
@ -11,42 +11,31 @@ import Loader from "@components/Loader";
type User = inferQueryOutput<"viewer.me">;
export interface IBusySlot {
start: string;
end: string;
title?: string;
source?: string | null;
}
const AvailabilityView = ({ user }: { user: User }) => {
const { t } = useLocale();
const [loading, setLoading] = useState(true);
const [availability, setAvailability] = useState<{ end: string; start: string; title?: string }[]>([]);
const [selectedDate, setSelectedDate] = useState(dayjs());
function convertMinsToHrsMins(mins: number) {
const h = Math.floor(mins / 60);
const m = mins % 60;
const hs = h < 10 ? "0" + h : h;
const ms = m < 10 ? "0" + m : m;
return `${hs}:${ms}`;
}
useEffect(() => {
const fetchAvailability = (date: Dayjs) => {
const dateFrom = date.startOf("day").utc().format();
const dateTo = date.endOf("day").utc().format();
setLoading(true);
fetch(`/api/availability/${user.username}?dateFrom=${dateFrom}&dateTo=${dateTo}`)
.then((res) => {
return res.json();
})
.then((availableIntervals) => {
setAvailability(availableIntervals.busy);
})
.catch((e) => {
console.error(e);
})
.finally(() => {
setLoading(false);
});
};
fetchAvailability(selectedDate);
}, [user.username, selectedDate]);
const { data, isLoading } = trpc.useQuery(
[
"viewer.availability.user",
{
username: user.username!,
dateFrom: selectedDate.startOf("day").utc().format(),
dateTo: selectedDate.endOf("day").utc().format(),
withSource: true,
},
],
{
enabled: !!user.username,
}
);
return (
<div className="max-w-xl overflow-hidden rounded-md bg-white shadow">
@ -67,26 +56,29 @@ const AvailabilityView = ({ user }: { user: User }) => {
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
</div>
</div>
{loading ? (
{isLoading ? (
<Loader />
) : availability.length > 0 ? (
availability.map((slot) => (
<div key={slot.start} className="overflow-hidden rounded-md bg-neutral-100">
<div className="px-4 py-5 text-black sm:p-6">
{t("calendar_shows_busy_between")}{" "}
<span className="font-medium text-neutral-800" title={slot.start}>
{dayjs(slot.start).format("HH:mm")}
</span>{" "}
{t("and")}{" "}
<span className="font-medium text-neutral-800" title={slot.end}>
{dayjs(slot.end).format("HH:mm")}
</span>{" "}
{t("on")} {dayjs(slot.start).format("D")}{" "}
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
{slot.title && ` - (${slot.title})`}
) : data && data.busy.length > 0 ? (
data.busy
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
.map((slot: IBusySlot) => (
<div key={slot.start} className="overflow-hidden rounded-md bg-neutral-100">
<div className="px-4 py-5 text-black sm:p-6">
{t("calendar_shows_busy_between")}{" "}
<span className="font-medium text-neutral-800" title={slot.start}>
{dayjs(slot.start).format("HH:mm")}
</span>{" "}
{t("and")}{" "}
<span className="font-medium text-neutral-800" title={slot.end}>
{dayjs(slot.end).format("HH:mm")}
</span>{" "}
{t("on")} {dayjs(slot.start).format("D")}{" "}
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
{slot.title && ` - (${slot.title})`}
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
</div>
</div>
</div>
))
))
) : (
<div className="overflow-hidden rounded-md bg-neutral-100">
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
@ -115,3 +107,11 @@ export default function Troubleshoot() {
</div>
);
}
function convertMinsToHrsMins(mins: number) {
const h = Math.floor(mins / 60);
const m = mins % 60;
const hs = h < 10 ? "0" + h : h;
const ms = m < 10 ? "0" + m : m;
return `${hs}:${ms}`;
}

View File

@ -94,7 +94,7 @@ const getCachedResults = async (
/** Filter out nulls */
if (!c) return [];
/** We rely on the index so we can match credentials with calendars */
const { id, type } = calendarCredentials[i];
const { id, type, appId } = calendarCredentials[i];
/** We just pass the calendars that matched the credential type,
* TODO: Migrate credential type or appId
*/
@ -113,7 +113,10 @@ const getCachedResults = async (
}
log.debug(`Cache MISS: Calendar Availability for key ${cacheKey}`);
/** If we don't then we actually fetch external calendars (which can be very slow) */
const availability = await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars);
const availability = (await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars)).map((a) => ({
...a,
source: `${appId}`,
}));
/** We save the availability to a few seconds so recurrent calls are nearly instant */
cache.put(cacheHashedKey, availability, CACHING_TIME);
@ -142,7 +145,6 @@ export const getBusyCalendarTimes = async (
} catch (error) {
log.warn(error);
}
return results.reduce((acc, availability) => acc.concat(availability), []);
};

View File

@ -42,7 +42,12 @@ export async function getBusyTimes(params: {
},
})
.then((bookings) =>
bookings.map(({ startTime, endTime, title }) => ({ end: endTime, start: startTime, title }))
bookings.map(({ startTime, endTime, title, id }) => ({
end: endTime,
start: startTime,
title,
source: `eventType-${eventTypeId}-booking-${id}`,
}))
);
logger.silly(`Busy Time from Cal Bookings ${JSON.stringify(busyTimes)}`);
const endPrismaBookingGet = performance.now();

View File

@ -19,6 +19,7 @@ const availabilitySchema = z
username: z.string().optional(),
userId: z.number().optional(),
afterEventBuffer: z.number().optional(),
withSource: z.boolean().optional(),
})
.refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in.");
@ -80,6 +81,7 @@ export type CurrentSeats = Awaited<ReturnType<typeof getCurrentSeats>>;
/** This should be called getUsersWorkingHoursAndBusySlots (...and remaining seats, and final timezone) */
export async function getUserAvailability(
query: {
withSource?: boolean;
username?: string;
userId?: number;
dateFrom: string;
@ -128,11 +130,13 @@ export async function getUserAvailability(
});
const bufferedBusyTimes = busyTimes.map((a) => ({
...a,
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toISOString(),
end: dayjs(a.end)
.add(currentUser.bufferTime + (afterEventBuffer || 0), "minute")
.toISOString(),
title: a.title,
source: query.withSource ? a.source : undefined,
}));
const schedule = eventType?.schedule

View File

@ -82,15 +82,10 @@ export const availabilityRouter = createProtectedRouter()
dateFrom: z.string(),
dateTo: z.string(),
eventTypeId: stringOrNumber.optional(),
withSource: z.boolean().optional(),
}),
async resolve({ input }) {
const { username, eventTypeId, dateTo, dateFrom } = input;
return getUserAvailability({
username,
dateFrom,
dateTo,
eventTypeId,
});
return getUserAvailability(input);
},
})
.mutation("schedule.create", {

View File

@ -25,10 +25,15 @@ export type Person = {
id?: string;
};
export type EventBusyDate = Record<"start" | "end", Date | string>;
export type EventBusyDate = {
start: Date | string;
end: Date | string;
source?: string | null;
};
export type EventBusyDetails = EventBusyDate & {
title?: string;
source?: string | null;
};
export type CalendarServiceType = typeof Calendar;