11import { Fragment , useEffect , useRef , type MouseEventHandler } from 'react' ;
22import { css , useTheme } from '@emotion/react' ;
33import styled from '@emotion/styled' ;
4+ import type { LocationDescriptor } from 'history' ;
45
56import type { ButtonProps } from '@sentry/scraps/button' ;
67import { Button } from '@sentry/scraps/button' ;
@@ -11,7 +12,6 @@ import {Tooltip} from '@sentry/scraps/tooltip';
1112import { DropdownMenu , type MenuItemProps } from 'sentry/components/dropdownMenu' ;
1213import { useFrontendVersion } from 'sentry/components/frontendVersionContext' ;
1314import { IconDefaultsProvider } from 'sentry/icons/useIconDefaults' ;
14- import type { Organization } from 'sentry/types/organization' ;
1515import { trackAnalytics } from 'sentry/utils/analytics' ;
1616import normalizeUrl from 'sentry/utils/url/normalizeUrl' ;
1717import { useLocation } from 'sentry/utils/useLocation' ;
@@ -24,51 +24,6 @@ import {useNavigationContext} from 'sentry/views/navigation/context';
2424import { PRIMARY_NAVIGATION_GROUP_CONFIG } from 'sentry/views/navigation/primary/config' ;
2525import type { PrimaryNavigationGroup } from 'sentry/views/navigation/types' ;
2626import { NavigationLayout } from 'sentry/views/navigation/types' ;
27- import { isLinkActive } from 'sentry/views/navigation/utils' ;
28-
29- interface SidebarItemLinkProps {
30- analyticsKey : string ;
31- group : PrimaryNavigationGroup ;
32- to : string ;
33- activeTo ?: string ;
34- analyticsParams ?: Record < string , unknown > ;
35- children ?: React . ReactNode ;
36- }
37-
38- interface SidebarItemDropdownProps {
39- analyticsKey : string ;
40- items : MenuItemProps [ ] ;
41- label : string ;
42- analyticsParams ?: Record < string , unknown > ;
43- children ?: React . ReactNode ;
44- disableTooltip ?: boolean ;
45- icon ?: React . ReactNode ;
46- onOpen ?: MouseEventHandler < HTMLButtonElement > ;
47- size ?: ButtonProps [ 'size' ] ;
48- triggerWrap ?: React . ComponentType < { children : React . ReactNode } > ;
49- }
50-
51- interface SidebarButtonProps {
52- analyticsKey : string ;
53- label : string ;
54- analyticsParams ?: Record < string , unknown > ;
55- buttonProps ?: Omit < ButtonProps , 'aria-label' > ;
56- children ?: React . ReactNode ;
57- className ?: string ;
58- onClick ?: MouseEventHandler < HTMLButtonElement > ;
59- }
60-
61- function recordPrimaryItemClick (
62- analyticsKey : string ,
63- organization : Organization ,
64- analyticsParams ?: Record < string , unknown >
65- ) {
66- trackAnalytics ( 'navigation.primary_item_clicked' , {
67- item : analyticsKey ,
68- organization,
69- ...analyticsParams ,
70- } ) ;
71- }
7227
7328interface SidebarItemProps extends React . HTMLAttributes < HTMLLIElement > {
7429 children : React . ReactNode ;
@@ -105,9 +60,17 @@ function SidebarItem({children, label, disableTooltip, ref, ...props}: SidebarIt
10560 ) ;
10661}
10762
108- // Stable module-level component to avoid remounts when used as `renderWrapAs`
109- function PassthroughWrapper ( { children} : { children : React . ReactNode } ) {
110- return children ;
63+ interface SidebarMenuProps {
64+ analyticsKey : string ;
65+ items : MenuItemProps [ ] ;
66+ label : string ;
67+ analyticsParams ?: Record < string , unknown > ;
68+ children ?: React . ReactNode ;
69+ disableTooltip ?: boolean ;
70+ icon ?: React . ReactNode ;
71+ onOpen ?: MouseEventHandler < HTMLButtonElement > ;
72+ size ?: ButtonProps [ 'size' ] ;
73+ triggerWrap ?: React . ComponentType < { children : React . ReactNode } > ;
11174}
11275
11376export function SidebarMenu ( {
@@ -121,7 +84,7 @@ export function SidebarMenu({
12184 icon,
12285 size,
12386 triggerWrap : TriggerWrap = Fragment ,
124- } : SidebarItemDropdownProps ) {
87+ } : SidebarMenuProps ) {
12588 // This component can be rendered without an organization in some cases
12689 const organization = useOrganization ( { allowNull : true } ) ;
12790 const { layout} = useNavigationContext ( ) ;
@@ -160,7 +123,11 @@ export function SidebarMenu({
160123 size = { size }
161124 onClick = { event => {
162125 if ( organization ) {
163- recordPrimaryItemClick ( analyticsKey , organization , analyticsParams ) ;
126+ trackAnalytics ( 'navigation.primary_item_clicked' , {
127+ item : analyticsKey ,
128+ organization,
129+ ...analyticsParams ,
130+ } ) ;
164131 }
165132 triggerProps . onClick ?.( event ) ;
166133 onOpen ?.( event ) ;
@@ -179,6 +146,41 @@ export function SidebarMenu({
179146 ) ;
180147}
181148
149+ interface SidebarItemLinkProps {
150+ analyticsKey : string ;
151+ group : PrimaryNavigationGroup ;
152+ to : string ;
153+ activeTo ?: string ;
154+ analyticsParams ?: Record < string , unknown > ;
155+ children ?: React . ReactNode ;
156+ }
157+
158+ export function SidebarLink ( {
159+ children,
160+ to,
161+ activeTo = to ,
162+ analyticsKey,
163+ analyticsParams,
164+ group,
165+ ...props
166+ } : SidebarItemLinkProps ) {
167+ const label = PRIMARY_NAVIGATION_GROUP_CONFIG [ group ] . label ;
168+
169+ return (
170+ < SidebarItem label = { label } { ...props } >
171+ < SidebarNavigationLink
172+ to = { to }
173+ activeTo = { activeTo }
174+ analyticsKey = { analyticsKey }
175+ analyticsParams = { analyticsParams }
176+ group = { group }
177+ >
178+ { children }
179+ </ SidebarNavigationLink >
180+ </ SidebarItem >
181+ ) ;
182+ }
183+
182184function SidebarNavigationLink ( {
183185 children,
184186 to,
@@ -190,7 +192,10 @@ function SidebarNavigationLink({
190192 const organization = useOrganization ( ) ;
191193 const { layout, activePrimaryNavigationGroup} = useNavigationContext ( ) ;
192194 const location = useLocation ( ) ;
193- const isActive = isLinkActive ( normalizeUrl ( activeTo , location ) , location . pathname ) ;
195+ const isActive = isSidebarLinkActive (
196+ normalizeUrl ( activeTo , location ) ,
197+ location . pathname
198+ ) ;
194199 const label = PRIMARY_NAVIGATION_GROUP_CONFIG [ group ] . label ;
195200
196201 // Reload the page when the frontend is stale to ensure users get the latest version
@@ -205,7 +210,11 @@ function SidebarNavigationLink({
205210 aria-current = { isActive ? 'page' : undefined }
206211 isMobile = { layout === NavigationLayout . MOBILE }
207212 onClick = { ( ) => {
208- recordPrimaryItemClick ( analyticsKey , organization , analyticsParams ) ;
213+ trackAnalytics ( 'navigation.primary_item_clicked' , {
214+ item : analyticsKey ,
215+ organization,
216+ ...analyticsParams ,
217+ } ) ;
209218 } }
210219 { ...{
211220 [ NAVIGATION_PRIMARY_LINK_DATA_ATTRIBUTE ] : true ,
@@ -226,30 +235,14 @@ function SidebarNavigationLink({
226235 ) ;
227236}
228237
229- export function SidebarLink ( {
230- children,
231- to,
232- activeTo = to ,
233- analyticsKey,
234- analyticsParams,
235- group,
236- ...props
237- } : SidebarItemLinkProps ) {
238- const label = PRIMARY_NAVIGATION_GROUP_CONFIG [ group ] . label ;
239-
240- return (
241- < SidebarItem label = { label } { ...props } >
242- < SidebarNavigationLink
243- to = { to }
244- activeTo = { activeTo }
245- analyticsKey = { analyticsKey }
246- analyticsParams = { analyticsParams }
247- group = { group }
248- >
249- { children }
250- </ SidebarNavigationLink >
251- </ SidebarItem >
252- ) ;
238+ interface SidebarButtonProps {
239+ analyticsKey : string ;
240+ label : string ;
241+ analyticsParams ?: Record < string , unknown > ;
242+ buttonProps ?: Omit < ButtonProps , 'aria-label' > ;
243+ children ?: React . ReactNode ;
244+ className ?: string ;
245+ onClick ?: MouseEventHandler < HTMLButtonElement > ;
253246}
254247
255248export function SidebarButton ( {
@@ -274,7 +267,11 @@ export function SidebarButton({
274267 className = { className }
275268 aria-label = { showLabel ? undefined : label }
276269 onClick = { ( e : React . MouseEvent < HTMLButtonElement > ) => {
277- recordPrimaryItemClick ( analyticsKey , organization , analyticsParams ) ;
270+ trackAnalytics ( 'navigation.primary_item_clicked' , {
271+ item : analyticsKey ,
272+ organization,
273+ ...analyticsParams ,
274+ } ) ;
278275 buttonProps . onClick ?.( e ) ;
279276 onClick ?.( e ) ;
280277 } }
@@ -517,3 +514,22 @@ export const SidebarList = styled('ul')<{isMobile: boolean; compact?: boolean}>`
517514 width: 100%;
518515 }
519516` ;
517+
518+ export function isSidebarLinkActive (
519+ to : LocationDescriptor | string ,
520+ pathname : string ,
521+ options : { end ?: boolean } = { end : false }
522+ ) : boolean {
523+ const toPathname = normalizeUrl ( typeof to === 'string' ? to : ( to . pathname ?? '/' ) ) ;
524+
525+ if ( options . end ) {
526+ return pathname === toPathname ;
527+ }
528+
529+ return pathname . startsWith ( toPathname ) ;
530+ }
531+
532+ // Stable module-level component to avoid remounts when used as `renderWrapAs`
533+ function PassthroughWrapper ( { children} : { children : React . ReactNode } ) {
534+ return children ;
535+ }
0 commit comments