import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible"; import { useSession } from "next-auth/react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import type { ComponentProps } from "react"; import React, { Suspense, useEffect, useState, useMemo } from "react"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; import Shell from "@calcom/features/shell/Shell"; import { classNames } from "@calcom/lib"; import { HOSTED_CAL_FEATURES, WEBAPP_URL } from "@calcom/lib/constants"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { IdentityProvider, MembershipRole, UserPermissionRole } from "@calcom/prisma/enums"; import { trpc } from "@calcom/trpc/react"; import type { VerticalTabItemProps } from "@calcom/ui"; import { Badge, Button, ErrorBoundary, Icon, Skeleton, useMeta, VerticalTabItem } from "@calcom/ui"; const tabs: VerticalTabItemProps[] = [ { name: "my_account", href: "/settings/my-account", icon: "user", children: [ { name: "profile", href: "/settings/my-account/profile" }, { name: "general", href: "/settings/my-account/general" }, { name: "calendars", href: "/settings/my-account/calendars" }, { name: "conferencing", href: "/settings/my-account/conferencing" }, { name: "appearance", href: "/settings/my-account/appearance" }, { name: "out_of_office", href: "/settings/my-account/out-of-office" }, // TODO // { name: "referrals", href: "/settings/my-account/referrals" }, ], }, { name: "security", href: "/settings/security", icon: "key", children: [ { name: "password", href: "/settings/security/password" }, { name: "impersonation", href: "/settings/security/impersonation" }, { name: "2fa_auth", href: "/settings/security/two-factor-auth" }, ], }, { name: "billing", href: "/settings/billing", icon: "credit-card", children: [{ name: "manage_billing", href: "/settings/billing" }], }, { name: "developer", href: "/settings/developer", icon: "terminal", children: [ // { name: "webhooks", href: "/settings/developer/webhooks" }, //{ name: "api_keys", href: "/settings/developer/api-keys" }, //{ name: "admin_api", href: "/settings/organizations/admin-api" }, // TODO: Add profile level for embeds // { name: "embeds", href: "/v2/settings/developer/embeds" }, ], }, { name: "organization", href: "/settings/organizations", children: [ { name: "profile", href: "/settings/organizations/profile", }, { name: "general", href: "/settings/organizations/general", }, { name: "members", href: "/settings/organizations/members", }, { name: "privacy", href: "/settings/organizations/privacy", }, { name: "billing", href: "/settings/organizations/billing", }, { name: "OAuth Clients", href: "/settings/organizations/platform/oauth-clients" }, { name: "SSO", href: "/settings/organizations/sso", }, { name: "directory_sync", href: "/settings/organizations/dsync", }, { name: "admin_api", href: "https://cal.com/docs/enterprise-features/api/api-reference/bookings#admin-access", }, ], }, { name: "teams", href: "/teams", icon: "users", children: [], }, { name: "other_teams", href: "/settings/organizations/teams/other", icon: "users", children: [], }, { name: "admin", href: "/settings/admin", icon: "lock", children: [ // { name: "features", href: "/settings/admin/flags" }, { name: "license", href: "/auth/setup?step=1" }, { name: "impersonation", href: "/settings/admin/impersonation" }, { name: "apps", href: "/settings/admin/apps/calendar" }, { name: "users", href: "/settings/admin/users" }, { name: "organizations", href: "/settings/admin/organizations" }, { name: "lockedSMS", href: "/settings/admin/lockedSMS" }, { name: "oAuth", href: "/settings/admin/oAuth" }, ], }, ]; tabs.find((tab) => { if (tab.name === "security" && !HOSTED_CAL_FEATURES) { tab.children?.push({ name: "sso_configuration", href: "/settings/security/sso" }); // TODO: Enable dsync for self hosters // tab.children?.push({ name: "directory_sync", href: "/settings/security/dsync" }); } }); // The following keys are assigned to admin only const adminRequiredKeys = ["admin"]; const organizationRequiredKeys = ["organization"]; const organizationAdminKeys = ["privacy", "billing", "OAuth Clients", "SSO", "directory_sync"]; const useTabs = () => { const session = useSession(); const { data: user } = trpc.viewer.me.useQuery({ includePasswordAdded: true }); const orgBranding = useOrgBranding(); const isAdmin = session.data?.user.role === UserPermissionRole.ADMIN; const isOrgAdminOrOwner = orgBranding?.role === MembershipRole.ADMIN || orgBranding?.role === MembershipRole.OWNER; const processTabsMemod = useMemo(() => { const processedTabs = tabs.map((tab) => { if (tab.href === "/settings/my-account") { if (!!session.data?.user?.org?.id) { tab.children = tab?.children?.filter((child) => child.href !== "/settings/my-account/appearance"); } return { ...tab, name: user?.name || "my_account", icon: undefined, avatar: getUserAvatarUrl(user), }; } else if (tab.href === "/settings/organizations") { const newArray = (tab?.children ?? []).filter( (child) => isOrgAdminOrOwner || !organizationAdminKeys.includes(child.name) ); return { ...tab, children: newArray, name: orgBranding?.name || "organization", avatar: getPlaceholderAvatar(orgBranding?.logoUrl, orgBranding?.name), }; } else if ( tab.href === "/settings/security" && user?.identityProvider === IdentityProvider.GOOGLE && !user?.twoFactorEnabled && !user?.passwordAdded ) { const filtered = tab?.children?.filter( (childTab) => childTab.href !== "/settings/security/two-factor-auth" ); return { ...tab, children: filtered }; } else if (tab.href === "/settings/developer" && !!orgBranding) { const filtered = tab?.children?.filter((childTab) => childTab.name !== "admin_api"); return { ...tab, children: filtered }; } return tab; }); // check if name is in adminRequiredKeys return processedTabs.filter((tab) => { if (organizationRequiredKeys.includes(tab.name)) return !!orgBranding; if (tab.name === "other_teams" && !isOrgAdminOrOwner) return false; if (isAdmin) return true; return !adminRequiredKeys.includes(tab.name); }); }, [isAdmin, orgBranding, isOrgAdminOrOwner, user]); return processTabsMemod; }; const BackButtonInSidebar = ({ name }: { name: string }) => { return ( {name} ); }; interface SettingsSidebarContainerProps { className?: string; navigationIsOpenedOnMobile?: boolean; bannersHeight?: number; } const TeamListCollapsible = () => { const { data: teams } = trpc.viewer.teams.list.useQuery(); const { t } = useLocale(); const [teamMenuState, setTeamMenuState] = useState<{ teamId: number | undefined; teamMenuOpen: boolean }[]>(); const searchParams = useCompatSearchParams(); useEffect(() => { if (teams) { const teamStates = teams?.map((team) => ({ teamId: team.id, teamMenuOpen: String(team.id) === searchParams?.get("id"), })); setTeamMenuState(teamStates); setTimeout(() => { const tabMembers = Array.from(document.getElementsByTagName("a")).filter( (bottom) => bottom.dataset.testid === "vertical-tab-Members" )[1]; tabMembers?.scrollIntoView({ behavior: "smooth" }); }, 100); } }, [searchParams?.get("id"), teams]); return ( <> {teams && teamMenuState && teams.map((team, index: number) => { if (!teamMenuState[index]) { return null; } if (teamMenuState.some((teamState) => teamState.teamId === team.id)) return ( setTeamMenuState([ ...teamMenuState, (teamMenuState[index] = { ...teamMenuState[index], teamMenuOpen: !teamMenuState[index].teamMenuOpen, }), ]) }>
setTeamMenuState([ ...teamMenuState, (teamMenuState[index] = { ...teamMenuState[index], teamMenuOpen: !teamMenuState[index].teamMenuOpen, }), ]) }>
{teamMenuState[index].teamMenuOpen ? ( ) : ( )}
{!team.parentId && ( {team.name )}

{team.name}

{!team.accepted && ( Inv. )}
{team.accepted && ( )} {(team.role === MembershipRole.OWNER || team.role === MembershipRole.ADMIN || // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this exists wtf? (team.isOrgAdmin && team.isOrgAdmin)) && ( <> {/* TODO */} {/* */} {/* Hide if there is a parent ID */} {!team.parentId ? ( <> ) : null} )}
); })} ); }; const SettingsSidebarContainer = ({ className = "", navigationIsOpenedOnMobile, bannersHeight, }: SettingsSidebarContainerProps) => { const searchParams = useCompatSearchParams(); const { t } = useLocale(); const tabsWithPermissions = useTabs(); const [otherTeamMenuState, setOtherTeamMenuState] = useState< { teamId: number | undefined; teamMenuOpen: boolean; }[] >(); const session = useSession(); const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { enabled: !!session.data?.user?.org, }); const { data: otherTeams } = trpc.viewer.organizations.listOtherTeams.useQuery(undefined, { enabled: !!session.data?.user?.org, }); // Same as above but for otherTeams useEffect(() => { if (otherTeams) { const otherTeamStates = otherTeams?.map((team) => ({ teamId: team.id, teamMenuOpen: String(team.id) === searchParams?.get("id"), })); setOtherTeamMenuState(otherTeamStates); setTimeout(() => { // @TODO: test if this works for 2 dataset testids const tabMembers = Array.from(document.getElementsByTagName("a")).filter( (bottom) => bottom.dataset.testid === "vertical-tab-Members" )[1]; tabMembers?.scrollIntoView({ behavior: "smooth" }); }, 100); } }, [searchParams?.get("id"), otherTeams]); const isOrgAdminOrOwner = currentOrg && currentOrg?.user?.role && ["OWNER", "ADMIN"].includes(currentOrg?.user?.role); return ( ); }; const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) => { const { t } = useLocale(); const router = useRouter(); return ( <> ); }; export default function SettingsLayout({ children, ...rest }: { children: React.ReactNode } & ComponentProps) { const pathname = usePathname(); const state = useState(false); const { t } = useLocale(); const [sideContainerOpen, setSideContainerOpen] = state; useEffect(() => { const closeSideContainer = () => { if (window.innerWidth >= 1024) { setSideContainerOpen(false); } }; window.addEventListener("resize", closeSideContainer); return () => { window.removeEventListener("resize", closeSideContainer); }; }, []); useEffect(() => { if (sideContainerOpen) { setSideContainerOpen(!sideContainerOpen); } }, [pathname]); return ( } drawerState={state} MobileNavigationContainer={null} TopNavContainer={ setSideContainerOpen(!sideContainerOpen)} /> }>
}>{children}
); } const SidebarContainerElement = ({ sideContainerOpen, bannersHeight, setSideContainerOpen, }: SidebarContainerElementProps) => { const { t } = useLocale(); return ( <> {/* Mobile backdrop */} {sideContainerOpen && ( )} ); }; type SidebarContainerElementProps = { sideContainerOpen: boolean; bannersHeight?: number; setSideContainerOpen: React.Dispatch>; }; export const getLayout = (page: React.ReactElement) => {page}; export function ShellHeader() { const { meta } = useMeta(); const { t, isLocaleReady } = useLocale(); return ( <>
{meta.backButton && ( )}
{meta.title && isLocaleReady ? (

{t(meta.title)}

) : (
)} {meta.description && isLocaleReady ? (

{t(meta.description)}

) : (
)}
{meta.CTA}
); }