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:
parent
6232a111ef
commit
5305f31266
|
@ -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>
|
||||
|
|
|
@ -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 })}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue
Block a user