feat: Add action‑item support to mobile “More” page and added navigation to refer page (#26431)

* Add action‑item support to mobile “More” page and hide arrow on copy‑link button

* updated code

* fix code

* updated code

* remove unused import and formatting

---------

Co-authored-by: Dhairyashil <dhairyashil10101010@gmail.com>
This commit is contained in:
Abhay Mishra
2026-02-07 13:57:24 +00:00
committed by GitHub
co-authored by Dhairyashil
parent 0beadfa80b
commit 0b0a5478fb
3 changed files with 120 additions and 94 deletions
@@ -6,6 +6,7 @@ import {
useOrgBranding,
type OrganizationBranding,
} from "@calcom/features/ee/organizations/context/provider";
import { useMobileMoreItems } from "./useMobileMoreItems";
import { useIsStandalone } from "@calcom/lib/hooks/useIsStandalone";
import classNames from "@calcom/ui/classNames";
import { useHasPaidPlan } from "@calcom/web/modules/billing/hooks/useHasPaidPlan";
@@ -14,7 +15,11 @@ import UnconfirmedBookingBadge from "../../bookings/components/UnconfirmedBookin
import { KBarTrigger } from "../Kbar";
import { TeamInviteBadge } from "../TeamInviteBadge";
import type { NavigationItemType } from "./NavigationItem";
import { NavigationItem, MobileNavigationItem, MobileNavigationMoreItem } from "./NavigationItem";
import {
NavigationItem,
MobileNavigationItem,
MobileNavigationMoreItem,
} from "./NavigationItem";
export const MORE_SEPARATOR_NAME = "more";
@@ -63,7 +68,10 @@ const getNavigationItems = (
moreOnMobile: true,
isCurrent: ({ pathname: path, item }) => {
// During Server rendering path is /v2/apps but on client it becomes /apps(weird..)
return (path?.startsWith(item.href) ?? false) && !(path?.includes("routing-forms/") ?? false);
return (
(path?.startsWith(item.href) ?? false) &&
!(path?.includes("routing-forms/") ?? false)
);
},
child: [
{
@@ -109,7 +117,8 @@ const getNavigationItems = (
name: "insights",
href: "/insights",
icon: "chart-bar",
isCurrent: ({ pathname: path, item }) => path?.startsWith(item.href) ?? false,
isCurrent: ({ pathname: path, item }) =>
path?.startsWith(item.href) ?? false,
moreOnMobile: true,
child: hasAllInsightsAccess
? [
@@ -121,18 +130,21 @@ const getNavigationItems = (
{
name: "routing",
href: "/insights/routing",
isCurrent: ({ pathname: path }) => path?.startsWith("/insights/routing") ?? false,
isCurrent: ({ pathname: path }) =>
path?.startsWith("/insights/routing") ?? false,
},
{
name: "router_position",
href: "/insights/router-position",
isCurrent: ({ pathname: path }) => path?.startsWith("/insights/router-position") ?? false,
isCurrent: ({ pathname: path }) =>
path?.startsWith("/insights/router-position") ?? false,
},
{
name: "call_history",
href: "/insights/call-history",
// icon: "phone",
isCurrent: ({ pathname: path }) => path?.startsWith("/insights/call-history") ?? false,
isCurrent: ({ pathname: path }) =>
path?.startsWith("/insights/call-history") ?? false,
},
]
: [
@@ -205,19 +217,34 @@ const useNavigationItems = (isPlatformNavigation = false) => {
? getNavigationItems(orgBranding, hasAllInsightsAccess)
: platformNavigationItems;
const desktopNavigationItems = items.filter((item) => item.name !== MORE_SEPARATOR_NAME);
const desktopNavigationItems = items.filter(
(item) => item.name !== MORE_SEPARATOR_NAME
);
const mobileNavigationBottomItems = items.filter(
(item) => (!item.moreOnMobile && !item.onlyDesktop) || item.name === MORE_SEPARATOR_NAME
(item) =>
(!item.moreOnMobile && !item.onlyDesktop) ||
item.name === MORE_SEPARATOR_NAME
);
const mobileNavigationMoreItems = items.filter(
(item) => item.moreOnMobile && !item.onlyDesktop && item.name !== MORE_SEPARATOR_NAME
(item) =>
item.moreOnMobile &&
!item.onlyDesktop &&
item.name !== MORE_SEPARATOR_NAME
);
return { desktopNavigationItems, mobileNavigationBottomItems, mobileNavigationMoreItems };
return {
desktopNavigationItems,
mobileNavigationBottomItems,
mobileNavigationMoreItems,
};
}, [hasPaidPlan, isPending, isPlatformNavigation, orgBranding]);
};
export const Navigation = ({ isPlatformNavigation = false }: { isPlatformNavigation?: boolean }) => {
export const Navigation = ({
isPlatformNavigation = false,
}: {
isPlatformNavigation?: boolean;
}) => {
const { desktopNavigationItems } = useNavigationItems(isPlatformNavigation);
return (
@@ -243,9 +270,14 @@ export function MobileNavigationContainer({
return <MobileNavigation isPlatformNavigation={isPlatformNavigation} />;
}
const MobileNavigation = ({ isPlatformNavigation = false }: { isPlatformNavigation?: boolean }) => {
const MobileNavigation = ({
isPlatformNavigation = false,
}: {
isPlatformNavigation?: boolean;
}) => {
const isEmbed = useIsEmbed();
const { mobileNavigationBottomItems } = useNavigationItems(isPlatformNavigation);
const { mobileNavigationBottomItems } =
useNavigationItems(isPlatformNavigation);
return (
<>
@@ -253,7 +285,8 @@ const MobileNavigation = ({ isPlatformNavigation = false }: { isPlatformNavigati
className={classNames(
"pwa:pb-[max(0.25rem,env(safe-area-inset-bottom))] pwa:-mx-2 bg-cal-muted/40 border-subtle fixed bottom-0 left-0 z-30 flex w-full border-t px-1 shadow backdrop-blur-md md:hidden",
isEmbed && "hidden"
)}>
)}
>
{mobileNavigationBottomItems.map((item) => (
<MobileNavigationItem key={item.name} item={item} />
))}
@@ -266,10 +299,13 @@ const MobileNavigation = ({ isPlatformNavigation = false }: { isPlatformNavigati
export const MobileNavigationMoreItems = () => {
const { mobileNavigationMoreItems } = useNavigationItems();
const bottomItems = useMobileMoreItems();
const allItems = [...mobileNavigationMoreItems, ...bottomItems];
return (
<ul className="border-subtle mt-2 rounded-md border">
{mobileNavigationMoreItems.map((item) => (
{allItems.map((item) => (
<MobileNavigationMoreItem key={item.name} item={item} />
))}
</ul>
@@ -27,10 +27,7 @@ const usePersistedExpansionState = (itemName: string) => {
const setPersistedExpansion = (expanded: boolean) => {
setIsExpanded(expanded);
sessionStorage.setItem(
`nav-expansion-${itemName}`,
JSON.stringify(expanded)
);
sessionStorage.setItem(`nav-expansion-${itemName}`, JSON.stringify(expanded));
};
return [isExpanded, setPersistedExpansion] as const;
@@ -67,16 +64,8 @@ export type NavigationItemType = {
}) => boolean;
};
const defaultIsCurrent: NavigationItemType["isCurrent"] = ({
isChild,
item,
pathname,
}) => {
return isChild
? item.href === pathname
: item.href
? pathname?.startsWith(item.href) ?? false
: false;
const defaultIsCurrent: NavigationItemType["isCurrent"] = ({ isChild, item, pathname }) => {
return isChild ? item.href === pathname : item.href ? (pathname?.startsWith(item.href) ?? false) : false;
};
export const NavigationItem: React.FC<{
@@ -87,12 +76,9 @@ export const NavigationItem: React.FC<{
const { item, isChild } = props;
const { t, isLocaleReady } = useLocale();
const pathname = usePathname();
const isCurrent: NavigationItemType["isCurrent"] =
item.isCurrent || defaultIsCurrent;
const isCurrent: NavigationItemType["isCurrent"] = item.isCurrent || defaultIsCurrent;
const current = isCurrent({ isChild: !!isChild, item, pathname });
const shouldDisplayNavigationItem = useShouldDisplayNavigationItem(
props.item
);
const shouldDisplayNavigationItem = useShouldDisplayNavigationItem(props.item);
const [isExpanded, setIsExpanded] = usePersistedExpansionState(item.name);
const isTablet = useMediaQuery("(max-width: 1024px)");
@@ -102,12 +88,8 @@ export const NavigationItem: React.FC<{
const hasChildren = item.child && item.child.length > 0;
const hasActiveChild =
hasChildren &&
item.child?.some((child) =>
isCurrent({ isChild: true, item: child, pathname })
);
const shouldShowChildren =
isExpanded || hasActiveChild || isCurrent({ pathname, isChild, item });
hasChildren && item.child?.some((child) => isCurrent({ isChild: true, item: child, pathname }));
const shouldShowChildren = isExpanded || hasActiveChild || isCurrent({ pathname, isChild, item });
const shouldShowChevron = hasChildren && !hasActiveChild;
const isParentNavigationItem = hasChildren && !isChild;
@@ -151,8 +133,7 @@ export const NavigationItem: React.FC<{
childIsCurrent
? "bg-emphasis text-white"
: "hover:bg-emphasis text-mute hover:text-emphasis"
)}
>
)}>
{t(childItem.name)}
</Link>
);
@@ -163,8 +144,7 @@ export const NavigationItem: React.FC<{
t(item.name)
)
}
className="lg:hidden"
>
className="lg:hidden">
<button
data-test-id={item.name}
aria-label={t(item.name)}
@@ -185,8 +165,7 @@ export const NavigationItem: React.FC<{
isLocaleReady
? "hover:bg-subtle todesktop:[&[aria-current='page']]:bg-emphasis todesktop:hover:bg-transparent hover:text-emphasis"
: ""
)}
>
)}>
{item.icon && (
<div className="relative">
<Icon
@@ -208,8 +187,7 @@ export const NavigationItem: React.FC<{
{isLocaleReady ? (
<span
className="hidden w-full justify-between truncate text-ellipsis lg:flex"
data-testid={`${item.name}-test`}
>
data-testid={`${item.name}-test`}>
{t(item.name)}
{item.badge && item.badge}
</span>
@@ -239,17 +217,14 @@ export const NavigationItem: React.FC<{
: `[&[aria-current='page']]:bg-emphasis`,
isChild
? `[&[aria-current='page']]:text-emphasis [&[aria-current='page']]:bg-emphasis hidden h-8 pl-16 lg:flex lg:pl-11 ${
props.index === 0
? "mt-0"
: "mt-1 hover:mt-1 [&[aria-current='page']]:mt-1"
props.index === 0 ? "mt-0" : "mt-1 hover:mt-1 [&[aria-current='page']]:mt-1"
}`
: "[&[aria-current='page']]:text-emphasis mt-0.5 text-sm md:justify-center lg:justify-start",
isLocaleReady
? "hover:bg-subtle todesktop:[&[aria-current='page']]:bg-emphasis todesktop:hover:bg-transparent hover:text-emphasis"
: ""
)}
aria-current={current ? "page" : undefined}
>
aria-current={current ? "page" : undefined}>
{item.icon && (
<Icon
name={item.isLoading ? "rotate-cw" : item.icon}
@@ -264,8 +239,7 @@ export const NavigationItem: React.FC<{
{isLocaleReady ? (
<span
className="hidden w-full justify-between truncate text-ellipsis lg:flex"
data-testid={`${item.name}-test`}
>
data-testid={`${item.name}-test`}>
{t(item.name)}
{item.badge && item.badge}
</span>
@@ -290,6 +264,7 @@ export const NavigationItem: React.FC<{
</div>
)}
</Fragment>
);
};
@@ -300,12 +275,9 @@ export const MobileNavigationItem: React.FC<{
const { item, isChild } = props;
const pathname = usePathname();
const { t, isLocaleReady } = useLocale();
const isCurrent: NavigationItemType["isCurrent"] =
item.isCurrent || defaultIsCurrent;
const isCurrent: NavigationItemType["isCurrent"] = item.isCurrent || defaultIsCurrent;
const current = isCurrent({ isChild: !!isChild, item, pathname });
const shouldDisplayNavigationItem = useShouldDisplayNavigationItem(
props.item
);
const shouldDisplayNavigationItem = useShouldDisplayNavigationItem(props.item);
if (!shouldDisplayNavigationItem) return null;
return (
@@ -314,8 +286,7 @@ export const MobileNavigationItem: React.FC<{
href={item.href}
target={item.target}
className="[&[aria-current='page']]:text-emphasis hover:text-default text-muted bg-transparent! relative my-2 min-w-0 flex-1 overflow-hidden rounded-md p-1 text-center text-xs font-medium focus:z-10 sm:text-sm"
aria-current={current ? "page" : undefined}
>
aria-current={current ? "page" : undefined}>
{item.badge && <div className="absolute right-1 top-1">{item.badge}</div>}
{item.icon && (
<Icon
@@ -325,11 +296,7 @@ export const MobileNavigationItem: React.FC<{
aria-current={current ? "page" : undefined}
/>
)}
{isLocaleReady ? (
<span className="block truncate">{t(item.name)}</span>
) : (
<SkeletonText />
)}
{isLocaleReady ? <span className="block truncate">{t(item.name)}</span> : <SkeletonText />}
</Link>
);
};
@@ -340,14 +307,25 @@ export const MobileNavigationMoreItem: React.FC<{
}> = (props) => {
const { item } = props;
const { t, isLocaleReady } = useLocale();
const shouldDisplayNavigationItem = useShouldDisplayNavigationItem(
props.item
);
const shouldDisplayNavigationItem = useShouldDisplayNavigationItem(props.item);
const [isExpanded, setIsExpanded] = usePersistedExpansionState(item.name);
if (!shouldDisplayNavigationItem) return null;
const hasChildren = item.child && item.child.length > 0;
const isActionItem = !item.href && item.onClick;
const itemContent = (
<>
<span className="text-default flex items-center font-semibold ">
{item.icon && (
<Icon name={item.icon} className="h-5 w-5 shrink-0 ltr:mr-3 rtl:ml-3" aria-hidden="true" />
)}
{isLocaleReady ? t(item.name) : <SkeletonText />}
</span>
{!isActionItem && <Icon name="arrow-right" className="text-subtle h-5 w-5" />}
</>
);
return (
<li className="border-subtle border-b last:border-b-0" key={item.name}>
@@ -355,22 +333,14 @@ export const MobileNavigationMoreItem: React.FC<{
<>
<button
onClick={() => setIsExpanded(!isExpanded)}
className="hover:bg-subtle flex w-full items-center justify-between p-5 text-left transition"
>
className="hover:bg-subtle flex w-full items-center justify-between p-5 text-left transition">
<span className="text-default flex items-center font-semibold">
{item.icon && (
<Icon
name={item.icon}
className="h-5 w-5 shrink-0 ltr:mr-3 rtl:ml-3"
aria-hidden="true"
/>
<Icon name={item.icon} className="h-5 w-5 shrink-0 ltr:mr-3 rtl:ml-3" aria-hidden="true" />
)}
{isLocaleReady ? t(item.name) : <SkeletonText />}
</span>
<Icon
name={isExpanded ? "chevron-up" : "chevron-down"}
className="text-subtle h-5 w-5"
/>
<Icon name={isExpanded ? "chevron-up" : "chevron-down"} className="text-subtle h-5 w-5" />
</button>
<div
className={classNames(
@@ -396,23 +366,20 @@ export const MobileNavigationMoreItem: React.FC<{
)}
</div>
</div>
</>
) : isActionItem ? (
<button
onClick={item.onClick}
className="hover:bg-subtle flex w-full items-center justify-between p-5 text-left transition">
{itemContent}
</button>
) : (
<Link
href={item.href}
className="hover:bg-subtle flex items-center justify-between p-5 transition"
>
<span className="text-default flex items-center font-semibold ">
{item.icon && (
<Icon
name={item.icon}
className="h-5 w-5 shrink-0 ltr:mr-3 rtl:ml-3"
aria-hidden="true"
/>
)}
{isLocaleReady ? t(item.name) : <SkeletonText />}
</span>
<Icon name="arrow-right" className="text-subtle h-5 w-5" />
target={item.target}
className="hover:bg-subtle flex items-center justify-between p-5 transition">
{itemContent}
</Link>
)}
</li>
@@ -0,0 +1,23 @@
import { getBookerBaseUrlSync } from "@calcom/features/ee/organizations/lib/getBookerBaseUrlSync";
import { useBottomNavItems } from "../useBottomNavItems";
import { UserPermissionRole } from "@calcom/prisma/enums";
import type { NavigationItemType } from "./NavigationItem";
import { useSession } from "next-auth/react";
export function useMobileMoreItems(): NavigationItemType[] {
const { data: session } = useSession();
const user = session?.user;
const isAdmin = user?.role === UserPermissionRole.ADMIN;
const publicPageUrl = `${getBookerBaseUrlSync(user?.org?.slug ?? null)}/${user?.orgAwareUsername ?? user?.username}`;
const bottomNavItems = useBottomNavItems({
publicPageUrl,
isAdmin,
user,
});
const filteredBottomNavItems = bottomNavItems.filter(
(item: NavigationItemType) => item.name !== "settings"
);
return filteredBottomNavItems;
}