Files
calendar/packages/features/apps/components/AppList.tsx
T
417a612aa3 refactor: extract NextRequest from handle new booking (#20082)
* extract NextRequest

* update api and tests

* booking limits tests

* fix more tests to use new approach

* update more tests with new format

* extract getOrgDomainConfig to not use req

* extract req from loadNewUsers and pass in hostname/forcedSlug

* fix instant meeting types and hostname fixes

* fix handleNewBookingReq

* fix type errors in tests

* make hostName and forcedSlug optional

* fix type err

* Revert "fix type err"

This reverts commit 9d5de9019d9dafe348c97b876baaa1d0675967e5.

* wip fix e2e

* fix: add missing headers

* migrate handle recurring event and also create tests specific to fn

* platform recurringbooking

* fix type

* hard code types on request object

* bump libraries

* fixup! bump libraries

* fix: accessing host if headers not passed

* fix: v2 recurring booking

* fix: accessing host if headers not passed

* chore: bump platform libraries

* fix tests

* push

* chore: bump libraries

* push lock changes

* bump libraries

---------

Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
2025-04-02 17:57:58 -04:00

261 lines
9.2 KiB
TypeScript

import { useCallback, useState } from "react";
import { AppSettings } from "@calcom/app-store/_components/AppSettings";
import { InstallAppButton } from "@calcom/app-store/components";
import { getLocationFromApp, type EventLocationType } from "@calcom/app-store/locations";
import type { CredentialOwner } from "@calcom/app-store/types";
import AppListCard from "@calcom/features/apps/components/AppListCard";
import type { UpdateUsersDefaultConferencingAppParams } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
import type {
BulkUpdatParams,
EventTypes,
} from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
import { BulkEditDefaultForEventsModal } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
import { isDelegationCredential } from "@calcom/lib/delegationCredential/clientAndServer";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/enums";
import { type RouterOutputs } from "@calcom/trpc";
import type { App } from "@calcom/types/App";
import { Alert } from "@calcom/ui/components/alert";
import { Button } from "@calcom/ui/components/button";
import {
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@calcom/ui/components/dropdown";
import { List } from "@calcom/ui/components/list";
import { showToast } from "@calcom/ui/components/toast";
export type HandleDisconnect = (credentialId: number, app: App["slug"], teamId?: number) => void;
interface AppListProps {
variant?: AppCategories;
data: RouterOutputs["viewer"]["apps"]["integrations"];
handleDisconnect: HandleDisconnect;
listClassName?: string;
defaultConferencingApp: RouterOutputs["viewer"]["apps"]["getUsersDefaultConferencingApp"];
handleUpdateUserDefaultConferencingApp: (params: UpdateUsersDefaultConferencingAppParams) => void;
handleBulkUpdateDefaultLocation: (params: BulkUpdatParams) => void;
isBulkUpdateDefaultLocationPending: boolean;
eventTypes?: EventTypes;
isEventTypesFetching?: boolean;
handleConnectDisconnectIntegrationMenuToggle: () => void;
handleBulkEditDialogToggle: () => void;
}
export const AppList = ({
data,
handleDisconnect,
variant,
listClassName,
defaultConferencingApp,
handleUpdateUserDefaultConferencingApp,
handleBulkUpdateDefaultLocation,
isBulkUpdateDefaultLocationPending,
eventTypes,
isEventTypesFetching,
handleConnectDisconnectIntegrationMenuToggle,
handleBulkEditDialogToggle,
}: AppListProps) => {
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
const [locationType, setLocationType] = useState<(EventLocationType & { slug: string }) | undefined>(
undefined
);
const onSuccessCallback = useCallback(() => {
setBulkUpdateModal(true);
showToast("Default app updated successfully", "success");
}, []);
const ChildAppCard = ({
item,
}: {
item: RouterOutputs["viewer"]["apps"]["integrations"]["items"][number] & {
credentialOwner?: CredentialOwner;
};
}) => {
const appSlug = item?.slug;
const appIsDefault =
appSlug === defaultConferencingApp?.appSlug ||
(appSlug === "daily-video" && !defaultConferencingApp?.appSlug);
return (
<AppListCard
key={item.name}
description={item.description}
title={item.name}
logo={item.logo}
isDefault={appIsDefault}
shouldHighlight
slug={item.slug}
invalidCredential={item?.invalidCredentialIds ? item.invalidCredentialIds.length > 0 : false}
credentialOwner={item?.credentialOwner}
actions={
!item.credentialOwner?.readOnly ? (
<div className="flex justify-end">
<Dropdown modal={false}>
<DropdownMenuTrigger asChild>
<Button StartIcon="ellipsis" variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent>
{!appIsDefault && variant === "conferencing" && (
<DropdownMenuItem>
<DropdownItem
type="button"
color="secondary"
StartIcon="video"
onClick={() => {
const locationType = getLocationFromApp(item?.locationOption?.value ?? "");
if (locationType?.linkType === "static") {
setLocationType({ ...locationType, slug: appSlug });
} else {
handleUpdateUserDefaultConferencingApp({
appSlug,
onSuccessCallback: () => setBulkUpdateModal(true),
onErrorCallback: () => {
return;
},
});
}
}}>
{t("set_as_default")}
</DropdownItem>
</DropdownMenuItem>
)}
<ConnectOrDisconnectIntegrationMenuItem
credentialId={item.credentialOwner?.credentialId || item.userCredentialIds[0]}
type={item.type}
app={item.slug}
isGlobal={item.isGlobal}
installed
invalidCredentialIds={item.invalidCredentialIds}
handleDisconnect={handleDisconnect}
teamId={item.credentialOwner ? item.credentialOwner?.teamId : undefined}
handleConnectDisconnectIntegrationMenuToggle={
handleConnectDisconnectIntegrationMenuToggle
}
/>
</DropdownMenuContent>
</Dropdown>
</div>
) : null
}>
<AppSettings slug={item.slug} />
</AppListCard>
);
};
const appsWithTeamCredentials = data.items.filter((app) => app.teams.length);
const cardsForAppsWithTeams = appsWithTeamCredentials.map((app) => {
const appCards = [];
if (app.userCredentialIds.length) {
appCards.push(<ChildAppCard item={app} />);
}
for (const team of app.teams) {
if (team) {
appCards.push(
<ChildAppCard
item={{
...app,
credentialOwner: {
name: team.name,
avatar: team.logoUrl,
teamId: team.teamId,
credentialId: team.credentialId,
readOnly: !team.isAdmin,
},
}}
/>
);
}
}
return appCards;
});
const { t } = useLocale();
return (
<>
<List className={listClassName}>
{cardsForAppsWithTeams.map((apps) => apps.map((cards) => cards))}
{data.items
.filter((item) => item.invalidCredentialIds)
.map((item, i) => {
if (!item.teams.length) return <ChildAppCard key={i} item={item} />;
})}
</List>
{locationType && (
<AppSetDefaultLinkDialog
locationType={locationType}
setLocationType={() => setLocationType(undefined)}
onSuccess={onSuccessCallback}
handleUpdateUserDefaultConferencingApp={handleUpdateUserDefaultConferencingApp}
/>
)}
{bulkUpdateModal && (
<BulkEditDefaultForEventsModal
bulkUpdateFunction={handleBulkUpdateDefaultLocation}
open={bulkUpdateModal}
setOpen={setBulkUpdateModal}
isPending={isBulkUpdateDefaultLocationPending}
description={t("default_conferencing_bulk_description")}
eventTypes={eventTypes}
isEventTypesFetching={isEventTypesFetching}
handleBulkEditDialogToggle={handleBulkEditDialogToggle}
/>
)}
</>
);
};
function ConnectOrDisconnectIntegrationMenuItem(props: {
credentialId: number;
type: App["type"];
isGlobal?: boolean;
installed?: boolean;
invalidCredentialIds?: number[];
teamId?: number;
app: App["slug"];
handleDisconnect: HandleDisconnect;
handleConnectDisconnectIntegrationMenuToggle: () => void;
}) {
const { type, credentialId, isGlobal, installed, handleDisconnect, teamId, app } = props;
const { t } = useLocale();
if (credentialId || type === "stripe_payment" || isGlobal) {
return (
<DropdownMenuItem>
<DropdownItem
color="destructive"
onClick={() => handleDisconnect(credentialId, app, teamId)}
disabled={isGlobal || isDelegationCredential({ credentialId })}
StartIcon="trash">
{t("remove_app")}
</DropdownItem>
</DropdownMenuItem>
);
}
if (!installed) {
return (
<div className="flex items-center truncate">
<Alert severity="warning" title={t("not_installed")} />
</div>
);
}
return (
<InstallAppButton
type={type}
render={(buttonProps) => (
<Button color="secondary" {...buttonProps} data-testid="integration-connection-button">
{t("install")}
</Button>
)}
onChanged={props.handleConnectDisconnectIntegrationMenuToggle}
/>
);
}