Files
calendar/packages/platform/atoms/connect/conferencing-apps/ConferencingAppsViewPlatformWrapper.tsx
Rajiv SahalGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>bot_apk
e5fd455a30 refactor: move AppList to features with slot props (#28100)
* refactor: move AppSetDefaultLinkDialog to features + inline QueryCell + fix broken type import

Co-Authored-By: [email protected] <[email protected]>

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

Co-Authored-By: [email protected] <[email protected]>

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

Co-Authored-By: [email protected] <[email protected]>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: bot_apk <[email protected]>
2026-03-12 11:00:20 +01:00

357 lines
13 KiB
TypeScript

"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 type { App } from "@calcom/types/App";
import { Button } from "@calcom/ui/components/button";
import {
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@calcom/ui/components/dropdown";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
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 AppListCardPlatformWrapper from "./AppListCardPlatformWrapper";
import { useAtomBulkUpdateEventTypesToDefaultLocation } from "./hooks/useAtomBulkUpdateEventTypesToDefaultLocation";
import { useAtomGetEventTypes } from "./hooks/useAtomGetEventTypes";
import {
QUERY_KEY as atomsConferencingAppsQueryKey,
useAtomsGetInstalledConferencingApps,
} from "./hooks/useAtomsGetInstalledConferencingApps";
import { useConnect } from "./hooks/useConnect";
import { useDeleteCredential } from "./hooks/useDeleteCredential";
import {
QUERY_KEY as defaultConferencingAppQueryKey,
useGetDefaultConferencingApp,
} from "./hooks/useGetDefaultConferencingApp";
import { useUpdateUserDefaultConferencingApp } from "./hooks/useUpdateUserDefaultConferencingApp";
type ConferencingAppSlug = typeof GOOGLE_MEET | typeof ZOOM | typeof OFFICE_365_VIDEO;
export type ConferencingAppsCustomClassNames = {
containerClassName?: string;
headerClassName?: string;
headerTitleClassName?: string;
headerDescriptionClassName?: string;
addButtonClassName?: string;
addDropdownClassName?: string;
appListClassName?: string;
appCardClassName?: string;
appCardMenuClassName?: string;
emptyScreenClassName?: string;
emptyScreenIconWrapperClassName?: string;
emptyScreenIconClassName?: string;
skeletonClassName?: string;
};
type ConferencingAppsViewPlatformWrapperProps = {
disableToasts?: boolean;
returnTo?: string;
onErrorReturnTo?: string;
teamId?: number;
apps?: ConferencingAppSlug[];
disableBulkUpdateEventTypes?: boolean;
customClassNames?: ConferencingAppsCustomClassNames;
};
type RemoveAppParams = { callback: () => void; app?: App["slug"] };
const SkeletonLoader = ({ className }: { className?: string }) => {
return (
<SkeletonContainer className={className}>
<div className="divide-subtle border-subtle stack-y-6 rounded-b-lg border border-t-0 px-6 py-4">
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
</div>
</SkeletonContainer>
);
};
type ModalState = {
isOpen: boolean;
credentialId: null | number;
app: App["slug"] | null;
};
export const ConferencingAppsViewPlatformWrapper = ({
disableToasts = false,
returnTo,
onErrorReturnTo,
teamId,
apps,
disableBulkUpdateEventTypes = false,
customClassNames,
}: ConferencingAppsViewPlatformWrapperProps) => {
const { t } = useLocale();
const queryClient = useQueryClient();
const { toast } = useToast();
const showToast = (message: string, variant: "success" | "warning" | "error") => {
if (!disableToasts) {
toast({ description: message });
}
};
const [modal, updateModal] = useReducer(
(data: ModalState, partialData: Partial<ModalState>) => ({ ...data, ...partialData }),
{
isOpen: false,
credentialId: null,
app: null,
}
);
const [isAccountModalOpen, setIsAccountModalOpen] = useState(false);
const handleModelClose = () => {
updateModal({ isOpen: false, credentialId: null, app: null });
};
const handleDisconnect = (credentialId: number, app: App["slug"]) => {
updateModal({ isOpen: true, credentialId, app });
};
const installedIntegrationsQuery = useAtomsGetInstalledConferencingApps(teamId);
const { data: defaultConferencingApp } = useGetDefaultConferencingApp(teamId);
const { data: eventTypesQuery, isFetching: isEventTypesFetching } = useAtomGetEventTypes(
teamId,
disableBulkUpdateEventTypes
);
const deleteCredentialMutation = useDeleteCredential({
onSuccess: () => {
showToast(t("app_removed_successfully"), "success");
handleModelClose();
queryClient.invalidateQueries({
queryKey: [atomsConferencingAppsQueryKey],
});
queryClient.resetQueries({
queryKey: [defaultConferencingAppQueryKey],
});
},
onError: () => {
showToast(t("error_removing_app"), "error");
handleModelClose();
},
teamId,
});
const updateDefaultAppMutation = useUpdateUserDefaultConferencingApp({
teamId,
});
const bulkUpdateEventTypesToDefaultLocation = useAtomBulkUpdateEventTypesToDefaultLocation({
teamId,
});
const handleRemoveApp = ({ app }: RemoveAppParams) => {
!!app && deleteCredentialMutation.mutate(app);
};
const handleUpdateUserDefaultConferencingApp = ({
appSlug,
onSuccessCallback,
onErrorCallback,
}: UpdateUsersDefaultConferencingAppParams) => {
updateDefaultAppMutation.mutate(appSlug, {
onSuccess: () => {
showToast("Default app updated successfully", "success");
queryClient.invalidateQueries({ queryKey: [defaultConferencingAppQueryKey] });
!disableBulkUpdateEventTypes && onSuccessCallback();
},
onError: (error) => {
showToast(`Error: ${error.message}`, "error");
onErrorCallback();
},
});
};
const handleBulkUpdateDefaultLocation = ({ eventTypeIds, callback }: BulkUpdatParams) => {
if (disableBulkUpdateEventTypes) {
callback();
return;
}
bulkUpdateEventTypesToDefaultLocation.mutate(eventTypeIds, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [defaultConferencingAppQueryKey] });
callback();
},
});
};
const handleConnectDisconnectIntegrationMenuToggle = () => {
queryClient.invalidateQueries({ queryKey: [atomsConferencingAppsQueryKey] });
};
const handleBulkEditDialogToggle = () => {
queryClient.invalidateQueries({ queryKey: [defaultConferencingAppQueryKey] });
};
const { connect } = useConnect({
onSuccess: () => {
showToast("app installed successfully", "success");
queryClient.invalidateQueries({ queryKey: [atomsConferencingAppsQueryKey] });
},
onError: () => {
queryClient.invalidateQueries({ queryKey: [atomsConferencingAppsQueryKey] });
showToast(`Error: unable to install app`, "error");
},
returnTo,
onErrorReturnTo,
teamId,
});
const AddConferencingButtonPlatform = ({
installedApps,
buttonClassName,
dropdownClassName,
}: {
installedApps?: Array<{ slug: string }>;
buttonClassName?: string;
dropdownClassName?: string;
}) => {
return (
<Dropdown>
<DropdownMenuTrigger asChild>
<Button color="secondary" StartIcon="plus" className={buttonClassName}>
{t("add")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className={dropdownClassName}>
{/* Show Google Meet if it's not installed and either no apps filter is provided or it's in the apps filter */}
{installedApps &&
!installedApps.find((app) => app.slug === GOOGLE_MEET) &&
(!apps || apps.includes(GOOGLE_MEET)) && (
<DropdownMenuItem>
<DropdownItem color="secondary" onClick={() => connect(GOOGLE_MEET)}>
{t("google_meet")}
</DropdownItem>
</DropdownMenuItem>
)}
{/* Show Zoom if it's not installed and either no apps filter is provided or it's in the apps filter */}
{installedApps &&
!installedApps.find((app) => app.slug === ZOOM) &&
(!apps || apps.includes(ZOOM)) && (
<DropdownMenuItem>
<DropdownItem color="secondary" onClick={() => connect(ZOOM)}>
{t("zoom")}
</DropdownItem>
</DropdownMenuItem>
)}
{/* Show Office 365 Video if it's not installed and either no apps filter is provided or it's in the apps filter */}
{installedApps &&
!installedApps.find((app) => app.slug === OFFICE_365_VIDEO) &&
(!apps || apps.includes(OFFICE_365_VIDEO)) && (
<DropdownMenuItem>
<DropdownItem color="secondary" onClick={() => setIsAccountModalOpen(true)}>
{t("office_365_video")}
</DropdownItem>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</Dropdown>
);
};
return (
<AtomsWrapper customClassName={customClassNames?.containerClassName}>
<SettingsHeader
title={t("conferencing")}
description={t("conferencing_description")}
CTA={
<AddConferencingButtonPlatform
installedApps={installedIntegrationsQuery.data?.items}
buttonClassName={customClassNames?.addButtonClassName}
dropdownClassName={customClassNames?.addDropdownClassName}
/>
}
borderInShellHeader={true}>
<>
<div className="bg-default w-full sm:mx-0 xl:mt-0">
{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}
isOpen={modal.isOpen}
credentialId={modal.credentialId}
app={modal.app}
handleRemoveApp={handleRemoveApp}
isPlatform={true}
/>
<AccountDialog
open={isAccountModalOpen}
onOpenChange={setIsAccountModalOpen}
handleSubmit={() => connect(OFFICE_365_VIDEO)}
/>
</>
</SettingsHeader>
</AtomsWrapper>
);
};