From 7b8c3bb2b05b5559aa9aa3e7fe059410e40b7599 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Sun, 7 Jul 2024 17:12:53 -0600 Subject: [PATCH] misc routing touch-up --- .tool-versions | 2 +- src/context/GlobalContext.tsx | 11 ++- src/context/Router.tsx | 115 +++++++++++++----------- src/layout/sidebar/SideBarWorkspace.tsx | 11 +-- src/lib/routing.ts | 23 +++-- 5 files changed, 92 insertions(+), 70 deletions(-) diff --git a/.tool-versions b/.tool-versions index ae96860..1825d0e 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -nodejs 16.20.2 +nodejs 16.13.0 diff --git a/src/context/GlobalContext.tsx b/src/context/GlobalContext.tsx index fa83b7f..38fb1cd 100644 --- a/src/context/GlobalContext.tsx +++ b/src/context/GlobalContext.tsx @@ -4,6 +4,7 @@ import * as storage from "../lib/storage"; import { HistoricResponse, Route, Workspace } from "../types"; import { useConfigContext } from "./ConfigContext"; +import { routeHref } from "../lib/routing"; export const Context = React.createContext({ workspaces: [] as Workspace[], @@ -29,6 +30,7 @@ export const Context = React.createContext({ }, activeRoute: null as null | Route, + activeHref: "", setActiveRoute: (activeRoute: Route | null) => { // noop }, @@ -47,7 +49,7 @@ export const Context = React.createContext({ export const useGlobalContext = () => useContext(Context); export function ContextProvider({ children }: { children: React.ReactNode }) { - const { workspaces } = useConfigContext(); + const { workspaces, basename } = useConfigContext(); const [activeRoute, setActiveRoute] = useState(null); const [keywords, setKeywordsInState] = useState( @@ -60,6 +62,11 @@ export function ContextProvider({ children }: { children: React.ReactNode }) { () => storage.get(storage.keys.darkMode) || false ); + const activeHref = useMemo( + () => routeHref(activeRoute, basename), + [activeRoute, basename] + ); + // Used to track the state of a Request for a particular Route before it becomes a HistoricResponse // after the Request is issued. This allows the user to flip back and forth between routes and still have // Params stay loaded. @@ -210,6 +217,7 @@ export function ContextProvider({ children }: { children: React.ReactNode }) { partialRequestResponses, setPartialRequestResponse, activeRoute, + activeHref, setActiveRoute, keywords, setKeywords, @@ -219,6 +227,7 @@ export function ContextProvider({ children }: { children: React.ReactNode }) { }), [ activeRoute, + activeHref, collapsedWorkspaces, darkMode, hideDeprecatedRoutes, diff --git a/src/context/Router.tsx b/src/context/Router.tsx index df821a5..6c10513 100644 --- a/src/context/Router.tsx +++ b/src/context/Router.tsx @@ -1,4 +1,11 @@ -import React, { ReactNode, useCallback, useEffect, useMemo } from "react"; +import React, { + AnchorHTMLAttributes, + ReactNode, + useCallback, + useEffect, + useMemo, + useRef, +} from "react"; import { Layout } from "../layout/Layout"; @@ -39,8 +46,6 @@ export function Router() { const handlePathChange = (): void => { const params = extractEndpointParams(location.pathname, basename ?? ""); - console.log("handlePathChange", params); - if (params) { const route = lookupRoute( params.workspaceId, @@ -70,76 +75,86 @@ export function Router() { ); } -export function useNavigate() { +export type NavigateFn = ( + route: Route | null, + replace?: boolean | undefined +) => void; + +export function useNavigate(): NavigateFn { const { basename } = useConfigContext(); - const { setActiveRoute } = useGlobalContext(); + const { activeHref, setActiveRoute } = useGlobalContext(); + const activeHrefRef = useRef(activeHref); + + activeHrefRef.current = activeHref; return useCallback( - (route: Route | null): void => { - if (route) { - history.pushState( - null, - "", - `${basename ?? ""}${routeHref( - route.workspaceId as string, - route.method, - route.path, - route.name - )}` - ); + (route, replace): void => { + const href = routeHref(route, basename); + + const push = + replace === undefined ? href !== activeHrefRef.current : !replace; + + if (push) { + history.pushState({}, "", href); } else { - history.pushState(null, "", basename ?? ""); + history.replaceState({}, "", href); } + [ + window, + ...Array.from(document.querySelectorAll(".overflow-y-scroll")), + ].forEach((scrollContainer) => { + scrollContainer.scrollTo(0, 0); + }); + setActiveRoute(route); }, [basename, setActiveRoute] ); } -type NavLinkProps = { - className: string | ((props: { isActive: boolean }) => string); +type NavLinkProps = Omit, "href"> & { to: Route | null; + replace?: boolean | undefined; children: ReactNode; + className: string; + activeClassName?: string | undefined; }; -export function NavLink({ className, to, children }: NavLinkProps) { +export function NavLink({ + to, + children, + className, + activeClassName, + onClick, + replace, + ...props +}: NavLinkProps) { const { basename } = useConfigContext(); - const { activeRoute } = useGlobalContext(); - const activeHref = useMemo(() => { - if (!activeRoute) { - return "/"; - } - - return routeHref( - activeRoute.workspaceId as string, - activeRoute.method, - activeRoute.path, - activeRoute.name - ); - }, [activeRoute]); - const linkHref = useMemo(() => { - if (!to) { - return "/"; - } - - return routeHref(to.workspaceId as string, to.method, to.path, to.name); - }, [to]); - + const { activeHref } = useGlobalContext(); + const linkHref = useMemo(() => routeHref(to, basename), [to, basename]); const navigate = useNavigate(); return ( { - event.preventDefault(); - navigate(to); + onClick?.(event); + + if ( + !event.isDefaultPrevented() && + (!props.target || props.target === "_self") && + !event.metaKey && + !event.ctrlKey + ) { + event.preventDefault(); + navigate(to, replace); + } }} + {...props} > {children} diff --git a/src/layout/sidebar/SideBarWorkspace.tsx b/src/layout/sidebar/SideBarWorkspace.tsx index c58e775..6a59445 100644 --- a/src/layout/sidebar/SideBarWorkspace.tsx +++ b/src/layout/sidebar/SideBarWorkspace.tsx @@ -83,15 +83,8 @@ export function SideBarWorkspace({ routes.map((route, idx) => ( - `block p-2 cursor-pointer border-b border-gray-300 ${ - styles.sidebarRouteText - }${ - isActive - ? ` ${darkMode ? "bg-purple-700" : "bg-purple-300"}` - : "" - }` - } + className={`block p-2 cursor-pointer border-b border-gray-300 ${styles.sidebarRouteText}`} + activeClassName={darkMode ? "bg-purple-700" : "bg-purple-300"} to={route} >
diff --git a/src/lib/routing.ts b/src/lib/routing.ts index fe694fe..60f95ad 100644 --- a/src/lib/routing.ts +++ b/src/lib/routing.ts @@ -11,7 +11,7 @@ type RouteLookupFn = ( export function routeLookupFactory(workspaces: Workspace[]): RouteLookupFn { const allRoutes = workspaces.reduce((acc, workspace) => { for (const route of workspace.routes) { - const key = routeHref(workspace.id, route.method, route.path, route.name); + const key = routeHref(route, ""); if (acc[key]) { console.warn("Duplicate route config:", acc[key], route); @@ -24,20 +24,25 @@ export function routeLookupFactory(workspaces: Workspace[]): RouteLookupFn { }, {} as Record); return (workspaceId, method, path, name) => { - const key = routeHref(workspaceId, method, path, name); + const key = routeHref({ workspaceId, method, path, name }, ""); return allRoutes[key] ?? undefined; }; } +type RouteHrefConfig = Pick; + export function routeHref( - workspaceId: string, - method: string | undefined, - path: string, - name: string + route: RouteHrefConfig | null, + basename: string | undefined ): string { - return `/${workspaceId}/${method?.toLowerCase() ?? "get"}/${ - path.startsWith("/") ? path.substring(1) : path - }/${kebabCase(name)}`; + if (route) { + const { workspaceId, method, path, name } = route; + return `${basename ?? ""}/${workspaceId}/${ + method?.toLowerCase() ?? "get" + }/${path.startsWith("/") ? path.substring(1) : path}/${kebabCase(name)}`; + } else { + return basename ?? "/"; + } } export type EndpointRouteParams = {