From 62dcd6612c016598da43d323c41c9cc2cefc3f49 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Fri, 3 Mar 2023 13:02:02 +0000 Subject: [PATCH] Removed some of the UTC logic which shouldn't be there (#7425) * Removed some of the UTC logic which shouldn't be there * Upgrade rhf, remove SSR on [schedule] page * Fix potential bug with date * Amend schedule trpc call & don't show edit availability if id is undefined * type fix * Order date overrides in list by date * Changed logic to check for unavailable * Fix bug report spotted by @CarinaWolli I can save date overrides for march 1st and 2nd (my date/time when saving was march 1st at 11PM GMT-5). It is saved successfully in the database however the date overrides don't show up in the list. That's why: - new Date() is utc so it is already march 2nd - override.date has always timestamp 00:00 so it is also smaller than the utc time which was in my case 2023-03-02T04:00 * Actual fix, gotta remember it's the schedule tz --- .../components/eventtype/AvailabilityTab.tsx | 22 ++-- apps/web/package.json | 2 +- apps/web/pages/availability/[schedule].tsx | 72 +++++------ packages/app-store/applecalendar/package.json | 2 +- .../app-store/caldavcalendar/package.json | 2 +- .../exchange2013calendar/package.json | 2 +- .../exchange2016calendar/package.json | 2 +- .../components/DateOverrideInputDialog.tsx | 83 ++++++------ .../schedules/components/DateOverrideList.tsx | 26 +++- .../server/routers/viewer/availability.tsx | 119 ++++++------------ packages/ui/package.json | 2 +- yarn.lock | 5 + 12 files changed, 159 insertions(+), 180 deletions(-) diff --git a/apps/web/components/eventtype/AvailabilityTab.tsx b/apps/web/components/eventtype/AvailabilityTab.tsx index b06c084d97a67..52dcd3c121068 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 59c0ac169bd68..601c3c544f7dd 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 a410bbd102ad9..3387bd48b894e 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 fe5b883c50fd3..8809bf7cb8316 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 36b2fabd81c68..01a9a3ba3521a 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 ecb5e23752286..216eba4ca49b8 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 982317eda1ecd..480fa52b8c576 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 9695398b4b67f..a54a4092ba004 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 7e469f25c755b..9df265b9cdca0 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 04d5185da0049..6fe68c733ea52 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 52103b367cce6..f0a84f471d2de 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 70ebdc2386cda..b2b86facdfdfc 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"