diff --git a/apps/web/pages/apps/[slug]/index.tsx b/apps/web/pages/apps/[slug]/index.tsx index c961f547e2..854f022616 100644 --- a/apps/web/pages/apps/[slug]/index.tsx +++ b/apps/web/pages/apps/[slug]/index.tsx @@ -2,6 +2,7 @@ import fs from "fs"; import matter from "gray-matter"; import MarkdownIt from "markdown-it"; import type { GetStaticPaths, GetStaticPropsContext } from "next"; +import Link from "next/link"; import path from "path"; import { z } from "zod"; @@ -33,7 +34,26 @@ const sourceSchema = z.object({ }), }); -function SingleAppPage({ data, source }: inferSSRProps) { +function SingleAppPage(props: inferSSRProps) { + // If it's not production environment, it would be a better idea to inform that the App is disabled. + if (props.isAppDisabled) { + if (process.env.NODE_ENV !== "production") { + // TODO: Improve disabled App UI. This is just a placeholder. + return ( +
+ This App seems to be disabled. If you are an admin, you can enable this app from{" "} + + here + +
+ ); + } + + // Disabled App should give 404 any ways in production. + return null; + } + + const { source, data } = props; return ( = async () => { export const getStaticProps = async (ctx: GetStaticPropsContext) => { if (typeof ctx.params?.slug !== "string") return { notFound: true }; - const app = await prisma.app.findUnique({ + const appMeta = await getAppWithMetadata({ + slug: ctx.params?.slug, + }); + + const appFromDb = await prisma.app.findUnique({ where: { slug: ctx.params.slug.toLowerCase() }, }); - if (!app) return { notFound: true }; + const isAppAvailableInFileSystem = appMeta; + const isAppDisabled = isAppAvailableInFileSystem && (!appFromDb || !appFromDb.enabled); - const singleApp = await getAppWithMetadata(app); + if (process.env.NODE_ENV !== "production" && isAppDisabled) { + return { + props: { + isAppDisabled: true as const, + data: { + ...appMeta, + }, + }, + }; + } - if (!singleApp) return { notFound: true }; + if (!appFromDb || !appMeta || isAppDisabled) return { notFound: true }; - const isTemplate = singleApp.isTemplate; - const appDirname = path.join(isTemplate ? "templates" : "", app.dirName); + 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, singleApp.description); + 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 = singleApp.description; + source = appMeta.description; } const result = matter(source); @@ -111,8 +145,8 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => { data.items = data.items.map((item) => { if (typeof item === "string") { return getAppAssetFullPath(item, { - dirName: singleApp.dirName, - isTemplate: singleApp.isTemplate, + dirName: appMeta.dirName, + isTemplate: appMeta.isTemplate, }); } return item; @@ -120,8 +154,9 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => { } return { props: { + isAppDisabled: false as const, source: { content, data }, - data: singleApp, + data: appMeta, }, }; }; diff --git a/packages/app-store-cli/src/components/AppCreateUpdateForm.tsx b/packages/app-store-cli/src/components/AppCreateUpdateForm.tsx index ede4e13935..f3e385a935 100644 --- a/packages/app-store-cli/src/components/AppCreateUpdateForm.tsx +++ b/packages/app-store-cli/src/components/AppCreateUpdateForm.tsx @@ -4,9 +4,9 @@ import SelectInput from "ink-select-input"; import TextInput from "ink-text-input"; import React, { useEffect, useState } from "react"; -import { AppMeta } from "@calcom/types/App"; +import type { AppMeta } from "@calcom/types/App"; -import { getSlugFromAppName, BaseAppFork, Seed, generateAppFiles, getAppDirPath } from "../core"; +import { getSlugFromAppName, BaseAppFork, generateAppFiles, getAppDirPath } from "../core"; import { getApp } from "../utils/getApp"; import Templates from "../utils/templates"; import Label from "./Label"; @@ -148,8 +148,6 @@ export const AppForm = ({ oldSlug: givenSlug, }); - await Seed.update({ slug, category: category, oldSlug: givenSlug, isTemplate }); - await generateAppFiles(); // FIXME: Even after CLI showing this message, it is stuck doing work before exiting @@ -242,6 +240,10 @@ export const AppForm = ({ Publisher Email: {email} + + Next Step: Enable the app from http://localhost:3000/settings/admin/apps as admin user (Email: + admin@example.com, Pass: ADMINadmin2022!) + )} diff --git a/packages/app-store-cli/src/components/DeleteForm.tsx b/packages/app-store-cli/src/components/DeleteForm.tsx index b1a3768f8c..54012cc159 100644 --- a/packages/app-store-cli/src/components/DeleteForm.tsx +++ b/packages/app-store-cli/src/components/DeleteForm.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from "react"; import { ImportantText } from "../components/ImportantText"; import { Message } from "../components/Message"; -import { BaseAppFork, Seed, generateAppFiles } from "../core"; +import { BaseAppFork, generateAppFiles } from "../core"; import { getApp } from "../utils/getApp"; export default function DeleteForm({ slug, action }: { slug: string; action: "delete" | "delete-template" }) { @@ -28,7 +28,6 @@ export default function DeleteForm({ slug, action }: { slug: string; action: "de if (state === "DELETION_CONFIRMATION_SUCCESSFUL") { (async () => { await BaseAppFork.delete({ slug, isTemplate }); - Seed.revert({ slug }); await generateAppFiles(); // successMsg({ text: `App with slug ${slug} has been deleted`, done: true }); setState("DELETION_COMPLETED"); diff --git a/packages/app-store-cli/src/core.ts b/packages/app-store-cli/src/core.ts index 7c06fbf172..5255c4884e 100644 --- a/packages/app-store-cli/src/core.ts +++ b/packages/app-store-cli/src/core.ts @@ -1,8 +1,6 @@ import fs from "fs"; import path from "path"; -import type seedAppStoreConfig from "@calcom/prisma/seed-app-store.config.json"; - import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants"; import execSync from "./utils/execSync"; @@ -27,10 +25,6 @@ export function getAppDirPath(slug: string, isTemplate: boolean) { return path.join(TEMPLATES_PATH, `${slug}`); } -function absolutePath(appRelativePath: string) { - return path.join(APP_STORE_PATH, appRelativePath); -} - const updatePackageJson = ({ slug, appDescription, @@ -138,58 +132,6 @@ export const BaseAppFork = { }, }; -export const Seed = { - seedConfigPath: absolutePath("../prisma/seed-app-store.config.json"), - update: async function ({ - slug, - category, - oldSlug, - isTemplate, - }: { - slug: string; - category: string; - oldSlug: string; - isTemplate: boolean; - }) { - let configContent = "[]"; - try { - if (fs.statSync(this.seedConfigPath)) { - configContent = fs.readFileSync(this.seedConfigPath).toString(); - } - } catch (e) {} - - let seedConfig: typeof seedAppStoreConfig = JSON.parse(configContent); - seedConfig = seedConfig.filter((app) => app.slug !== oldSlug); - - if (!seedConfig.find((app) => app.slug === slug)) { - seedConfig.push({ - dirName: slug, - categories: [category], - slug: slug, - type: `${slug}_${category}`, - isTemplate: isTemplate, - }); - } - - // Add the message as a property to first item so that it stays always at the top - seedConfig[0]["/*"] = - "This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually"; - - // Add the message as a property to first item so that it stays always at the top - seedConfig[0]["/*"] = - "This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually"; - - fs.writeFileSync(this.seedConfigPath, JSON.stringify(seedConfig, null, 2)); - await execSync(`cd ${workspaceDir}/packages/prisma && yarn seed-app-store seed-templates`); - }, - revert: async function ({ slug }: { slug: string }) { - let seedConfig: typeof seedAppStoreConfig = JSON.parse(fs.readFileSync(this.seedConfigPath).toString()); - seedConfig = seedConfig.filter((app) => app.slug !== slug); - fs.writeFileSync(this.seedConfigPath, JSON.stringify(seedConfig, null, 2)); - await execSync(`yarn workspace @calcom/prisma delete-app ${slug}`); - }, -}; - export const generateAppFiles = async () => { await execSync(`yarn ts-node --transpile-only src/build.ts`); }; diff --git a/packages/app-store/_appRegistry.ts b/packages/app-store/_appRegistry.ts index b8b979d3cf..709b665ddb 100644 --- a/packages/app-store/_appRegistry.ts +++ b/packages/app-store/_appRegistry.ts @@ -5,8 +5,22 @@ import { userMetadata } from "@calcom/prisma/zod-utils"; import type { AppFrontendPayload as App } from "@calcom/types/App"; import type { CredentialFrontendPayload as Credential } from "@calcom/types/Credential"; -export async function getAppWithMetadata(app: { dirName: string }) { - const appMetadata: App | null = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata] as App; +/** + * Get App metdata either using dirName or slug + */ +export async function getAppWithMetadata(app: { dirName: string } | { slug: string }) { + let appMetadata: App | null; + + if ("dirName" in app) { + appMetadata = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata] as App; + } else { + const foundEntry = Object.entries(appStoreMetadata).find(([, meta]) => { + return meta.slug === app.slug; + }); + if (!foundEntry) return null; + appMetadata = foundEntry[1] as App; + } + if (!appMetadata) return null; // Let's not leak api keys to the front end // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/packages/prisma/seed-app-store.config.json b/packages/prisma/seed-app-store.config.json index f3b6f15dd5..eaa3d006a7 100644 --- a/packages/prisma/seed-app-store.config.json +++ b/packages/prisma/seed-app-store.config.json @@ -1,6 +1,6 @@ [ { - "/*": "This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually", + "/*": "This file is deprecated now. No new entry should be added to it.", "dirName": "routing-forms", "categories": ["other"], "slug": "routing-forms", diff --git a/packages/prisma/seed-app-store.ts b/packages/prisma/seed-app-store.ts index 821deee798..0e6fad36df 100644 --- a/packages/prisma/seed-app-store.ts +++ b/packages/prisma/seed-app-store.ts @@ -1,3 +1,7 @@ +/** + * @deprecated + * This file is deprecated. The only use of this file is to seed the database for E2E tests. Each test should take care of seeding it's own data going forward. + */ import type { Prisma } from "@prisma/client"; import dotEnv from "dotenv"; import fs from "fs";