From 89808cb4f6d1ad3a8245162b1dd296a8641914f6 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Thu, 3 Aug 2023 21:55:31 +0530 Subject: [PATCH] fix: Remove Email embed from Routing Form[Stacked PR] (#10389) --- .../eventtype/EventTypeSingleLayout.tsx | 6 +- apps/web/pages/event-types/index.tsx | 8 +- .../playwright/embed-code-generator.e2e.ts | 8 +- .../routing-forms/components/FormActions.tsx | 7 +- .../features/embed}/Embed.tsx | 976 +----------------- packages/features/embed/EventTypeEmbed.tsx | 15 + packages/features/embed/RoutingFormEmbed.tsx | 16 + packages/features/embed/lib/EmbedCodes.tsx | 180 ++++ packages/features/embed/lib/EmbedTabs.tsx | 262 +++++ packages/features/embed/lib/constants.ts | 4 + packages/features/embed/lib/getDimension.tsx | 6 + packages/features/embed/lib/hooks/index.tsx | 324 ++++++ packages/features/embed/types/index.d.ts | 27 + 13 files changed, 900 insertions(+), 939 deletions(-) rename {apps/web/components => packages/features/embed}/Embed.tsx (53%) create mode 100644 packages/features/embed/EventTypeEmbed.tsx create mode 100644 packages/features/embed/RoutingFormEmbed.tsx create mode 100644 packages/features/embed/lib/EmbedCodes.tsx create mode 100644 packages/features/embed/lib/EmbedTabs.tsx create mode 100644 packages/features/embed/lib/constants.ts create mode 100644 packages/features/embed/lib/getDimension.tsx create mode 100644 packages/features/embed/lib/hooks/index.tsx create mode 100644 packages/features/embed/types/index.d.ts diff --git a/apps/web/components/eventtype/EventTypeSingleLayout.tsx b/apps/web/components/eventtype/EventTypeSingleLayout.tsx index 1bbd8bf0b4..da9015e7d4 100644 --- a/apps/web/components/eventtype/EventTypeSingleLayout.tsx +++ b/apps/web/components/eventtype/EventTypeSingleLayout.tsx @@ -8,6 +8,7 @@ import type { UseFormReturn } from "react-hook-form"; import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; +import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed"; import Shell from "@calcom/features/shell/Shell"; import { classNames } from "@calcom/lib"; import { CAL_URL } from "@calcom/lib/constants"; @@ -51,7 +52,6 @@ import { Loader, } from "@calcom/ui/components/icon"; -import { EmbedButton, EmbedDialog } from "@components/Embed"; import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab"; type Props = { @@ -310,7 +310,7 @@ function EventTypeSingleLayout({ showToast("Link copied!", "success"); }} /> - - + ); } diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index dedc08d7a2..6b6bad7450 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -9,6 +9,7 @@ import { z } from "zod"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; import useIntercom from "@calcom/features/ee/support/lib/intercom/useIntercom"; +import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed"; import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components"; import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog"; import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog"; @@ -70,7 +71,6 @@ import { import useMeQuery from "@lib/hooks/useMeQuery"; -import { EmbedButton, EmbedDialog } from "@components/Embed"; import PageWrapper from "@components/PageWrapper"; import SkeletonLoader from "@components/eventtype/SkeletonLoader"; @@ -507,7 +507,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL )} {!isManagedEventType && ( - {t("embed")} - + )} {/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */} @@ -892,7 +892,7 @@ const Main = ({ ) )} {data.eventTypeGroups.length === 0 && } - + {searchParams?.get("dialog") === "duplicate" && } ); diff --git a/apps/web/playwright/embed-code-generator.e2e.ts b/apps/web/playwright/embed-code-generator.e2e.ts index 6988073cf3..f49b853259 100644 --- a/apps/web/playwright/embed-code-generator.e2e.ts +++ b/apps/web/playwright/embed-code-generator.e2e.ts @@ -1,6 +1,8 @@ import type { Page } from "@playwright/test"; import { expect } from "@playwright/test"; +import { EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants"; + import { test } from "./lib/fixtures"; function chooseEmbedType(page: Page, embedType: string) { @@ -81,12 +83,16 @@ async function expectToContainValidCode(page: Page, { embedType }: { embedType: }; } +/** + * Let's just check if iframe is opened with preview.html. preview.html tests are responsibility of embed-core + */ async function expectToContainValidPreviewIframe( page: Page, { embedType, calLink }: { embedType: string; calLink: string } ) { + const bookerUrl = `${WEBAPP_URL}`; expect(await page.locator("[data-testid=embed-preview]").getAttribute("src")).toContain( - `/preview.html?embedType=${embedType}&calLink=${calLink}` + `/preview.html?embedType=${embedType}&calLink=${calLink}&embedLibUrl=${EMBED_LIB_URL}&bookerUrl=${bookerUrl}` ); } diff --git a/packages/app-store/routing-forms/components/FormActions.tsx b/packages/app-store/routing-forms/components/FormActions.tsx index f12433ed79..17a2932d1c 100644 --- a/packages/app-store/routing-forms/components/FormActions.tsx +++ b/packages/app-store/routing-forms/components/FormActions.tsx @@ -6,6 +6,7 @@ import { v4 as uuidv4 } from "uuid"; import { z } from "zod"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; +import { RoutingFormEmbedButton, RoutingFormEmbedDialog } from "@calcom/features/embed/RoutingFormEmbed"; import { classNames } from "@calcom/lib"; import { CAL_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -32,8 +33,6 @@ import { } from "@calcom/ui"; import { MoreHorizontal } from "@calcom/ui/components/icon"; -import { EmbedButton, EmbedDialog } from "@components/Embed"; - import getFieldIdentifier from "../lib/getFieldIdentifier"; import type { SerializableForm } from "../types/types"; @@ -230,7 +229,7 @@ function Dialogs({ }); return (
- + openModal({ action: "duplicate", target: routingForm?.id }), }, embed: { - as: EmbedButton, + as: RoutingFormEmbedButton, // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore embedUrl: embedLink, diff --git a/apps/web/components/Embed.tsx b/packages/features/embed/Embed.tsx similarity index 53% rename from apps/web/components/Embed.tsx rename to packages/features/embed/Embed.tsx index f906aed757..b91106302e 100644 --- a/apps/web/components/Embed.tsx +++ b/packages/features/embed/Embed.tsx @@ -1,10 +1,9 @@ import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible"; import classNames from "classnames"; import { useSession } from "next-auth/react"; -import type { TFunction } from "next-i18next"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import type { MutableRefObject, RefObject } from "react"; -import { createRef, forwardRef, useRef, useState } from "react"; +import type { RefObject } from "react"; +import { createRef, useRef, useState } from "react"; import type { ControlProps } from "react-select"; import { components } from "react-select"; import { shallow } from "zustand/shallow"; @@ -13,16 +12,14 @@ import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; import { AvailableTimes } from "@calcom/features/bookings"; import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store"; -import type { BookerLayout } from "@calcom/features/bookings/Booker/types"; import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event"; import { useTimePreferences } from "@calcom/features/bookings/lib/timePreferences"; import DatePicker from "@calcom/features/calendars/DatePicker"; import { useFlagMap } from "@calcom/features/flags/context/provider"; import { useNonEmptyScheduleDays } from "@calcom/features/schedules"; import { useSlotsForDate } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate"; -import { APP_NAME, CAL_URL, EMBED_LIB_URL, IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants"; +import { APP_NAME, CAL_URL } from "@calcom/lib/constants"; import { weekdayToWeekIndex } from "@calcom/lib/date-fns"; -import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { BookerLayouts } from "@calcom/prisma/zod-utils"; import type { RouterOutputs } from "@calcom/trpc/react"; @@ -39,15 +36,15 @@ import { Select, showToast, Switch, - TextArea, TextField, TimezoneSelect, } from "@calcom/ui"; -import { ArrowDown, ArrowLeft, ArrowUp, Code, Sun, Trello } from "@calcom/ui/components/icon"; +import { ArrowDown, ArrowLeft, ArrowUp, Sun } from "@calcom/ui/components/icon"; + +import { getDimension } from "./lib/getDimension"; +import type { EmbedTabs, EmbedType, EmbedTypes, PreviewState } from "./types"; type EventType = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"] | undefined; -type EmbedType = "inline" | "floating-popup" | "element-click" | "email"; -type EmbedFramework = "react" | "HTML"; const enum Theme { auto = "auto", @@ -55,43 +52,8 @@ const enum Theme { dark = "dark", } -// Preview HTML and embed lib doesn't need org context. They are static per instance -const EMBED_PREVIEW_HTML_URL = `${WEBAPP_URL}/embed/preview.html`; -const embedLibUrl = EMBED_LIB_URL; - -const useEmbedCalOrigin = () => { - const bookerUrl = useBookerUrl(); - return bookerUrl; -}; - -type PreviewState = { - inline: { - width: string; - height: string; - }; - theme: Theme; - floatingPopup: { - config?: { - layout: BookerLayouts; - }; - [key: string]: string | boolean | undefined | Record; - }; - elementClick: Record; - palette: { - brandColor: string; - }; - hideEventTypeDetails: boolean; - layout: BookerLayouts; -}; const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl", "eventId"]; -const getDimension = (dimension: string) => { - if (dimension.match(/^\d+$/)) { - dimension = `${dimension}%`; - } - return dimension; -}; - function useRouterHelpers() { const router = useRouter(); const searchParams = useSearchParams(); @@ -102,6 +64,7 @@ function useRouterHelpers() { Object.keys(newSearchParams).forEach((key) => { newQuery.set(key, newSearchParams[key]); }); + router.push(`${pathname}?${newQuery.toString()}`); }; @@ -124,867 +87,7 @@ const getQueryParam = (queryParam: string) => { return params.get(queryParam); }; -/** - * It allows us to show code with certain reusable blocks indented according to the block variable placement - * So, if you add a variable ${abc} with indentation of 4 spaces, it will automatically indent all newlines in `abc` with the same indent before constructing the final string - * `A${var}C` with var = "B" -> partsWithoutBlock=['A','C'] blocksOrVariables=['B'] - */ -const code = (partsWithoutBlock: TemplateStringsArray, ...blocksOrVariables: string[]) => { - const constructedCode: string[] = []; - for (let i = 0; i < partsWithoutBlock.length; i++) { - const partWithoutBlock = partsWithoutBlock[i]; - // blocksOrVariables length would always be 1 less than partsWithoutBlock - // So, last item should be concatenated as is. - if (i >= blocksOrVariables.length) { - constructedCode.push(partWithoutBlock); - continue; - } - const block = blocksOrVariables[i]; - const indentedBlock: string[] = []; - let indent = ""; - block.split("\n").forEach((line) => { - indentedBlock.push(line); - }); - // non-null assertion is okay because we know that we are referencing last element. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const indentationMatch = partWithoutBlock - .split("\n") - .at(-1)! - .match(/(^[\t ]*).*$/); - if (indentationMatch) { - indent = indentationMatch[1]; - } - constructedCode.push(partWithoutBlock + indentedBlock.join("\n" + indent)); - } - return constructedCode.join(""); -}; - -const getInstructionString = ({ - apiName, - instructionName, - instructionArg, -}: { - apiName: string; - instructionName: string; - instructionArg: Record; -}) => { - return `${apiName}("${instructionName}", ${JSON.stringify(instructionArg)});`; -}; - -const getEmbedUIInstructionString = ({ - apiName, - theme, - brandColor, - hideEventTypeDetails, - layout, -}: { - apiName: string; - theme?: string; - brandColor: string; - hideEventTypeDetails: boolean; - layout?: string; -}) => { - theme = theme !== "auto" ? theme : undefined; - return getInstructionString({ - apiName, - instructionName: "ui", - instructionArg: { - theme, - styles: { - branding: { - brandColor, - }, - }, - hideEventTypeDetails: hideEventTypeDetails, - layout, - }, - }); -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const Codes = { - react: { - inline: ({ - calLink, - uiInstructionCode, - previewState, - embedCalOrigin, - }: { - calLink: string; - uiInstructionCode: string; - previewState: PreviewState; - embedCalOrigin: string; - }) => { - const width = getDimension(previewState.inline.width); - const height = getDimension(previewState.inline.height); - return code` -import Cal, { getCalApi } from "@calcom/embed-react"; -import { useEffect } from "react"; -export default function MyApp() { - useEffect(()=>{ - (async function () { - const cal = await getCalApi(); - ${uiInstructionCode} - })(); - }, []) - return ; -};`; - }, - "floating-popup": ({ - floatingButtonArg, - uiInstructionCode, - }: { - floatingButtonArg: string; - uiInstructionCode: string; - }) => { - return code` -import { getCalApi } from "@calcom/embed-react"; -import { useEffect } from "react"; -export default function App() { - useEffect(()=>{ - (async function () { - const cal = await getCalApi(${IS_SELF_HOSTED ? `"${embedLibUrl}"` : ""}); - cal("floatingButton", ${floatingButtonArg}); - ${uiInstructionCode} - })(); - }, []) -};`; - }, - "element-click": ({ - calLink, - uiInstructionCode, - previewState, - embedCalOrigin, - }: { - calLink: string; - uiInstructionCode: string; - previewState: PreviewState; - embedCalOrigin: string; - }) => { - return code` -import { getCalApi } from "@calcom/embed-react"; -import { useEffect } from "react"; -export default function App() { - useEffect(()=>{ - (async function () { - const cal = await getCalApi(${IS_SELF_HOSTED ? `"${embedLibUrl}"` : ""}); - ${uiInstructionCode} - })(); - }, []) - return ; -};`; - }, - }, - HTML: { - inline: ({ - calLink, - uiInstructionCode, - previewState, - }: { - calLink: string; - uiInstructionCode: string; - previewState: PreviewState; - }) => { - return code`Cal("inline", { - elementOrSelector:"#my-cal-inline", - calLink: "${calLink}", - layout: "${previewState.layout}" -}); - -${uiInstructionCode}`; - }, - - "floating-popup": ({ - floatingButtonArg, - uiInstructionCode, - }: { - floatingButtonArg: string; - uiInstructionCode: string; - }) => { - return code`Cal("floatingButton", ${floatingButtonArg}); -${uiInstructionCode}`; - }, - "element-click": ({ - calLink, - uiInstructionCode, - previewState, - }: { - calLink: string; - uiInstructionCode: string; - previewState: PreviewState; - }) => { - return code` -// Important: Please add following attributes to the element you want to open Cal on click -// \`data-cal-link="${calLink}"\` -// \`data-cal-config='${JSON.stringify({ - layout: previewState.layout, - })}'\` - -${uiInstructionCode}`; - }, - }, -} satisfies Record string>>; - -type EmbedCommonProps = { embedType: EmbedType; calLink: string; previewState: PreviewState }; - -const getEmbedTypeSpecificString = ({ - embedFramework, - embedType, - calLink, - previewState, - embedCalOrigin, -}: { - embedFramework: EmbedFramework; - embedCalOrigin: string; -} & EmbedCommonProps) => { - const frameworkCodes = Codes[embedFramework]; - if (!frameworkCodes) { - throw new Error(`No code available for the framework:${embedFramework}`); - } - if (embedType === "email") return ""; - let uiInstructionStringArg: { - apiName: string; - theme: PreviewState["theme"]; - brandColor: string; - hideEventTypeDetails: boolean; - layout?: BookerLayout; - }; - if (embedFramework === "react") { - uiInstructionStringArg = { - apiName: "cal", - theme: previewState.theme, - brandColor: previewState.palette.brandColor, - hideEventTypeDetails: previewState.hideEventTypeDetails, - layout: previewState.layout, - }; - } else { - uiInstructionStringArg = { - apiName: "Cal", - theme: previewState.theme, - brandColor: previewState.palette.brandColor, - hideEventTypeDetails: previewState.hideEventTypeDetails, - layout: previewState.layout, - }; - } - if (!frameworkCodes[embedType]) { - throw new Error(`Code not available for framework:${embedFramework} and embedType:${embedType}`); - } - if (embedType === "inline") { - return frameworkCodes[embedType]({ - calLink, - uiInstructionCode: getEmbedUIInstructionString(uiInstructionStringArg), - previewState, - embedCalOrigin, - }); - } else if (embedType === "floating-popup") { - const floatingButtonArg = { - calLink, - ...(IS_SELF_HOSTED ? { calOrigin: embedCalOrigin } : null), - ...previewState.floatingPopup, - }; - return frameworkCodes[embedType]({ - floatingButtonArg: JSON.stringify(floatingButtonArg), - uiInstructionCode: getEmbedUIInstructionString(uiInstructionStringArg), - }); - } else if (embedType === "element-click") { - return frameworkCodes[embedType]({ - calLink, - uiInstructionCode: getEmbedUIInstructionString(uiInstructionStringArg), - previewState, - embedCalOrigin, - }); - } - return ""; -}; - -const embeds = (t: TFunction) => - (() => { - return [ - { - title: t("inline_embed"), - subtitle: t("load_inline_content"), - type: "inline", - illustration: ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ), - }, - { - title: t("floating_pop_up_button"), - subtitle: t("floating_button_trigger_modal"), - type: "floating-popup", - illustration: ( - - - - - - - - - - - ), - }, - { - title: t("pop_up_element_click"), - subtitle: t("open_dialog_with_element_click"), - type: "element-click", - illustration: ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ), - }, - { - title: t("email_embed"), - subtitle: t("add_times_to_your_email"), - type: "email", - illustration: ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ), - }, - ]; - })(); - -const tabs = [ - { - name: "HTML", - href: "embedTabName=embed-code", - icon: Code, - type: "code", - Component: forwardRef( - function EmbedHtml({ embedType, calLink, previewState }, ref) { - const { t } = useLocale(); - const embedSnippetString = useGetEmbedSnippetString(); - const embedCalOrigin = useEmbedCalOrigin(); - if (ref instanceof Function || !ref) { - return null; - } - if (ref.current && !(ref.current instanceof HTMLTextAreaElement)) { - return null; - } - return ( - <> -
- - {t("place_where_cal_widget_appear", { appName: APP_NAME })} - -
-