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
This commit is contained in:
Alex van Andel
2023-03-03 14:02:02 +01:00
committed by GitHub
parent 295748adca
commit 62dcd6612c
12 changed files with 159 additions and 180 deletions
@@ -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 (
<div className="space-y-4 rounded border px-6 pb-4">
@@ -156,15 +156,17 @@ const EventTypeScheduleDetails = () => {
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
<Button
href={`/availability/${schedule?.schedule.id}`}
disabled={isLoading}
color="minimal"
EndIcon={FiExternalLink}
target="_blank"
rel="noopener noreferrer">
{t("edit_availability")}
</Button>
{!!schedule?.id && (
<Button
href={`/availability/${schedule.id}`}
disabled={isLoading}
color="minimal"
EndIcon={FiExternalLink}
target="_blank"
rel="noopener noreferrer">
{t("edit_availability")}
</Button>
)}
</div>
</div>
);
+1 -1
View File
@@ -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",
+33 -39
View File
@@ -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<AvailabilityFormValues>({ defaultValues });
const { control } = form;
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId },
{
enabled: !!scheduleId,
}
);
const form = useForm<AvailabilityFormValues>({
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 (
<Shell
backPath="/availability"
title={data?.schedule.name ? data.schedule.name + " | " + t("availability") : t("availability")}
title={schedule?.name ? schedule.name + " | " + t("availability") : t("availability")}
heading={
<Controller
control={form.control}
@@ -155,8 +165,8 @@ export default function Availability({ schedule }: { schedule: number }) {
/>
}
subtitle={
data ? (
data.schedule.availability
schedule ? (
schedule.schedule
.filter((availability) => !!availability.days.length)
.map((availability) => (
<span key={availability.id}>
@@ -179,7 +189,7 @@ export default function Availability({ schedule }: { schedule: number }) {
</Skeleton>
<Switch
id="hiddenSwitch"
disabled={isLoading || data?.isDefault}
disabled={isLoading || schedule?.isDefault}
checked={form.watch("isDefault")}
onCheckedChange={(e) => {
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")}
</ConfirmationDialogContent>
@@ -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">
<div className="flex-1 flex-row xl:mr-0">
@@ -230,7 +241,7 @@ export default function Availability({ schedule }: { schedule: number }) {
<div>
{typeof me.data?.weekStart === "string" && (
<Schedule
control={control}
control={form.control}
name="schedule"
weekStart={
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
@@ -242,7 +253,7 @@ export default function Availability({ schedule }: { schedule: number }) {
</div>
</div>
<div className="my-6 rounded-md border">
{data?.workingHours && <DateOverride workingHours={data.workingHours} />}
{schedule?.workingHours && <DateOverride workingHours={schedule.workingHours} />}
</div>
</div>
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
@@ -282,20 +293,3 @@ export default function Availability({ schedule }: { schedule: number }) {
</Shell>
);
}
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(),
},
};
};
@@ -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": "*"
@@ -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": "*"
@@ -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": {
@@ -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": {
@@ -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 (
<Form
@@ -108,7 +101,15 @@ const DateOverrideForm = ({
handleSubmit={(values) => {
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(),
}))
@@ -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 (
<ul className="rounded border border-gray-200" data-testid="date-overrides-list">
{items.map((item, index) => (
{items.sort(sortByDate).map((item, index) => (
<li key={item.id} className="flex justify-between border-b px-5 py-4 last:border-b-0">
<div>
<h3 className="text-sm text-gray-900">
@@ -50,7 +64,7 @@ const DateOverrideList = ({
day: "numeric",
}).format(item.ranges[0].start)}
</h3>
{item.ranges[0].end.getUTCHours() === 0 && item.ranges[0].end.getUTCMinutes() === 0 ? (
{item.ranges[0].start.valueOf() - item.ranges[0].end.valueOf() === 0 ? (
<p className="text-xs text-gray-500">{t("unavailable")}</p>
) : (
item.ranges.map((range, i) => (
@@ -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<User>) => {
return !user.defaultScheduleId || user.defaultScheduleId === scheduleId;
};
const getDefaultScheduleId = async (userId: number, prisma: PrismaClient) => {
const user = await prisma.user.findUnique({
where: {
+1 -1
View File
@@ -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"
},
+5
View File
@@ -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"