From 9c16054bc3cdcf2c8bbce06658d72483eae0eb61 Mon Sep 17 00:00:00 2001 From: Hariom Date: Mon, 14 Oct 2024 11:19:23 +0530 Subject: [PATCH] Simplify things --- .../components/booking/BookingListItem.tsx | 2 +- apps/web/components/dialog/RerouteDialog.tsx | 229 ++++++++++++------ apps/web/public/static/locales/en/common.json | 5 + .../components/FormInputFields.tsx | 16 ++ packages/ui/components/dialog/Dialog.tsx | 4 +- 5 files changed, 176 insertions(+), 80 deletions(-) diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 473c08176415d9..29d6d81397772a 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -228,7 +228,7 @@ function BookingListItem(booking: BookingItemProps) { ? [ { id: "reroute", - label: t("re-route"), + label: t("reroute"), onClick: () => { setRerouteDialogIsOpen(true); }, diff --git a/apps/web/components/dialog/RerouteDialog.tsx b/apps/web/components/dialog/RerouteDialog.tsx index 7a9fde19e7ea4b..6af1051985d5fb 100644 --- a/apps/web/components/dialog/RerouteDialog.tsx +++ b/apps/web/components/dialog/RerouteDialog.tsx @@ -4,7 +4,9 @@ import { useState } from "react"; import { useEffect, useCallback } from "react"; import type { z } from "zod"; -import FormInputFields from "@calcom/app-store/routing-forms/components/FormInputFields"; +import FormInputFields, { + FormInputFieldsSkeleton, +} from "@calcom/app-store/routing-forms/components/FormInputFields"; import { getAbsoluteEventTypeRedirectUrl } from "@calcom/app-store/routing-forms/getEventTypeRedirectUrl"; import { findMatchingRoute } from "@calcom/app-store/routing-forms/lib/processRoute"; import { substituteVariables } from "@calcom/app-store/routing-forms/lib/substituteVariables"; @@ -24,6 +26,12 @@ import { Dialog, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui"; import { Button } from "@calcom/ui"; import { showToast } from "@calcom/ui/components/toast"; +const enum ReroutingStatusEnum { + REROUTING_NOT_INITIATED = "not_initiated", + REROUTING_IN_PROGRESS = "in_progress", + REROUTING_COMPLETE = "complete", +} + type ResponseWithForm = RouterOutputs["viewer"]["appRoutingForms"]["getResponseWithFormFields"]; type BookingToReroute = Pick & { @@ -54,16 +62,23 @@ type RerouteDialogProps = { booking: BookingToReroute; }; -type ReroutingState = { - /** - * UID of the rescheduled booking - */ - newBooking: string; - /** - * window for the new tab that is opened for rerouting - */ - reschedulerWindow: Window; -}; +type ReroutingState = + | { + type: "reschedule_to_same_event_new_tab" | "reschedule_to_different_event_new_tab"; + /** + * UID of the rescheduled booking + */ + newBooking: string | null; + /** + * window for the new tab that is opened for rerouting + */ + reschedulerWindow: Window; + } + | { + type: "same_timeslot_same_event"; + newBooking: string | null; + reschedulerWindow: null; + }; type TeamMemberMatchingAttributeLogic = { id: number; @@ -71,22 +86,18 @@ type TeamMemberMatchingAttributeLogic = { email: string; }; -function ErrorMsg({ message }: { message: string }) { - return !message ? null :
{message}
; -} - function rebookInNewTab({ responseWithForm, teamMemberIdsMatchingAttributeLogic, chosenRoute, booking, - setReroutingState, + reroutingState, }: { responseWithForm: ResponseWithForm; booking: TeamEventTypeBookingToReroute; teamMemberIdsMatchingAttributeLogic: number[] | null; chosenRoute: LocalRoute; - setReroutingState: React.Dispatch>; + reroutingState: ReturnType; }) { const { form, response } = responseWithForm; const formFields = form.fields || []; @@ -101,7 +112,6 @@ function rebookInNewTab({ rescheduleUid: booking.uid, }); const bookingEventTypeSlug = getFullSlugForEvent(booking.eventType); - const eventTypeUrlWithResolvedVariables = substituteVariables( chosenRoute.action.value, response, @@ -124,25 +134,73 @@ function rebookInNewTab({ }); const reschedulerWindow = window.open(url, "_blank"); + if (!reschedulerWindow) { throw new Error("Failed to open new tab"); } - setReroutingState((prev) => { - if (!prev) return null; - return { - ...prev, - reschedulerWindow, - }; + + reroutingState.setValue({ + type: "reschedule_to_same_event_new_tab", + reschedulerWindow, + newBooking: null, }); + + return reschedulerWindow; +} + +const getFullSlugForEvent = ( + eventType: Pick +) => { + return eventType.team + ? `team/${eventType.team.slug}/${eventType.slug}` + : `${eventType.users[0].username}/${eventType.slug}`; +}; + +function isBookingTimeslotInPast(booking: BookingToReroute) { + return dayjs(booking.startTime).isBefore(dayjs()); } +const useReroutingState = ({ isOpenDialog }: Pick) => { + const [value, setValue] = useState(null); + let state = value; + + const isDialogClosedButReroutingWindowNotClosed = !isOpenDialog && state?.reschedulerWindow; + + if (isDialogClosedButReroutingWindowNotClosed) { + state = null; + } + + if (isDialogClosedButReroutingWindowNotClosed) { + state?.reschedulerWindow?.close(); + } + + useEffect(() => { + const checkInterval = setInterval(() => { + if (state?.reschedulerWindow?.closed) { + // Ensure that render happens again + setValue(null); + clearInterval(checkInterval); + } + }, 1); + + return () => clearInterval(checkInterval); + }, [value, setValue]); + + const status = (() => { + if (!value) return ReroutingStatusEnum.REROUTING_NOT_INITIATED; + if (!!value.newBooking) return ReroutingStatusEnum.REROUTING_COMPLETE; + return ReroutingStatusEnum.REROUTING_IN_PROGRESS; + })(); + + return { value: state, setValue, status }; +}; + const NewRoutingManager = ({ chosenRoute, booking, responseWithForm, teamMembersMatchingAttributeLogic, reroutingState, - setReroutingState, setIsOpenDialog, }: Pick & { chosenRoute: LocalRoute; @@ -152,8 +210,7 @@ const NewRoutingManager = ({ isPending: boolean; data: TeamMemberMatchingAttributeLogic[] | null; }; - reroutingState: ReroutingState | null; - setReroutingState: React.Dispatch>; + reroutingState: ReturnType; }) => { const { t } = useLocale(); const router = useRouter(); @@ -165,13 +222,22 @@ const NewRoutingManager = ({ const bookingEventType = booking.eventType; const isRoundRobinScheduling = bookingEventType.schedulingType === SchedulingType.ROUND_ROBIN; - const isReroutingInProgress = !reroutingState?.newBooking && !reroutingState?.reschedulerWindow; - const isReroutingComplete = !!reroutingState?.newBooking; - const isReroutingInNewTab = !!reroutingState?.reschedulerWindow; + const createBookingMutation = useMutation({ mutationFn: createBooking, onSuccess: (booking) => { showToast("Re-routed booker successfully", "success"); + reroutingState.setValue((prev) => { + if (!prev) return null; + if (!booking.uid) { + console.error("Booking UID is not there"); + throw new Error(t("something_went_wrong")); + } + return { + ...prev, + newBooking: booking.uid, + }; + }); // FIXME: DO we need to send other params as well? router.push(`/booking/${booking.uid}?cal.rerouting=true`); }, @@ -190,6 +256,11 @@ const NewRoutingManager = ({ console.error("Chosen route must be there for rerouting"); throw new Error(t("something_went_wrong")); } + + if (isBookingTimeslotInPast(booking)) { + showToast("You cannot reschedule to a past timeslot", "error"); + return; + } const booker = booking.attendees[0]; const getFieldsThatRemainSame = () => { return { @@ -262,6 +333,12 @@ const NewRoutingManager = ({ ...getRoutingFormRelatedFields(), ...getSalesforceContactOwnerFields(), }); + + reroutingState.setValue({ + newBooking: null, + type: "same_timeslot_same_event", + reschedulerWindow: null, + }); } function handleRebookInNewTab() { @@ -275,7 +352,7 @@ const NewRoutingManager = ({ teamMemberIdsMatchingAttributeLogic, chosenRoute, booking, - setReroutingState, + reroutingState, }); } @@ -291,7 +368,7 @@ const NewRoutingManager = ({ ); } if (chosenRoute.action.type === RouteActionType.EventTypeRedirectUrl) { - // Computed state for eventTypeSlugToRedirect + // Computed value for eventTypeSlugToRedirect const eventTypeSlugToRedirect = chosenRoute.action.type === RouteActionType.EventTypeRedirectUrl ? chosenRoute.action.value : null; @@ -315,7 +392,7 @@ const NewRoutingManager = ({ {isRoundRobinScheduling ? t("reroute_preview_possible_host") : t("Hosts")}: - + {" "} {bookingHosts()} @@ -332,8 +409,6 @@ const NewRoutingManager = ({ }; const reroutingCTAs = () => { - if (!isReroutingInProgress) return null; - return (
+ + + + ); + if (!responseWithForm) return
{t("something_went_wrong")}
; return ( @@ -485,10 +563,11 @@ const RerouteDialogContentAndFooterWithFormResponse = ({ const [responseFromOrganizer, setResponseFromOrganizer] = useState({}); const isResponseFromOrganizerUnpopulated = Object.keys(responseFromOrganizer).length === 0; const response = isResponseFromOrganizerUnpopulated ? responseWithForm.response : responseFromOrganizer; - const [errorMsg, setErrorMsg] = useState(""); const [chosenRoute, setChosenRoute] = useState(null); - const [reroutingState, setReroutingState] = useState(null); + const reroutingState = useReroutingState({ + isOpenDialog, + }); const [teamMembersMatchingAttributeLogic, setTeamMembersMatchingAttributeLogic] = useState< | { @@ -506,15 +585,10 @@ const RerouteDialogContentAndFooterWithFormResponse = ({ }, }); - if (!isOpenDialog && reroutingState?.reschedulerWindow) { - reroutingState.reschedulerWindow.close(); - setReroutingState(null); - } - const messageListener = useCallback((event: MessageEvent) => { if (event.data.type === "CAL:rescheduleBookingSuccessfulV2") { const calEventData = event.data.data; - setReroutingState((prev) => { + reroutingState.setValue((prev) => { if (!prev) return null; return { ...prev, newBooking: calEventData.uid }; }); @@ -522,9 +596,9 @@ const RerouteDialogContentAndFooterWithFormResponse = ({ }, []); const beforeUnloadListener = useCallback(() => { - reroutingState?.reschedulerWindow?.close(); - setReroutingState(null); - }, [reroutingState?.reschedulerWindow]); + reroutingState.value?.reschedulerWindow?.close(); + reroutingState.setValue(null); + }, [reroutingState.value?.reschedulerWindow]); useEffect(() => { window.addEventListener("message", messageListener); @@ -536,15 +610,14 @@ const RerouteDialogContentAndFooterWithFormResponse = ({ }, [messageListener, beforeUnloadListener]); function verifyRoute() { - // if (!response) { - // setErrorMsg("You need to make some changes to the form first."); - // return; - // } + if (isResponseFromOrganizerUnpopulated) { + showToast("Please make some changes to allow rerouting.", "error"); + return; + } // Reset all states - setReroutingState(null); + reroutingState.setValue(null); setTeamMembersMatchingAttributeLogic(null); - setErrorMsg(""); const route = findMatchingRoute({ form, @@ -564,7 +637,6 @@ const RerouteDialogContentAndFooterWithFormResponse = ({ return (
-
@@ -579,7 +651,6 @@ const RerouteDialogContentAndFooterWithFormResponse = ({ }} reroutingState={reroutingState} setIsOpenDialog={setIsOpenDialog} - setReroutingState={setReroutingState} responseWithForm={responseWithForm} /> )} @@ -589,7 +660,11 @@ const RerouteDialogContentAndFooterWithFormResponse = ({ - +
); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 059eaec279025c..b583ff78bc10b8 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2649,5 +2649,10 @@ "reroute_preview_external_redirect": "It results in redirecting to {{externalUrl}}. Try changing the response to route to an event", "reroute_preview_possible_host": "Possible host", "current_routing_status": "Current routing status", + "reroute_booking": "Reroute booking", + "reroute_booking_description": "Reroute the booking to different team members", + "verify_new_route": "Verify new route", + "reroute": "Reroute", + "new_route_status": "New route status", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/packages/app-store/routing-forms/components/FormInputFields.tsx b/packages/app-store/routing-forms/components/FormInputFields.tsx index 1def029a37a7db..248cd5f07d6c4e 100644 --- a/packages/app-store/routing-forms/components/FormInputFields.tsx +++ b/packages/app-store/routing-forms/components/FormInputFields.tsx @@ -1,6 +1,8 @@ import type { App_RoutingForms_Form } from "@prisma/client"; import type { Dispatch, SetStateAction } from "react"; +import { SkeletonText } from "@calcom/ui"; + import getFieldIdentifier from "../lib/getFieldIdentifier"; import { getQueryBuilderConfigForFormFields } from "../lib/getQueryBuilderConfig"; import isRouterLinkedField from "../lib/isRouterLinkedField"; @@ -67,3 +69,17 @@ export default function FormInputFields(props: FormInputFieldsProps) { ); } + +export const FormInputFieldsSkeleton = () => { + const numberOfFields = 5; + return ( + <> + {Array.from({ length: numberOfFields }).map((_, index) => ( +
+ + +
+ ))} + + ); +}; diff --git a/packages/ui/components/dialog/Dialog.tsx b/packages/ui/components/dialog/Dialog.tsx index 03cd0ec95aabcc..83d3378a99dd58 100644 --- a/packages/ui/components/dialog/Dialog.tsx +++ b/packages/ui/components/dialog/Dialog.tsx @@ -96,7 +96,7 @@ type DialogContentProps = React.ComponentProps<(typeof DialogPrimitive)["Content // enableOverflow:- use this prop whenever content inside DialogContent could overflow and require scrollbar export const DialogContent = React.forwardRef( ( - { children, title, Icon: icon, enableOverflow, forceOverlayWhenNoModal, type = "creation", ...props }, + { children, title, Icon: icon, enableOverflow, forceOverlayWhenNoModal, type = "creation", preventCloseOnOutsideClick, ...props }, forwardedRef ) => { const isPlatform = useIsPlatform(); @@ -121,7 +121,7 @@ export const DialogContent = React.forwardRef { - if (props.preventCloseOnOutsideClick) { + if (preventCloseOnOutsideClick) { e.preventDefault(); } }}