chore: [app-router-migration-3] apps/[slug]/index, apps/[slug]/setup (#12620)
Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
parent
a20179727b
commit
098d41fc62
|
@ -295,3 +295,5 @@ AB_TEST_BUCKET_PROBABILITY=50
|
||||||
# whether we redirect to the future/event-types from event-types or not
|
# whether we redirect to the future/event-types from event-types or not
|
||||||
APP_ROUTER_EVENT_TYPES_ENABLED=1
|
APP_ROUTER_EVENT_TYPES_ENABLED=1
|
||||||
APP_ROUTER_SETTINGS_ADMIN_ENABLED=1
|
APP_ROUTER_SETTINGS_ADMIN_ENABLED=1
|
||||||
|
APP_ROUTER_APPS_SLUG_ENABLED=1
|
||||||
|
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=1
|
||||||
|
|
|
@ -6,6 +6,8 @@ import z from "zod";
|
||||||
const ROUTES: [URLPattern, boolean][] = [
|
const ROUTES: [URLPattern, boolean][] = [
|
||||||
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
||||||
["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const,
|
["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const,
|
||||||
|
["/apps/:slug", Boolean(process.env.APP_ROUTER_APPS_SLUG_ENABLED)] as const,
|
||||||
|
["/apps/:slug/setup", Boolean(process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED)] as const,
|
||||||
].map(([pathname, enabled]) => [
|
].map(([pathname, enabled]) => [
|
||||||
new URLPattern({
|
new URLPattern({
|
||||||
pathname,
|
pathname,
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { type ReactElement } from "react";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapperAppDir";
|
||||||
|
|
||||||
|
type EventTypesLayoutProps = {
|
||||||
|
children: ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Layout({ children }: EventTypesLayoutProps) {
|
||||||
|
return (
|
||||||
|
<PageWrapper getLayout={null} requiresLicense={false} nonce={undefined} themeBasis={null}>
|
||||||
|
{children}
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
import AppPage from "@pages/apps/[slug]/index";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
import fs from "fs";
|
||||||
|
import matter from "gray-matter";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import path from "path";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||||
|
import { getAppAssetFullPath } from "@calcom/app-store/getAppAssetFullPath";
|
||||||
|
import { APP_NAME, IS_PRODUCTION } from "@calcom/lib/constants";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
const sourceSchema = z.object({
|
||||||
|
content: z.string(),
|
||||||
|
data: z.object({
|
||||||
|
description: z.string().optional(),
|
||||||
|
items: z
|
||||||
|
.array(
|
||||||
|
z.union([
|
||||||
|
z.string(),
|
||||||
|
z.object({
|
||||||
|
iframe: z.object({ src: z.string() }),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||||
|
const { data } = await getPageProps({ params });
|
||||||
|
|
||||||
|
return await _generateMetadata(
|
||||||
|
() => `${data.name} | ${APP_NAME}`,
|
||||||
|
() => data.description
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateStaticParams = async () => {
|
||||||
|
try {
|
||||||
|
const appStore = await prisma.app.findMany({ select: { slug: true } });
|
||||||
|
return appStore.map(({ slug }) => ({ slug }));
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Prisma.PrismaClientInitializationError) {
|
||||||
|
// Database is not available at build time, but that's ok – we fall back to resolving paths on demand
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||||
|
if (typeof params?.slug !== "string") {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const appMeta = await getAppWithMetadata({
|
||||||
|
slug: params?.slug,
|
||||||
|
});
|
||||||
|
|
||||||
|
const appFromDb = await prisma.app.findUnique({
|
||||||
|
where: { slug: params.slug.toLowerCase() },
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAppAvailableInFileSystem = appMeta;
|
||||||
|
const isAppDisabled = isAppAvailableInFileSystem && (!appFromDb || !appFromDb.enabled);
|
||||||
|
|
||||||
|
if (!IS_PRODUCTION && isAppDisabled) {
|
||||||
|
return {
|
||||||
|
isAppDisabled: true as const,
|
||||||
|
data: {
|
||||||
|
...appMeta,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!appFromDb || !appMeta || isAppDisabled) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTemplate = appMeta.isTemplate;
|
||||||
|
const appDirname = path.join(isTemplate ? "templates" : "", appFromDb.dirName);
|
||||||
|
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
|
||||||
|
const postFilePath = path.join(README_PATH);
|
||||||
|
let source = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
source = fs.readFileSync(postFilePath).toString();
|
||||||
|
source = source.replace(/{DESCRIPTION}/g, appMeta.description);
|
||||||
|
} catch (error) {
|
||||||
|
/* If the app doesn't have a README we fallback to the package description */
|
||||||
|
console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
|
||||||
|
source = appMeta.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = matter(source);
|
||||||
|
const { content, data } = sourceSchema.parse({ content: result.content, data: result.data });
|
||||||
|
if (data.items) {
|
||||||
|
data.items = data.items.map((item) => {
|
||||||
|
if (typeof item === "string") {
|
||||||
|
return getAppAssetFullPath(item, {
|
||||||
|
dirName: appMeta.dirName,
|
||||||
|
isTemplate: appMeta.isTemplate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isAppDisabled: false as const,
|
||||||
|
source: { content, data },
|
||||||
|
data: appMeta,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||||
|
const pageProps = await getPageProps({ params });
|
||||||
|
|
||||||
|
return <AppPage {...pageProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
|
@ -0,0 +1,36 @@
|
||||||
|
import SetupPage from "@pages/apps/[slug]/setup";
|
||||||
|
import { _generateMetadata } from "app/_utils";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import { cookies, headers } from "next/headers";
|
||||||
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
|
||||||
|
import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps";
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
|
|
||||||
|
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||||
|
return await _generateMetadata(
|
||||||
|
() => `${params.slug} | ${APP_NAME}`,
|
||||||
|
() => ""
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||||
|
const req = { headers: headers(), cookies: cookies() };
|
||||||
|
|
||||||
|
const result = await getServerSideProps({ params, req } as unknown as GetServerSidePropsContext);
|
||||||
|
|
||||||
|
if (!result || "notFound" in result) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("redirect" in result) {
|
||||||
|
redirect(result.redirect.destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.props;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||||
|
const pageProps = await getPageProps({ params });
|
||||||
|
return <SetupPage {...pageProps} />;
|
||||||
|
}
|
|
@ -103,6 +103,12 @@ export const config = {
|
||||||
"/future/event-types/",
|
"/future/event-types/",
|
||||||
"/settings/admin/:path*",
|
"/settings/admin/:path*",
|
||||||
"/future/settings/admin/:path*",
|
"/future/settings/admin/:path*",
|
||||||
|
|
||||||
|
"/apps/:slug/",
|
||||||
|
"/future/apps/:slug/",
|
||||||
|
|
||||||
|
"/apps/:slug/setup/",
|
||||||
|
"/future/apps/:slug/setup/",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import matter from "gray-matter";
|
import matter from "gray-matter";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import type { InferGetServerSidePropsType } from "next";
|
import type { InferGetServerSidePropsType } from "next";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { expect } from "@playwright/test";
|
||||||
|
|
||||||
|
import { test } from "./lib/fixtures";
|
||||||
|
|
||||||
|
test.describe.configure({ mode: "parallel" });
|
||||||
|
|
||||||
|
test.describe("apps/ A/B tests", () => {
|
||||||
|
test("should point to the /future/apps/[slug]", async ({ page, users, context }) => {
|
||||||
|
await context.addCookies([
|
||||||
|
{
|
||||||
|
name: "x-calcom-future-routes-override",
|
||||||
|
value: "1",
|
||||||
|
url: "http://localhost:3000",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const user = await users.create();
|
||||||
|
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await page.goto("/apps/telegram");
|
||||||
|
|
||||||
|
await page.waitForLoadState();
|
||||||
|
|
||||||
|
const dataNextJsRouter = await page.evaluate(() =>
|
||||||
|
window.document.documentElement.getAttribute("data-nextjs-router")
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(dataNextJsRouter).toEqual("app");
|
||||||
|
|
||||||
|
const locator = page.getByRole("heading", { name: "Telegram" });
|
||||||
|
|
||||||
|
await expect(locator).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should point to the /future/apps/[slug]/setup", async ({ page, users, context }) => {
|
||||||
|
await context.addCookies([
|
||||||
|
{
|
||||||
|
name: "x-calcom-future-routes-override",
|
||||||
|
value: "1",
|
||||||
|
url: "http://localhost:3000",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const user = await users.create();
|
||||||
|
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await page.goto("/apps/apple-calendar/setup");
|
||||||
|
|
||||||
|
await page.waitForLoadState();
|
||||||
|
|
||||||
|
const dataNextJsRouter = await page.evaluate(() =>
|
||||||
|
window.document.documentElement.getAttribute("data-nextjs-router")
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(dataNextJsRouter).toEqual("app");
|
||||||
|
|
||||||
|
const locator = page.getByRole("heading", { name: "Connect to Apple Server" });
|
||||||
|
|
||||||
|
await expect(locator).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
|
@ -198,6 +198,8 @@
|
||||||
"ALLOWED_HOSTNAMES",
|
"ALLOWED_HOSTNAMES",
|
||||||
"ANALYZE",
|
"ANALYZE",
|
||||||
"API_KEY_PREFIX",
|
"API_KEY_PREFIX",
|
||||||
|
"APP_ROUTER_APPS_SLUG_ENABLED",
|
||||||
|
"APP_ROUTER_APPS_SLUG_SETUP_ENABLED",
|
||||||
"APP_ROUTER_EVENT_TYPES_ENABLED",
|
"APP_ROUTER_EVENT_TYPES_ENABLED",
|
||||||
"APP_ROUTER_SETTINGS_ADMIN_ENABLED",
|
"APP_ROUTER_SETTINGS_ADMIN_ENABLED",
|
||||||
"APP_USER_NAME",
|
"APP_USER_NAME",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user