Use slug everywhere instead of app type

This commit is contained in:
Hariom Balhara 2022-06-01 15:15:07 +05:30
parent c563415795
commit d563343669
17 changed files with 112 additions and 45 deletions

View File

@ -21,6 +21,7 @@ import Badge from "@components/ui/Badge";
export default function App({ export default function App({
name, name,
type, type,
slug,
logo, logo,
body, body,
categories, categories,
@ -36,6 +37,7 @@ export default function App({
privacy, privacy,
}: { }: {
name: string; name: string;
slug: string;
type: AppType["type"]; type: AppType["type"];
isGlobal?: AppType["isGlobal"]; isGlobal?: AppType["isGlobal"];
logo: string; logo: string;
@ -60,6 +62,7 @@ export default function App({
useGrouping: false, useGrouping: false,
}).format(price); }).format(price);
const [installedApp, setInstalledApp] = useState(false); const [installedApp, setInstalledApp] = useState(false);
useEffect(() => { useEffect(() => {
async function getInstalledApp(appCredentialType: string) { async function getInstalledApp(appCredentialType: string) {
const queryParam = new URLSearchParams(); const queryParam = new URLSearchParams();
@ -80,8 +83,9 @@ export default function App({
} }
} }
} }
getInstalledApp(type); getInstalledApp(slug);
}, [type]); }, [slug]);
return ( return (
<> <>
<Shell large isPublic> <Shell large isPublic>

View File

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

View File

@ -3,6 +3,27 @@ import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error"; import { HttpError } from "@lib/core/http/error";
function getSlugFromLegacy(legacySlug) {
const oldTypes = ["video", "other", "calendar", "web3", "payment", "messaging"];
// There can be two types of legacy slug
// - zoom_video
// - zoomvideo
// Transform `zoom_video` to `zoomvideo`;
let slug = legacySlug.split("_").join("");
// Transform zoomvideo to zoom
oldTypes.some((type) => {
const matcher = new RegExp(`(.+)${type}$`);
if (legacySlug.match(matcher)) {
slug = legacySlug.replace(matcher, "$1");
return true;
}
});
return slug;
}
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Check that user is authenticated // Check that user is authenticated
req.session = await getSession({ req }); req.session = await getSession({ req });
@ -14,12 +35,13 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
} }
const [_appName, apiEndpoint] = args; const [_appName, apiEndpoint] = args;
const appName = _appName.split("_").join(""); // Transform `zoom_video` to `zoomvideo`; const appName = getSlugFromLegacy(_appName);
try { try {
/* Absolute path didn't work */ /* Absolute path didn't work */
const handlerMap = (await import("@calcom/app-store/apps.generated")).apiHandlers; const handlerMap = (await import("@calcom/app-store/apps.generated")).apiHandlers;
const handlers = await handlerMap[appName as keyof typeof handlerMap]; const handlerKey = appName as keyof typeof handlerMap;
console.log(handlerKey);
const handlers = await handlerMap[handlerKey];
const handler = handlers[apiEndpoint as keyof typeof handlers] as NextApiHandler; const handler = handlers[apiEndpoint as keyof typeof handlers] as NextApiHandler;
if (typeof handler !== "function") if (typeof handler !== "function")

View File

@ -49,6 +49,7 @@ const components = {
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) { function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
return ( return (
<App <App
slug={data.slug}
name={data.name} name={data.name}
isGlobal={data.isGlobal} isGlobal={data.isGlobal}
type={data.type} type={data.type}

View File

@ -622,7 +622,7 @@ const loggedInViewerRouter = createProtectedRouter()
const apps = getApps(credentials).map( const apps = getApps(credentials).map(
({ credentials: _, credential: _1 /* don't leak to frontend */, ...app }) => ({ ({ credentials: _, credential: _1 /* don't leak to frontend */, ...app }) => ({
...app, ...app,
credentialIds: credentials.filter((c) => c.type === app.type).map((c) => c.id), credentialIds: credentials.filter((c) => c.appId === app.slug).map((c) => c.id),
}) })
); );
// `flatMap()` these work like `.filter()` but infers the types correctly // `flatMap()` these work like `.filter()` but infers the types correctly

View File

@ -14,16 +14,29 @@ Change name and description
## TODO ## TODO
- Put lowercase and - restriction only on App name - Beta Release
- Add space restriction as well for Appname. Maybe look for valid dirname or slug regex - Handle legacy apps which have dirname as something else and type as something else. type is used to do lookups with key
- Merge app-store:watch and app-store commands, introduce app-store --watch - Add comment in config.json that this file shouldn't be modified manually.
- Get strong confirmation for deletion of app. Get the name of the app from user that he wants to delete - Install button not coming
- App Description Missing - Put lowercase and - restriction only on slug. Keep App Name and others unchanged. Also, use slug instead of appName for dirNames
- Select Box for App Type - Add space restriction as well for Appname. Maybe look for valid dirname or slug regex
- Credentials table doesn't get new entries with cli. Figure out how to do it. - Get strong confirmation for deletion of app. Get the name of the app from user that he wants to delete
- App already exists check. Ask user to run edit/regenerate command - App Description Missing
- Allow deletion of App, cleaning up everything - Select Box for App Type
- folder - App types Validations
- prisma credentials table - Credentials table doesn't get new entries with cli. Figure out how to do it.
- seed.config.json - Using app/config.json -> Allow Editing App Details.
- 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.
- Improvements
- 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
## Roadmap
- 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

@ -113,7 +113,7 @@ const CreateApp = ({ noDbUpdate }) => {
const fieldName = fields[inputIndex]?.name || ""; const fieldName = fields[inputIndex]?.name || "";
const fieldValue = appInputData[fieldName] || ""; const fieldValue = appInputData[fieldName] || "";
const appName = appInputData["appName"]; const appName = appInputData["appName"];
const appType = appInputData["appType"]; const appType = `${appName}_${appInputData["appType"]}`;
const appTitle = appInputData["appTitle"]; const appTitle = appInputData["appTitle"];
const publisherName = appInputData["publisherName"]; const publisherName = appInputData["publisherName"];
const publisherEmail = appInputData["publisherEmail"]; const publisherEmail = appInputData["publisherEmail"];

View File

@ -8,11 +8,12 @@ import _package from "./package.json";
export const metadata = { export const metadata = {
description: _package.description, description: _package.description,
category: "other", 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, rating: 0,
reviews: 0, reviews: 0,
trending: true, trending: true,
verified: true, verified: true,
email: "CLI_BASE__PUBLISHER_EMAIL",
...config, ...config,
} as App; } as App;

View File

@ -2,16 +2,23 @@ import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma"; 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) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session?.user?.id) { if (!req.session?.user?.id) {
return res.status(401).json({ message: "You must be logged in to do this" }); return res.status(401).json({ message: "You must be logged in to do this" });
} }
// TODO: Define appType once and import everywhere // TODO: Define appType once and import everywhere
const appType = "CLI_BASE__APP_NAME_CLI_BASE__APP_TYPE"; const slug = appConfig.slug;
try { try {
const alreadyInstalled = await prisma.credential.findFirst({ const alreadyInstalled = await prisma.credential.findFirst({
where: { where: {
type: appType, appId: slug,
userId: req.session.user.id, userId: req.session.user.id,
}, },
}); });
@ -20,21 +27,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
const installation = await prisma.credential.create({ const installation = await prisma.credential.create({
data: { data: {
type: appType, // TODO: Why do we need type in Credential? Why can't we simply use appId
type: slug,
key: {}, key: {},
userId: req.session.user.id, userId: req.session.user.id,
appId: "CLI_BASE__APP_NAME", appId: slug,
}, },
}); });
if (!installation) { if (!installation) {
throw new Error("Unable to create user credential for CLI_BASE__APP_NAME"); throw new Error(`Unable to create user credential for ${slug}`);
} }
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
console.error(error.message);
return res.status(500).json({ message: error.message }); return res.status(500).json({ message: error.message });
} }
return res.status(500); return res.status(500);
} }
return res.status(200).json({ url: "/apps/zapier/setup" }); return res.status(200).json({ url: "/apps/installed" });
} }

View File

@ -1,11 +1,10 @@
import type { InstallAppButtonProps } from "@calcom/app-store/types"; import type { InstallAppButtonProps } from "@calcom/app-store/types";
import useAddAppMutation from "../../_utils/useAddAppMutation"; import useAddAppMutation from "../../_utils/useAddAppMutation";
import appConfig from "../config.json";
export default function InstallAppButton(props: InstallAppButtonProps) { export default function InstallAppButton(props: InstallAppButtonProps) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment const mutation = useAddAppMutation(appConfig.slug);
//@ts-ignore
const mutation = useAddAppMutation("CLI_BASE__APP_NAME_CLI_BASE__APP_TYPE");
return ( return (
<> <>

View File

@ -98,6 +98,12 @@ if (isInWatchMode) {
debouncedGenerateFiles(); debouncedGenerateFiles();
} }
}) })
.on("change", (filePath) => {
if (filePath.endsWith("config.json")) {
console.log("Config file changed");
debouncedGenerateFiles();
}
})
.on("unlinkDir", (dirPath) => { .on("unlinkDir", (dirPath) => {
const appName = getAppName(dirPath); const appName = getAppName(dirPath);
if (appName) { if (appName) {

View File

@ -8,11 +8,11 @@ import _package from "./package.json";
export const metadata = { export const metadata = {
description: _package.description, description: _package.description,
category: "other", category: "other",
installed: true,
rating: 0, rating: 0,
reviews: 0, reviews: 0,
trending: true, trending: true,
verified: true, verified: true,
email: "CLI_BASE__PUBLISHER_EMAIL",
...config, ...config,
} as App; } as App;

View File

@ -2,16 +2,18 @@ import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import appConfig from "../config.json";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session?.user?.id) { if (!req.session?.user?.id) {
return res.status(401).json({ message: "You must be logged in to do this" }); return res.status(401).json({ message: "You must be logged in to do this" });
} }
// TODO: Define appType once and import everywhere // TODO: Define appType once and import everywhere
const appType = "CLI_BASE__APP_NAME_CLI_BASE__APP_TYPE"; const slug = appConfig.slug;
try { try {
const alreadyInstalled = await prisma.credential.findFirst({ const alreadyInstalled = await prisma.credential.findFirst({
where: { where: {
type: appType, appId: slug,
userId: req.session.user.id, userId: req.session.user.id,
}, },
}); });
@ -20,17 +22,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
const installation = await prisma.credential.create({ const installation = await prisma.credential.create({
data: { data: {
type: appType, // TODO: Why do we need type in Credential? Why can't we simply use appId
type: slug,
key: {}, key: {},
userId: req.session.user.id, userId: req.session.user.id,
appId: "CLI_BASE__APP_NAME", appId: slug,
}, },
}); });
if (!installation) { if (!installation) {
throw new Error("Unable to create user credential for CLI_BASE__APP_NAME"); throw new Error(`Unable to create user credential for ${slug}`);
} }
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
console.error(error.message);
return res.status(500).json({ message: error.message }); return res.status(500).json({ message: error.message });
} }
return res.status(500); return res.status(500);

View File

@ -1,11 +1,12 @@
import type { InstallAppButtonProps } from "@calcom/app-store/types"; import type { InstallAppButtonProps } from "@calcom/app-store/types";
import useAddAppMutation from "../../_utils/useAddAppMutation"; import useAddAppMutation from "../../_utils/useAddAppMutation";
import appConfig from "../config.json";
export default function InstallAppButton(props: InstallAppButtonProps) { export default function InstallAppButton(props: InstallAppButtonProps) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
const mutation = useAddAppMutation("CLI_BASE__APP_NAME_CLI_BASE__APP_TYPE"); const mutation = useAddAppMutation(appConfig.slug);
return ( return (
<> <>

View File

@ -1,7 +1,7 @@
{ {
"name": "demo", "name": "demo",
"title": "it's a demo app", "title": "it's a demo app",
"type": "other", "type": "demo_other",
"slug": "demo", "slug": "demo",
"imageSrc": "/api/app-store/demo/icon.svg", "imageSrc": "/api/app-store/demo/icon.svg",
"logo": "/api/app-store/demo/icon.svg", "logo": "/api/app-store/demo/icon.svg",

View File

@ -2,9 +2,9 @@
width="640" width="640"
height="360" height="360"
src="https://www.loom.com/embed/f8d2cd9b2ac74f0c916f20c4441bd1da" src="https://www.loom.com/embed/f8d2cd9b2ac74f0c916f20c4441bd1da"
frameborder="0" frameBorder="0"
webkitallowfullscreen webkitallowfullscreen="true"
mozallowfullscreen mozallowfullscreen="true"
allowfullscreen></iframe> allowFullScreen></iframe>
Looking to honor May 4th? Search no further. Download this app to make your booking success page resemble a long time ago in a galaxy far far away. Looking to honor May 4th? Search no further. Download this app to make your booking success page resemble a long time ago in a galaxy far far away.

View File

@ -4,5 +4,11 @@
"dirName": "demo", "dirName": "demo",
"categories": ["other"], "categories": ["other"],
"type": "demo_other" "type": "demo_other"
},
{
"name": "demovideo",
"dirName": "demovideo",
"categories": ["video"],
"type": "demovideo_video"
} }
] ]