feat: Support headless router as well in embed (#17012)
* Support /router/embed path and add Headless router usecase in playground * Fix broken seeded-form testing wrong thing
This commit is contained in:
@@ -291,8 +291,8 @@ const nextConfig = {
|
||||
destination: "/apps/routing-forms/routing-link/:formQuery*",
|
||||
},
|
||||
{
|
||||
source: "/router",
|
||||
destination: "/apps/routing-forms/router",
|
||||
source: "/router/:path*",
|
||||
destination: "/apps/routing-forms/router/:path*",
|
||||
},
|
||||
{
|
||||
source: "/success/:path*",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { Ensure } from "@calcom/types/utils";
|
||||
|
||||
function getUserAndEventTypeSlug(eventTypeRedirectUrl: string) {
|
||||
if (eventTypeRedirectUrl.startsWith("/")) {
|
||||
@@ -31,6 +32,7 @@ export function getAbsoluteEventTypeRedirectUrl({
|
||||
eventTypeRedirectUrl,
|
||||
form,
|
||||
allURLSearchParams,
|
||||
isEmbed,
|
||||
}: {
|
||||
eventTypeRedirectUrl: string;
|
||||
form: {
|
||||
@@ -56,7 +58,9 @@ export function getAbsoluteEventTypeRedirectUrl({
|
||||
teamOrigin: string;
|
||||
};
|
||||
allURLSearchParams: URLSearchParams;
|
||||
isEmbed?: boolean;
|
||||
}) {
|
||||
eventTypeRedirectUrl = `${eventTypeRedirectUrl}${isEmbed ? "/embed" : ""}`;
|
||||
// It could be using the old(before migration) username/team-slug or it could be using the new one(after migration)
|
||||
// If it's using the old one, it would work by redirection as long as we use CAL_URL(which is non-org domain)
|
||||
// But if it's using the new one, it has to use the org domain.
|
||||
@@ -86,3 +90,9 @@ export function getAbsoluteEventTypeRedirectUrl({
|
||||
|
||||
return `${origin}/${eventTypeRedirectUrl}?${allURLSearchParams}`;
|
||||
}
|
||||
|
||||
export function getAbsoluteEventTypeRedirectUrlWithEmbedSupport(
|
||||
args: Ensure<Parameters<typeof getAbsoluteEventTypeRedirectUrl>[0], "isEmbed">
|
||||
) {
|
||||
return getAbsoluteEventTypeRedirectUrl({ ...args });
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function Router({ form, message }: inferSSRProps<typeof getServer
|
||||
</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="bg-default -mx-4 rounded-sm border border-neutral-200 p-4 py-6 sm:mx-0 sm:px-8">
|
||||
<div className="text-default bg-default -mx-4 rounded-sm border border-neutral-200 p-4 py-6 sm:mx-0 sm:px-8">
|
||||
<div>{message}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { TRPCError } from "@calcom/trpc/server";
|
||||
import type { AppGetServerSidePropsContext, AppPrisma } from "@calcom/types/AppGetServerSideProps";
|
||||
|
||||
import { enrichFormWithMigrationData } from "../../enrichFormWithMigrationData";
|
||||
import { getAbsoluteEventTypeRedirectUrl } from "../../getEventTypeRedirectUrl";
|
||||
import { getAbsoluteEventTypeRedirectUrlWithEmbedSupport } from "../../getEventTypeRedirectUrl";
|
||||
import getFieldIdentifier from "../../lib/getFieldIdentifier";
|
||||
import { getSerializableForm } from "../../lib/getSerializableForm";
|
||||
import { findMatchingRoute } from "../../lib/processRoute";
|
||||
@@ -27,11 +27,24 @@ const querySchema = z
|
||||
})
|
||||
.catchall(z.string().or(z.array(z.string())));
|
||||
|
||||
function getNamedParams(params: { appPages: string[] } | undefined) {
|
||||
const [embed] = params?.appPages || [];
|
||||
return {
|
||||
// There might not be item at index 0, so explicit assertion is needed
|
||||
embed: embed as typeof embed | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export const getServerSideProps = async function getServerSideProps(
|
||||
context: AppGetServerSidePropsContext,
|
||||
prisma: AppPrisma
|
||||
) {
|
||||
const queryParsed = querySchema.safeParse(context.query);
|
||||
const { embed } = getNamedParams(context.params);
|
||||
const pageProps = {
|
||||
isEmbed: !!embed,
|
||||
};
|
||||
|
||||
if (!queryParsed.success) {
|
||||
log.warn("Error parsing query", queryParsed.error);
|
||||
return {
|
||||
@@ -139,6 +152,7 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
if (e instanceof TRPCError) {
|
||||
return {
|
||||
props: {
|
||||
...pageProps,
|
||||
form: serializableForm,
|
||||
message: e.message,
|
||||
},
|
||||
@@ -150,6 +164,7 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
if (decidedAction.type === "customPageMessage") {
|
||||
return {
|
||||
props: {
|
||||
...pageProps,
|
||||
form: serializableForm,
|
||||
message: decidedAction.value,
|
||||
},
|
||||
@@ -163,7 +178,7 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
|
||||
return {
|
||||
redirect: {
|
||||
destination: getAbsoluteEventTypeRedirectUrl({
|
||||
destination: getAbsoluteEventTypeRedirectUrlWithEmbedSupport({
|
||||
eventTypeRedirectUrl: eventTypeUrlWithResolvedVariables,
|
||||
form: serializableForm,
|
||||
allURLSearchParams: getUrlSearchParamsToForward({
|
||||
@@ -174,6 +189,7 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
// formResponseId is guaranteed to be set because in catch block of trpc request we return from the function and otherwise it would have been set
|
||||
formResponseId: formResponseId!,
|
||||
}),
|
||||
isEmbed: pageProps.isEmbed,
|
||||
}),
|
||||
permanent: false,
|
||||
},
|
||||
@@ -187,9 +203,12 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Consider throwing error here as there is no value of decidedAction.type that would cause the flow to be here
|
||||
return {
|
||||
props: {
|
||||
...pageProps,
|
||||
form: serializableForm,
|
||||
message: "Unhandled type of action",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { Button, showToast, useCalcomTheme } from "@calcom/ui";
|
||||
|
||||
import FormInputFields from "../../components/FormInputFields";
|
||||
import { getAbsoluteEventTypeRedirectUrl } from "../../getEventTypeRedirectUrl";
|
||||
import { getAbsoluteEventTypeRedirectUrlWithEmbedSupport } from "../../getEventTypeRedirectUrl";
|
||||
import getFieldIdentifier from "../../lib/getFieldIdentifier";
|
||||
import { findMatchingRoute } from "../../lib/processRoute";
|
||||
import { substituteVariables } from "../../lib/substituteVariables";
|
||||
@@ -122,10 +122,11 @@ function RoutingForm({ form, profile, ...restProps }: Props) {
|
||||
} else if (decidedAction.type === "eventTypeRedirectUrl") {
|
||||
const eventTypeUrlWithResolvedVariables = substituteVariables(decidedAction.value, response, fields);
|
||||
router.push(
|
||||
getAbsoluteEventTypeRedirectUrl({
|
||||
getAbsoluteEventTypeRedirectUrlWithEmbedSupport({
|
||||
form,
|
||||
eventTypeRedirectUrl: eventTypeUrlWithResolvedVariables,
|
||||
allURLSearchParams,
|
||||
isEmbed: !!isEmbed
|
||||
})
|
||||
);
|
||||
} else if (decidedAction.type === "externalRedirectUrl") {
|
||||
|
||||
@@ -345,6 +345,14 @@ test.describe("Routing Forms", () => {
|
||||
`/router?form=${routingForm.id}&Test field=doesntmatter&${Identifiers.multiNewFormat}=d1302635-9f12-17b1-9153-c3a854649182`
|
||||
);
|
||||
await expect(page.locator("text=Multiselect chosen")).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Negative test - Ensures that the "No logic" situation where a Route is considered to be always passing isn't hitting
|
||||
await page.goto(`/router?form=${routingForm.id}&Test field=kuchbhi`);
|
||||
await page.waitForURL((url) => {
|
||||
return url.searchParams.get("Test field") === "kuchbhi";
|
||||
});
|
||||
|
||||
expect(page.url()).not.toContain("/pro/30min");
|
||||
});
|
||||
|
||||
test("Routing Link should validate fields", async ({ page, users }) => {
|
||||
|
||||
@@ -311,7 +311,9 @@
|
||||
data-cal-link="forms/948ae412-d995-4865-875a-48302588de03">
|
||||
Book through Routing Form [Dark Theme]
|
||||
</button>
|
||||
|
||||
<button data-cal-namespace="routingFormHeadlessRouter" data-cal-link="router?form=948ae412-d995-4865-875a-48302588de03&Test field=event-routing" data-cal-config='{"flag.coep": "true"}'>
|
||||
Book through Headless Router
|
||||
</button>
|
||||
<h2>Popup Examples - Child Element Target Tests</h2>
|
||||
<a style="display: block;" data-cal-namespace="childElementTarget" href="javascript:void(0)" data-cal-link="free/30min" data-cal-config='{"flag.coep": "true"}'>
|
||||
I am Anchor
|
||||
|
||||
@@ -419,6 +419,11 @@ Cal("init", "routingFormAuto", {
|
||||
origin: origin,
|
||||
});
|
||||
|
||||
Cal("init", "routingFormHeadlessRouter", {
|
||||
debug: true,
|
||||
origin: origin,
|
||||
});
|
||||
|
||||
Cal.ns.routingFormAuto("on", {
|
||||
action: "routed",
|
||||
callback: (e) => {
|
||||
|
||||
@@ -54,7 +54,7 @@ async function seedAppData() {
|
||||
"8988bbb8-0123-4456-b89a-b1823f70c5ff": {
|
||||
type: "rule",
|
||||
properties: {
|
||||
field: "c4296635-9f12-47b1-8153-c3a854649182",
|
||||
field: "c1296635-9f12-47b1-8153-c3a854649182",
|
||||
value: ["event-routing"],
|
||||
operator: "equal",
|
||||
valueSrc: ["value"],
|
||||
@@ -74,7 +74,7 @@ async function seedAppData() {
|
||||
"b99b8a89-89ab-4cde-b012-31823f718ff5": {
|
||||
type: "rule",
|
||||
properties: {
|
||||
field: "c4296635-9f12-47b1-8153-c3a854649182",
|
||||
field: "c1296635-9f12-47b1-8153-c3a854649182",
|
||||
value: ["custom-page"],
|
||||
operator: "equal",
|
||||
valueSrc: ["value"],
|
||||
@@ -94,7 +94,7 @@ async function seedAppData() {
|
||||
"998b9b9a-0123-4456-b89a-b1823f7232b9": {
|
||||
type: "rule",
|
||||
properties: {
|
||||
field: "c4296635-9f12-47b1-8153-c3a854649182",
|
||||
field: "c1296635-9f12-47b1-8153-c3a854649182",
|
||||
value: ["external-redirect"],
|
||||
operator: "equal",
|
||||
valueSrc: ["value"],
|
||||
|
||||
Reference in New Issue
Block a user