chore: refactor app list into common component (#9754)
Co-authored-by: Ty Kerr <tykerr@Tys-MacBook-Pro.local> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com>pull/11105/head^2
parent
798707a553
commit
538e3bf07c
|
@ -0,0 +1,231 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppList = ({ data, handleDisconnect, variant }: 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>
|
||||||
|
{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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,35 +1,13 @@
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { useCallback, useReducer, useState } from "react";
|
import { useReducer } from "react";
|
||||||
import z from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { AppSettings } from "@calcom/app-store/_components/AppSettings";
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
|
||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
|
||||||
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
|
|
||||||
import type { CredentialOwner } from "@calcom/app-store/types";
|
|
||||||
import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
|
|
||||||
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
||||||
import { BulkEditDefaultConferencingModal } from "@calcom/features/eventtypes/components/BulkEditDefaultConferencingModal";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { AppCategories } from "@calcom/prisma/enums";
|
import { AppCategories } from "@calcom/prisma/enums";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { App } from "@calcom/types/App";
|
|
||||||
import type { AppGetServerSidePropsContext } from "@calcom/types/AppGetServerSideProps";
|
import type { AppGetServerSidePropsContext } from "@calcom/types/AppGetServerSideProps";
|
||||||
import {
|
import { Button, EmptyScreen, AppSkeletonLoader as SkeletonLoader, ShellSubHeading } from "@calcom/ui";
|
||||||
Alert,
|
|
||||||
Button,
|
|
||||||
EmptyScreen,
|
|
||||||
List,
|
|
||||||
AppSkeletonLoader as SkeletonLoader,
|
|
||||||
ShellSubHeading,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
Dropdown,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownItem,
|
|
||||||
showToast,
|
|
||||||
} from "@calcom/ui";
|
|
||||||
import type { LucideIcon } from "@calcom/ui/components/icon";
|
import type { LucideIcon } from "@calcom/ui/components/icon";
|
||||||
import {
|
import {
|
||||||
BarChart,
|
BarChart,
|
||||||
|
@ -38,231 +16,24 @@ import {
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Grid,
|
Grid,
|
||||||
Mail,
|
Mail,
|
||||||
MoreHorizontal,
|
|
||||||
Plus,
|
Plus,
|
||||||
Share2,
|
Share2,
|
||||||
Trash,
|
|
||||||
Video,
|
Video,
|
||||||
} from "@calcom/ui/components/icon";
|
} from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
import AppListCard from "@components/AppListCard";
|
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
import { AppList } from "@components/apps/AppList";
|
||||||
import { CalendarListContainer } from "@components/apps/CalendarListContainer";
|
import { CalendarListContainer } from "@components/apps/CalendarListContainer";
|
||||||
import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout";
|
import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout";
|
||||||
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IntegrationsContainerProps {
|
interface IntegrationsContainerProps {
|
||||||
variant?: AppCategories;
|
variant?: AppCategories;
|
||||||
exclude?: AppCategories[];
|
exclude?: AppCategories[];
|
||||||
handleDisconnect: (credentialId: number) => void;
|
handleDisconnect: (credentialId: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IntegrationsListProps {
|
|
||||||
variant?: IntegrationsContainerProps["variant"];
|
|
||||||
data: RouterOutputs["viewer"]["integrations"];
|
|
||||||
handleDisconnect: (credentialId: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListProps) => {
|
|
||||||
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>
|
|
||||||
{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} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const IntegrationsContainer = ({
|
const IntegrationsContainer = ({
|
||||||
variant,
|
variant,
|
||||||
exclude,
|
exclude,
|
||||||
|
@ -329,7 +100,8 @@ const IntegrationsContainer = ({
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<IntegrationsList handleDisconnect={handleDisconnect} data={data} variant={variant} />
|
|
||||||
|
<AppList handleDisconnect={handleDisconnect} data={data} variant={variant} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,33 +1,16 @@
|
||||||
import { useCallback, useState } from "react";
|
import { useReducer } from "react";
|
||||||
|
|
||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
||||||
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
|
|
||||||
import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
|
|
||||||
import { BulkEditDefaultConferencingModal } from "@calcom/features/eventtypes/components/BulkEditDefaultConferencingModal";
|
|
||||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import {
|
import { Button, EmptyScreen, Meta, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||||
Button,
|
import { Calendar, Plus } from "@calcom/ui/components/icon";
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
Dropdown,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
List,
|
|
||||||
Meta,
|
|
||||||
showToast,
|
|
||||||
SkeletonContainer,
|
|
||||||
SkeletonText,
|
|
||||||
} from "@calcom/ui";
|
|
||||||
import { AlertCircle, MoreHorizontal, Trash, Video, Plus } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
import AppListCard from "@components/AppListCard";
|
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
import { AppList } from "@components/apps/AppList";
|
||||||
|
|
||||||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -46,161 +29,86 @@ const AddConferencingButton = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button color="secondary" StartIcon={Plus} href="/apps/categories/video">
|
<Button color="secondary" StartIcon={Plus} href="/apps/categories/conferencing">
|
||||||
{t("add_conferencing_app")}
|
{t("add_conferencing_app")}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ModalState = {
|
||||||
|
isOpen: boolean;
|
||||||
|
credentialId: null | number;
|
||||||
|
};
|
||||||
|
|
||||||
const ConferencingLayout = () => {
|
const ConferencingLayout = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
|
||||||
|
|
||||||
const { data: defaultConferencingApp, isLoading: defaultConferencingAppLoading } =
|
const [modal, updateModal] = useReducer(
|
||||||
trpc.viewer.getUsersDefaultConferencingApp.useQuery();
|
(data: ModalState, partialData: Partial<ModalState>) => ({ ...data, ...partialData }),
|
||||||
|
{
|
||||||
|
isOpen: false,
|
||||||
|
credentialId: null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const { data: apps, isLoading } = trpc.viewer.integrations.useQuery({
|
const query = trpc.viewer.integrations.useQuery({
|
||||||
variant: "conferencing",
|
variant: "conferencing",
|
||||||
onlyInstalled: true,
|
onlyInstalled: true,
|
||||||
});
|
});
|
||||||
const deleteAppMutation = trpc.viewer.deleteCredential.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
showToast("Integration deleted successfully", "success");
|
|
||||||
utils.viewer.integrations.invalidate({ variant: "conferencing", onlyInstalled: true });
|
|
||||||
setDeleteAppModal(false);
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
showToast("Error deleting app", "error");
|
|
||||||
setDeleteAppModal(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSuccessCallback = useCallback(() => {
|
const handleModelClose = () => {
|
||||||
setBulkUpdateModal(true);
|
updateModal({ isOpen: false, credentialId: null });
|
||||||
showToast("Default app updated successfully", "success");
|
};
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({
|
const handleDisconnect = (credentialId: number) => {
|
||||||
onSuccess: async () => {
|
updateModal({ isOpen: true, credentialId });
|
||||||
await utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
};
|
||||||
onSuccessCallback();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
showToast(`Error: ${error.message}`, "error");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [deleteAppModal, setDeleteAppModal] = useState(false);
|
|
||||||
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
|
|
||||||
const [locationType, setLocationType] = useState<(EventLocationType & { slug: string }) | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
const [deleteCredentialId, setDeleteCredentialId] = useState<number>(0);
|
|
||||||
|
|
||||||
if (isLoading || defaultConferencingAppLoading)
|
|
||||||
return <SkeletonLoader title={t("conferencing")} description={t("conferencing_description")} />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="bg-default w-full sm:mx-0 xl:mt-0">
|
<div className="bg-default w-full sm:mx-0 xl:mt-0">
|
||||||
<Meta
|
<Meta
|
||||||
title={t("conferencing")}
|
title={t("conferencing")}
|
||||||
description={t("conferencing_description")}
|
description={t("conferencing_description")}
|
||||||
CTA={<AddConferencingButton />}
|
CTA={<AddConferencingButton />}
|
||||||
/>
|
/>
|
||||||
<List>
|
<QueryCell
|
||||||
{apps?.items &&
|
query={query}
|
||||||
apps.items
|
customLoader={
|
||||||
.map((app) => ({ ...app, title: app.title || app.name }))
|
<SkeletonLoader title={t("conferencing")} description={t("conferencing_description")} />
|
||||||
.map((app) => {
|
}
|
||||||
const appSlug = app?.slug;
|
success={({ data }) => {
|
||||||
const appIsDefault =
|
console.log(data);
|
||||||
appSlug === defaultConferencingApp?.appSlug ||
|
if (!data.items.length) {
|
||||||
(appSlug === "daily-video" && !defaultConferencingApp?.appSlug); // Default to cal video if the user doesnt have it set (we do this on new account creation but not old)
|
|
||||||
return (
|
return (
|
||||||
<AppListCard
|
<EmptyScreen
|
||||||
description={app.description}
|
Icon={Calendar}
|
||||||
title={app.title}
|
headline={t("no_category_apps", {
|
||||||
logo={app.logo}
|
category: t("conferencing").toLowerCase(),
|
||||||
key={app.title}
|
|
||||||
isDefault={appIsDefault} // @TODO: Handle when a user doesnt have this value set
|
|
||||||
actions={
|
|
||||||
<div>
|
|
||||||
<Dropdown>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button StartIcon={MoreHorizontal} variant="icon" color="secondary" />
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent>
|
|
||||||
{!appIsDefault && (
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<DropdownItem
|
|
||||||
type="button"
|
|
||||||
color="secondary"
|
|
||||||
StartIcon={Video}
|
|
||||||
onClick={() => {
|
|
||||||
const locationType = getEventLocationTypeFromApp(
|
|
||||||
app?.locationOption?.value ?? ""
|
|
||||||
);
|
|
||||||
if (locationType?.linkType === "static") {
|
|
||||||
setLocationType({ ...locationType, slug: appSlug });
|
|
||||||
} else {
|
|
||||||
updateDefaultAppMutation.mutate({
|
|
||||||
appSlug,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{t("set_as_default")}
|
|
||||||
</DropdownItem>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<DropdownItem
|
|
||||||
type="button"
|
|
||||||
color="destructive"
|
|
||||||
disabled={app.isGlobal}
|
|
||||||
StartIcon={Trash}
|
|
||||||
onClick={() => {
|
|
||||||
setDeleteCredentialId(app.userCredentialIds[0]);
|
|
||||||
setDeleteAppModal(true);
|
|
||||||
}}>
|
|
||||||
{t("remove_app")}
|
|
||||||
</DropdownItem>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</List>
|
description={t("no_category_apps_description_conferencing")}
|
||||||
|
buttonRaw={
|
||||||
<Dialog open={deleteAppModal} onOpenChange={setDeleteAppModal}>
|
<Button
|
||||||
<DialogContent
|
color="secondary"
|
||||||
title={t("Remove app")}
|
data-testid="connect-conferencing-apps"
|
||||||
description={t("are_you_sure_you_want_to_remove_this_app")}
|
href="/apps/categories/conferencing">
|
||||||
type="confirmation"
|
{t("connect_conferencing_apps")}
|
||||||
Icon={AlertCircle}>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button color="primary" onClick={() => deleteAppMutation.mutate({ id: deleteCredentialId })}>
|
|
||||||
{t("yes_remove_app")}
|
|
||||||
</Button>
|
</Button>
|
||||||
<DialogClose />
|
}
|
||||||
</DialogFooter>
|
/>
|
||||||
</DialogContent>
|
);
|
||||||
</Dialog>
|
}
|
||||||
|
return <AppList handleDisconnect={handleDisconnect} data={data} variant="conferencing" />;
|
||||||
{locationType && (
|
}}
|
||||||
<AppSetDefaultLinkDialog
|
|
||||||
locationType={locationType}
|
|
||||||
setLocationType={setLocationType}
|
|
||||||
onSuccess={onSuccessCallback}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{bulkUpdateModal && (
|
|
||||||
<BulkEditDefaultConferencingModal open={bulkUpdateModal} setOpen={setBulkUpdateModal} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<DisconnectIntegrationModal
|
||||||
|
handleModelClose={handleModelClose}
|
||||||
|
isOpen={modal.isOpen}
|
||||||
|
credentialId={modal.credentialId}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue