Embedded Routing Forms - Part1 (#3530)
* Support dark theme in routing form * Fix Embed detection * Add routing form embed example * Better rendering support in dark mode for react-select * Fix more theme issues * Added test for Routing Form Embed Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
parent
e871781079
commit
1be5510c5e
@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from "react";
|
|||||||
import ReactSelect, { components, GroupBase, Props, InputProps, SingleValue, MultiValue } from "react-select";
|
import ReactSelect, { components, GroupBase, Props, InputProps, SingleValue, MultiValue } from "react-select";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
import useTheme from "@lib/hooks/useTheme";
|
||||||
|
|
||||||
export type SelectProps<
|
export type SelectProps<
|
||||||
Option,
|
Option,
|
||||||
@ -27,6 +28,54 @@ function Select<
|
|||||||
IsMulti extends boolean = false,
|
IsMulti extends boolean = false,
|
||||||
Group extends GroupBase<Option> = GroupBase<Option>
|
Group extends GroupBase<Option> = GroupBase<Option>
|
||||||
>({ className, ...props }: SelectProps<Option, IsMulti, Group>) {
|
>({ className, ...props }: SelectProps<Option, IsMulti, Group>) {
|
||||||
|
const { resolvedTheme, forcedTheme, isReady } = useTheme();
|
||||||
|
const hasDarkTheme = !forcedTheme && resolvedTheme === "dark";
|
||||||
|
const darkThemeColors = {
|
||||||
|
/** Dark Theme starts */
|
||||||
|
//primary - Border when selected and Selected Option background
|
||||||
|
primary: "rgb(41 41 41 / var(--tw-border-opacity))",
|
||||||
|
|
||||||
|
neutral0: "rgb(62 62 62 / var(--tw-bg-opacity))",
|
||||||
|
// Down Arrow hover color
|
||||||
|
neutral5: "white",
|
||||||
|
|
||||||
|
neutral10: "rgb(41 41 41 / var(--tw-border-opacity))",
|
||||||
|
|
||||||
|
// neutral20 - border color + down arrow default color
|
||||||
|
neutral20: "rgb(41 41 41 / var(--tw-border-opacity))",
|
||||||
|
|
||||||
|
// neutral30 - hover border color
|
||||||
|
neutral30: "rgb(41 41 41 / var(--tw-border-opacity))",
|
||||||
|
|
||||||
|
neutral40: "white",
|
||||||
|
|
||||||
|
danger: "white",
|
||||||
|
|
||||||
|
// Cross button in multiselect
|
||||||
|
dangerLight: "rgb(41 41 41 / var(--tw-border-opacity))",
|
||||||
|
|
||||||
|
// neutral50 - MultiSelect - "Select Text" color
|
||||||
|
neutral50: "white",
|
||||||
|
|
||||||
|
// neutral60 - Down Arrow color
|
||||||
|
neutral60: "white",
|
||||||
|
|
||||||
|
neutral70: "red",
|
||||||
|
|
||||||
|
// neutral80 - Selected option
|
||||||
|
neutral80: "white",
|
||||||
|
|
||||||
|
neutral90: "blue",
|
||||||
|
|
||||||
|
primary50: "rgba(209 , 213, 219, var(--tw-bg-opacity))",
|
||||||
|
primary25: "rgba(244, 245, 246, var(--tw-bg-opacity))",
|
||||||
|
/** Dark Theme ends */
|
||||||
|
};
|
||||||
|
|
||||||
|
// Till we know in JS the theme is ready, we can't render react-select as it would render with light theme instead
|
||||||
|
if (!isReady) {
|
||||||
|
return <input type="text" className={className} />;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<ReactSelect
|
<ReactSelect
|
||||||
theme={(theme) => ({
|
theme={(theme) => ({
|
||||||
@ -34,10 +83,16 @@ function Select<
|
|||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
colors: {
|
colors: {
|
||||||
...theme.colors,
|
...theme.colors,
|
||||||
primary: "var(--brand-color)",
|
...(hasDarkTheme
|
||||||
|
? darkThemeColors
|
||||||
|
: {
|
||||||
|
/** Light Theme starts */
|
||||||
|
primary: "var(--brand-color)",
|
||||||
|
|
||||||
primary50: "rgba(209 , 213, 219, var(--tw-bg-opacity))",
|
primary50: "rgba(209 , 213, 219, var(--tw-bg-opacity))",
|
||||||
primary25: "rgba(244, 245, 246, var(--tw-bg-opacity))",
|
primary25: "rgba(244, 245, 246, var(--tw-bg-opacity))",
|
||||||
|
/** Light Theme Ends */
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
styles={{
|
styles={{
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from "next-auth/react";
|
||||||
import { appWithTranslation } from "next-i18next";
|
import { appWithTranslation } from "next-i18next";
|
||||||
import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
|
import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
|
||||||
|
import { NextRouter } from "next/router";
|
||||||
import { ComponentProps, ReactNode } from "react";
|
import { ComponentProps, ReactNode } from "react";
|
||||||
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@ -15,7 +16,10 @@ const I18nextAdapter = appWithTranslation<NextJsAppProps & { children: React.Rea
|
|||||||
|
|
||||||
// Workaround for https://github.com/vercel/next.js/issues/8592
|
// Workaround for https://github.com/vercel/next.js/issues/8592
|
||||||
export type AppProps = Omit<NextAppProps, "Component"> & {
|
export type AppProps = Omit<NextAppProps, "Component"> & {
|
||||||
Component: NextAppProps["Component"] & { requiresLicense?: boolean; isThemeSupported?: boolean };
|
Component: NextAppProps["Component"] & {
|
||||||
|
requiresLicense?: boolean;
|
||||||
|
isThemeSupported?: boolean | ((arg: { router: NextRouter }) => boolean);
|
||||||
|
};
|
||||||
/** Will be defined only is there was an error */
|
/** Will be defined only is there was an error */
|
||||||
err?: Error;
|
err?: Error;
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useTheme as useNextTheme } from "next-themes";
|
import { useTheme as useNextTheme } from "next-themes";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useEmbedTheme } from "@calcom/embed-core/embed-iframe";
|
import { useEmbedTheme } from "@calcom/embed-core/embed-iframe";
|
||||||
import { Maybe } from "@calcom/trpc/server";
|
import { Maybe } from "@calcom/trpc/server";
|
||||||
@ -7,7 +7,8 @@ import { Maybe } from "@calcom/trpc/server";
|
|||||||
// makes sure the ui doesn't flash
|
// makes sure the ui doesn't flash
|
||||||
export default function useTheme(theme?: Maybe<string>) {
|
export default function useTheme(theme?: Maybe<string>) {
|
||||||
theme = theme || "system";
|
theme = theme || "system";
|
||||||
const { theme: currentTheme, setTheme } = useNextTheme();
|
const { resolvedTheme, setTheme, forcedTheme } = useNextTheme();
|
||||||
|
const [isReady, setIsReady] = useState<boolean>(false);
|
||||||
const embedTheme = useEmbedTheme();
|
const embedTheme = useEmbedTheme();
|
||||||
// Embed UI configuration takes more precedence over App Configuration
|
// Embed UI configuration takes more precedence over App Configuration
|
||||||
theme = embedTheme || theme;
|
theme = embedTheme || theme;
|
||||||
@ -16,10 +17,13 @@ export default function useTheme(theme?: Maybe<string>) {
|
|||||||
if (theme) {
|
if (theme) {
|
||||||
setTheme(theme);
|
setTheme(theme);
|
||||||
}
|
}
|
||||||
|
setIsReady(true);
|
||||||
}, [theme, setTheme]);
|
}, [theme, setTheme]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentTheme,
|
resolvedTheme,
|
||||||
setTheme,
|
setTheme,
|
||||||
|
isReady,
|
||||||
|
forcedTheme,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@ import LicenseRequired from "@ee/components/LicenseRequired";
|
|||||||
|
|
||||||
import AppProviders, { AppProps } from "@lib/app-providers";
|
import AppProviders, { AppProps } from "@lib/app-providers";
|
||||||
import { seoConfig } from "@lib/config/next-seo.config";
|
import { seoConfig } from "@lib/config/next-seo.config";
|
||||||
import useTheme from "@lib/hooks/useTheme";
|
|
||||||
|
|
||||||
import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
||||||
|
|
||||||
@ -28,13 +27,28 @@ import "../styles/globals.css";
|
|||||||
function MyApp(props: AppProps) {
|
function MyApp(props: AppProps) {
|
||||||
const { Component, pageProps, err, router } = props;
|
const { Component, pageProps, err, router } = props;
|
||||||
let pageStatus = "200";
|
let pageStatus = "200";
|
||||||
|
|
||||||
if (router.pathname === "/404") {
|
if (router.pathname === "/404") {
|
||||||
pageStatus = "404";
|
pageStatus = "404";
|
||||||
} else if (router.pathname === "/500") {
|
} else if (router.pathname === "/500") {
|
||||||
pageStatus = "500";
|
pageStatus = "500";
|
||||||
}
|
}
|
||||||
const forcedTheme = Component.isThemeSupported ? undefined : "light";
|
|
||||||
|
let isThemeSupported = null;
|
||||||
|
|
||||||
|
if (typeof Component.isThemeSupported === "function") {
|
||||||
|
isThemeSupported = Component.isThemeSupported({ router });
|
||||||
|
} else {
|
||||||
|
isThemeSupported = Component.isThemeSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
const forcedTheme = isThemeSupported ? undefined : "light";
|
||||||
|
|
||||||
|
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
|
||||||
|
// One such example is our Embeds Demo and Testing page at http://localhost:3100
|
||||||
|
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
|
||||||
|
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
|
||||||
|
const storageKey = typeof embedNamespace === "string" ? `embed-theme-${embedNamespace}` : "theme";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
|
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
|
||||||
<ContractsProvider>
|
<ContractsProvider>
|
||||||
@ -46,7 +60,11 @@ function MyApp(props: AppProps) {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||||
</Head>
|
</Head>
|
||||||
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
|
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
|
||||||
<ThemeProvider enableColorScheme={false} forcedTheme={forcedTheme} attribute="class">
|
<ThemeProvider
|
||||||
|
enableColorScheme={false}
|
||||||
|
storageKey={storageKey}
|
||||||
|
forcedTheme={forcedTheme}
|
||||||
|
attribute="class">
|
||||||
{Component.requiresLicense ? (
|
{Component.requiresLicense ? (
|
||||||
<LicenseRequired>
|
<LicenseRequired>
|
||||||
<Component {...pageProps} err={err} />
|
<Component {...pageProps} err={err} />
|
||||||
|
@ -2,6 +2,24 @@ import Document, { DocumentContext, Head, Html, Main, NextScript, DocumentProps
|
|||||||
|
|
||||||
type Props = Record<string, unknown> & DocumentProps;
|
type Props = Record<string, unknown> & DocumentProps;
|
||||||
|
|
||||||
|
function toRunBeforeReactOnClient() {
|
||||||
|
window.sessionStorage.setItem("calEmbedMode", String(location.search.includes("embed=")));
|
||||||
|
window.isEmbed = () => {
|
||||||
|
return window.sessionStorage.getItem("calEmbedMode") === "true";
|
||||||
|
};
|
||||||
|
|
||||||
|
window.getEmbedTheme = () => {
|
||||||
|
const url = new URL(document.URL);
|
||||||
|
return url.searchParams.get("theme") as "dark" | "light";
|
||||||
|
};
|
||||||
|
|
||||||
|
window.getEmbedNamespace = () => {
|
||||||
|
const url = new URL(document.URL);
|
||||||
|
const namespace = url.searchParams.get("embed");
|
||||||
|
return namespace;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class MyDocument extends Document<Props> {
|
class MyDocument extends Document<Props> {
|
||||||
static async getInitialProps(ctx: DocumentContext) {
|
static async getInitialProps(ctx: DocumentContext) {
|
||||||
const initialProps = await Document.getInitialProps(ctx);
|
const initialProps = await Document.getInitialProps(ctx);
|
||||||
@ -31,13 +49,12 @@ class MyDocument extends Document<Props> {
|
|||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
/>
|
/>
|
||||||
<link rel="preload" href="/fonts/cal.ttf" as="font" type="font/ttf" crossOrigin="anonymous" />
|
<link rel="preload" href="/fonts/cal.ttf" as="font" type="font/ttf" crossOrigin="anonymous" />
|
||||||
{/* Define isEmbed here so that it can be shared with App(embed-iframe) as well as the following code to change background and hide body */}
|
{/* Define isEmbed here so that it can be shared with App(embed-iframe) as well as the following code to change background and hide body
|
||||||
|
Persist the embed mode in sessionStorage because query param might get lost during browsing.
|
||||||
|
*/}
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `
|
__html: `(${toRunBeforeReactOnClient.toString()})()`,
|
||||||
window.isEmbed = ()=> {
|
|
||||||
return location.search.includes("embed=")
|
|
||||||
}`,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { NextRouter, useRouter } from "next/router";
|
||||||
|
|
||||||
import RoutingFormsRoutingConfig from "@calcom/app-store/ee/routing_forms/pages/app-routing.config";
|
import RoutingFormsRoutingConfig from "@calcom/app-store/ee/routing_forms/pages/app-routing.config";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
@ -31,7 +31,7 @@ function getRoute(appName: string, pages: string[]) {
|
|||||||
notFound: false;
|
notFound: false;
|
||||||
// A component than can accept any properties
|
// A component than can accept any properties
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
Component: (props: any) => JSX.Element;
|
Component: ((props: any) => JSX.Element) & { isThemeSupported?: boolean };
|
||||||
getServerSideProps: AppGetServerSideProps;
|
getServerSideProps: AppGetServerSideProps;
|
||||||
};
|
};
|
||||||
if (!appPage) {
|
if (!appPage) {
|
||||||
@ -59,7 +59,13 @@ export default function AppPage(props: inferSSRProps<typeof getServerSideProps>)
|
|||||||
return <route.Component {...componentProps} />;
|
return <route.Component {...componentProps} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
AppPage.isThemeSupported = true;
|
AppPage.isThemeSupported = ({ router }: { router: NextRouter }) => {
|
||||||
|
const route = getRoute(router.query.slug as string, router.query.pages as string[]);
|
||||||
|
if (route.notFound) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return route.Component.isThemeSupported;
|
||||||
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
context: GetServerSidePropsContext<{
|
context: GetServerSidePropsContext<{
|
||||||
|
@ -52,7 +52,7 @@ const TextWidget = (props: TextWidgetProps & { type?: string }) => {
|
|||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className="flex flex-grow border-gray-300 text-sm"
|
className="flex flex-grow border-gray-300 text-sm dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500"
|
||||||
value={textValue}
|
value={textValue}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
@ -68,7 +68,7 @@ function NumberWidget({ value, setValue, ...remainingProps }: NumberWidgetProps)
|
|||||||
<Input
|
<Input
|
||||||
name="query-builder"
|
name="query-builder"
|
||||||
type="number"
|
type="number"
|
||||||
className="mt-0"
|
className="mt-0 border-gray-300 text-sm dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setValue(e.target.value);
|
setValue(e.target.value);
|
||||||
@ -102,7 +102,7 @@ const MultiSelectWidget = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
className="block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 sm:text-sm"
|
className="block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500 sm:text-sm"
|
||||||
menuPosition="fixed"
|
menuPosition="fixed"
|
||||||
onChange={(items) => {
|
onChange={(items) => {
|
||||||
setValue(items?.map((item) => item.value));
|
setValue(items?.map((item) => item.value));
|
||||||
@ -136,7 +136,7 @@ function SelectWidget({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
className="data-testid-select block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 sm:text-sm"
|
className="data-testid-select block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500 sm:text-sm"
|
||||||
menuPosition="fixed"
|
menuPosition="fixed"
|
||||||
onChange={(item) => {
|
onChange={(item) => {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
|
@ -6,12 +6,17 @@ import { Utils as QbUtils } from "react-awesome-query-builder";
|
|||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
import showToast from "@calcom/lib/notification";
|
import showToast from "@calcom/lib/notification";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { AppGetServerSidePropsContext, AppPrisma } from "@calcom/types/AppGetServerSideProps";
|
import { AppGetServerSidePropsContext, AppPrisma } from "@calcom/types/AppGetServerSideProps";
|
||||||
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button } from "@calcom/ui";
|
||||||
|
|
||||||
|
import useTheme from "@lib/hooks/useTheme";
|
||||||
|
|
||||||
import { getSerializableForm } from "../../utils";
|
import { getSerializableForm } from "../../utils";
|
||||||
import { getQueryBuilderConfig } from "../route-builder/[...appPages]";
|
import { getQueryBuilderConfig } from "../route-builder/[...appPages]";
|
||||||
|
|
||||||
@ -27,9 +32,11 @@ type Form = inferSSRProps<typeof getServerSideProps>["form"];
|
|||||||
|
|
||||||
type Route = NonNullable<Form["routes"]>[0];
|
type Route = NonNullable<Form["routes"]>[0];
|
||||||
|
|
||||||
function RoutingForm({ form }: inferSSRProps<typeof getServerSideProps>) {
|
function RoutingForm({ form, profile }: inferSSRProps<typeof getServerSideProps>) {
|
||||||
const [customPageMessage, setCustomPageMessage] = useState<Route["action"]["value"]>("");
|
const [customPageMessage, setCustomPageMessage] = useState<Route["action"]["value"]>("");
|
||||||
const formFillerIdRef = useRef(uuidv4());
|
const formFillerIdRef = useRef(uuidv4());
|
||||||
|
const isEmbed = useIsEmbed();
|
||||||
|
useTheme(profile.theme);
|
||||||
|
|
||||||
// TODO: We might want to prevent spam from a single user by having same formFillerId across pageviews
|
// TODO: We might want to prevent spam from a single user by having same formFillerId across pageviews
|
||||||
// But technically, a user can fill form multiple times due to any number of reasons and we currently can't differentiate b/w that.
|
// But technically, a user can fill form multiple times due to any number of reasons and we currently can't differentiate b/w that.
|
||||||
@ -71,7 +78,7 @@ function RoutingForm({ form }: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
} else if (decidedAction.type === "externalRedirectUrl") {
|
} else if (decidedAction.type === "externalRedirectUrl") {
|
||||||
window.location.href = decidedAction.value;
|
window.location.href = decidedAction.value;
|
||||||
}
|
}
|
||||||
showToast("Form submitted successfully! Redirecting now ...", "success");
|
// showToast("Form submitted successfully! Redirecting now ...", "success");
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
if (e?.message) {
|
if (e?.message) {
|
||||||
@ -80,7 +87,7 @@ function RoutingForm({ form }: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
if (e?.data?.code === "CONFLICT") {
|
if (e?.data?.code === "CONFLICT") {
|
||||||
return void showToast("Form already submitted", "error");
|
return void showToast("Form already submitted", "error");
|
||||||
}
|
}
|
||||||
showToast("Something went wrong", "error");
|
// showToast("Something went wrong", "error");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -93,94 +100,104 @@ function RoutingForm({ form }: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
onSubmit(response);
|
onSubmit(response);
|
||||||
};
|
};
|
||||||
|
|
||||||
return !customPageMessage ? (
|
return (
|
||||||
<>
|
<div>
|
||||||
<Head>
|
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
|
||||||
<title>{form.name} | Cal.com Forms</title>
|
|
||||||
</Head>
|
|
||||||
<div className="mx-auto my-0 max-w-3xl md:my-24">
|
|
||||||
<div className="w-full max-w-4xl ltr:mr-2 rtl:ml-2">
|
|
||||||
<div className="mx-0 rounded-sm border border-neutral-200 bg-white p-4 py-6 sm:-mx-4 sm:px-8">
|
|
||||||
<Toaster position="bottom-right" />
|
|
||||||
|
|
||||||
<form onSubmit={handleOnSubmit}>
|
<div>
|
||||||
<div className="mb-8">
|
{!customPageMessage ? (
|
||||||
<h1 className="font-cal mb-1 text-xl font-bold capitalize tracking-wide text-gray-900">
|
<>
|
||||||
{form.name}
|
<Head>
|
||||||
</h1>
|
<title>{form.name} | Cal.com Forms</title>
|
||||||
{form.description ? (
|
</Head>
|
||||||
<p className="min-h-10 text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{form.description}</p>
|
<div className={classNames("mx-auto my-0 max-w-3xl", isEmbed ? "" : "md:my-24")}>
|
||||||
) : null}
|
<div className="w-full max-w-4xl ltr:mr-2 rtl:ml-2">
|
||||||
</div>
|
<div className="main border-bookinglightest mx-0 rounded-md bg-white p-4 py-6 dark:bg-gray-800 sm:-mx-4 sm:px-8 sm:dark:border-gray-600 md:border">
|
||||||
{form.fields?.map((field) => {
|
<Toaster position="bottom-right" />
|
||||||
const widget = queryBuilderConfig.widgets[field.type];
|
|
||||||
if (!("factory" in widget)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const Component = widget.factory;
|
|
||||||
|
|
||||||
const optionValues = field.selectText?.trim().split("\n");
|
<form onSubmit={handleOnSubmit}>
|
||||||
const options = optionValues?.map((value) => {
|
<div className="mb-8">
|
||||||
const title = value;
|
<h1 className="font-cal mb-1 text-xl font-bold capitalize tracking-wide text-gray-900 dark:text-white">
|
||||||
return {
|
{form.name}
|
||||||
value,
|
</h1>
|
||||||
title,
|
{form.description ? (
|
||||||
};
|
<p className="min-h-10 text-sm text-neutral-500 ltr:mr-4 rtl:ml-4 dark:text-white">
|
||||||
});
|
{form.description}
|
||||||
return (
|
</p>
|
||||||
<div key={field.id} className="mb-4 block flex-col sm:flex ">
|
) : null}
|
||||||
<div className="min-w-48 mb-2 flex-grow">
|
|
||||||
<label
|
|
||||||
id="slug-label"
|
|
||||||
htmlFor="slug"
|
|
||||||
className="flex text-sm font-medium text-neutral-700">
|
|
||||||
{field.label}
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex rounded-sm">
|
{form.fields?.map((field) => {
|
||||||
<Component
|
const widget = queryBuilderConfig.widgets[field.type];
|
||||||
value={response[field.id]?.value}
|
if (!("factory" in widget)) {
|
||||||
// required property isn't accepted by query-builder types
|
return null;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
}
|
||||||
/* @ts-ignore */
|
const Component = widget.factory;
|
||||||
required={!!field.required}
|
|
||||||
listValues={options}
|
const optionValues = field.selectText?.trim().split("\n");
|
||||||
setValue={(value) => {
|
const options = optionValues?.map((value) => {
|
||||||
setResponse((response) => {
|
const title = value;
|
||||||
response = response || {};
|
return {
|
||||||
return {
|
value,
|
||||||
...response,
|
title,
|
||||||
[field.id]: {
|
};
|
||||||
label: field.label,
|
});
|
||||||
value,
|
return (
|
||||||
},
|
<div key={field.id} className="mb-4 block flex-col sm:flex ">
|
||||||
};
|
<div className="min-w-48 mb-2 flex-grow">
|
||||||
});
|
<label
|
||||||
}}
|
id="slug-label"
|
||||||
/>
|
htmlFor="slug"
|
||||||
|
className="flex text-sm font-medium text-neutral-700 dark:text-white">
|
||||||
|
{field.label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex rounded-sm">
|
||||||
|
<Component
|
||||||
|
value={response[field.id]?.value}
|
||||||
|
// required property isn't accepted by query-builder types
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
/* @ts-ignore */
|
||||||
|
required={!!field.required}
|
||||||
|
listValues={options}
|
||||||
|
setValue={(value) => {
|
||||||
|
setResponse((response) => {
|
||||||
|
response = response || {};
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
[field.id]: {
|
||||||
|
label: field.label,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
|
||||||
|
<Button
|
||||||
|
loading={responseMutation.isLoading}
|
||||||
|
type="submit"
|
||||||
|
className="dark:text-darkmodebrandcontrast text-brandcontrast bg-brand dark:bg-darkmodebrand relative inline-flex items-center rounded-sm border border-transparent px-3 py-2 text-sm font-medium hover:bg-opacity-90 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
);
|
</div>
|
||||||
})}
|
|
||||||
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
|
|
||||||
<Button
|
|
||||||
loading={responseMutation.isLoading}
|
|
||||||
type="submit"
|
|
||||||
className="dark:text-darkmodebrandcontrast text-brandcontrast bg-brand dark:bg-darkmodebrand relative inline-flex items-center rounded-sm border border-transparent px-3 py-2 text-sm font-medium hover:bg-opacity-90 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1">
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="mx-auto my-0 max-w-3xl md:my-24">
|
||||||
|
<div className="w-full max-w-4xl ltr:mr-2 rtl:ml-2">
|
||||||
|
<div className="-mx-4 rounded-sm border border-neutral-200 bg-white p-4 py-6 sm:mx-0 sm:px-8">
|
||||||
|
<div>{customPageMessage}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="mx-auto my-0 max-w-3xl md:my-24">
|
|
||||||
<div className="w-full max-w-4xl ltr:mr-2 rtl:ml-2">
|
|
||||||
<div className="-mx-4 rounded-sm border border-neutral-200 bg-white p-4 py-6 sm:mx-0 sm:px-8">
|
|
||||||
<div>{customPageMessage}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -216,7 +233,10 @@ function processRoute({ form, response }: { form: Form; response: Response }) {
|
|||||||
for (const [uuid, { value }] of Object.entries(response)) {
|
for (const [uuid, { value }] of Object.entries(response)) {
|
||||||
responseValues[uuid] = value;
|
responseValues[uuid] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logic) {
|
if (logic) {
|
||||||
|
// Leave the logs for easy debugging of routing form logic test.
|
||||||
|
console.log("Checking logic with response", logic, responseValues);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
result = jsonLogic.apply(logic as any, responseValues);
|
result = jsonLogic.apply(logic as any, responseValues);
|
||||||
} else {
|
} else {
|
||||||
@ -232,8 +252,8 @@ function processRoute({ form, response }: { form: Form; response: Response }) {
|
|||||||
return decidedAction;
|
return decidedAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RoutingLink({ form }: { form: Form }) {
|
export default function RoutingLink({ form, profile }: inferSSRProps<typeof getServerSideProps>) {
|
||||||
return <RoutingForm form={form} />;
|
return <RoutingForm form={form} profile={profile} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
RoutingLink.isThemeSupported = true;
|
RoutingLink.isThemeSupported = true;
|
||||||
@ -259,6 +279,15 @@ export const getServerSideProps = async function getServerSideProps(
|
|||||||
where: {
|
where: {
|
||||||
id: formId,
|
id: formId,
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
theme: true,
|
||||||
|
brandColor: true,
|
||||||
|
darkBrandColor: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!form || form.disabled) {
|
if (!form || form.disabled) {
|
||||||
@ -269,6 +298,11 @@ export const getServerSideProps = async function getServerSideProps(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
profile: {
|
||||||
|
theme: form.user.theme,
|
||||||
|
brandColor: form.user.brandColor,
|
||||||
|
darkBrandColor: form.user.darkBrandColor,
|
||||||
|
},
|
||||||
form: getSerializableForm(form),
|
form: getSerializableForm(form),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -36,6 +36,12 @@ Make `dist/embed.umd.js` servable on URL <http://cal.com/embed.js>
|
|||||||
|
|
||||||
- Hot reload doesn't work with CSS files in the way we use vite.
|
- Hot reload doesn't work with CSS files in the way we use vite.
|
||||||
|
|
||||||
|
## Steps to make a page compatiable with Embed
|
||||||
|
|
||||||
|
- Define `main` class on the element that has the entire content of the page with no auto margins
|
||||||
|
- Adding `main` class allows iframe height to adjust according to it, making sure that the content within `main` is visible without scroll as long as device dimensions permits it.
|
||||||
|
- It also becomes the area beyond which if user clicks, modal-box would close.
|
||||||
|
|
||||||
## Known Bugs and Upcoming Improvements
|
## Known Bugs and Upcoming Improvements
|
||||||
|
|
||||||
- Unsupported Browsers and versions. Documenting them and gracefully handling that.
|
- Unsupported Browsers and versions. Documenting them and gracefully handling that.
|
||||||
|
@ -10,9 +10,9 @@
|
|||||||
const url = new URL(document.URL);
|
const url = new URL(document.URL);
|
||||||
// Only run the example specified by only=, avoids distraction and faster to test.
|
// Only run the example specified by only=, avoids distraction and faster to test.
|
||||||
const only = url.searchParams.get("only");
|
const only = url.searchParams.get("only");
|
||||||
const namespace = only ? only.replace("ns:",""): null
|
const elementIdentifier = only ? only.replace("ns:",""): null
|
||||||
if (namespace) {
|
if (elementIdentifier) {
|
||||||
location.hash="#cal-booking-place-" + namespace + "-iframe"
|
location.hash="#cal-booking-place-" + elementIdentifier + "-iframe"
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
</script>
|
</script>
|
||||||
@ -89,6 +89,8 @@
|
|||||||
<button data-cal-namespace="popupTeamLinkDarkTheme" data-cal-config='{"theme":"dark"}' data-cal-link="team/seeded-team/collective-seeded-team-event">Book with Test Team[Dark Theme]</button>
|
<button data-cal-namespace="popupTeamLinkDarkTheme" data-cal-config='{"theme":"dark"}' data-cal-link="team/seeded-team/collective-seeded-team-event">Book with Test Team[Dark Theme]</button>
|
||||||
<button data-cal-namespace="popupTeamLinksList" data-cal-link="team/seeded-team/">See Team Links [Auto Theme]</button>
|
<button data-cal-namespace="popupTeamLinksList" data-cal-link="team/seeded-team/">See Team Links [Auto Theme]</button>
|
||||||
<button data-cal-namespace="popupPaidEvent" data-cal-link="pro/paid">Book Paid Event [Auto Theme]</button>
|
<button data-cal-namespace="popupPaidEvent" data-cal-link="pro/paid">Book Paid Event [Auto Theme]</button>
|
||||||
|
<button data-cal-namespace="routingFormAuto" data-cal-link="forms/948ae412-d995-4865-875a-48302588de03">Book through Routing Form [Auto Theme]</button>
|
||||||
|
<button data-cal-namespace="routingFormDark" data-cal-config='{"theme":"dark"}' data-cal-link="forms/948ae412-d995-4865-875a-48302588de03">Book through Routing Form [Dark Theme]</button>
|
||||||
<div>
|
<div>
|
||||||
<h2>Embed for Pages behind authentication</h2>
|
<h2>Embed for Pages behind authentication</h2>
|
||||||
<button data-cal-namespace="upcomingBookings" data-cal-config='{"theme":"dark"}' data-cal-link="bookings/upcoming">Show Upcoming Bookings</button>
|
<button data-cal-namespace="upcomingBookings" data-cal-config='{"theme":"dark"}' data-cal-link="bookings/upcoming">Show Upcoming Bookings</button>
|
||||||
@ -189,6 +191,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="debug" id="cal-booking-place-inline-routing-form">
|
||||||
|
<h2>Inline Routing Form</h2>
|
||||||
|
<div>
|
||||||
|
<i><a href="?only=inline-routing-form">Test in Zen Mode</a></i>
|
||||||
|
</div>
|
||||||
|
<i class="last-action">
|
||||||
|
<i>You would see last Booking page action in my place</i>
|
||||||
|
</i>
|
||||||
|
<div style="display:flex;align-items: center;">
|
||||||
|
<h2 style="width: 30%">
|
||||||
|
On the right side you can book a team meeting =>
|
||||||
|
</h2>
|
||||||
|
<div style="width: 70%" class="place">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const callback = function (e) {
|
const callback = function (e) {
|
||||||
@ -380,6 +398,28 @@
|
|||||||
calLink: "free",
|
calLink: "free",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!only || only === "inline-routing-form") {
|
||||||
|
Cal('init', 'inline-routing-form', {
|
||||||
|
debug: 1,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
})
|
||||||
|
Cal.ns['inline-routing-form'](
|
||||||
|
[
|
||||||
|
"inline",
|
||||||
|
{
|
||||||
|
elementOrSelector: "#cal-booking-place-inline-routing-form .place",
|
||||||
|
calLink: "forms/9a7e8801-2f34-45ae-aa79-51859766a860",
|
||||||
|
config: {
|
||||||
|
iframeAttrs: {
|
||||||
|
id: "cal-booking-place-inline-routing-form-iframe"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Cal('init', 'popupDarkTheme', {
|
Cal('init', 'popupDarkTheme', {
|
||||||
debug: 1,
|
debug: 1,
|
||||||
origin: "http://localhost:3000",
|
origin: "http://localhost:3000",
|
||||||
@ -416,11 +456,23 @@
|
|||||||
debug: 1,
|
debug: 1,
|
||||||
origin: "http://localhost:3000",
|
origin: "http://localhost:3000",
|
||||||
});
|
});
|
||||||
if (!only || only == "ns:floatingButton") {
|
|
||||||
Cal.ns.floatingButton("floatingButton", {
|
Cal('init', 'routingFormAuto', {
|
||||||
calLink: "pro"
|
debug: 1,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
Cal('init', 'routingFormDark', {
|
||||||
|
debug: 1,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if (!only || only == "ns:floatingButton") {
|
||||||
|
Cal.ns.floatingButton("floatingButton", {
|
||||||
|
calLink: "pro"
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<script></script>
|
<script></script>
|
||||||
</body>
|
</body>
|
||||||
|
@ -71,6 +71,7 @@ export type ExpectedUrlDetails = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
namespace PlaywrightTest {
|
namespace PlaywrightTest {
|
||||||
//FIXME: how to restrict it to Frame only
|
//FIXME: how to restrict it to Frame only
|
||||||
interface Matchers<R> {
|
interface Matchers<R> {
|
||||||
@ -78,7 +79,7 @@ declare global {
|
|||||||
calNamespace: string,
|
calNamespace: string,
|
||||||
getActionFiredDetails: Function,
|
getActionFiredDetails: Function,
|
||||||
expectedUrlDetails?: ExpectedUrlDetails
|
expectedUrlDetails?: ExpectedUrlDetails
|
||||||
): R;
|
): Promise<R>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ test("should open embed iframe on click - Configured with light theme", async ({
|
|||||||
|
|
||||||
embedIframe = await getEmbedIframe({ page, pathname: "/free" });
|
embedIframe = await getEmbedIframe({ page, pathname: "/free" });
|
||||||
|
|
||||||
expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, {
|
await expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, {
|
||||||
pathname: "/free",
|
pathname: "/free",
|
||||||
});
|
});
|
||||||
expect(await page.screenshot()).toMatchSnapshot("event-types-list.png");
|
expect(await page.screenshot()).toMatchSnapshot("event-types-list.png");
|
||||||
@ -39,3 +39,28 @@ todo("Floating Button Test with Dark Theme");
|
|||||||
todo("Floating Button Test with Light Theme");
|
todo("Floating Button Test with Light Theme");
|
||||||
|
|
||||||
todo("Add snapshot test for embed iframe");
|
todo("Add snapshot test for embed iframe");
|
||||||
|
|
||||||
|
test("should open Routing Forms embed on click", async ({
|
||||||
|
page,
|
||||||
|
addEmbedListeners,
|
||||||
|
getActionFiredDetails,
|
||||||
|
}) => {
|
||||||
|
await deleteAllBookingsByEmail("embed-user@example.com");
|
||||||
|
|
||||||
|
const calNamespace = "routingFormAuto";
|
||||||
|
await addEmbedListeners(calNamespace);
|
||||||
|
await page.goto("/?only=prerender-test");
|
||||||
|
let embedIframe = await getEmbedIframe({ page, pathname: "/forms/948ae412-d995-4865-875a-48302588de03" });
|
||||||
|
expect(embedIframe).toBeFalsy();
|
||||||
|
await page.click(
|
||||||
|
`[data-cal-namespace=${calNamespace}][data-cal-link="forms/948ae412-d995-4865-875a-48302588de03"]`
|
||||||
|
);
|
||||||
|
embedIframe = await getEmbedIframe({ page, pathname: "/forms/948ae412-d995-4865-875a-48302588de03" });
|
||||||
|
if (!embedIframe) {
|
||||||
|
throw new Error("Routing Form embed iframe not found");
|
||||||
|
}
|
||||||
|
await expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, {
|
||||||
|
pathname: "/forms/948ae412-d995-4865-875a-48302588de03",
|
||||||
|
});
|
||||||
|
await expect(embedIframe.locator("text=Seeded Form - Pro")).toBeVisible();
|
||||||
|
});
|
||||||
|
@ -16,6 +16,8 @@ declare global {
|
|||||||
CalComPageStatus: string;
|
CalComPageStatus: string;
|
||||||
CalComPlan: string;
|
CalComPlan: string;
|
||||||
isEmbed: () => boolean;
|
isEmbed: () => boolean;
|
||||||
|
getEmbedNamespace: () => string | null;
|
||||||
|
getEmbedTheme: () => "dark" | "light" | null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,8 +199,7 @@ function getNamespace() {
|
|||||||
return embedStore.namespace;
|
return embedStore.namespace;
|
||||||
}
|
}
|
||||||
if (isBrowser) {
|
if (isBrowser) {
|
||||||
const url = new URL(document.URL);
|
const namespace = window.getEmbedNamespace();
|
||||||
const namespace = url.searchParams.get("embed");
|
|
||||||
embedStore.namespace = namespace;
|
embedStore.namespace = namespace;
|
||||||
return namespace;
|
return namespace;
|
||||||
}
|
}
|
||||||
@ -219,7 +220,7 @@ export const useIsEmbed = () => {
|
|||||||
// We can't simply return isEmbed() from this method.
|
// We can't simply return isEmbed() from this method.
|
||||||
// isEmbed() returns different values on server and browser, which messes up the hydration.
|
// isEmbed() returns different values on server and browser, which messes up the hydration.
|
||||||
// TODO: We can avoid using document.URL and instead use Router.
|
// TODO: We can avoid using document.URL and instead use Router.
|
||||||
const [_isEmbed, setIsEmbed] = useState<boolean | null>(null);
|
const [isEmbed, setIsEmbed] = useState<boolean | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const namespace = getNamespace();
|
const namespace = getNamespace();
|
||||||
const _isValidNamespace = isValidNamespace(namespace);
|
const _isValidNamespace = isValidNamespace(namespace);
|
||||||
@ -230,7 +231,7 @@ export const useIsEmbed = () => {
|
|||||||
}
|
}
|
||||||
setIsEmbed(window.isEmbed());
|
setIsEmbed(window.isEmbed());
|
||||||
}, []);
|
}, []);
|
||||||
return _isEmbed;
|
return isEmbed;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useEmbedType = () => {
|
export const useEmbedType = () => {
|
||||||
@ -369,7 +370,7 @@ function keepParentInformedAboutDimensionChanges() {
|
|||||||
|
|
||||||
if (isBrowser) {
|
if (isBrowser) {
|
||||||
const url = new URL(document.URL);
|
const url = new URL(document.URL);
|
||||||
embedStore.theme = (url.searchParams.get("theme") || "auto") as UiConfig["theme"];
|
embedStore.theme = (window.getEmbedTheme() || "auto") as UiConfig["theme"];
|
||||||
if (url.searchParams.get("prerender") !== "true" && window.isEmbed()) {
|
if (url.searchParams.get("prerender") !== "true" && window.isEmbed()) {
|
||||||
log("Initializing embed-iframe");
|
log("Initializing embed-iframe");
|
||||||
// HACK
|
// HACK
|
||||||
|
@ -1,11 +1,106 @@
|
|||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { uuid } from "short-uuid";
|
||||||
|
|
||||||
import prisma from ".";
|
import prisma from ".";
|
||||||
|
|
||||||
require("dotenv").config({ path: "../../.env.appStore" });
|
require("dotenv").config({ path: "../../.env.appStore" });
|
||||||
|
|
||||||
|
async function seedAppData() {
|
||||||
|
const form = await prisma.app_RoutingForms_Form.findUnique({
|
||||||
|
where: {
|
||||||
|
id: "948ae412-d995-4865-875a-48302588de03",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (form) {
|
||||||
|
console.log(`Skipping Routing Form - Form Seed, "Seeded Form - Pro" already exists`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await prisma.app_RoutingForms_Form.create({
|
||||||
|
data: {
|
||||||
|
id: "948ae412-d995-4865-875a-48302588de03",
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
id: "8a898988-89ab-4cde-b012-31823f708642",
|
||||||
|
action: { type: "eventTypeRedirectUrl", value: "pro/30min" },
|
||||||
|
queryValue: {
|
||||||
|
id: "8a898988-89ab-4cde-b012-31823f708642",
|
||||||
|
type: "group",
|
||||||
|
children1: {
|
||||||
|
"8988bbb8-0123-4456-b89a-b1823f70c5ff": {
|
||||||
|
type: "rule",
|
||||||
|
properties: {
|
||||||
|
field: "c4296635-9f12-47b1-8153-c3a854649182",
|
||||||
|
value: ["event-routing"],
|
||||||
|
operator: "equal",
|
||||||
|
valueSrc: ["value"],
|
||||||
|
valueType: ["text"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "aa8aaba9-cdef-4012-b456-71823f70f7ef",
|
||||||
|
action: { type: "customPageMessage", value: "Custom Page Result" },
|
||||||
|
queryValue: {
|
||||||
|
id: "aa8aaba9-cdef-4012-b456-71823f70f7ef",
|
||||||
|
type: "group",
|
||||||
|
children1: {
|
||||||
|
"b99b8a89-89ab-4cde-b012-31823f718ff5": {
|
||||||
|
type: "rule",
|
||||||
|
properties: {
|
||||||
|
field: "c4296635-9f12-47b1-8153-c3a854649182",
|
||||||
|
value: ["custom-page"],
|
||||||
|
operator: "equal",
|
||||||
|
valueSrc: ["value"],
|
||||||
|
valueType: ["text"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a8ba9aab-4567-489a-bcde-f1823f71b4ad",
|
||||||
|
action: { type: "externalRedirectUrl", value: "https://google.com" },
|
||||||
|
queryValue: {
|
||||||
|
id: "a8ba9aab-4567-489a-bcde-f1823f71b4ad",
|
||||||
|
type: "group",
|
||||||
|
children1: {
|
||||||
|
"998b9b9a-0123-4456-b89a-b1823f7232b9": {
|
||||||
|
type: "rule",
|
||||||
|
properties: {
|
||||||
|
field: "c4296635-9f12-47b1-8153-c3a854649182",
|
||||||
|
value: ["external-redirect"],
|
||||||
|
operator: "equal",
|
||||||
|
valueSrc: ["value"],
|
||||||
|
valueType: ["text"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "898899aa-4567-489a-bcde-f1823f708646",
|
||||||
|
action: { type: "customPageMessage", value: "Fallback Message" },
|
||||||
|
isFallback: true,
|
||||||
|
queryValue: { id: "898899aa-4567-489a-bcde-f1823f708646", type: "group" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{ id: "c4296635-9f12-47b1-8153-c3a854649182", type: "text", label: "Test field", required: true },
|
||||||
|
],
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
username: "pro",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: "Seeded Form - Pro",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function createApp(
|
async function createApp(
|
||||||
/** The App identifier in the DB also used for public page in `/apps/[slug]` */
|
/** The App identifier in the DB also used for public page in `/apps/[slug]` */
|
||||||
slug: Prisma.AppCreateInput["slug"],
|
slug: Prisma.AppCreateInput["slug"],
|
||||||
@ -28,7 +123,7 @@ async function createApp(
|
|||||||
console.log(`📲 Upserted app: '${slug}'`);
|
console.log(`📲 Upserted app: '${slug}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
export default async function main() {
|
||||||
// Calendar apps
|
// Calendar apps
|
||||||
await createApp("apple-calendar", "applecalendar", ["calendar"], "apple_calendar");
|
await createApp("apple-calendar", "applecalendar", ["calendar"], "apple_calendar");
|
||||||
await createApp("caldav-calendar", "caldavcalendar", ["calendar"], "caldav_calendar");
|
await createApp("caldav-calendar", "caldavcalendar", ["calendar"], "caldav_calendar");
|
||||||
@ -144,13 +239,6 @@ async function main() {
|
|||||||
const generatedApp = generatedApps[i];
|
const generatedApp = generatedApps[i];
|
||||||
await createApp(generatedApp.slug, generatedApp.dirName, generatedApp.categories, generatedApp.type);
|
await createApp(generatedApp.slug, generatedApp.dirName, generatedApp.categories, generatedApp.type);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
await seedAppData();
|
||||||
.catch((e) => {
|
}
|
||||||
console.error(e);
|
|
||||||
process.exit(1);
|
|
||||||
})
|
|
||||||
.finally(async () => {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
});
|
|
||||||
|
@ -6,7 +6,7 @@ import { hashPassword } from "@calcom/lib/auth";
|
|||||||
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
||||||
|
|
||||||
import prisma from ".";
|
import prisma from ".";
|
||||||
import "./seed-app-store";
|
import mainAppStore from "./seed-app-store";
|
||||||
|
|
||||||
require("dotenv").config({ path: "../../.env" });
|
require("dotenv").config({ path: "../../.env" });
|
||||||
async function createUserAndEventType(opts: {
|
async function createUserAndEventType(opts: {
|
||||||
@ -564,6 +564,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
.then(() => mainAppStore())
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
Loading…
Reference in New Issue
Block a user