import {createContext, useCallback, useContext, useEffect, useState} from 'react'; import {PlainRoute} from 'react-router'; import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes'; import {useRoutes} from 'sentry/utils/useRoutes'; type ExplicitTitleProps = { routes: PlainRoute[]; title: string; }; type PathMap = Record; type Context = { /** * Represents a mapping of paths to breadcrumb names */ pathMap: PathMap; /** * Set's an explicit breadcrumb title at the provided routes path. * * You should call the returned cleanup function to have the explicit title * removed when appropriate (typically on unmount of the route component) */ setExplicitTitle: (update: ExplicitTitleProps) => () => void; }; const BreadcrumbContext = createContext(undefined); type ProviderProps = { children: React.ReactNode; }; function BreadcrumbProvider({children}: ProviderProps) { const routes = useRoutes(); // Maps path strings to breadcrumb names const [pathMap, setPathMap] = useState({}); // The explicit path map is used when we override the name for a specific // route. We keep this separate from the path map which is resolved from the // `routes` match array so that that does not override an explicit title const [explicitPathMap, setExplicitPathMap] = useState({}); // When our routes change update the path mapping useEffect( () => setPathMap(oldPathMap => { const routePath = getRouteStringFromRoutes(routes); const newPathMap = {...oldPathMap}; for (const fullPath in newPathMap) { if (!routePath.startsWith(fullPath)) { delete newPathMap[fullPath]; } } return newPathMap; }), [routes, setPathMap] ); const setExplicitTitle = useCallback( ({routes: updateRoutes, title}: ExplicitTitleProps) => { const key = getRouteStringFromRoutes(updateRoutes); setExplicitPathMap(lastState => ({...lastState, [key]: title})); return () => setExplicitPathMap(lastState => { const {[key]: _removed, ...newExplicitPathMap} = lastState; return newExplicitPathMap; }); }, [setExplicitPathMap] ); const ctx: Context = { pathMap: {...pathMap, ...explicitPathMap}, setExplicitTitle, }; return {children}; } /** * Provides the mapping of paths to breadcrumb titles. * * Outside of the BreadcrumbContext this will return an empty mapping */ function useBreadcrumbsPathmap() { return useContext(BreadcrumbContext)?.pathMap ?? {}; } /** * Used to set the breadcrumb title of the passed route while the current * component is rendererd. * * Is a no-op if used outside of the BreadcrumbContext. */ function useBreadcrumbTitleEffect(props: ExplicitTitleProps) { const context = useContext(BreadcrumbContext); const setExplicitTitle = context?.setExplicitTitle; useEffect(() => setExplicitTitle?.(props), [setExplicitTitle, props]); } export {BreadcrumbProvider, useBreadcrumbsPathmap, useBreadcrumbTitleEffect};