233 lines
7.6 KiB
TypeScript
233 lines
7.6 KiB
TypeScript
import { useCallback, useState } from "react";
|
|
|
|
import { AppSettings } from "@calcom/app-store/_components/AppSettings";
|
|
import { InstallAppButton } from "@calcom/app-store/components";
|
|
import { getEventLocationTypeFromApp, type EventLocationType } from "@calcom/app-store/locations";
|
|
import type { CredentialOwner } from "@calcom/app-store/types";
|
|
import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
|
|
import { BulkEditDefaultConferencingModal } from "@calcom/features/eventtypes/components/BulkEditDefaultConferencingModal";
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
import type { AppCategories } from "@calcom/prisma/enums";
|
|
import { trpc, type RouterOutputs } from "@calcom/trpc";
|
|
import type { App } from "@calcom/types/App";
|
|
import {
|
|
Dropdown,
|
|
DropdownItem,
|
|
DropdownMenuContent,
|
|
DropdownMenuTrigger,
|
|
List,
|
|
showToast,
|
|
Button,
|
|
DropdownMenuItem,
|
|
Alert,
|
|
} from "@calcom/ui";
|
|
import { MoreHorizontal, Trash, Video } from "@calcom/ui/components/icon";
|
|
|
|
import AppListCard from "@components/AppListCard";
|
|
|
|
interface AppListProps {
|
|
variant?: AppCategories;
|
|
data: RouterOutputs["viewer"]["integrations"];
|
|
handleDisconnect: (credentialId: number) => void;
|
|
listClassName?: string;
|
|
}
|
|
|
|
export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppListProps) => {
|
|
const { data: defaultConferencingApp } = trpc.viewer.getUsersDefaultConferencingApp.useQuery();
|
|
const utils = trpc.useContext();
|
|
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 updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({
|
|
onSuccess: () => {
|
|
showToast("Default app updated successfully", "success");
|
|
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
|
},
|
|
onError: (error) => {
|
|
showToast(`Error: ${error.message}`, "error");
|
|
},
|
|
});
|
|
|
|
const ChildAppCard = ({
|
|
item,
|
|
}: {
|
|
item: RouterOutputs["viewer"]["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={MoreHorizontal} variant="icon" color="secondary" />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
{!appIsDefault && variant === "conferencing" && !item.credentialOwner?.teamId && (
|
|
<DropdownMenuItem>
|
|
<DropdownItem
|
|
type="button"
|
|
color="secondary"
|
|
StartIcon={Video}
|
|
onClick={() => {
|
|
const locationType = getEventLocationTypeFromApp(item?.locationOption?.value ?? "");
|
|
if (locationType?.linkType === "static") {
|
|
setLocationType({ ...locationType, slug: appSlug });
|
|
} else {
|
|
updateDefaultAppMutation.mutate({
|
|
appSlug,
|
|
});
|
|
setBulkUpdateModal(true);
|
|
}
|
|
}}>
|
|
{t("set_as_default")}
|
|
</DropdownItem>
|
|
</DropdownMenuItem>
|
|
)}
|
|
<ConnectOrDisconnectIntegrationMenuItem
|
|
credentialId={item.credentialOwner?.credentialId || item.userCredentialIds[0]}
|
|
type={item.type}
|
|
isGlobal={item.isGlobal}
|
|
installed
|
|
invalidCredentialIds={item.invalidCredentialIds}
|
|
handleDisconnect={handleDisconnect}
|
|
teamId={item.credentialOwner ? item.credentialOwner?.teamId : undefined}
|
|
/>
|
|
</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.logo,
|
|
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) => {
|
|
if (!item.teams.length) return <ChildAppCard item={item} />;
|
|
})}
|
|
</List>
|
|
{locationType && (
|
|
<AppSetDefaultLinkDialog
|
|
locationType={locationType}
|
|
setLocationType={() => setLocationType(undefined)}
|
|
onSuccess={onSuccessCallback}
|
|
/>
|
|
)}
|
|
|
|
{bulkUpdateModal && (
|
|
<BulkEditDefaultConferencingModal open={bulkUpdateModal} setOpen={setBulkUpdateModal} />
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
function ConnectOrDisconnectIntegrationMenuItem(props: {
|
|
credentialId: number;
|
|
type: App["type"];
|
|
isGlobal?: boolean;
|
|
installed?: boolean;
|
|
invalidCredentialIds?: number[];
|
|
teamId?: number;
|
|
handleDisconnect: (credentialId: number, teamId?: number) => void;
|
|
}) {
|
|
const { type, credentialId, isGlobal, installed, handleDisconnect, teamId } = props;
|
|
const { t } = useLocale();
|
|
|
|
const utils = trpc.useContext();
|
|
const handleOpenChange = () => {
|
|
utils.viewer.integrations.invalidate();
|
|
};
|
|
|
|
if (credentialId || type === "stripe_payment" || isGlobal) {
|
|
return (
|
|
<DropdownMenuItem>
|
|
<DropdownItem
|
|
color="destructive"
|
|
onClick={() => handleDisconnect(credentialId, teamId)}
|
|
disabled={isGlobal}
|
|
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={handleOpenChange}
|
|
/>
|
|
);
|
|
}
|