From 962e64401eef9bab55c3c09d8da8af009c4ed449 Mon Sep 17 00:00:00 2001 From: Anirban Singha <143536290+SinghaAnirban005@users.noreply.github.com> Date: Sat, 13 Jun 2026 17:46:34 +0000 Subject: [PATCH] refactor: derive category metadata from single Record source of truth --- .../[category]/installed-category-view.tsx | 43 +++++---- packages/app-store/_utils/getAppCategories.ts | 91 ++++++++----------- 2 files changed, 58 insertions(+), 76 deletions(-) diff --git a/apps/web/modules/apps/installed/[category]/installed-category-view.tsx b/apps/web/modules/apps/installed/[category]/installed-category-view.tsx index 495cda27810796..05328b1ccf2136 100644 --- a/apps/web/modules/apps/installed/[category]/installed-category-view.tsx +++ b/apps/web/modules/apps/installed/[category]/installed-category-view.tsx @@ -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"; @@ -21,6 +20,7 @@ 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; @@ -28,6 +28,10 @@ interface IntegrationsContainerProps { handleDisconnect: HandleDisconnect; } +const LEGACY_CATEGORY_MAP: Partial> = { + video: "conferencing", +}; + const IntegrationsContainer = ({ variant, exclude, @@ -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(); @@ -95,41 +107,28 @@ const IntegrationsContainer = ({ utils.viewer.apps.getUsersDefaultConferencingApp.invalidate(); }; - // TODO: Refactor and reuse getAppCategories? - const emptyIcon: Record["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 ( } success={({ data }) => { if (!data.items.length) { - const emptyHeaderCategory = getAppCategoryTitle(variant || "other", true); + const resolvedVariant = resolveEmptyStateVariant(variant); + const emptyHeaderCategory = getAppCategoryTitle(resolvedVariant, true); return ( - {t(`connect_${variant || "other"}_apps`)} + data-testid={`connect-${resolvedVariant}-apps`} + href={`/apps/categories/${resolvedVariant}`}> + {t(`connect_${resolvedVariant}_apps`)} } /> @@ -258,4 +257,4 @@ export default function InstalledApps({ category, connectedCalendars, installedC /> ); -} +} \ No newline at end of file diff --git a/packages/app-store/_utils/getAppCategories.ts b/packages/app-store/_utils/getAppCategories.ts index 496386decdf92d..7945b7a3e1fac8 100644 --- a/packages/app-store/_utils/getAppCategories.ts +++ b/packages/app-store/_utils/getAppCategories.ts @@ -8,6 +8,8 @@ function getHref(baseURL: string, category: string, useQueryParam: boolean) { return useQueryParam ? `${baseUrlParsed.toString()}` : `${baseURL}/${category}`; } +export type ActiveAppCategoryKeys = Exclude; + type AppCategoryEntry = { name: AppCategories; href: string; @@ -15,59 +17,40 @@ type AppCategoryEntry = { "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 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> = { + 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; \ No newline at end of file