Feature/parallel booking availability (#3087)
parent
7599f2384e
commit
2d28ca61a4
|
@ -4,7 +4,7 @@ import { useEffect } from "react";
|
|||
import { trpc } from "@lib/trpc";
|
||||
|
||||
export function useViewerI18n() {
|
||||
return trpc.useQuery(["viewer.i18n"], {
|
||||
return trpc.useQuery(["viewer.public.i18n"], {
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export default function SAMLLogin(props: Props) {
|
|||
const methods = useFormContext();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const mutation = trpc.useMutation("viewer.samlTenantProduct", {
|
||||
const mutation = trpc.useMutation("viewer.public.samlTenantProduct", {
|
||||
onSuccess: async (data) => {
|
||||
await signIn("saml", {}, { tenant: data.tenant, product: data.product });
|
||||
},
|
||||
|
|
|
@ -36,7 +36,7 @@ import { yyyymmdd } from "@calcom/lib/date-fns";
|
|||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
import DatePicker from "@calcom/ui/booker/DatePicker";
|
||||
import DatePicker, { Day } from "@calcom/ui/booker/DatePicker";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { timeZone } from "@lib/clock";
|
||||
|
@ -54,8 +54,6 @@ import { HeadSeo } from "@components/seo/head-seo";
|
|||
import AvatarGroup from "@components/ui/AvatarGroup";
|
||||
import PoweredByCal from "@components/ui/PoweredByCal";
|
||||
|
||||
import type { Slot } from "@server/routers/viewer/slots";
|
||||
|
||||
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
||||
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
||||
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
||||
|
@ -123,14 +121,18 @@ const useSlots = ({
|
|||
startTime: Date;
|
||||
endTime: Date;
|
||||
}) => {
|
||||
const { data, isLoading } = trpc.useQuery([
|
||||
"viewer.slots.getSchedule",
|
||||
{
|
||||
eventTypeId,
|
||||
startTime: startTime.toISOString(),
|
||||
endTime: endTime.toISOString(),
|
||||
},
|
||||
]);
|
||||
const { data, isLoading } = trpc.useQuery(
|
||||
[
|
||||
"viewer.public.slots.getSchedule",
|
||||
{
|
||||
eventTypeId,
|
||||
startTime: startTime.toISOString(),
|
||||
endTime: endTime.toISOString(),
|
||||
},
|
||||
],
|
||||
/** Prevents fetching past dates */
|
||||
{ enabled: dayjs(startTime).isAfter(dayjs().subtract(1, "day")) }
|
||||
);
|
||||
|
||||
return { slots: data?.slots || {}, isLoading };
|
||||
};
|
||||
|
@ -165,18 +167,10 @@ const SlotPicker = ({
|
|||
|
||||
const { slots, isLoading } = useSlots({
|
||||
eventTypeId: eventType.id,
|
||||
startTime: startDate,
|
||||
startTime: dayjs(startDate).startOf("day").toDate(),
|
||||
endTime: dayjs(startDate).endOf("month").toDate(),
|
||||
});
|
||||
|
||||
const [times, setTimes] = useState<Slot[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDate && slots[yyyymmdd(selectedDate)]) {
|
||||
setTimes(slots[yyyymmdd(selectedDate)]);
|
||||
}
|
||||
}, [selectedDate, slots]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DatePicker
|
||||
|
@ -187,19 +181,27 @@ const SlotPicker = ({
|
|||
? "sm:w-1/2 sm:border-r sm:pl-4 sm:pr-6 sm:dark:border-gray-700 md:w-1/3 "
|
||||
: "sm:pl-4")
|
||||
}
|
||||
locale={isLocaleReady ? i18n.language : "en"}
|
||||
includedDates={Object.keys(slots).filter((k) => slots[k].length > 0)}
|
||||
locale={isLocaleReady ? i18n.language : "en"}
|
||||
selected={selectedDate}
|
||||
onChange={setSelectedDate}
|
||||
onMonthChange={setStartDate}
|
||||
onMonthChange={(startDate) => {
|
||||
// set the minimum day to today in the current month, not the beginning of the month
|
||||
setStartDate(
|
||||
dayjs(startDate).isBefore(dayjs().subtract(1, "day"))
|
||||
? dayjs(new Date()).startOf("day").toDate()
|
||||
: startDate
|
||||
);
|
||||
}}
|
||||
weekStart={weekStart}
|
||||
// DayComponent={(props) => <DayContainer {...props} eventTypeId={eventType.id} />}
|
||||
/>
|
||||
|
||||
<div className="mt-4 ml-1 block sm:hidden">{timezoneDropdown}</div>
|
||||
|
||||
{selectedDate && (
|
||||
<AvailableTimes
|
||||
slots={times}
|
||||
slots={slots[yyyymmdd(selectedDate)]}
|
||||
date={dayjs(selectedDate)}
|
||||
timeFormat={timeFormat}
|
||||
eventTypeId={eventType.id}
|
||||
|
@ -261,6 +263,9 @@ const useDateSelected = ({ timeZone }: { timeZone?: string }) => {
|
|||
const [selectedDate, _setSelectedDate] = useState<Date>();
|
||||
|
||||
useEffect(() => {
|
||||
/** TODO: router.query.date is comming as `null` even when set like this:
|
||||
* `/user/type?date=2022-06-22-0600`
|
||||
*/
|
||||
const dateString = asStringOrNull(router.query.date);
|
||||
if (dateString) {
|
||||
const offsetString = dateString.substr(11, 14); // hhmm
|
||||
|
@ -275,6 +280,7 @@ const useDateSelected = ({ timeZone }: { timeZone?: string }) => {
|
|||
(offsetMinute !== "" ? parseInt(offsetMinute) : 0));
|
||||
|
||||
const date = dayjs(dateString.substr(0, 10)).utcOffset(utcOffsetInMinutes, true);
|
||||
console.log("date.isValid()", date.isValid());
|
||||
if (date.isValid()) {
|
||||
setSelectedDate(date.toDate());
|
||||
}
|
||||
|
@ -674,4 +680,29 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||
);
|
||||
};
|
||||
|
||||
const DayContainer = (props: React.ComponentProps<typeof Day> & { eventTypeId: number }) => {
|
||||
const { eventTypeId, ...rest } = props;
|
||||
/** :
|
||||
* Fetch each individual day here. All these are batched with tRPC anyways.
|
||||
**/
|
||||
const { slots } = useSlots({
|
||||
eventTypeId,
|
||||
startTime: dayjs(props.date).startOf("day").toDate(),
|
||||
endTime: dayjs(props.date).endOf("day").toDate(),
|
||||
});
|
||||
const includedDates = Object.keys(slots).filter((k) => slots[k].length > 0);
|
||||
const disabled = includedDates.length > 0 ? !includedDates.includes(yyyymmdd(props.date)) : props.disabled;
|
||||
return <Day {...{ ...rest, disabled }} />;
|
||||
};
|
||||
|
||||
const AvailableTimesContainer = (props: React.ComponentProps<typeof AvailableTimes>) => {
|
||||
const { date, eventTypeId } = props;
|
||||
const { slots } = useSlots({
|
||||
eventTypeId,
|
||||
startTime: dayjs(date).startOf("day").toDate(),
|
||||
endTime: dayjs(date).endOf("day").toDate(),
|
||||
});
|
||||
return <AvailableTimes {...props} slots={slots[date.format("YYYY-MM-DD")]} />;
|
||||
};
|
||||
|
||||
export default AvailabilityPage;
|
||||
|
|
|
@ -17,7 +17,7 @@ const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonati
|
|||
await utils.invalidateQueries(["viewer.me"]);
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.invalidateQueries(["viewer.i18n"]);
|
||||
await utils.invalidateQueries(["viewer.public.i18n"]);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -101,7 +101,13 @@ type TError = TRPCClientErrorLike<AppRouter>;
|
|||
|
||||
const withQuery = <TPath extends keyof TQueryValues & string>(
|
||||
pathAndInput: [path: TPath, ...args: inferHandlerInput<TQueries[TPath]>],
|
||||
params?: UseTRPCQueryOptions<TPath, TQueryValues[TPath]["input"], TQueryValues[TPath]["output"], TError>
|
||||
params?: UseTRPCQueryOptions<
|
||||
TPath,
|
||||
TQueryValues[TPath]["input"],
|
||||
TQueryValues[TPath]["output"],
|
||||
TQueryValues[TPath]["output"],
|
||||
TError
|
||||
>
|
||||
) => {
|
||||
return function WithQuery(
|
||||
opts: Omit<
|
||||
|
|
|
@ -26,7 +26,7 @@ type AppPropsWithChildren = AppProps & {
|
|||
};
|
||||
|
||||
const CustomI18nextProvider = (props: AppPropsWithChildren) => {
|
||||
const { i18n, locale } = trpc.useQuery(["viewer.i18n"]).data ?? {
|
||||
const { i18n, locale } = trpc.useQuery(["viewer.public.i18n"]).data ?? {
|
||||
locale: "en",
|
||||
};
|
||||
|
||||
|
@ -42,7 +42,7 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
|
|||
};
|
||||
|
||||
const AppProviders = (props: AppPropsWithChildren) => {
|
||||
const session = trpc.useQuery(["viewer.session"]).data;
|
||||
const session = trpc.useQuery(["viewer.public.session"]).data;
|
||||
// No need to have intercom on public pages - Good for Page Performance
|
||||
const isPublicPage = usePublicPage();
|
||||
const RemainingProviders = (
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { EventType, PeriodType } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import dayjsBusinessTime from "dayjs-business-days2";
|
||||
|
||||
dayjs.extend(dayjsBusinessTime);
|
||||
|
||||
function isOutOfBounds(
|
||||
time: dayjs.ConfigType,
|
||||
|
|
|
@ -58,10 +58,10 @@
|
|||
"@radix-ui/react-tooltip": "^0.1.0",
|
||||
"@stripe/react-stripe-js": "^1.8.0",
|
||||
"@stripe/stripe-js": "^1.29.0",
|
||||
"@trpc/client": "^9.23.4",
|
||||
"@trpc/next": "^9.23.4",
|
||||
"@trpc/react": "^9.23.4",
|
||||
"@trpc/server": "^9.23.4",
|
||||
"@trpc/client": "^9.25.2",
|
||||
"@trpc/next": "^9.25.2",
|
||||
"@trpc/react": "^9.25.2",
|
||||
"@trpc/server": "^9.25.2",
|
||||
"@vercel/edge-functions-ui": "^0.2.1",
|
||||
"@wojtekmaj/react-daterange-picker": "^3.3.1",
|
||||
"accept-language-parser": "^1.5.0",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { UserPlan } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import { GetStaticPropsContext } from "next";
|
||||
import { JSONObject } from "superjson/dist/types";
|
||||
import { z } from "zod";
|
||||
|
@ -54,7 +55,10 @@ export default function Type(props: AvailabilityPageProps) {
|
|||
);
|
||||
}
|
||||
|
||||
async function getUserPageProps({ username, slug }: { username: string; slug: string }) {
|
||||
async function getUserPageProps(context: GetStaticPropsContext) {
|
||||
const { type: slug, user: username } = paramsSchema.parse(context.params);
|
||||
const { ssgInit } = await import("@server/lib/ssg");
|
||||
const ssg = await ssgInit(context);
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
username,
|
||||
|
@ -150,6 +154,13 @@ async function getUserPageProps({ username, slug }: { username: string; slug: st
|
|||
|
||||
const profile = eventType.users[0] || user;
|
||||
|
||||
const startTime = new Date();
|
||||
await ssg.fetchQuery("viewer.public.slots.getSchedule", {
|
||||
eventTypeId: eventType.id,
|
||||
startTime: dayjs(startTime).startOf("day").toISOString(),
|
||||
endTime: dayjs(startTime).endOf("day").toISOString(),
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
eventType: eventTypeObject,
|
||||
|
@ -168,18 +179,18 @@ async function getUserPageProps({ username, slug }: { username: string; slug: st
|
|||
},
|
||||
away: user?.away,
|
||||
isDynamic: false,
|
||||
trpcState: ssg.dehydrate(),
|
||||
},
|
||||
revalidate: 10, // seconds
|
||||
};
|
||||
}
|
||||
|
||||
async function getDynamicGroupPageProps({
|
||||
usernameList,
|
||||
length,
|
||||
}: {
|
||||
usernameList: string[];
|
||||
length: number;
|
||||
}) {
|
||||
async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
||||
const { ssgInit } = await import("@server/lib/ssg");
|
||||
const ssg = await ssgInit(context);
|
||||
const { type: typeParam, user: userParam } = paramsSchema.parse(context.params);
|
||||
const usernameList = getUsernameList(userParam);
|
||||
const length = parseInt(typeParam);
|
||||
const eventType = getDefaultEvent("" + length);
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
|
@ -264,6 +275,7 @@ async function getDynamicGroupPageProps({
|
|||
profile,
|
||||
isDynamic: true,
|
||||
away: false,
|
||||
trpcState: ssg.dehydrate(),
|
||||
},
|
||||
revalidate: 10, // seconds
|
||||
};
|
||||
|
@ -272,17 +284,13 @@ async function getDynamicGroupPageProps({
|
|||
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
||||
|
||||
export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||
const { type: typeParam, user: userParam } = paramsSchema.parse(context.params);
|
||||
|
||||
const { user: userParam } = paramsSchema.parse(context.params);
|
||||
// dynamic groups are not generated at build time, but otherwise are probably cached until infinity.
|
||||
const isDynamicGroup = userParam.includes("+");
|
||||
if (isDynamicGroup) {
|
||||
return await getDynamicGroupPageProps({
|
||||
usernameList: getUsernameList(userParam),
|
||||
length: parseInt(typeParam),
|
||||
});
|
||||
return await getDynamicGroupPageProps(context);
|
||||
} else {
|
||||
return await getUserPageProps({ username: userParam, slug: typeParam });
|
||||
return await getUserPageProps(context);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -13,7 +13,9 @@ import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
|||
|
||||
import type { AppRouter } from "@server/routers/_app";
|
||||
import { httpBatchLink } from "@trpc/client/links/httpBatchLink";
|
||||
import { httpLink } from "@trpc/client/links/httpLink";
|
||||
import { loggerLink } from "@trpc/client/links/loggerLink";
|
||||
import { splitLink } from "@trpc/client/links/splitLink";
|
||||
import { withTRPC } from "@trpc/next";
|
||||
import type { TRPCClientErrorLike } from "@trpc/react";
|
||||
import { Maybe } from "@trpc/server";
|
||||
|
@ -56,6 +58,13 @@ function MyApp(props: AppProps) {
|
|||
|
||||
export default withTRPC<AppRouter>({
|
||||
config() {
|
||||
const url =
|
||||
typeof window !== "undefined"
|
||||
? "/api/trpc"
|
||||
: process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}/api/trpc`
|
||||
: `http://${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;
|
||||
|
||||
/**
|
||||
* If you want to use SSR, you need to use the server's full URL
|
||||
* @link https://trpc.io/docs/ssr
|
||||
|
@ -70,8 +79,17 @@ export default withTRPC<AppRouter>({
|
|||
enabled: (opts) =>
|
||||
!!process.env.NEXT_PUBLIC_DEBUG || (opts.direction === "down" && opts.result instanceof Error),
|
||||
}),
|
||||
httpBatchLink({
|
||||
url: `/api/trpc`,
|
||||
splitLink({
|
||||
// check for context property `skipBatch`
|
||||
condition: (op) => op.context.skipBatch === true,
|
||||
// when condition is true, use normal request
|
||||
true: httpLink({ url }),
|
||||
// when condition is false, use batching
|
||||
false: httpBatchLink({
|
||||
url,
|
||||
/** @link https://github.com/trpc/trpc/issues/2008 */
|
||||
// maxBatchSize: 7
|
||||
}),
|
||||
}),
|
||||
],
|
||||
/**
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import {
|
||||
BookingStatus,
|
||||
User,
|
||||
Booking,
|
||||
Attendee,
|
||||
Booking,
|
||||
BookingReference,
|
||||
BookingStatus,
|
||||
EventType,
|
||||
User,
|
||||
WebhookTriggerEvents,
|
||||
} from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
|
@ -13,7 +13,7 @@ import { getSession } from "next-auth/react";
|
|||
import type { TFunction } from "next-i18next";
|
||||
import { z, ZodError } from "zod";
|
||||
|
||||
import { getCalendar } from "@calcom/core/CalendarManager";
|
||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
|
||||
import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director";
|
||||
import { deleteMeeting } from "@calcom/core/videoClient";
|
||||
|
|
|
@ -3,8 +3,8 @@ import async from "async";
|
|||
import dayjs from "dayjs";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
|
||||
import { getCalendar } from "@calcom/core/CalendarManager";
|
||||
import { deleteMeeting } from "@calcom/core/videoClient";
|
||||
import { sendCancelledEmails } from "@calcom/emails";
|
||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||
|
|
|
@ -29,7 +29,23 @@ export default trpcNext.createNextApiHandler({
|
|||
/**
|
||||
* @link https://trpc.io/docs/caching#api-response-caching
|
||||
*/
|
||||
// responseMeta() {
|
||||
// // ...
|
||||
// },
|
||||
responseMeta({ ctx, paths, type, errors }) {
|
||||
// assuming we have all our public routes in `viewer.public`
|
||||
const allPublic = paths && paths.every((path) => path.startsWith("viewer.public."));
|
||||
// checking that no procedures errored
|
||||
const allOk = errors.length === 0;
|
||||
// checking we're doing a query request
|
||||
const isQuery = type === "query";
|
||||
|
||||
if (allPublic && allOk && isQuery) {
|
||||
// cache request for 1 day + revalidate once every 5 seconds
|
||||
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
|
||||
return {
|
||||
headers: {
|
||||
"cache-control": `s-maxage=5, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -84,7 +84,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
|||
document?.getElementsByTagName("main")[0]?.scrollTo({ top: 0, behavior: "smooth" });
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.invalidateQueries(["viewer.i18n"]);
|
||||
await utils.invalidateQueries(["viewer.public.i18n"]);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -481,7 +481,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
|
|||
);
|
||||
}
|
||||
|
||||
const WithQuery = withQuery(["viewer.i18n"]);
|
||||
const WithQuery = withQuery(["viewer.public.i18n"]);
|
||||
|
||||
export default function Settings(props: Props) {
|
||||
const { t } = useLocale();
|
||||
|
|
|
@ -37,7 +37,7 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
|
|||
});
|
||||
|
||||
// always preload i18n
|
||||
await ssg.fetchQuery("viewer.i18n");
|
||||
await ssg.fetchQuery("viewer.public.i18n");
|
||||
|
||||
return ssg;
|
||||
}
|
||||
|
|
|
@ -21,8 +21,8 @@ export async function ssrInit(context: GetServerSidePropsContext) {
|
|||
ctx,
|
||||
});
|
||||
|
||||
// always preload "viewer.i18n"
|
||||
await ssr.fetchQuery("viewer.i18n");
|
||||
// always preload "viewer.public.i18n"
|
||||
await ssr.fetchQuery("viewer.public.i18n");
|
||||
|
||||
return ssr;
|
||||
}
|
||||
|
|
|
@ -68,7 +68,8 @@ const publicViewerRouter = createRouter()
|
|||
|
||||
return await samlTenantProduct(prisma, email);
|
||||
},
|
||||
});
|
||||
})
|
||||
.merge("slots.", slotsRouter);
|
||||
|
||||
// routes only available to authenticated users
|
||||
const loggedInViewerRouter = createProtectedRouter()
|
||||
|
@ -944,12 +945,11 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
});
|
||||
|
||||
export const viewerRouter = createRouter()
|
||||
.merge(publicViewerRouter)
|
||||
.merge("public.", publicViewerRouter)
|
||||
.merge(loggedInViewerRouter)
|
||||
.merge("bookings.", bookingsRouter)
|
||||
.merge("eventTypes.", eventTypesRouter)
|
||||
.merge("availability.", availabilityRouter)
|
||||
.merge("teams.", viewerTeamsRouter)
|
||||
.merge("webhook.", webhookRouter)
|
||||
.merge("apiKeys.", apiKeysRouter)
|
||||
.merge("slots.", slotsRouter);
|
||||
.merge("apiKeys.", apiKeysRouter);
|
||||
|
|
|
@ -9,6 +9,7 @@ import { availabilityUserSelect } from "@calcom/prisma";
|
|||
import { stringToDayjs } from "@calcom/prisma/zod-utils";
|
||||
import { TimeRange, WorkingHours } from "@calcom/types/schedule";
|
||||
|
||||
import isOutOfBounds from "@lib/isOutOfBounds";
|
||||
import getSlots from "@lib/slots";
|
||||
|
||||
import { createRouter } from "@server/createRouter";
|
||||
|
@ -137,6 +138,11 @@ export const slotsRouter = createRouter().query("getSchedule", {
|
|||
beforeEventBuffer: true,
|
||||
afterEventBuffer: true,
|
||||
schedulingType: true,
|
||||
periodType: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
periodCountCalendarDays: true,
|
||||
periodDays: true,
|
||||
schedule: {
|
||||
select: {
|
||||
availability: true,
|
||||
|
@ -202,6 +208,14 @@ export const slotsRouter = createRouter().query("getSchedule", {
|
|||
afterBufferTime: eventType.afterEventBuffer,
|
||||
currentSeats,
|
||||
};
|
||||
const isWithinBounds = (_time: Parameters<typeof isOutOfBounds>[0]) =>
|
||||
!isOutOfBounds(_time, {
|
||||
periodType: eventType.periodType,
|
||||
periodStartDate: eventType.periodStartDate,
|
||||
periodEndDate: eventType.periodEndDate,
|
||||
periodCountCalendarDays: eventType.periodCountCalendarDays,
|
||||
periodDays: eventType.periodDays,
|
||||
});
|
||||
|
||||
let time = dayjs(startTime);
|
||||
do {
|
||||
|
@ -215,18 +229,17 @@ export const slotsRouter = createRouter().query("getSchedule", {
|
|||
});
|
||||
|
||||
// if ROUND_ROBIN - slots stay available on some() - if normal / COLLECTIVE - slots only stay available on every()
|
||||
const filteredTimes =
|
||||
const filterStrategy =
|
||||
!eventType.schedulingType || eventType.schedulingType === SchedulingType.COLLECTIVE
|
||||
? times.filter((time) =>
|
||||
userSchedules.every((schedule) =>
|
||||
checkForAvailability({ time, ...schedule, ...availabilityCheckProps })
|
||||
)
|
||||
)
|
||||
: times.filter((time) =>
|
||||
userSchedules.some((schedule) =>
|
||||
checkForAvailability({ time, ...schedule, ...availabilityCheckProps })
|
||||
)
|
||||
);
|
||||
? ("every" as const)
|
||||
: ("some" as const);
|
||||
const filteredTimes = times
|
||||
.filter(isWithinBounds)
|
||||
.filter((time) =>
|
||||
userSchedules[filterStrategy]((schedule) =>
|
||||
checkForAvailability({ time, ...schedule, ...availabilityCheckProps })
|
||||
)
|
||||
);
|
||||
|
||||
slots[yyyymmdd(time.toDate())] = filteredTimes.map((time) => ({
|
||||
time: time.toISOString(),
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
import { Credential, SelectedCalendar } from "@prisma/client";
|
||||
import { createHash } from "crypto";
|
||||
import _ from "lodash";
|
||||
import cache from "memory-cache";
|
||||
|
||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||
import getApps from "@calcom/app-store/utils";
|
||||
import { getUid } from "@calcom/lib/CalEventParser";
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import type { CalendarEvent, EventBusyDate, NewCalendarEventType } from "@calcom/types/Calendar";
|
||||
import type { Event } from "@calcom/types/Event";
|
||||
import type { EventResult } from "@calcom/types/EventManager";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["CalendarManager"] });
|
||||
|
||||
/** TODO: Remove once all references are updated to app-store */
|
||||
export { getCalendar };
|
||||
|
||||
export const getCalendarCredentials = (credentials: Array<Credential>, userId: number) => {
|
||||
const calendarCredentials = getApps(credentials)
|
||||
.filter((app) => app.type.endsWith("_calendar"))
|
||||
|
@ -77,20 +74,51 @@ export const getConnectedCalendars = async (
|
|||
return connectedCalendars;
|
||||
};
|
||||
|
||||
const CACHING_TIME = 30_000; // 30 seconds
|
||||
|
||||
const getCachedResults = (
|
||||
withCredentials: Credential[],
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: SelectedCalendar[]
|
||||
) => {
|
||||
const calendarCredentials = withCredentials.filter((credential) => credential.type.endsWith("_calendar"));
|
||||
const calendars = calendarCredentials.map((credential) => getCalendar(credential));
|
||||
const results = calendars.map(async (c, i) => {
|
||||
/** Filter out nulls */
|
||||
if (!c) return [];
|
||||
/** We rely on the index so we can match credentials with calendars */
|
||||
const { id, type } = calendarCredentials[i];
|
||||
/** We just pass the calendars that matched the credential type,
|
||||
* TODO: Migrate credential type or appId
|
||||
*/
|
||||
const passedSelectedCalendars = selectedCalendars.filter((sc) => sc.integration === type);
|
||||
/** We extract external Ids so we don't cache too much */
|
||||
const selectedCalendarIds = passedSelectedCalendars.map((sc) => sc.externalId);
|
||||
/** We create a unque hash key based on the input data */
|
||||
const cacheKey = createHash("md5").update(JSON.stringify({ id, selectedCalendarIds })).digest("hex");
|
||||
/** Check if we already have cached data and return */
|
||||
const cachedAvailability = cache.get(cacheKey);
|
||||
if (cachedAvailability) return cachedAvailability;
|
||||
/** If we don't then we actually fetch external calendars (which can be very slow) */
|
||||
const availability = await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars);
|
||||
/** We save the availability to a few seconds so recurrent calls are nearly instant */
|
||||
cache.put(cacheKey, availability, CACHING_TIME);
|
||||
return availability;
|
||||
});
|
||||
|
||||
return Promise.all(results);
|
||||
};
|
||||
|
||||
export const getBusyCalendarTimes = async (
|
||||
withCredentials: Credential[],
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: SelectedCalendar[]
|
||||
) => {
|
||||
const calendars = withCredentials
|
||||
.filter((credential) => credential.type.endsWith("_calendar"))
|
||||
.map((credential) => getCalendar(credential))
|
||||
.filter(notEmpty);
|
||||
|
||||
let results: EventBusyDate[][] = [];
|
||||
try {
|
||||
results = await Promise.all(calendars.map((c) => c.getAvailability(dateFrom, dateTo, selectedCalendars)));
|
||||
results = await getCachedResults(withCredentials, dateFrom, dateTo, selectedCalendars);
|
||||
} catch (error) {
|
||||
log.warn(error);
|
||||
}
|
||||
|
|
|
@ -34,19 +34,15 @@ export type DatePickerProps = {
|
|||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
const Day = ({
|
||||
export const Day = ({
|
||||
date,
|
||||
active,
|
||||
...props
|
||||
}: JSX.IntrinsicElements["button"] & { active: boolean; date: Date }) => {
|
||||
return (
|
||||
<button
|
||||
style={props.disabled ? {} : {}}
|
||||
className={classNames(
|
||||
"absolute top-0 left-0 right-0 bottom-0 mx-auto w-full rounded-sm border border-transparent text-center",
|
||||
props.disabled
|
||||
? "text-bookinglighter cursor-default font-light"
|
||||
: "hover:border-brand font-medium dark:hover:border-white",
|
||||
"hover:border-brand disabled:text-bookinglighter absolute top-0 left-0 right-0 bottom-0 mx-auto w-full rounded-sm border border-transparent text-center font-medium disabled:cursor-default disabled:border-transparent disabled:font-light dark:hover:border-white disabled:dark:border-transparent",
|
||||
active
|
||||
? "bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast"
|
||||
: !props.disabled
|
||||
|
@ -68,9 +64,11 @@ const Days = ({
|
|||
includedDates = [],
|
||||
browsingDate,
|
||||
weekStart,
|
||||
DayComponent = Day,
|
||||
selected,
|
||||
...props
|
||||
}: Omit<DatePickerProps, "locale" | "className" | "weekStart"> & {
|
||||
DayComponent?: React.FC<React.ComponentProps<typeof Day>>;
|
||||
browsingDate: Date;
|
||||
weekStart: number;
|
||||
}) => {
|
||||
|
@ -90,16 +88,11 @@ const Days = ({
|
|||
return (
|
||||
<>
|
||||
{days.map((day, idx) => (
|
||||
<div
|
||||
key={day === null ? `e-${idx}` : `day-${day}`}
|
||||
style={{
|
||||
paddingTop: "100%",
|
||||
}}
|
||||
className="relative w-full">
|
||||
<div key={day === null ? `e-${idx}` : `day-${day}`} className="relative w-full pt-[100%]">
|
||||
{day === null ? (
|
||||
<div key={`e-${idx}`} />
|
||||
) : (
|
||||
<Day
|
||||
<DayComponent
|
||||
date={day}
|
||||
onClick={() => props.onChange(day)}
|
||||
disabled={
|
||||
|
@ -139,7 +132,7 @@ const DatePicker = ({
|
|||
onMonthChange,
|
||||
isLoading = false,
|
||||
...passThroughProps
|
||||
}: DatePickerProps) => {
|
||||
}: DatePickerProps & Partial<React.ComponentProps<typeof Days>>) => {
|
||||
const [month, setMonth] = useState(selected ? selected.getMonth() : new Date().getMonth());
|
||||
|
||||
const changeMonth = (newMonth: number) => {
|
||||
|
|
41
yarn.lock
41
yarn.lock
|
@ -3140,32 +3140,32 @@
|
|||
javascript-natural-sort "0.7.1"
|
||||
lodash "4.17.21"
|
||||
|
||||
"@trpc/client@^9.23.4":
|
||||
version "9.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@trpc/client/-/client-9.23.4.tgz#d887d013ca9146299df1a33a1f13d57366a3483f"
|
||||
integrity sha512-usZgbydmqqzUqJQD5yY9f+KftYBKz6ed0J90MYETqRwXl9XtR9kKOwRRMBQwZDdeMhai3Hlk4sDWeC9HR3auRQ==
|
||||
"@trpc/client@^9.25.2":
|
||||
version "9.25.2"
|
||||
resolved "https://registry.yarnpkg.com/@trpc/client/-/client-9.25.2.tgz#3ece63bc58436a12a49a7ef3aa3ff54f03bdd750"
|
||||
integrity sha512-wW4KEvG+mF5ParqmyTgSjxwOjvxtsQCso6nZeZ3PI19d9bBUprFx9E+UkS5R3gqNm9lddQx7tx+ie+OFkDmp3g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.0"
|
||||
|
||||
"@trpc/next@^9.23.4":
|
||||
version "9.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@trpc/next/-/next-9.23.4.tgz#45c4da34cd99b882aefa0ddc065dd23c5d5467a2"
|
||||
integrity sha512-fPFU/PNkGawiUS56wRx3KsnRh/cvI3RtGYbZKIxLr38oYyHLct/v1uqA+0xJIoyPlcFDLoB6et4MZJuv3/v8Dg==
|
||||
"@trpc/next@^9.25.2":
|
||||
version "9.25.2"
|
||||
resolved "https://registry.yarnpkg.com/@trpc/next/-/next-9.25.2.tgz#721182d1426e28848c87ef473dff781e9c812ab4"
|
||||
integrity sha512-OiRcmNd5sxTR23O2oVxBVoSj4DNVZe84GzXBKxShYjeDyld0OKmQzqB1H2rMsCmNU7/Yd4lYDQBjGnDVP5VxPQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.0"
|
||||
react-ssr-prepass "^1.5.0"
|
||||
|
||||
"@trpc/react@^9.23.4":
|
||||
version "9.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@trpc/react/-/react-9.23.4.tgz#6abb2bbabd76d72cc1a3fb72f6ecc33702cb1c3d"
|
||||
integrity sha512-adori41F2hgjijThOmoeMkIXcaRjScZi6lczRwIIy6UqoUjYb+FfO46Jr7cP/QSX1Ur8VatD6UgLCNViURTfCQ==
|
||||
"@trpc/react@^9.25.2":
|
||||
version "9.25.2"
|
||||
resolved "https://registry.yarnpkg.com/@trpc/react/-/react-9.25.2.tgz#93c9c9c46bfee16ede9b326c2c964a38b4f8b84d"
|
||||
integrity sha512-osP1JmhhBLWmSkoCc19YcGiQdeEq0RtvO0uvLe2DfzGpQL1orB72nvqWHXlIGHWA4E0cnc1FRmoXokWYCwHPxQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.0"
|
||||
|
||||
"@trpc/server@^9.23.4":
|
||||
version "9.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@trpc/server/-/server-9.23.4.tgz#78ecebbdda79db6252067d66c4a0edef025a501b"
|
||||
integrity sha512-nlOgft5g4BziNplDHhw7f4m9+k8lRPFtVVi8VGxOINt7sQ8pzzEti0WMIl2BX6gugw92t+kae2O5e793AFJs9g==
|
||||
"@trpc/server@^9.25.2":
|
||||
version "9.25.2"
|
||||
resolved "https://registry.yarnpkg.com/@trpc/server/-/server-9.25.2.tgz#3ac58753b4cf7b2aa1a2a51d94bd0798b495a78a"
|
||||
integrity sha512-E5ibK5jLgWremiPs2pO+Y/YktRH7+CqmMwp97mTp9ymYZn3od4C9TuFg6bxEK1bQKnUezpzHJyGRADVKCWrjsw==
|
||||
|
||||
"@tryvital/vital-node@^1.3.6":
|
||||
version "1.3.6"
|
||||
|
@ -3560,11 +3560,16 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
|
||||
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
|
||||
|
||||
"@types/node@*", "@types/node@16.9.1", "@types/node@>=12.0.0", "@types/node@>=8.1.0", "@types/node@^12.12.6":
|
||||
"@types/node@*", "@types/node@16.9.1", "@types/node@>=12.0.0", "@types/node@>=8.1.0":
|
||||
version "16.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"
|
||||
integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==
|
||||
|
||||
"@types/node@^12.12.6":
|
||||
version "12.20.55"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240"
|
||||
integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==
|
||||
|
||||
"@types/nodemailer@^6.4.4":
|
||||
version "6.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b"
|
||||
|
@ -17625,4 +17630,4 @@ zwitch@^1.0.0:
|
|||
zwitch@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.2.tgz#91f8d0e901ffa3d66599756dde7f57b17c95dce1"
|
||||
integrity sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA==
|
||||
integrity sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA==
|
||||
|
|
Loading…
Reference in New Issue