From 1a79e0624c7eac48268067eb9cc1c9926f4c1f18 Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Thu, 5 May 2022 18:16:25 -0300 Subject: [PATCH] Recurring Events (#2562) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Init dev * UI changes for recurring event + prisma * Revisiting schema + changes WIP * UI done, BE WIP * Feature completion * Unused query param removed * Invalid comment removed * Removed unused translation * Update apps/web/public/static/locales/en/common.json Thanks! Co-authored-by: Peer Richelsen * Success page changes * More progress * Email text tweaks + test + seed * Tweaking emails + Cal Apps support WIP * No app integration for now Final email and pages tweaks to avoid recurring info showed * Missing comment for clarity * Yet again, comment * Last minute fix * Missing tooltip for upcoming bookings * Fixing seed * Fixing import * Increasing timeout for e2e * Fixing any * Apply suggestions from code review Co-authored-by: Omar López * Update apps/web/pages/d/[link]/book.tsx Co-authored-by: Omar López * Code improvements * More code improvements * Reverting back number input arrows * Update BookingPage.tsx * Update BookingPage.tsx * Adds fallback for sendOrganizerPaymentRefundFailedEmail * Type overkill * Type fixes * Type fixes * Nitpicks * Update success.tsx * Update success.tsx * Update success.tsx * Fixing types Co-authored-by: Peer Richelsen Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Omar López --- .github/workflows/e2e.yml | 2 +- README.md | 4 +- apps/web/components/BookingsShell.tsx | 4 + .../web/components/booking/AvailableTimes.tsx | 4 + .../components/booking/BookingListItem.tsx | 88 +- .../booking/pages/AvailabilityPage.tsx | 57 +- .../components/booking/pages/BookingPage.tsx | 170 ++- .../components/dialog/RescheduleDialog.tsx | 2 +- .../eventtype/EventTypeDescription.tsx | 20 +- .../eventtype/RecurringEventController.tsx | 132 ++ apps/web/components/team/MemberListItem.tsx | 2 +- apps/web/components/team/TeamListItem.tsx | 2 +- apps/web/components/ui/InfoBadge.tsx | 2 +- .../components/webhook/WebhookListItem.tsx | 2 +- .../components/apiKeys/ApiKeyDialogForm.tsx | 2 +- .../ee/components/apiKeys/ApiKeyListItem.tsx | 2 +- .../api/integrations/stripepayment/webhook.ts | 22 +- apps/web/lib/emails/email-manager.ts | 76 +- .../templates/attendee-declined-email.ts | 8 +- .../templates/attendee-request-email.ts | 15 +- .../attendee-request-reschedule-email.ts | 6 +- .../templates/attendee-rescheduled-email.ts | 2 +- .../templates/attendee-scheduled-email.ts | 44 +- .../templates/organizer-request-email.ts | 4 +- .../organizer-request-reschedule-email.ts | 6 +- .../templates/organizer-scheduled-email.ts | 41 +- .../bookings/create-recurring-booking.ts | 22 + apps/web/lib/parseDate.ts | 32 +- apps/web/lib/queries/teams/index.ts | 1 + apps/web/lib/types/booking.ts | 1 + apps/web/package.json | 1 + apps/web/pages/[user].tsx | 2 + apps/web/pages/[user]/[type].tsx | 3 + apps/web/pages/[user]/book.tsx | 16 +- apps/web/pages/api/book/confirm.ts | 124 +- apps/web/pages/api/book/event.ts | 98 +- apps/web/pages/api/book/request-reschedule.ts | 2 +- apps/web/pages/bookings/[status].tsx | 24 +- apps/web/pages/d/[link]/[slug].tsx | 4 + apps/web/pages/d/[link]/book.tsx | 16 +- apps/web/pages/event-types/[type].tsx | 27 +- apps/web/pages/event-types/index.tsx | 2 +- apps/web/pages/success.tsx | 180 ++- apps/web/pages/team/[slug]/[type].tsx | 4 + apps/web/pages/team/[slug]/book.tsx | 24 +- apps/web/playwright/event-types.test.ts | 39 + apps/web/public/static/locales/en/common.json | 30 + apps/web/server/routers/viewer.tsx | 49 +- apps/web/server/routers/viewer/eventTypes.tsx | 13 + apps/web/styles/globals.css | 4 +- .../components/wipeMyCalActionButton.tsx | 2 +- .../zapier/components/zapierSetup.tsx | 2 +- packages/core/builders/CalendarEvent/class.ts | 1 + packages/lib/defaultEvents.ts | 1 + .../migration.sql | 5 + packages/prisma/schema.prisma | 3 + packages/prisma/seed.ts | 105 ++ packages/prisma/zod-utils.ts | 11 + packages/types/Calendar.d.ts | 11 + .../components => packages/ui}/Tooltip.tsx | 2 +- yarn.lock | 1311 +---------------- 61 files changed, 1416 insertions(+), 1475 deletions(-) create mode 100644 apps/web/components/eventtype/RecurringEventController.tsx create mode 100644 apps/web/lib/mutations/bookings/create-recurring-booking.ts create mode 100644 packages/prisma/migrations/20220423022403_recurring_event/migration.sql rename {apps/web/components => packages/ui}/Tooltip.tsx (88%) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 34d45183..aff95c12 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -7,7 +7,7 @@ on: - public/static/locales/** jobs: test: - timeout-minutes: 15 + timeout-minutes: 20 name: Testing ${{ matrix.node }} and ${{ matrix.os }} strategy: matrix: diff --git a/README.md b/README.md index ffdd09c6..931c499e 100644 --- a/README.md +++ b/README.md @@ -190,8 +190,10 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env ### E2E-Testing +Be sure to set the environment variable `NEXTAUTH_URL` to the correct value. If you are running locally, as the documentation within `.env.example` mentions, the value should be `http://localhost:3000`. + ```sh -# In a terminal. Just run: +# In a terminal just run: yarn test-e2e # To open last HTML report run: diff --git a/apps/web/components/BookingsShell.tsx b/apps/web/components/BookingsShell.tsx index 700d0029..014e379e 100644 --- a/apps/web/components/BookingsShell.tsx +++ b/apps/web/components/BookingsShell.tsx @@ -11,6 +11,10 @@ export default function BookingsShell({ children }: { children: React.ReactNode name: t("upcoming"), href: "/bookings/upcoming", }, + { + name: t("recurring"), + href: "/bookings/recurring", + }, { name: t("past"), href: "/bookings/past", diff --git a/apps/web/components/booking/AvailableTimes.tsx b/apps/web/components/booking/AvailableTimes.tsx index 8afb3b77..a6c89b33 100644 --- a/apps/web/components/booking/AvailableTimes.tsx +++ b/apps/web/components/booking/AvailableTimes.tsx @@ -21,6 +21,7 @@ type AvailableTimesProps = { afterBufferTime: number; eventTypeId: number; eventLength: number; + recurringCount: number | undefined; eventTypeSlug: string; slotInterval: number | null; date: Dayjs; @@ -37,6 +38,7 @@ const AvailableTimes: FC = ({ eventTypeSlug, slotInterval, minimumBookingNotice, + recurringCount, timeFormat, users, schedulingType, @@ -90,6 +92,8 @@ const AvailableTimes: FC = ({ date: slot.time.format(), type: eventTypeId, slug: eventTypeSlug, + /** Treat as recurring only when a count exist and it's not a rescheduling workflow */ + count: recurringCount && !rescheduleUid ? recurringCount : undefined, }, }; diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 85c0ef1c..45492c72 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -1,26 +1,37 @@ import { BanIcon, CheckIcon, ClockIcon, XIcon, PencilAltIcon } from "@heroicons/react/outline"; import { PaperAirplaneIcon } from "@heroicons/react/outline"; +import { RefreshIcon } from "@heroicons/react/solid"; import { BookingStatus } from "@prisma/client"; import dayjs from "dayjs"; import { useState } from "react"; import { useMutation } from "react-query"; +import { Frequency as RRuleFrequency } from "rrule"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import Button from "@calcom/ui/Button"; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog"; +import { Tooltip } from "@calcom/ui/Tooltip"; import { TextArea } from "@calcom/ui/form/fields"; import { HttpError } from "@lib/core/http/error"; import useMeQuery from "@lib/hooks/useMeQuery"; -import { inferQueryOutput, trpc } from "@lib/trpc"; +import { parseRecurringDates } from "@lib/parseDate"; +import { inferQueryOutput, trpc, inferQueryInput } from "@lib/trpc"; import { RescheduleDialog } from "@components/dialog/RescheduleDialog"; import TableActions, { ActionType } from "@components/ui/TableActions"; +type BookingListingStatus = inferQueryInput<"viewer.bookings">["status"]; + type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number]; -function BookingListItem(booking: BookingItem) { +type BookingItemProps = BookingItem & { + listingStatus: BookingListingStatus; + recurringCount?: number; +}; + +function BookingListItem(booking: BookingItemProps) { // Get user so we can determine 12/24 hour format preferences const query = useMeQuery(); const user = query.data; @@ -30,14 +41,22 @@ function BookingListItem(booking: BookingItem) { const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false); const mutation = useMutation( async (confirm: boolean) => { + let body = { + id: booking.id, + confirmed: confirm, + language: i18n.language, + reason: rejectionReason, + }; + /** + * Only pass down the recurring event id when we need to confirm the entire series, which happens in + * the "Upcoming" tab, to support confirming discretionally in the "Recurring" tab. + */ + if (booking.listingStatus === "upcoming" && booking.recurringEventId !== null) { + body = Object.assign({}, body, { recurringEventId: booking.recurringEventId }); + } const res = await fetch("/api/book/confirm", { method: "PATCH", - body: JSON.stringify({ - id: booking.id, - confirmed: confirm, - language: i18n.language, - reason: rejectionReason, - }), + body: JSON.stringify(body), headers: { "Content-Type": "application/json", }, @@ -58,14 +77,20 @@ function BookingListItem(booking: BookingItem) { const pendingActions: ActionType[] = [ { id: "reject", - label: t("reject"), + label: + booking.listingStatus === "upcoming" && booking.recurringEventId !== null + ? t("reject_all") + : t("reject"), onClick: () => setRejectionDialogIsOpen(true), icon: BanIcon, disabled: mutation.isLoading, }, { id: "confirm", - label: t("confirm"), + label: + booking.listingStatus === "upcoming" && booking.recurringEventId !== null + ? t("confirm_all") + : t("confirm"), onClick: () => mutation.mutate(true), icon: CheckIcon, disabled: mutation.isLoading, @@ -112,6 +137,19 @@ function BookingListItem(booking: BookingItem) { const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY"); const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false); + + // Calculate the booking date(s) + let recurringStrings: string[] = []; + if (booking.recurringCount && booking.eventType.recurringEvent?.freq !== null) { + [recurringStrings] = parseRecurringDates( + { + startDate: booking.startTime, + recurringEvent: booking.eventType.recurringEvent, + recurringCount: booking.recurringCount, + }, + i18n + ); + } return ( <> - +
{startTime}
{dayjs(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "} {dayjs(booking.endTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
+
+ {booking.recurringCount && + booking.eventType?.recurringEvent?.freq && + booking.listingStatus === "upcoming" && ( +
+
+ ( +

{aDate}

+ ))}> +

+ + {`${t("every_for_freq", { + freq: t( + `${RRuleFrequency[booking.eventType.recurringEvent.freq] + .toString() + .toLowerCase()}` + ), + })} ${booking.recurringCount} ${t( + `${RRuleFrequency[booking.eventType.recurringEvent.freq].toString().toLowerCase()}`, + { count: booking.recurringCount } + )}`} +

+
+
+
+ )} +
diff --git a/apps/web/components/booking/pages/AvailabilityPage.tsx b/apps/web/components/booking/pages/AvailabilityPage.tsx index 296eb0da..f9d2b21b 100644 --- a/apps/web/components/booking/pages/AvailabilityPage.tsx +++ b/apps/web/components/booking/pages/AvailabilityPage.tsx @@ -8,6 +8,7 @@ import { CreditCardIcon, GlobeIcon, InformationCircleIcon, + RefreshIcon, } from "@heroicons/react/solid"; import * as Collapsible from "@radix-ui/react-collapsible"; import { useContracts } from "contexts/contractsContext"; @@ -17,6 +18,7 @@ import utc from "dayjs/plugin/utc"; import { useRouter } from "next/router"; import { useEffect, useMemo, useState } from "react"; import { FormattedNumber, IntlProvider } from "react-intl"; +import { Frequency as RRuleFrequency } from "rrule"; import { useEmbedStyles, @@ -27,11 +29,11 @@ import { useEmbedNonStylesConfig, } from "@calcom/embed-core"; import classNames from "@calcom/lib/classNames"; +import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { asStringOrNull } from "@lib/asStringOrNull"; import { timeZone } from "@lib/clock"; -import { BASE_URL, WEBAPP_URL } from "@lib/config/constants"; import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally"; import useTheme from "@lib/hooks/useTheme"; import { isBrandingHidden } from "@lib/isBrandingHidden"; @@ -101,6 +103,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage } const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false); const [timeFormat, setTimeFormat] = useState(detectBrowserTimeFormat); + const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count); const telemetry = useTelemetry(); @@ -142,6 +145,15 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage setTimeFormat(is24hClock ? "HH:mm" : "h:mma"); }; + // Recurring event sidebar requires more space + const maxWidth = selectedDate + ? recurringEventCount + ? "max-w-6xl" + : "max-w-5xl" + : recurringEventCount + ? "max-w-4xl" + : "max-w-3xl"; + return ( <> @@ -158,9 +170,8 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage className={classNames( shouldAlignCentrally ? "mx-auto" : "", isEmbed - ? classNames(selectedDate ? "max-w-5xl" : "max-w-3xl") - : "transition-max-width mx-auto my-0 duration-500 ease-in-out md:my-24 " + - (selectedDate ? "max-w-5xl" : "max-w-3xl") + ? classNames(maxWidth) + : classNames("transition-max-width mx-auto my-0 duration-500 ease-in-out md:my-24", maxWidth) )}> {isReady && (
{/* mobile: details */}
@@ -243,7 +254,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
{eventType?.description && ( -

+

{eventType.description}

)} -

+

{eventType.length} {t("minutes")}

+ {!rescheduleUid && eventType.recurringEvent?.count && eventType.recurringEvent?.freq && ( +
+ +

+ {t("every_for_freq", { + freq: t( + `${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}` + ), + })} +

+ { + setRecurringEventCount(parseInt(event?.target.value)); + }} + /> +

+ {t(`${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, { + count: recurringEventCount, + })} +

+
+ )} {eventType.price > 0 && (

@@ -302,7 +340,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage {booking?.startTime && rescheduleUid && (

{t("former_time")}

@@ -340,6 +378,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage eventTypeSlug={eventType.slug} slotInterval={eventType.slotInterval} eventLength={eventType.length} + recurringCount={recurringEventCount} date={selectedDate} users={eventType.users} schedulingType={eventType.schedulingType ?? null} diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index 27882cc8..59240726 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -4,6 +4,7 @@ import { CreditCardIcon, ExclamationIcon, InformationCircleIcon, + RefreshIcon, } from "@heroicons/react/solid"; import { zodResolver } from "@hookform/resolvers/zod"; import { EventTypeCustomInputType } from "@prisma/client"; @@ -18,6 +19,8 @@ import { Controller, useForm, useWatch } from "react-hook-form"; import { FormattedNumber, IntlProvider } from "react-intl"; import { ReactMultiEmail } from "react-multi-email"; import { useMutation } from "react-query"; +import { Frequency as RRuleFrequency } from "rrule"; +import { v4 as uuidv4 } from "uuid"; import { z } from "zod"; import { @@ -31,7 +34,9 @@ import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; import { createPaymentLink } from "@calcom/stripe/client"; +import { RecurringEvent } from "@calcom/types/Calendar"; import { Button } from "@calcom/ui/Button"; +import { Tooltip } from "@calcom/ui/Tooltip"; import { EmailInput, Form } from "@calcom/ui/form/fields"; import { asStringOrNull } from "@lib/asStringOrNull"; @@ -40,9 +45,11 @@ import { ensureArray } from "@lib/ensureArray"; import useTheme from "@lib/hooks/useTheme"; import { LocationType } from "@lib/location"; import createBooking from "@lib/mutations/bookings/create-booking"; -import { parseDate } from "@lib/parseDate"; +import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking"; +import { parseDate, parseRecurringDates } from "@lib/parseDate"; import slugify from "@lib/slugify"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; +import { BookingCreateBody } from "@lib/types/booking"; import CustomBranding from "@components/CustomBranding"; import AvatarGroup from "@components/ui/AvatarGroup"; @@ -76,6 +83,7 @@ const BookingPage = ({ booking, profile, isDynamicGroupBooking, + recurringEventCount, locationLabels, hasHashedBookingLink, hashedLink, @@ -140,6 +148,37 @@ const BookingPage = ({ }, }); + const recurringMutation = useMutation(createRecurringBooking, { + onSuccess: async (responseData = []) => { + const { attendees = [], recurringEventId } = responseData[0] || {}; + const location = (function humanReadableLocation(location) { + if (!location) { + return; + } + if (location.includes("integration")) { + return t("web_conferencing_details_to_follow"); + } + return location; + })(responseData[0].location); + + return router.push({ + pathname: "/success", + query: { + date, + type: eventType.id, + eventSlug: eventType.slug, + recur: recurringEventId, + user: profile.slug, + reschedule: !!rescheduleUid, + name: attendees[0].name, + email: attendees[0].email, + location, + eventName: profile.eventName || "", + }, + }); + }, + }); + const rescheduleUid = router.query.rescheduleUid as string; const { isReady, Theme } = useTheme(profile.theme); const date = asStringOrNull(router.query.date); @@ -243,6 +282,20 @@ const BookingPage = ({ } }; + // Calculate the booking date(s) + let recurringStrings: string[] = [], + recurringDates: Date[] = []; + if (eventType.recurringEvent?.freq && recurringEventCount !== null) { + [recurringStrings, recurringDates] = parseRecurringDates( + { + startDate: date, + recurringEvent: eventType.recurringEvent, + recurringCount: parseInt(recurringEventCount.toString()), + }, + i18n + ); + } + const bookEvent = (booking: BookingFormValues) => { telemetry.withJitsu((jitsu) => jitsu.track( @@ -265,7 +318,7 @@ const BookingPage = ({ {} ); - let web3Details; + let web3Details: Record<"userWallet" | "userSignature", string> | undefined; if (eventTypeDetail.metadata.smartContractAddress) { web3Details = { // @ts-ignore @@ -274,28 +327,59 @@ const BookingPage = ({ }; } - mutation.mutate({ - ...booking, - web3Details, - start: dayjs(date).format(), - end: dayjs(date).add(eventType.length, "minute").format(), - eventTypeId: eventType.id, - eventTypeSlug: eventType.slug, - timeZone: timeZone(), - language: i18n.language, - rescheduleUid, - user: router.query.user, - location: getLocationValue( - booking.locationType ? booking : { ...booking, locationType: selectedLocation } - ), - metadata, - customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({ - label: eventType.customInputs.find((input) => input.id === parseInt(inputId))!.label, - value: booking.customInputs![inputId], - })), - hasHashedBookingLink, - hashedLink, - }); + if (recurringDates.length) { + // Identify set of bookings to one intance of recurring event to support batch changes + const recurringEventId = uuidv4(); + const recurringBookings = recurringDates.map((recurringDate) => ({ + ...booking, + web3Details, + start: dayjs(recurringDate).format(), + end: dayjs(recurringDate).add(eventType.length, "minute").format(), + eventTypeId: eventType.id, + eventTypeSlug: eventType.slug, + recurringEventId, + // Added to track down the number of actual occurrences selected by the user + recurringCount: recurringDates.length, + timeZone: timeZone(), + language: i18n.language, + rescheduleUid, + user: router.query.user, + location: getLocationValue( + booking.locationType ? booking : { ...booking, locationType: selectedLocation } + ), + metadata, + customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({ + label: eventType.customInputs.find((input) => input.id === parseInt(inputId))!.label, + value: booking.customInputs![inputId], + })), + hasHashedBookingLink, + hashedLink, + })); + recurringMutation.mutate(recurringBookings); + } else { + mutation.mutate({ + ...booking, + web3Details, + start: dayjs(date).format(), + end: dayjs(date).add(eventType.length, "minute").format(), + eventTypeId: eventType.id, + eventTypeSlug: eventType.slug, + timeZone: timeZone(), + language: i18n.language, + rescheduleUid, + user: router.query.user, + location: getLocationValue( + booking.locationType ? booking : { ...booking, locationType: selectedLocation } + ), + metadata, + customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({ + label: eventType.customInputs.find((input) => input.id === parseInt(inputId))!.label, + value: booking.customInputs![inputId], + })), + hasHashedBookingLink, + hashedLink, + }); + } }; const disableInput = !!rescheduleUid; @@ -375,10 +459,40 @@ const BookingPage = ({

)} -

- - {parseDate(date, i18n)} -

+ {!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && ( +
+ +

+ {`${t("every_for_freq", { + freq: t(`${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`), + })} ${recurringEventCount} ${t( + `${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, + { count: parseInt(recurringEventCount.toString()) } + )}`} +

+
+ )} +
+ +
+ {(rescheduleUid || !eventType.recurringEvent.freq) && parseDate(date, i18n)} + {!rescheduleUid && + eventType.recurringEvent.freq && + recurringStrings.slice(0, 5).map((aDate, key) =>

{aDate}

)} + {!rescheduleUid && eventType.recurringEvent.freq && recurringStrings.length > 5 && ( +
+ ( +

{aDate}

+ ))}> +

+ {t("plus_more", { count: recurringStrings.length - 5 })} +

+
+
+ )} +
+
{eventTypeDetail.isWeb3Active && eventType.metadata.smartContractAddress && (

{t("requires_ownership_of_a_token") + " " + eventType.metadata.smartContractAddress} diff --git a/apps/web/components/dialog/RescheduleDialog.tsx b/apps/web/components/dialog/RescheduleDialog.tsx index a93a0f48..fd228f89 100644 --- a/apps/web/components/dialog/RescheduleDialog.tsx +++ b/apps/web/components/dialog/RescheduleDialog.tsx @@ -77,7 +77,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => { /> - +