diff --git a/apps/web/components/apps/AppList.tsx b/apps/web/components/apps/AppList.tsx new file mode 100644 index 0000000000..71856d3452 --- /dev/null +++ b/apps/web/components/apps/AppList.tsx @@ -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 ( + 0 : false} + credentialOwner={item?.credentialOwner} + actions={ + !item.credentialOwner?.readOnly ? ( +
+ + +
+ ) : null + }> + +
+ ); + }; + + const appsWithTeamCredentials = data.items.filter((app) => app.teams.length); + const cardsForAppsWithTeams = appsWithTeamCredentials.map((app) => { + const appCards = []; + + if (app.userCredentialIds.length) { + appCards.push(); + } + for (const team of app.teams) { + if (team) { + appCards.push( + + ); + } + } + return appCards; + }); + + const { t } = useLocale(); + return ( + <> + + {cardsForAppsWithTeams.map((apps) => apps.map((cards) => cards))} + {data.items + .filter((item) => item.invalidCredentialIds) + .map((item) => { + if (!item.teams.length) return ; + })} + + {locationType && ( + setLocationType(undefined)} + onSuccess={onSuccessCallback} + /> + )} + + {bulkUpdateModal && ( + + )} + + ); +}; + +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 ( + + handleDisconnect(credentialId, teamId)} + disabled={isGlobal} + StartIcon={Trash}> + {t("remove_app")} + + + ); + } + + if (!installed) { + return ( +
+ +
+ ); + } + + return ( + ( + + )} + onChanged={handleOpenChange} + /> + ); +} diff --git a/apps/web/pages/apps/installed/[category].tsx b/apps/web/pages/apps/installed/[category].tsx index ae12e3d96e..83ad068cdb 100644 --- a/apps/web/pages/apps/installed/[category].tsx +++ b/apps/web/pages/apps/installed/[category].tsx @@ -1,35 +1,13 @@ import { useSearchParams } from "next/navigation"; -import { useCallback, useReducer, useState } from "react"; -import z from "zod"; +import { useReducer } from "react"; +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 { BulkEditDefaultConferencingModal } from "@calcom/features/eventtypes/components/BulkEditDefaultConferencingModal"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { AppCategories } from "@calcom/prisma/enums"; -import type { RouterOutputs } 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 { - Alert, - Button, - EmptyScreen, - List, - AppSkeletonLoader as SkeletonLoader, - ShellSubHeading, - DropdownMenuTrigger, - DropdownMenuContent, - Dropdown, - DropdownMenuItem, - DropdownItem, - showToast, -} from "@calcom/ui"; +import { Button, EmptyScreen, AppSkeletonLoader as SkeletonLoader, ShellSubHeading } from "@calcom/ui"; import type { LucideIcon } from "@calcom/ui/components/icon"; import { BarChart, @@ -38,231 +16,24 @@ import { CreditCard, Grid, Mail, - MoreHorizontal, Plus, Share2, - Trash, Video, } from "@calcom/ui/components/icon"; import { QueryCell } from "@lib/QueryCell"; -import AppListCard from "@components/AppListCard"; import PageWrapper from "@components/PageWrapper"; +import { AppList } from "@components/apps/AppList"; import { CalendarListContainer } from "@components/apps/CalendarListContainer"; 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 ( - - handleDisconnect(credentialId, teamId)} - disabled={isGlobal} - StartIcon={Trash}> - {t("remove_app")} - - - ); - } - - if (!installed) { - return ( -
- -
- ); - } - - return ( - ( - - )} - onChanged={handleOpenChange} - /> - ); -} - interface IntegrationsContainerProps { variant?: AppCategories; exclude?: AppCategories[]; 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 ( - 0 : false} - credentialOwner={item?.credentialOwner} - actions={ - !item.credentialOwner?.readOnly ? ( -
- - -
- ) : null - }> - -
- ); - }; - - const appsWithTeamCredentials = data.items.filter((app) => app.teams.length); - const cardsForAppsWithTeams = appsWithTeamCredentials.map((app) => { - const appCards = []; - - if (app.userCredentialIds.length) { - appCards.push(); - } - for (const team of app.teams) { - if (team) { - appCards.push( - - ); - } - } - return appCards; - }); - - const { t } = useLocale(); - return ( - <> - - {cardsForAppsWithTeams.map((apps) => apps.map((cards) => cards))} - {data.items - .filter((item) => item.invalidCredentialIds) - .map((item) => { - if (!item.teams.length) return ; - })} - - {locationType && ( - setLocationType(undefined)} - onSuccess={onSuccessCallback} - /> - )} - - {bulkUpdateModal && ( - - )} - - ); -}; - const IntegrationsContainer = ({ variant, exclude, @@ -329,7 +100,8 @@ const IntegrationsContainer = ({ } /> - + + ); }} diff --git a/apps/web/pages/settings/my-account/conferencing.tsx b/apps/web/pages/settings/my-account/conferencing.tsx index 923b155f8f..be48afc6ca 100644 --- a/apps/web/pages/settings/my-account/conferencing.tsx +++ b/apps/web/pages/settings/my-account/conferencing.tsx @@ -1,33 +1,16 @@ -import { useCallback, useState } from "react"; +import { useReducer } from "react"; -import type { EventLocationType } from "@calcom/app-store/locations"; -import { getEventLocationTypeFromApp } from "@calcom/app-store/locations"; -import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog"; -import { BulkEditDefaultConferencingModal } from "@calcom/features/eventtypes/components/BulkEditDefaultConferencingModal"; +import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal"; import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; -import { - Button, - Dialog, - DialogClose, - 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 { Button, EmptyScreen, Meta, SkeletonContainer, SkeletonText } from "@calcom/ui"; +import { Calendar, Plus } from "@calcom/ui/components/icon"; + +import { QueryCell } from "@lib/QueryCell"; -import AppListCard from "@components/AppListCard"; import PageWrapper from "@components/PageWrapper"; +import { AppList } from "@components/apps/AppList"; const SkeletonLoader = ({ title, description }: { title: string; description: string }) => { return ( @@ -46,161 +29,86 @@ const AddConferencingButton = () => { return ( <> - ); }; +type ModalState = { + isOpen: boolean; + credentialId: null | number; +}; + const ConferencingLayout = () => { const { t } = useLocale(); - const utils = trpc.useContext(); - const { data: defaultConferencingApp, isLoading: defaultConferencingAppLoading } = - trpc.viewer.getUsersDefaultConferencingApp.useQuery(); + const [modal, updateModal] = useReducer( + (data: ModalState, partialData: Partial) => ({ ...data, ...partialData }), + { + isOpen: false, + credentialId: null, + } + ); - const { data: apps, isLoading } = trpc.viewer.integrations.useQuery({ + const query = trpc.viewer.integrations.useQuery({ variant: "conferencing", 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(() => { - setBulkUpdateModal(true); - showToast("Default app updated successfully", "success"); - }, []); + const handleModelClose = () => { + updateModal({ isOpen: false, credentialId: null }); + }; - const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({ - onSuccess: async () => { - 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(0); - - if (isLoading || defaultConferencingAppLoading) - return ; + const handleDisconnect = (credentialId: number) => { + updateModal({ isOpen: true, credentialId }); + }; return ( -
- } - /> - - {apps?.items && - apps.items - .map((app) => ({ ...app, title: app.title || app.name })) - .map((app) => { - const appSlug = app?.slug; - const appIsDefault = - appSlug === defaultConferencingApp?.appSlug || - (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) + <> +
+ } + /> + + } + success={({ data }) => { + console.log(data); + if (!data.items.length) { return ( - - - -
+ + {t("connect_conferencing_apps")} + } /> ); - })} -
- - - - - - - - - - - {locationType && ( - ; + }} /> - )} - {bulkUpdateModal && ( - - )} -
+ + + ); };