diff --git a/apps/web/components/eventtype/AvailabilityTab.tsx b/apps/web/components/eventtype/AvailabilityTab.tsx index b06c084d97..52dcd3c121 100644 --- a/apps/web/components/eventtype/AvailabilityTab.tsx +++ b/apps/web/components/eventtype/AvailabilityTab.tsx @@ -113,7 +113,7 @@ const EventTypeScheduleDetails = () => { ); const filterDays = (dayNum: number) => - schedule?.schedule.availability.filter((item) => item.days.includes((dayNum + 1) % 7)) || []; + schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || []; return (
@@ -156,15 +156,17 @@ const EventTypeScheduleDetails = () => { {schedule?.timeZone || } - + {!!schedule?.id && ( + + )}
); diff --git a/apps/web/package.json b/apps/web/package.json index 59c0ac169b..601c3c544f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -104,7 +104,7 @@ "react-dom": "^18.2.0", "react-easy-crop": "^3.5.2", "react-feather": "^2.0.10", - "react-hook-form": "^7.34.2", + "react-hook-form": "^7.43.3", "react-hot-toast": "^2.3.0", "react-intl": "^5.25.1", "react-live-chat-loader": "^2.7.3", diff --git a/apps/web/pages/availability/[schedule].tsx b/apps/web/pages/availability/[schedule].tsx index a410bbd102..3387bd48b8 100644 --- a/apps/web/pages/availability/[schedule].tsx +++ b/apps/web/pages/availability/[schedule].tsx @@ -1,4 +1,3 @@ -import type { GetServerSidePropsContext } from "next"; import { useRouter } from "next/router"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import { z } from "zod"; @@ -9,7 +8,7 @@ import Shell from "@calcom/features/shell/Shell"; import { availabilityAsString } from "@calcom/lib/availability"; import { yyyymmdd } from "@calcom/lib/date-fns"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { stringOrNumber } from "@calcom/prisma/zod-utils"; +import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { trpc } from "@calcom/trpc/react"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/types/schedule"; @@ -35,10 +34,8 @@ import { HttpError } from "@lib/core/http/error"; import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader"; import EditableHeading from "@components/ui/EditableHeading"; -import { ssrInit } from "@server/lib/ssr"; - const querySchema = z.object({ - schedule: stringOrNumber, + schedule: z.coerce.number().positive().optional(), }); type AvailabilityFormValues = { @@ -88,16 +85,29 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => { ); }; -export default function Availability({ schedule }: { schedule: number }) { +export default function Availability() { const { t, i18n } = useLocale(); const router = useRouter(); const utils = trpc.useContext(); const me = useMeQuery(); + const { + data: { schedule: scheduleId }, + } = useTypedQuery(querySchema); + const { timeFormat } = me.data || { timeFormat: null }; - const { data, isLoading } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId: schedule }); - const { data: defaultValues } = trpc.viewer.availability.defaultValues.useQuery({ scheduleId: schedule }); - const form = useForm({ defaultValues }); - const { control } = form; + const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery( + { scheduleId }, + { + enabled: !!scheduleId, + } + ); + + const form = useForm({ + values: schedule && { + ...schedule, + schedule: schedule?.availability || [], + }, + }); const updateMutation = trpc.viewer.availability.schedule.update.useMutation({ onSuccess: async ({ prevDefaultId, currentDefaultId, ...data }) => { if (prevDefaultId && currentDefaultId) { @@ -144,7 +154,7 @@ export default function Availability({ schedule }: { schedule: number }) { return ( } subtitle={ - data ? ( - data.schedule.availability + schedule ? ( + schedule.schedule .filter((availability) => !!availability.days.length) .map((availability) => ( @@ -179,7 +189,7 @@ export default function Availability({ schedule }: { schedule: number }) { { form.setValue("isDefault", e); @@ -199,7 +209,7 @@ export default function Availability({ schedule }: { schedule: number }) { confirmBtnText={t("delete")} loadingText={t("delete")} onConfirm={() => { - deleteMutation.mutate({ scheduleId: schedule }); + scheduleId && deleteMutation.mutate({ scheduleId }); }}> {t("delete_schedule_description")} @@ -218,11 +228,12 @@ export default function Availability({ schedule }: { schedule: number }) { form={form} id="availability-form" handleSubmit={async ({ dateOverrides, ...values }) => { - updateMutation.mutate({ - scheduleId: schedule, - dateOverrides: dateOverrides.flatMap((override) => override.ranges), - ...values, - }); + scheduleId && + updateMutation.mutate({ + scheduleId, + dateOverrides: dateOverrides.flatMap((override) => override.ranges), + ...values, + }); }} className="flex flex-col sm:mx-0 xl:flex-row xl:space-x-6">
@@ -230,7 +241,7 @@ export default function Availability({ schedule }: { schedule: number }) {
{typeof me.data?.weekStart === "string" && (
- {data?.workingHours && } + {schedule?.workingHours && }
@@ -282,20 +293,3 @@ export default function Availability({ schedule }: { schedule: number }) { ); } - -export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { - const params = querySchema.safeParse(ctx.params); - const ssr = await ssrInit(ctx); - - if (!params.success) return { notFound: true }; - - const scheduleId = params.data.schedule; - await ssr.viewer.availability.schedule.get.fetch({ scheduleId }); - await ssr.viewer.availability.defaultValues.fetch({ scheduleId }); - return { - props: { - schedule: scheduleId, - trpcState: ssr.dehydrate(), - }, - }; -}; diff --git a/packages/app-store/applecalendar/package.json b/packages/app-store/applecalendar/package.json index fe5b883c50..8809bf7cb8 100644 --- a/packages/app-store/applecalendar/package.json +++ b/packages/app-store/applecalendar/package.json @@ -7,7 +7,7 @@ "description": "Apple calendar runs both the macOS and iOS mobile operating systems. Offering online cloud backup of calendars using Apple’s iCloud service, it can sync with Google Calendar and Microsoft Exchange Server. Users can schedule events in their day that include time, location, duration, and extra notes.", "dependencies": { "@calcom/prisma": "*", - "react-hook-form": "^7.34.2" + "react-hook-form": "^7.43.3" }, "devDependencies": { "@calcom/types": "*" diff --git a/packages/app-store/caldavcalendar/package.json b/packages/app-store/caldavcalendar/package.json index 36b2fabd81..01a9a3ba35 100644 --- a/packages/app-store/caldavcalendar/package.json +++ b/packages/app-store/caldavcalendar/package.json @@ -10,7 +10,7 @@ "@calcom/lib": "*", "@calcom/prisma": "*", "@calcom/ui": "*", - "react-hook-form": "^7.34.2" + "react-hook-form": "^7.43.3" }, "devDependencies": { "@calcom/types": "*" diff --git a/packages/app-store/exchange2013calendar/package.json b/packages/app-store/exchange2013calendar/package.json index ecb5e23752..216eba4ca4 100644 --- a/packages/app-store/exchange2013calendar/package.json +++ b/packages/app-store/exchange2013calendar/package.json @@ -9,7 +9,7 @@ "@calcom/lib": "*", "@calcom/prisma": "*", "@calcom/ui": "*", - "react-hook-form": "^7.34.2", + "react-hook-form": "^7.43.3", "ews-javascript-api": "^0.11.0" }, "devDependencies": { diff --git a/packages/app-store/exchange2016calendar/package.json b/packages/app-store/exchange2016calendar/package.json index 982317eda1..480fa52b8c 100644 --- a/packages/app-store/exchange2016calendar/package.json +++ b/packages/app-store/exchange2016calendar/package.json @@ -9,7 +9,7 @@ "@calcom/lib": "*", "@calcom/prisma": "*", "@calcom/ui": "*", - "react-hook-form": "^7.34.2", + "react-hook-form": "^7.43.3", "ews-javascript-api": "^0.11.0" }, "devDependencies": { diff --git a/packages/features/schedules/components/DateOverrideInputDialog.tsx b/packages/features/schedules/components/DateOverrideInputDialog.tsx index 9695398b4b..a54a4092ba 100644 --- a/packages/features/schedules/components/DateOverrideInputDialog.tsx +++ b/packages/features/schedules/components/DateOverrideInputDialog.tsx @@ -1,12 +1,13 @@ -import { useState, useEffect, useMemo } from "react"; +import { useState, useMemo } from "react"; import { useForm } from "react-hook-form"; -import dayjs, { Dayjs } from "@calcom/dayjs"; +import type { Dayjs } from "@calcom/dayjs"; +import dayjs from "@calcom/dayjs"; import { classNames } from "@calcom/lib"; import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; -import { WorkingHours } from "@calcom/types/schedule"; +import type { WorkingHours } from "@calcom/types/schedule"; import { Dialog, DialogContent, @@ -19,12 +20,8 @@ import { } from "@calcom/ui"; import DatePicker from "../../calendars/DatePicker"; -import { DayRanges, TimeRange } from "./Schedule"; - -const ALL_DAY_RANGE = { - start: new Date(dayjs.utc().hour(0).minute(0).second(0).format()), - end: new Date(dayjs.utc().hour(0).minute(0).second(0).format()), -}; +import type { TimeRange } from "./Schedule"; +import { DayRanges } from "./Schedule"; // eslint-disable-next-line @typescript-eslint/no-empty-function const noop = () => {}; @@ -70,37 +67,33 @@ const DateOverrideForm = ({ [browsingDate] ); - const form = useForm<{ range: TimeRange[] }>(); - const { reset } = form; - - useEffect(() => { - if (value) { - reset({ - range: value.map((range) => ({ - start: new Date( - dayjs.utc().hour(range.start.getUTCHours()).minute(range.start.getUTCMinutes()).second(0).format() - ), - end: new Date( - dayjs.utc().hour(range.end.getUTCHours()).minute(range.end.getUTCMinutes()).second(0).format() - ), - })), - }); - return; - } - const dayRanges = (workingHours || []).reduce((dayRanges, workingHour) => { - if (date && workingHour.days.includes(date.day())) { - dayRanges.push({ - start: dayjs.utc().startOf("day").add(workingHour.startTime, "minute").toDate(), - end: dayjs.utc().startOf("day").add(workingHour.endTime, "minute").toDate(), - }); - } - return dayRanges; - }, [] as TimeRange[]); - reset({ - range: dayRanges, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [date, value]); + const form = useForm({ + values: { + range: value + ? value.map((range) => ({ + start: new Date( + dayjs + .utc() + .hour(range.start.getUTCHours()) + .minute(range.start.getUTCMinutes()) + .second(0) + .format() + ), + end: new Date( + dayjs.utc().hour(range.end.getUTCHours()).minute(range.end.getUTCMinutes()).second(0).format() + ), + })) + : (workingHours || []).reduce((dayRanges, workingHour) => { + if (date && workingHour.days.includes(date.day())) { + dayRanges.push({ + start: dayjs.utc().startOf("day").add(workingHour.startTime, "minute").toDate(), + end: dayjs.utc().startOf("day").add(workingHour.endTime, "minute").toDate(), + }); + } + return dayRanges; + }, [] as TimeRange[]), + }, + }); return (
{ if (!date) return; onChange( - (datesUnavailable ? [ALL_DAY_RANGE] : values.range).map((item) => ({ + (datesUnavailable + ? [ + { + start: date.utc(true).startOf("day").toDate(), + end: date.utc(true).startOf("day").add(1, "day").toDate(), + }, + ] + : values.range + ).map((item) => ({ start: date.hour(item.start.getHours()).minute(item.start.getMinutes()).toDate(), end: date.hour(item.end.getHours()).minute(item.end.getMinutes()).toDate(), })) diff --git a/packages/features/schedules/components/DateOverrideList.tsx b/packages/features/schedules/components/DateOverrideList.tsx index 7e469f25c7..9df265b9cd 100644 --- a/packages/features/schedules/components/DateOverrideList.tsx +++ b/packages/features/schedules/components/DateOverrideList.tsx @@ -1,12 +1,25 @@ -import { UseFieldArrayRemove } from "react-hook-form"; +import type { UseFieldArrayRemove } from "react-hook-form"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { TimeRange, WorkingHours } from "@calcom/types/schedule"; +import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; +import type { TimeRange, WorkingHours } from "@calcom/types/schedule"; import { Button, DialogTrigger, Tooltip } from "@calcom/ui"; import { FiEdit2, FiTrash2 } from "@calcom/ui/components/icon"; import DateOverrideInputDialog from "./DateOverrideInputDialog"; +const sortByDate = (a: { ranges: TimeRange[]; id: string }, b: { ranges: TimeRange[]; id: string }) => { + return a.ranges[0].start > b.ranges[0].start ? 1 : -1; +}; + +const useSettings = () => { + const { data } = useMeQuery(); + return { + hour12: data?.timeFormat === 12, + timeZone: data?.timeZone, + }; +}; + const DateOverrideList = ({ items, remove, @@ -22,17 +35,18 @@ const DateOverrideList = ({ excludedDates?: string[]; }) => { const { t, i18n } = useLocale(); + const { hour12 } = useSettings(); if (!items.length) { return <>; } const timeSpan = ({ start, end }: TimeRange) => { return ( - new Intl.DateTimeFormat(i18n.language, { hour: "numeric", minute: "numeric", hour12: true }).format( + new Intl.DateTimeFormat(i18n.language, { hour: "numeric", minute: "numeric", hour12 }).format( new Date(start.toISOString().slice(0, -1)) ) + " - " + - new Intl.DateTimeFormat(i18n.language, { hour: "numeric", minute: "numeric", hour12: true }).format( + new Intl.DateTimeFormat(i18n.language, { hour: "numeric", minute: "numeric", hour12 }).format( new Date(end.toISOString().slice(0, -1)) ) ); @@ -40,7 +54,7 @@ const DateOverrideList = ({ return (
    - {items.map((item, index) => ( + {items.sort(sortByDate).map((item, index) => (
  • @@ -50,7 +64,7 @@ const DateOverrideList = ({ day: "numeric", }).format(item.ranges[0].start)}

    - {item.ranges[0].end.getUTCHours() === 0 && item.ranges[0].end.getUTCMinutes() === 0 ? ( + {item.ranges[0].start.valueOf() - item.ranges[0].end.valueOf() === 0 ? (

    {t("unavailable")}

    ) : ( item.ranges.map((range, i) => ( diff --git a/packages/trpc/server/routers/viewer/availability.tsx b/packages/trpc/server/routers/viewer/availability.tsx index 04d5185da0..6fe68c733e 100644 --- a/packages/trpc/server/routers/viewer/availability.tsx +++ b/packages/trpc/server/routers/viewer/availability.tsx @@ -59,76 +59,6 @@ export const availabilityRouter = router({ .query(({ input }) => { return getUserAvailability(input); }), - defaultValues: authedProcedure.input(z.object({ scheduleId: z.number() })).query(async ({ ctx, input }) => { - const { prisma, user } = ctx; - const schedule = await prisma.schedule.findUnique({ - where: { - id: input.scheduleId || (await getDefaultScheduleId(user.id, prisma)), - }, - select: { - id: true, - userId: true, - name: true, - availability: true, - timeZone: true, - eventType: { - select: { - _count: true, - id: true, - eventName: true, - }, - }, - }, - }); - if (!schedule || schedule.userId !== user.id) { - throw new TRPCError({ - code: "UNAUTHORIZED", - }); - } - const availability = convertScheduleToAvailability(schedule); - return { - name: schedule.name, - rawSchedule: schedule, - schedule: availability.map((a) => - a.map((startAndEnd) => ({ - ...startAndEnd, - // Turn our limited granularity into proper end of day. - end: new Date(startAndEnd.end.toISOString().replace("23:59:00.000Z", "23:59:59.999Z")), - })) - ), - dateOverrides: schedule.availability.reduce((acc, override) => { - // only iff future date override - if (!override.date || override.date < new Date()) { - return acc; - } - const newValue = { - start: dayjs - .utc(override.date) - .hour(override.startTime.getUTCHours()) - .minute(override.startTime.getUTCMinutes()) - .toDate(), - end: dayjs - .utc(override.date) - .hour(override.endTime.getUTCHours()) - .minute(override.endTime.getUTCMinutes()) - .toDate(), - }; - const dayRangeIndex = acc.findIndex( - // early return prevents override.date from ever being empty. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (item) => yyyymmdd(item.ranges[0].start) === yyyymmdd(override.date!) - ); - if (dayRangeIndex === -1) { - acc.push({ ranges: [newValue] }); - return acc; - } - acc[dayRangeIndex].ranges.push(newValue); - return acc; - }, [] as { ranges: TimeRange[] }[]), - timeZone: schedule.timeZone || user.timeZone, - isDefault: !input.scheduleId || user.defaultScheduleId === schedule.id, - }; - }), schedule: router({ get: authedProcedure .input( @@ -162,15 +92,52 @@ export const availabilityRouter = router({ code: "UNAUTHORIZED", }); } - const availability = convertScheduleToAvailability(schedule); + const timeZone = schedule.timeZone || user.timeZone; return { - schedule, + id: schedule.id, + name: schedule.name, workingHours: getWorkingHours( { timeZone: schedule.timeZone || undefined }, schedule.availability || [] ), - availability, - timeZone: schedule.timeZone || user.timeZone, + schedule: schedule.availability, + availability: convertScheduleToAvailability(schedule).map((a) => + a.map((startAndEnd) => ({ + ...startAndEnd, + // Turn our limited granularity into proper end of day. + end: new Date(startAndEnd.end.toISOString().replace("23:59:00.000Z", "23:59:59.999Z")), + })) + ), + timeZone, + dateOverrides: schedule.availability.reduce((acc, override) => { + // only iff future date override + if (!override.date || dayjs.tz(override.date, timeZone).isBefore(dayjs(), "day")) { + return acc; + } + const newValue = { + start: dayjs + .utc(override.date) + .hour(override.startTime.getUTCHours()) + .minute(override.startTime.getUTCMinutes()) + .toDate(), + end: dayjs + .utc(override.date) + .hour(override.endTime.getUTCHours()) + .minute(override.endTime.getUTCMinutes()) + .toDate(), + }; + const dayRangeIndex = acc.findIndex( + // early return prevents override.date from ever being empty. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (item) => yyyymmdd(item.ranges[0].start) === yyyymmdd(override.date!) + ); + if (dayRangeIndex === -1) { + acc.push({ ranges: [newValue] }); + return acc; + } + acc[dayRangeIndex].ranges.push(newValue); + return acc; + }, [] as { ranges: TimeRange[] }[]), isDefault: !input.scheduleId || user.defaultScheduleId === schedule.id, }; }), @@ -469,10 +436,6 @@ const setupDefaultSchedule = async (userId: number, scheduleId: number, prisma: }); }; -const _isDefaultSchedule = (scheduleId: number, user: Partial) => { - return !user.defaultScheduleId || user.defaultScheduleId === scheduleId; -}; - const getDefaultScheduleId = async (userId: number, prisma: PrismaClient) => { const user = await prisma.user.findUnique({ where: { diff --git a/packages/ui/package.json b/packages/ui/package.json index 52103b367c..f0a84f471d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -32,7 +32,7 @@ "react": "^18.2.0", "react-colorful": "^5.6.0", "react-feather": "^2.0.10", - "react-hook-form": "^7.34.2", + "react-hook-form": "^7.43.3", "react-icons": "^4.4.0", "react-select": "^5.4.0" }, diff --git a/yarn.lock b/yarn.lock index 70ebdc2386..b2b86facdf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23014,6 +23014,11 @@ react-hook-form@^7.34.2: resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.34.2.tgz#9ac6d1a309a7c4aaa369d1269357a70e9e9bf4de" integrity sha512-1lYWbEqr0GW7HHUjMScXMidGvV0BE2RJV3ap2BL7G0EJirkqpccTaawbsvBO8GZaB3JjCeFBEbnEWI1P8ZoLRQ== +react-hook-form@^7.43.3: + version "7.43.3" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.43.3.tgz#780af64ea1f3c5864626a377e302bfcc7750af6f" + integrity sha512-LV6Fixh+hirrl6dXbM78aB6n//82aKbsNbcofF3wc6nx1UJLu3Jj/gsg1E5C9iISnLX+du8VTUyBUz2aCy+H7w== + react-hot-toast@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.3.0.tgz#70b3d183ac2a4afb6b17cda4a7f4cfe02e730415"