Zeke Zhang 05cf9986b0
feat: enable direct dialog opening via URL and support for page mode (#4706)
* refactor: optimize page tabs routing

* test: add e2e test for page tabs

* feat: add popup routing

* fix: resolve nested issue

* refactor: rename file utils to pagePopupUtils

* perf: enhance animation and overall performance

* fix: fix filterByTK

* fix(sourceId): resolve error when sourceId is undefined

* fix: fix List and GridCard

* fix: fix params not fresh

* fix: fix parent record

* fix: resolve the issue on block data not refreshing after popup closure

* feat: bind tab with URL in popups

* feat(sub-page): enable popup to open in page mode

* chore: optimize

* feat: support association fields

* fix: address the issue of no data in associaiton field

* fix: resolve the issue with opening nested dialog in association field

* fix: fix the issue of dialog content not refreshing

* perf: use useNavigateNoUpdate to replace useNavigate

* perf: enhance popups performance by avoiding unnecessary rendering

* fix: fix tab page

* fix: fix bulk edit action

* chore: fix unit test

* chore: fix unit tests

* fix: fix bug to pass e2e tests

* chore: fix build

* fix: fix bugs to pass e2e tests

* chore: avoid crashing

* chore: make e2e tests pass

* chore: make e2e tests pass

* chore: fix unit tests

* fix(multi-app): fix known issues

* fix(Duplicate): should no page mode

* chore: fix build

* fix(mobile): fix known issues

* fix: fix open mode of Add new

* refactor: rename 'popupUid' to 'popupuid'

* refactor: rename 'subPageUid' tp 'subpageuid'

* refactor(subpage): simplify configuration of router

* fix(variable): refresh data after value change

* test: add e2e test for sub page

* refactor: refactor and add tests

* fix: fix association field

* refactor(subPage): avoid blank page occurrences

* chore: fix unit tests

* fix: correct first-click context setting for association fields

* refactor: use Action's uid for subpage

* refactor: rename x-nb-popup-context to x-action-context and move it to Action schema

* feat: add context during the creation of actions

* chore: fix build

* chore: make e2e tests pass

* fix(addChild): fix context of Add child

* fix: avoid loss or query string

* fix: avoid including 'popups' in the path

* fix: resolve issue with popup variables and add tests

* chore(e2e): fix e2e test

* fix(sideMenu): resolve the disappearing sidebar issue and add tests

* chore(e2e): fix e2e test

* fix: should refresh block data after mutiple popups closed

* chore: fix e2e test

* fix(associationField): fix wrong context

* fix: address issue with special characters
2024-06-30 23:25:01 +08:00

181 lines
5.0 KiB
TypeScript

/**
* 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<BrowserRouterProps, 'children'> {
type?: 'browser';
}
export interface HashRouterOptions extends Omit<HashRouterProps, 'children'> {
type?: 'hash';
}
export interface MemoryRouterOptions extends Omit<MemoryRouterProps, 'children'> {
type?: 'memory';
}
export type RouterOptions = (HashRouterOptions | BrowserRouterOptions | MemoryRouterOptions) & {
renderComponent?: RenderComponentType;
routes?: Record<string, RouteType>;
};
export type ComponentTypeAndString<T = any> = ComponentType<T> | string;
export interface RouteType extends Omit<RouteObject, 'children' | 'Component'> {
Component?: ComponentTypeAndString;
}
export type RenderComponentType = (Component: ComponentTypeAndString, props?: any) => React.ReactNode;
export class RouterManager {
protected routes: Record<string, RouteType> = {};
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<string, RouteTypeWithChildren> = {};
/**
* { '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<RouteObject[]>((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 (
<RouterContextCleaner>
<ReactRouter {...opts}>
<CustomRouterContextProvider>
<BaseLayout>
<RenderRoutes />
{children}
</BaseLayout>
</CustomRouterContextProvider>
</ReactRouter>
</RouterContextCleaner>
);
};
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);
}