Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import type { Icon } from "@calcom/ui/components/icon";
import { ShellSubHeading } from "@calcom/ui/components/layout";
import { showToast } from "@calcom/ui/components/toast";
import AppListCardWebWrapper from "@calcom/web/modules/apps/components/AppListCardWebWrapper";
Expand All @@ -21,13 +20,18 @@ import { CalendarListContainer } from "@components/apps/CalendarListContainer";
import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout";
import { QueryCell } from "@lib/QueryCell";
import { useReducer } from "react";
import { APP_CATEGORY_ENTRIES, ActiveAppCategoryKeys } from "@calcom/app-store/_utils/getAppCategories";

interface IntegrationsContainerProps {
variant?: AppCategories;
exclude?: AppCategories[];
handleDisconnect: HandleDisconnect;
}

const LEGACY_CATEGORY_MAP: Partial<Record<AppCategories, ActiveAppCategoryKeys>> = {
video: "conferencing",
};

const IntegrationsContainer = ({
variant,
exclude,
Expand All @@ -48,6 +52,14 @@ const IntegrationsContainer = ({

const updateLocationsMutation = trpc.viewer.eventTypes.bulkUpdateToDefaultLocation.useMutation();

const isActiveCategory = (v: AppCategories): v is ActiveAppCategoryKeys => v in APP_CATEGORY_ENTRIES;

const resolveEmptyStateVariant = (v?: AppCategories): ActiveAppCategoryKeys => {
if (!v) return "other";
if (isActiveCategory(v)) return v;
return LEGACY_CATEGORY_MAP[v] || "other";
};

const { data: eventTypesQueryData, isFetching: isEventTypesFetching } =
trpc.viewer.eventTypes.bulkEventFetch.useQuery();

Expand Down Expand Up @@ -95,41 +107,28 @@ const IntegrationsContainer = ({
utils.viewer.apps.getUsersDefaultConferencingApp.invalidate();
};

// TODO: Refactor and reuse getAppCategories?
const emptyIcon: Record<AppCategories, React.ComponentProps<typeof Icon>["name"]> = {
calendar: "calendar",
conferencing: "video",
automation: "share-2",
analytics: "chart-bar",
payment: "credit-card",
other: "grid-3x3",
web3: "credit-card", // deprecated
video: "video", // deprecated
messaging: "mail",
crm: "contact",
};

return (
<QueryCell
query={query}
customLoader={<SkeletonLoader />}
success={({ data }) => {
if (!data.items.length) {
const emptyHeaderCategory = getAppCategoryTitle(variant || "other", true);
const resolvedVariant = resolveEmptyStateVariant(variant);
const emptyHeaderCategory = getAppCategoryTitle(resolvedVariant, true);

return (
<EmptyScreen
Icon={emptyIcon[variant || "other"]}
Icon={APP_CATEGORY_ENTRIES[resolvedVariant].icon}
headline={t("no_category_apps", {
category: emptyHeaderCategory,
})}
description={t(`no_category_apps_description_${variant || "other"}`)}
description={t(`no_category_apps_description_${resolvedVariant}`)}
buttonRaw={
<Button
color="secondary"
data-testid={`connect-${variant || "other"}-apps`}
href={variant ? `/apps/categories/${variant}` : "/apps/categories/other"}>
{t(`connect_${variant || "other"}_apps`)}
data-testid={`connect-${resolvedVariant}-apps`}
href={`/apps/categories/${resolvedVariant}`}>
{t(`connect_${resolvedVariant}_apps`)}
</Button>
}
/>
Expand Down Expand Up @@ -258,4 +257,4 @@ export default function InstalledApps({ category, connectedCalendars, installedC
/>
</>
);
}
}
91 changes: 37 additions & 54 deletions packages/app-store/_utils/getAppCategories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,66 +8,49 @@ function getHref(baseURL: string, category: string, useQueryParam: boolean) {
return useQueryParam ? `${baseUrlParsed.toString()}` : `${baseURL}/${category}`;
}

export type ActiveAppCategoryKeys = Exclude<AppCategories, "video" | "web3">;

type AppCategoryEntry = {
name: AppCategories;
href: string;
icon: IconName;
"data-testid": string;
};

const getAppCategories = (baseURL: string, useQueryParam: boolean): AppCategoryEntry[] => {
// Manually sorted alphabetically, but leaving "Other" at the end
// TODO: Refactor and type with Record<AppCategories, AppCategoryEntry> to enforce consistency
return [
{
name: "analytics",
href: getHref(baseURL, "analytics", useQueryParam),
icon: "chart-bar",
"data-testid": "analytics",
},
{
name: "automation",
href: getHref(baseURL, "automation", useQueryParam),
icon: "share-2",
"data-testid": "automation",
},
{
name: "calendar",
href: getHref(baseURL, "calendar", useQueryParam),
icon: "calendar",
"data-testid": "calendar",
},
{
name: "conferencing",
href: getHref(baseURL, "conferencing", useQueryParam),
icon: "video",
"data-testid": "conferencing",
},
{
name: "crm",
href: getHref(baseURL, "crm", useQueryParam),
icon: "contact",
"data-testid": "crm",
},
{
name: "messaging",
href: getHref(baseURL, "messaging", useQueryParam),
icon: "mail",
"data-testid": "messaging",
},
{
name: "payment",
href: getHref(baseURL, "payment", useQueryParam),
icon: "credit-card",
"data-testid": "payment",
},
{
name: "other",
href: getHref(baseURL, "other", useQueryParam),
icon: "grid-3x3",
"data-testid": "other",
},
];
export const APP_CATEGORY_ENTRIES: Record<ActiveAppCategoryKeys, Omit<AppCategoryEntry, "name">> = {
analytics: { href: "", icon: "chart-bar", "data-testid": "analytics" },
automation: { href: "", icon: "share-2", "data-testid": "automation" },
calendar: { href: "", icon: "calendar", "data-testid": "calendar" },
conferencing: { href: "", icon: "video", "data-testid": "conferencing" },
crm: { href: "", icon: "contact", "data-testid": "crm" },
messaging: { href: "", icon: "mail", "data-testid": "messaging" },
payment: { href: "", icon: "credit-card", "data-testid": "payment" },
other: { href: "", icon: "grid-3x3", "data-testid": "other" },
};

export default getAppCategories;
const CATEGORY_ORDER = [
"analytics",
"automation",
"calendar",
"conferencing",
"crm",
"messaging",
"payment",
"other",
] as const satisfies readonly ActiveAppCategoryKeys[];

const _assertCategoryOrderIsExhaustive: Exclude<
ActiveAppCategoryKeys,
(typeof CATEGORY_ORDER)[number]
> extends never
? true
: never = true;

const getAppCategories = (baseURL: string, useQueryParam: boolean): AppCategoryEntry[] =>
CATEGORY_ORDER.map((name): AppCategoryEntry => ({
name,
...APP_CATEGORY_ENTRIES[name],
href: getHref(baseURL, name, useQueryParam),
}));

export default getAppCategories;
Loading