diff --git a/messages/en-US.json b/messages/en-US.json index b0035f81..b782bf9d 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1155,7 +1155,7 @@ "sidebarUsers": "Users", "sidebarInvitations": "Invitations", "sidebarRoles": "Roles", - "sidebarShareableLinks": "Shareable Links", + "sidebarShareableLinks": "Links", "sidebarApiKeys": "API Keys", "sidebarSettings": "Settings", "sidebarAllUsers": "All Users", diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index e3478fa1..6941b39b 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -88,19 +88,25 @@ export const orgNavSections = ( items: [ { title: "sidebarUsers", - href: "/{orgId}/settings/access/users", - icon: + icon: , + items: [ + { + title: "sidebarUsers", + href: "/{orgId}/settings/access/users", + icon: + }, + { + title: "sidebarInvitations", + href: "/{orgId}/settings/access/invitations", + icon: + } + ] }, { title: "sidebarRoles", href: "/{orgId}/settings/access/roles", icon: }, - { - title: "sidebarInvitations", - href: "/{orgId}/settings/access/invitations", - icon: - }, ...(build == "saas" ? [ { @@ -122,24 +128,30 @@ export const orgNavSections = ( heading: "Analytics", items: [ { - title: "sidebarLogsRequest", - href: "/{orgId}/settings/logs/request", - icon: - }, - ...(build != "oss" - ? [ - { - title: "sidebarLogsAccess", - href: "/{orgId}/settings/logs/access", - icon: - }, - { - title: "sidebarLogsAction", - href: "/{orgId}/settings/logs/action", - icon: - } - ] - : []) + title: "sidebarLogs", + icon: , + items: [ + { + title: "sidebarLogsRequest", + href: "/{orgId}/settings/logs/request", + icon: + }, + ...(build != "oss" + ? [ + { + title: "sidebarLogsAccess", + href: "/{orgId}/settings/logs/access", + icon: + }, + { + title: "sidebarLogsAction", + href: "/{orgId}/settings/logs/action", + icon: + } + ] + : []) + ] + } ] }, { diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index 7aaebfff..fb1f0044 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -14,14 +14,26 @@ import { TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from "@app/components/ui/collapsible"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; +import { ChevronDown } from "lucide-react"; import { build } from "@server/build"; export type SidebarNavItem = { - href: string; + href?: string; title: string; icon?: React.ReactNode; showEE?: boolean; isBeta?: boolean; + items?: SidebarNavItem[]; }; export type SidebarNavSection = { @@ -36,6 +48,102 @@ export interface SidebarNavProps extends React.HTMLAttributes { isCollapsed?: boolean; } +type CollapsibleNavItemProps = { + item: SidebarNavItem; + level: number; + isChildActive: boolean; + isDisabled: boolean; + isCollapsed: boolean; + renderNavItem: (item: SidebarNavItem, level: number) => React.ReactNode; + t: (key: string) => string; + build: string; + isUnlocked: () => boolean; +}; + +function CollapsibleNavItem({ + item, + level, + isChildActive, + isDisabled, + isCollapsed, + renderNavItem, + t, + build, + isUnlocked +}: CollapsibleNavItemProps) { + const [isOpen, setIsOpen] = React.useState(isChildActive); + + // Update open state when child active state changes + React.useEffect(() => { + if (isChildActive) { + setIsOpen(true); + } + }, [isChildActive]); + + return ( + + + + + +
+ {item.items!.map((childItem) => + renderNavItem(childItem, level + 1) + )} +
+
+
+ ); +} + export function SidebarNav({ className, sections, @@ -56,7 +164,8 @@ export function SidebarNav({ const { user } = useUserContext(); const t = useTranslations(); - function hydrateHref(val: string): string { + function hydrateHref(val?: string): string | undefined { + if (!val) return undefined; return val .replace("{orgId}", orgId) .replace("{niceId}", niceId) @@ -66,18 +175,53 @@ export function SidebarNav({ .replace("{clientId}", clientId); } + function isItemOrChildActive(item: SidebarNavItem): boolean { + const hydratedHref = hydrateHref(item.href); + if (hydratedHref && pathname.startsWith(hydratedHref)) { + return true; + } + if (item.items) { + return item.items.some((child) => isItemOrChildActive(child)); + } + return false; + } + const renderNavItem = ( item: SidebarNavItem, - hydratedHref: string, - isActive: boolean, - isDisabled: boolean - ) => { + level: number = 0 + ): React.ReactNode => { + const hydratedHref = hydrateHref(item.href); + const hasNestedItems = item.items && item.items.length > 0; + const isActive = hydratedHref ? pathname.startsWith(hydratedHref) : false; + const isChildActive = hasNestedItems ? isItemOrChildActive(item) : false; + const isEE = + build === "enterprise" && item.showEE && !isUnlocked(); + const isDisabled = disabled || isEE; const tooltipText = item.showEE && !isUnlocked() ? `${t(item.title)} (${t("licenseBadge")})` : t(item.title); - const itemContent = ( + // If item has nested items, render as collapsible + if (hasNestedItems && !isCollapsed) { + return ( + + ); + } + + // Regular item without nested items + const itemContent = hydratedHref ? ( - {t(item.title)} - {item.isBeta && ( - - {t("beta")} - - )} - {build === "enterprise" && - item.showEE && - !isUnlocked() && ( + {t(item.title)} +
+ {item.isBeta && ( - {t("licenseBadge")} + {t("beta")} )} + {build === "enterprise" && + item.showEE && + !isUnlocked() && ( + + {t("licenseBadge")} + + )} +
)} + ) : ( +
+ {item.icon && ( + {item.icon} + )} + {t(item.title)} +
+ {item.isBeta && ( + + {t("beta")} + + )} + {build === "enterprise" && + item.showEE && + !isUnlocked() && ( + + {t("licenseBadge")} + + )} +
+
); if (isCollapsed) { + // If item has nested items, show popover instead of tooltip + if (hasNestedItems) { + return ( + + + + + +
+ {item.items!.map((childItem) => { + const childHydratedHref = hydrateHref( + childItem.href + ); + const childIsActive = childHydratedHref + ? pathname.startsWith( + childHydratedHref + ) + : false; + const childIsEE = + build === "enterprise" && + childItem.showEE && + !isUnlocked(); + const childIsDisabled = + disabled || childIsEE; + + if (!childHydratedHref) { + return null; + } + + return ( + { + if (childIsDisabled) { + e.preventDefault(); + } else if (onItemClick) { + onItemClick(); + } + }} + > + {childItem.icon && ( + + {childItem.icon} + + )} + + {t(childItem.title)} + + {childItem.isBeta && ( + + {t("beta")} + + )} + {build === "enterprise" && + childItem.showEE && + !isUnlocked() && ( + + {t("licenseBadge")} + + )} + + ); + })} +
+
+
+ ); + } + + // Regular item without nested items - show tooltip return ( - + {itemContent} @@ -145,7 +426,7 @@ export function SidebarNav({ } return ( - {itemContent} + {itemContent} ); }; @@ -161,26 +442,12 @@ export function SidebarNav({ {sections.map((section) => (
{!isCollapsed && ( -
+
{section.heading}
)}
- {section.items.map((item) => { - const hydratedHref = hydrateHref(item.href); - const isActive = pathname.startsWith(hydratedHref); - const isEE = - build === "enterprise" && - item.showEE && - !isUnlocked(); - const isDisabled = disabled || isEE; - return renderNavItem( - item, - hydratedHref, - isActive, - isDisabled || false - ); - })} + {section.items.map((item) => renderNavItem(item, 0))}
))}