Bugfix/recurring dst change (#5172)

* Structural fix to recurring times

* Remove conversion regression

* Revert current time -> startTime based utcOffset

* Fixing remaining events count

* Using user's preference for recurring tooltip

* Missing refactor

* Showing better datetime in booking page
Added an extra seeded example for recurring

Co-authored-by: Leo Giovanetti <hello@leog.me>
This commit is contained in:
Alex van Andel 2022-10-24 23:37:55 +01:00 committed by GitHub
parent 6232a111ef
commit 5305f31266
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 102 additions and 55 deletions

View File

@ -1,6 +1,6 @@
import { BookingStatus } from "@prisma/client";
import { useRouter } from "next/router";
import { useState, useRef } from "react";
import { useState, useMemo } from "react";
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
import dayjs from "@calcom/dayjs";
@ -19,6 +19,7 @@ import MeetingTimeInTimezones from "@calcom/ui/v2/core/MeetingTimeInTimezones";
import showToast from "@calcom/ui/v2/core/notifications";
import useMeQuery from "@lib/hooks/useMeQuery";
import { extractRecurringDates } from "@lib/parseDate";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
@ -175,14 +176,16 @@ function BookingListItem(booking: BookingItemProps) {
setLocationMutation.mutate({ bookingId: booking.id, newLocation });
};
// Extract recurring dates is intensive to run, so use useMemo.
// Calculate the booking date(s) and setup recurring event data to show
const recurringStrings: string[] = [];
const recurringDates: Date[] = [];
// @FIXME: This is importing the RRULE library which is already heavy. Find out a more optimal way do this.
// if (booking.recurringBookings !== undefined && booking.eventType.recurringEvent?.freq !== undefined) {
// [recurringStrings, recurringDates] = extractRecurringDates(booking, user?.timeZone, i18n);
// }
const [recurringStrings, recurringDates] = useMemo(() => {
if (booking.recurringBookings !== undefined && booking.eventType.recurringEvent?.freq !== undefined) {
return extractRecurringDates(booking, user?.timeZone, i18n);
}
return [[], []];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user?.timeZone, i18n.language, booking.recurringBookings]);
const location = booking.location || "";
@ -291,11 +294,7 @@ function BookingListItem(booking: BookingItemProps) {
)}
<div className="mt-2 text-sm text-gray-400">
<RecurringBookingsTooltip
booking={booking}
recurringStrings={recurringStrings}
recurringDates={recurringDates}
/>
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
</div>
</div>
</td>
@ -333,11 +332,7 @@ function BookingListItem(booking: BookingItemProps) {
</Badge>
)}
<div className="text-sm text-gray-400 sm:hidden">
<RecurringBookingsTooltip
booking={booking}
recurringStrings={recurringStrings}
recurringDates={recurringDates}
/>
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
</div>
</div>
@ -398,17 +393,18 @@ function BookingListItem(booking: BookingItemProps) {
interface RecurringBookingsTooltipProps {
booking: BookingItemProps;
recurringStrings: string[];
recurringDates: Date[];
}
const RecurringBookingsTooltip = ({
booking,
recurringStrings,
recurringDates,
}: RecurringBookingsTooltipProps) => {
const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookingsTooltipProps) => {
// Get user so we can determine 12/24 hour format preferences
const query = useMeQuery();
const user = query.data;
const { t } = useLocale();
const now = new Date();
const recurringCount = recurringDates.filter((date) => {
return date >= now;
}).length;
return (
(booking.recurringBookings &&
@ -419,9 +415,11 @@ const RecurringBookingsTooltip = ({
<div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
<div className="flex">
<Tooltip
content={recurringStrings.map((aDate, key) => (
content={recurringDates.map((aDate, key) => (
<p key={key} className={classNames(recurringDates[key] < now && "line-through")}>
{aDate}
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)}
{" - "}
{dayjs(aDate).format("D MMMM YYYY")}
</p>
))}>
<div className="text-gray-600 dark:text-white">
@ -432,14 +430,12 @@ const RecurringBookingsTooltip = ({
<p className="mt-1 pl-5 text-xs">
{booking.status === BookingStatus.ACCEPTED
? `${t("event_remaining", {
count: recurringDates.length,
count: recurringCount,
})}`
: getEveryFreqFor({
t,
recurringEvent: booking.eventType.recurringEvent,
recurringCount: recurringDates.filter((date) => {
return date >= now;
}).length,
recurringCount,
})}
</p>
</div>

View File

@ -31,6 +31,7 @@ import {
} from "@calcom/embed-core/embed-iframe";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import { formatTime } from "@calcom/lib/date-fns";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
@ -46,6 +47,7 @@ import { Button } from "@calcom/ui/v2";
import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock";
import { ensureArray } from "@lib/ensureArray";
import useMeQuery from "@lib/hooks/useMeQuery";
import createBooking from "@lib/mutations/bookings/create-booking";
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
import { parseDate, parseRecurringDates } from "@lib/parseDate";
@ -86,6 +88,9 @@ const BookingPage = ({
...restProps
}: BookingPageProps) => {
const { t, i18n } = useLocale();
// Get user so we can determine 12/24 hour format preferences
const query = useMeQuery();
const user = query.data;
const isEmbed = useIsEmbed(restProps.isEmbed);
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
@ -493,15 +498,25 @@ const BookingPage = ({
<Icon.FiCalendar className="mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" />
<div className="text-sm font-medium">
{(rescheduleUid || !eventType.recurringEvent?.freq) &&
parseDate(dayjs(date).tz(timeZone()), i18n)}
`${formatTime(dayjs(date).toDate(), user?.timeFormat, user?.timeZone)}, ${dayjs(
date
).format("dddd, D MMMM YYYY")}`}
{!rescheduleUid &&
eventType.recurringEvent?.freq &&
recurringStrings.slice(0, 5).map((aDate, key) => <p key={key}>{aDate}</p>)}
recurringDates.slice(0, 5).map((aDate, key) => {
return (
<p key={key}>{`${formatTime(aDate, user?.timeFormat, user?.timeZone)}, ${dayjs(
aDate
).format("dddd, D MMMM YYYY")}`}</p>
);
})}
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
<div className="flex">
<Tooltip
content={recurringStrings.slice(5).map((aDate, key) => (
<p key={key}>{aDate}</p>
content={recurringDates.slice(5).map((aDate, key) => (
<p key={key}>{`${formatTime(aDate, user?.timeFormat, user?.timeZone)}, ${dayjs(
aDate
).format("dddd, D MMMM YYYY")}`}</p>
))}>
<p className="dark:text-darkgray-600 text-sm">
{t("plus_more", { count: recurringStrings.length - 5 })}

View File

@ -20,6 +20,15 @@ export const parseDate = (date: string | null | Dayjs, i18n: I18n) => {
return processDate(date, i18n);
};
// tzid is currently broken in rrule library.
// @see https://github.com/jakubroztocil/rrule/issues/523
const dateWithZone = (d: Date, timeZone?: string) => {
const dateInLocalTZ = new Date(d.toLocaleString("en-US", { timeZone: "UTC" }));
const dateInTargetTZ = new Date(d.toLocaleString("en-US", { timeZone: timeZone || "UTC" }));
const tzOffset = dateInTargetTZ.getTime() - dateInLocalTZ.getTime();
return new Date(d.getTime() - tzOffset);
};
export const parseRecurringDates = (
{
startDate,
@ -39,22 +48,16 @@ export const parseRecurringDates = (
const rule = new RRule({
...restRecurringEvent,
count: recurringCount,
dtstart: new Date(
Date.UTC(
dayjs.utc(startDate).get("year"),
dayjs.utc(startDate).get("month"),
dayjs.utc(startDate).get("date"),
dayjs.utc(startDate).get("hour"),
dayjs.utc(startDate).get("minute")
)
),
dtstart: dayjs(startDate).utc(true).toDate(),
});
// UTC times with tzOffset applied to account for DST
const times = rule.all().map((t) => dateWithZone(t, timeZone));
const dateStrings = times.map((t) => {
// undo DST diffs for localized display.
return processDate(dayjs.utc(t).tz(timeZone), i18n);
});
const utcOffset = dayjs(startDate).tz(timeZone).utcOffset();
const dateStrings = rule.all().map((r) => {
return processDate(dayjs.utc(r).utcOffset(utcOffset), i18n);
});
return [dateStrings, rule.all()];
return [dateStrings, times];
};
export const extractRecurringDates = (
@ -66,6 +69,7 @@ export const extractRecurringDates = (
timeZone: string | undefined,
i18n: I18n
): [string[], Date[]] => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { count = 0, ...rest } =
booking.eventType.recurringEvent !== null ? booking.eventType.recurringEvent : {};
const recurringInfo = booking.recurringBookings.find(

View File

@ -638,8 +638,6 @@ export function RecurringBookings({
: null;
if (recurringBookingsSorted && listingStatus === "recurring") {
// recurring bookings should only be adjusted to the start date.
const utcOffset = dayjs(recurringBookingsSorted[0]).utcOffset();
return (
<>
{eventType.recurringEvent?.count && (
@ -654,10 +652,9 @@ export function RecurringBookings({
{eventType.recurringEvent?.count &&
recurringBookingsSorted.slice(0, 4).map((dateStr, idx) => (
<div key={idx} className="mb-2">
{dayjs(dateStr).utcOffset(utcOffset).format("MMMM DD, YYYY")}
{dayjs(dateStr).format("MMMM DD, YYYY")}
<br />
{dayjs(dateStr).utcOffset(utcOffset).format("LT")} -{" "}
{dayjs(dateStr).utcOffset(utcOffset).add(eventType.length, "m").format("LT")}{" "}
{dayjs(dateStr).format("LT")} - {dayjs(dateStr).add(eventType.length, "m").format("LT")}{" "}
<span className="text-bookinglight">
({localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()})
</span>
@ -674,10 +671,9 @@ export function RecurringBookings({
{eventType.recurringEvent?.count &&
recurringBookingsSorted.slice(4).map((dateStr, idx) => (
<div key={idx} className="mb-2">
{dayjs(dateStr).utcOffset(utcOffset).format("MMMM DD, YYYY")}
{dayjs(dateStr).format("MMMM DD, YYYY")}
<br />
{dayjs(dateStr).utcOffset(utcOffset).format("LT")} -{" "}
{dayjs(dateStr).utcOffset(utcOffset).add(eventType.length, "m").format("LT")}{" "}
{dayjs(dateStr).format("LT")} - {dayjs(dateStr).add(eventType.length, "m").format("LT")}{" "}
<span className="text-bookinglight">
({localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()})
</span>

View File

@ -333,6 +333,42 @@ async function main() {
endTime: dayjs().add(1, "day").add(5, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
title: "Seeded Yoga class",
description: "seeded",
recurringEventId: Buffer.from("seeded-yoga-class").toString("base64"),
startTime: dayjs().subtract(4, "day").toDate(),
endTime: dayjs().subtract(4, "day").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
title: "Seeded Yoga class",
description: "seeded",
recurringEventId: Buffer.from("seeded-yoga-class").toString("base64"),
startTime: dayjs().subtract(4, "day").toDate(),
endTime: dayjs().subtract(4, "day").add(1, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
title: "Seeded Yoga class",
description: "seeded",
recurringEventId: Buffer.from("seeded-yoga-class").toString("base64"),
startTime: dayjs().subtract(4, "day").toDate(),
endTime: dayjs().subtract(4, "day").add(2, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
title: "Seeded Yoga class",
description: "seeded",
recurringEventId: Buffer.from("seeded-yoga-class").toString("base64"),
startTime: dayjs().subtract(4, "day").toDate(),
endTime: dayjs().subtract(4, "day").add(3, "week").add(30, "minutes").toDate(),
status: BookingStatus.ACCEPTED,
},
],
},
{