diff --git a/apps/web/components/apps/App.tsx b/apps/web/components/apps/App.tsx index 2dd39a4518..4a4aabcb5f 100644 --- a/apps/web/components/apps/App.tsx +++ b/apps/web/components/apps/App.tsx @@ -12,7 +12,7 @@ import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/consta import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { App as AppType } from "@calcom/types/App"; -import { Button, Icon, showToast, SkeletonButton, SkeletonText, HeadSeo } from "@calcom/ui"; +import { Button, Icon, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui"; const Component = ({ name, @@ -34,6 +34,7 @@ const Component = ({ privacy, isProOnly, images, + isTemplate, }: Parameters[0]) => { const { t } = useLocale(); const hasImages = images && images.length > 0; @@ -106,6 +107,11 @@ const Component = ({ {" "} • {t("published_by", { author })} + {isTemplate && ( + + Template - Available in Dev Environment only for testing + + )} {!appCredentials.isLoading ? ( @@ -310,6 +316,7 @@ export default function App(props: { licenseRequired: AppType["licenseRequired"]; isProOnly: AppType["isProOnly"]; images?: string[]; + isTemplate?: boolean; }) { const { t } = useLocale(); diff --git a/apps/web/components/apps/IntegrationListItem.tsx b/apps/web/components/apps/IntegrationListItem.tsx index c4a96e7c03..f641d2b8c1 100644 --- a/apps/web/components/apps/IntegrationListItem.tsx +++ b/apps/web/components/apps/IntegrationListItem.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/router"; import { ReactNode, useEffect, useState } from "react"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Icon, ListItem, ListItemText, ListItemTitle, showToast } from "@calcom/ui"; +import { Badge, Icon, ListItem, ListItemText, ListItemTitle, showToast } from "@calcom/ui"; import classNames from "@lib/classNames"; @@ -19,6 +19,7 @@ function IntegrationListItem(props: { destination?: boolean; separate?: boolean; invalidCredential?: boolean; + isTemplate?: boolean; }): JSX.Element { const { t } = useLocale(); const router = useRouter(); @@ -50,8 +51,13 @@ function IntegrationListItem(props: {
{props.logo && {title}}
- + {props.name || title} + {props.isTemplate && ( + + Template + + )} {props.description} {/* Alert error that key stopped working. */} diff --git a/apps/web/components/eventtype/EventAppsTab.tsx b/apps/web/components/eventtype/EventAppsTab.tsx index de5dc16b84..715a5f59da 100644 --- a/apps/web/components/eventtype/EventAppsTab.tsx +++ b/apps/web/components/eventtype/EventAppsTab.tsx @@ -1,43 +1,17 @@ import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { useFormContext } from "react-hook-form"; -import EventTypeAppContext, { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext"; -import { EventTypeAddonMap } from "@calcom/app-store/apps.browser.generated"; +import { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext"; +import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface"; import { EventTypeAppCardComponentProps } from "@calcom/app-store/types"; import { EventTypeAppsList } from "@calcom/app-store/utils"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { RouterOutputs, trpc } from "@calcom/trpc/react"; -import { Button, EmptyScreen, ErrorBoundary, Icon } from "@calcom/ui"; +import { trpc } from "@calcom/trpc/react"; +import { Button, EmptyScreen, Icon } from "@calcom/ui"; -type EventType = Pick["eventType"] & +export type EventType = Pick["eventType"] & EventTypeAppCardComponentProps["eventType"]; -function AppCardWrapper({ - app, - eventType, - getAppData, - setAppData, -}: { - app: RouterOutputs["viewer"]["apps"][number]; - eventType: EventType; - getAppData: GetAppData; - setAppData: SetAppData; -}) { - const dirName = app.slug === "stripe" ? "stripepayment" : app.slug; - const Component = EventTypeAddonMap[dirName as keyof typeof EventTypeAddonMap]; - - if (!Component) { - throw new Error('No component found for "' + dirName + '"'); - } - return ( - - - - - - ); -} - export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { const { t } = useLocale(); const { data: eventTypeApps, isLoading } = trpc.viewer.apps.useQuery({ @@ -97,7 +71,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { /> ) : null} {installedApps?.map((app) => ( - { ) : null}
{notInstalledApps?.map((app) => ( - ) { licenseRequired={data.licenseRequired} isProOnly={data.isProOnly} images={source.data?.items as string[] | undefined} + isTemplate={data.isTemplate} // tos="https://zoom.us/terms" // privacy="https://zoom.us/privacy" body={ @@ -70,21 +71,31 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => { if (!singleApp) return { notFound: true }; - const appDirname = app.dirName; + const isTemplate = singleApp.isTemplate; + const appDirname = path.join(isTemplate ? "templates" : "", app.dirName); const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`); const postFilePath = path.join(README_PATH); let source = ""; try { - /* If the app doesn't have a README we fallback to the package description */ source = fs.readFileSync(postFilePath).toString(); + source = source.replace(/{DESCRIPTION}/g, singleApp.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; } const { content, data } = matter(source); - + if (data.items) { + data.items = data.items.map((item: string) => { + if (!item.includes("/api/app-store")) { + // Make relative paths absolute + return `/api/app-store/${appDirname}/${item}`; + } + return item; + }); + } return { props: { source: { content, data }, diff --git a/apps/web/pages/apps/installed/[category].tsx b/apps/web/pages/apps/installed/[category].tsx index d7468cf327..a14a5d5ec4 100644 --- a/apps/web/pages/apps/installed/[category].tsx +++ b/apps/web/pages/apps/installed/[category].tsx @@ -115,6 +115,7 @@ const IntegrationsList = ({ data }: IntegrationsListProps) => { logo={item.logo} description={item.description} separate={true} + isTemplate={item.isTemplate} invalidCredential={item.invalidCredentialIds.length > 0} actions={
diff --git a/package.json b/package.json index 1fc291ea9c..b43afa4eae 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,12 @@ "app-store:build": "yarn app-store-cli build", "app-store:watch": "yarn app-store-cli watch", "app-store": "yarn app-store-cli cli", + "create-app": "yarn app-store create", + "edit-app": "yarn app-store edit", + "delete-app": "yarn app-store delete", + "create-app-template": "yarn app-store create-template", + "edit-app-template": "yarn app-store edit-template", + "delete-app-template": "yarn app-store delete-template", "build": "turbo run build --filter=@calcom/web...", "clean": "find . -name node_modules -o -name .next -o -name .turbo -o -name dist -type d -prune | xargs rm -rf", "db-deploy": "turbo run db-deploy", diff --git a/packages/app-store-cli/README.md b/packages/app-store-cli/README.md new file mode 100644 index 0000000000..48153efd01 --- /dev/null +++ b/packages/app-store-cli/README.md @@ -0,0 +1,9 @@ +## How to build an App using the CLI +Refer to https://developer.cal.com/guides/how-to-build-an-app + +## TODO +- Merge app-store:watch and app-store commands; introduce app-store --watch +- An app created through CLI should be able to completely skip API validation for testing purposes. Credentials should be created with no API specified specific to the app. It would allow us to test any app end to end not worrying about the corresponding API endpoint. +- Someone can add wrong directory name(which doesn't satisfy slug requirements) manually. How to handle it. +- Allow editing and updating app from the cal app itself - including assets uploading when developing locally. +- Use AppDeclarativeHandler across all apps. Whatever isn't supported in it, support that. \ No newline at end of file diff --git a/packages/app-store-cli/package.json b/packages/app-store-cli/package.json index f6a63397fb..22b63dabe7 100644 --- a/packages/app-store-cli/package.json +++ b/packages/app-store-cli/package.json @@ -7,10 +7,10 @@ "node": ">=10" }, "scripts": { - "build": "ts-node --transpile-only src/app-store.ts", + "build": "ts-node --transpile-only src/build.ts", "cli": "ts-node --transpile-only src/cli.tsx", - "watch": "ts-node --transpile-only src/app-store.ts --watch", - "generate": "ts-node --transpile-only src/app-store.ts", + "watch": "ts-node --transpile-only src/build.ts --watch", + "generate": "ts-node --transpile-only src/build.ts", "post-install": "yarn build" }, "files": [ diff --git a/packages/app-store-cli/readme.md b/packages/app-store-cli/readme.md deleted file mode 100644 index 5b095ac91e..0000000000 --- a/packages/app-store-cli/readme.md +++ /dev/null @@ -1,37 +0,0 @@ -## Steps to create an app - -- Create a folder in packages/app-store/{APP_NAME} = {APP} -- Fill it with a sample app - - Modify {APP}/\_metadata.ts with the data provided - -## Approach - -- appType is derived from App Name(a slugify operation that makes a string that can be used as a director name, a variable name for imports and a URL path). -- appType is then used to create the app directory. It becomes `config.type` of config.json. config.type is the value used to create an entry in App table and retrieve any apps or credentials. It also becomes App.dirName -- dirnames that don't start with \_ are considered apps in packages/app-store and based on those apps .generated.ts\* files are created. This allows pre-cli apps to keep on working. -- app directory is populated with app-store/\_baseApp with newly updated config.json and package.json -- `packages/prisma/seed-app-store.config.json` is updated with new app. - -NOTE: After app-store-cli is live, Credential.appId and Credential.type would be same for new apps. For old apps they would remain different. Credential.type would be used to identify credentials in integrations call and Credential.appId/App.slug would be used to identify apps. -If we rename all existing apps to their slug names, we can remove type and then use just appId to refer to things everywhere. This can be done later on. - -## TODO - -- Improvements - - Edit command Improvements - - Prefill fields in edit command -> It allows only that content to change which user wants to change. - - Don't override icon.svg - - Merge app-store:watch and app-store commands; introduce app-store --watch - - Allow inputs in non interactive way as well - That would allow easily copy pasting commands. - - An app created through CLI should be able to completely skip API validation for testing purposes. Credentials should be created with no API specified specific to the app. It would allow us to test any app end to end not worrying about the corresponding API endpoint. - - Require assets path relative to app dir. - -## Roadmap - -- Avoid delete and edit on apps created outside of cli -- Someone can add wrong directory name(which doesn't satisfy slug requirements) manually. How to handle it. -- Allow editing and updating app from the cal app itself - including assets uploading when developing locally. -- Improvements in shared code across app - - Use baseApp/api/add.ts for all apps with configuration of credentials creation and redirection URL. -- Delete creation side effects if App creation fails - Might make debugging difficult - - This is so that web app doesn't break because of additional app folders or faulty db-seed diff --git a/packages/app-store-cli/src/App.tsx b/packages/app-store-cli/src/App.tsx new file mode 100644 index 0000000000..01236c0e49 --- /dev/null +++ b/packages/app-store-cli/src/App.tsx @@ -0,0 +1,49 @@ +import React, { FC } from "react"; +import { SupportedCommands } from "src/types"; + +import Create from "./commandViews/Create"; +import CreateTemplate from "./commandViews/Create"; +import Delete from "./commandViews/Delete"; +import DeleteTemplate from "./commandViews/DeleteTemplate"; +import Edit from "./commandViews/Edit"; +import EditTemplate from "./commandViews/EditTemplate"; + +export const App: FC<{ + template: string; + command: SupportedCommands; + slug?: string; +}> = ({ command, template, slug }) => { + if (command === "create") { + return ; + } + + if (command === "edit") { + return ; + } + + if (command === "edit-template") { + return ; + } + + if (command === "delete") { + if (!slug) { + throw new Error('Slug is required for "delete" command'); + } + return ; + } + + if (command === "create-template") { + return ; + } + + if (command === "delete-template") { + if (!slug) { + throw new Error('Slug is required for "delete-template" command'); + } + return ; + } + + return null; +}; + +export default App; diff --git a/packages/app-store-cli/src/CliApp.tsx b/packages/app-store-cli/src/CliApp.tsx deleted file mode 100644 index eb6fe8f370..0000000000 --- a/packages/app-store-cli/src/CliApp.tsx +++ /dev/null @@ -1,463 +0,0 @@ -import fs from "fs"; -import { Box, Text } from "ink"; -import SelectInput from "ink-select-input"; -import TextInput from "ink-text-input"; -import path from "path"; -import React, { FC, useEffect, useState } from "react"; - -import execSync from "./execSync"; - -const slugify = (str: string) => { - // It is to be a valid dir name, a valid JS variable name and a valid URL path - return str.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase(); -}; - -function getSlugFromAppName(appName: string | null): string | null { - if (!appName) { - return appName; - } - return slugify(appName); -} - -function getAppDirPath(slug: any) { - return path.join(appStoreDir, `${slug}`); -} - -const appStoreDir = path.resolve(__dirname, "..", "..", "app-store"); -const workspaceDir = path.resolve(__dirname, "..", "..", ".."); - -function absolutePath(appRelativePath) { - return path.join(appStoreDir, appRelativePath); -} -const updatePackageJson = ({ slug, appDescription, appDirPath }) => { - const packageJsonConfig = JSON.parse(fs.readFileSync(`${appDirPath}/package.json`).toString()); - packageJsonConfig.name = `@calcom/${slug}`; - packageJsonConfig.description = appDescription; - // packageJsonConfig.description = `@calcom/${appName}`; - fs.writeFileSync(`${appDirPath}/package.json`, JSON.stringify(packageJsonConfig, null, 2)); -}; - -const BaseAppFork = { - create: function* ({ - category, - subCategory, - editMode = false, - appDescription, - appName, - slug, - publisherName, - publisherEmail, - extendsFeature, - }) { - const appDirPath = getAppDirPath(slug); - let message = !editMode ? "Forking base app" : "Updating app"; - yield message; - if (!editMode) { - execSync(`mkdir -p ${appDirPath}`); - execSync(`cp -r ${absolutePath("_baseApp/*")} ${appDirPath}`); - } - updatePackageJson({ slug, appDirPath, appDescription }); - - const categoryToVariantMap = { - video: "conferencing", - }; - - const dataFromCategory = - category === "video" - ? { - appData: { - location: { - type: `integrations:${slug}_video`, - label: `${appName}`, - }, - }, - } - : {}; - const dataFromSubCategory = - category === "video" && subCategory === "static" - ? { - appData: { - ...dataFromCategory.appData, - location: { - ...dataFromCategory.appData.location, - linkType: "static", - organizerInputPlaceholder: "https://anything.anything", - urlRegExp: "", - }, - }, - } - : {}; - let config = { - "/*": "Don't modify slug - If required, do it using cli edit command", - name: appName, - // Plan to remove it. DB already has it and name of dir is also the same. - slug: slug, - type: `${slug}_${category}`, - imageSrc: `/api/app-store/${slug}/icon.svg`, - logo: `/api/app-store/${slug}/icon.svg`, - url: `https://cal.com/apps/${slug}`, - variant: categoryToVariantMap[category] || category, - categories: [category], - publisher: publisherName, - email: publisherEmail, - description: appDescription, - extendsFeature: extendsFeature, - // TODO: Use this to avoid edit and delete on the apps created outside of cli - __createdUsingCli: true, - ...dataFromCategory, - ...dataFromSubCategory, - }; - const currentConfig = JSON.parse(fs.readFileSync(`${appDirPath}/config.json`).toString()); - config = { - ...currentConfig, - ...config, - }; - fs.writeFileSync(`${appDirPath}/config.json`, JSON.stringify(config, null, 2)); - fs.writeFileSync( - `${appDirPath}/DESCRIPTION.md`, - fs - .readFileSync(`${appDirPath}/DESCRIPTION.md`) - .toString() - .replace(/_DESCRIPTION_/g, appDescription) - .replace(/_APP_DIR_/g, slug) - ); - message = !editMode ? "Forked base app" : "Updated app"; - yield message; - }, - delete: function ({ slug }) { - const appDirPath = getAppDirPath(slug); - execSync(`rm -rf ${appDirPath}`); - }, -}; - -const Seed = { - seedConfigPath: absolutePath("../prisma/seed-app-store.config.json"), - update: function ({ slug, category, noDbUpdate }) { - let configContent = "[]"; - try { - if (fs.statSync(this.seedConfigPath)) { - configContent = fs.readFileSync(this.seedConfigPath).toString(); - } - } catch (e) {} - const seedConfig = JSON.parse(configContent); - - if (!seedConfig.find((app) => app.slug === slug)) { - seedConfig.push({ - dirName: slug, - categories: [category], - slug: slug, - type: `${slug}_${category}`, - }); - } - - // 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)); - if (!noDbUpdate) { - execSync(`cd ${workspaceDir} && yarn db-seed`); - } - }, - revert: async function ({ slug, noDbUpdate }) { - let seedConfig = JSON.parse(fs.readFileSync(this.seedConfigPath).toString()); - seedConfig = seedConfig.filter((app) => app.slug !== slug); - fs.writeFileSync(this.seedConfigPath, JSON.stringify(seedConfig, null, 2)); - if (!noDbUpdate) { - execSync(`yarn workspace @calcom/prisma delete-app ${slug}`); - } - }, -}; - -const generateAppFiles = () => { - execSync(`cd ${__dirname} && yarn ts-node --transpile-only src/app-store.ts`); -}; - -const CreateApp = ({ noDbUpdate, slug = null, editMode = false }) => { - // AppName - // Type of App - Other, Calendar, Video, Payment, Messaging, Web3 - const [appInputData, setAppInputData] = useState({}); - const [inputIndex, setInputIndex] = useState(0); - const fields = [ - { label: "App Title", name: "appName", type: "text", explainer: "Keep it very short" }, - { - label: "App Description", - name: "appDescription", - type: "text", - explainer: - "A detailed description of your app. You can later modify DESCRIPTION.md to add slider and other components", - }, - { - label: "Category of App", - name: "appCategory", - type: "select", - options: [ - { label: "Calendar", value: "calendar" }, - { - label: - "Static Link - Video(Apps like Ping.gg/Riverside/Whereby which require you to provide a link to join your room)", - value: "video_static", - }, - { label: "Other - Video", value: "video_other" }, - { label: "Payment", value: "payment" }, - { label: "Messaging", value: "messaging" }, - { label: "Web3", value: "web3" }, - { label: "Automation", value: "automation" }, - { label: "Analytics", value: "analytics" }, - { label: "Other", value: "other" }, - ], - explainer: "This is how apps are categorized in App Store.", - }, - { - label: "What kind of app would you consider it?", - name: "extendsFeature", - options: [ - { label: "User", value: "User" }, - { - label: "Event Type(Available for configuration in Apps tab for all Event Types)", - value: "EventType", - }, - ], - }, - { label: "Publisher Name", name: "publisherName", type: "text", explainer: "Let users know who you are" }, - { - label: "Publisher Email", - name: "publisherEmail", - type: "text", - explainer: "Let users know how they can contact you.", - }, - ]; - const field = fields[inputIndex]; - const fieldLabel = field?.label || ""; - const fieldName = field?.name || ""; - const fieldValue = appInputData[fieldName] || ""; - const appName = appInputData["appName"]; - const rawCategory = appInputData["appCategory"] || ""; - const appDescription = appInputData["appDescription"]; - const publisherName = appInputData["publisherName"]; - const publisherEmail = appInputData["publisherEmail"]; - let extendsFeature = appInputData["extendsFeature"] || []; - if (rawCategory === "analytics") { - // Analytics only means EventType Analytics as of now - extendsFeature = "EventType"; - } - const [status, setStatus] = useState<"inProgress" | "done">("inProgress"); - const allFieldsFilled = inputIndex === fields.length; - const [progressUpdate, setProgressUpdate] = useState(""); - const category = rawCategory.split("_")[0]; - const subCategory = rawCategory.split("_")[1]; - if (!editMode) { - slug = getSlugFromAppName(appName); - } - useEffect(() => { - // When all fields have been filled - if (allFieldsFilled) { - const it = BaseAppFork.create({ - category, - subCategory, - appDescription, - appName, - slug, - publisherName, - publisherEmail, - extendsFeature, - }); - for (const item of it) { - setProgressUpdate(item); - } - - Seed.update({ slug, category, noDbUpdate }); - - generateAppFiles(); - - // FIXME: Even after CLI showing this message, it is stuck doing work before exiting - // So we ask the user to wait for some time - setStatus("done"); - } - }); - - if (!slug && editMode) { - return --slug is required; - } - - if (allFieldsFilled) { - return ( - - - {editMode - ? `Editing app with slug ${slug}` - : `Creating app with name '${appName}' of type '${category}'`} - - {progressUpdate} - {status === "done" ? ( - - - Just wait for a few seconds for process to exit and then you are good to go. Your App code - exists at ${getAppDirPath(slug)} - Tip : Go and change the logo of your app by replacing {getAppDirPath(slug) + "/static/icon.svg"} - - - App Summary: - - - - Slug: - {slug} - - - App URL: - {`http://localhost:3000/apps/${slug}`} - - - Name: - {appName} - - - Description: - {appDescription} - - - Category: - {category} - - - Publisher Name: - {publisherName} - - - Publisher Email: - {publisherEmail} - - - - ) : ( - Please wait... - )} - - Note: You should not rename app directory manually. Use cli only to do that as it needs to be - updated in DB as well - - - ); - } - - // Hack: using field.name == "appTitle" to identify that app Name has been submitted and not being edited. - if (!editMode && field.name === "appTitle" && slug && fs.existsSync(getAppDirPath(slug))) { - return ( - <> - App with slug {slug} already exists. If you want to edit it, use edit command - - ); - } - return ( - - - - {`${fieldLabel}:`} - {field.type == "text" ? ( - { - if (!value) { - return; - } - setInputIndex((index) => { - return index + 1; - }); - }} - onChange={(value) => { - setAppInputData((appInputData) => { - return { - ...appInputData, - [fieldName]: value, - }; - }); - }} - /> - ) : ( - - items={field.options} - onSelect={(item) => { - setAppInputData((appInputData) => { - return { - ...appInputData, - [fieldName]: item.value, - }; - }); - setInputIndex((index) => { - return index + 1; - }); - }} - /> - )} - - - - {field.explainer} - - - - - ); -}; - -const DeleteApp = ({ noDbUpdate, slug }) => { - const [confirmedAppSlug, setConfirmedAppSlug] = useState(""); - const [allowDeletion, setAllowDeletion] = useState(false); - const [state, setState] = useState({ done: null, description: null }); - useEffect(() => { - if (allowDeletion) { - BaseAppFork.delete({ slug }); - Seed.revert({ slug }); - generateAppFiles(); - setState({ description: `App with slug ${slug} has been deleted`, done: true }); - } - }, [allowDeletion, slug]); - return ( - <> - - Confirm the slug of the app that you want to delete. Note, that it would cleanup the app directory, - App table and Credential table - - {!state.done && ( - { - if (value === slug) { - setState({ description: `Deletion started`, done: true }); - setAllowDeletion(true); - } else { - setState({ description: `Slug doesn't match - Should have been ${slug}`, done: true }); - } - }} - onChange={(val) => { - setConfirmedAppSlug(val); - }} - /> - )} - {state.description} - - ); -}; - -const App: FC<{ noDbUpdate?: boolean; command: "create" | "delete"; slug?: string }> = ({ - command, - noDbUpdate, - slug, -}) => { - if (command === "create") { - return ; - } - if (command === "delete") { - return ; - } - if (command === "edit") { - return ; - } -}; -module.exports = App; -export default App; diff --git a/packages/app-store-cli/src/app-store.ts b/packages/app-store-cli/src/app-store.ts deleted file mode 100644 index 2a3a3ec519..0000000000 --- a/packages/app-store-cli/src/app-store.ts +++ /dev/null @@ -1,278 +0,0 @@ -import chokidar from "chokidar"; -import fs from "fs"; -import { debounce } from "lodash"; -import path from "path"; -import prettier from "prettier"; - -import { AppMeta } from "@calcom/types/App"; - -import prettierConfig from "../../config/prettier-preset"; -import execSync from "./execSync"; - -function isFileThere(path) { - try { - fs.statSync(path); - return true; - } catch (e) { - return false; - } -} -let isInWatchMode = false; -if (process.argv[2] === "--watch") { - isInWatchMode = true; -} - -const formatOutput = (source: string) => prettier.format(source, prettierConfig); - -const getVariableName = function (appName: string) { - return appName.replace("-", "_"); -}; - -const getAppId = function (app: { name: string }) { - // Handle stripe separately as it's an old app with different dirName than slug/appId - return app.name === "stripepayment" ? "stripe" : app.name; -}; - -const APP_STORE_PATH = path.join(__dirname, "..", "..", "app-store"); -type App = Partial & { - name: string; - path: string; -}; -function getAppName(candidatePath) { - function isValidAppName(candidatePath) { - if ( - !candidatePath.startsWith("_") && - candidatePath !== "ee" && - !candidatePath.includes("/") && - !candidatePath.includes("\\") - ) { - return candidatePath; - } - } - if (isValidAppName(candidatePath)) { - // Already a dirname of an app - return candidatePath; - } - // Get dirname of app from full path - const dirName = path.relative(APP_STORE_PATH, candidatePath); - return isValidAppName(dirName) ? dirName : null; -} - -function generateFiles() { - const browserOutput = [`import dynamic from "next/dynamic"`]; - const metadataOutput = []; - const schemasOutput = []; - const appKeysSchemasOutput = []; - const serverOutput = []; - const appDirs: { name: string; path: string }[] = []; - - fs.readdirSync(`${APP_STORE_PATH}`).forEach(function (dir) { - if (dir === "ee") { - fs.readdirSync(path.join(APP_STORE_PATH, dir)).forEach(function (eeDir) { - if (fs.statSync(path.join(APP_STORE_PATH, dir, eeDir)).isDirectory()) { - if (!getAppName(path.resolve(eeDir))) { - appDirs.push({ - name: eeDir, - path: path.join(dir, eeDir), - }); - } - } - }); - } else { - if (fs.statSync(path.join(APP_STORE_PATH, dir)).isDirectory()) { - if (!getAppName(dir)) { - return; - } - appDirs.push({ - name: dir, - path: dir, - }); - } - } - }); - - function forEachAppDir(callback: (arg: App) => void) { - for (let i = 0; i < appDirs.length; i++) { - const configPath = path.join(APP_STORE_PATH, appDirs[i].path, "config.json"); - let app; - - if (fs.existsSync(configPath)) { - app = JSON.parse(fs.readFileSync(configPath).toString()); - } else { - app = {}; - } - - callback({ - ...app, - name: appDirs[i].name, - path: appDirs[i].path, - }); - } - } - - forEachAppDir((app) => { - const templateDestinationDir = path.join(APP_STORE_PATH, app.path, "extensions"); - const templateDestinationFilePath = path.join(templateDestinationDir, "EventTypeAppCard.tsx"); - const zodDestinationFilePath = path.join(APP_STORE_PATH, app.path, "zod.ts"); - - if (app.extendsFeature === "EventType" && !isFileThere(templateDestinationFilePath)) { - execSync(`mkdir -p ${templateDestinationDir}`); - execSync(`cp ../app-store/_templates/extensions/EventTypeAppCard.tsx ${templateDestinationFilePath}`); - execSync(`cp ../app-store/_templates/zod.ts ${zodDestinationFilePath}`); - } - }); - - function getObjectExporter( - objectName, - { - fileToBeImported, - importBuilder, - entryBuilder, - }: { - fileToBeImported: string; - importBuilder?: (arg: App) => string; - entryBuilder: (arg: App) => string; - } - ) { - const output = []; - forEachAppDir((app) => { - if ( - fs.existsSync(path.join(APP_STORE_PATH, app.path, fileToBeImported)) && - typeof importBuilder === "function" - ) { - output.push(importBuilder(app)); - } - }); - - output.push(`export const ${objectName} = {`); - - forEachAppDir((app) => { - if (fs.existsSync(path.join(APP_STORE_PATH, app.path, fileToBeImported))) { - output.push(entryBuilder(app)); - } - }); - - output.push(`};`); - return output; - } - - serverOutput.push( - ...getObjectExporter("apiHandlers", { - fileToBeImported: "api/index.ts", - // Import path must have / even for windows and not \ - entryBuilder: (app) => ` "${app.name}": import("./${app.path.replace(/\\/g, "/")}/api"),`, - }) - ); - - metadataOutput.push( - ...getObjectExporter("appStoreMetadata", { - fileToBeImported: "_metadata.ts", - // Import path must have / even for windows and not \ - importBuilder: (app) => - `import { metadata as ${getVariableName(app.name)}_meta } from "./${app.path.replace( - /\\/g, - "/" - )}/_metadata";`, - entryBuilder: (app) => ` "${app.name}":${getVariableName(app.name)}_meta,`, - }) - ); - - schemasOutput.push( - ...getObjectExporter("appDataSchemas", { - fileToBeImported: "zod.ts", - // Import path must have / even for windows and not \ - importBuilder: (app) => - `import { appDataSchema as ${getVariableName(app.name)}_schema } from "./${app.path.replace( - /\\/g, - "/" - )}/zod";`, - // Key must be appId as this is used by eventType metadata and lookup is by appId - entryBuilder: (app) => ` "${getAppId(app)}":${getVariableName(app.name)}_schema ,`, - }) - ); - - appKeysSchemasOutput.push( - ...getObjectExporter("appKeysSchemas", { - fileToBeImported: "zod.ts", - // Import path must have / even for windows and not \ - importBuilder: (app) => - `import { appKeysSchema as ${getVariableName(app.name)}_keys_schema } from "./${app.path.replace( - /\\/g, - "/" - )}/zod";`, - // Key must be appId as this is used by eventType metadata and lookup is by appId - entryBuilder: (app) => ` "${getAppId(app)}":${getVariableName(app.name)}_keys_schema ,`, - }) - ); - - browserOutput.push( - ...getObjectExporter("InstallAppButtonMap", { - fileToBeImported: "components/InstallAppButton.tsx", - entryBuilder: (app) => - ` ${app.name}: dynamic(() =>import("./${app.path}/components/InstallAppButton")),`, - }) - ); - - // TODO: Make a component map creator that accepts ComponentName and does the rest. - // TODO: dailyvideo has a slug of daily-video, so that mapping needs to be taken care of. But it is an old app, so it doesn't need AppSettings - browserOutput.push( - ...getObjectExporter("AppSettingsComponentsMap", { - fileToBeImported: "components/AppSettings.tsx", - entryBuilder: (app) => ` ${app.name}: dynamic(() =>import("./${app.path}/components/AppSettings")),`, - }) - ); - - browserOutput.push( - ...getObjectExporter("EventTypeAddonMap", { - fileToBeImported: path.join("extensions", "EventTypeAppCard.tsx"), - entryBuilder: (app) => - ` ${app.name}: dynamic(() =>import("./${app.path}/extensions/EventTypeAppCard")),`, - }) - ); - - const banner = `/** - This file is autogenerated using the command \`yarn app-store:build --watch\`. - Don't modify this file manually. -**/ -`; - const filesToGenerate: [string, string[]][] = [ - ["apps.metadata.generated.ts", metadataOutput], - ["apps.server.generated.ts", serverOutput], - ["apps.browser.generated.tsx", browserOutput], - ["apps.schemas.generated.ts", schemasOutput], - ["apps.keys-schemas.generated.ts", appKeysSchemasOutput], - ]; - filesToGenerate.forEach(([fileName, output]) => { - fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`)); - }); - console.log(`Generated ${filesToGenerate.map(([fileName]) => fileName).join(", ")}`); -} - -const debouncedGenerateFiles = debounce(generateFiles); - -if (isInWatchMode) { - chokidar - .watch(APP_STORE_PATH) - .on("addDir", (dirPath) => { - const appName = getAppName(dirPath); - if (appName) { - console.log(`Added ${appName}`); - debouncedGenerateFiles(); - } - }) - .on("change", (filePath) => { - if (filePath.endsWith("config.json")) { - console.log("Config file changed"); - debouncedGenerateFiles(); - } - }) - .on("unlinkDir", (dirPath) => { - const appName = getAppName(dirPath); - if (appName) { - console.log(`Removed ${appName}`); - debouncedGenerateFiles(); - } - }); -} else { - generateFiles(); -} diff --git a/packages/app-store-cli/src/build.ts b/packages/app-store-cli/src/build.ts new file mode 100644 index 0000000000..0d34d8fad8 --- /dev/null +++ b/packages/app-store-cli/src/build.ts @@ -0,0 +1,348 @@ +import chokidar from "chokidar"; +import fs from "fs"; +import { debounce } from "lodash"; +import path from "path"; +import prettier from "prettier"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +//@ts-ignore +import prettierConfig from "@calcom/config/prettier-preset"; +import { AppMeta } from "@calcom/types/App"; + +import { APP_STORE_PATH } from "./constants"; +import { getAppName } from "./utils/getAppName"; + +let isInWatchMode = false; +if (process.argv[2] === "--watch") { + isInWatchMode = true; +} + +const formatOutput = (source: string) => + prettier.format(source, { + parser: "babel", + ...prettierConfig, + }); + +const getVariableName = function (appName: string) { + return appName.replace(/[-.]/g, "_"); +}; + +const getAppId = function (app: { name: string }) { + // Handle stripe separately as it's an old app with different dirName than slug/appId + return app.name === "stripepayment" ? "stripe" : app.name; +}; + +type App = Partial & { + name: string; + path: string; +}; +function generateFiles() { + const browserOutput = [`import dynamic from "next/dynamic"`]; + const metadataOutput = []; + const schemasOutput = []; + const appKeysSchemasOutput = []; + const serverOutput = []; + const appDirs: { name: string; path: string }[] = []; + + fs.readdirSync(`${APP_STORE_PATH}`).forEach(function (dir) { + if (dir === "ee" || dir === "templates") { + fs.readdirSync(path.join(APP_STORE_PATH, dir)).forEach(function (subDir) { + if (fs.statSync(path.join(APP_STORE_PATH, dir, subDir)).isDirectory()) { + if (getAppName(subDir)) { + appDirs.push({ + name: subDir, + path: path.join(dir, subDir), + }); + } + } + }); + } else { + if (fs.statSync(path.join(APP_STORE_PATH, dir)).isDirectory()) { + if (!getAppName(dir)) { + return; + } + appDirs.push({ + name: dir, + path: dir, + }); + } + } + }); + + function forEachAppDir(callback: (arg: App) => void) { + for (let i = 0; i < appDirs.length; i++) { + const configPath = path.join(APP_STORE_PATH, appDirs[i].path, "config.json"); + let app; + + if (fs.existsSync(configPath)) { + app = JSON.parse(fs.readFileSync(configPath).toString()); + } else { + app = {}; + } + + callback({ + ...app, + name: appDirs[i].name, + path: appDirs[i].path, + }); + } + } + + /** + * Windows has paths with backslashes, so we need to replace them with forward slashes + * .ts and .tsx files are imported without extensions + * If a file has index.ts or index.tsx, it can be imported after removing the index.ts* part + */ + function getModulePath(path: string, moduleName: string) { + return ( + `./${path.replace(/\\/g, "/")}/` + + moduleName.replace(/\/index\.ts|\/index\.tsx/, "").replace(/\.tsx$|\.ts$/, "") + ); + } + + type ImportConfig = + | { + fileToBeImported: string; + importName?: string; + } + | [ + { + fileToBeImported: string; + importName?: string; + }, + { + fileToBeImported: string; + importName: string; + } + ]; + + /** + * If importConfig is an array, only 2 items are allowed. First one is the main one and second one is the fallback + */ + function getExportedObject( + objectName: string, + { + lazyImport = false, + importConfig, + entryObjectKeyGetter = (app) => app.name, + }: { + lazyImport?: boolean; + importConfig: ImportConfig; + entryObjectKeyGetter?: (arg: App, importName?: string) => string; + } + ) { + const output: string[] = []; + + const getLocalImportName = ( + app: { name: string }, + chosenConfig: ReturnType + ) => `${getVariableName(app.name)}_${getVariableName(chosenConfig.fileToBeImported)}`; + + const fileToBeImportedExists = ( + app: { path: string }, + chosenConfig: ReturnType + ) => fs.existsSync(path.join(APP_STORE_PATH, app.path, chosenConfig.fileToBeImported)); + + addImportStatements(); + createExportObject(); + + return output; + + function addImportStatements() { + forEachAppDir((app) => { + const chosenConfig = getChosenImportConfig(importConfig, app); + if (fileToBeImportedExists(app, chosenConfig) && chosenConfig.importName) { + const importName = chosenConfig.importName; + if (!lazyImport) { + if (importName !== "default") { + // Import with local alias that will be used by createExportObject + output.push( + `import { ${importName} as ${getLocalImportName(app, chosenConfig)} } from "${getModulePath( + app.path, + chosenConfig.fileToBeImported + )}"` + ); + } else { + // Default Import + output.push( + `import ${getLocalImportName(app, chosenConfig)} from "${getModulePath( + app.path, + chosenConfig.fileToBeImported + )}"` + ); + } + } + } + }); + } + + function createExportObject() { + output.push(`export const ${objectName} = {`); + + forEachAppDir((app) => { + const chosenConfig = getChosenImportConfig(importConfig, app); + + if (fileToBeImportedExists(app, chosenConfig)) { + if (!lazyImport) { + const key = entryObjectKeyGetter(app); + output.push(`"${key}": ${getLocalImportName(app, chosenConfig)},`); + } else { + const key = entryObjectKeyGetter(app); + if (chosenConfig.fileToBeImported.endsWith(".tsx")) { + output.push( + `"${key}": dynamic(() => import("${getModulePath( + app.path, + chosenConfig.fileToBeImported + )}")),` + ); + } else { + output.push(`"${key}": import("${getModulePath(app.path, chosenConfig.fileToBeImported)}"),`); + } + } + } + }); + + output.push(`};`); + } + + function getChosenImportConfig(importConfig: ImportConfig, app: { path: string }) { + let chosenConfig; + + if (!(importConfig instanceof Array)) { + chosenConfig = importConfig; + } else { + if (fs.existsSync(path.join(APP_STORE_PATH, app.path, importConfig[0].fileToBeImported))) { + chosenConfig = importConfig[0]; + } else { + chosenConfig = importConfig[1]; + } + } + return chosenConfig; + } + } + + serverOutput.push( + ...getExportedObject("apiHandlers", { + importConfig: { + fileToBeImported: "api/index.ts", + }, + lazyImport: true, + }) + ); + + metadataOutput.push( + ...getExportedObject("appStoreMetadata", { + // Try looking for config.json and if it's not found use _metadata.ts to generate appStoreMetadata + importConfig: [ + { + fileToBeImported: "config.json", + importName: "default", + }, + { + fileToBeImported: "_metadata.ts", + importName: "metadata", + }, + ], + }) + ); + + schemasOutput.push( + ...getExportedObject("appDataSchemas", { + // Import path must have / even for windows and not \ + importConfig: { + fileToBeImported: "zod.ts", + importName: "appDataSchema", + }, + // HACK: Key must be appId as this is used by eventType metadata and lookup is by appId + // This can be removed once we rename the ids of apps like stripe to that of their app folder name + entryObjectKeyGetter: (app) => getAppId(app), + }) + ); + + appKeysSchemasOutput.push( + ...getExportedObject("appKeysSchemas", { + importConfig: { + fileToBeImported: "zod.ts", + importName: "appKeysSchema", + }, + // HACK: Key must be appId as this is used by eventType metadata and lookup is by appId + // This can be removed once we rename the ids of apps like stripe to that of their app folder name + entryObjectKeyGetter: (app) => getAppId(app), + }) + ); + + browserOutput.push( + ...getExportedObject("InstallAppButtonMap", { + importConfig: { + fileToBeImported: "components/InstallAppButton.tsx", + }, + lazyImport: true, + }) + ); + + // TODO: Make a component map creator that accepts ComponentName and does the rest. + // TODO: dailyvideo has a slug of daily-video, so that mapping needs to be taken care of. But it is an old app, so it doesn't need AppSettings + browserOutput.push( + ...getExportedObject("AppSettingsComponentsMap", { + importConfig: { + fileToBeImported: "components/AppSettingsInterface.tsx", + }, + lazyImport: true, + }) + ); + + browserOutput.push( + ...getExportedObject("EventTypeAddonMap", { + importConfig: { + fileToBeImported: path.join("components", "EventTypeAppCardInterface.tsx"), + }, + lazyImport: true, + }) + ); + + const banner = `/** + This file is autogenerated using the command \`yarn app-store:build --watch\`. + Don't modify this file manually. +**/ +`; + const filesToGenerate: [string, string[]][] = [ + ["apps.metadata.generated.ts", metadataOutput], + ["apps.server.generated.ts", serverOutput], + ["apps.browser.generated.tsx", browserOutput], + ["apps.schemas.generated.ts", schemasOutput], + ["apps.keys-schemas.generated.ts", appKeysSchemasOutput], + ]; + filesToGenerate.forEach(([fileName, output]) => { + fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`)); + }); + console.log(`Generated ${filesToGenerate.map(([fileName]) => fileName).join(", ")}`); +} + +const debouncedGenerateFiles = debounce(generateFiles); + +if (isInWatchMode) { + chokidar + .watch(APP_STORE_PATH) + .on("addDir", (dirPath) => { + const appName = getAppName(dirPath); + if (appName) { + console.log(`Added ${appName}`); + debouncedGenerateFiles(); + } + }) + .on("change", (filePath) => { + if (filePath.endsWith("config.json")) { + console.log("Config file changed"); + debouncedGenerateFiles(); + } + }) + .on("unlinkDir", (dirPath) => { + const appName = getAppName(dirPath); + if (appName) { + console.log(`Removed ${appName}`); + debouncedGenerateFiles(); + } + }); +} else { + generateFiles(); +} diff --git a/packages/app-store-cli/src/cli.tsx b/packages/app-store-cli/src/cli.tsx index 3f8196d870..33001ffa1f 100644 --- a/packages/app-store-cli/src/cli.tsx +++ b/packages/app-store-cli/src/cli.tsx @@ -3,23 +3,35 @@ import { render } from "ink"; import meow from "meow"; import React from "react"; -import App from "./CliApp"; +import App from "./App"; +import { SupportedCommands } from "./types"; const cli = meow( ` Usage - $ app-store create/delete/edit - Edit and Delete commands must be used on apps created using cli + $ 'app-store create' or 'app-store create-template' - Creates a new app or template + Options + [--template -t] Template to use. + - Options - [--slug] Slug. This is the name of app dir for apps created with cli. + $ 'app-store edit' or 'app-store edit-template' - Edit the App or Template identified by slug + Options + [--slug -s] Slug. This is the name of app dir for apps created with cli. + + + $ 'app-store delete' or 'app-store delete-template' - Deletes the app or template identified by slug + Options + [--slug -s] Slug. This is the name of app dir for apps created with cli. `, { flags: { - noDbUpdate: { - type: "boolean", - }, slug: { type: "string", + alias: "s", + }, + template: { + type: "string", + alias: "t", }, }, allowUnknownFlags: false, @@ -30,20 +42,32 @@ if (cli.input.length !== 1) { cli.showHelp(); } -const command = cli.input[0] as "create" | "delete" | "edit"; -const supportedCommands = ["create", "delete", "edit"]; +const command = cli.input[0] as SupportedCommands; +const supportedCommands = [ + "create", + "delete", + "edit", + "create-template", + "delete-template", + "edit-template", +] as const; if (!supportedCommands.includes(command)) { cli.showHelp(); } +let slug; -let slug = null; - -if (command === "delete" || command === "edit") { +if ( + command === "delete" || + command === "edit" || + command === "delete-template" || + command === "edit-template" +) { slug = cli.flags.slug; if (!slug) { console.log("--slug is required"); - cli.showHelp(); + cli.showHelp(0); } } -render(); + +render(); diff --git a/packages/app-store-cli/src/commandViews/Create.tsx b/packages/app-store-cli/src/commandViews/Create.tsx new file mode 100644 index 0000000000..ca0e3c8ce2 --- /dev/null +++ b/packages/app-store-cli/src/commandViews/Create.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +import { AppForm } from "../components/AppCreateUpdateForm"; + +export default function Create(props: Omit, "action">) { + return ; +} diff --git a/packages/app-store-cli/src/commandViews/CreateTemplate.tsx b/packages/app-store-cli/src/commandViews/CreateTemplate.tsx new file mode 100644 index 0000000000..fbddc3b208 --- /dev/null +++ b/packages/app-store-cli/src/commandViews/CreateTemplate.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +import { AppForm } from "../components/AppCreateUpdateForm"; + +export default function CreateTemplate(props: Omit, "action">) { + return ; +} diff --git a/packages/app-store-cli/src/commandViews/Delete.tsx b/packages/app-store-cli/src/commandViews/Delete.tsx new file mode 100644 index 0000000000..5282f864bf --- /dev/null +++ b/packages/app-store-cli/src/commandViews/Delete.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +import DeleteForm from "../components/DeleteForm"; + +export default function Delete({ slug }: { slug: string }) { + return ; +} diff --git a/packages/app-store-cli/src/commandViews/DeleteTemplate.tsx b/packages/app-store-cli/src/commandViews/DeleteTemplate.tsx new file mode 100644 index 0000000000..ecc4ff320e --- /dev/null +++ b/packages/app-store-cli/src/commandViews/DeleteTemplate.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +import DeleteForm from "../components/DeleteForm"; + +export default function Delete({ slug }: { slug: string }) { + return ; +} diff --git a/packages/app-store-cli/src/commandViews/Edit.tsx b/packages/app-store-cli/src/commandViews/Edit.tsx new file mode 100644 index 0000000000..faeb409338 --- /dev/null +++ b/packages/app-store-cli/src/commandViews/Edit.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +import { AppForm } from "../components/AppCreateUpdateForm"; + +export default function Edit(props: Omit, "action">) { + return ; +} diff --git a/packages/app-store-cli/src/commandViews/EditTemplate.tsx b/packages/app-store-cli/src/commandViews/EditTemplate.tsx new file mode 100644 index 0000000000..4934d2c52d --- /dev/null +++ b/packages/app-store-cli/src/commandViews/EditTemplate.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +import { AppForm } from "../components/AppCreateUpdateForm"; + +export default function Edit(props: Omit, "action">) { + return ; +} diff --git a/packages/app-store-cli/src/components/AppCreateUpdateForm.tsx b/packages/app-store-cli/src/components/AppCreateUpdateForm.tsx new file mode 100644 index 0000000000..ede4e13935 --- /dev/null +++ b/packages/app-store-cli/src/components/AppCreateUpdateForm.tsx @@ -0,0 +1,353 @@ +import fs from "fs"; +import { Box, Newline, Text, useApp } from "ink"; +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 { getSlugFromAppName, BaseAppFork, Seed, generateAppFiles, getAppDirPath } from "../core"; +import { getApp } from "../utils/getApp"; +import Templates from "../utils/templates"; +import Label from "./Label"; +import { Message } from "./Message"; + +export const AppForm = ({ + template: cliTemplate = "", + slug: givenSlug = "", + action, +}: { + template?: string; + slug?: string; + action: "create" | "edit" | "create-template" | "edit-template"; +}) => { + cliTemplate = Templates.find((t) => t.value === cliTemplate)?.value || ""; + const { exit } = useApp(); + const isTemplate = action === "create-template" || action === "edit-template"; + const isEditAction = action === "edit" || action === "edit-template"; + let initialConfig = { + template: cliTemplate, + name: "", + description: "", + category: "", + publisher: "", + email: "", + }; + + const [app] = useState(() => getApp(givenSlug, isTemplate)); + + if ((givenSlug && action === "edit-template") || action === "edit") + try { + const config = JSON.parse( + fs.readFileSync(`${getAppDirPath(givenSlug, isTemplate)}/config.json`).toString() + ) as AppMeta; + initialConfig = { + ...config, + category: config.categories[0], + template: config.__template, + }; + } catch (e) {} + + const fields = [ + { + label: "App Title", + name: "name", + type: "text", + explainer: "Keep it short and sweet like 'Google Meet'", + optional: false, + defaultValue: "", + }, + { + label: "App Description", + name: "description", + type: "text", + explainer: + "A detailed description of your app. You can later modify DESCRIPTION.mdx to add markdown as well", + optional: false, + defaultValue: "", + }, + // You can't edit the base template of an App or Template - You need to start fresh for that. + cliTemplate || isEditAction + ? null + : { + label: "Choose a base Template", + name: "template", + type: "select", + options: Templates, + optional: false, + defaultValue: "", + }, + { + optional: false, + label: "Category of App", + name: "category", + type: "select", + options: [ + { label: "Calendar", value: "calendar" }, + { label: "Video", value: "video" }, + { label: "Payment", value: "payment" }, + { label: "Messaging", value: "messaging" }, + { label: "Web3", value: "web3" }, + { label: "Automation", value: "automation" }, + { label: "Analytics", value: "analytics" }, + { label: "Other", value: "other" }, + ], + defaultValue: "", + explainer: "This is how apps are categorized in App Store.", + }, + { + optional: true, + label: "Publisher Name", + name: "publisher", + type: "text", + explainer: "Let users know who you are", + defaultValue: "Your Name", + }, + { + optional: true, + label: "Publisher Email", + name: "email", + type: "text", + explainer: "Let users know how they can contact you.", + defaultValue: "email@example.com", + }, + ].filter((f) => f); + const [appInputData, setAppInputData] = useState(initialConfig); + const [inputIndex, setInputIndex] = useState(0); + const [slugFinalized, setSlugFinalized] = useState(false); + + const field = fields[inputIndex]; + const fieldLabel = field?.label || ""; + const fieldName = field?.name || ""; + let fieldValue = appInputData[fieldName as keyof typeof appInputData] || ""; + let validationResult: Parameters[0]["message"] | null = null; + const { name, category, description, publisher, email, template } = appInputData; + + const [status, setStatus] = useState<"inProgress" | "done">("inProgress"); + const formCompleted = inputIndex === fields.length; + if (field?.name === "appCategory") { + // Use template category as the default category + fieldValue = Templates.find((t) => t.value === appInputData["template"])?.category || ""; + } + const slug = getSlugFromAppName(name) || givenSlug; + + useEffect(() => { + // When all fields have been filled + (async () => { + if (formCompleted) { + await BaseAppFork.create({ + category, + description, + name, + slug, + publisher, + email, + template, + editMode: isEditAction, + isTemplate, + 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 + // So we ask the user to wait for some time + setStatus("done"); + } + })(); + }, [formCompleted]); + + if (action === "edit" || action === "edit-template") { + if (!slug) { + return --slug is required; + } + if (!app) { + return ( + + ); + } + } + + if (status === "done") { + // HACK: This is a hack to exit the process manually because due to some reason cli isn't automatically exiting + setTimeout(() => { + exit(); + }, 500); + } + + if (formCompleted) { + return ( + + {status !== "done" && ( + + )} + {status === "done" && ( + + + Just wait for a few seconds for process to exit and then you are good to go. Your{" "} + {isTemplate ? "Template" : "App"} code exists at {getAppDirPath(slug, isTemplate)} + + + Tip : Go and change the logo of your {isTemplate ? "template" : "app"} by replacing{" "} + {getAppDirPath(slug, isTemplate) + "/static/icon.svg"} + + + + App Summary: + + + + Slug: + {slug} + + + {isTemplate ? "Template" : "App"} URL: + {`http://localhost:3000/apps/${slug}`} + + + Name: + {name} + + + Description: + {description} + + + Category: + {category} + + + Publisher Name: + {publisher} + + + Publisher Email: + {email} + + + + )} + + Note: You should not rename app directory manually. Use cli only to do that as it needs to be + updated in DB as well + + + ); + } + if (slug && slug !== givenSlug && fs.existsSync(getAppDirPath(slug, isTemplate))) { + validationResult = { + text: `${ + action === "create" ? "App" : "Template" + } with slug ${slug} already exists. If you want to edit it, use edit command`, + type: "error", + }; + + if (slugFinalized) { + return ; + } + } + const selectedOptionIndex = + field?.type === "select" ? field?.options?.findIndex((o) => o.value === fieldValue) : 0; + return ( + + + {isEditAction ? ( + + ) : ( + + )} + + + {field?.type == "text" ? ( + { + if (!value && !field.optional) { + return; + } + setSlugFinalized(true); + setInputIndex((index) => { + return index + 1; + }); + }} + onChange={(value) => { + setAppInputData((appInputData) => { + return { + ...appInputData, + [fieldName]: value, + }; + }); + }} + /> + ) : ( + + items={field?.options} + itemComponent={(item) => { + const myItem = item as { value: string; label: string }; + return ( + + + {myItem.value}: + + {item.label} + + ); + }} + key={fieldName} + initialIndex={selectedOptionIndex === -1 ? 0 : selectedOptionIndex} + onSelect={(item) => { + setAppInputData((appInputData) => { + return { + ...appInputData, + [fieldName]: item.value, + }; + }); + setInputIndex((index) => { + return index + 1; + }); + }} + /> + )} + + + {validationResult ? ( + + ) : ( + + {field?.explainer} + + )} + + + + ); +}; diff --git a/packages/app-store-cli/src/components/DeleteForm.tsx b/packages/app-store-cli/src/components/DeleteForm.tsx new file mode 100644 index 0000000000..b1a3768f8c --- /dev/null +++ b/packages/app-store-cli/src/components/DeleteForm.tsx @@ -0,0 +1,109 @@ +import { Text } from "ink"; +import TextInput from "ink-text-input"; +import React, { useEffect, useState } from "react"; + +import { ImportantText } from "../components/ImportantText"; +import { Message } from "../components/Message"; +import { BaseAppFork, Seed, generateAppFiles } from "../core"; +import { getApp } from "../utils/getApp"; + +export default function DeleteForm({ slug, action }: { slug: string; action: "delete" | "delete-template" }) { + const [confirmedAppSlug, setConfirmedAppSlug] = useState(""); + const [state, setState] = useState< + | "INITIALIZED" + | "DELETION_CONFIRMATION_FAILED" + | "DELETION_CONFIRMATION_SUCCESSFUL" + | "DELETION_COMPLETED" + | "APP_NOT_EXISTS" + >("INITIALIZED"); + const isTemplate = action === "delete-template"; + const app = getApp(slug, isTemplate); + useEffect(() => { + if (!app) { + setState("APP_NOT_EXISTS"); + } + }, []); + + useEffect(() => { + 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"); + })(); + } + }, [slug, state]); + + if (state === "INITIALIZED") { + return ( + <> + + Type below the slug of the {isTemplate ? "Template" : "App"} that you want to delete. + + + It would cleanup the app directory and App table and Credential table. + + { + if (value === slug) { + setState("DELETION_CONFIRMATION_SUCCESSFUL"); + } else { + setState("DELETION_CONFIRMATION_FAILED"); + } + }} + onChange={(val) => { + setConfirmedAppSlug(val); + }} + /> + + ); + } + if (state === "APP_NOT_EXISTS") { + return ( + + ); + } + if (state === "DELETION_CONFIRMATION_SUCCESSFUL") { + return ( + + ); + } + + if (state === "DELETION_COMPLETED") { + return ( + + ); + } + if (state === "DELETION_CONFIRMATION_FAILED") { + return ( + + ); + } + return null; +} diff --git a/packages/app-store-cli/src/components/ImportantText.tsx b/packages/app-store-cli/src/components/ImportantText.tsx new file mode 100644 index 0000000000..7920a6ab9b --- /dev/null +++ b/packages/app-store-cli/src/components/ImportantText.tsx @@ -0,0 +1,6 @@ +import { Text } from "ink"; +import React from "react"; + +export function ImportantText({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/packages/app-store-cli/src/components/Label.tsx b/packages/app-store-cli/src/components/Label.tsx new file mode 100644 index 0000000000..c3288bf138 --- /dev/null +++ b/packages/app-store-cli/src/components/Label.tsx @@ -0,0 +1,11 @@ +import { Box, Text } from "ink"; +import React from "react"; + +export default function Label({ children }: { children: React.ReactNode }) { + return ( + + {children} + : + + ); +} diff --git a/packages/app-store-cli/src/components/Message.tsx b/packages/app-store-cli/src/components/Message.tsx new file mode 100644 index 0000000000..949275a875 --- /dev/null +++ b/packages/app-store-cli/src/components/Message.tsx @@ -0,0 +1,29 @@ +import { Text } from "ink"; +import React, { useEffect, useState } from "react"; + +export function Message({ + message, +}: { + message: { text: string; type?: "info" | "error" | "success"; showInProgressIndicator?: boolean }; +}) { + const color = message.type === "success" ? "green" : message.type === "error" ? "red" : "white"; + const [progressText, setProgressText] = useState("..."); + useEffect(() => { + if (message.showInProgressIndicator) { + const interval = setInterval(() => { + setProgressText((progressText) => { + return progressText.length > 3 ? "" : progressText + "."; + }); + }, 1000); + return () => { + clearInterval(interval); + }; + } + }, [message.showInProgressIndicator]); + return ( + + {message.text} + {message.showInProgressIndicator && progressText} + + ); +} diff --git a/packages/app-store-cli/src/constants.ts b/packages/app-store-cli/src/constants.ts new file mode 100644 index 0000000000..500c973154 --- /dev/null +++ b/packages/app-store-cli/src/constants.ts @@ -0,0 +1,4 @@ +import path from "path"; + +export const APP_STORE_PATH = path.join(__dirname, "..", "..", "app-store"); +export const TEMPLATES_PATH = path.join(APP_STORE_PATH, "templates"); diff --git a/packages/app-store-cli/src/core.ts b/packages/app-store-cli/src/core.ts new file mode 100644 index 0000000000..a38d525cf0 --- /dev/null +++ b/packages/app-store-cli/src/core.ts @@ -0,0 +1,192 @@ +import fs from "fs"; +import path from "path"; + +import seedAppStoreConfig from "@calcom/prisma/seed-app-store.config.json"; + +import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants"; +import execSync from "./utils/execSync"; + +const slugify = (str: string) => { + // A valid dir name + // A valid URL path + // It is okay to not be a valid variable name. This is so that we can use hyphens which look better then underscores in URL and as directory name + return str.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase(); +}; + +export function getSlugFromAppName(appName: string): string { + if (!appName) { + return appName; + } + return slugify(appName); +} + +export function getAppDirPath(slug: string, isTemplate: boolean) { + if (!isTemplate) { + return path.join(APP_STORE_PATH, `${slug}`); + } + return path.join(TEMPLATES_PATH, `${slug}`); +} + +function absolutePath(appRelativePath: string) { + return path.join(APP_STORE_PATH, appRelativePath); +} + +const updatePackageJson = ({ + slug, + appDescription, + appDirPath, +}: { + slug: string; + appDescription: string; + appDirPath: string; +}) => { + const packageJsonConfig = JSON.parse(fs.readFileSync(`${appDirPath}/package.json`).toString()); + packageJsonConfig.name = `@calcom/${slug}`; + packageJsonConfig.description = appDescription; + // packageJsonConfig.description = `@calcom/${appName}`; + fs.writeFileSync(`${appDirPath}/package.json`, JSON.stringify(packageJsonConfig, null, 2)); +}; + +const workspaceDir = path.resolve(__dirname, "..", "..", ".."); + +export const BaseAppFork = { + create: async function ({ + category, + editMode = false, + description, + name, + slug, + publisher, + email, + template, + isTemplate, + oldSlug, + }: { + category: string; + editMode?: boolean; + description: string; + name: string; + slug: string; + publisher: string; + email: string; + template: string; + isTemplate: boolean; + oldSlug?: string; + }) { + const appDirPath = getAppDirPath(slug, isTemplate); + if (!editMode) { + await execSync(`mkdir -p ${appDirPath}`); + await execSync(`cp -r ${TEMPLATES_PATH}/${template}/* ${appDirPath}`); + } else { + if (!oldSlug) { + throw new Error("oldSlug is required when editMode is true"); + } + if (oldSlug !== slug) { + // We need to rename only if they are different + const oldAppDirPath = getAppDirPath(oldSlug, isTemplate); + + await execSync(`mv ${oldAppDirPath} ${appDirPath}`); + } + } + updatePackageJson({ slug, appDirPath, appDescription: description }); + + const categoryToVariantMap = { + video: "conferencing", + }; + + let config = { + name: name, + // Plan to remove it. DB already has it and name of dir is also the same. + slug: slug, + type: `${slug}_${category}`, + // TODO: Remove usage of imageSrc, it is being used in ConnectCalendars.tsx. After that delete imageSrc in all configs and from here + imageSrc: `icon.svg`, + logo: `icon.svg`, + variant: categoryToVariantMap[category as keyof typeof categoryToVariantMap] || category, + categories: [category], + publisher: publisher, + email: email, + description: description, + // TODO: Use this to avoid edit and delete on the apps created outside of cli + __createdUsingCli: true, + isTemplate, + // Store the template used to create an app + __template: template, + }; + const currentConfig = JSON.parse(fs.readFileSync(`${appDirPath}/config.json`).toString()); + config = { + ...currentConfig, + ...config, + }; + fs.writeFileSync(`${appDirPath}/config.json`, JSON.stringify(config, null, 2)); + fs.writeFileSync( + `${appDirPath}/DESCRIPTION.md`, + fs + .readFileSync(`${appDirPath}/DESCRIPTION.md`) + .toString() + .replace(/_DESCRIPTION_/g, description) + .replace(/_APP_DIR_/g, slug) + ); + }, + + delete: async function ({ slug, isTemplate }: { slug: string; isTemplate: boolean }) { + const appDirPath = getAppDirPath(slug, isTemplate); + await execSync(`rm -rf ${appDirPath}`); + }, +}; + +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-cli/src/execSync.ts b/packages/app-store-cli/src/execSync.ts deleted file mode 100644 index 807fbd7e31..0000000000 --- a/packages/app-store-cli/src/execSync.ts +++ /dev/null @@ -1,13 +0,0 @@ -import child_process from "child_process"; - -const execSync = (cmd: string) => { - if (process.env.DEBUG === "1") { - console.log(`${process.cwd()}$: ${cmd}`); - } - const result = child_process.execSync(cmd).toString(); - if (process.env.DEBUG === "1") { - console.log(result); - } - return cmd; -}; -export default execSync; diff --git a/packages/app-store-cli/src/types.d.ts b/packages/app-store-cli/src/types.d.ts new file mode 100644 index 0000000000..b1a3366c4d --- /dev/null +++ b/packages/app-store-cli/src/types.d.ts @@ -0,0 +1,7 @@ +export type SupportedCommands = + | "create" + | "edit" + | "delete" + | "create-template" + | "delete-template" + | "edit-template"; diff --git a/packages/app-store-cli/src/utils/execSync.ts b/packages/app-store-cli/src/utils/execSync.ts new file mode 100644 index 0000000000..dce49a2ef6 --- /dev/null +++ b/packages/app-store-cli/src/utils/execSync.ts @@ -0,0 +1,26 @@ +import child_process from "child_process"; + +const execSync = async (cmd: string) => { + const silent = process.env.DEBUG === "1" ? false : true; + if (!silent) { + console.log(`${process.cwd()}$: ${cmd}`); + } + const result: string = await new Promise((resolve, reject) => { + child_process.exec(cmd, (err, stdout, stderr) => { + if (err) { + reject(err); + console.log(err); + } + if (stderr && !silent) { + console.log(stderr); + } + resolve(stdout); + }); + }); + + if (!silent) { + console.log(result.toString()); + } + return cmd; +}; +export default execSync; diff --git a/packages/app-store-cli/src/utils/getApp.ts b/packages/app-store-cli/src/utils/getApp.ts new file mode 100644 index 0000000000..6ff3e9c4f1 --- /dev/null +++ b/packages/app-store-cli/src/utils/getApp.ts @@ -0,0 +1,26 @@ +import fs from "fs"; +import path from "path"; + +import { APP_STORE_PATH, TEMPLATES_PATH } from "../constants"; +import { getAppName } from "./getAppName"; + +export const getApp = (slug: string, isTemplate: boolean) => { + const base = isTemplate ? TEMPLATES_PATH : APP_STORE_PATH; + const foundApp = fs + .readdirSync(base) + .filter((dir) => { + if (fs.statSync(path.join(base, dir)).isDirectory() && getAppName(dir)) { + return true; + } + return false; + }) + .find((appName) => appName === slug); + if (foundApp) { + try { + return JSON.parse(fs.readFileSync(path.join(base, foundApp, "config.json")).toString()); + } catch (e) { + return {}; + } + } + return null; +}; diff --git a/packages/app-store-cli/src/utils/getAppName.ts b/packages/app-store-cli/src/utils/getAppName.ts new file mode 100644 index 0000000000..396ef35b8a --- /dev/null +++ b/packages/app-store-cli/src/utils/getAppName.ts @@ -0,0 +1,23 @@ +import path from "path"; + +import { APP_STORE_PATH } from "../constants"; + +export function getAppName(candidatePath) { + function isValidAppName(candidatePath) { + if ( + !candidatePath.startsWith("_") && + candidatePath !== "ee" && + !candidatePath.includes("/") && + !candidatePath.includes("\\") + ) { + return candidatePath; + } + } + if (isValidAppName(candidatePath)) { + // Already a dirname of an app + return candidatePath; + } + // Get dirname of app from full path + const dirName = path.relative(APP_STORE_PATH, candidatePath); + return isValidAppName(dirName) ? dirName : null; +} diff --git a/packages/app-store-cli/src/utils/templates.ts b/packages/app-store-cli/src/utils/templates.ts new file mode 100644 index 0000000000..f2e786156b --- /dev/null +++ b/packages/app-store-cli/src/utils/templates.ts @@ -0,0 +1,29 @@ +import fs from "fs"; +import path from "path"; + +import { TEMPLATES_PATH } from "../constants"; +import { getAppName } from "./getAppName"; + +const Templates = fs + .readdirSync(TEMPLATES_PATH) + .filter((dir) => { + if (fs.statSync(path.join(TEMPLATES_PATH, dir)).isDirectory() && getAppName(dir)) { + return true; + } + return false; + }) + .map((dir) => { + try { + const config = JSON.parse(fs.readFileSync(path.join(TEMPLATES_PATH, dir, "config.json")).toString()); + return { + label: `${config.description}`, + value: dir, + category: config.categories[0], + }; + } catch (e) { + // config.json might not exist + return null; + } + }) + .filter((item) => !!item) as { label: string; value: string; category: string }[]; +export default Templates; diff --git a/packages/app-store-cli/tsconfig.json b/packages/app-store-cli/tsconfig.json index abbeb20d05..5e2711b276 100644 --- a/packages/app-store-cli/tsconfig.json +++ b/packages/app-store-cli/tsconfig.json @@ -1,12 +1,14 @@ { "compilerOptions": { + "strict": true, "module": "commonjs", "jsx": "react", "esModuleInterop": true, "outDir": "dist", "noEmitOnError": false, "target": "ES2020", - "baseUrl": "." + "baseUrl": ".", + "resolveJsonModule": true }, "include": [ "next-env.d.ts", diff --git a/packages/app-store/BookingPageTagManager.tsx b/packages/app-store/BookingPageTagManager.tsx index d22f1c5312..b6a2f248d1 100644 --- a/packages/app-store/BookingPageTagManager.tsx +++ b/packages/app-store/BookingPageTagManager.tsx @@ -1,13 +1,9 @@ import Script from "next/script"; +import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; import { getEventTypeAppData } from "@calcom/app-store/utils"; -import { trackingApps } from "./eventTypeAnalytics"; - -export type AppScript = { attrs?: Record } & ( - | { src: undefined; content?: string } - | { src?: string; content: undefined } -); +import { appDataSchemas } from "./apps.schemas.generated"; export default function BookingPageTagManager({ eventType, @@ -16,16 +12,20 @@ export default function BookingPageTagManager({ }) { return ( <> - {Object.entries(trackingApps).map(([appId, scriptConfig]) => { - const trackingId = getEventTypeAppData(eventType, appId as keyof typeof trackingApps)?.trackingId; + {Object.entries(appStoreMetadata).map(([appId, app]) => { + const tag = app.appData?.tag; + if (!tag) { + return null; + } + const trackingId = getEventTypeAppData(eventType, appId as keyof typeof appDataSchemas)?.trackingId; if (!trackingId) { return null; } const parseValue = (val: T): T => val ? (val.replace(/\{TRACKING_ID\}/g, trackingId) as T) : val; - return scriptConfig.scripts.map((script, index) => { - const parsedAttributes: NonNullable = {}; + return tag.scripts.map((script, index) => { + const parsedAttributes: NonNullable = {}; const attrs = script.attrs || {}; Object.entries(attrs).forEach(([name, value]) => { if (typeof value === "string") { diff --git a/packages/app-store/_appRegistry.ts b/packages/app-store/_appRegistry.ts index b6e464a59e..ec63be2ba4 100644 --- a/packages/app-store/_appRegistry.ts +++ b/packages/app-store/_appRegistry.ts @@ -1,25 +1,18 @@ +import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; import prisma, { safeAppSelect, safeCredentialSelect } from "@calcom/prisma"; import { AppFrontendPayload as App } from "@calcom/types/App"; import { CredentialFrontendPayload as Credential } from "@calcom/types/Credential"; export async function getAppWithMetadata(app: { dirName: string }) { - let appMetadata: App | null = null; - try { - appMetadata = (await import(`./${app.dirName}/_metadata`)).default as App; - } catch (error) { - try { - appMetadata = (await import(`./ee/${app.dirName}/_metadata`)).default as App; - } catch (e) { - if (error instanceof Error) { - console.error(`No metadata found for: "${app.dirName}". Message:`, error.message); - } - return null; - } - } + const appMetadata: App | null = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata] 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 const { key, ...metadata } = appMetadata; + if (metadata.logo && !metadata.logo.includes("/api/app-store/")) { + const appDirName = `${metadata.isTemplate ? "templates" : ""}/${app.dirName}`; + metadata.logo = `/api/app-store/${appDirName}/${metadata.logo}`; + } return metadata; } diff --git a/packages/app-store/_baseApp/DESCRIPTION.md b/packages/app-store/_baseApp/DESCRIPTION.md deleted file mode 100644 index 47afcfe654..0000000000 --- a/packages/app-store/_baseApp/DESCRIPTION.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -description: _DESCRIPTION_ -items: - - /api/app-store/_APP_DIR_/1.jpg - - /api/app-store/_APP_DIR_/2.jpg - - /api/app-store/_APP_DIR_/3.jpg ---- - -_DESCRIPTION_ diff --git a/packages/app-store/_baseApp/_metadata.ts b/packages/app-store/_baseApp/_metadata.ts deleted file mode 100644 index 9c7f2aa320..0000000000 --- a/packages/app-store/_baseApp/_metadata.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { AppMeta } from "@calcom/types/App"; - -import config from "./config.json"; - -export const metadata = { - category: "other", - ...config, -} as AppMeta; - -export default metadata; diff --git a/packages/app-store/_baseApp/config.json b/packages/app-store/_baseApp/config.json deleted file mode 100644 index 74be1674b4..0000000000 --- a/packages/app-store/_baseApp/config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "/*": "This file would be automatically updated by cli according to the inputs" -} diff --git a/packages/app-store/_baseApp/index.ts b/packages/app-store/_baseApp/index.ts deleted file mode 100644 index 5d372ceda3..0000000000 --- a/packages/app-store/_baseApp/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as api from "./api"; -export { metadata } from "./_metadata"; diff --git a/packages/app-store/_baseApp/static/1.jpg b/packages/app-store/_baseApp/static/1.jpg deleted file mode 100644 index 3ef5a741ed..0000000000 Binary files a/packages/app-store/_baseApp/static/1.jpg and /dev/null differ diff --git a/packages/app-store/_baseApp/static/2.jpg b/packages/app-store/_baseApp/static/2.jpg deleted file mode 100644 index cbba9b938e..0000000000 Binary files a/packages/app-store/_baseApp/static/2.jpg and /dev/null differ diff --git a/packages/app-store/_baseApp/static/3.jpg b/packages/app-store/_baseApp/static/3.jpg deleted file mode 100644 index cbba9b938e..0000000000 Binary files a/packages/app-store/_baseApp/static/3.jpg and /dev/null differ diff --git a/packages/app-store/_baseApp/static/icon.svg b/packages/app-store/_baseApp/static/icon.svg deleted file mode 100644 index 059dfae262..0000000000 --- a/packages/app-store/_baseApp/static/icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/app-store/_components/DynamicComponent.tsx b/packages/app-store/_components/DynamicComponent.tsx index 26ca507292..6d216e9959 100644 --- a/packages/app-store/_components/DynamicComponent.tsx +++ b/packages/app-store/_components/DynamicComponent.tsx @@ -4,10 +4,12 @@ export function DynamicComponent>(props: { wrapperClassName?: string; }) { const { componentMap, slug, ...rest } = props; + const dirName = slug === "stripe" ? "stripepayment" : slug; + // There can be apps with no matching component if (!componentMap[slug]) return null; - const Component = componentMap[slug]; + const Component = componentMap[dirName]; return (
diff --git a/packages/app-store/_components/EventTypeAppCardInterface.tsx b/packages/app-store/_components/EventTypeAppCardInterface.tsx new file mode 100644 index 0000000000..0c9b8fc08a --- /dev/null +++ b/packages/app-store/_components/EventTypeAppCardInterface.tsx @@ -0,0 +1,23 @@ +import EventTypeAppContext, { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext"; +import { EventTypeAddonMap } from "@calcom/app-store/apps.browser.generated"; +import { RouterOutputs } from "@calcom/trpc/react"; +import { ErrorBoundary } from "@calcom/ui"; + +import { EventTypeAppCardComponentProps } from "../types"; +import { DynamicComponent } from "./DynamicComponent"; + +export const EventTypeAppCard = (props: { + app: RouterOutputs["viewer"]["apps"][number]; + eventType: EventTypeAppCardComponentProps["eventType"]; + getAppData: GetAppData; + setAppData: SetAppData; +}) => { + const { app, getAppData, setAppData } = props; + return ( + + + + + + ); +}; diff --git a/packages/app-store/_example/_metadata.ts b/packages/app-store/_example/_metadata.ts deleted file mode 100644 index 39e5d653db..0000000000 --- a/packages/app-store/_example/_metadata.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { AppMeta } from "@calcom/types/App"; - -import _package from "./package.json"; - -export const metadata = { - name: _package.name, - description: _package.description, - installed: true, - category: "video", - // If using static next public folder, can then be referenced from the base URL (/). - imageSrc: "/api/app-store/_example/icon.svg", - logo: "/api/app-store/_example/icon.svg", - publisher: "Cal.com", - rating: 5, - reviews: 69, - slug: "example_video", - title: "Example App", - trending: true, - type: "example_video", - url: "https://cal.com/", - variant: "conferencing", - verified: true, - email: "help@cal.com", -} as AppMeta; - -export default metadata; diff --git a/packages/app-store/_example/api/example.ts b/packages/app-store/_example/api/example.ts deleted file mode 100644 index 38dc4766ac..0000000000 --- a/packages/app-store/_example/api/example.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -/** - * This is an example endpoint for an app, these will run under `/api/integrations/[...args]` - * @param req - * @param res - */ -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - res.status(200); -} diff --git a/packages/app-store/_example/api/index.ts b/packages/app-store/_example/api/index.ts deleted file mode 100644 index c10a1b92b3..0000000000 --- a/packages/app-store/_example/api/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as example } from "./example"; diff --git a/packages/app-store/_example/components/InstallAppButton.tsx b/packages/app-store/_example/components/InstallAppButton.tsx deleted file mode 100644 index 4316d0b4af..0000000000 --- a/packages/app-store/_example/components/InstallAppButton.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { InstallAppButtonProps } from "../../types"; - -export default function InstallAppButton(props: InstallAppButtonProps) { - return ( - <> - {props.render({ - onClick() { - alert("You can put your install code in here!"); - }, - })} - - ); -} diff --git a/packages/app-store/_example/components/index.ts b/packages/app-store/_example/components/index.ts deleted file mode 100644 index 0d6008d4ca..0000000000 --- a/packages/app-store/_example/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as InstallAppButton } from "./InstallAppButton"; diff --git a/packages/app-store/_example/index.ts b/packages/app-store/_example/index.ts deleted file mode 100644 index 5373eb04ef..0000000000 --- a/packages/app-store/_example/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * as api from "./api"; -export * as lib from "./lib"; -export { metadata } from "./_metadata"; diff --git a/packages/app-store/_example/lib/VideoApiAdapter.ts b/packages/app-store/_example/lib/VideoApiAdapter.ts deleted file mode 100644 index 86609db478..0000000000 --- a/packages/app-store/_example/lib/VideoApiAdapter.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { VideoApiAdapterFactory } from "@calcom/types/VideoApiAdapter"; - -/** This is a barebones factory function for a video integration */ -const ExampleVideoApiAdapter: VideoApiAdapterFactory = (credential) => { - return { - getAvailability: async () => { - try { - return []; - } catch (err) { - console.error(err); - return []; - } - }, - createMeeting: async (event) => { - return Promise.resolve({ - type: "example_video", - id: "", - password: "", - url: "", - }); - }, - deleteMeeting: async (uid) => { - return Promise.resolve(); - }, - updateMeeting: async (bookingRef, event) => { - return Promise.resolve({ - type: "example_video", - id: bookingRef.meetingId as string, - password: bookingRef.meetingPassword as string, - url: bookingRef.meetingUrl as string, - }); - }, - }; -}; - -export default ExampleVideoApiAdapter; diff --git a/packages/app-store/_example/lib/index.ts b/packages/app-store/_example/lib/index.ts deleted file mode 100644 index dc61768d60..0000000000 --- a/packages/app-store/_example/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as VideoApiAdapter } from "./VideoApiAdapter"; diff --git a/packages/app-store/_example/static/icon.svg b/packages/app-store/_example/static/icon.svg deleted file mode 100644 index d8f2d80f09..0000000000 --- a/packages/app-store/_example/static/icon.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/packages/app-store/amie/api/add.ts b/packages/app-store/amie/api/add.ts index 3304d3dd57..2537562817 100644 --- a/packages/app-store/amie/api/add.ts +++ b/packages/app-store/amie/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/amie/config.json b/packages/app-store/amie/config.json index cc7ffcd8fd..e1b4575706 100644 --- a/packages/app-store/amie/config.json +++ b/packages/app-store/amie/config.json @@ -11,6 +11,5 @@ "publisher": "Cal.com, Inc.", "email": "support@cal.com", "description": "The joyful productivity app\r\r", - "extendsFeature": "User", "__createdUsingCli": true } diff --git a/packages/app-store/appStoreMetaData.ts b/packages/app-store/appStoreMetaData.ts new file mode 100644 index 0000000000..c5212788d8 --- /dev/null +++ b/packages/app-store/appStoreMetaData.ts @@ -0,0 +1,18 @@ +import { AppMeta } from "@calcom/types/App"; + +import { appStoreMetadata as rawAppStoreMetadata } from "./apps.metadata.generated"; + +type RawAppStoreMetaData = typeof rawAppStoreMetadata; +type AppStoreMetaData = { + [key in keyof RawAppStoreMetaData]: AppMeta; +}; + +export const appStoreMetadata = {} as AppStoreMetaData; + +for (const [key, value] of Object.entries(rawAppStoreMetadata)) { + appStoreMetadata[key as keyof typeof appStoreMetadata] = { + appData: null, + __template: "", + ...value, + } as AppStoreMetaData[keyof AppStoreMetaData]; +} diff --git a/packages/app-store/apps.browser.generated.tsx b/packages/app-store/apps.browser.generated.tsx index 7bfb735ced..0a47322388 100644 --- a/packages/app-store/apps.browser.generated.tsx +++ b/packages/app-store/apps.browser.generated.tsx @@ -30,15 +30,26 @@ export const InstallAppButtonMap = { zoomvideo: dynamic(() => import("./zoomvideo/components/InstallAppButton")), }; export const AppSettingsComponentsMap = { - weather_in_your_calendar: dynamic(() => import("./weather_in_your_calendar/components/AppSettings")), - zapier: dynamic(() => import("./zapier/components/AppSettings")), + "general-app-settings": dynamic(() => + import("./templates/general-app-settings/components/AppSettingsInterface") + ), + weather_in_your_calendar: dynamic(() => + import("./weather_in_your_calendar/components/AppSettingsInterface") + ), + zapier: dynamic(() => import("./zapier/components/AppSettingsInterface")), }; export const EventTypeAddonMap = { - fathom: dynamic(() => import("./fathom/extensions/EventTypeAppCard")), - ga4: dynamic(() => import("./ga4/extensions/EventTypeAppCard")), - giphy: dynamic(() => import("./giphy/extensions/EventTypeAppCard")), - plausible: dynamic(() => import("./plausible/extensions/EventTypeAppCard")), - qr_code: dynamic(() => import("./qr_code/extensions/EventTypeAppCard")), - rainbow: dynamic(() => import("./rainbow/extensions/EventTypeAppCard")), - stripepayment: dynamic(() => import("./stripepayment/extensions/EventTypeAppCard")), + fathom: dynamic(() => import("./fathom/components/EventTypeAppCardInterface")), + ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")), + giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")), + plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")), + qr_code: dynamic(() => import("./qr_code/components/EventTypeAppCardInterface")), + rainbow: dynamic(() => import("./rainbow/components/EventTypeAppCardInterface")), + stripepayment: dynamic(() => import("./stripepayment/components/EventTypeAppCardInterface")), + "booking-pages-tag": dynamic(() => + import("./templates/booking-pages-tag/components/EventTypeAppCardInterface") + ), + "event-type-app-card": dynamic(() => + import("./templates/event-type-app-card/components/EventTypeAppCardInterface") + ), }; diff --git a/packages/app-store/apps.keys-schemas.generated.ts b/packages/app-store/apps.keys-schemas.generated.ts index 23864a1892..1ac71dc1d2 100644 --- a/packages/app-store/apps.keys-schemas.generated.ts +++ b/packages/app-store/apps.keys-schemas.generated.ts @@ -2,46 +2,50 @@ This file is autogenerated using the command `yarn app-store:build --watch`. Don't modify this file manually. **/ -import { appKeysSchema as dailyvideo_keys_schema } from "./dailyvideo/zod"; -import { appKeysSchema as routing_forms_keys_schema } from "./ee/routing-forms/zod"; -import { appKeysSchema as fathom_keys_schema } from "./fathom/zod"; -import { appKeysSchema as ga4_keys_schema } from "./ga4/zod"; -import { appKeysSchema as giphy_keys_schema } from "./giphy/zod"; -import { appKeysSchema as googlecalendar_keys_schema } from "./googlecalendar/zod"; -import { appKeysSchema as hubspot_keys_schema } from "./hubspot/zod"; -import { appKeysSchema as larkcalendar_keys_schema } from "./larkcalendar/zod"; -import { appKeysSchema as office365calendar_keys_schema } from "./office365calendar/zod"; -import { appKeysSchema as office365video_keys_schema } from "./office365video/zod"; -import { appKeysSchema as plausible_keys_schema } from "./plausible/zod"; -import { appKeysSchema as qr_code_keys_schema } from "./qr_code/zod"; -import { appKeysSchema as rainbow_keys_schema } from "./rainbow/zod"; -import { appKeysSchema as salesforce_keys_schema } from "./salesforce/zod"; -import { appKeysSchema as stripepayment_keys_schema } from "./stripepayment/zod"; -import { appKeysSchema as tandemvideo_keys_schema } from "./tandemvideo/zod"; -import { appKeysSchema as vital_keys_schema } from "./vital/zod"; -import { appKeysSchema as wordpress_keys_schema } from "./wordpress/zod"; -import { appKeysSchema as zapier_keys_schema } from "./zapier/zod"; -import { appKeysSchema as zoomvideo_keys_schema } from "./zoomvideo/zod"; +import { appKeysSchema as dailyvideo_zod_ts } from "./dailyvideo/zod"; +import { appKeysSchema as routing_forms_zod_ts } from "./ee/routing-forms/zod"; +import { appKeysSchema as fathom_zod_ts } from "./fathom/zod"; +import { appKeysSchema as ga4_zod_ts } from "./ga4/zod"; +import { appKeysSchema as giphy_zod_ts } from "./giphy/zod"; +import { appKeysSchema as googlecalendar_zod_ts } from "./googlecalendar/zod"; +import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod"; +import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod"; +import { appKeysSchema as office365calendar_zod_ts } from "./office365calendar/zod"; +import { appKeysSchema as office365video_zod_ts } from "./office365video/zod"; +import { appKeysSchema as plausible_zod_ts } from "./plausible/zod"; +import { appKeysSchema as qr_code_zod_ts } from "./qr_code/zod"; +import { appKeysSchema as rainbow_zod_ts } from "./rainbow/zod"; +import { appKeysSchema as salesforce_zod_ts } from "./salesforce/zod"; +import { appKeysSchema as stripepayment_zod_ts } from "./stripepayment/zod"; +import { appKeysSchema as tandemvideo_zod_ts } from "./tandemvideo/zod"; +import { appKeysSchema as booking_pages_tag_zod_ts } from "./templates/booking-pages-tag/zod"; +import { appKeysSchema as event_type_app_card_zod_ts } from "./templates/event-type-app-card/zod"; +import { appKeysSchema as vital_zod_ts } from "./vital/zod"; +import { appKeysSchema as wordpress_zod_ts } from "./wordpress/zod"; +import { appKeysSchema as zapier_zod_ts } from "./zapier/zod"; +import { appKeysSchema as zoomvideo_zod_ts } from "./zoomvideo/zod"; export const appKeysSchemas = { - dailyvideo: dailyvideo_keys_schema, - "routing-forms": routing_forms_keys_schema, - fathom: fathom_keys_schema, - ga4: ga4_keys_schema, - giphy: giphy_keys_schema, - googlecalendar: googlecalendar_keys_schema, - hubspot: hubspot_keys_schema, - larkcalendar: larkcalendar_keys_schema, - office365calendar: office365calendar_keys_schema, - office365video: office365video_keys_schema, - plausible: plausible_keys_schema, - qr_code: qr_code_keys_schema, - rainbow: rainbow_keys_schema, - salesforce: salesforce_keys_schema, - stripe: stripepayment_keys_schema, - tandemvideo: tandemvideo_keys_schema, - vital: vital_keys_schema, - wordpress: wordpress_keys_schema, - zapier: zapier_keys_schema, - zoomvideo: zoomvideo_keys_schema, + dailyvideo: dailyvideo_zod_ts, + "routing-forms": routing_forms_zod_ts, + fathom: fathom_zod_ts, + ga4: ga4_zod_ts, + giphy: giphy_zod_ts, + googlecalendar: googlecalendar_zod_ts, + hubspot: hubspot_zod_ts, + larkcalendar: larkcalendar_zod_ts, + office365calendar: office365calendar_zod_ts, + office365video: office365video_zod_ts, + plausible: plausible_zod_ts, + qr_code: qr_code_zod_ts, + rainbow: rainbow_zod_ts, + salesforce: salesforce_zod_ts, + stripe: stripepayment_zod_ts, + tandemvideo: tandemvideo_zod_ts, + "booking-pages-tag": booking_pages_tag_zod_ts, + "event-type-app-card": event_type_app_card_zod_ts, + vital: vital_zod_ts, + wordpress: wordpress_zod_ts, + zapier: zapier_zod_ts, + zoomvideo: zoomvideo_zod_ts, }; diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts index 0ecf92fdb0..5bf5cf5637 100644 --- a/packages/app-store/apps.metadata.generated.ts +++ b/packages/app-store/apps.metadata.generated.ts @@ -2,100 +2,112 @@ This file is autogenerated using the command `yarn app-store:build --watch`. Don't modify this file manually. **/ -import { metadata as amie_meta } from "./amie/_metadata"; -import { metadata as applecalendar_meta } from "./applecalendar/_metadata"; -import { metadata as around_meta } from "./around/_metadata"; -import { metadata as caldavcalendar_meta } from "./caldavcalendar/_metadata"; -import { metadata as campfire_meta } from "./campfire/_metadata"; -import { metadata as closecom_meta } from "./closecom/_metadata"; -import { metadata as dailyvideo_meta } from "./dailyvideo/_metadata"; -import { metadata as routing_forms_meta } from "./ee/routing-forms/_metadata"; -import { metadata as exchange2013calendar_meta } from "./exchange2013calendar/_metadata"; -import { metadata as exchange2016calendar_meta } from "./exchange2016calendar/_metadata"; -import { metadata as exchangecalendar_meta } from "./exchangecalendar/_metadata"; -import { metadata as fathom_meta } from "./fathom/_metadata"; -import { metadata as ga4_meta } from "./ga4/_metadata"; -import { metadata as giphy_meta } from "./giphy/_metadata"; -import { metadata as googlecalendar_meta } from "./googlecalendar/_metadata"; -import { metadata as googlevideo_meta } from "./googlevideo/_metadata"; -import { metadata as hubspot_meta } from "./hubspot/_metadata"; -import { metadata as huddle01video_meta } from "./huddle01video/_metadata"; -import { metadata as jitsivideo_meta } from "./jitsivideo/_metadata"; -import { metadata as larkcalendar_meta } from "./larkcalendar/_metadata"; -import { metadata as n8n_meta } from "./n8n/_metadata"; -import { metadata as office365calendar_meta } from "./office365calendar/_metadata"; -import { metadata as office365video_meta } from "./office365video/_metadata"; -import { metadata as ping_meta } from "./ping/_metadata"; -import { metadata as pipedream_meta } from "./pipedream/_metadata"; -import { metadata as plausible_meta } from "./plausible/_metadata"; -import { metadata as qr_code_meta } from "./qr_code/_metadata"; -import { metadata as rainbow_meta } from "./rainbow/_metadata"; -import { metadata as raycast_meta } from "./raycast/_metadata"; -import { metadata as riverside_meta } from "./riverside/_metadata"; -import { metadata as salesforce_meta } from "./salesforce/_metadata"; -import { metadata as sendgrid_meta } from "./sendgrid/_metadata"; -import { metadata as signal_meta } from "./signal/_metadata"; -import { metadata as sirius_video_meta } from "./sirius_video/_metadata"; -import { metadata as stripepayment_meta } from "./stripepayment/_metadata"; -import { metadata as tandemvideo_meta } from "./tandemvideo/_metadata"; -import { metadata as telegram_meta } from "./telegram/_metadata"; -import { metadata as typeform_meta } from "./typeform/_metadata"; -import { metadata as vimcal_meta } from "./vimcal/_metadata"; -import { metadata as vital_meta } from "./vital/_metadata"; -import { metadata as weather_in_your_calendar_meta } from "./weather_in_your_calendar/_metadata"; -import { metadata as whatsapp_meta } from "./whatsapp/_metadata"; -import { metadata as whereby_meta } from "./whereby/_metadata"; -import { metadata as wipemycalother_meta } from "./wipemycalother/_metadata"; -import { metadata as wordpress_meta } from "./wordpress/_metadata"; -import { metadata as zapier_meta } from "./zapier/_metadata"; -import { metadata as zoomvideo_meta } from "./zoomvideo/_metadata"; +import amie_config_json from "./amie/config.json"; +import { metadata as applecalendar__metadata_ts } from "./applecalendar/_metadata"; +import around_config_json from "./around/config.json"; +import { metadata as caldavcalendar__metadata_ts } from "./caldavcalendar/_metadata"; +import campfire_config_json from "./campfire/config.json"; +import closecom_config_json from "./closecom/config.json"; +import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata"; +import routing_forms_config_json from "./ee/routing-forms/config.json"; +import { metadata as exchange2013calendar__metadata_ts } from "./exchange2013calendar/_metadata"; +import { metadata as exchange2016calendar__metadata_ts } from "./exchange2016calendar/_metadata"; +import exchangecalendar_config_json from "./exchangecalendar/config.json"; +import fathom_config_json from "./fathom/config.json"; +import ga4_config_json from "./ga4/config.json"; +import { metadata as giphy__metadata_ts } from "./giphy/_metadata"; +import { metadata as googlecalendar__metadata_ts } from "./googlecalendar/_metadata"; +import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata"; +import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata"; +import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata"; +import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata"; +import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata"; +import n8n_config_json from "./n8n/config.json"; +import { metadata as office365calendar__metadata_ts } from "./office365calendar/_metadata"; +import office365video_config_json from "./office365video/config.json"; +import ping_config_json from "./ping/config.json"; +import pipedream_config_json from "./pipedream/config.json"; +import plausible_config_json from "./plausible/config.json"; +import qr_code_config_json from "./qr_code/config.json"; +import rainbow_config_json from "./rainbow/config.json"; +import raycast_config_json from "./raycast/config.json"; +import riverside_config_json from "./riverside/config.json"; +import salesforce_config_json from "./salesforce/config.json"; +import sendgrid_config_json from "./sendgrid/config.json"; +import signal_config_json from "./signal/config.json"; +import sirius_video_config_json from "./sirius_video/config.json"; +import { metadata as stripepayment__metadata_ts } from "./stripepayment/_metadata"; +import { metadata as tandemvideo__metadata_ts } from "./tandemvideo/_metadata"; +import telegram_config_json from "./telegram/config.json"; +import basic_config_json from "./templates/basic/config.json"; +import booking_pages_tag_config_json from "./templates/booking-pages-tag/config.json"; +import event_type_app_card_config_json from "./templates/event-type-app-card/config.json"; +import event_type_location_video_static_config_json from "./templates/event-type-location-video-static/config.json"; +import general_app_settings_config_json from "./templates/general-app-settings/config.json"; +import link_as_an_app_config_json from "./templates/link-as-an-app/config.json"; +import typeform_config_json from "./typeform/config.json"; +import vimcal_config_json from "./vimcal/config.json"; +import { metadata as vital__metadata_ts } from "./vital/_metadata"; +import weather_in_your_calendar_config_json from "./weather_in_your_calendar/config.json"; +import whatsapp_config_json from "./whatsapp/config.json"; +import whereby_config_json from "./whereby/config.json"; +import { metadata as wipemycalother__metadata_ts } from "./wipemycalother/_metadata"; +import wordpress_config_json from "./wordpress/config.json"; +import { metadata as zapier__metadata_ts } from "./zapier/_metadata"; +import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata"; export const appStoreMetadata = { - amie: amie_meta, - applecalendar: applecalendar_meta, - around: around_meta, - caldavcalendar: caldavcalendar_meta, - campfire: campfire_meta, - closecom: closecom_meta, - dailyvideo: dailyvideo_meta, - "routing-forms": routing_forms_meta, - exchange2013calendar: exchange2013calendar_meta, - exchange2016calendar: exchange2016calendar_meta, - exchangecalendar: exchangecalendar_meta, - fathom: fathom_meta, - ga4: ga4_meta, - giphy: giphy_meta, - googlecalendar: googlecalendar_meta, - googlevideo: googlevideo_meta, - hubspot: hubspot_meta, - huddle01video: huddle01video_meta, - jitsivideo: jitsivideo_meta, - larkcalendar: larkcalendar_meta, - n8n: n8n_meta, - office365calendar: office365calendar_meta, - office365video: office365video_meta, - ping: ping_meta, - pipedream: pipedream_meta, - plausible: plausible_meta, - qr_code: qr_code_meta, - rainbow: rainbow_meta, - raycast: raycast_meta, - riverside: riverside_meta, - salesforce: salesforce_meta, - sendgrid: sendgrid_meta, - signal: signal_meta, - sirius_video: sirius_video_meta, - stripepayment: stripepayment_meta, - tandemvideo: tandemvideo_meta, - telegram: telegram_meta, - typeform: typeform_meta, - vimcal: vimcal_meta, - vital: vital_meta, - weather_in_your_calendar: weather_in_your_calendar_meta, - whatsapp: whatsapp_meta, - whereby: whereby_meta, - wipemycalother: wipemycalother_meta, - wordpress: wordpress_meta, - zapier: zapier_meta, - zoomvideo: zoomvideo_meta, + amie: amie_config_json, + applecalendar: applecalendar__metadata_ts, + around: around_config_json, + caldavcalendar: caldavcalendar__metadata_ts, + campfire: campfire_config_json, + closecom: closecom_config_json, + dailyvideo: dailyvideo__metadata_ts, + "routing-forms": routing_forms_config_json, + exchange2013calendar: exchange2013calendar__metadata_ts, + exchange2016calendar: exchange2016calendar__metadata_ts, + exchangecalendar: exchangecalendar_config_json, + fathom: fathom_config_json, + ga4: ga4_config_json, + giphy: giphy__metadata_ts, + googlecalendar: googlecalendar__metadata_ts, + googlevideo: googlevideo__metadata_ts, + hubspot: hubspot__metadata_ts, + huddle01video: huddle01video__metadata_ts, + jitsivideo: jitsivideo__metadata_ts, + larkcalendar: larkcalendar__metadata_ts, + n8n: n8n_config_json, + office365calendar: office365calendar__metadata_ts, + office365video: office365video_config_json, + ping: ping_config_json, + pipedream: pipedream_config_json, + plausible: plausible_config_json, + qr_code: qr_code_config_json, + rainbow: rainbow_config_json, + raycast: raycast_config_json, + riverside: riverside_config_json, + salesforce: salesforce_config_json, + sendgrid: sendgrid_config_json, + signal: signal_config_json, + sirius_video: sirius_video_config_json, + stripepayment: stripepayment__metadata_ts, + tandemvideo: tandemvideo__metadata_ts, + telegram: telegram_config_json, + basic: basic_config_json, + "booking-pages-tag": booking_pages_tag_config_json, + "event-type-app-card": event_type_app_card_config_json, + "event-type-location-video-static": event_type_location_video_static_config_json, + "general-app-settings": general_app_settings_config_json, + "link-as-an-app": link_as_an_app_config_json, + typeform: typeform_config_json, + vimcal: vimcal_config_json, + vital: vital__metadata_ts, + weather_in_your_calendar: weather_in_your_calendar_config_json, + whatsapp: whatsapp_config_json, + whereby: whereby_config_json, + wipemycalother: wipemycalother__metadata_ts, + wordpress: wordpress_config_json, + zapier: zapier__metadata_ts, + zoomvideo: zoomvideo__metadata_ts, }; diff --git a/packages/app-store/apps.schemas.generated.ts b/packages/app-store/apps.schemas.generated.ts index 113eeadc47..fa6858e3c5 100644 --- a/packages/app-store/apps.schemas.generated.ts +++ b/packages/app-store/apps.schemas.generated.ts @@ -2,46 +2,50 @@ This file is autogenerated using the command `yarn app-store:build --watch`. Don't modify this file manually. **/ -import { appDataSchema as dailyvideo_schema } from "./dailyvideo/zod"; -import { appDataSchema as routing_forms_schema } from "./ee/routing-forms/zod"; -import { appDataSchema as fathom_schema } from "./fathom/zod"; -import { appDataSchema as ga4_schema } from "./ga4/zod"; -import { appDataSchema as giphy_schema } from "./giphy/zod"; -import { appDataSchema as googlecalendar_schema } from "./googlecalendar/zod"; -import { appDataSchema as hubspot_schema } from "./hubspot/zod"; -import { appDataSchema as larkcalendar_schema } from "./larkcalendar/zod"; -import { appDataSchema as office365calendar_schema } from "./office365calendar/zod"; -import { appDataSchema as office365video_schema } from "./office365video/zod"; -import { appDataSchema as plausible_schema } from "./plausible/zod"; -import { appDataSchema as qr_code_schema } from "./qr_code/zod"; -import { appDataSchema as rainbow_schema } from "./rainbow/zod"; -import { appDataSchema as salesforce_schema } from "./salesforce/zod"; -import { appDataSchema as stripepayment_schema } from "./stripepayment/zod"; -import { appDataSchema as tandemvideo_schema } from "./tandemvideo/zod"; -import { appDataSchema as vital_schema } from "./vital/zod"; -import { appDataSchema as wordpress_schema } from "./wordpress/zod"; -import { appDataSchema as zapier_schema } from "./zapier/zod"; -import { appDataSchema as zoomvideo_schema } from "./zoomvideo/zod"; +import { appDataSchema as dailyvideo_zod_ts } from "./dailyvideo/zod"; +import { appDataSchema as routing_forms_zod_ts } from "./ee/routing-forms/zod"; +import { appDataSchema as fathom_zod_ts } from "./fathom/zod"; +import { appDataSchema as ga4_zod_ts } from "./ga4/zod"; +import { appDataSchema as giphy_zod_ts } from "./giphy/zod"; +import { appDataSchema as googlecalendar_zod_ts } from "./googlecalendar/zod"; +import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod"; +import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod"; +import { appDataSchema as office365calendar_zod_ts } from "./office365calendar/zod"; +import { appDataSchema as office365video_zod_ts } from "./office365video/zod"; +import { appDataSchema as plausible_zod_ts } from "./plausible/zod"; +import { appDataSchema as qr_code_zod_ts } from "./qr_code/zod"; +import { appDataSchema as rainbow_zod_ts } from "./rainbow/zod"; +import { appDataSchema as salesforce_zod_ts } from "./salesforce/zod"; +import { appDataSchema as stripepayment_zod_ts } from "./stripepayment/zod"; +import { appDataSchema as tandemvideo_zod_ts } from "./tandemvideo/zod"; +import { appDataSchema as booking_pages_tag_zod_ts } from "./templates/booking-pages-tag/zod"; +import { appDataSchema as event_type_app_card_zod_ts } from "./templates/event-type-app-card/zod"; +import { appDataSchema as vital_zod_ts } from "./vital/zod"; +import { appDataSchema as wordpress_zod_ts } from "./wordpress/zod"; +import { appDataSchema as zapier_zod_ts } from "./zapier/zod"; +import { appDataSchema as zoomvideo_zod_ts } from "./zoomvideo/zod"; export const appDataSchemas = { - dailyvideo: dailyvideo_schema, - "routing-forms": routing_forms_schema, - fathom: fathom_schema, - ga4: ga4_schema, - giphy: giphy_schema, - googlecalendar: googlecalendar_schema, - hubspot: hubspot_schema, - larkcalendar: larkcalendar_schema, - office365calendar: office365calendar_schema, - office365video: office365video_schema, - plausible: plausible_schema, - qr_code: qr_code_schema, - rainbow: rainbow_schema, - salesforce: salesforce_schema, - stripe: stripepayment_schema, - tandemvideo: tandemvideo_schema, - vital: vital_schema, - wordpress: wordpress_schema, - zapier: zapier_schema, - zoomvideo: zoomvideo_schema, + dailyvideo: dailyvideo_zod_ts, + "routing-forms": routing_forms_zod_ts, + fathom: fathom_zod_ts, + ga4: ga4_zod_ts, + giphy: giphy_zod_ts, + googlecalendar: googlecalendar_zod_ts, + hubspot: hubspot_zod_ts, + larkcalendar: larkcalendar_zod_ts, + office365calendar: office365calendar_zod_ts, + office365video: office365video_zod_ts, + plausible: plausible_zod_ts, + qr_code: qr_code_zod_ts, + rainbow: rainbow_zod_ts, + salesforce: salesforce_zod_ts, + stripe: stripepayment_zod_ts, + tandemvideo: tandemvideo_zod_ts, + "booking-pages-tag": booking_pages_tag_zod_ts, + "event-type-app-card": event_type_app_card_zod_ts, + vital: vital_zod_ts, + wordpress: wordpress_zod_ts, + zapier: zapier_zod_ts, + zoomvideo: zoomvideo_zod_ts, }; diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts index 71d7faa024..66ed6541b8 100644 --- a/packages/app-store/apps.server.generated.ts +++ b/packages/app-store/apps.server.generated.ts @@ -39,6 +39,12 @@ export const apiHandlers = { stripepayment: import("./stripepayment/api"), tandemvideo: import("./tandemvideo/api"), telegram: import("./telegram/api"), + basic: import("./templates/basic/api"), + "booking-pages-tag": import("./templates/booking-pages-tag/api"), + "event-type-app-card": import("./templates/event-type-app-card/api"), + "event-type-location-video-static": import("./templates/event-type-location-video-static/api"), + "general-app-settings": import("./templates/general-app-settings/api"), + "link-as-an-app": import("./templates/link-as-an-app/api"), typeform: import("./typeform/api"), vimcal: import("./vimcal/api"), vital: import("./vital/api"), diff --git a/packages/app-store/campfire/api/add.ts b/packages/app-store/campfire/api/add.ts index 45c6741f36..8f569c854c 100644 --- a/packages/app-store/campfire/api/add.ts +++ b/packages/app-store/campfire/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/ee/routing-forms/api/add.ts b/packages/app-store/ee/routing-forms/api/add.ts index 9b5bf2bc17..30cedc3f81 100644 --- a/packages/app-store/ee/routing-forms/api/add.ts +++ b/packages/app-store/ee/routing-forms/api/add.ts @@ -4,7 +4,6 @@ import { AppDeclarativeHandler } from "@calcom/types/AppHandler"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/eventTypeAnalytics.ts b/packages/app-store/eventTypeAnalytics.ts deleted file mode 100644 index 58ff75b599..0000000000 --- a/packages/app-store/eventTypeAnalytics.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { AppScript } from "./BookingPageTagManager"; -import { appDataSchemas } from "./apps.schemas.generated"; - -// TODO: This config might be imported from {APP}/eventTypeAnalytics.ts. -export const trackingApps: Partial< - Record< - keyof typeof appDataSchemas, - { - scripts: AppScript[]; - } - > -> = { - fathom: { - scripts: [ - { - src: "https://cdn.usefathom.com/script.js", - content: undefined, - attrs: { - "data-site": "{TRACKING_ID}", - }, - }, - ], - }, - plausible: { - scripts: [ - { - src: "https://plausible.io/js/script.js", - content: undefined, - attrs: { - "data-domain": "{TRACKED_DOMAIN}", - }, - }, - ], - }, - ga4: { - scripts: [ - { - src: "https://www.googletagmanager.com/gtag/js?id={TRACKING_ID}", - content: undefined, - attrs: {}, - }, - { - src: undefined, - content: ` - window.dataLayer = window.dataLayer || []; - function gtag(){dataLayer.push(arguments);} - gtag('js', new Date()); - gtag('config', '{TRACKING_ID}'); - `, - }, - ], - }, -}; diff --git a/packages/app-store/eventTypeAppCardZod.ts b/packages/app-store/eventTypeAppCardZod.ts index 8bfa059cfa..a41b2e45c5 100644 --- a/packages/app-store/eventTypeAppCardZod.ts +++ b/packages/app-store/eventTypeAppCardZod.ts @@ -4,3 +4,5 @@ import { z } from "zod"; export const eventTypeAppCardZod = z.object({ enabled: z.boolean().optional(), }); + +export const appKeysSchema = z.object({}); diff --git a/packages/app-store/fathom/api/add.ts b/packages/app-store/fathom/api/add.ts index 45c6741f36..8f569c854c 100644 --- a/packages/app-store/fathom/api/add.ts +++ b/packages/app-store/fathom/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/fathom/extensions/EventTypeAppCard.tsx b/packages/app-store/fathom/components/EventTypeAppCardInterface.tsx similarity index 100% rename from packages/app-store/fathom/extensions/EventTypeAppCard.tsx rename to packages/app-store/fathom/components/EventTypeAppCardInterface.tsx diff --git a/packages/app-store/fathom/config.json b/packages/app-store/fathom/config.json index 742f56e84a..435053f0f9 100644 --- a/packages/app-store/fathom/config.json +++ b/packages/app-store/fathom/config.json @@ -11,6 +11,18 @@ "publisher": "Cal.com, Inc.", "email": "help@cal.com", "extendsFeature": "EventType", + "appData": { + "tag": { + "scripts": [ + { + "src": "https://cdn.usefathom.com/script.js", + "attrs": { + "data-site": "{TRACKING_ID}" + } + } + ] + } + }, "description": "Fathom Analytics provides simple, privacy-focused website analytics. We're a GDPR-compliant, Google Analytics alternative.", "__createdUsingCli": true } diff --git a/packages/app-store/ga4/api/add.ts b/packages/app-store/ga4/api/add.ts index 45c6741f36..8f569c854c 100644 --- a/packages/app-store/ga4/api/add.ts +++ b/packages/app-store/ga4/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/ga4/extensions/EventTypeAppCard.tsx b/packages/app-store/ga4/components/EventTypeAppCardInterface.tsx similarity index 100% rename from packages/app-store/ga4/extensions/EventTypeAppCard.tsx rename to packages/app-store/ga4/components/EventTypeAppCardInterface.tsx diff --git a/packages/app-store/ga4/config.json b/packages/app-store/ga4/config.json index 5f8b4d3146..d614bdaad8 100644 --- a/packages/app-store/ga4/config.json +++ b/packages/app-store/ga4/config.json @@ -12,5 +12,16 @@ "email": "support@cal.com", "description": "Google Analytics is a web analytics service offered by Google that tracks and reports website traffic, currently as a platform inside the Google Marketing Platform brand.", "extendsFeature": "EventType", + "tag": { + "scripts": [ + { + "src": "https://www.googletagmanager.com/gtag/js?id={TRACKING_ID}", + "attrs": {} + }, + { + "content": "window.dataLayer = window.dataLayer || [];\n function gtag(){dataLayer.push(arguments);}\n gtag('js', new Date());\n gtag('config', '{TRACKING_ID}');" + } + ] + }, "__createdUsingCli": true } diff --git a/packages/app-store/giphy/extensions/EventTypeAppCard.tsx b/packages/app-store/giphy/components/EventTypeAppCardInterface.tsx similarity index 100% rename from packages/app-store/giphy/extensions/EventTypeAppCard.tsx rename to packages/app-store/giphy/components/EventTypeAppCardInterface.tsx diff --git a/packages/app-store/locations.ts b/packages/app-store/locations.ts index 7b7efaab88..daecdc69f3 100644 --- a/packages/app-store/locations.ts +++ b/packages/app-store/locations.ts @@ -1,11 +1,11 @@ import { BookingStatus } from "@prisma/client"; import type { TFunction } from "next-i18next"; +import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; import logger from "@calcom/lib/logger"; import { Ensure, Optional } from "@calcom/types/utils"; import type { EventLocationTypeFromAppMeta } from "../types/App"; -import { appStoreMetadata } from "./apps.metadata.generated"; export type DefaultEventLocationType = { default: true; @@ -142,6 +142,14 @@ const locationsFromApps: EventLocationTypeFromApp[] = []; for (const [appName, meta] of Object.entries(appStoreMetadata)) { const location = meta.appData?.location; if (location) { + // TODO: This template variable replacement should happen once during app-store:build. + for (const [key, value] of Object.entries(location)) { + if (typeof value === "string") { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + location[key] = value.replace(/{SLUG}/g, meta.slug).replace(/{TITLE}/g, meta.name); + } + } const newLocation = { ...location, messageForOrganizer: location.messageForOrganizer || `Set ${location.label} link`, diff --git a/packages/app-store/n8n/api/add.ts b/packages/app-store/n8n/api/add.ts index 039ac60e87..b4c95f1426 100644 --- a/packages/app-store/n8n/api/add.ts +++ b/packages/app-store/n8n/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/pipedream/api/add.ts b/packages/app-store/pipedream/api/add.ts index 8ecf1ce2a3..3b4b5384ef 100644 --- a/packages/app-store/pipedream/api/add.ts +++ b/packages/app-store/pipedream/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/pipedream/config.json b/packages/app-store/pipedream/config.json index 77205a2951..a696fa49d1 100644 --- a/packages/app-store/pipedream/config.json +++ b/packages/app-store/pipedream/config.json @@ -11,6 +11,5 @@ "publisher": "Pipedream, Inc.", "email": "support@pipedream.com", "description": "Connect APIs, remarkably fast. Stop writing boilerplate code, struggling with authentication and managing infrastructure. Start connecting APIs with code-level control when you need it — and no code when you don't", - "extendsFeature": "User", "__createdUsingCli": true } diff --git a/packages/app-store/plausible/api/add.ts b/packages/app-store/plausible/api/add.ts index 45c6741f36..8f569c854c 100644 --- a/packages/app-store/plausible/api/add.ts +++ b/packages/app-store/plausible/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/plausible/extensions/EventTypeAppCard.tsx b/packages/app-store/plausible/components/EventTypeAppCardInterface.tsx similarity index 100% rename from packages/app-store/plausible/extensions/EventTypeAppCard.tsx rename to packages/app-store/plausible/components/EventTypeAppCardInterface.tsx diff --git a/packages/app-store/plausible/config.json b/packages/app-store/plausible/config.json index f514b7172c..afe469d189 100644 --- a/packages/app-store/plausible/config.json +++ b/packages/app-store/plausible/config.json @@ -11,6 +11,16 @@ "publisher": "Cal.com, Inc.", "email": "help@cal.com", "extendsFeature": "EventType", + "tag": { + "scripts": [ + { + "src": "https://plausible.io/js/script.js", + "attrs": { + "data-domain": "{TRACKED_DOMAIN}" + } + } + ] + }, "description": "Simple, privacy-friendly Google Analytics alternative.", "__createdUsingCli": true } diff --git a/packages/app-store/qr_code/api/add.ts b/packages/app-store/qr_code/api/add.ts index 45c6741f36..8f569c854c 100644 --- a/packages/app-store/qr_code/api/add.ts +++ b/packages/app-store/qr_code/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/qr_code/components/.gitkeep b/packages/app-store/qr_code/components/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/app-store/qr_code/extensions/EventTypeAppCard.tsx b/packages/app-store/qr_code/components/EventTypeAppCardInterface.tsx similarity index 100% rename from packages/app-store/qr_code/extensions/EventTypeAppCard.tsx rename to packages/app-store/qr_code/components/EventTypeAppCardInterface.tsx diff --git a/packages/app-store/rainbow/api/add.ts b/packages/app-store/rainbow/api/add.ts index bfcb5d2be1..5834fdbd88 100644 --- a/packages/app-store/rainbow/api/add.ts +++ b/packages/app-store/rainbow/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, slug: appConfig.slug, variant: appConfig.slug, diff --git a/packages/app-store/rainbow/extensions/EventTypeAppCard.tsx b/packages/app-store/rainbow/components/EventTypeAppCardInterface.tsx similarity index 100% rename from packages/app-store/rainbow/extensions/EventTypeAppCard.tsx rename to packages/app-store/rainbow/components/EventTypeAppCardInterface.tsx diff --git a/packages/app-store/raycast/api/add.ts b/packages/app-store/raycast/api/add.ts index 93294b1c41..7f13c0244e 100644 --- a/packages/app-store/raycast/api/add.ts +++ b/packages/app-store/raycast/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, slug: appConfig.slug, variant: appConfig.variant, diff --git a/packages/app-store/salesforce/config.json b/packages/app-store/salesforce/config.json index 52e0ad3308..855f1166df 100644 --- a/packages/app-store/salesforce/config.json +++ b/packages/app-store/salesforce/config.json @@ -11,6 +11,5 @@ "publisher": "Cal.com", "email": "help@cal.com", "description": "Salesforce (Sales Cloud) is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day.", - "extendsFeature": "User", "__createdUsingCli": true } diff --git a/packages/app-store/sendgrid/config.json b/packages/app-store/sendgrid/config.json index d95d4b11ce..3dc3936858 100644 --- a/packages/app-store/sendgrid/config.json +++ b/packages/app-store/sendgrid/config.json @@ -11,6 +11,5 @@ "publisher": "Cal.com", "email": "help@cal.com", "description": "SendGrid delivers your transactional and marketing emails through the world's largest cloud-based email delivery platform.", - "extendsFeature": "User", "__createdUsingCli": true } diff --git a/packages/app-store/signal/api/add.ts b/packages/app-store/signal/api/add.ts index 45c6741f36..8f569c854c 100644 --- a/packages/app-store/signal/api/add.ts +++ b/packages/app-store/signal/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/signal/config.json b/packages/app-store/signal/config.json index 8c850718f7..4b1cdd2621 100644 --- a/packages/app-store/signal/config.json +++ b/packages/app-store/signal/config.json @@ -11,7 +11,6 @@ "publisher": "Cal.com, Inc.", "email": "support@cal.com", "description": "Schedule a chat with your guests or have a Signal Video call.", - "extendsFeature": "User", "__createdUsingCli": true, "appData": { "location": { diff --git a/packages/app-store/sirius_video/api/add.ts b/packages/app-store/sirius_video/api/add.ts index 45c6741f36..8f569c854c 100644 --- a/packages/app-store/sirius_video/api/add.ts +++ b/packages/app-store/sirius_video/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/stripepayment/extensions/EventTypeAppCard.tsx b/packages/app-store/stripepayment/components/EventTypeAppCardInterface.tsx similarity index 100% rename from packages/app-store/stripepayment/extensions/EventTypeAppCard.tsx rename to packages/app-store/stripepayment/components/EventTypeAppCardInterface.tsx diff --git a/packages/app-store/telegram/api/add.ts b/packages/app-store/telegram/api/add.ts index 45c6741f36..8f569c854c 100644 --- a/packages/app-store/telegram/api/add.ts +++ b/packages/app-store/telegram/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/telegram/config.json b/packages/app-store/telegram/config.json index f4d2f0a8e5..e36861cf10 100644 --- a/packages/app-store/telegram/config.json +++ b/packages/app-store/telegram/config.json @@ -11,7 +11,6 @@ "publisher": "Cal.com, Inc.", "email": "support@cal.com", "description": "Schedule a chat with your guests or have a Telegram Video call.", - "extendsFeature": "User", "__createdUsingCli": true, "appData": { "location": { diff --git a/packages/app-store/templates/_auth-based-app/README.md b/packages/app-store/templates/_auth-based-app/README.md new file mode 100644 index 0000000000..b3d6e23ef2 --- /dev/null +++ b/packages/app-store/templates/_auth-based-app/README.md @@ -0,0 +1,2 @@ +## TODO: +Identify an ideal app among existing auth based apps and add it here \ No newline at end of file diff --git a/packages/app-store/templates/_calendar/README.md b/packages/app-store/templates/_calendar/README.md new file mode 100644 index 0000000000..124b3749c6 --- /dev/null +++ b/packages/app-store/templates/_calendar/README.md @@ -0,0 +1 @@ +Calendar apps are not streamlined and thus a template for it needs some good amount of work \ No newline at end of file diff --git a/packages/app-store/templates/basic/DESCRIPTION.md b/packages/app-store/templates/basic/DESCRIPTION.md new file mode 100644 index 0000000000..a4b15502c3 --- /dev/null +++ b/packages/app-store/templates/basic/DESCRIPTION.md @@ -0,0 +1,8 @@ +--- +items: + - 1.jpeg + - 2.jpeg + - 3.jpeg +--- + +{DESCRIPTION} diff --git a/packages/app-store/_baseApp/api/add.ts b/packages/app-store/templates/basic/api/add.ts similarity index 66% rename from packages/app-store/_baseApp/api/add.ts rename to packages/app-store/templates/basic/api/add.ts index 45c6741f36..f8c6354180 100644 --- a/packages/app-store/_baseApp/api/add.ts +++ b/packages/app-store/templates/basic/api/add.ts @@ -1,10 +1,9 @@ +import { createDefaultInstallation } from "@calcom/app-store/_utils/installation"; import { AppDeclarativeHandler } from "@calcom/types/AppHandler"; -import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/_baseApp/api/index.ts b/packages/app-store/templates/basic/api/index.ts similarity index 100% rename from packages/app-store/_baseApp/api/index.ts rename to packages/app-store/templates/basic/api/index.ts diff --git a/packages/app-store/_baseApp/components/.gitkeep b/packages/app-store/templates/basic/components/.gitkeep similarity index 100% rename from packages/app-store/_baseApp/components/.gitkeep rename to packages/app-store/templates/basic/components/.gitkeep diff --git a/packages/app-store/templates/basic/config.json b/packages/app-store/templates/basic/config.json new file mode 100644 index 0000000000..145dbda64b --- /dev/null +++ b/packages/app-store/templates/basic/config.json @@ -0,0 +1,19 @@ +{ + "/*": "Don't modify slug - If required, do it using cli edit command", + "name": "Basic", + "slug": "basic", + "type": "basic_other", + "imageSrc": "icon.svg", + "logo": "icon.svg", + "url": "https://example.com/link", + "variant": "other", + "categories": [ + "other" + ], + "publisher": "Cal.com, Inc.", + "email": "support@cal.com", + "description": "It is a template for an app that can be installed and provides no other feature.", + "isTemplate": true, + "__createdUsingCli": true, + "__template": "link-as-an-app" +} \ No newline at end of file diff --git a/packages/app-store/templates/basic/index.ts b/packages/app-store/templates/basic/index.ts new file mode 100644 index 0000000000..d7f3602204 --- /dev/null +++ b/packages/app-store/templates/basic/index.ts @@ -0,0 +1 @@ +export * as api from "./api"; diff --git a/packages/app-store/templates/basic/package.json b/packages/app-store/templates/basic/package.json new file mode 100644 index 0000000000..ec2364b805 --- /dev/null +++ b/packages/app-store/templates/basic/package.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "name": "@calcom/basic", + "version": "0.0.0", + "main": "./index.ts", + "dependencies": { + "@calcom/lib": "*" + }, + "devDependencies": { + "@calcom/types": "*" + }, + "description": "It is a template for an app that can be installed and provides no other feature." +} \ No newline at end of file diff --git a/packages/app-store/templates/basic/static/1.jpeg b/packages/app-store/templates/basic/static/1.jpeg new file mode 100644 index 0000000000..d3ba68492d Binary files /dev/null and b/packages/app-store/templates/basic/static/1.jpeg differ diff --git a/packages/app-store/templates/basic/static/2.jpeg b/packages/app-store/templates/basic/static/2.jpeg new file mode 100644 index 0000000000..dbdcdb5473 Binary files /dev/null and b/packages/app-store/templates/basic/static/2.jpeg differ diff --git a/packages/app-store/templates/basic/static/3.jpeg b/packages/app-store/templates/basic/static/3.jpeg new file mode 100644 index 0000000000..d3ba68492d Binary files /dev/null and b/packages/app-store/templates/basic/static/3.jpeg differ diff --git a/packages/app-store/templates/basic/static/icon.svg b/packages/app-store/templates/basic/static/icon.svg new file mode 100644 index 0000000000..44966edfc1 --- /dev/null +++ b/packages/app-store/templates/basic/static/icon.svg @@ -0,0 +1 @@ +blockchain \ No newline at end of file diff --git a/packages/app-store/templates/booking-pages-tag/DESCRIPTION.md b/packages/app-store/templates/booking-pages-tag/DESCRIPTION.md new file mode 100644 index 0000000000..02c40f00ac --- /dev/null +++ b/packages/app-store/templates/booking-pages-tag/DESCRIPTION.md @@ -0,0 +1,8 @@ +--- +items: + - 1.jpeg + - 2.jpeg + - 3.jpeg +--- + +{DESCRIPTION} \ No newline at end of file diff --git a/packages/app-store/fathom/components/.gitkeep b/packages/app-store/templates/booking-pages-tag/README.md similarity index 100% rename from packages/app-store/fathom/components/.gitkeep rename to packages/app-store/templates/booking-pages-tag/README.md diff --git a/packages/app-store/templates/booking-pages-tag/api/add.ts b/packages/app-store/templates/booking-pages-tag/api/add.ts new file mode 100644 index 0000000000..f8c6354180 --- /dev/null +++ b/packages/app-store/templates/booking-pages-tag/api/add.ts @@ -0,0 +1,16 @@ +import { createDefaultInstallation } from "@calcom/app-store/_utils/installation"; +import { AppDeclarativeHandler } from "@calcom/types/AppHandler"; + +import appConfig from "../config.json"; + +const handler: AppDeclarativeHandler = { + appType: appConfig.type, + variant: appConfig.variant, + slug: appConfig.slug, + supportsMultipleInstalls: false, + handlerType: "add", + createCredential: ({ appType, user, slug }) => + createDefaultInstallation({ appType, userId: user.id, slug, key: {} }), +}; + +export default handler; diff --git a/packages/app-store/templates/booking-pages-tag/api/index.ts b/packages/app-store/templates/booking-pages-tag/api/index.ts new file mode 100644 index 0000000000..4c0d2ead01 --- /dev/null +++ b/packages/app-store/templates/booking-pages-tag/api/index.ts @@ -0,0 +1 @@ +export { default as add } from "./add"; diff --git a/packages/app-store/templates/booking-pages-tag/components/EventTypeAppCardInterface.tsx b/packages/app-store/templates/booking-pages-tag/components/EventTypeAppCardInterface.tsx new file mode 100644 index 0000000000..77c5b45b59 --- /dev/null +++ b/packages/app-store/templates/booking-pages-tag/components/EventTypeAppCardInterface.tsx @@ -0,0 +1,38 @@ +import { useState } from "react"; + +import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; +import AppCard from "@calcom/app-store/_components/AppCard"; +import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; +import { TextField } from "@calcom/ui"; + +import { appDataSchema } from "../zod"; + +const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) { + const [getAppData, setAppData] = useAppContextWithSchema(); + const trackingId = getAppData("trackingId"); + const [enabled, setEnabled] = useState(getAppData("enabled")); + + return ( + { + if (!e) { + setEnabled(false); + } else { + setEnabled(true); + } + }} + switchChecked={enabled}> + { + setAppData("trackingId", e.target.value); + }} + /> + + ); +}; + +export default EventTypeAppCard; diff --git a/packages/app-store/templates/booking-pages-tag/config.json b/packages/app-store/templates/booking-pages-tag/config.json new file mode 100644 index 0000000000..a518f33b64 --- /dev/null +++ b/packages/app-store/templates/booking-pages-tag/config.json @@ -0,0 +1,28 @@ +{ + "name": "Booking Pages Tag", + "slug": "booking-pages-tag", + "type": "booking-pages-tag_analytics", + "imageSrc": "icon.svg", + "logo": "icon.svg", + "url": "https://example.com/link", + "variant": "analytics", + "categories": ["analytics"], + "publisher": "Cal.com, Inc.", + "email": "support@cal.com", + "description": "It is a template demoing a Booking Pages tracking app like GA4, Fathom and Plausible.", + "extendsFeature": "EventType", + "appData": { + "tag": { + "scripts": [ + { + "src": "https://cdn.example.com/script.js", + "attrs": { + "data-site": "{TRACKING_ID}" + } + } + ] + } + }, + "isTemplate":true, + "__createdUsingCli": true +} diff --git a/packages/app-store/templates/booking-pages-tag/index.ts b/packages/app-store/templates/booking-pages-tag/index.ts new file mode 100644 index 0000000000..d7f3602204 --- /dev/null +++ b/packages/app-store/templates/booking-pages-tag/index.ts @@ -0,0 +1 @@ +export * as api from "./api"; diff --git a/packages/app-store/_baseApp/package.json b/packages/app-store/templates/booking-pages-tag/package.json similarity index 71% rename from packages/app-store/_baseApp/package.json rename to packages/app-store/templates/booking-pages-tag/package.json index ff2856f018..024bc7b430 100644 --- a/packages/app-store/_baseApp/package.json +++ b/packages/app-store/templates/booking-pages-tag/package.json @@ -1,10 +1,9 @@ { "$schema": "https://json.schemastore.org/package.json", "private": true, - "name": "@calcom/cli_base__app_name", + "name": "@calcom/template-event-type-analytics", "version": "0.0.0", "main": "./index.ts", - "description": "Your app description goes here.", "dependencies": { "@calcom/lib": "*" }, diff --git a/packages/app-store/templates/booking-pages-tag/static/1.jpeg b/packages/app-store/templates/booking-pages-tag/static/1.jpeg new file mode 100644 index 0000000000..d3ba68492d Binary files /dev/null and b/packages/app-store/templates/booking-pages-tag/static/1.jpeg differ diff --git a/packages/app-store/templates/booking-pages-tag/static/2.jpeg b/packages/app-store/templates/booking-pages-tag/static/2.jpeg new file mode 100644 index 0000000000..dbdcdb5473 Binary files /dev/null and b/packages/app-store/templates/booking-pages-tag/static/2.jpeg differ diff --git a/packages/app-store/templates/booking-pages-tag/static/3.jpeg b/packages/app-store/templates/booking-pages-tag/static/3.jpeg new file mode 100644 index 0000000000..d3ba68492d Binary files /dev/null and b/packages/app-store/templates/booking-pages-tag/static/3.jpeg differ diff --git a/packages/app-store/templates/booking-pages-tag/static/icon.svg b/packages/app-store/templates/booking-pages-tag/static/icon.svg new file mode 100644 index 0000000000..44966edfc1 --- /dev/null +++ b/packages/app-store/templates/booking-pages-tag/static/icon.svg @@ -0,0 +1 @@ +blockchain \ No newline at end of file diff --git a/packages/app-store/templates/booking-pages-tag/zod.ts b/packages/app-store/templates/booking-pages-tag/zod.ts new file mode 100644 index 0000000000..6f45d901cc --- /dev/null +++ b/packages/app-store/templates/booking-pages-tag/zod.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod"; + +export const appDataSchema = eventTypeAppCardZod.merge( + z.object({ + trackingId: z.string(), + }) +); + +export const appKeysSchema = z.object({}); diff --git a/packages/app-store/templates/event-type-app-card/DESCRIPTION.md b/packages/app-store/templates/event-type-app-card/DESCRIPTION.md new file mode 100644 index 0000000000..30a038f826 --- /dev/null +++ b/packages/app-store/templates/event-type-app-card/DESCRIPTION.md @@ -0,0 +1,9 @@ +--- +items: + - 1.jpeg + - 2.jpeg + - 3.jpeg +--- + +{DESCRIPTION} + diff --git a/packages/app-store/templates/event-type-app-card/api/add.ts b/packages/app-store/templates/event-type-app-card/api/add.ts new file mode 100644 index 0000000000..f8c6354180 --- /dev/null +++ b/packages/app-store/templates/event-type-app-card/api/add.ts @@ -0,0 +1,16 @@ +import { createDefaultInstallation } from "@calcom/app-store/_utils/installation"; +import { AppDeclarativeHandler } from "@calcom/types/AppHandler"; + +import appConfig from "../config.json"; + +const handler: AppDeclarativeHandler = { + appType: appConfig.type, + variant: appConfig.variant, + slug: appConfig.slug, + supportsMultipleInstalls: false, + handlerType: "add", + createCredential: ({ appType, user, slug }) => + createDefaultInstallation({ appType, userId: user.id, slug, key: {} }), +}; + +export default handler; diff --git a/packages/app-store/templates/event-type-app-card/api/index.ts b/packages/app-store/templates/event-type-app-card/api/index.ts new file mode 100644 index 0000000000..4c0d2ead01 --- /dev/null +++ b/packages/app-store/templates/event-type-app-card/api/index.ts @@ -0,0 +1 @@ +export { default as add } from "./add"; diff --git a/packages/app-store/_templates/extensions/EventTypeAppCard.tsx b/packages/app-store/templates/event-type-app-card/components/EventTypeAppCardInterface.tsx similarity index 94% rename from packages/app-store/_templates/extensions/EventTypeAppCard.tsx rename to packages/app-store/templates/event-type-app-card/components/EventTypeAppCardInterface.tsx index 930052fdcf..3eda0cc54d 100644 --- a/packages/app-store/_templates/extensions/EventTypeAppCard.tsx +++ b/packages/app-store/templates/event-type-app-card/components/EventTypeAppCardInterface.tsx @@ -32,8 +32,8 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ an AppCard for Event with Title: {eventType.title}
{" "}
- Edit packages/app-store/{app.slug}/extensions/EventTypeAppCard.tsx{" "} - to play with me + Edit packages/app-store/{app.slug}/EventTypeAppCardInterface.tsx to + play with me
diff --git a/packages/app-store/templates/event-type-app-card/config.json b/packages/app-store/templates/event-type-app-card/config.json new file mode 100644 index 0000000000..73fb7cc7b3 --- /dev/null +++ b/packages/app-store/templates/event-type-app-card/config.json @@ -0,0 +1,16 @@ +{ + "name": "EventType AppCard", + "slug": "event-type-app-card", + "type": "event-type-app-card_other", + "imageSrc": "icon.svg", + "logo": "icon.svg", + "url": "https://example.com/link", + "variant": "other", + "categories": ["other"], + "publisher": "Cal.com, Inc.", + "email": "support@cal.com", + "description": "A template showcasing how an app with EventType AppCard can be built e.g. Giphy, QR Code", + "extendsFeature": "EventType", + "isTemplate":true, + "__createdUsingCli": true +} diff --git a/packages/app-store/templates/event-type-app-card/index.ts b/packages/app-store/templates/event-type-app-card/index.ts new file mode 100644 index 0000000000..d7f3602204 --- /dev/null +++ b/packages/app-store/templates/event-type-app-card/index.ts @@ -0,0 +1 @@ +export * as api from "./api"; diff --git a/packages/app-store/_example/package.json b/packages/app-store/templates/event-type-app-card/package.json similarity index 61% rename from packages/app-store/_example/package.json rename to packages/app-store/templates/event-type-app-card/package.json index 757b0296d3..65ddce10eb 100644 --- a/packages/app-store/_example/package.json +++ b/packages/app-store/templates/event-type-app-card/package.json @@ -1,12 +1,11 @@ { "$schema": "https://json.schemastore.org/package.json", "private": true, - "name": "@calcom/example-app", + "name": "@calcom/template-event-type-app-card", "version": "0.0.0", "main": "./index.ts", - "description": "This is a brief description for the Example App.", "dependencies": { - "@calcom/prisma": "*" + "@calcom/lib": "*" }, "devDependencies": { "@calcom/types": "*" diff --git a/packages/app-store/templates/event-type-app-card/static/1.jpeg b/packages/app-store/templates/event-type-app-card/static/1.jpeg new file mode 100644 index 0000000000..d3ba68492d Binary files /dev/null and b/packages/app-store/templates/event-type-app-card/static/1.jpeg differ diff --git a/packages/app-store/templates/event-type-app-card/static/2.jpeg b/packages/app-store/templates/event-type-app-card/static/2.jpeg new file mode 100644 index 0000000000..dbdcdb5473 Binary files /dev/null and b/packages/app-store/templates/event-type-app-card/static/2.jpeg differ diff --git a/packages/app-store/templates/event-type-app-card/static/3.jpeg b/packages/app-store/templates/event-type-app-card/static/3.jpeg new file mode 100644 index 0000000000..d3ba68492d Binary files /dev/null and b/packages/app-store/templates/event-type-app-card/static/3.jpeg differ diff --git a/packages/app-store/templates/event-type-app-card/static/icon.svg b/packages/app-store/templates/event-type-app-card/static/icon.svg new file mode 100644 index 0000000000..44966edfc1 --- /dev/null +++ b/packages/app-store/templates/event-type-app-card/static/icon.svg @@ -0,0 +1 @@ +blockchain \ No newline at end of file diff --git a/packages/app-store/_templates/zod.ts b/packages/app-store/templates/event-type-app-card/zod.ts similarity index 69% rename from packages/app-store/_templates/zod.ts rename to packages/app-store/templates/event-type-app-card/zod.ts index 99e1f6da5d..5bb6cdc59f 100644 --- a/packages/app-store/_templates/zod.ts +++ b/packages/app-store/templates/event-type-app-card/zod.ts @@ -1,11 +1,10 @@ import { z } from "zod"; -import { eventTypeAppCardZod } from "../eventTypeAppCardZod"; +import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod"; export const appDataSchema = eventTypeAppCardZod.merge( z.object({ isSunrise: z.boolean(), }) ); - export const appKeysSchema = z.object({}); diff --git a/packages/app-store/templates/event-type-location-video-static/DESCRIPTION.md b/packages/app-store/templates/event-type-location-video-static/DESCRIPTION.md new file mode 100644 index 0000000000..02c40f00ac --- /dev/null +++ b/packages/app-store/templates/event-type-location-video-static/DESCRIPTION.md @@ -0,0 +1,8 @@ +--- +items: + - 1.jpeg + - 2.jpeg + - 3.jpeg +--- + +{DESCRIPTION} \ No newline at end of file diff --git a/packages/app-store/templates/event-type-location-video-static/api/add.ts b/packages/app-store/templates/event-type-location-video-static/api/add.ts new file mode 100644 index 0000000000..f8c6354180 --- /dev/null +++ b/packages/app-store/templates/event-type-location-video-static/api/add.ts @@ -0,0 +1,16 @@ +import { createDefaultInstallation } from "@calcom/app-store/_utils/installation"; +import { AppDeclarativeHandler } from "@calcom/types/AppHandler"; + +import appConfig from "../config.json"; + +const handler: AppDeclarativeHandler = { + appType: appConfig.type, + variant: appConfig.variant, + slug: appConfig.slug, + supportsMultipleInstalls: false, + handlerType: "add", + createCredential: ({ appType, user, slug }) => + createDefaultInstallation({ appType, userId: user.id, slug, key: {} }), +}; + +export default handler; diff --git a/packages/app-store/templates/event-type-location-video-static/api/index.ts b/packages/app-store/templates/event-type-location-video-static/api/index.ts new file mode 100644 index 0000000000..4c0d2ead01 --- /dev/null +++ b/packages/app-store/templates/event-type-location-video-static/api/index.ts @@ -0,0 +1 @@ +export { default as add } from "./add"; diff --git a/packages/app-store/templates/event-type-location-video-static/config.json b/packages/app-store/templates/event-type-location-video-static/config.json new file mode 100644 index 0000000000..751de618ca --- /dev/null +++ b/packages/app-store/templates/event-type-location-video-static/config.json @@ -0,0 +1,25 @@ +{ + "/*": "Don't modify slug - If required, do it using cli edit command", + "name": "EventType Location Video - Static", + "slug": "event-type-location-video-static", + "type": "event-type-location-video-static_video", + "imageSrc": "icon.svg", + "logo": "icon.svg", + "url": "https://example.com/link", + "variant": "conferencing", + "categories": ["video"], + "publisher": "Cal.com Inc", + "email": "support@cal.com", + "appData": { + "location": { + "type": "integrations:{SLUG}_video", + "label": "{TITLE}", + "linkType": "static", + "organizerInputPlaceholder": "https://video.app/mylink", + "urlRegExp": "^http(s)?:\\/\\/(www\\.)?video.app\\/[a-zA-Z0-9]*" + } + }, + "description": "It is a template showing how to add a static URL EventType location e.g. Around, Whereby", + "isTemplate":true, + "__createdUsingCli": true +} diff --git a/packages/app-store/templates/event-type-location-video-static/index.ts b/packages/app-store/templates/event-type-location-video-static/index.ts new file mode 100644 index 0000000000..d7f3602204 --- /dev/null +++ b/packages/app-store/templates/event-type-location-video-static/index.ts @@ -0,0 +1 @@ +export * as api from "./api"; diff --git a/packages/app-store/templates/event-type-location-video-static/package.json b/packages/app-store/templates/event-type-location-video-static/package.json new file mode 100644 index 0000000000..0227642829 --- /dev/null +++ b/packages/app-store/templates/event-type-location-video-static/package.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "name": "@calcom/template-event-type-location-video-static", + "version": "0.0.0", + "main": "./index.ts", + "dependencies": { + "@calcom/lib": "*" + }, + "devDependencies": { + "@calcom/types": "*" + } +} diff --git a/packages/app-store/templates/event-type-location-video-static/static/1.jpeg b/packages/app-store/templates/event-type-location-video-static/static/1.jpeg new file mode 100644 index 0000000000..d3ba68492d Binary files /dev/null and b/packages/app-store/templates/event-type-location-video-static/static/1.jpeg differ diff --git a/packages/app-store/templates/event-type-location-video-static/static/2.jpeg b/packages/app-store/templates/event-type-location-video-static/static/2.jpeg new file mode 100644 index 0000000000..dbdcdb5473 Binary files /dev/null and b/packages/app-store/templates/event-type-location-video-static/static/2.jpeg differ diff --git a/packages/app-store/templates/event-type-location-video-static/static/3.jpeg b/packages/app-store/templates/event-type-location-video-static/static/3.jpeg new file mode 100644 index 0000000000..d3ba68492d Binary files /dev/null and b/packages/app-store/templates/event-type-location-video-static/static/3.jpeg differ diff --git a/packages/app-store/templates/event-type-location-video-static/static/icon.svg b/packages/app-store/templates/event-type-location-video-static/static/icon.svg new file mode 100644 index 0000000000..44966edfc1 --- /dev/null +++ b/packages/app-store/templates/event-type-location-video-static/static/icon.svg @@ -0,0 +1 @@ +blockchain \ No newline at end of file diff --git a/packages/app-store/templates/general-app-settings/DESCRIPTION.md b/packages/app-store/templates/general-app-settings/DESCRIPTION.md new file mode 100644 index 0000000000..02c40f00ac --- /dev/null +++ b/packages/app-store/templates/general-app-settings/DESCRIPTION.md @@ -0,0 +1,8 @@ +--- +items: + - 1.jpeg + - 2.jpeg + - 3.jpeg +--- + +{DESCRIPTION} \ No newline at end of file diff --git a/packages/app-store/templates/general-app-settings/api/add.ts b/packages/app-store/templates/general-app-settings/api/add.ts new file mode 100644 index 0000000000..f8c6354180 --- /dev/null +++ b/packages/app-store/templates/general-app-settings/api/add.ts @@ -0,0 +1,16 @@ +import { createDefaultInstallation } from "@calcom/app-store/_utils/installation"; +import { AppDeclarativeHandler } from "@calcom/types/AppHandler"; + +import appConfig from "../config.json"; + +const handler: AppDeclarativeHandler = { + appType: appConfig.type, + variant: appConfig.variant, + slug: appConfig.slug, + supportsMultipleInstalls: false, + handlerType: "add", + createCredential: ({ appType, user, slug }) => + createDefaultInstallation({ appType, userId: user.id, slug, key: {} }), +}; + +export default handler; diff --git a/packages/app-store/templates/general-app-settings/api/index.ts b/packages/app-store/templates/general-app-settings/api/index.ts new file mode 100644 index 0000000000..4c0d2ead01 --- /dev/null +++ b/packages/app-store/templates/general-app-settings/api/index.ts @@ -0,0 +1 @@ +export { default as add } from "./add"; diff --git a/packages/app-store/templates/general-app-settings/components/AppSettingsInterface.tsx b/packages/app-store/templates/general-app-settings/components/AppSettingsInterface.tsx new file mode 100644 index 0000000000..666f6eaeb7 --- /dev/null +++ b/packages/app-store/templates/general-app-settings/components/AppSettingsInterface.tsx @@ -0,0 +1,23 @@ +import { useState } from "react"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Button, TextField } from "@calcom/ui"; + +export default function AppSettings() { + const { t } = useLocale(); + const [input, setInput] = useState(""); + + return ( +
+ { + setInput(e.target.value); + }} + /> + +
+ ); +} diff --git a/packages/app-store/templates/general-app-settings/config.json b/packages/app-store/templates/general-app-settings/config.json new file mode 100644 index 0000000000..f7f33c89bb --- /dev/null +++ b/packages/app-store/templates/general-app-settings/config.json @@ -0,0 +1,15 @@ +{ + "name": "General App Settings", + "slug": "general-app-settings", + "type": "general-app-settings_other", + "imageSrc": "icon.svg", + "logo": "icon.svg", + "url": "https://example.com/link", + "variant": "other", + "categories": ["other"], + "publisher": "Cal.com Inc", + "email": "support@cal.com", + "description": "It is a template showing how an App can have settings in installed section that configures the app globally e.g. Weather in your Calendar", + "isTemplate":true, + "__createdUsingCli": true +} diff --git a/packages/app-store/templates/general-app-settings/index.ts b/packages/app-store/templates/general-app-settings/index.ts new file mode 100644 index 0000000000..d7f3602204 --- /dev/null +++ b/packages/app-store/templates/general-app-settings/index.ts @@ -0,0 +1 @@ +export * as api from "./api"; diff --git a/packages/app-store/templates/general-app-settings/package.json b/packages/app-store/templates/general-app-settings/package.json new file mode 100644 index 0000000000..13ee6a80bb --- /dev/null +++ b/packages/app-store/templates/general-app-settings/package.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "name": "@calcom/template-general-app-settings", + "version": "0.0.0", + "main": "./index.ts", + "dependencies": { + "@calcom/lib": "*" + }, + "devDependencies": { + "@calcom/types": "*" + } +} diff --git a/packages/app-store/templates/general-app-settings/static/1.jpeg b/packages/app-store/templates/general-app-settings/static/1.jpeg new file mode 100644 index 0000000000..d3ba68492d Binary files /dev/null and b/packages/app-store/templates/general-app-settings/static/1.jpeg differ diff --git a/packages/app-store/templates/general-app-settings/static/2.jpeg b/packages/app-store/templates/general-app-settings/static/2.jpeg new file mode 100644 index 0000000000..dbdcdb5473 Binary files /dev/null and b/packages/app-store/templates/general-app-settings/static/2.jpeg differ diff --git a/packages/app-store/templates/general-app-settings/static/3.jpeg b/packages/app-store/templates/general-app-settings/static/3.jpeg new file mode 100644 index 0000000000..d3ba68492d Binary files /dev/null and b/packages/app-store/templates/general-app-settings/static/3.jpeg differ diff --git a/packages/app-store/templates/general-app-settings/static/icon.svg b/packages/app-store/templates/general-app-settings/static/icon.svg new file mode 100644 index 0000000000..44966edfc1 --- /dev/null +++ b/packages/app-store/templates/general-app-settings/static/icon.svg @@ -0,0 +1 @@ +blockchain \ No newline at end of file diff --git a/packages/app-store/templates/link-as-an-app/DESCRIPTION.md b/packages/app-store/templates/link-as-an-app/DESCRIPTION.md new file mode 100644 index 0000000000..a4b15502c3 --- /dev/null +++ b/packages/app-store/templates/link-as-an-app/DESCRIPTION.md @@ -0,0 +1,8 @@ +--- +items: + - 1.jpeg + - 2.jpeg + - 3.jpeg +--- + +{DESCRIPTION} diff --git a/packages/app-store/templates/link-as-an-app/api/add.ts b/packages/app-store/templates/link-as-an-app/api/add.ts new file mode 100644 index 0000000000..d27cdfd098 --- /dev/null +++ b/packages/app-store/templates/link-as-an-app/api/add.ts @@ -0,0 +1,20 @@ +import { createDefaultInstallation } from "@calcom/app-store/_utils/installation"; +import { AppDeclarativeHandler } from "@calcom/types/AppHandler"; + +import appConfig from "../config.json"; + +const handler: AppDeclarativeHandler = { + appType: appConfig.type, + variant: appConfig.variant, + slug: appConfig.slug, + supportsMultipleInstalls: false, + handlerType: "add", + redirect: { + newTab: true, + url: "https://example.com/link", + }, + createCredential: ({ appType, user, slug }) => + createDefaultInstallation({ appType, userId: user.id, slug, key: {} }), +}; + +export default handler; diff --git a/packages/app-store/templates/link-as-an-app/api/index.ts b/packages/app-store/templates/link-as-an-app/api/index.ts new file mode 100644 index 0000000000..4c0d2ead01 --- /dev/null +++ b/packages/app-store/templates/link-as-an-app/api/index.ts @@ -0,0 +1 @@ +export { default as add } from "./add"; diff --git a/packages/app-store/plausible/components/.gitkeep b/packages/app-store/templates/link-as-an-app/components/.gitkeep similarity index 100% rename from packages/app-store/plausible/components/.gitkeep rename to packages/app-store/templates/link-as-an-app/components/.gitkeep diff --git a/packages/app-store/templates/link-as-an-app/config.json b/packages/app-store/templates/link-as-an-app/config.json new file mode 100644 index 0000000000..24f8fe95d8 --- /dev/null +++ b/packages/app-store/templates/link-as-an-app/config.json @@ -0,0 +1,16 @@ +{ + "/*": "Don't modify slug - If required, do it using cli edit command", + "name": "Link as An App", + "slug": "link-as-an-app", + "type": "link-as-an-app_other", + "imageSrc": "icon.svg", + "logo": "icon.svg", + "url": "https://example.com/link", + "variant": "other", + "categories": ["other"], + "publisher": "Cal.com, Inc.", + "email": "support@cal.com", + "description": "It's a template showing how an app, that is just a link to some webpage, can be made e.g. Pipedream, Amie, Vimcal.", + "isTemplate":true, + "__createdUsingCli": true +} diff --git a/packages/app-store/templates/link-as-an-app/index.ts b/packages/app-store/templates/link-as-an-app/index.ts new file mode 100644 index 0000000000..d7f3602204 --- /dev/null +++ b/packages/app-store/templates/link-as-an-app/index.ts @@ -0,0 +1 @@ +export * as api from "./api"; diff --git a/packages/app-store/templates/link-as-an-app/package.json b/packages/app-store/templates/link-as-an-app/package.json new file mode 100644 index 0000000000..ad8ec5fe0b --- /dev/null +++ b/packages/app-store/templates/link-as-an-app/package.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "name": "@calcom/link-as-an-app", + "version": "0.0.0", + "main": "./index.ts", + "dependencies": { + "@calcom/lib": "*" + }, + "devDependencies": { + "@calcom/types": "*" + } +} diff --git a/packages/app-store/templates/link-as-an-app/static/1.jpeg b/packages/app-store/templates/link-as-an-app/static/1.jpeg new file mode 100644 index 0000000000..d3ba68492d Binary files /dev/null and b/packages/app-store/templates/link-as-an-app/static/1.jpeg differ diff --git a/packages/app-store/templates/link-as-an-app/static/2.jpeg b/packages/app-store/templates/link-as-an-app/static/2.jpeg new file mode 100644 index 0000000000..dbdcdb5473 Binary files /dev/null and b/packages/app-store/templates/link-as-an-app/static/2.jpeg differ diff --git a/packages/app-store/templates/link-as-an-app/static/3.jpeg b/packages/app-store/templates/link-as-an-app/static/3.jpeg new file mode 100644 index 0000000000..d3ba68492d Binary files /dev/null and b/packages/app-store/templates/link-as-an-app/static/3.jpeg differ diff --git a/packages/app-store/templates/link-as-an-app/static/icon.svg b/packages/app-store/templates/link-as-an-app/static/icon.svg new file mode 100644 index 0000000000..44966edfc1 --- /dev/null +++ b/packages/app-store/templates/link-as-an-app/static/icon.svg @@ -0,0 +1 @@ +blockchain \ No newline at end of file diff --git a/packages/app-store/typeform/api/add.ts b/packages/app-store/typeform/api/add.ts index 5ea6d24a58..dc41ae123c 100644 --- a/packages/app-store/typeform/api/add.ts +++ b/packages/app-store/typeform/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, slug: appConfig.slug, variant: appConfig.variant, diff --git a/packages/app-store/types.d.ts b/packages/app-store/types.d.ts index b586522960..37c8b91951 100644 --- a/packages/app-store/types.d.ts +++ b/packages/app-store/types.d.ts @@ -10,6 +10,12 @@ export type IntegrationOAuthCallbackState = { installGoogleVideo?: boolean; }; +type AppScript = { attrs?: Record } & { src?: string; content?: string }; + +export type Tag = { + scripts: AppScript[]; +}; + export interface InstallAppButtonProps { render: ( renderProps: ButtonProps & { diff --git a/packages/app-store/utils.ts b/packages/app-store/utils.ts index cdef280d0d..05fc9f742f 100644 --- a/packages/app-store/utils.ts +++ b/packages/app-store/utils.ts @@ -2,20 +2,25 @@ import { Prisma } from "@prisma/client"; import { TFunction } from "next-i18next"; import { z } from "zod"; +// If you import this file on any app it should produce circular dependency +// import appStore from "./index"; +import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; import { defaultLocations, EventLocationType } from "@calcom/app-store/locations"; import { EventTypeModel } from "@calcom/prisma/zod"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { App, AppMeta } from "@calcom/types/App"; -// If you import this file on any app it should produce circular dependency -// import appStore from "./index"; -import { appStoreMetadata } from "./apps.metadata.generated"; - export type EventTypeApps = NonNullable>["apps"]>; export type EventTypeAppsList = keyof EventTypeApps; const ALL_APPS_MAP = Object.keys(appStoreMetadata).reduce((store, key) => { - store[key] = appStoreMetadata[key as keyof typeof appStoreMetadata]; + const metadata = appStoreMetadata[key as keyof typeof appStoreMetadata] as AppMeta; + if (metadata.logo && !metadata.logo.includes("/")) { + const appDirName = `${metadata.isTemplate ? "templates" : ""}/${metadata.slug}`; + metadata.logo = `/api/app-store/${appDirName}/${metadata.logo}`; + } + store[key] = metadata; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore delete store[key]["/*"]; diff --git a/packages/app-store/vimcal/api/add.ts b/packages/app-store/vimcal/api/add.ts index a9a7df96d3..b1cbdc1ac1 100644 --- a/packages/app-store/vimcal/api/add.ts +++ b/packages/app-store/vimcal/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/vimcal/config.json b/packages/app-store/vimcal/config.json index 41bfd3b790..7ea2953c5e 100644 --- a/packages/app-store/vimcal/config.json +++ b/packages/app-store/vimcal/config.json @@ -11,6 +11,5 @@ "publisher": "Cal.com, Inc.", "email": "support@cal.com", "description": "The world's fastest calendar, beautifully designed for a remote world\r", - "extendsFeature": "User", "__createdUsingCli": true } diff --git a/packages/app-store/weather_in_your_calendar/api/add.ts b/packages/app-store/weather_in_your_calendar/api/add.ts index 45c6741f36..8f569c854c 100644 --- a/packages/app-store/weather_in_your_calendar/api/add.ts +++ b/packages/app-store/weather_in_your_calendar/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/weather_in_your_calendar/components/AppSettings.tsx b/packages/app-store/weather_in_your_calendar/components/AppSettingsInterface.tsx similarity index 100% rename from packages/app-store/weather_in_your_calendar/components/AppSettings.tsx rename to packages/app-store/weather_in_your_calendar/components/AppSettingsInterface.tsx diff --git a/packages/app-store/whatsapp/api/add.ts b/packages/app-store/whatsapp/api/add.ts index 45c6741f36..8f569c854c 100644 --- a/packages/app-store/whatsapp/api/add.ts +++ b/packages/app-store/whatsapp/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/whatsapp/config.json b/packages/app-store/whatsapp/config.json index d18f8e792f..6ce34f58e5 100644 --- a/packages/app-store/whatsapp/config.json +++ b/packages/app-store/whatsapp/config.json @@ -11,7 +11,6 @@ "publisher": "Cal.com, Inc.", "email": "support@cal.com", "description": "Schedule a chat with your guests or have a WhatsApp Video call.", - "extendsFeature": "User", "__createdUsingCli": true, "appData": { "location": { diff --git a/packages/app-store/wordpress/api/add.ts b/packages/app-store/wordpress/api/add.ts index fb6e427a8a..f4b106f9d4 100644 --- a/packages/app-store/wordpress/api/add.ts +++ b/packages/app-store/wordpress/api/add.ts @@ -4,7 +4,6 @@ import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { - // Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, diff --git a/packages/app-store/zapier/components/AppSettings.tsx b/packages/app-store/zapier/components/AppSettingsInterface.tsx similarity index 100% rename from packages/app-store/zapier/components/AppSettings.tsx rename to packages/app-store/zapier/components/AppSettingsInterface.tsx diff --git a/packages/app-store/zapier/components/TemplateCard.tsx b/packages/app-store/zapier/components/TemplateCard.tsx index dd6d546938..34b955858f 100644 --- a/packages/app-store/zapier/components/TemplateCard.tsx +++ b/packages/app-store/zapier/components/TemplateCard.tsx @@ -1,6 +1,6 @@ import { Button } from "@calcom/ui"; -import { Template } from "./AppSettings"; +import { Template } from "./AppSettingsInterface"; export default function TemplateCard({ template }: { template: Template }) { return ( diff --git a/packages/features/apps/AdminAppsList.tsx b/packages/features/apps/AdminAppsList.tsx index cda2986067..328aaebbc8 100644 --- a/packages/features/apps/AdminAppsList.tsx +++ b/packages/features/apps/AdminAppsList.tsx @@ -11,6 +11,7 @@ import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { RouterOutputs, trpc } from "@calcom/trpc/react"; import { + Badge, Button, ConfirmationDialogContent, Dialog, @@ -76,8 +77,13 @@ const IntegrationContainer = ({
{app.logo && {app.title}}
-

+

{app.name || app.title}

+ {app.isTemplate && ( + + Template + + )}

{app.description}

diff --git a/packages/features/kbar/Kbar.tsx b/packages/features/kbar/Kbar.tsx index e4e1b2350c..a641800f12 100644 --- a/packages/features/kbar/Kbar.tsx +++ b/packages/features/kbar/Kbar.tsx @@ -11,7 +11,7 @@ import { import { useRouter } from "next/router"; import { useMemo } from "react"; -import { appStoreMetadata } from "@calcom/app-store/apps.metadata.generated"; +import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { isMac } from "@calcom/lib/isMac"; import { RouterOutputs, trpc } from "@calcom/trpc/react"; diff --git a/packages/prisma/seed-app-store.config.json b/packages/prisma/seed-app-store.config.json index 01f313ddc3..1ee0c8efbe 100644 --- a/packages/prisma/seed-app-store.config.json +++ b/packages/prisma/seed-app-store.config.json @@ -2,158 +2,264 @@ { "/*": "This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually", "dirName": "routing-forms", - "categories": ["other"], + "categories": [ + "other" + ], "slug": "routing-forms", "type": "routing-forms_other" }, { "dirName": "whereby", - "categories": ["video"], + "categories": [ + "video" + ], "slug": "whereby", "type": "whereby_video" }, { "dirName": "around", - "categories": ["video"], + "categories": [ + "video" + ], "slug": "around", "type": "around_video" }, { "dirName": "riverside", - "categories": ["video"], + "categories": [ + "video" + ], "slug": "riverside", "type": "riverside_video" }, { "dirName": "typeform", - "categories": ["other"], + "categories": [ + "other" + ], "slug": "typeform", "type": "typeform_other" }, { "dirName": "ping", - "categories": ["video"], + "categories": [ + "video" + ], "slug": "ping", "type": "ping_video" }, { "dirName": "campfire", - "categories": ["video"], + "categories": [ + "video" + ], "slug": "campfire", "type": "campfire_video" }, { "dirName": "rainbow", - "categories": ["web3"], + "categories": [ + "web3" + ], "slug": "rainbow", "type": "rainbow_web3" }, { "dirName": "raycast", - "categories": ["other"], + "categories": [ + "other" + ], "slug": "raycast", "type": "raycast_other" }, { "dirName": "n8n", - "categories": ["automation"], + "categories": [ + "automation" + ], "slug": "n8n", "type": "n8n_automation" }, { "dirName": "exchangecalendar", - "categories": ["calendar"], + "categories": [ + "calendar" + ], "slug": "exchange", "type": "exchange_calendar" }, { "dirName": "qr_code", - "categories": ["other"], + "categories": [ + "other" + ], "slug": "qr_code", "type": "qr_code_other" }, { "dirName": "weather_in_your_calendar", - "categories": ["other"], + "categories": [ + "other" + ], "slug": "weather_in_your_calendar", "type": "weather_in_your_calendar_other" }, { "dirName": "fathom", - "categories": ["analytics"], + "categories": [ + "analytics" + ], "slug": "fathom", "type": "fathom_analytics" }, { "dirName": "plausible", - "categories": ["analytics"], + "categories": [ + "analytics" + ], "slug": "plausible", "type": "plausible_analytics" }, { "dirName": "wordpress", - "categories": ["other"], + "categories": [ + "other" + ], "slug": "wordpress", "type": "wordpress_other" }, { "dirName": "ga4", - "categories": ["analytics"], + "categories": [ + "analytics" + ], "slug": "ga4", "type": "ga4_analytics" }, { "dirName": "pipedream", - "categories": ["automation"], + "categories": [ + "automation" + ], "slug": "pipedream", "type": "pipedream_automation" }, { "dirName": "sirius_video", - "categories": ["video"], + "categories": [ + "video" + ], "slug": "sirius_video", "type": "sirius_video_video" }, { "dirName": "sendgrid", - "categories": ["other"], + "categories": [ + "other" + ], "slug": "sendgrid", "type": "sendgrid_other_calendar" }, { "dirName": "closecom", - "categories": ["other"], + "categories": [ + "other" + ], "slug": "closecom", "type": "closecom_other_calendar" }, { "dirName": "whatsapp", - "categories": ["video"], + "categories": [ + "video" + ], "slug": "whatsapp", "type": "whatsapp_video" }, { "dirName": "telegram", - "categories": ["video"], + "categories": [ + "video" + ], "slug": "telegram", "type": "telegram_video" }, { "dirName": "signal", - "categories": ["video"], + "categories": [ + "video" + ], "slug": "signal", "type": "signal_video" }, { "dirName": "vimcal", - "categories": ["calendar"], + "categories": [ + "calendar" + ], "slug": "vimcal", "type": "vimcal_other" }, { "dirName": "amie", - "categories": ["calendar"], + "categories": [ + "calendar" + ], "slug": "amie", "type": "amie_other" + }, + { + "dirName": "booking-pages-tag", + "categories": [ + "analytics" + ], + "slug": "booking-pages-tag", + "type": "booking-pages-tag_other", + "isTemplate": true + }, + { + "dirName": "event-type-app-card", + "categories": [ + "other" + ], + "slug": "event-type-app-card", + "type": "event-type-app-card_other", + "isTemplate": true + }, + { + "dirName": "event-type-location-video-static", + "categories": [ + "video" + ], + "slug": "event-type-location-video-static", + "type": "event-type-location-video-static_other", + "isTemplate": true + }, + { + "dirName": "general-app-settings", + "categories": [ + "other" + ], + "slug": "general-app-settings", + "type": "general-app-settings_other", + "isTemplate": true + }, + { + "dirName": "link-as-an-app", + "categories": [ + "other" + ], + "slug": "link-as-an-app", + "type": "link-as-an-app_other", + "isTemplate": true + }, + { + "dirName": "basic", + "categories": [ + "other" + ], + "slug": "basic", + "type": "basic_other", + "isTemplate": true } -] +] \ No newline at end of file diff --git a/packages/prisma/seed-app-store.ts b/packages/prisma/seed-app-store.ts index 5bc0b877d7..54ce69d978 100644 --- a/packages/prisma/seed-app-store.ts +++ b/packages/prisma/seed-app-store.ts @@ -154,7 +154,8 @@ async function createApp( categories: Prisma.AppCreateInput["categories"], /** This is used so credentials gets linked to the correct app */ type: Prisma.CredentialCreateInput["type"], - keys?: Prisma.AppCreateInput["keys"] + keys?: Prisma.AppCreateInput["keys"], + isTemplate?: boolean ) { await prisma.app.upsert({ where: { slug }, @@ -165,7 +166,7 @@ async function createApp( where: { type }, data: { appId: slug }, }); - console.log(`📲 Upserted app: '${slug}'`); + console.log(`📲 Upserted ${isTemplate ? "template" : "app"}: '${slug}'`); } export default async function main() { @@ -293,7 +294,17 @@ export default async function main() { ); for (let i = 0; i < generatedApps.length; i++) { const generatedApp = generatedApps[i]; - await createApp(generatedApp.slug, generatedApp.dirName, generatedApp.categories, generatedApp.type); + if (generatedApp.isTemplate && process.argv[2] !== "seed-templates") { + continue; + } + await createApp( + generatedApp.slug, + generatedApp.dirName, + generatedApp.categories, + generatedApp.type, + undefined, + generatedApp.isTemplate + ); } await seedAppData(); diff --git a/packages/trpc/server/routers/viewer.tsx b/packages/trpc/server/routers/viewer.tsx index 598df2a4ef..29b5520611 100644 --- a/packages/trpc/server/routers/viewer.tsx +++ b/packages/trpc/server/routers/viewer.tsx @@ -474,7 +474,6 @@ const loggedInViewerRouter = router({ if (onlyInstalled) { apps = apps.flatMap((item) => (item.credentialIds.length > 0 || item.isGlobal ? [item] : [])); } - return { items: apps, }; diff --git a/packages/trpc/server/routers/viewer/apps.tsx b/packages/trpc/server/routers/viewer/apps.tsx index 09f54bb453..0f053e27e0 100644 --- a/packages/trpc/server/routers/viewer/apps.tsx +++ b/packages/trpc/server/routers/viewer/apps.tsx @@ -22,6 +22,7 @@ interface FilteredApp { dirName: string; keys: Prisma.JsonObject | null; enabled: boolean; + isTemplate?: boolean; } export const appsRouter = router({ @@ -72,6 +73,7 @@ export const appsRouter = router({ keys: dbData.keys, dirName: app.dirName || app.slug, enabled: dbData?.enabled || false, + isTemplate: app.isTemplate, }); } else { const keysSchema = appKeysSchemas[app.dirName as keyof typeof appKeysSchemas]; diff --git a/packages/types/App.d.ts b/packages/types/App.d.ts index 783bd514be..a632c0a82e 100644 --- a/packages/types/App.d.ts +++ b/packages/types/App.d.ts @@ -1,5 +1,7 @@ import type { Prisma } from "@prisma/client"; +import { Tag } from "@calcom/app-store/types"; + import { Optional } from "./utils"; type CommonProperties = { @@ -30,9 +32,10 @@ type DynamicLinkBasedEventLocation = { export type EventLocationTypeFromAppMeta = StaticLinkBasedEventLocation | DynamicLinkBasedEventLocation; -type EventLocationAppData = { - location: EventLocationTypeFromAppMeta; -}; +type AppData = { + location?: EventLocationTypeFromAppMeta; + tag?: Tag; +} | null; /** * This is the definition for an app store's app metadata. @@ -41,7 +44,7 @@ type EventLocationAppData = { export interface App { /** * @deprecated - * Wheter if the app is installed or not. Usually we check for api keys in env + * Whether if the app is installed or not. Usually we check for api keys in env * variables to determine if this is true or not. * */ installed?: boolean; @@ -107,13 +110,13 @@ export interface App { /** Optional documentation website URL */ docsUrl?: string; /** Wether if the app is verified by Cal.com or not */ - verified: boolean; + verified?: boolean; /** Wether the app should appear in the trending section of the app store */ - trending: boolean; + trending?: boolean; /** Rating from 0 to 5, harcoded for now. Should be fetched later on. */ - rating: number; + rating?: number; /** Number of reviews, harcoded for now. Should be fetched later on. */ - reviews: number; + reviews?: number; /** * Wheter if the app is installed globally or needs user intervention. * Used to show Connect/Disconnect buttons in App Store @@ -131,8 +134,10 @@ export interface App { commission?: number; licenseRequired?: boolean; isProOnly?: boolean; - appData?: EventLocationAppData; + appData?: AppData; dirName?: string; + isTemplate?: boolean; + __template?: string; } export type AppFrontendPayload = Omit & { diff --git a/packages/ui/components/apps/AppCard.tsx b/packages/ui/components/apps/AppCard.tsx index b0f05106e5..342b80bf05 100644 --- a/packages/ui/components/apps/AppCard.tsx +++ b/packages/ui/components/apps/AppCard.tsx @@ -72,6 +72,7 @@ export function AppCard({ app, credentials, searchText }: AppCardProps) { }}> {app.description}

+