import React, { useMemo, useState } from "react"; import { matchPath, PathMatch, useNavigate } from "react-router-dom"; import { useSnackbar } from "notistack"; import { getPage, getPath, getTitle, isNavigation, parseOperationId, stripPathPrefix, } from "../router/routes"; import { useApi } from "./ApiContext"; export interface RouteInfo { id: string; app: string; view: string; action: string; title: string; /** Whether this route should be used for navigation in the router context */ navigation: boolean; path: string; page: string; } export type RouterParams = Record; export type RouterQuery = string | URLSearchParams | string[][] | Record; interface RouterContext { routes: RouteInfo[]; getCurrent(): { route: RouteInfo; match: PathMatch } | undefined; getRoute(reverse: string): RouteInfo | undefined; setCustomRoutes(routes: RouteInfo[]): void; navigate( route?: number | string | RouteInfo, options?: { params?: RouterParams; query?: RouterQuery; replace?: boolean; }, ): void; } const RouterContext = React.createContext({ routes: [], getCurrent: () => void 0, getRoute: (reverse) => void reverse, setCustomRoutes: (routes) => void routes, navigate: (route, options) => void [route, options], }); export const useRouter = () => React.useContext(RouterContext); export const RouterContextProvider: React.FC< React.PropsWithChildren<{ stripPathPrefix?: string; basename?: string }> > = ({ children, ...props }) => { const api = useApi(); const [customRoutes, setCustomRoutes] = useState([]); const apiRoutes = useMemo(() => { const routes = []; for (const operation of Object.values(api.operations)) { const parsedOperationId = parseOperationId(operation.id); if (!parsedOperationId) { throw new TypeError(`Could not parse operation id ${operation.id}`); } const { app, view, action } = parsedOperationId; const title = getTitle(view, operation.summary); const navigation = isNavigation(operation.tags); const path = stripPathPrefix( getPath(operation.endpoint, operation.method, action), props.stripPathPrefix, ); const page = getPage(path, action); routes.push({ id: operation.id, app, view, action, title, navigation, path, page, }); } return routes; }, [api]); const routes = useMemo( () => customRoutes.concat(apiRoutes), [customRoutes, apiRoutes], ); const routerNavigate = useNavigate(); const { enqueueSnackbar } = useSnackbar(); const getCurrent = () => { let currentPath = stripPathPrefix(location.pathname, props.basename); if (!currentPath.startsWith("/")) { currentPath = `/${currentPath}`; } for (const route of routes) { const match = matchPath(route.path, currentPath); if (match !== null) { return { route, match }; } } return undefined; }; const getRoute = (reverse: string) => { return routes.find(({ id }) => reverse === id)!; }; const navigate = ( route?: number | string | RouteInfo, options?: { params?: RouterParams; query?: RouterQuery; replace?: boolean; }, ) => { // Relative history, e.g. go back or forward x steps if (typeof route === "number") { routerNavigate(route); return; } // Direct operation id routes, requires reversing the operation id if (typeof route === "string") { const routeInfo = getRoute(route); if (routeInfo == null) { enqueueSnackbar("Failed to navigate, view console for more info", { variant: "error", }); throw new Error(`Could not find route with reverse: ${route}`); } route = routeInfo; } let pathname = route?.path; if (pathname != null) { for (const [key, value] of Object.entries(options?.params ?? {})) { pathname = pathname.replace(`:${key}`, encodeURIComponent(value)); } } routerNavigate( { pathname, search: new URLSearchParams(options?.query).toString(), }, { replace: options?.replace }, ); }; return ( {children} ); }; export default RouterContext;