Use slug everywhere, add edit command and other improvements

This commit is contained in:
Hariom Balhara 2022-06-02 15:49:39 +05:30
parent 721e1c0d33
commit 95d7c17e4b
37 changed files with 409 additions and 127 deletions

View File

@ -64,9 +64,9 @@ export default function App({
const [installedApp, setInstalledApp] = useState(false);
useEffect(() => {
async function getInstalledApp(appCredentialType: string) {
async function getInstalledApp(slug: string) {
const queryParam = new URLSearchParams();
queryParam.set("app-credential-type", appCredentialType);
queryParam.set("app-slug", slug);
try {
const result = await fetch(`/api/app-store/installed?${queryParam.toString()}`, {
method: "GET",
@ -83,8 +83,8 @@ export default function App({
}
}
}
getInstalledApp(type);
}, [type]);
getInstalledApp(slug);
}, [slug]);
return (
<>
@ -117,7 +117,7 @@ export default function App({
</Button>
) : (
<InstallAppButton
type={type}
slug={slug}
render={(buttonProps) => (
<Button data-testid="install-app-button" {...buttonProps}>
{t("install_app")}

View File

@ -7,20 +7,33 @@ import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
req.session = await getSession({ req });
if (req.method === "GET" && req.session && req.session.user.id && req.query) {
const { "app-credential-type": appCredentialType } = req.query;
const { "app-slug": appSlug } = req.query;
if (!appCredentialType && Array.isArray(appCredentialType)) {
if (!appSlug && Array.isArray(appSlug)) {
return res.status(400);
}
const userId = req.session.user.id;
let where;
if (appSlug === "giphy") {
where = {
userId: userId,
type: "giphy_other",
};
} else if (appSlug === "slack") {
where = {
userId: userId,
type: "slack_app",
};
} else {
where = {
userId: userId,
appId: appSlug,
};
}
try {
const installedApp = await prisma.credential.findFirst({
where: {
type: appCredentialType as string,
userId: userId,
},
where,
});
if (installedApp && !!installedApp.key) {

View File

@ -1,6 +1,6 @@
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
import { deriveAppKeyFromType } from "@calcom/lib/deriveAppKeyFromSlug";
import { deriveAppKeyFromSlugOrType } from "@calcom/lib/deriveAppKeyFromSlugOrType";
import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error";
@ -20,8 +20,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
/* Absolute path didn't work */
const handlerMap = (await import("@calcom/app-store/apps.generated")).apiHandlers;
const handlerKey = deriveAppKeyFromType(appName, handlerMap);
console.log(handlerKey);
const handlerKey = deriveAppKeyFromSlugOrType(appName, handlerMap);
const handlers = await handlerMap[handlerKey];
const handler = handlers[apiEndpoint as keyof typeof handlers] as NextApiHandler;
if (typeof handler !== "function")

View File

@ -619,10 +619,21 @@ const loggedInViewerRouter = createProtectedRouter()
function countActive(items: { credentialIds: unknown[] }[]) {
return items.reduce((acc, item) => acc + item.credentialIds.length, 0);
}
const apps = getApps(credentials).map(
({ credentials: _, credential: _1 /* don't leak to frontend */, ...app }) => ({
...app,
credentialIds: credentials.filter((c) => c.type === app.type).map((c) => c.id),
credentialIds: credentials
.filter((c) => {
const slug = app.slug;
if (slug === "giphy") {
return c.type === "giphy_other";
} else if (slug === "slack") {
return c.type === "slack_app";
}
return c.appId === app.slug;
})
.map((c) => c.id),
})
);
// `flatMap()` these work like `.filter()` but infers the types correctly

View File

@ -25,7 +25,8 @@
"docs-build": "turbo run build --scope=\"@calcom/docs\" --include-dependencies",
"docs-start": "turbo run start --scope=\"@calcom/docs\"",
"app-store:watch": "node packages/app-store/app-store.js --watch",
"dx": "yarn predev && (git submodule update || true) && turbo run dx",
"dx:web": "yarn predev && (git submodule update || true) && turbo run dx",
"dx": "run-p 'dx:web' 'app-store:watch'",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"heroku-postbuild": "turbo run @calcom/web#build",
"lint": "turbo run lint",

View File

@ -15,12 +15,12 @@
"dependencies": {
"@calcom/lib": "*",
"ink": "^3.2.0",
"ink-select-input": "^4.2.1",
"ink-text-input": "^4.0.3",
"meow": "^9.0.0",
"react": "^17.0.2"
},
"devDependencies": {
"ts-node": "^10.6.0",
"@ava/typescript": "^3.0.1",
"@types/react": "^18.0.9",
"ava": "^4.2.0",
@ -29,6 +29,7 @@
"eslint-plugin-react": "^7.30.0",
"eslint-plugin-react-hooks": "^4.5.0",
"ink-testing-library": "^2.1.0",
"ts-node": "^10.6.0",
"typescript": "^4.6.4",
"xo": "^0.39.1"
}

View File

@ -18,34 +18,18 @@ If we rename all existing apps to their slug names, we can remove type and then
## TODO
- Beta Release
- Print slug after creation of app. Also, mention that it would be same as dir name
- Handle legacy apps which have dirname as something else and type as something else. type is used to do lookups with key
- Add comment in config.json that this file shouldn't be modified manually.
- Install button not coming
- Put lowercase and - restriction only on slug. Keep App Name and others unchanged. Also, use slug instead of appName for dirNames
- Add space restriction as well for Appname. Maybe look for valid dirname or slug regex
- Get strong confirmation for deletion of app. Get the name of the app from user that he wants to delete
- App Description Missing
- Select Box for App Type
- App types Validations
- Credentials table doesn't get new entries with cli. Figure out how to do it.
- Using app/config.json -> Allow Editing App Details.
- Edit App Type, Description, Title, Publisher Name, Email - Name shouldn't be allowed to change as that is unique.
- Show a warning somewhere that app directory must not be renamed manually, edit command must be used.
- Improvements
- Prefill fields in edit command
- 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.
- Maybe get dx to run app-store:watch
- App already exists check. Ask user to run edit/regenerate command
- 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.
### Why we shouldn't have appType
- App can have multiple types and thus categories is more suitable for this.
- The reason we seem to be using appType is to refer to an app uniquely but we already have app slug for that.
- Require assets path relative to app dir.
## Roadmap
- 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.

View File

@ -1,6 +1,7 @@
import child_process from "child_process";
import fs from "fs";
import { Box, Text, useApp, useInput, useStdin } from "ink";
import SelectInput from "ink-select-input";
import TextInput from "ink-text-input";
import path from "path";
import React, { FC, useEffect, useRef, useState } from "react";
@ -25,33 +26,48 @@ const appStoreDir = path.resolve(__dirname, "..", "..", "app-store");
const workspaceDir = path.resolve(__dirname, "..", "..", "..");
const execSync = (...args) => {
const result = child_process.execSync(...args).toString();
console.log(`$: ${args[0]}`);
console.log(result);
if (process.env.DEBUG === "1") {
console.log(`$: ${args[0]}`);
console.log(result);
}
return args[0];
};
function absolutePath(appRelativePath) {
return path.join(appStoreDir, appRelativePath);
}
const updatePackageJson = ({ slug, appDirPath }) => {
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* ({ appType, appName, slug, appTitle, publisherName, publisherEmail }) {
create: function* ({
appType,
editMode,
appDescription,
appName,
slug,
appTitle,
publisherName,
publisherEmail,
}) {
const appDirPath = getAppDirPath(slug);
yield "Forking base app";
execSync(`mkdir -p ${appDirPath}`);
execSync(`cp -r ${absolutePath("_baseApp/*")} ${appDirPath}`);
updatePackageJson({ slug, appDirPath });
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 });
let config = {
"/*": "Don't modify slug - If required, do it using cli edit command",
name: appName,
title: appTitle,
// @deprecated - It shouldn't exist.
type: slug,
slug: slug,
imageSrc: `/api/app-store/${slug}/icon.svg`,
logo: `/api/app-store/${slug}/icon.svg`,
@ -59,14 +75,16 @@ const BaseAppFork = {
variant: appType,
publisher: publisherName,
email: publisherEmail,
description: appDescription,
};
const baseConfig = JSON.parse(fs.readFileSync(`${appDirPath}/config.json`).toString());
const currentConfig = JSON.parse(fs.readFileSync(`${appDirPath}/config.json`).toString());
config = {
...baseConfig,
...currentConfig,
...config,
};
fs.writeFileSync(`${appDirPath}/config.json`, JSON.stringify(config, null, 2));
yield "Forked base app";
message = !editMode ? "Forked base app" : "Updated app";
yield message;
},
delete: function ({ slug }) {
const appDirPath = getAppDirPath(slug);
@ -105,24 +123,39 @@ const generateAppFiles = () => {
execSync(`cd ${appStoreDir} && node app-store.js`);
};
const CreateApp = ({ noDbUpdate }) => {
const CreateApp = ({ noDbUpdate, 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 Name", name: "appName" },
{ label: "App Title", name: "appTitle" },
{ label: "Type of App", name: "appType" },
{ label: "Publisher Name", name: "publisherName" },
{ label: "Publisher Email", name: "publisherEmail" },
{ label: "App Name", name: "appName", type: "text" },
{ label: "App Title", name: "appTitle", type: "text" },
{ label: "App Description", name: "appDescription", type: "text" },
{
label: "Type of App",
name: "appType",
type: "select",
options: [
{ label: "calendar", value: "calendar" },
{ label: "video", value: "video" },
{ label: "payment", value: "payment" },
{ label: "messaging", value: "messaging" },
{ label: "web3", value: "web3" },
{ label: "other", value: "other" },
],
},
{ label: "Publisher Name", name: "publisherName", type: "text" },
{ label: "Publisher Email", name: "publisherEmail", type: "text" },
];
const fieldLabel = fields[inputIndex]?.label || "";
const fieldName = fields[inputIndex]?.name || "";
const field = fields[inputIndex];
const fieldLabel = field?.label || "";
const fieldName = field?.name || "";
const fieldValue = appInputData[fieldName] || "";
const appName = appInputData["appName"];
const appType = appInputData["appType"];
const appTitle = appInputData["appTitle"];
const appDescription = appInputData["appDescription"];
const publisherName = appInputData["publisherName"];
const publisherEmail = appInputData["publisherEmail"];
const [result, setResult] = useState("...");
@ -132,7 +165,15 @@ const CreateApp = ({ noDbUpdate }) => {
useEffect(() => {
// When all fields have been filled
if (allFieldsFilled) {
const it = BaseAppFork.create({ appType, appName, slug, appTitle, publisherName, publisherEmail });
const it = BaseAppFork.create({
appType,
appDescription,
appName,
slug,
appTitle,
publisherName,
publisherEmail,
});
for (const item of it) {
setResult(item);
}
@ -142,7 +183,12 @@ const CreateApp = ({ noDbUpdate }) => {
generateAppFiles();
// FIXME: Even after CLI showing this message, it is stuck doing work before exiting
setResult("App almost generated. Wait for a few seconds.");
// So we ask the user to wait for some time
setResult(
`App has been given slug: ${slug}. Just wait for a few seconds for the process to complete and start editing ${getAppDirPath(
slug
)} to work on your app.`
);
}
});
@ -153,40 +199,100 @@ const CreateApp = ({ noDbUpdate }) => {
Creating app with name "{appName}" of type "{appType}"
</Text>
<Text>{result}</Text>
<Text>
Please note that you should use cli only to rename an app directory as it needs to be updated in DB
as well
</Text>
</>
);
}
// 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 (
<>
<Text>App with slug {slug} already exists. If you want to edit it, use edit command</Text>
</>
);
}
return (
<Box>
<Text color="green">{`${fieldLabel}:`}</Text>
<TextInput
value={fieldValue}
onSubmit={(value) => {
if (!value) {
return;
}
setInputIndex((index) => {
return index + 1;
});
}}
onChange={(value) => {
setAppInputData((appInputData) => {
return {
...appInputData,
[fieldName]: value,
};
});
}}
/>
{field.type == "text" ? (
<TextInput
value={fieldValue}
onSubmit={(value) => {
if (!value) {
return;
}
setInputIndex((index) => {
return index + 1;
});
}}
onChange={(value) => {
setAppInputData((appInputData) => {
return {
...appInputData,
[fieldName]: value,
};
});
}}
/>
) : (
<SelectInput<string>
items={field.options}
onSelect={(item) => {
setAppInputData((appInputData) => {
return {
...appInputData,
[fieldName]: item.value,
};
});
setInputIndex((index) => {
return index + 1;
});
}}></SelectInput>
)}
</Box>
);
};
const DeleteApp = ({ noDbUpdate, slug }) => {
BaseAppFork.delete({ slug });
Seed.revert({ slug });
generateAppFiles();
return <Text>Deleted App {slug}.</Text>;
const [confirmedAppSlug, setConfirmedAppSlug] = useState("");
const [allowDeletion, setAllowDeletion] = useState(false);
const [state, setState] = useState({});
useEffect(() => {
if (allowDeletion) {
BaseAppFork.delete({ slug });
Seed.revert({ slug });
generateAppFiles();
setState({ description: `App with slug ${slug} has been deleted`, done: true });
}
}, [allowDeletion, slug]);
return (
<>
<Text>
Confirm the slug of the app that you want to delete. Note, that it would cleanup the app directory,
App table and Credential table
</Text>
{!state.done && (
<TextInput
value={confirmedAppSlug}
onSubmit={(value) => {
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);
}}></TextInput>
)}
<Text>{state.description}</Text>
</>
);
};
const App: FC<{ noDbUpdate?: boolean; command: "create" | "delete"; slug?: string }> = ({
@ -200,6 +306,9 @@ const App: FC<{ noDbUpdate?: boolean; command: "create" | "delete"; slug?: strin
if (command === "delete") {
return <DeleteApp slug={slug} noDbUpdate={noDbUpdate} />;
}
if (command === "edit") {
return <CreateApp editMode={true} noDbUpdate={noDbUpdate} />;
}
};
module.exports = App;
export default App;

View File

@ -30,8 +30,10 @@ if (cli.input.length !== 1) {
cli.showHelp();
}
const command = cli.input[0] as "create" | "delete";
if (command !== "create" && command != "delete") {
const command = cli.input[0] as "create" | "delete" | "edit";
const supportedCommands = ["create", "delete", "edit"];
if (!supportedCommands.includes(command)) {
cli.showHelp();
}

View File

@ -0,0 +1 @@
Edit README.mdx file to update it.

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 122.88 73.7" style="enable-background:new 0 0 122.88 73.7" xml:space="preserve"><style type="text/css">.st0{fill-rule:evenodd;clip-rule:evenodd;}</style><g><path class="st0" d="M8.34,0h106.2c4.59,0,8.34,3.77,8.34,8.34v57.02c0,4.56-3.77,8.34-8.34,8.34H8.34C3.77,73.7,0,69.95,0,65.36 V8.34C0,3.75,3.75,0,8.34,0L8.34,0z M18.5,21.47h5.98c3.86,0,6.48,0.18,7.84,0.53c1.36,0.35,2.4,0.93,3.11,1.74 c0.71,0.81,1.16,1.72,1.33,2.71c0.18,0.99,0.26,2.95,0.26,5.86v10.78c0,2.76-0.13,4.6-0.4,5.53c-0.26,0.93-0.71,1.66-1.36,2.18 c-0.64,0.53-1.44,0.89-2.39,1.11c-0.95,0.21-2.38,0.31-4.3,0.31H18.5V21.47L18.5,21.47z M26.49,26.73v20.23 c1.16,0,1.87-0.23,2.13-0.69c0.27-0.46,0.4-1.71,0.4-3.78V30.55c0-1.39-0.04-2.29-0.12-2.68c-0.1-0.39-0.29-0.67-0.61-0.86 C27.96,26.82,27.37,26.73,26.49,26.73L26.49,26.73z M40.68,21.47h13.34v6.16h-5.34v5.83h5v5.86h-5v6.77h5.87v6.15H40.68V21.47 L40.68,21.47z M82.22,21.47v30.76h-6.99V31.47l-2.79,20.76h-4.96l-2.95-20.29v20.29h-6.99V21.47h10.36 c0.31,1.85,0.62,4.03,0.97,6.54l1.1,7.83l1.83-14.37H82.22L82.22,21.47z M104.38,39.48c0,3.09-0.07,5.28-0.21,6.56 c-0.15,1.29-0.6,2.46-1.36,3.53c-0.77,1.06-1.8,1.88-3.11,2.45c-1.31,0.57-2.83,0.86-4.56,0.86c-1.65,0-3.13-0.27-4.44-0.81 c-1.32-0.54-2.37-1.34-3.17-2.42c-0.8-1.08-1.28-2.25-1.43-3.51c-0.15-1.27-0.23-3.48-0.23-6.66v-5.26c0-3.09,0.07-5.28,0.23-6.58 c0.14-1.28,0.59-2.46,1.36-3.52c0.77-1.06,1.8-1.88,3.11-2.45c1.3-0.57,2.82-0.86,4.56-0.86c1.65,0,3.12,0.27,4.43,0.81 c1.31,0.54,2.37,1.35,3.17,2.42c0.79,1.08,1.27,2.25,1.42,3.52c0.15,1.26,0.23,3.48,0.23,6.66V39.48L104.38,39.48z M96.39,29.38 c0-1.44-0.08-2.35-0.24-2.75c-0.15-0.4-0.48-0.6-0.98-0.6c-0.42,0-0.74,0.16-0.96,0.49c-0.23,0.32-0.34,1.27-0.34,2.86v14.35 c0,1.8,0.07,2.9,0.22,3.31c0.15,0.42,0.49,0.62,1.02,0.62c0.55,0,0.89-0.23,1.05-0.72c0.15-0.48,0.23-1.63,0.23-3.45V29.38 L96.39,29.38z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,6 +1,5 @@
const fs = require("fs");
const path = require("path");
const appDirs = [];
let isInWatchMode = false;
if (process.argv[2] === "--watch") {
isInWatchMode = true;
@ -27,6 +26,7 @@ function getAppName(candidatePath) {
function generateFiles() {
let clientOutput = [`import dynamic from "next/dynamic"`];
let serverOutput = [];
const appDirs = [];
fs.readdirSync(`${__dirname}`).forEach(function (dir) {
if (fs.statSync(`${__dirname}/${dir}`).isDirectory()) {
@ -105,12 +105,12 @@ if (isInWatchMode) {
debouncedGenerateFiles();
}
})
// .on("change", (filePath) => {
// if (filePath.endsWith("config.json")) {
// console.log("Config file changed");
// debouncedGenerateFiles();
// }
// })
.on("change", (filePath) => {
if (filePath.endsWith("config.json")) {
console.log("Config file changed");
debouncedGenerateFiles();
}
})
.on("unlinkDir", (dirPath) => {
const appName = getAppName(dirPath);
if (appName) {

View File

@ -1,7 +1,7 @@
import { useSession } from "next-auth/react";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { deriveAppKeyFromType } from "@calcom/lib/deriveAppKeyFromSlug";
import { deriveAppKeyFromSlugOrType } from "@calcom/lib/deriveAppKeyFromSlugOrType";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { App } from "@calcom/types/App";
import Button from "@calcom/ui/Button";
@ -11,12 +11,12 @@ import { InstallAppButtonProps } from "./types";
export const InstallAppButton = (
props: {
type: App["type"];
slug: App["slug"];
} & InstallAppButtonProps
) => {
const { status } = useSession();
const { t } = useLocale();
const key = deriveAppKeyFromType(props.type, InstallAppButtonMap);
const key = deriveAppKeyFromSlugOrType(props.slug, InstallAppButtonMap);
const InstallAppButtonComponent = InstallAppButtonMap[key as keyof typeof InstallAppButtonMap];
if (!InstallAppButtonComponent) return null;
if (status === "unauthenticated")

View File

@ -0,0 +1 @@
Edit README.mdx file to update it.

View File

@ -1,12 +1,13 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Demo App",
"title": "It is a demo app",
"type": "demo_app",
"title": "Demo app",
"slug": "demo_app",
"imageSrc": "/api/app-store/demo_app/icon.svg",
"logo": "/api/app-store/demo_app/icon.svg",
"url": "https://cal.com/apps/demo_app",
"variant": "other",
"publisher": "hariom",
"email": "har@gmail"
}
"publisher": "d",
"email": "d",
"description": "it is a demo app"
}

View File

@ -4,11 +4,11 @@
"name": "@calcom/demo_app",
"version": "0.0.0",
"main": "./index.ts",
"description": "Your app description goes here.",
"description": "it is a demo app",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
}
}
}

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 122.88 73.7" style="enable-background:new 0 0 122.88 73.7" xml:space="preserve"><style type="text/css">.st0{fill-rule:evenodd;clip-rule:evenodd;}</style><g><path class="st0" d="M8.34,0h106.2c4.59,0,8.34,3.77,8.34,8.34v57.02c0,4.56-3.77,8.34-8.34,8.34H8.34C3.77,73.7,0,69.95,0,65.36 V8.34C0,3.75,3.75,0,8.34,0L8.34,0z M18.5,21.47h5.98c3.86,0,6.48,0.18,7.84,0.53c1.36,0.35,2.4,0.93,3.11,1.74 c0.71,0.81,1.16,1.72,1.33,2.71c0.18,0.99,0.26,2.95,0.26,5.86v10.78c0,2.76-0.13,4.6-0.4,5.53c-0.26,0.93-0.71,1.66-1.36,2.18 c-0.64,0.53-1.44,0.89-2.39,1.11c-0.95,0.21-2.38,0.31-4.3,0.31H18.5V21.47L18.5,21.47z M26.49,26.73v20.23 c1.16,0,1.87-0.23,2.13-0.69c0.27-0.46,0.4-1.71,0.4-3.78V30.55c0-1.39-0.04-2.29-0.12-2.68c-0.1-0.39-0.29-0.67-0.61-0.86 C27.96,26.82,27.37,26.73,26.49,26.73L26.49,26.73z M40.68,21.47h13.34v6.16h-5.34v5.83h5v5.86h-5v6.77h5.87v6.15H40.68V21.47 L40.68,21.47z M82.22,21.47v30.76h-6.99V31.47l-2.79,20.76h-4.96l-2.95-20.29v20.29h-6.99V21.47h10.36 c0.31,1.85,0.62,4.03,0.97,6.54l1.1,7.83l1.83-14.37H82.22L82.22,21.47z M104.38,39.48c0,3.09-0.07,5.28-0.21,6.56 c-0.15,1.29-0.6,2.46-1.36,3.53c-0.77,1.06-1.8,1.88-3.11,2.45c-1.31,0.57-2.83,0.86-4.56,0.86c-1.65,0-3.13-0.27-4.44-0.81 c-1.32-0.54-2.37-1.34-3.17-2.42c-0.8-1.08-1.28-2.25-1.43-3.51c-0.15-1.27-0.23-3.48-0.23-6.66v-5.26c0-3.09,0.07-5.28,0.23-6.58 c0.14-1.28,0.59-2.46,1.36-3.52c0.77-1.06,1.8-1.88,3.11-2.45c1.3-0.57,2.82-0.86,4.56-0.86c1.65,0,3.12,0.27,4.43,0.81 c1.31,0.54,2.37,1.35,3.17,2.42c0.79,1.08,1.27,2.25,1.42,3.52c0.15,1.26,0.23,3.48,0.23,6.66V39.48L104.38,39.48z M96.39,29.38 c0-1.44-0.08-2.35-0.24-2.75c-0.15-0.4-0.48-0.6-0.98-0.6c-0.42,0-0.74,0.16-0.96,0.49c-0.23,0.32-0.34,1.27-0.34,2.86v14.35 c0,1.8,0.07,2.9,0.22,3.31c0.15,0.42,0.49,0.62,1.02,0.62c0.55,0,0.89-0.23,1.05-0.72c0.15-0.48,0.23-1.63,0.23-3.45V29.38 L96.39,29.38z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,9 @@
# Base App - App Store CLI
It contains the boiler plate code for a new app. There is a one time copying of files right now.
You can read details of how exactly the CLI uses this base app [here](../../app-store-cli/README.md).
## TODO
- Rename it _baseApp to convey very clearly that it is not an actual app.

View File

@ -0,0 +1 @@
Edit README.mdx file to update it.

View File

@ -0,0 +1,20 @@
import type { App } from "@calcom/types/App";
import config from "./config.json";
import _package from "./package.json";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
export const metadata = {
description: _package.description,
category: "other",
// FIXME: Currently for an app to be shown as installed, it must have this variable set. Either hardcoded or if it depends on some env variable, that should be checked here
installed: true,
rating: 0,
reviews: 0,
trending: true,
verified: true,
...config,
} as App;
export default metadata;

View File

@ -0,0 +1,51 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import appConfig from "../config.json";
// TODO: There is a lot of code here that would be used by almost all apps
// - Login Validation
// - Looking up credential.
// - Creating credential would be specific to app, so there can be just createCredential method that app can expose
// - Redirection after successful installation can also be configured by app
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session?.user?.id) {
return res.status(401).json({ message: "You must be logged in to do this" });
}
// TODO: Define appType once and import everywhere
const slug = appConfig.slug;
try {
const alreadyInstalled = await prisma.credential.findFirst({
where: {
appId: slug,
userId: req.session.user.id,
},
});
if (alreadyInstalled) {
throw new Error("Already installed");
}
const installation = await prisma.credential.create({
data: {
// TODO: Why do we need type in Credential? Why can't we simply use appId
// Using slug as type for new credentials so that we keep on using type in requests.
// `deriveAppKeyFromSlug` should be able to handle old type and new type which is equal to slug
type: slug,
key: {},
userId: req.session.user.id,
appId: slug,
},
});
if (!installation) {
throw new Error(`Unable to create user credential for ${slug}`);
}
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
return res.status(500).json({ message: error.message });
}
return res.status(500);
}
return res.status(200).json({ url: "/apps/installed" });
}

View File

@ -0,0 +1 @@
export { default as add } from "./add";

View File

@ -0,0 +1,19 @@
import type { InstallAppButtonProps } from "@calcom/app-store/types";
import useAddAppMutation from "../../_utils/useAddAppMutation";
import appConfig from "../config.json";
export default function InstallAppButton(props: InstallAppButtonProps) {
const mutation = useAddAppMutation(appConfig.slug);
return (
<>
{props.render({
onClick() {
mutation.mutate("");
},
loading: mutation.isLoading,
})}
</>
);
}

View File

@ -0,0 +1,15 @@
export default function Icon() {
return (
<svg
width="40"
height="40"
viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid">
<path
d="M159.999 128.056a76.55 76.55 0 0 1-4.915 27.024 76.745 76.745 0 0 1-27.032 4.923h-.108c-9.508-.012-18.618-1.75-27.024-4.919A76.557 76.557 0 0 1 96 128.056v-.112a76.598 76.598 0 0 1 4.91-27.02A76.492 76.492 0 0 1 127.945 96h.108a76.475 76.475 0 0 1 27.032 4.923 76.51 76.51 0 0 1 4.915 27.02v.112zm94.223-21.389h-74.716l52.829-52.833a128.518 128.518 0 0 0-13.828-16.349v-.004a129 129 0 0 0-16.345-13.816l-52.833 52.833V1.782A128.606 128.606 0 0 0 128.064 0h-.132c-7.248.004-14.347.62-21.265 1.782v74.716L53.834 23.665A127.82 127.82 0 0 0 37.497 37.49l-.028.02A128.803 128.803 0 0 0 23.66 53.834l52.837 52.833H1.782S0 120.7 0 127.956v.088c0 7.256.615 14.367 1.782 21.289h74.716l-52.837 52.833a128.91 128.91 0 0 0 30.173 30.173l52.833-52.837v74.72a129.3 129.3 0 0 0 21.24 1.778h.181a129.15 129.15 0 0 0 21.24-1.778v-74.72l52.838 52.837a128.994 128.994 0 0 0 16.341-13.82l.012-.012a129.245 129.245 0 0 0 13.816-16.341l-52.837-52.833h74.724c1.163-6.91 1.77-14 1.778-21.24v-.186c-.008-7.24-.615-14.33-1.778-21.24z"
fill="#FF4A00"
/>
</svg>
);
}

View File

@ -0,0 +1,2 @@
export { default as InstallAppButton } from "./InstallAppButton";
export { default as Icon } from "./icon";

View File

@ -0,0 +1,13 @@
{
"name": "Demo App For Video",
"title": "Demo app forVideo",
"type": "demo_app_for_video",
"slug": "demo_app_for_video",
"imageSrc": "/api/app-store/demo_app_for_video/icon.svg",
"logo": "/api/app-store/demo_app_for_video/icon.svg",
"url": "https://cal.com/apps/demo_app_for_video",
"variant": "video",
"publisher": "hari",
"email": "hari@gmail.com",
"description": "Description Demo AppVideo"
}

View File

@ -0,0 +1,3 @@
export * as api from "./api";
export * as components from "./components";
export { metadata } from "./_metadata";

View File

@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/demo_app_for_video",
"version": "0.0.0",
"main": "./index.ts",
"description": "Description Demo AppVideo",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
}
}

View File

@ -5,6 +5,7 @@ import _package from "./package.json";
export const metadata = {
name: "Giphy",
description: _package.description,
installed: true,
category: "other",
// If using static next public folder, can then be referenced from the base URL (/).
imageSrc: "/api/app-store/giphy/icon.svg",

View File

@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
type: appType,
key: {},
userId: req.session.user.id,
appId: "giphy",
},
});
if (!installation) {

View File

@ -33,10 +33,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
let key = "";
if (code) {
const token = await oAuth2Client.getToken(code);
// TEMPORARY: REMOVE IT
try {
if (code) {
const token = await oAuth2Client.getToken(code);
key = token.res?.data;
key = token.res?.data;
}
} catch (e) {
console.log(e);
}
await prisma.credential.create({

View File

@ -4,6 +4,7 @@ import _package from "./package.json";
export const metadata = {
name: "HubSpot CRM",
installed: !!process.env.HUBSPOT_CLIENT_ID,
description: _package.description,
type: "hubspot_other_calendar",
imageSrc: "/api/app-store/hubspotothercalendar/icon.svg",

View File

@ -1,36 +1,36 @@
export function deriveAppKeyFromType(appType, map) {
export function deriveAppKeyFromSlugOrType(slugOrType, map) {
const oldTypes = ["video", "other", "calendar", "web3", "payment", "messaging"];
// slack has a bug where in config slack_app is the type(to match Credential.type) but directory is `slackmessaging`
// We can't derive slackmessaging from slack_app without hardcoding it.
if (appType === "slack_app") {
if (slugOrType === "slack_app") {
return "slackmessaging";
}
let handlers = map[appType];
let handlers = map[slugOrType];
if (handlers) {
return appType;
return slugOrType;
}
// There can be two types of legacy types
// - zoom_video
// - zoomvideo
// Transform `zoom_video` to `zoomvideo`;
appType = appType.split("_").join("");
handlers = map[appType];
slugOrType = slugOrType.split("_").join("");
handlers = map[slugOrType];
if (handlers) {
return appType;
return slugOrType;
}
// Instead of doing a blind split at _ and using the first part, apply this hack only on strings that match legacy type.
// Transform zoomvideo to zoom
oldTypes.some((type) => {
const matcher = new RegExp(`(.+)${type}$`);
if (appType.match(matcher)) {
appType = appType.replace(matcher, "$1");
if (slugOrType.match(matcher)) {
slugOrType = slugOrType.replace(matcher, "$1");
return true;
}
});
return appType;
return slugOrType;
}

View File

@ -86,6 +86,7 @@ model Credential {
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int?
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
// How to make it a required column?
appId String?
}

View File

@ -1,7 +1,16 @@
[
{
"dirName": "demo_app",
"categories": ["other"],
"categories": [
"other"
],
"slug": "demo_app"
},
{
"dirName": "demo_app_for_video",
"categories": [
"video"
],
"slug": "demo_app_for_video"
}
]
]

View File

@ -22,7 +22,7 @@ async function createApp(
update: { dirName, categories, keys },
});
await prisma.credential.updateMany({
where: { type },
where: { appId: slug },
data: { appId: slug },
});
console.log(`📲 Upserted app: '${slug}'`);

View File

@ -13,15 +13,6 @@ export interface App {
* variables to determine if this is true or not.
* */
installed?: boolean;
/** The app type */
type:
| `${string}_calendar`
| `${string}_messaging`
| `${string}_payment`
| `${string}_video`
| `${string}_web3`
| `${string}_other`
| `${string}_other_calendar`;
/** The display name for the app, TODO settle between this or name */
title: string;
/** The display name for the app */