Fix app categories (#6016)

* Fix app metas

* Optimize categories on app store

* Remove commented code

* Remove console log

* Update apps/web/pages/apps/index.tsx

Co-authored-by: Omar López <zomars@me.com>

* Add categories to missing app metadata

* Fix type error

* Type fix

* Type fixes

* More type fixes

* Clean up

* Fix build error

* No leaky please

* Remove comment

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
This commit is contained in:
Joe Au-Yeung 2022-12-20 17:15:06 -05:00 committed by GitHub
parent 2d9064e92f
commit 540bf3a1cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 99 additions and 63 deletions

View File

@ -47,13 +47,7 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
}
export const getStaticPaths = async () => {
const appStore = await getAppRegistry();
const paths = appStore.reduce((categories, app) => {
if (!categories.includes(app.category)) {
categories.push(app.category);
}
return categories;
}, [] as string[]);
const paths = Object.keys(AppCategories);
return {
paths: paths.map((category) => ({ params: { category } })),

View File

@ -44,7 +44,9 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
export const getStaticProps = async () => {
const appStore = await getAppRegistry();
const categories = appStore.reduce((c, app) => {
c[app.category] = c[app.category] ? c[app.category] + 1 : 1;
for (const category of app.categories) {
c[category] = c[category] ? c[category] + 1 : 1;
}
return c;
}, {} as Record<string, number>);

View File

@ -1,4 +1,4 @@
import { GetServerSideProps, InferGetServerSidePropsType } from "next";
import { GetServerSidePropsContext } from "next";
import { ChangeEventHandler, useState } from "react";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
@ -6,6 +6,7 @@ import { classNames } from "@calcom/lib";
import { getSession } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/client";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { AllApps, AppsLayout, AppStoreCategories, Icon, TextField, TrendingAppsSlider } from "@calcom/ui";
import { ssgInit } from "@server/lib/ssg";
@ -30,10 +31,7 @@ function AppsSearch({
);
}
export default function Apps({
categories,
appStore,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
export default function Apps({ categories, appStore }: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const [searchText, setSearchText] = useState<string | undefined>(undefined);
@ -52,12 +50,16 @@ export default function Apps({
<TrendingAppsSlider items={appStore} />
</>
)}
<AllApps apps={appStore} searchText={searchText} />
<AllApps
apps={appStore}
searchText={searchText}
categories={categories.map((category) => category.name)}
/>
</AppsLayout>
);
}
export const getServerSideProps: GetServerSideProps = async (context) => {
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssg = await ssgInit(context);
const session = await getSession(context);

View File

@ -1,7 +1,6 @@
import type { Credential } from "@prisma/client";
import prisma from "@calcom/prisma";
import { App } from "@calcom/types/App";
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;
@ -30,7 +29,7 @@ export async function getAppRegistry() {
where: { enabled: true },
select: { dirName: true, slug: true, categories: true, enabled: true },
});
const apps = [] as Omit<App, "key">[];
const apps = [] as App[];
for await (const dbapp of dbApps) {
const app = await getAppWithMetadata(dbapp);
if (!app) continue;
@ -56,9 +55,15 @@ export async function getAppRegistry() {
export async function getAppRegistryWithCredentials(userId: number) {
const dbApps = await prisma.app.findMany({
where: { enabled: true },
include: { credentials: { where: { userId } } },
select: {
...safeAppSelect,
credentials: {
where: { userId },
select: safeCredentialSelect,
},
},
});
const apps = [] as (Omit<App, "key"> & {
const apps = [] as (App & {
credentials: Credential[];
})[];
for await (const dbapp of dbApps) {
@ -77,8 +82,7 @@ export async function getAppRegistryWithCredentials(userId: number) {
...remainingAppProps,
categories: dbapp.categories,
credentials: dbapp.credentials,
installed:
true /* All apps from DB are considered installed by default. @TODO: Add and filter our by `enabled` property */,
installed: true,
});
}
return apps;

View File

@ -10,6 +10,7 @@ export const metadata = {
title: "Apple Calendar",
imageSrc: "/api/app-store/applecalendar/icon.svg",
variant: "calendar",
categories: ["calendar"],
category: "calendar",
logo: "/api/app-store/applecalendar/icon.svg",
publisher: "Cal.com",

View File

@ -3,7 +3,6 @@ import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
dirName: "around",
appData: {
location: {

View File

@ -11,6 +11,7 @@ export const metadata = {
imageSrc: "/api/app-store/caldavcalendar/icon.svg",
variant: "calendar",
category: "calendar",
categories: ["calendar"],
logo: "/api/app-store/caldavcalendar/icon.svg",
publisher: "Cal.com",
rating: 5,

View File

@ -11,6 +11,7 @@ export const metadata = {
imageSrc: "/api/app-store/caldavcalendar/icon.svg",
variant: "calendar",
category: "calendar",
categories: ["calendar"],
logo: "/api/app-store/caldavcalendar/icon.svg",
publisher: "Cal.com",
rating: 5,

View File

@ -3,7 +3,6 @@ import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
dirName: "caldavcalendar",
...config,
} as AppMeta;

View File

@ -10,6 +10,7 @@ export const metadata = {
imageSrc: "/api/app-store/dailyvideo/icon.svg",
variant: "conferencing",
url: "https://daily.co",
categories: ["calendar"],
trending: true,
logo: "/api/app-store/dailyvideo/icon.svg",
publisher: "Cal.com",

View File

@ -11,6 +11,7 @@ export const metadata = {
imageSrc: "/api/app-store/exchange2013calendar/icon.svg",
variant: "calendar",
category: "calendar",
categories: ["calendar"],
label: "Exchange Calendar",
logo: "/api/app-store/exchange2013calendar/icon.svg",
publisher: "Cal.com",

View File

@ -11,6 +11,7 @@ export const metadata = {
imageSrc: "/api/app-store/exchange2016calendar/icon.svg",
variant: "calendar",
category: "calendar",
categories: ["calendar"],
label: "Exchange Calendar",
logo: "/api/app-store/exchange2016calendar/icon.svg",
publisher: "Cal.com",

View File

@ -3,7 +3,6 @@ import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
dirName: "ga4",
...config,
} as AppMeta;

View File

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

View File

@ -12,6 +12,7 @@ export const metadata = {
imageSrc: "/api/app-store/googlecalendar/icon.svg",
variant: "calendar",
category: "calendar",
categories: ["calendar"],
logo: "/api/app-store/googlecalendar/icon.svg",
publisher: "Cal.com",
rating: 5,

View File

@ -9,6 +9,7 @@ export const metadata = {
installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)),
slug: "google-meet",
category: "video",
categories: ["video"],
type: "google_video",
title: "Google Meet",
imageSrc: "/api/app-store/googlevideo/logo.webp",

View File

@ -15,7 +15,7 @@ export const metadata = {
verified: true,
rating: 4.3, // TODO: placeholder for now, pull this from TrustPilot or G2
reviews: 69, // TODO: placeholder for now, pull this from TrustPilot or G2
category: "other",
categories: ["other"],
label: "HubSpot CRM",
slug: "hubspot",
title: "HubSpot CRM",

View File

@ -10,6 +10,7 @@ export const metadata = {
type: "huddle01_video",
imageSrc: "/api/app-store/huddle01video/icon.svg",
variant: "conferencing",
categories: ["video", "web3"],
logo: "/api/app-store/huddle01video/icon.svg",
publisher: "huddle01.com",
url: "https://huddle01.com",

View File

@ -9,13 +9,13 @@ export const metadata = {
type: "jitsi_video",
imageSrc: "/api/app-store/jitsivideo/icon.svg",
variant: "conferencing",
categories: ["video"],
logo: "/api/app-store/jitsivideo/icon.svg",
publisher: "Cal.com",
url: "https://jitsi.org/",
verified: true,
rating: 0, // TODO: placeholder for now, pull this from TrustPilot or G2
reviews: 0, // TODO: placeholder for now, pull this from TrustPilot or G2
category: "video",
slug: "jitsi",
title: "Jitsi Meet",
trending: true,

View File

@ -10,7 +10,7 @@ export const metadata = {
title: "Lark Calendar",
imageSrc: "/api/app-store/larkcalendar/icon.svg",
variant: "calendar",
category: "calendar",
categories: ["calendar"],
logo: "/api/app-store/larkcalendar/icon.svg",
publisher: "Lark",
rating: 5,

View File

@ -10,6 +10,7 @@ export const metadata = {
imageSrc: "/api/app-store/office365calendar/icon.svg",
variant: "calendar",
category: "calendar",
categories: ["calendar"],
logo: "/api/app-store/office365calendar/icon.svg",
publisher: "Cal.com",
rating: 5,

View File

@ -11,6 +11,7 @@
"rating": 4.3,
"reviews": 69,
"category": "video",
"categories": ["video"],
"slug": "msteams",
"title": "MS Teams (Requires work/school account)",
"trending": true,

View File

@ -3,7 +3,6 @@ import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
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,

View File

@ -3,7 +3,6 @@ import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
dirName: "pipedream",
...config,
} as AppMeta;

View File

@ -3,7 +3,6 @@ import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
dirName: "rainbow",
...config,
} as AppMeta;

View File

@ -3,7 +3,6 @@ import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
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,

View File

@ -3,7 +3,6 @@ import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
dirName: "salesforce",
...config,
} as AppMeta;

View File

@ -3,7 +3,6 @@ import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
dirName: "sendgrid",
...config,
} as AppMeta;

View File

@ -3,7 +3,6 @@ import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
dirName: "signal",
...config,
} as AppMeta;

View File

@ -3,7 +3,6 @@ import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
dirName: "sirius_video",
...config,
} as AppMeta;

View File

@ -12,6 +12,7 @@ export const metadata = {
),
slug: "stripe",
category: "payment",
categories: ["payment"],
logo: "/api/app-store/stripepayment/icon.svg",
rating: 4.6,
trending: true,

View File

@ -9,6 +9,7 @@ export const metadata = {
title: "Tandem Video",
imageSrc: "/api/app-store/tandemvideo/icon.svg",
variant: "conferencing",
categories: ["video"],
slug: "tandem",
category: "video",
logo: "/api/app-store/tandemvideo/icon.svg",

View File

@ -3,7 +3,6 @@ import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
dirName: "telegram",
...config,
} as AppMeta;

View File

@ -76,7 +76,10 @@ export function getLocationGroupedOptions(integrations: ReturnType<typeof getApp
const apps: Record<string, { label: string; value: string; disabled?: boolean; icon?: string }[]> = {};
integrations.forEach((app) => {
if (app.locationOption) {
const category = app.category;
// All apps that are labeled as a locationOption are video apps. Extract the secondary category if available
let category =
app.categories.length >= 2 ? app.categories.find((category) => category !== "video") : app.category;
if (!category) category = "video";
const option = { ...app.locationOption, icon: app.imageSrc };
if (apps[category]) {
apps[category] = [...apps[category], option];

View File

@ -7,6 +7,7 @@ export const metadata = {
description: _package.description,
installed: true,
category: "other",
categories: ["other"],
// If using static next public folder, can then be referenced from the base URL (/).
imageSrc: "/api/app-store/vital/icon.svg",
logo: "/api/app-store/vital/icon.svg",

View File

@ -3,7 +3,6 @@ import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
dirName: "whatsapp",
...config,
} as AppMeta;

View File

@ -3,7 +3,6 @@ import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
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,

View File

@ -7,6 +7,7 @@ export const metadata = {
description: _package.description,
installed: true,
category: "other",
categories: ["other"],
// If using static next public folder, can then be referenced from the base URL (/).
imageSrc: "/api/app-store/wipemycalother/icon-dark.svg",
logo: "/api/app-store/wipemycalother/icon-dark.svg",

View File

@ -7,6 +7,7 @@ export const metadata = {
description: _package.description,
installed: true,
category: "automation",
categories: ["automation"],
imageSrc: "/api/app-store/zapier/icon.svg",
logo: "/api/app-store/zapier/icon.svg",
publisher: "Cal.com",

View File

@ -7,6 +7,7 @@ export const metadata = {
name: "Zoom Video",
description: _package.description,
type: "zoom_video",
categories: ["video"],
imageSrc: "/api/app-store/zoomvideo/icon.svg",
variant: "conferencing",
logo: "/api/app-store/zoomvideo/icon.svg",

View File

@ -0,0 +1,12 @@
import { Prisma } from "@prisma/client";
export const safeAppSelect = Prisma.validator<Prisma.AppSelect>()({
slug: true,
dirName: true,
/** Omitting to avoid frontend leaks */
// keys: true,
categories: true,
createdAt: true,
updatedAt: true,
enabled: true,
});

View File

@ -0,0 +1,11 @@
import { Prisma } from "@prisma/client";
export const safeCredentialSelect = Prisma.validator<Prisma.CredentialSelect>()({
id: true,
type: true,
/** Omitting to avoid frontend leaks */
// key: true,
userId: true,
appId: true,
invalid: true,
});

View File

@ -1,3 +1,5 @@
export { safeAppSelect } from "./app";
export * from "./booking";
export { safeCredentialSelect } from "./credential";
export * from "./event-types";
export * from "./user";

View File

@ -89,10 +89,10 @@ export interface App {
/*
* @deprecated Use categories
*/
category: string;
category?: string;
/** The category to which this app belongs, currently we have `calendar`, `payment` or `video` */
categories?: string[];
categories: string[];
/**
* `User` is the broadest category. `EventType` is when you want to add features to EventTypes.
* See https://app.gitbook.com/o/6snd8PyPYMhg0wUw6CeQ/s/VXRprBTuMlihk37NQgUU/~/changes/6xkqZ4qvJ3Xh9k8UaWaZ/engineering/product-specs/app-store#user-apps for more details
@ -121,9 +121,6 @@ export interface App {
isGlobal?: boolean;
/** A contact email, mainly to ask for support */
email: string;
/** Needed API Keys (usually for global apps) */
key?: Prisma.JsonValue;
/** Needed API Keys (usually for global apps) */
key?: Prisma.JsonValue;
/** If not free, what kind of fees does the app have */
@ -138,4 +135,9 @@ export interface App {
dirName?: string;
}
export type AppFrontendPayload = Omit<App, "key"> & {
/** We should type error if keys are leaked to the frontend */
key?: never;
};
export type AppMeta = Optional<App, "rating" | "trending" | "reviews" | "verified">;

View File

@ -16,4 +16,9 @@ export type CredentialPayload = Prisma.CredentialGetPayload<{
};
}>;
export type CredentialFrontendPayload = Omit<CredentialPayload, "key"> & {
/** We should type error if keys are leaked to the frontend */
key?: never;
};
export type CredentialWithAppName = CredentialPayload & { appName: string };

View File

@ -1,11 +1,11 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { Credential } from "@prisma/client";
import { useRouter } from "next/router";
import { UIEvent, useEffect, useRef, useState } from "react";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { App } from "@calcom/types/App";
import type { AppFrontendPayload as App } from "@calcom/types/App";
import type { CredentialFrontendPayload as Credential } from "@calcom/types/Credential";
import { Icon } from "@calcom/ui";
import EmptyScreen from "../EmptyScreen";
@ -37,7 +37,11 @@ export function useShouldShowArrows() {
return { ref, calculateScroll, leftVisible: showArrowScroll.left, rightVisible: showArrowScroll.right };
}
type AllAppsPropsType = { apps: (App & { credentials: Credential[] | undefined })[]; searchText?: string };
type AllAppsPropsType = {
apps: (App & { credentials?: Credential[] })[];
searchText?: string;
categories: string[];
};
interface CategoryTabProps {
selectedCategory: string | null;
@ -125,18 +129,12 @@ function CategoryTab({ selectedCategory, categories, searchText }: CategoryTabPr
);
}
export default function AllApps({ apps, searchText }: AllAppsPropsType) {
export default function AllApps({ apps, searchText, categories }: AllAppsPropsType) {
const router = useRouter();
const { t } = useLocale();
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [appsContainerRef, enableAnimation] = useAutoAnimate<HTMLDivElement>();
const categories = apps
.map((app) => app.category)
.filter((cat, pos, self) => {
return self.indexOf(cat) === pos;
});
if (searchText) {
enableAnimation && enableAnimation(false);
}

View File

@ -1,11 +1,11 @@
import type { Credential } from "@prisma/client";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { App } from "@calcom/types/App";
import { AppFrontendPayload as App } from "@calcom/types/App";
import type { CredentialFrontendPayload as Credential } from "@calcom/types/Credential";
import { Button, Icon } from "../../..";
import { showToast } from "../notifications";
@ -123,8 +123,6 @@ export default function AppCard({ app, credentials, searchText }: AppCardProps)
},
};
}
props.color;
// ^?
return (
<Button
StartIcon={Icon.FiPlus}

View File

@ -1,5 +1,5 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { App } from "@calcom/types/App";
import { AppFrontendPayload as App } from "@calcom/types/App";
import AppCard from "./AppCard";
import Slider from "./Slider";