/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ import { get, set } from 'lodash'; import React, { ComponentType } from 'react'; import { BrowserRouter, BrowserRouterProps, HashRouter, HashRouterProps, MemoryRouter, MemoryRouterProps, RouteObject, useRoutes, } from 'react-router-dom'; import { Application } from './Application'; import { CustomRouterContextProvider } from './CustomRouterContextProvider'; import { BlankComponent, RouterContextCleaner } from './components'; export interface BrowserRouterOptions extends Omit { type?: 'browser'; } export interface HashRouterOptions extends Omit { type?: 'hash'; } export interface MemoryRouterOptions extends Omit { type?: 'memory'; } export type RouterOptions = (HashRouterOptions | BrowserRouterOptions | MemoryRouterOptions) & { renderComponent?: RenderComponentType; routes?: Record; }; export type ComponentTypeAndString = ComponentType | string; export interface RouteType extends Omit { Component?: ComponentTypeAndString; } export type RenderComponentType = (Component: ComponentTypeAndString, props?: any) => React.ReactNode; export class RouterManager { protected routes: Record = {}; protected options: RouterOptions; public app: Application; constructor(options: RouterOptions = {}, app: Application) { this.options = options; this.app = app; this.routes = options.routes || {}; } /** * @internal */ getRoutesTree(): RouteObject[] { type RouteTypeWithChildren = RouteType & { children?: RouteTypeWithChildren }; const routes: Record = {}; /** * { 'a': { name: '1' }, 'a.b': { name: '2' }, 'a.c': { name: '3' } }; * => * { a: { name: '1', children: { b: { name: '2' }, c: {name: '3'} } } } */ for (const [name, route] of Object.entries(this.routes)) { set(routes, name.split('.').join('.children.'), { ...get(routes, name.split('.').join('.children.')), ...route }); } /** * get RouteObject tree * * @example * { a: { name: '1', children: { b: { name: '2' }, c: { children: { d: { name: '3' } } } } } } * => * { name: '1', children: [{ name: '2' }, { name: '3' }] } */ const buildRoutesTree = (routes: RouteTypeWithChildren): RouteObject[] => { return Object.values(routes).reduce((acc, item) => { if (Object.keys(item).length === 1 && item.children) { acc.push(...buildRoutesTree(item.children)); } else { const { Component, element, children, ...reset } = item; let ele = element; if (Component) { if (typeof Component === 'string') { ele = this.app.renderComponent(Component); } else { ele = React.createElement(Component); } } const res = { ...reset, element: ele, children: children ? buildRoutesTree(children) : undefined, } as RouteObject; acc.push(res); } return acc; }, []); }; return buildRoutesTree(routes); } getRoutes() { return this.routes; } setType(type: RouterOptions['type']) { this.options.type = type; } getBasename() { return this.options.basename; } setBasename(basename: string) { this.options.basename = basename; } /** * @internal */ getRouterComponent(children?: React.ReactNode) { const { type = 'browser', ...opts } = this.options; const Routers = { hash: HashRouter, browser: BrowserRouter, memory: MemoryRouter, }; const ReactRouter = Routers[type]; const routes = this.getRoutesTree(); const RenderRoutes = () => { const element = useRoutes(routes); return element; }; const RenderRouter: React.FC<{ BaseLayout?: ComponentType }> = ({ BaseLayout = BlankComponent }) => { return ( {children} ); }; return RenderRouter; } add(name: string, route: RouteType) { this.routes[name] = route; } get(name: string) { return this.routes[name]; } has(name: string) { return !!this.get(name); } remove(name: string) { delete this.routes[name]; } } export function createRouterManager(options?: RouterOptions, app?: Application) { return new RouterManager(options, app); }