Files
RomitandGitHub a4a01a0fa8 cleanup(attributes): remove entrypoints (#29034)
* refactor: remove attributes and segments entrypoints

* fix: restore segment entrypoints

* cleanup
2026-05-05 15:03:00 +05:30

608 lines
20 KiB
TypeScript

"use client";
import { HAS_ORG_OPT_IN_FEATURES } from "@calcom/features/feature-opt-in/config";
import type { TeamFeatures } from "@calcom/features/flags/config";
import { IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { useIsStandalone } from "@calcom/lib/hooks/useIsStandalone";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { IdentityProvider, UserPermissionRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import classNames from "@calcom/ui/classNames";
import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { ErrorBoundary } from "@calcom/ui/components/errorBoundary";
import { Icon } from "@calcom/ui/components/icon";
import type { VerticalTabItemProps } from "@calcom/ui/components/navigation";
import { VerticalTabItem } from "@calcom/ui/components/navigation";
import { Skeleton } from "@calcom/ui/components/skeleton";
import { ArrowLeftIcon } from "@coss/ui/icons";
import Image from "next/image";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import type { ComponentProps } from "react";
import React, { useEffect, useMemo, useState } from "react";
import Shell from "~/shell/Shell";
const getTabs = (
orgBranding: {
id?: number;
slug?: string;
name?: string;
logoUrl?: string | null;
fullDomain?: string | null;
} | null
) => {
const tabs: VerticalTabItemProps[] = [
{
name: "my_account",
href: "/settings/my-account",
icon: "user",
children: [
{
name: "profile",
href: "/settings/my-account/profile",
trackingMetadata: { section: "my_account", page: "profile" },
},
{
name: "general",
href: "/settings/my-account/general",
trackingMetadata: { section: "my_account", page: "general" },
},
{
name: "calendars",
href: "/settings/my-account/calendars",
trackingMetadata: { section: "my_account", page: "calendars" },
},
{
name: "conferencing",
href: "/settings/my-account/conferencing",
trackingMetadata: { section: "my_account", page: "conferencing" },
},
{
name: "appearance",
href: "/settings/my-account/appearance",
trackingMetadata: { section: "my_account", page: "appearance" },
},
{
name: "out_of_office",
href: "/settings/my-account/out-of-office",
trackingMetadata: { section: "my_account", page: "out_of_office" },
},
{
name: "push_notifications",
href: "/settings/my-account/push-notifications",
trackingMetadata: { section: "my_account", page: "push_notifications" },
},
// TODO
// { name: "referrals", href: "/settings/my-account/referrals" },
],
},
{
name: "security",
href: "/settings/security",
icon: "key",
children: [
{
name: "password",
href: "/settings/security/password",
trackingMetadata: { section: "security", page: "password" },
},
{
name: "2fa_auth",
href: "/settings/security/two-factor-auth",
trackingMetadata: { section: "security", page: "2fa_auth" },
},
],
},
{
name: "developer",
href: "/settings/developer",
icon: "terminal",
children: [
//
{
name: "webhooks",
href: "/settings/developer/webhooks",
trackingMetadata: { section: "developer", page: "webhooks" },
},
{
name: "oAuth",
href: "/settings/developer/oauth",
trackingMetadata: { section: "developer", page: "oauth_clients" },
},
{
name: "api_keys",
href: "/settings/developer/api-keys",
trackingMetadata: { section: "developer", page: "api_keys" },
},
{
name: "api_docs",
href: "/docs",
trackingMetadata: { section: "developer", page: "api_docs" },
},
// 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",
trackingMetadata: { section: "organization", page: "profile" },
},
{
name: "general",
href: "/settings/organizations/general",
trackingMetadata: { section: "organization", page: "general" },
},
{
name: "guest_notifications",
href: "/settings/organizations/guest-notifications",
},
...(orgBranding
? [
{
name: "members",
href: `${WEBAPP_URL}/settings/organizations/${orgBranding?.slug}/members`,
isExternalLink: true,
trackingMetadata: { section: "organization", page: "members" },
},
]
: []),
{
name: "privacy_and_security",
href: "/settings/organizations/privacy",
trackingMetadata: { section: "organization", page: "privacy_and_security" },
},
{
name: "SSO",
href: "/settings/organizations/sso",
trackingMetadata: { section: "organization", page: "sso" },
},
{
name: "directory_sync",
href: "/settings/organizations/dsync",
trackingMetadata: { section: "organization", page: "directory_sync" },
},
{
name: "api_docs",
href: "/docs",
trackingMetadata: { section: "organization", page: "api_docs" },
},
...(HAS_ORG_OPT_IN_FEATURES
? [
{
name: "features",
href: "/settings/organizations/features",
trackingMetadata: { section: "organization", page: "features" },
},
]
: []),
],
},
{
name: "admin",
href: "/settings/admin",
icon: "lock",
children: [
//
{
name: "features",
href: "/settings/admin/flags",
trackingMetadata: { section: "admin", page: "features" },
},
{
name: "license",
href: "/auth/setup?step=1",
trackingMetadata: { section: "admin", page: "license" },
},
{
name: "apps",
href: "/settings/admin/apps/calendar",
trackingMetadata: { section: "admin", page: "apps" },
},
{
name: "users",
href: "/settings/admin/users",
trackingMetadata: { section: "admin", page: "users" },
},
{
name: "lockedSMS",
href: "/settings/admin/lockedSMS",
trackingMetadata: { section: "admin", page: "locked_sms" },
},
{
name: "oAuth",
href: "/settings/admin/oauth",
trackingMetadata: { section: "admin", page: "oauth" },
},
{
name: "Playground",
href: "/settings/admin/playground",
trackingMetadata: { section: "admin", page: "playground" },
},
],
},
];
for (const tab of tabs) {
if (tab.name === "admin" && IS_CALCOM) {
tab.children?.push({
name: "create_org",
href: "/settings/organizations/new",
trackingMetadata: { section: "admin", page: "create_org" },
});
tab.children?.push({
name: "create_license_key",
href: "/settings/license-key/new",
trackingMetadata: { section: "admin", page: "create_license_key" },
});
}
}
return tabs;
};
// The following keys are assigned to admin only
const adminRequiredKeys = ["admin"];
const organizationRequiredKeys = ["organization"];
const organizationAdminKeys = [
"privacy",
"privacy_and_security",
"SSO",
"directory_sync",
"delegation_credential",
];
interface SettingsPermissions {
canViewRoles?: boolean;
canViewOrganizationBilling?: boolean;
canUpdateOrganization?: boolean;
}
const useTabs = ({
isDelegationCredentialEnabled,
isPbacEnabled,
permissions,
}: {
isDelegationCredentialEnabled: boolean;
isPbacEnabled: boolean;
permissions?: SettingsPermissions;
}) => {
const session = useSession();
const { data: user } = trpc.viewer.me.get.useQuery({ includePasswordAdded: true });
const orgBranding = null as { id?: number; slug?: string; name?: string; logoUrl?: string | null } | null;
const isAdmin = session.data?.user.role === UserPermissionRole.ADMIN;
const processTabsMemod = useMemo(() => {
const processedTabs = getTabs(orgBranding).map((tab) => {
if (tab.href === "/settings/my-account") {
return {
...tab,
name: user?.name || "my_account",
icon: undefined,
avatar: getUserAvatarUrl(user),
};
} else if (tab.href === "/settings/organizations") {
const newArray = (tab?.children ?? []).filter(
(child) => permissions?.canUpdateOrganization || !organizationAdminKeys.includes(child.name)
);
// Add delegation-credential menu item only if feature flag is enabled
if (isDelegationCredentialEnabled) {
newArray.push({
name: "delegation_credential",
href: "/settings/organizations/delegation-credential",
trackingMetadata: { section: "organization", page: "delegation_credential" },
});
}
// Add pbac menu item - show opt-in page if not enabled, regular page if enabled
if (isPbacEnabled) {
if (permissions?.canViewRoles) {
newArray.push({
name: "roles_and_permissions",
href: "/settings/organizations/roles",
trackingMetadata: { section: "organization", page: "roles_and_permissions" },
});
}
if (permissions?.canViewOrganizationBilling) {
newArray.push({
name: "billing",
href: "/settings/organizations/billing",
trackingMetadata: { section: "organization", page: "billing" },
});
}
} else {
if (permissions?.canUpdateOrganization) {
newArray.push({
name: "billing",
href: "/settings/organizations/billing",
trackingMetadata: { section: "organization", page: "billing" },
});
}
// Show roles page (modal will appear if PBAC not enabled)
if (permissions?.canUpdateOrganization) {
newArray.push({
name: "roles",
href: "/settings/organizations/roles",
isBadged: true, // Show "New" badge,
trackingMetadata: { section: "organization", page: "roles" },
});
}
}
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") {
const filtered = tab?.children?.filter(
(childTab) => permissions?.canUpdateOrganization || childTab.name !== "api_docs"
);
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" && !permissions?.canUpdateOrganization) return false;
if (isAdmin) return true;
return !adminRequiredKeys.includes(tab.name);
});
}, [isAdmin, orgBranding, user, isDelegationCredentialEnabled, isPbacEnabled, permissions]);
return processTabsMemod;
};
const BackButtonInSidebar = ({ name }: { name: string }) => {
return (
<Link
href="/event-types"
className="group my-6 todesktop:mt-10 flex h-6 max-h-6 w-full flex-row items-center rounded-md px-3 py-2 font-medium text-emphasis text-sm leading-4 transition hover:bg-subtle group-hover:text-default [&[aria-current='page']]:bg-emphasis [&[aria-current='page']]:text-emphasis"
data-testid={`vertical-tab-${name}`}>
<ArrowLeftIcon className="h-4 w-4 stroke-[2px] md:mt-0 ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180" />
<Skeleton title={name} as="p" className="min-h-4 max-w-36 truncate" loadingClassName="ms-3">
{name}
</Skeleton>
</Link>
);
};
interface SettingsSidebarContainerProps {
className?: string;
navigationIsOpenedOnMobile?: boolean;
bannersHeight?: number;
teamFeatures?: Record<number, TeamFeatures>;
permissions?: SettingsPermissions;
}
const SettingsSidebarContainer = ({
className = "",
navigationIsOpenedOnMobile,
bannersHeight,
teamFeatures,
permissions,
}: SettingsSidebarContainerProps) => {
const { t } = useLocale();
const tabsWithPermissions = useTabs({
isDelegationCredentialEnabled: false,
isPbacEnabled: false,
permissions,
});
return (
<nav
style={{ maxHeight: `calc(100vh - ${bannersHeight}px)`, top: `${bannersHeight}px` }}
className={classNames(
"no-scrollbar stack-y-1 fixed top-0 bottom-0 left-0 z-20 flex max-h-screen w-56 flex-col overflow-x-hidden overflow-y-scroll bg-cal-muted px-2 pb-3 transition-transform max-lg:z-10 lg:sticky lg:flex",
className,
navigationIsOpenedOnMobile
? "translate-x-0 opacity-100"
: "-translate-x-full opacity-0 lg:translate-x-0 lg:opacity-100"
)}
aria-label={t("settings_navigation")}>
<BackButtonInSidebar name={t("back")} />
{tabsWithPermissions.map((tab) => {
return (
<React.Fragment key={tab.href}>
<div className={`${!tab.children?.length ? "mb-3!" : ""}`}>
<div className="group flex h-7 w-full flex-row items-center rounded-md px-2 font-medium text-default text-sm leading-none [&[aria-current='page']]:bg-emphasis [&[aria-current='page']]:text-emphasis">
{tab?.icon && (
<Icon
name={tab.icon}
className="h-[16px] w-[16px] stroke-[2px] text-subtle md:mt-0 ltr:mr-3 rtl:ml-3"
/>
)}
{}
{!tab.icon && tab?.avatar && (
<Image
width={16}
height={16}
className="rounded-full ltr:mr-3 rtl:ml-3"
src={tab?.avatar}
alt="Organization Logo"
/>
)}
<Skeleton
title={tab.name}
as="p"
className="truncate font-medium text-sm text-subtle leading-5"
loadingClassName="ms-3">
{t(tab.name)}
</Skeleton>
</div>
</div>
<div className="flex flex-col space-y-1">
{tab.children?.map((child, index) => (
<div key={child.href} className="flex items-start gap-2">
<VerticalTabItem
name={t(child.name)}
isExternalLink={child.isExternalLink}
href={child.href || "/"}
trackingMetadata={child.trackingMetadata}
textClassNames="text-emphasis font-medium text-sm"
className={`h-auto min-h-7 w-fit px-2! py-1! ${
tab.children && index === tab.children?.length - 1 && "mb-3!"
}`}
disableChevron
/>
{child.isBadged && (
<Badge variant="blue" className="mt-0.5 text-xs">
New
</Badge>
)}
</div>
))}
</div>
</React.Fragment>
);
})}
</nav>
);
};
const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) => {
const { t } = useLocale();
const router = useRouter();
const isStandalone = useIsStandalone();
if (isStandalone) return null;
return (
<nav className="sticky top-0 z-20 flex w-full items-center justify-between border-muted border-b bg-cal-muted px-2 py-2 sm:relative lg:hidden">
<div className="flex items-center space-x-3">
<Button StartIcon="menu" color="minimal" variant="icon" onClick={props.onSideContainerOpen}>
<span className="sr-only">{t("show_navigation")}</span>
</Button>
<button
className="flex items-center space-x-2 rounded-md px-3 py-1 hover:bg-emphasis rtl:space-x-reverse"
onClick={() => router.back()}>
<ArrowLeftIcon className="h-4 w-4 text-default" />
<p className="font-semibold text-emphasis">{t("settings")}</p>
</button>
</div>
</nav>
);
};
type SettingsLayoutProps = {
children: React.ReactNode;
containerClassName?: string;
teamFeatures?: Record<number, TeamFeatures>;
permissions?: SettingsPermissions;
} & ComponentProps<typeof Shell>;
function SettingsLayoutAppDirClient({ children, teamFeatures, permissions, ...rest }: SettingsLayoutProps) {
const _pathname = usePathname();
const state = useState(false);
const [sideContainerOpen, setSideContainerOpen] = state;
useEffect(() => {
const closeSideContainer = () => {
if (window.innerWidth >= 1024) {
setSideContainerOpen(false);
}
};
window.addEventListener("resize", closeSideContainer);
return () => {
window.removeEventListener("resize", closeSideContainer);
};
}, [setSideContainerOpen]);
useEffect(() => {
setSideContainerOpen((prev) => (prev ? false : prev));
}, [setSideContainerOpen]);
return (
<Shell
flexChildrenContainer
{...rest}
SidebarContainer={
<SidebarContainerElement
sideContainerOpen={sideContainerOpen}
setSideContainerOpen={setSideContainerOpen}
teamFeatures={teamFeatures}
permissions={permissions}
/>
}
drawerState={state}
MobileNavigationContainer={null}
TopNavContainer={
<MobileSettingsContainer onSideContainerOpen={() => setSideContainerOpen(!sideContainerOpen)} />
}>
<div className="flex flex-1 *:flex-1">
<div
className={classNames("mx-auto max-w-full justify-center lg:max-w-3xl", rest.containerClassName)}>
<ErrorBoundary>{children}</ErrorBoundary>
</div>
</div>
</Shell>
);
}
type SidebarContainerElementProps = {
sideContainerOpen: boolean;
bannersHeight?: number;
setSideContainerOpen: React.Dispatch<React.SetStateAction<boolean>>;
teamFeatures?: Record<number, TeamFeatures>;
permissions?: SettingsPermissions;
};
const SidebarContainerElement = ({
sideContainerOpen,
bannersHeight,
setSideContainerOpen,
teamFeatures,
permissions,
}: SidebarContainerElementProps) => {
const { t } = useLocale();
return (
<>
{/* Mobile backdrop */}
{sideContainerOpen && (
<button
onClick={() => setSideContainerOpen(false)}
className="fixed top-0 left-0 z-10 h-full w-full bg-black/50">
<span className="sr-only">{t("hide_navigation")}</span>
</button>
)}
<SettingsSidebarContainer
navigationIsOpenedOnMobile={sideContainerOpen}
bannersHeight={bannersHeight}
teamFeatures={teamFeatures}
permissions={permissions}
/>
</>
);
};
export type { SettingsLayoutProps, SettingsPermissions };
export default SettingsLayoutAppDirClient;