diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 29d6d81397772a..6da82803fe3beb 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -232,7 +232,7 @@ function BookingListItem(booking: BookingItemProps) { onClick: () => { setRerouteDialogIsOpen(true); }, - icon: "pencil" as const, + icon: "waypoints" as const, }, ] : []), diff --git a/apps/web/components/dialog/RerouteDialog.tsx b/apps/web/components/dialog/RerouteDialog.tsx index 6af1051985d5fb..39adafc15714e6 100644 --- a/apps/web/components/dialog/RerouteDialog.tsx +++ b/apps/web/components/dialog/RerouteDialog.tsx @@ -1,4 +1,5 @@ import { useMutation } from "@tanstack/react-query"; +import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { useEffect, useCallback } from "react"; @@ -15,6 +16,7 @@ import type { FormResponse, LocalRoute } from "@calcom/app-store/routing-forms/t import { RouteActionType } from "@calcom/app-store/routing-forms/zod"; import dayjs from "@calcom/dayjs"; import { createBooking } from "@calcom/features/bookings/lib/create-booking"; +import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { EventType, User, Team, Attendee, Booking as PrismaBooking } from "@calcom/prisma/client"; import { SchedulingType } from "@calcom/prisma/enums"; @@ -26,6 +28,8 @@ import { Dialog, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui"; import { Button } from "@calcom/ui"; import { showToast } from "@calcom/ui/components/toast"; +import useRouterQuery from "@lib/hooks/useRouterQuery"; + const enum ReroutingStatusEnum { REROUTING_NOT_INITIATED = "not_initiated", REROUTING_IN_PROGRESS = "in_progress", @@ -86,53 +90,60 @@ type TeamMemberMatchingAttributeLogic = { email: string; }; -function rebookInNewTab({ - responseWithForm, - teamMemberIdsMatchingAttributeLogic, +function getEventTypeUrlsForTheChosenRoute({ chosenRoute, + form, + currentResponse, + teamMemberIdsMatchingAttributeLogic, booking, - reroutingState, + searchParams, }: { - responseWithForm: ResponseWithForm; - booking: TeamEventTypeBookingToReroute; + form: ResponseWithForm["form"]; + currentResponse: FormResponse; teamMemberIdsMatchingAttributeLogic: number[] | null; - chosenRoute: LocalRoute; - reroutingState: ReturnType; + chosenRoute: LocalRoute | null; + booking: TeamEventTypeBookingToReroute; + searchParams: URLSearchParams; }) { - const { form, response } = responseWithForm; + if (!chosenRoute) { + return null; + } + const formFields = form.fields || []; const routedFromRoutingFormReponseId = booking.routedFromRoutingFormReponse.id; + const allURLSearchParams = getUrlSearchParamsToForwardForReroute({ - formResponse: response, + formResponse: currentResponse, formResponseId: routedFromRoutingFormReponseId, fields: formFields, - searchParams: new URLSearchParams(window.location.search), + searchParams, teamMembersMatchingAttributeLogic: teamMemberIdsMatchingAttributeLogic, - attributeRoutingConfig: chosenRoute?.attributeRoutingConfig ?? null, + attributeRoutingConfig: chosenRoute.attributeRoutingConfig ?? null, rescheduleUid: booking.uid, + reroutingFormResponses: Object.fromEntries( + Object.entries(currentResponse).map(([key, response]) => [key, { value: response.value }]) + ), }); - const bookingEventTypeSlug = getFullSlugForEvent(booking.eventType); - const eventTypeUrlWithResolvedVariables = substituteVariables( - chosenRoute.action.value, - response, - formFields - ); - if (bookingEventTypeSlug !== eventTypeUrlWithResolvedVariables) { - showToast( - "Rerouting to a different event type is not supported yet. It would require cancelling the current booking and creating a new one.", - "error" - ); - return; - } + const eventFullSlug = substituteVariables(chosenRoute.action.value, currentResponse, formFields); - const url = getAbsoluteEventTypeRedirectUrl({ + const eventBookingAbsoluteUrl = getAbsoluteEventTypeRedirectUrl({ form, - eventTypeRedirectUrl: eventTypeUrlWithResolvedVariables, + eventTypeRedirectUrl: eventFullSlug, allURLSearchParams, isEmbed: false, }); + return { eventFullSlug, eventBookingAbsoluteUrl }; +} + +function rescheduleInNewTab({ + url, + reroutingState, +}: { + url: string; + reroutingState: ReturnType; +}) { const reschedulerWindow = window.open(url, "_blank"); if (!reschedulerWindow) { @@ -162,16 +173,12 @@ function isBookingTimeslotInPast(booking: BookingToReroute) { const useReroutingState = ({ isOpenDialog }: Pick) => { const [value, setValue] = useState(null); - let state = value; - - const isDialogClosedButReroutingWindowNotClosed = !isOpenDialog && state?.reschedulerWindow; - - if (isDialogClosedButReroutingWindowNotClosed) { - state = null; - } + const state = value; - if (isDialogClosedButReroutingWindowNotClosed) { - state?.reschedulerWindow?.close(); + // If dialog is closed but the rerouting window is still open, close it + if (!isOpenDialog && state?.reschedulerWindow) { + state.reschedulerWindow.close(); + setValue(null); } useEffect(() => { @@ -195,17 +202,30 @@ const useReroutingState = ({ isOpenDialog }: Pick { + const { setQuery: setIsReroutingQuery } = useRouterQuery("isRerouting"); + useEffect(() => { + if (isOpenDialog) { + setIsReroutingQuery("true"); + } else { + setIsReroutingQuery(undefined); + } + }, [isOpenDialog]); +}; + const NewRoutingManager = ({ chosenRoute, booking, - responseWithForm, + form, + currentResponse, teamMembersMatchingAttributeLogic, reroutingState, setIsOpenDialog, }: Pick & { chosenRoute: LocalRoute; booking: TeamEventTypeBookingToReroute; - responseWithForm: ResponseWithForm; + form: ResponseWithForm["form"]; + currentResponse: FormResponse; teamMembersMatchingAttributeLogic: { isPending: boolean; data: TeamMemberMatchingAttributeLogic[] | null; @@ -214,14 +234,29 @@ const NewRoutingManager = ({ }) => { const { t } = useLocale(); const router = useRouter(); - const { form, response } = responseWithForm; + const bookerUrl = useBookerUrl(); const teamMemberIdsMatchingAttributeLogic = teamMembersMatchingAttributeLogic?.data?.map((member) => member.id) || null; const routedFromRoutingFormReponseId = booking.routedFromRoutingFormReponse.id; const bookingEventType = booking.eventType; + // Provide a reason for rescheduling that can be customized by user in the future + // const rescheduleReason = "Rerouted"; const isRoundRobinScheduling = bookingEventType.schedulingType === SchedulingType.ROUND_ROBIN; + const chosenEventUrls = getEventTypeUrlsForTheChosenRoute({ + chosenRoute, + form, + currentResponse, + teamMemberIdsMatchingAttributeLogic, + booking, + searchParams: new URLSearchParams({ + // rescheduleReason + }), + }); + + const currentBookingEventFullSlug = getFullSlugForEvent(booking.eventType); + const isReroutingToDifferentEvent = currentBookingEventFullSlug !== chosenEventUrls?.eventFullSlug; const createBookingMutation = useMutation({ mutationFn: createBooking, @@ -251,6 +286,17 @@ const NewRoutingManager = ({ if (!chosenRoute) return null; + return ( +
+

{t("new_routing_status")}

+
+ {reroutingState.status === ReroutingStatusEnum.REROUTING_NOT_INITIATED && reroutingPreview()} + {reroutingState.status === ReroutingStatusEnum.REROUTING_NOT_INITIATED && reroutingCTAs()} + {reroutingStatus()} +
+
+ ); + function rescheduleToSameTimeslotOfSameEvent() { if (!chosenRoute) { console.error("Chosen route must be there for rerouting"); @@ -326,7 +372,8 @@ const NewRoutingManager = ({ createBookingMutation.mutate({ isRerouting: true, rescheduleUid: booking.uid, - reroutingFormResponses: response, + // rescheduleReason, + reroutingFormResponses: currentResponse, ...getTimeslotFields(), ...getFieldsThatRemainSame(), ...getFieldFromEventTypeThatRemainSame(), @@ -341,22 +388,29 @@ const NewRoutingManager = ({ }); } - function handleRebookInNewTab() { - if (!chosenRoute) { - console.error("Chosen route must be there for rerouting"); + function rescheduleSameEventInNewTab() { + if (!chosenEventUrls?.eventBookingAbsoluteUrl) { + console.error("URL must be there for opening new tab"); throw new Error(t("something_went_wrong")); } + rescheduleInNewTab({ + reroutingState, + url: chosenEventUrls.eventBookingAbsoluteUrl, + }); + } - rebookInNewTab({ - responseWithForm, - teamMemberIdsMatchingAttributeLogic, - chosenRoute, - booking, + function rescheduleDifferentEventInNewTab() { + if (!chosenEventUrls?.eventBookingAbsoluteUrl) { + console.error("URL must be there for opening new tab"); + throw new Error(t("something_went_wrong")); + } + rescheduleInNewTab({ reroutingState, + url: chosenEventUrls.eventBookingAbsoluteUrl, }); } - const reroutingPreview = () => { + function reroutingPreview() { if (chosenRoute.action.type === RouteActionType.CustomPageMessage) { return {t("reroute_preview_custom_message")}; } @@ -383,7 +437,7 @@ const NewRoutingManager = ({ {eventTypeSlugToRedirect} @@ -406,27 +460,32 @@ const NewRoutingManager = ({ return hostEmails || t("no_matching_members"); } return null; - }; + } - const reroutingCTAs = () => { + function reroutingCTAs() { + const shouldDisableCTAs = teamMembersMatchingAttributeLogic.isPending || createBookingMutation.isPending; return (
+ {!isReroutingToDifferentEvent ? ( + + ) : ( + + )} -
); - }; + } - const reroutingStatus = () => { + function reroutingStatus() { if (reroutingState.status === ReroutingStatusEnum.REROUTING_IN_PROGRESS) { return (
); } return null; - }; - - return ( -
-

{t("new_route_status")}

-
- {reroutingState.status === ReroutingStatusEnum.REROUTING_NOT_INITIATED && reroutingPreview()} - {reroutingState.status === ReroutingStatusEnum.REROUTING_NOT_INITIATED && reroutingCTAs()} - {reroutingStatus()} -
-
- ); + } }; const CurrentRoutingStatus = ({ @@ -485,7 +526,7 @@ const CurrentRoutingStatus = ({ }) => { const fullSlug = getFullSlugForEvent(booking.eventType); const { t } = useLocale(); - + const bookerUrl = useBookerUrl(); if (!fullSlug) return null; return (
@@ -493,9 +534,14 @@ const CurrentRoutingStatus = ({
{t("event_type")}:{" "} - - {booking.eventType.title} ({fullSlug}) - + + {fullSlug} + {t("organizer")}: {booking.user.email} @@ -522,7 +568,7 @@ const RerouteDialogContentAndFooter = ({ }); const { t } = useLocale(); - + useUpdateIsReroutingQueryParam({ isOpenDialog }); if (isRoutingFormLoading) return ( <> @@ -562,7 +608,9 @@ const RerouteDialogContentAndFooterWithFormResponse = ({ const [responseFromOrganizer, setResponseFromOrganizer] = useState({}); const isResponseFromOrganizerUnpopulated = Object.keys(responseFromOrganizer).length === 0; - const response = isResponseFromOrganizerUnpopulated ? responseWithForm.response : responseFromOrganizer; + const currentResponse = isResponseFromOrganizerUnpopulated + ? responseWithForm.response + : responseFromOrganizer; const [chosenRoute, setChosenRoute] = useState(null); const reroutingState = useReroutingState({ @@ -621,7 +669,7 @@ const RerouteDialogContentAndFooterWithFormResponse = ({ const route = findMatchingRoute({ form, - response: response, + response: currentResponse, }); setChosenRoute(route || null); @@ -630,7 +678,7 @@ const RerouteDialogContentAndFooterWithFormResponse = ({ findTeamMembersMatchingAttributeLogicMutation.mutate({ formId: form.id, - response: response, + response: currentResponse, routeId: route.id, }); } @@ -638,7 +686,13 @@ const RerouteDialogContentAndFooterWithFormResponse = ({ return (
- + + {form.name} + +
{chosenRoute && ( @@ -651,28 +705,51 @@ const RerouteDialogContentAndFooterWithFormResponse = ({ }} reroutingState={reroutingState} setIsOpenDialog={setIsOpenDialog} - responseWithForm={responseWithForm} + form={form} + currentResponse={currentResponse} /> )}
- - - - + {renderFooter()}
); + + function renderFooter() { + if (reroutingState.status !== ReroutingStatusEnum.REROUTING_COMPLETE) { + return ( + + + + + ); + } + + if (reroutingState.status === ReroutingStatusEnum.REROUTING_COMPLETE) { + return ( + + + + ); + } + return null; + } }; export const RerouteDialog = ({ isOpenDialog, setIsOpenDialog, booking }: RerouteDialogProps) => { const { t } = useLocale(); - + const { isRerouting, setQuery: setIsReroutingQuery } = useRouterQuery("isRerouting"); const bookingEventType = booking.eventType; if (!bookingEventType.team) { diff --git a/apps/web/modules/bookings/views/bookings-listing-view.tsx b/apps/web/modules/bookings/views/bookings-listing-view.tsx index 69e4cb87950efb..ed1b4d1449349a 100644 --- a/apps/web/modules/bookings/views/bookings-listing-view.tsx +++ b/apps/web/modules/bookings/views/bookings-listing-view.tsx @@ -20,6 +20,7 @@ import { Alert, Button, EmptyScreen, HorizontalTabs } from "@calcom/ui"; import { useInViewObserver } from "@lib/hooks/useInViewObserver"; import useMeQuery from "@lib/hooks/useMeQuery"; +import useRouterQuery from "@lib/hooks/useRouterQuery"; import BookingListItem from "@components/booking/BookingListItem"; import SkeletonLoader from "@components/booking/SkeletonLoader"; @@ -78,7 +79,7 @@ export default function Bookings() { const { t } = useLocale(); const user = useMeQuery().data; const [isFiltersVisible, setIsFiltersVisible] = useState(false); - + const { isRerouting } = useRouterQuery("isRerouting"); const query = trpc.viewer.bookings.get.useInfiniteQuery( { limit: 10, @@ -88,12 +89,9 @@ export default function Bookings() { }, }, { - // first render has status `undefined` - enabled: true, + // It ensures that a booking that is being rescheduled(and thus cancelled) through a new tab flow, doesn't get removed from the list(removing the Reroute Dialog abruptly which is inside BookingListItem component). + enabled: !isRerouting, getNextPageParam: (lastPage) => lastPage.nextCursor, - - // It ensures that on window focus a booking that was rescheduled(actually re-routed) through a new tab flow, doesn't get removed and thus the Dialog for that booking doesn't automatically get removed/closed. - refetchOnWindowFocus: false, } ); diff --git a/apps/web/public/icons/sprite.svg b/apps/web/public/icons/sprite.svg index 23f1abd825a1fc..e650b0f6c584f0 100644 --- a/apps/web/public/icons/sprite.svg +++ b/apps/web/public/icons/sprite.svg @@ -659,6 +659,15 @@ + + + + + + + + + diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index b583ff78bc10b8..e9b7cde938f506 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2653,6 +2653,6 @@ "reroute_booking_description": "Reroute the booking to different team members", "verify_new_route": "Verify new route", "reroute": "Reroute", - "new_route_status": "New route status", + "new_routing_status": "New routing 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/TODO.md b/packages/app-store/routing-forms/TODO.md index 9304f862031a1d..57510d1a32ef99 100644 --- a/packages/app-store/routing-forms/TODO.md +++ b/packages/app-store/routing-forms/TODO.md @@ -18,7 +18,17 @@ ## TODO - ReRouting - [ ] Need to get teamMemberEmail from Salesforce - [ ] When 'Verify Route' is clicked, it doesn't seem to run the form field logic as there is a warning in console. Fix it. -- [ ] Make sure that without making the FORM dirty, user can't reroute +- [x] Make sure that without making the FORM dirty, user can't reroute +- [ ] Give a proper icon to Reroute button +- [ ] When rescheduling to a new event + - The booking title remains same which will most probably not match the event-type title. This is the behaviour of reschedule. Should we use the new event-type title? +- [ ] What params should we forward to the booking page that we show after rerouting +- [ ] On completing the rerouting just show the new Routing status +- [ ] Fetch bookings on window focus but not when rerouting dialog is open. As soon as it is closed, we could refetch again. +- [ ] We disallow changing the email identifier field as during a reschedule user isn't allowed to change their email currently. Allowing to change that email response would mean that we support changing email during reschedule which might be disabled for some reasons unknown yet. +- [ ] What if there are changes to Form before rerouting but after initial routing. +- [ ] Not able to prefill rescheduleReason due to a bug in useInitialFormValues hook +- [ ] It is becoming a requirement to have BookingAttempt record to allow to store reroutingFormResponses as well as teamMemberIds in it. Other query params could also be stored later on when needed. - To Test - [ ] Hashed link booking diff --git a/packages/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward.ts b/packages/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward.ts index fceb4d674c9941..3ca6067cd4b72c 100644 --- a/packages/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward.ts +++ b/packages/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward.ts @@ -1,9 +1,10 @@ import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import getFieldIdentifier from "../../lib/getFieldIdentifier"; -import type { LocalRoute } from "../../types/types"; +import type { FormResponse, LocalRoute } from "../../types/types"; import type { getServerSideProps } from "./getServerSideProps"; +type FormResponseValueOnly = { [key: string]: { value: FormResponse[keyof FormResponse]["value"] } }; type Props = inferSSRProps; type AttributeRoutingConfig = NonNullable; type GetUrlSearchParamsToForwardOptions = { @@ -21,6 +22,7 @@ type GetUrlSearchParamsToForwardOptions = { formResponseId: number; teamMembersMatchingAttributeLogic: number[] | null; attributeRoutingConfig: AttributeRoutingConfig | null; + reroutingFormResponses?: FormResponseValueOnly; }; export function getUrlSearchParamsToForward({ @@ -30,6 +32,7 @@ export function getUrlSearchParamsToForward({ teamMembersMatchingAttributeLogic, formResponseId, attributeRoutingConfig, + reroutingFormResponses, }: GetUrlSearchParamsToForwardOptions) { type Params = Record; const paramsFromResponse: Params = {}; @@ -86,6 +89,9 @@ export function getUrlSearchParamsToForward({ : null), ["cal.routingFormResponseId"]: String(formResponseId), ...(attributeRoutingConfig?.skipContactOwner ? { ["cal.skipContactOwner"]: "true" } : {}), + ...(reroutingFormResponses + ? { ["cal.reroutingFormResponses"]: JSON.stringify(reroutingFormResponses) } + : null), }; const allQueryURLSearchParams = new URLSearchParams(); @@ -109,8 +115,10 @@ export function getUrlSearchParamsToForwardForReroute({ teamMembersMatchingAttributeLogic, attributeRoutingConfig, rescheduleUid, + reroutingFormResponses, }: GetUrlSearchParamsToForwardOptions & { rescheduleUid: string; + reroutingFormResponses: FormResponseValueOnly; }) { searchParams.set("rescheduleUid", rescheduleUid); searchParams.set("cal.rerouting", "true"); @@ -121,5 +129,6 @@ export function getUrlSearchParamsToForwardForReroute({ searchParams, teamMembersMatchingAttributeLogic, attributeRoutingConfig, + reroutingFormResponses, }); } diff --git a/packages/app-store/routing-forms/trpc/raqbUtils.ts b/packages/app-store/routing-forms/trpc/raqbUtils.ts index 796b0804843933..bac748c9b13cb6 100644 --- a/packages/app-store/routing-forms/trpc/raqbUtils.ts +++ b/packages/app-store/routing-forms/trpc/raqbUtils.ts @@ -36,7 +36,7 @@ function compatibleForAttributeAndFormFieldMatch( ) as T extends string[] ? string[] : string; } -const raqbQueryValueUtils = { +export const raqbQueryValueUtils = { isQueryValueARuleGroup: function isQueryValueARuleGroup(queryValue: JsonTree): queryValue is JsonGroup { return queryValue.type === "group"; }, @@ -77,6 +77,12 @@ const raqbQueryValueUtils = { return raqbFieldValueType; }, + isQueryValueEmpty: function isQueryValueEmpty(queryValue: JsonTree | null): queryValue is null { + if (!queryValue) { + return true; + } + return !queryValue.children1; + }, }; /** diff --git a/packages/app-store/routing-forms/trpc/utils.ts b/packages/app-store/routing-forms/trpc/utils.ts index 1f768395f35feb..46cc64c66f309d 100644 --- a/packages/app-store/routing-forms/trpc/utils.ts +++ b/packages/app-store/routing-forms/trpc/utils.ts @@ -16,7 +16,7 @@ import { import isRouter from "../lib/isRouter"; import type { SerializableField, OrderedResponses } from "../types/types"; import type { FormResponse, SerializableForm } from "../types/types"; -import { acrossQueryValueCompatiblity } from "./raqbUtils"; +import { acrossQueryValueCompatiblity, raqbQueryValueUtils } from "./raqbUtils"; const { getAttributesData: getAttributes, @@ -121,7 +121,7 @@ export async function findTeamMembersMatchingAttributeLogicOfRoute({ getFieldResponse, }); - if (!attributesQueryValue) { + if (raqbQueryValueUtils.isQueryValueEmpty(attributesQueryValue)) { return null; } diff --git a/packages/app-store/routing-forms/zod.ts b/packages/app-store/routing-forms/zod.ts index 2da70439591db9..22f723ef3633f4 100644 --- a/packages/app-store/routing-forms/zod.ts +++ b/packages/app-store/routing-forms/zod.ts @@ -119,7 +119,7 @@ export const appKeysSchema = z.object({}); // TODO: Can we confirm that during the options id support, the response scheme remained same? export const responseInDbSchema = z.record( z.object({ - label: z.string(), + label: z.string().optional(), value: z.union([z.string(), z.number(), z.array(z.string())]), }) ); diff --git a/packages/features/bookings/lib/book-event-form/booking-to-mutation-input-mapper.tsx b/packages/features/bookings/lib/book-event-form/booking-to-mutation-input-mapper.tsx index 554b396716d00a..1673db05c5f288 100644 --- a/packages/features/bookings/lib/book-event-form/booking-to-mutation-input-mapper.tsx +++ b/packages/features/bookings/lib/book-event-form/booking-to-mutation-input-mapper.tsx @@ -48,6 +48,7 @@ export const mapBookingToMutationInput = ({ const routingFormResponseId = routingFormResponseIdParam ? Number(routingFormResponseIdParam) : undefined; const skipContactOwner = searchParams.get("cal.skipContactOwner") === "true"; const isRerouting = searchParams.get("cal.rerouting") === "true"; + const reroutingFormResponses = searchParams.get("cal.reroutingFormResponses"); return { ...values, user: username, @@ -73,6 +74,8 @@ export const mapBookingToMutationInput = ({ routingFormResponseId, skipContactOwner, isRerouting, + // In case of rerouting, the form responses are actually the responses that we need to update. + reroutingFormResponses: reroutingFormResponses ? JSON.parse(reroutingFormResponses) : undefined, }; }; diff --git a/packages/ui/components/icon/icon-list.mjs b/packages/ui/components/icon/icon-list.mjs index e706bcfb449565..42f6e9e226a280 100644 --- a/packages/ui/components/icon/icon-list.mjs +++ b/packages/ui/components/icon/icon-list.mjs @@ -135,4 +135,5 @@ export const lucideIconList = new Set([ "webhook", "x", "zap", + "waypoints" ]); diff --git a/packages/ui/components/icon/icon-names.ts b/packages/ui/components/icon/icon-names.ts index edb4673d8a9ceb..0c86290053ce90 100644 --- a/packages/ui/components/icon/icon-names.ts +++ b/packages/ui/components/icon/icon-names.ts @@ -132,6 +132,7 @@ export type IconName = | "users" | "venetian-mask" | "video" + | "waypoints" | "webhook" | "x" | "zap";