refactor: move AppList to features with slot props (#28100)

* refactor: move AppSetDefaultLinkDialog to features + inline QueryCell + fix broken type import

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* refactor: move AppList to features with slot props + delete AppListCard switcher

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* fix: correct ConnectedApps type to avoid double nesting of items

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: bot_apk <apk@cognition.ai>
This commit is contained in:
Rajiv Sahal
2026-03-12 11:00:20 +01:00
committed by GitHub
co-authored by rajiv@cal.com Devin AI bot_apk
parent 972d70379f
commit e5fd455a30
10 changed files with 104 additions and 133 deletions
@@ -11,12 +11,11 @@ import { ShellSubHeading } from "@calcom/ui/components/layout";
import { List } from "@calcom/ui/components/list";
import { showToast } from "@calcom/ui/components/toast";
import { revalidateSettingsCalendars } from "@calcom/web/app/cache/path/settings/my-account";
import AppListCard from "@calcom/web/modules/apps/components/AppListCard";
import AppListCardWebWrapper from "@calcom/web/modules/apps/components/AppListCardWebWrapper";
import { SkeletonLoader } from "@calcom/web/modules/apps/components/SkeletonLoader";
import { SelectedCalendarsSettingsWebWrapper } from "@calcom/web/modules/calendars/components/SelectedCalendarsSettingsWebWrapper";
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import { QueryCell } from "@lib/QueryCell";
import { Suspense, useEffect } from "react";
import { DestinationCalendarSettingsWebWrapper } from "./DestinationCalendarSettingsWebWrapper";
@@ -45,7 +44,7 @@ function CalendarList(props: Props): JSX.Element {
success={({ data }) => (
<List>
{data.items.map((item) => (
<AppListCard
<AppListCardWebWrapper
title={item.name}
key={item.name}
logo={item.logo}
@@ -1,16 +1,7 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
// eslint-disable-next-line no-restricted-imports
import { noop } from "lodash";
import type { FC } from "react";
import { useReducer, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
import AppListCard from "@calcom/web/modules/apps/components/AppListCard";
import { Dialog } from "@calcom/features/components/controlled-dialog";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -20,19 +11,25 @@ import { trpc } from "@calcom/trpc/react";
import cs from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
import {
ConfirmationDialogContent,
DialogClose,
DialogContent,
DialogFooter,
DialogClose,
ConfirmationDialogContent,
} from "@calcom/ui/components/dialog";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { Form } from "@calcom/ui/components/form";
import { TextField } from "@calcom/ui/components/form";
import { Switch } from "@calcom/ui/components/form";
import { Form, Switch, TextField } from "@calcom/ui/components/form";
import { List } from "@calcom/ui/components/list";
import { PencilIcon } from "@coss/ui/icons";
import { SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
import { showToast } from "@calcom/ui/components/toast";
import AppListCardWebWrapper from "@calcom/web/modules/apps/components/AppListCardWebWrapper";
import { PencilIcon } from "@coss/ui/icons";
import { zodResolver } from "@hookform/resolvers/zod";
// eslint-disable-next-line no-restricted-imports
import { noop } from "lodash";
import type { FC } from "react";
import { useReducer, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
type App = RouterOutputs["viewer"]["apps"]["listLocal"][number];
@@ -83,7 +80,7 @@ const IntegrationContainer = ({
return (
<li>
<AppListCard
<AppListCardWebWrapper
logo={app.logo}
description={app.description}
title={app.name}
@@ -1,25 +0,0 @@
"use client";
import dynamic from "next/dynamic";
import { useMemo } from "react";
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
import type { AppListCardProps } from "@calcom/ui/components/app-list-card";
export default function AppListCard(props: AppListCardProps) {
const isPlatform = useIsPlatform();
// Dynamically import the correct wrapper
const AppListCardWrapper = useMemo(
() =>
dynamic(
() => (isPlatform ? import("./AppListCardPlatformWrapper") : import("./AppListCardWebWrapper")),
{
ssr: false, // Prevents SSR issues if needed
}
),
[isPlatform]
);
return <AppListCardWrapper {...props} />;
}
@@ -1,18 +1,17 @@
"use client";
import { useReducer } from "react";
import { AppList } from "@calcom/features/apps/components/AppList";
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
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 { SkeletonText, SkeletonContainer } from "@calcom/ui/components/skeleton";
import { SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
import { showToast } from "@calcom/ui/components/toast";
import { AppList } from "./AppList";
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
import AppListCardWebWrapper from "@calcom/web/modules/apps/components/AppListCardWebWrapper";
import { useReducer } from "react";
export type UpdateUsersDefaultConferencingAppParams = {
appSlug: string;
@@ -142,6 +141,7 @@ export const InstalledConferencingApps = ({
isEventTypesFetching={false}
handleConnectDisconnectIntegrationMenuToggle={handleConnectDisconnectIntegrationMenuToggle}
handleBulkEditDialogToggle={handleBulkEditDialogToggle}
AppListCardComponent={AppListCardWebWrapper}
/>
);
};
@@ -1,28 +1,26 @@
"use client";
import { useReducer } from "react";
import getAppCategoryTitle from "@calcom/app-store/_utils/getAppCategoryTitle";
import { AppList, type HandleDisconnect } from "@calcom/web/modules/apps/components/AppList";
import type { UpdateUsersDefaultConferencingAppParams } from "@calcom/web/modules/apps/components/AppSetDefaultLinkDialog";
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
import { AppList, type HandleDisconnect } from "@calcom/features/apps/components/AppList";
import type { UpdateUsersDefaultConferencingAppParams } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
import type { RemoveAppParams } from "@calcom/features/apps/components/DisconnectIntegrationModal";
import { SkeletonLoader } from "@calcom/web/modules/apps/components/SkeletonLoader";
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
import type { BulkUpdatParams } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { AppCategories } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
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 { QueryCell } from "@lib/QueryCell";
import AppListCardWebWrapper from "@calcom/web/modules/apps/components/AppListCardWebWrapper";
import { SkeletonLoader } from "@calcom/web/modules/apps/components/SkeletonLoader";
import { CalendarListContainer } from "@components/apps/CalendarListContainer";
import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout";
import { QueryCell } from "@lib/QueryCell";
import { useReducer } from "react";
interface IntegrationsContainerProps {
variant?: AppCategories;
@@ -166,6 +164,7 @@ const IntegrationsContainer = ({
isEventTypesFetching={isEventTypesFetching}
handleConnectDisconnectIntegrationMenuToggle={handleConnectDisconnectIntegrationMenuToggle}
handleBulkEditDialogToggle={handleBulkEditDialogToggle}
AppListCardComponent={AppListCardWebWrapper}
/>
</div>
);
@@ -5,10 +5,10 @@ import { trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/components/alert";
import { Select } from "@calcom/ui/components/form";
import { List } from "@calcom/ui/components/list";
import AppListCard from "@calcom/web/modules/apps/components/AppListCard";
import AppListCardWebWrapper from "@calcom/web/modules/apps/components/AppListCardWebWrapper";
import CredentialActionsDropdown from "@calcom/web/modules/apps/components/CredentialActionsDropdown";
import { CalendarSwitch } from "@calcom/web/modules/calendars/components/CalendarSwitch";
import AdditionalCalendarSelector from "@calcom/web/modules/calendars/components/AdditionalCalendarSelector";
import { CalendarSwitch } from "@calcom/web/modules/calendars/components/CalendarSwitch";
import Link from "next/link";
import React from "react";
@@ -57,7 +57,7 @@ const ConnectedCalendarList = ({
{items.map((connectedCalendar) => {
if (!!connectedCalendar.calendars && connectedCalendar.calendars.length > 0) {
return (
<AppListCard
<AppListCardWebWrapper
key={`list-${connectedCalendar.credentialId}-${scope}`}
shouldHighlight
slug={connectedCalendar.integration.slug}
@@ -104,7 +104,7 @@ const ConnectedCalendarList = ({
</>
)}
</div>
</AppListCard>
</AppListCardWebWrapper>
);
}
return (
@@ -1,12 +1,10 @@
import { useCallback, useState } from "react";
import { InstallAppButton } from "@calcom/app-store/InstallAppButton";
import { AppSettings } from "@calcom/app-store/_components/AppSettings";
import { getLocationFromApp, type EventLocationType } from "@calcom/app-store/locations";
import type { ConnectedApps } from "@calcom/app-store/_utils/getConnectedApps";
import { InstallAppButton } from "@calcom/app-store/InstallAppButton";
import { type EventLocationType, getLocationFromApp } from "@calcom/app-store/locations";
import type { AppCardApp } from "@calcom/app-store/types";
import AppListCard from "@calcom/web/modules/apps/components/AppListCard";
import type { UpdateUsersDefaultConferencingAppParams } from "@calcom/web/modules/apps/components/AppSetDefaultLinkDialog";
import { AppSetDefaultLinkDialog } from "@calcom/web/modules/apps/components/AppSetDefaultLinkDialog";
import type { UpdateUsersDefaultConferencingAppParams } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
import type {
BulkUpdatParams,
EventTypes,
@@ -15,9 +13,10 @@ import { BulkEditDefaultForEventsModal } from "@calcom/features/eventtypes/compo
import { isDelegationCredential } from "@calcom/lib/delegationCredential";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/enums";
import { type RouterOutputs } from "@calcom/trpc/react";
import type { DefaultConferencingApp } from "@calcom/prisma/zod-utils";
import type { App } from "@calcom/types/App";
import { Alert } from "@calcom/ui/components/alert";
import type { AppListCardProps } from "@calcom/ui/components/app-list-card";
import { Button } from "@calcom/ui/components/button";
import {
Dropdown,
@@ -28,17 +27,19 @@ import {
} from "@calcom/ui/components/dropdown";
import { List } from "@calcom/ui/components/list";
import { showToast } from "@calcom/ui/components/toast";
import type { ComponentType, ReactNode } from "react";
import { useCallback, useState } from "react";
export type HandleDisconnect = (credentialId: number, app: App["slug"], teamId?: number) => void;
interface AppListProps {
variant?: AppCategories;
data: RouterOutputs["viewer"]["apps"]["integrations"];
data: ConnectedApps;
handleDisconnect: HandleDisconnect;
listClassName?: string;
appCardClassName?: string;
appCardMenuClassName?: string;
defaultConferencingApp: RouterOutputs["viewer"]["apps"]["getUsersDefaultConferencingApp"];
defaultConferencingApp: DefaultConferencingApp | undefined;
handleUpdateUserDefaultConferencingApp: (params: UpdateUsersDefaultConferencingAppParams) => void;
handleBulkUpdateDefaultLocation: (params: BulkUpdatParams) => void;
isBulkUpdateDefaultLocationPending: boolean;
@@ -46,6 +47,7 @@ interface AppListProps {
isEventTypesFetching?: boolean;
handleConnectDisconnectIntegrationMenuToggle: () => void;
handleBulkEditDialogToggle: () => void;
AppListCardComponent: ComponentType<AppListCardProps & { children?: ReactNode }>;
}
export const AppList = ({
@@ -63,6 +65,7 @@ export const AppList = ({
isEventTypesFetching,
handleConnectDisconnectIntegrationMenuToggle,
handleBulkEditDialogToggle,
AppListCardComponent,
}: AppListProps) => {
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
const [locationType, setLocationType] = useState<(EventLocationType & { slug: string }) | undefined>(
@@ -79,7 +82,7 @@ export const AppList = ({
appSlug === defaultConferencingApp?.appSlug ||
(appSlug === "daily-video" && !defaultConferencingApp?.appSlug);
return (
<AppListCard
<AppListCardComponent
key={item.name}
description={item.description}
title={item.name}
@@ -141,7 +144,7 @@ export const AppList = ({
) : null
}>
<AppSettings slug={item.slug} />
</AppListCard>
</AppListCardComponent>
);
};
@@ -1,5 +1,5 @@
import { AppListCard } from "@calcom/ui/components/app-list-card";
import type { AppListCardProps } from "@calcom/ui/components/app-list-card";
import { AppListCard } from "@calcom/ui/components/app-list-card";
export default function AppListCardPlatformWrapper(props: AppListCardProps) {
const logo = `https://app.cal.com${props.logo}`;
@@ -1,11 +1,13 @@
"use client";
import AccountDialog from "@calcom/app-store/office365video/components/AccountDialog";
import { AppList } from "@calcom/features/apps/components/AppList";
import type { UpdateUsersDefaultConferencingAppParams } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
import type { BulkUpdatParams } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { GOOGLE_MEET, OFFICE_365_VIDEO, ZOOM } from "@calcom/platform-constants";
import { QueryCell } from "@calcom/trpc/components/QueryCell";
import type { App } from "@calcom/types/App";
import { Button } from "@calcom/ui/components/button";
import {
@@ -17,16 +19,12 @@ import {
} from "@calcom/ui/components/dropdown";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
import { AppList } from "@calcom/web/modules/apps/components/AppList";
import { useQueryClient } from "@tanstack/react-query";
import { useReducer, useState } from "react";
import { AtomsWrapper } from "../../src/components/atoms-wrapper";
import { useToast } from "../../src/components/ui/use-toast";
import { cn } from "../../src/lib/utils";
import type {
BulkUpdatParams,
UpdateUsersDefaultConferencingAppParams,
} from "./ConferencingAppsViewWebWrapper";
import AppListCardPlatformWrapper from "./AppListCardPlatformWrapper";
import { useAtomBulkUpdateEventTypesToDefaultLocation } from "./hooks/useAtomBulkUpdateEventTypesToDefaultLocation";
import { useAtomGetEventTypes } from "./hooks/useAtomGetEventTypes";
import {
@@ -286,56 +284,56 @@ export const ConferencingAppsViewPlatformWrapper = ({
borderInShellHeader={true}>
<>
<div className="bg-default w-full sm:mx-0 xl:mt-0">
<QueryCell
query={installedIntegrationsQuery}
customLoader={<SkeletonLoader className={customClassNames?.skeletonClassName} />}
success={({ data }) => {
if (!data.items.length) {
return (
<EmptyScreen
Icon="calendar"
headline={t("no_category_apps", {
category: t("conferencing").toLowerCase(),
})}
description={t("no_category_apps_description_conferencing")}
buttonRaw={
<AddConferencingButtonPlatform
installedApps={data?.items}
buttonClassName={customClassNames?.addButtonClassName}
dropdownClassName={customClassNames?.addDropdownClassName}
/>
}
className={customClassNames?.emptyScreenClassName}
iconWrapperClassName={customClassNames?.emptyScreenIconWrapperClassName}
iconClassName={customClassNames?.emptyScreenIconClassName}
/>
);
}
return (
<AppList
listClassName={cn(
"rounded-lg rounded-t-none border-t-0 max-w-full",
customClassNames?.appListClassName
)}
appCardClassName={customClassNames?.appCardClassName}
appCardMenuClassName={customClassNames?.appCardMenuClassName}
handleDisconnect={handleDisconnect}
data={data}
variant="conferencing"
defaultConferencingApp={defaultConferencingApp}
handleUpdateUserDefaultConferencingApp={handleUpdateUserDefaultConferencingApp}
handleBulkUpdateDefaultLocation={handleBulkUpdateDefaultLocation}
isBulkUpdateDefaultLocationPending={bulkUpdateEventTypesToDefaultLocation?.isPending}
eventTypes={eventTypesQuery?.eventTypes}
isEventTypesFetching={isEventTypesFetching}
handleConnectDisconnectIntegrationMenuToggle={
handleConnectDisconnectIntegrationMenuToggle
}
handleBulkEditDialogToggle={handleBulkEditDialogToggle}
{installedIntegrationsQuery.status === "pending" ? (
<SkeletonLoader className={customClassNames?.skeletonClassName} />
) : installedIntegrationsQuery.status === "error" ? (
<EmptyScreen
Icon="calendar"
headline={t("no_category_apps", {
category: t("conferencing").toLowerCase(),
})}
description={t("no_category_apps_description_conferencing")}
/>
) : !installedIntegrationsQuery.data.items.length ? (
<EmptyScreen
Icon="calendar"
headline={t("no_category_apps", {
category: t("conferencing").toLowerCase(),
})}
description={t("no_category_apps_description_conferencing")}
buttonRaw={
<AddConferencingButtonPlatform
installedApps={installedIntegrationsQuery.data?.items}
buttonClassName={customClassNames?.addButtonClassName}
dropdownClassName={customClassNames?.addDropdownClassName}
/>
);
}}
/>
}
className={customClassNames?.emptyScreenClassName}
iconWrapperClassName={customClassNames?.emptyScreenIconWrapperClassName}
iconClassName={customClassNames?.emptyScreenIconClassName}
/>
) : (
<AppList
listClassName={cn(
"rounded-lg rounded-t-none border-t-0 max-w-full",
customClassNames?.appListClassName
)}
appCardClassName={customClassNames?.appCardClassName}
appCardMenuClassName={customClassNames?.appCardMenuClassName}
handleDisconnect={handleDisconnect}
data={installedIntegrationsQuery.data}
variant="conferencing"
defaultConferencingApp={defaultConferencingApp}
handleUpdateUserDefaultConferencingApp={handleUpdateUserDefaultConferencingApp}
handleBulkUpdateDefaultLocation={handleBulkUpdateDefaultLocation}
isBulkUpdateDefaultLocationPending={bulkUpdateEventTypesToDefaultLocation?.isPending}
eventTypes={eventTypesQuery?.eventTypes}
isEventTypesFetching={isEventTypesFetching}
handleConnectDisconnectIntegrationMenuToggle={handleConnectDisconnectIntegrationMenuToggle}
handleBulkEditDialogToggle={handleBulkEditDialogToggle}
AppListCardComponent={AppListCardPlatformWrapper}
/>
)}
</div>
<DisconnectIntegrationModal
handleModelClose={handleModelClose}