Skip to content

Commit

Permalink
misc routing touch-up
Browse files Browse the repository at this point in the history
  • Loading branch information
mmiller42 committed Jul 7, 2024
1 parent b7d59bf commit 7b8c3bb
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 70 deletions.
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
nodejs 16.20.2
nodejs 16.13.0
11 changes: 10 additions & 1 deletion src/context/GlobalContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand All @@ -29,6 +30,7 @@ export const Context = React.createContext({
},

activeRoute: null as null | Route,
activeHref: "",
setActiveRoute: (activeRoute: Route | null) => {
// noop
},
Expand All @@ -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 | Route>(null);
const [keywords, setKeywordsInState] = useState(
Expand All @@ -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.
Expand Down Expand Up @@ -210,6 +217,7 @@ export function ContextProvider({ children }: { children: React.ReactNode }) {
partialRequestResponses,
setPartialRequestResponse,
activeRoute,
activeHref,
setActiveRoute,
keywords,
setKeywords,
Expand All @@ -219,6 +227,7 @@ export function ContextProvider({ children }: { children: React.ReactNode }) {
}),
[
activeRoute,
activeHref,
collapsedWorkspaces,
darkMode,
hideDeprecatedRoutes,
Expand Down
115 changes: 65 additions & 50 deletions src/context/Router.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<AnchorHTMLAttributes<HTMLAnchorElement>, "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 (
<a
href={`${basename ?? ""}${linkHref}`}
className={
typeof className === "string"
? className
: className({ isActive: activeHref === linkHref })
}
href={linkHref}
className={`${className}${
activeClassName && activeHref === linkHref ? ` ${activeClassName}` : ""
}`}
onClick={(event) => {
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}
</a>
Expand Down
11 changes: 2 additions & 9 deletions src/layout/sidebar/SideBarWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,8 @@ export function SideBarWorkspace({
routes.map((route, idx) => (
<NavLink
key={idx}
className={({ isActive }) =>
`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}
>
<div className="flex items-baseline">
Expand Down
23 changes: 14 additions & 9 deletions src/lib/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -24,20 +24,25 @@ export function routeLookupFactory(workspaces: Workspace[]): RouteLookupFn {
}, {} as Record<string, Route>);

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<Route, "workspaceId" | "method" | "path" | "name">;

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 = {
Expand Down

0 comments on commit 7b8c3bb

Please sign in to comment.