mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-12-01 19:58:15 +08:00
refactor(client)!: application, router and plugin (#2068)
BREAKING CHANGE: * refactor: update umi version 3.x to version 4.x * refactor: update react-router-dom version to 6.x * refactor(react-router-dom): change Layout Component `props.children` to `<Outlet />` * refactor(react-router-dom): change <Route /> props and <RouteSwitch /> correct * refactor(react-router-dom): replace `<Redirect />` to `<Navigate replace />` * refactor(react-router-dom): replace `useHistory` to `useNavigate` * refactor(react-router-dom): replace `useRouteMatch` to `useParams` * refactor(react-router-dom & dumi): fix <RouteSwitch /> & umi document bug * refactor(react-router-dom): `useRoutes` Optimize `<RouteSwitch />` code * refactor(react-router-dom): update `Route` types and docs * refactor(react-router-dom): optimize RouteSwitch code * refactor(react-router-dom): `useLocation` no generics type * refactor(react-router-dom): add `less v3.9.0` to `resolutions` to solve the error of `gulp-less` * refactor(react-router-dom): fix `<RouteSwitch />` `props.routes` as an array is not handled * chore: upgrade `dumi` and refactor docs * fix: completed code review, add `targets` to solve browser compatibility & removed `chainWebpack` * refactor(dumi): upgraded dumi under `packages/core/client` * refactor(dumi): delete `packages/core/dumi-theme-nocobase` * refactor(dumi): degrade `react` & replace `dumi-theme-antd` to `dumi-theme-nocobase` * refactor(dumi): solve conflicts between multiple dumi applications * fix: login page error in react 17 * refactor(dumi): remove less resolutions * refactor(dumi): umi add `msfu: true` config * fix: merge bug * fix: self code review * fix: code reivew and test bug * refactor: upgrade react to 18 * refactor: degrade react types to 17 * chore: fix ci error * fix: support routerBase & fix workflow page params * fix(doc): menu externel link * fix: build error * fix: delete * fix: vitest error * fix: react-router new code replace * fix: vitest markdown error * fix: title is none when refresh * fix: merge error * fix: sidebar width is wrong * fix: useProps error * fix: side-menu-width * fix: menu selectId is wrong & useProps is string * fix: menu selected first default & side menu hide when change * fix: test error & v0.10 change log * fix: new compnent doc modify * fix: set umi `fastRefresh=false` * refactor: application v2 * fix: improve code * fix: bug * fix: page = 0 error * fix: workflow navigate error * feat: plugin manager * fix: afterAdd * feat: complete basic functional refactor * fix: performance Application * feat: support client and server build * refactor: nocobase build-in plugin and providers * fix: server can't start * refactor: all plugins package `Prodiver` change to `Plugin` * feat: nested router and change mobile client * feat: delete application-v1 and router-switch * feat: improve routes * fix: change mobile not nested * feat: delete RouteSwitchContext and change buildin Provider to Plugin * feat: delete RouteSwitchContext plugins * fix: refactor SchemaComponentOptions * feat: improve SchemaComponentOptions * fix: add useAdminSchemaUid * fix: merge master error * fix: vitest error * fix: bug * feat: bugs * fix: improve code * fix: restore code * feat: vitest * fix: bugs * fix: bugs * docs: update doc * feat: improve code * feat: add docs and imporve code * fix: bugs * feat: add tests * fix: remove deps * fix: muti app router error * fix: router error * fix: workflow error * fix: cli error * feat: change NoCobase -> Nocobase * fix: code review * fix: type error * fix: cli error and plugin demo * feat: update doc theme * fix: build error * fix: mobile router * fix: code rewview * fix: bug * fix: test bug * fix: bug * refactor: add the "client" directory to all plugins * refactor: modify samples client and plugin template * fix: merge error * fix: add files in package.json * refactor: add README to files in package.json * fix: adjust plugins depencies * refactor: completing plugins' devDependencies and dependencies * fix: bug * refactor: remove @emotion/css * refactor: jsonwebtoken deps * refactor: remove sequelize * refactor: dayjs and moment deps * fix: bugs * fix: bug * fix: cycle detect * fix: merge bug * feat: new plugin bug * fix: lang bug * fix: dynamic import bug * refactor: plugins and example add father config * feat: improve code * fix: add AppSpin and AppError components * Revert "refactor: plugins and example add father config" This reverts commit 483315bca5524e4b8cbbb20cbad77986f081089d. # Conflicts: # packages/plugins/auth/package.json # packages/plugins/multi-app-manager/package.json # packages/samples/command/package.json # packages/samples/custom-collection-template/package.json # packages/samples/ratelimit/package.json # packages/samples/shop-actions/package.json # packages/samples/shop-events/package.json # packages/samples/shop-modeling/package.json * feat: update doc --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
50786621bb
commit
2cb1203aa4
@ -23,3 +23,4 @@ packages/core/database/src/sql-parser/index.js
|
||||
**/.dumi/tmp
|
||||
**/.dumi/tmp-test
|
||||
**/.dumi/tmp-production
|
||||
packages/core/cli/templates/plugin/src/client/*.tpl
|
||||
|
@ -34,6 +34,8 @@
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"no-empty-function": "off",
|
||||
|
@ -13,3 +13,4 @@ packages/core/client/src/locale/*
|
||||
**/.dumi/tmp
|
||||
**/.dumi/tmp-test
|
||||
**/.dumi/tmp-production
|
||||
packages/core/cli/templates/plugin/src/client/*.tpl
|
||||
|
@ -85,6 +85,7 @@ const sidebar = {
|
||||
// '/welcome/release/index',
|
||||
// '/welcome/release/v08-changelog',
|
||||
'/welcome/release/v10-changelog',
|
||||
'/welcome/release/v11-changelog',
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -260,7 +261,7 @@ const sidebar = {
|
||||
children: [
|
||||
// '/api/client',
|
||||
'/api/client/application',
|
||||
'/api/client/route-switch',
|
||||
'/api/client/router',
|
||||
{
|
||||
title: 'SchemaDesigner',
|
||||
'title.zh-CN': 'SchemaDesigner',
|
||||
|
@ -32,7 +32,6 @@ Add Providers, build-in Providers are:
|
||||
- APIClientProvider
|
||||
- I18nextProvider
|
||||
- AntdConfigProvider
|
||||
- RemoteRouteSwitchProvider
|
||||
- SystemSettingsProvider
|
||||
- PluginManagerProvider
|
||||
- SchemaComponentProvider
|
||||
|
@ -1,74 +0,0 @@
|
||||
# RouteSwitch
|
||||
|
||||
## `<RouteSwitchProvider />`
|
||||
|
||||
```ts
|
||||
interface RouteSwitchProviderProps {
|
||||
components?: ReactComponent;
|
||||
routes?: RouteRedirectProps[];
|
||||
}
|
||||
```
|
||||
|
||||
## `<RouteSwitch />`
|
||||
|
||||
```ts
|
||||
interface RouteSwitchProps {
|
||||
routes?: RouteRedirectProps[];
|
||||
components?: ReactComponent;
|
||||
}
|
||||
|
||||
type RouteRedirectProps = RedirectProps | RouteProps;
|
||||
|
||||
interface RedirectProps {
|
||||
type: 'redirect';
|
||||
to: any;
|
||||
path?: string;
|
||||
push?: boolean;
|
||||
from?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface RouteProps {
|
||||
type: 'route';
|
||||
path?: string | string[];
|
||||
sensitive?: boolean;
|
||||
component?: any;
|
||||
routes?: RouteProps[];
|
||||
[key: string]: any;
|
||||
}
|
||||
```
|
||||
|
||||
## Full Example
|
||||
|
||||
```tsx | pure
|
||||
import React from 'react';
|
||||
import { Link, MemoryRouter as Router } from 'react-router-dom';
|
||||
import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client';
|
||||
|
||||
const Home = () => <h1>Home</h1>;
|
||||
const About = () => <h1>About</h1>;
|
||||
|
||||
const routes: RouteRedirectProps[] = [
|
||||
{
|
||||
type: 'route',
|
||||
path: '/',
|
||||
component: 'Home',
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/about',
|
||||
component: 'About',
|
||||
},
|
||||
];
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<RouteSwitchProvider components={{ Home, About }}>
|
||||
<Router initialEntries={['/']}>
|
||||
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
|
||||
<RouteSwitch routes={routes} />
|
||||
</Router>
|
||||
</RouteSwitchProvider>
|
||||
);
|
||||
};
|
||||
```
|
180
docs/en-US/api/client/router.md
Normal file
180
docs/en-US/api/client/router.md
Normal file
@ -0,0 +1,180 @@
|
||||
# Router
|
||||
|
||||
## API
|
||||
|
||||
### Initial
|
||||
|
||||
```tsx | pure
|
||||
|
||||
const app = new Application({
|
||||
router: {
|
||||
type: 'browser' // type default value is `browser`
|
||||
}
|
||||
})
|
||||
|
||||
// or
|
||||
const app = new Application({
|
||||
router: {
|
||||
type: 'memory',
|
||||
initialEntries: ['/']
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### add Route
|
||||
|
||||
#### basic
|
||||
|
||||
```tsx | pure
|
||||
import { RouteObject } from 'react-router-dom'
|
||||
const app = new Application()
|
||||
|
||||
const Hello = () => {
|
||||
return <div>Hello</div>
|
||||
}
|
||||
|
||||
// first argument is `name` of route, second argument is `RouteObject`
|
||||
app.router.add('root', {
|
||||
path: '/',
|
||||
element: <Hello />
|
||||
})
|
||||
|
||||
app.router.add('root', {
|
||||
path: '/',
|
||||
Component: Hello
|
||||
})
|
||||
```
|
||||
|
||||
#### Component is String
|
||||
|
||||
```tsx | pure
|
||||
app.addComponents({
|
||||
Hello
|
||||
})
|
||||
app.router.add('root', {
|
||||
path: '/',
|
||||
Component: 'Hello'
|
||||
})
|
||||
```
|
||||
|
||||
#### nested
|
||||
|
||||
```tsx | pure
|
||||
import { Outlet } from 'react-router-dom'
|
||||
|
||||
const Layout = () => {
|
||||
return <div>
|
||||
<Link to='/home'>Home</Link>
|
||||
<Link to='/about'>about</Link>
|
||||
|
||||
<Outlet />
|
||||
</div>
|
||||
}
|
||||
|
||||
const Home = () => {
|
||||
return <div>Home</div>
|
||||
}
|
||||
|
||||
const About = () => {
|
||||
return <div>About</div>
|
||||
}
|
||||
|
||||
app.router.add('root', {
|
||||
element: <Layout />
|
||||
})
|
||||
app.router.add('root.home', {
|
||||
path: '/home',
|
||||
element: <Home />
|
||||
})
|
||||
app.router.add('root.about', {
|
||||
path: '/about',
|
||||
element: <About />
|
||||
})
|
||||
```
|
||||
|
||||
It will generate the following routes:
|
||||
|
||||
```tsx | pure
|
||||
{
|
||||
element: <Layout />,
|
||||
children: [
|
||||
{
|
||||
path: '/home',
|
||||
element: <Home />
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
element: <About />
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### remove Route
|
||||
|
||||
```tsx | pure
|
||||
// remove route by name
|
||||
app.router.remove('root.home')
|
||||
app.router.remove('hello')
|
||||
```
|
||||
|
||||
#### Router in plugin
|
||||
|
||||
```tsx | pure
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
// add route
|
||||
this.app.router.add('hello', {
|
||||
path: '/hello',
|
||||
element: <div>hello</div>,
|
||||
})
|
||||
|
||||
// remove route
|
||||
this.app.router.remove('world');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* defaultShowCode: true
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { Application } from '@nocobase/client';
|
||||
|
||||
const Home = () => <h1>Home</h1>;
|
||||
const About = () => <h1>About</h1>;
|
||||
|
||||
const Layout = () => {
|
||||
return <div>
|
||||
<div><Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link></div>
|
||||
<Outlet />
|
||||
</div>
|
||||
}
|
||||
|
||||
const app = new Application({
|
||||
router: {
|
||||
type: 'memory',
|
||||
initialEntries: ['/']
|
||||
}
|
||||
})
|
||||
|
||||
app.router.add('root', {
|
||||
element: <Layout />
|
||||
})
|
||||
|
||||
app.router.add('root.home', {
|
||||
path: '/',
|
||||
element: <Home />
|
||||
})
|
||||
|
||||
app.router.add('root.about', {
|
||||
path: '/about',
|
||||
element: <About />
|
||||
})
|
||||
|
||||
export default app.getRootComponent();
|
||||
```
|
@ -7,7 +7,6 @@ Most of the extensions for the NocoBase client are provided as Providers.
|
||||
- APIClientProvider
|
||||
- I18nextProvider
|
||||
- AntdConfigProvider
|
||||
- RemoteRouteSwitchProvider
|
||||
- SystemSettingsProvider
|
||||
- PluginManagerProvider
|
||||
- SchemaComponentProvider
|
||||
|
@ -1,62 +1,66 @@
|
||||
# UI Routing
|
||||
|
||||
NocoBase Client's Router is based on [React Router](https://v5.reactrouter.com/web/guides/quick-start) and can be configured via `<RouteSwitch routes={[]} />` to configure ui routes with the following example.
|
||||
NocoBase Client's Router is based on [React Router](https://v5.reactrouter.com/web/guides/quick-start) and can be configured via `app.router` to configure ui routes with the following example.
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* defaultShowCode: true
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Link, MemoryRouter as Router } from 'react-router-dom';
|
||||
import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client';
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { Application } from '@nocobase/client';
|
||||
|
||||
const Home = () => <h1>Home</h1>;
|
||||
const About = () => <h1>About</h1>;
|
||||
|
||||
const routes: RouteRedirectProps[] = [
|
||||
{
|
||||
type: 'route',
|
||||
path: '/',
|
||||
component: 'Home',
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/about',
|
||||
component: 'About',
|
||||
},
|
||||
];
|
||||
const Layout = () => {
|
||||
return <div>
|
||||
<div><Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link></div>
|
||||
<Outlet />
|
||||
</div>
|
||||
}
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<RouteSwitchProvider components={{ Home, About }}>
|
||||
<Router initialEntries={['/']}>
|
||||
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
|
||||
<RouteSwitch routes={routes} />
|
||||
</Router>
|
||||
</RouteSwitchProvider>
|
||||
);
|
||||
};
|
||||
const app = new Application({
|
||||
router: {
|
||||
type: 'memory',
|
||||
initialEntries: ['/']
|
||||
}
|
||||
})
|
||||
|
||||
app.router.add('root', {
|
||||
element: <Layout />
|
||||
})
|
||||
|
||||
app.router.add('root.home', {
|
||||
path: '/',
|
||||
element: <Home />
|
||||
})
|
||||
|
||||
app.router.add('root.about', {
|
||||
path: '/about',
|
||||
element: <About />
|
||||
})
|
||||
|
||||
export default app.getRootComponent();
|
||||
```
|
||||
|
||||
In a full NocoBase application, the Route can be extended in a similar way as follows.
|
||||
|
||||
```tsx | pure
|
||||
import { RouteSwitchContext } from '@nocobase/client';
|
||||
import React, { useContext } from 'react';
|
||||
import { Plugin } from '@nocobase/client';
|
||||
|
||||
const HelloWorld = () => {
|
||||
return <div>Hello ui router</div>;
|
||||
};
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
// add
|
||||
this.app.router.add('hello', {
|
||||
path: '/hello',
|
||||
element: <div>hello</div>,
|
||||
})
|
||||
|
||||
export default React.memo((props) => {
|
||||
const ctx = useContext(RouteSwitchContext);
|
||||
ctx.routes.push({
|
||||
type: 'route',
|
||||
path: '/hello-world',
|
||||
component: HelloWorld,
|
||||
});
|
||||
return <RouteSwitchContext.Provider value={ctx}>{props.children}</RouteSwitchContext.Provider>;
|
||||
});
|
||||
// remove
|
||||
this.app.router.remove('hello');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [packages/samples/custom-page](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-page) for the full example
|
||||
|
100
docs/en-US/welcome/release/v11-changelog.md
Normal file
100
docs/en-US/welcome/release/v11-changelog.md
Normal file
@ -0,0 +1,100 @@
|
||||
# v0.11: Update instructions
|
||||
|
||||
## Plugin registration and use
|
||||
|
||||
before you had to pass a component and the component needed to pass `props.children`, for example:
|
||||
|
||||
```tsx | pure
|
||||
const HelloProvider = (props) => {
|
||||
// do something logic
|
||||
return <div>
|
||||
{props.children}
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default HelloProvider
|
||||
```
|
||||
|
||||
now you need to change to the plugin way, for example:
|
||||
|
||||
```diff | pure
|
||||
+import { Plugin } from '@nocobase/client'
|
||||
|
||||
const HelloProvider = (props) => {
|
||||
// do something logic
|
||||
return <div>
|
||||
{props.children}
|
||||
</div>;
|
||||
}
|
||||
|
||||
+ export class HelloPlugin extends Plugin {
|
||||
+ async load() {
|
||||
+ this.app.addProvider(HelloProvider);
|
||||
+ }
|
||||
+ }
|
||||
|
||||
- export default HelloProvider;
|
||||
+ export default HelloPlugin;
|
||||
```
|
||||
|
||||
plugins are very powerful and can do a lot of things in the `load` phase:
|
||||
|
||||
- modify routes
|
||||
- add Components
|
||||
- add Providers
|
||||
- add Scopes
|
||||
- load other plugins
|
||||
|
||||
if you used `RouteSwitchContext` to modify the route before, you now need to replace it with a plugin:
|
||||
|
||||
```tsx | pure
|
||||
import { RouteSwitchContext } from '@nocobase/client';
|
||||
|
||||
const HelloProvider = () => {
|
||||
const { routes, ...others } = useContext(RouteSwitchContext);
|
||||
routes[1].routes.unshift({
|
||||
path: '/hello',
|
||||
component: Hello,
|
||||
});
|
||||
|
||||
return <div>
|
||||
<RouteSwitchContext.Provider value={{ ...others, routes }}>
|
||||
{props.children}
|
||||
</RouteSwitchContext.Provider>
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
now you need to change to the plugin way, for example:
|
||||
|
||||
```diff | pure
|
||||
- import { RouteSwitchContext } from '@nocobase/client';
|
||||
+ import { Plugin } from '@nocobase/client';
|
||||
|
||||
const HelloProvider = (props) => {
|
||||
- const { routes, ...others } = useContext(RouteSwitchContext);
|
||||
- routes[1].routes.unshift({
|
||||
- path: '/hello',
|
||||
- component: Hello,
|
||||
- });
|
||||
|
||||
return <div>
|
||||
- <RouteSwitchContext.Provider value={{ ...others, routes }}>
|
||||
{props.children}
|
||||
- </RouteSwitchContext.Provider>
|
||||
</div>
|
||||
}
|
||||
|
||||
+ export class HelloPlugin extends Plugin {
|
||||
+ async load() {
|
||||
+ this.app.router.add('admin.hello', {
|
||||
+ path: '/hello',
|
||||
+ Component: Hello,
|
||||
+ });
|
||||
+ this.app.addProvider(HelloProvider);
|
||||
+ }
|
||||
+ }
|
||||
+ export default HelloPlugin;
|
||||
```
|
||||
|
||||
more details can be found in [plugin development](/development/client).
|
@ -32,7 +32,6 @@ const app = new Application({
|
||||
- APIClientProvider
|
||||
- I18nextProvider
|
||||
- AntdConfigProvider
|
||||
- RemoteRouteSwitchProvider
|
||||
- SystemSettingsProvider
|
||||
- PluginManagerProvider
|
||||
- SchemaComponentProvider
|
||||
@ -59,4 +58,4 @@ export const app = new Application({
|
||||
});
|
||||
|
||||
export default app.render();
|
||||
```
|
||||
```
|
||||
|
@ -1,74 +0,0 @@
|
||||
# RouteSwitch
|
||||
|
||||
## `<RouteSwitchProvider />`
|
||||
|
||||
```ts
|
||||
interface RouteSwitchProviderProps {
|
||||
components?: ReactComponent;
|
||||
routes?: RouteRedirectProps[];
|
||||
}
|
||||
```
|
||||
|
||||
## `<RouteSwitch />`
|
||||
|
||||
```ts
|
||||
interface RouteSwitchProps {
|
||||
routes?: RouteRedirectProps[];
|
||||
components?: ReactComponent;
|
||||
}
|
||||
|
||||
type RouteRedirectProps = RedirectProps | RouteProps;
|
||||
|
||||
interface RedirectProps {
|
||||
type: 'redirect';
|
||||
to: any;
|
||||
path?: string;
|
||||
push?: boolean;
|
||||
from?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface RouteProps {
|
||||
type: 'route';
|
||||
path?: string | string[];
|
||||
sensitive?: boolean;
|
||||
component?: any;
|
||||
routes?: RouteProps[];
|
||||
[key: string]: any;
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```tsx | pure
|
||||
import React from 'react';
|
||||
import { Link, MemoryRouter as Router } from 'react-router-dom';
|
||||
import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client';
|
||||
|
||||
const Home = () => <h1>Home</h1>;
|
||||
const About = () => <h1>About</h1>;
|
||||
|
||||
const routes: RouteRedirectProps[] = [
|
||||
{
|
||||
type: 'route',
|
||||
path: '/',
|
||||
component: 'Home',
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/about',
|
||||
component: 'About',
|
||||
},
|
||||
];
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<RouteSwitchProvider components={{ Home, About }}>
|
||||
<Router initialEntries={['/']}>
|
||||
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
|
||||
<RouteSwitch routes={routes} />
|
||||
</Router>
|
||||
</RouteSwitchProvider>
|
||||
);
|
||||
};
|
||||
```
|
180
docs/tr-TR/api/client/router.md
Normal file
180
docs/tr-TR/api/client/router.md
Normal file
@ -0,0 +1,180 @@
|
||||
# Router
|
||||
|
||||
## API
|
||||
|
||||
### Initial
|
||||
|
||||
```tsx | pure
|
||||
|
||||
const app = new Application({
|
||||
router: {
|
||||
type: 'browser' // type default value is `browser`
|
||||
}
|
||||
})
|
||||
|
||||
// or
|
||||
const app = new Application({
|
||||
router: {
|
||||
type: 'memory',
|
||||
initialEntries: ['/']
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### add Route
|
||||
|
||||
#### basic
|
||||
|
||||
```tsx | pure
|
||||
import { RouteObject } from 'react-router-dom'
|
||||
const app = new Application()
|
||||
|
||||
const Hello = () => {
|
||||
return <div>Hello</div>
|
||||
}
|
||||
|
||||
// first argument is `name` of route, second argument is `RouteObject`
|
||||
app.router.add('root', {
|
||||
path: '/',
|
||||
element: <Hello />
|
||||
})
|
||||
|
||||
app.router.add('root', {
|
||||
path: '/',
|
||||
Component: Hello
|
||||
})
|
||||
```
|
||||
|
||||
#### Component is String
|
||||
|
||||
```tsx | pure
|
||||
app.addComponents({
|
||||
Hello
|
||||
})
|
||||
app.router.add('root', {
|
||||
path: '/',
|
||||
Component: 'Hello'
|
||||
})
|
||||
```
|
||||
|
||||
#### nested
|
||||
|
||||
```tsx | pure
|
||||
import { Outlet } from 'react-router-dom'
|
||||
|
||||
const Layout = () => {
|
||||
return <div>
|
||||
<Link to='/home'>Home</Link>
|
||||
<Link to='/about'>about</Link>
|
||||
|
||||
<Outlet />
|
||||
</div>
|
||||
}
|
||||
|
||||
const Home = () => {
|
||||
return <div>Home</div>
|
||||
}
|
||||
|
||||
const About = () => {
|
||||
return <div>About</div>
|
||||
}
|
||||
|
||||
app.router.add('root', {
|
||||
element: <Layout />
|
||||
})
|
||||
app.router.add('root.home', {
|
||||
path: '/home',
|
||||
element: <Home />
|
||||
})
|
||||
app.router.add('root.about', {
|
||||
path: '/about',
|
||||
element: <About />
|
||||
})
|
||||
```
|
||||
|
||||
It will generate the following routes:
|
||||
|
||||
```tsx | pure
|
||||
{
|
||||
element: <Layout />,
|
||||
children: [
|
||||
{
|
||||
path: '/home',
|
||||
element: <Home />
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
element: <About />
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### remove Route
|
||||
|
||||
```tsx | pure
|
||||
// remove route by name
|
||||
app.router.remove('root.home')
|
||||
app.router.remove('hello')
|
||||
```
|
||||
|
||||
#### Router in plugin
|
||||
|
||||
```tsx | pure
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
// add route
|
||||
this.app.router.add('hello', {
|
||||
path: '/hello',
|
||||
element: <div>hello</div>,
|
||||
})
|
||||
|
||||
// remove route
|
||||
this.app.router.remove('world');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* defaultShowCode: true
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { Application } from '@nocobase/client';
|
||||
|
||||
const Home = () => <h1>Home</h1>;
|
||||
const About = () => <h1>About</h1>;
|
||||
|
||||
const Layout = () => {
|
||||
return <div>
|
||||
<div><Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link></div>
|
||||
<Outlet />
|
||||
</div>
|
||||
}
|
||||
|
||||
const app = new Application({
|
||||
router: {
|
||||
type: 'memory',
|
||||
initialEntries: ['/']
|
||||
}
|
||||
})
|
||||
|
||||
app.router.add('root', {
|
||||
element: <Layout />
|
||||
})
|
||||
|
||||
app.router.add('root.home', {
|
||||
path: '/',
|
||||
element: <Home />
|
||||
})
|
||||
|
||||
app.router.add('root.about', {
|
||||
path: '/about',
|
||||
element: <About />
|
||||
})
|
||||
|
||||
export default app.getRootComponent();
|
||||
```
|
@ -7,7 +7,6 @@ Most of the extensions for the NocoBase client are provided as Providers.
|
||||
- APIClientProvider
|
||||
- I18nextProvider
|
||||
- AntdConfigProvider
|
||||
- RemoteRouteSwitchProvider
|
||||
- SystemSettingsProvider
|
||||
- PluginManagerProvider
|
||||
- SchemaComponentProvider
|
||||
|
@ -1,62 +1,66 @@
|
||||
# UI Routing
|
||||
|
||||
NocoBase Client's Router is based on [React Router](https://v5.reactrouter.com/web/guides/quick-start) and can be configured via `<RouteSwitch routes={[]} />` to configure ui routes with the following example.
|
||||
NocoBase Client's Router is based on [React Router](https://v5.reactrouter.com/web/guides/quick-start) and can be configured via `app.router` to configure ui routes with the following example.
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* defaultShowCode: true
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Link, MemoryRouter as Router } from 'react-router-dom';
|
||||
import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client';
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { Application } from '@nocobase/client';
|
||||
|
||||
const Home = () => <h1>Home</h1>;
|
||||
const About = () => <h1>About</h1>;
|
||||
|
||||
const routes: RouteRedirectProps[] = [
|
||||
{
|
||||
type: 'route',
|
||||
path: '/',
|
||||
component: 'Home',
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/about',
|
||||
component: 'About',
|
||||
},
|
||||
];
|
||||
const Layout = () => {
|
||||
return <div>
|
||||
<div><Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link></div>
|
||||
<Outlet />
|
||||
</div>
|
||||
}
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<RouteSwitchProvider components={{ Home, About }}>
|
||||
<Router initialEntries={['/']}>
|
||||
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
|
||||
<RouteSwitch routes={routes} />
|
||||
</Router>
|
||||
</RouteSwitchProvider>
|
||||
);
|
||||
};
|
||||
const app = new Application({
|
||||
router: {
|
||||
type: 'memory',
|
||||
initialEntries: ['/']
|
||||
}
|
||||
})
|
||||
|
||||
app.router.add('root', {
|
||||
element: <Layout />
|
||||
})
|
||||
|
||||
app.router.add('root.home', {
|
||||
path: '/',
|
||||
element: <Home />
|
||||
})
|
||||
|
||||
app.router.add('root.about', {
|
||||
path: '/about',
|
||||
element: <About />
|
||||
})
|
||||
|
||||
export default app.getRootComponent();
|
||||
```
|
||||
|
||||
In a full NocoBase application, the Route can be extended in a similar way as follows.
|
||||
|
||||
```tsx | pure
|
||||
import { RouteSwitchContext } from '@nocobase/client';
|
||||
import React, { useContext } from 'react';
|
||||
import { Plugin } from '@nocobase/client';
|
||||
|
||||
const HelloWorld = () => {
|
||||
return <div>Hello ui router</div>;
|
||||
};
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
// add
|
||||
this.app.router.add('hello', {
|
||||
path: '/hello',
|
||||
element: <div>hello</div>,
|
||||
})
|
||||
|
||||
export default React.memo((props) => {
|
||||
const ctx = useContext(RouteSwitchContext);
|
||||
ctx.routes.push({
|
||||
type: 'route',
|
||||
path: '/hello-world',
|
||||
component: HelloWorld,
|
||||
});
|
||||
return <RouteSwitchContext.Provider value={ctx}>{props.children}</RouteSwitchContext.Provider>;
|
||||
});
|
||||
// remove
|
||||
this.app.router.remove('hello');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [packages/samples/custom-page](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-page) for the full example
|
||||
|
100
docs/tr-TR/welcome/release/v11-changelog.md
Normal file
100
docs/tr-TR/welcome/release/v11-changelog.md
Normal file
@ -0,0 +1,100 @@
|
||||
# v0.11: Update instructions
|
||||
|
||||
## Plugin registration and use
|
||||
|
||||
before you had to pass a component and the component needed to pass `props.children`, for example:
|
||||
|
||||
```tsx | pure
|
||||
const HelloProvider = (props) => {
|
||||
// do something logic
|
||||
return <div>
|
||||
{props.children}
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default HelloProvider
|
||||
```
|
||||
|
||||
now you need to change to the plugin way, for example:
|
||||
|
||||
```diff | pure
|
||||
+import { Plugin } from '@nocobase/client'
|
||||
|
||||
const HelloProvider = (props) => {
|
||||
// do something logic
|
||||
return <div>
|
||||
{props.children}
|
||||
</div>;
|
||||
}
|
||||
|
||||
+ export class HelloPlugin extends Plugin {
|
||||
+ async load() {
|
||||
+ this.app.addProvider(HelloProvider);
|
||||
+ }
|
||||
+ }
|
||||
|
||||
- export default HelloProvider;
|
||||
+ export default HelloPlugin;
|
||||
```
|
||||
|
||||
plugins are very powerful and can do a lot of things in the `load` phase:
|
||||
|
||||
- modify routes
|
||||
- add Components
|
||||
- add Providers
|
||||
- add Scopes
|
||||
- load other plugins
|
||||
|
||||
if you used `RouteSwitchContext` to modify the route before, you now need to replace it with a plugin:
|
||||
|
||||
```tsx | pure
|
||||
import { RouteSwitchContext } from '@nocobase/client';
|
||||
|
||||
const HelloProvider = () => {
|
||||
const { routes, ...others } = useContext(RouteSwitchContext);
|
||||
routes[1].routes.unshift({
|
||||
path: '/hello',
|
||||
component: Hello,
|
||||
});
|
||||
|
||||
return <div>
|
||||
<RouteSwitchContext.Provider value={{ ...others, routes }}>
|
||||
{props.children}
|
||||
</RouteSwitchContext.Provider>
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
now you need to change to the plugin way, for example:
|
||||
|
||||
```diff | pure
|
||||
- import { RouteSwitchContext } from '@nocobase/client';
|
||||
+ import { Plugin } from '@nocobase/client';
|
||||
|
||||
const HelloProvider = (props) => {
|
||||
- const { routes, ...others } = useContext(RouteSwitchContext);
|
||||
- routes[1].routes.unshift({
|
||||
- path: '/hello',
|
||||
- component: Hello,
|
||||
- });
|
||||
|
||||
return <div>
|
||||
- <RouteSwitchContext.Provider value={{ ...others, routes }}>
|
||||
{props.children}
|
||||
- </RouteSwitchContext.Provider>
|
||||
</div>
|
||||
}
|
||||
|
||||
+ export class HelloPlugin extends Plugin {
|
||||
+ async load() {
|
||||
+ this.app.router.add('admin.hello', {
|
||||
+ path: '/hello',
|
||||
+ Component: Hello,
|
||||
+ });
|
||||
+ this.app.addProvider(HelloProvider);
|
||||
+ }
|
||||
+ }
|
||||
+ export default HelloPlugin;
|
||||
```
|
||||
|
||||
more details can be found in [plugin development](/development/client).
|
@ -32,7 +32,6 @@ const app = new Application({
|
||||
- APIClientProvider
|
||||
- I18nextProvider
|
||||
- AntdConfigProvider
|
||||
- RemoteRouteSwitchProvider
|
||||
- SystemSettingsProvider
|
||||
- PluginManagerProvider
|
||||
- SchemaComponentProvider
|
||||
@ -59,4 +58,4 @@ export const app = new Application({
|
||||
});
|
||||
|
||||
export default app.render();
|
||||
```
|
||||
```
|
||||
|
@ -1,74 +0,0 @@
|
||||
# RouteSwitch
|
||||
|
||||
## `<RouteSwitchProvider />`
|
||||
|
||||
```ts
|
||||
interface RouteSwitchProviderProps {
|
||||
components?: ReactComponent;
|
||||
routes?: RouteRedirectProps[];
|
||||
}
|
||||
```
|
||||
|
||||
## `<RouteSwitch />`
|
||||
|
||||
```ts
|
||||
interface RouteSwitchProps {
|
||||
routes?: RouteRedirectProps[];
|
||||
components?: ReactComponent;
|
||||
}
|
||||
|
||||
type RouteRedirectProps = RedirectProps | RouteProps;
|
||||
|
||||
interface RedirectProps {
|
||||
type: 'redirect';
|
||||
to: any;
|
||||
path?: string;
|
||||
push?: boolean;
|
||||
from?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface RouteProps {
|
||||
type: 'route';
|
||||
path?: string | string[];
|
||||
sensitive?: boolean;
|
||||
component?: any;
|
||||
routes?: RouteProps[];
|
||||
[key: string]: any;
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```tsx | pure
|
||||
import React from 'react';
|
||||
import { Link, MemoryRouter as Router } from 'react-router-dom';
|
||||
import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client';
|
||||
|
||||
const Home = () => <h1>Home</h1>;
|
||||
const About = () => <h1>About</h1>;
|
||||
|
||||
const routes: RouteRedirectProps[] = [
|
||||
{
|
||||
type: 'route',
|
||||
path: '/',
|
||||
component: 'Home',
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/about',
|
||||
component: 'About',
|
||||
},
|
||||
];
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<RouteSwitchProvider components={{ Home, About }}>
|
||||
<Router initialEntries={['/']}>
|
||||
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
|
||||
<RouteSwitch routes={routes} />
|
||||
</Router>
|
||||
</RouteSwitchProvider>
|
||||
);
|
||||
};
|
||||
```
|
183
docs/zh-CN/api/client/router.md
Normal file
183
docs/zh-CN/api/client/router.md
Normal file
@ -0,0 +1,183 @@
|
||||
# Router
|
||||
|
||||
## API
|
||||
|
||||
### 初始化
|
||||
|
||||
```tsx | pure
|
||||
|
||||
const app = new Application({
|
||||
router: {
|
||||
type: 'browser' // type 的默认值就是 `browser`
|
||||
}
|
||||
})
|
||||
|
||||
// or
|
||||
const app = new Application({
|
||||
router: {
|
||||
type: 'memory',
|
||||
initialEntries: ['/']
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 添加路由
|
||||
|
||||
#### 基础用法
|
||||
|
||||
```tsx | pure
|
||||
import { RouteObject } from 'react-router-dom'
|
||||
const app = new Application()
|
||||
|
||||
const Hello = () => {
|
||||
return <div>Hello</div>
|
||||
}
|
||||
|
||||
// 第一个参数是名称, 第二个参数是 `RouteObject`
|
||||
app.router.add('root', {
|
||||
path: '/',
|
||||
element: <Hello />
|
||||
})
|
||||
|
||||
app.router.add('root', {
|
||||
path: '/',
|
||||
Component: Hello
|
||||
})
|
||||
```
|
||||
|
||||
#### 支持 Component 是字符串
|
||||
|
||||
```tsx | pure
|
||||
// register Hello
|
||||
app.addComponents({
|
||||
Hello
|
||||
})
|
||||
|
||||
// Component is `Hello` string
|
||||
app.router.add('root', {
|
||||
path: '/',
|
||||
Component: 'Hello'
|
||||
})
|
||||
```
|
||||
|
||||
#### 嵌套路由
|
||||
|
||||
```tsx | pure
|
||||
import { Outlet } from 'react-router-dom'
|
||||
|
||||
const Layout = () => {
|
||||
return <div>
|
||||
<Link to='/home'>Home</Link>
|
||||
<Link to='/about'>about</Link>
|
||||
|
||||
<Outlet />
|
||||
</div>
|
||||
}
|
||||
|
||||
const Home = () => {
|
||||
return <div>Home</div>
|
||||
}
|
||||
|
||||
const About = () => {
|
||||
return <div>About</div>
|
||||
}
|
||||
|
||||
app.router.add('root', {
|
||||
element: <Layout />
|
||||
})
|
||||
app.router.add('root.home', {
|
||||
path: '/home',
|
||||
element: <Home />
|
||||
})
|
||||
app.router.add('root.about', {
|
||||
path: '/about',
|
||||
element: <About />
|
||||
})
|
||||
```
|
||||
|
||||
它将会被渲染为如下形式:
|
||||
|
||||
```tsx | pure
|
||||
{
|
||||
element: <Layout />,
|
||||
children: [
|
||||
{
|
||||
path: '/home',
|
||||
element: <Home />
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
element: <About />
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 删除路由
|
||||
|
||||
```tsx | pure
|
||||
// 传递 name 即可删除
|
||||
app.router.remove('root.home')
|
||||
app.router.remove('hello')
|
||||
```
|
||||
|
||||
#### 插件中修改路由
|
||||
|
||||
```tsx | pure
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
// add route
|
||||
this.app.router.add('hello', {
|
||||
path: '/hello',
|
||||
element: <div>hello</div>,
|
||||
})
|
||||
|
||||
// remove route
|
||||
this.app.router.remove('world');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 示例
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* defaultShowCode: true
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { Application } from '@nocobase/client';
|
||||
|
||||
const Home = () => <h1>Home</h1>;
|
||||
const About = () => <h1>About</h1>;
|
||||
|
||||
const Layout = () => {
|
||||
return <div>
|
||||
<div><Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link></div>
|
||||
<Outlet />
|
||||
</div>
|
||||
}
|
||||
|
||||
const app = new Application({
|
||||
router: {
|
||||
type: 'memory',
|
||||
initialEntries: ['/']
|
||||
}
|
||||
})
|
||||
|
||||
app.router.add('root', {
|
||||
element: <Layout />
|
||||
})
|
||||
|
||||
app.router.add('root.home', {
|
||||
path: '/',
|
||||
element: <Home />
|
||||
})
|
||||
|
||||
app.router.add('root.about', {
|
||||
path: '/about',
|
||||
element: <About />
|
||||
})
|
||||
|
||||
export default app.getRootComponent();
|
||||
```
|
@ -7,7 +7,6 @@ NocoBase 客户端的扩展大多以 Provider 的形式提供。
|
||||
- APIClientProvider
|
||||
- I18nextProvider
|
||||
- AntdConfigProvider
|
||||
- RemoteRouteSwitchProvider
|
||||
- SystemSettingsProvider
|
||||
- PluginManagerProvider
|
||||
- SchemaComponentProvider
|
||||
|
@ -1,62 +1,66 @@
|
||||
# UI 路由
|
||||
|
||||
NocoBase Client 的 Router 基于 [React Router](https://v5.reactrouter.com/web/guides/quick-start),可以通过 `<RouteSwitch routes={[]} />` 来配置 ui routes,例子如下:
|
||||
NocoBase Client 的 Router 基于 [React Router](https://v5.reactrouter.com/web/guides/quick-start),可以通过 `app.router` 来配置 ui routes,例子如下:
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* defaultShowCode: true
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Link, MemoryRouter as Router } from 'react-router-dom';
|
||||
import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client';
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { Application } from '@nocobase/client';
|
||||
|
||||
const Home = () => <h1>Home</h1>;
|
||||
const About = () => <h1>About</h1>;
|
||||
|
||||
const routes: RouteRedirectProps[] = [
|
||||
{
|
||||
type: 'route',
|
||||
path: '/',
|
||||
component: 'Home',
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/about',
|
||||
component: 'About',
|
||||
},
|
||||
];
|
||||
const Layout = () => {
|
||||
return <div>
|
||||
<div><Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link></div>
|
||||
<Outlet />
|
||||
</div>
|
||||
}
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<RouteSwitchProvider components={{ Home, About }}>
|
||||
<Router initialEntries={['/']}>
|
||||
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
|
||||
<RouteSwitch routes={routes} />
|
||||
</Router>
|
||||
</RouteSwitchProvider>
|
||||
);
|
||||
};
|
||||
const app = new Application({
|
||||
router: {
|
||||
type: 'memory',
|
||||
initialEntries: ['/']
|
||||
}
|
||||
})
|
||||
|
||||
app.router.add('root', {
|
||||
element: <Layout />
|
||||
})
|
||||
|
||||
app.router.add('root.home', {
|
||||
path: '/',
|
||||
element: <Home />
|
||||
})
|
||||
|
||||
app.router.add('root.about', {
|
||||
path: '/about',
|
||||
element: <About />
|
||||
})
|
||||
|
||||
export default app.getRootComponent();
|
||||
```
|
||||
|
||||
在完整的 NocoBase 应用里,可以类似以下的的方式扩展 Route:
|
||||
|
||||
```tsx | pure
|
||||
import { RouteSwitchContext } from '@nocobase/client';
|
||||
import React, { useContext } from 'react';
|
||||
import { Plugin } from '@nocobase/client';
|
||||
|
||||
const HelloWorld = () => {
|
||||
return <div>Hello ui router</div>;
|
||||
};
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
// 添加一条路由
|
||||
this.app.router.add('hello', {
|
||||
path: '/hello',
|
||||
element: <div>hello</div>,
|
||||
})
|
||||
|
||||
export default React.memo((props) => {
|
||||
const ctx = useContext(RouteSwitchContext);
|
||||
ctx.routes.push({
|
||||
type: 'route',
|
||||
path: '/hello-world',
|
||||
component: HelloWorld,
|
||||
});
|
||||
return <RouteSwitchContext.Provider value={ctx}>{props.children}</RouteSwitchContext.Provider>;
|
||||
});
|
||||
// 删除一条路由
|
||||
this.app.router.remove('hello');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
完整示例查看 [packages/samples/custom-page](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-page)
|
||||
|
120
docs/zh-CN/welcome/release/v11-changelog.md
Normal file
120
docs/zh-CN/welcome/release/v11-changelog.md
Normal file
@ -0,0 +1,120 @@
|
||||
# v0.11:更新说明
|
||||
|
||||
## 新特性
|
||||
|
||||
- 全新的客户端 Application、Plugin 和 Router
|
||||
- antd 升级到 v5
|
||||
- 新插件
|
||||
- 数据可视化
|
||||
- API 秘钥
|
||||
- Google 地图
|
||||
|
||||
## 不兼容的变化
|
||||
|
||||
### 全新的客户端 Application、Plugin 和 Router
|
||||
|
||||
#### 插件的变化
|
||||
|
||||
以前必须传递一个组件,并且组件需要透传 `props.children`,例如:
|
||||
|
||||
```tsx | pure
|
||||
const HelloProvider = (props) => {
|
||||
// do something logic
|
||||
return <div>
|
||||
{props.children}
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default HelloProvider
|
||||
```
|
||||
|
||||
现在需要改为插件的方式,例如:
|
||||
|
||||
```diff | pure
|
||||
+import { Plugin } from '@nocobase/client'
|
||||
|
||||
const HelloProvider = (props) => {
|
||||
// do something logic
|
||||
return <div>
|
||||
{props.children}
|
||||
</div>;
|
||||
}
|
||||
|
||||
+ export class HelloPlugin extends Plugin {
|
||||
+ async load() {
|
||||
+ this.app.addProvider(HelloProvider);
|
||||
+ }
|
||||
+ }
|
||||
|
||||
- export default HelloProvider;
|
||||
+ export default HelloPlugin;
|
||||
```
|
||||
|
||||
插件的功能很强大,可以在 `load` 阶段做很多事情:
|
||||
|
||||
- 修改路由
|
||||
- 增加 Components
|
||||
- 增加 Providers
|
||||
- 增加 Scopes
|
||||
- 加载其他插件
|
||||
|
||||
#### 路由的变化
|
||||
|
||||
如果之前使用了 `RouteSwitchContext` 进行路由修改,现在需要通过插件替换:
|
||||
|
||||
```tsx | pure
|
||||
import { RouteSwitchContext } from '@nocobase/client';
|
||||
|
||||
const HelloProvider = () => {
|
||||
const { routes, ...others } = useContext(RouteSwitchContext);
|
||||
routes[1].routes.unshift({
|
||||
path: '/hello',
|
||||
component: Hello,
|
||||
});
|
||||
|
||||
return <div>
|
||||
<RouteSwitchContext.Provider value={{ ...others, routes }}>
|
||||
{props.children}
|
||||
</RouteSwitchContext.Provider>
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
需要改为:
|
||||
|
||||
```diff | pure
|
||||
- import { RouteSwitchContext } from '@nocobase/client';
|
||||
+ import { Plugin } from '@nocobase/client';
|
||||
|
||||
const HelloProvider = (props) => {
|
||||
- const { routes, ...others } = useContext(RouteSwitchContext);
|
||||
- routes[1].routes.unshift({
|
||||
- path: '/hello',
|
||||
- component: Hello,
|
||||
- });
|
||||
|
||||
return <div>
|
||||
- <RouteSwitchContext.Provider value={{ ...others, routes }}>
|
||||
{props.children}
|
||||
- </RouteSwitchContext.Provider>
|
||||
</div>
|
||||
}
|
||||
|
||||
+ export class HelloPlugin extends Plugin {
|
||||
+ async load() {
|
||||
+ this.app.router.add('admin.hello', {
|
||||
+ path: '/hello',
|
||||
+ Component: Hello,
|
||||
+ });
|
||||
+ this.app.addProvider(HelloProvider);
|
||||
+ }
|
||||
+ }
|
||||
+ export default HelloPlugin;
|
||||
```
|
||||
|
||||
更多文档和示例见 [packages/core/client/src/application/index.md](https://github.com/nocobase/nocobase/blob/main/packages/core/client/src/application/index.md)
|
||||
|
||||
### antd 升级到 v5
|
||||
|
||||
- antd 相关详情查看官网 [从 v4 到 v5](https://ant.design/docs/react/migration-v5-cn)
|
||||
- `@formily/antd` 替换为 `@formily/antd-v5`
|
@ -44,6 +44,7 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"commander": "^9.2.0",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"react": "^18.0.0",
|
||||
@ -57,7 +58,7 @@
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"auto-changelog": "^2.4.0",
|
||||
"dumi": "^2.2.0",
|
||||
"dumi-theme-nocobase": "^0.2.12",
|
||||
"dumi-theme-nocobase": "^0.2.14",
|
||||
"ghooks": "^2.0.4",
|
||||
"jsdom-worker": "^0.3.0",
|
||||
"prettier": "^2.2.1",
|
||||
|
@ -20,8 +20,7 @@ export default defineConfig({
|
||||
{ rel: 'stylesheet', href: '/global.css' },
|
||||
],
|
||||
headScripts: [
|
||||
'/browser-checker.js',
|
||||
'/set-router.js',
|
||||
'/browser-checker.js'
|
||||
],
|
||||
hash: true,
|
||||
alias: {
|
||||
|
@ -1,2 +0,0 @@
|
||||
var match = location.pathname.match(/^\/apps\/([^/]*)\//);
|
||||
window.routerBase = match ? match[0] : "/";
|
@ -1,5 +1,6 @@
|
||||
import '@/theme';
|
||||
import { Application } from '@nocobase/client';
|
||||
import { NocoBaseClientPresetPlugin } from '@nocobase/preset-nocobase/client';
|
||||
|
||||
export const app = new Application({
|
||||
apiClient: {
|
||||
@ -8,6 +9,7 @@ export const app = new Application({
|
||||
dynamicImport: (name: string) => {
|
||||
return import(`../plugins/${name}`);
|
||||
},
|
||||
plugins: [NocoBaseClientPresetPlugin],
|
||||
});
|
||||
|
||||
export default app.render();
|
||||
export default app.getRootComponent();
|
||||
|
2
packages/app/client/src/plugins/acl.ts
Normal file
2
packages/app/client/src/plugins/acl.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default } from '@nocobase/plugin-acl/client';
|
||||
|
1
packages/app/client/src/plugins/client.ts
Normal file
1
packages/app/client/src/plugins/client.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@nocobase/plugin-client/client';
|
1
packages/app/client/src/plugins/collection-manager.ts
Normal file
1
packages/app/client/src/plugins/collection-manager.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@nocobase/plugin-collection-manager/client';
|
1
packages/app/client/src/plugins/error-handler.ts
Normal file
1
packages/app/client/src/plugins/error-handler.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@nocobase/plugin-error-handler/client';
|
1
packages/app/client/src/plugins/system-settings.ts
Normal file
1
packages/app/client/src/plugins/system-settings.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@nocobase/plugin-system-settings/client';
|
1
packages/app/client/src/plugins/ui-routes-storage.ts
Normal file
1
packages/app/client/src/plugins/ui-routes-storage.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@nocobase/plugin-ui-routes-storage/client';
|
1
packages/app/client/src/plugins/ui-schema-storage.ts
Normal file
1
packages/app/client/src/plugins/ui-schema-storage.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@nocobase/plugin-ui-schema-storage/client';
|
@ -11,13 +11,24 @@ import randomColor from './randomColor';
|
||||
import registerBabel from './registerBabel';
|
||||
import rollup from './rollup';
|
||||
import { Dispose, IBundleOptions, IBundleTypeOutput, ICjs, IEsm, IOpts } from './types';
|
||||
import { getExistFile, getLernaPackages } from './utils';
|
||||
import { getExistFiles, getLernaPackages } from './utils';
|
||||
|
||||
export function getBundleOpts(opts: IOpts): IBundleOptions[] {
|
||||
const { cwd, buildArgs = {}, rootConfig = {} } = opts;
|
||||
const entry = getExistFile({
|
||||
const entry = getExistFiles({
|
||||
cwd,
|
||||
files: ['src/index.tsx', 'src/index.ts', 'src/index.jsx', 'src/index.js'],
|
||||
files: [
|
||||
'src/index.tsx',
|
||||
'src/index.ts',
|
||||
'src/index.jsx',
|
||||
'src/index.js',
|
||||
'src/server/index.ts',
|
||||
'src/server/index.js',
|
||||
'src/client/index.js',
|
||||
'src/client/index.ts',
|
||||
'src/client/index.tsx'
|
||||
],
|
||||
onlyOne: false,
|
||||
returnRelative: true,
|
||||
});
|
||||
const userConfig = getUserConfig({ cwd, customPath: buildArgs.config });
|
||||
|
@ -5,7 +5,7 @@ import signale from 'signale';
|
||||
import slash from 'slash2';
|
||||
import schema from './schema';
|
||||
import { IBundleOptions } from './types';
|
||||
import { getExistFile } from './utils';
|
||||
import { getExistFiles } from './utils';
|
||||
|
||||
function testDefault(obj) {
|
||||
return obj.default || obj;
|
||||
@ -51,7 +51,7 @@ export default function({ cwd, customPath }: { cwd: string; customPath?: string
|
||||
|
||||
const configFile =
|
||||
finalPath ||
|
||||
getExistFile({
|
||||
getExistFiles({
|
||||
cwd,
|
||||
files: CONFIG_FILES,
|
||||
returnRelative: false,
|
||||
|
@ -1,13 +1,17 @@
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
export function getExistFile({ cwd, files, returnRelative }) {
|
||||
export function getExistFiles({ cwd, files, returnRelative, onlyOne = true }) {
|
||||
const res = [];
|
||||
for (const file of files) {
|
||||
const absFilePath = join(cwd, file);
|
||||
if (existsSync(absFilePath)) {
|
||||
return returnRelative ? file : absFilePath;
|
||||
const filePath = returnRelative ? file : absFilePath;
|
||||
res.push(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
return onlyOne ? res[0] : res; // undefined or string[]
|
||||
}
|
||||
|
||||
export { getLernaPackages } from './getLernaPackages';
|
||||
|
@ -1,3 +1,3 @@
|
||||
// @ts-nocheck
|
||||
export * from './lib/client';
|
||||
export { default } from './lib/client';
|
||||
export * from './src/client';
|
||||
export { default } from './src/client';
|
||||
|
||||
|
@ -1,65 +1 @@
|
||||
'use strict';
|
||||
|
||||
function _getRequireWildcardCache(nodeInterop) {
|
||||
if (typeof WeakMap !== 'function') return null;
|
||||
var cacheBabelInterop = new WeakMap();
|
||||
var cacheNodeInterop = new WeakMap();
|
||||
return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) {
|
||||
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
|
||||
})(nodeInterop);
|
||||
}
|
||||
|
||||
function _interopRequireWildcard(obj, nodeInterop) {
|
||||
if (!nodeInterop && obj && obj.__esModule) {
|
||||
return obj;
|
||||
}
|
||||
if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
|
||||
return { default: obj };
|
||||
}
|
||||
var cache = _getRequireWildcardCache(nodeInterop);
|
||||
if (cache && cache.has(obj)) {
|
||||
return cache.get(obj);
|
||||
}
|
||||
var newObj = {};
|
||||
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
|
||||
for (var key in obj) {
|
||||
if (key !== 'default' && Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
|
||||
if (desc && (desc.get || desc.set)) {
|
||||
Object.defineProperty(newObj, key, desc);
|
||||
} else {
|
||||
newObj[key] = obj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
newObj.default = obj;
|
||||
if (cache) {
|
||||
cache.set(obj, newObj);
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
var _index = _interopRequireWildcard(require('./lib/client'));
|
||||
|
||||
Object.defineProperty(exports, '__esModule', {
|
||||
value: true,
|
||||
});
|
||||
var _exportNames = {};
|
||||
Object.defineProperty(exports, 'default', {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index.default;
|
||||
},
|
||||
});
|
||||
|
||||
Object.keys(_index).forEach(function (key) {
|
||||
if (key === 'default' || key === '__esModule') return;
|
||||
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
||||
if (key in exports && exports[key] === _index[key]) return;
|
||||
Object.defineProperty(exports, key, {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index[key];
|
||||
},
|
||||
});
|
||||
});
|
||||
module.exports = require('./lib/client/index.js');
|
||||
|
@ -2,6 +2,17 @@
|
||||
"name": "{{{packageName}}}",
|
||||
"version": "{{{packageVersion}}}",
|
||||
"main": "lib/server/index.js",
|
||||
"files": [
|
||||
"lib",
|
||||
"src",
|
||||
"README.md",
|
||||
"README.zh-CN.md",
|
||||
"CHANGELOG.md",
|
||||
"server.js",
|
||||
"server.d.ts",
|
||||
"client.js",
|
||||
"client.d.ts"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@nocobase/server": "{{{nocobaseVersion}}}",
|
||||
"@nocobase/test": "{{{nocobaseVersion}}}"
|
||||
|
@ -1,3 +1,3 @@
|
||||
// @ts-nocheck
|
||||
export * from './lib/server';
|
||||
export { default } from './lib/server';
|
||||
export * from './src/server';
|
||||
export { default } from './src/server';
|
||||
|
||||
|
@ -1,65 +1 @@
|
||||
'use strict';
|
||||
|
||||
function _getRequireWildcardCache(nodeInterop) {
|
||||
if (typeof WeakMap !== 'function') return null;
|
||||
var cacheBabelInterop = new WeakMap();
|
||||
var cacheNodeInterop = new WeakMap();
|
||||
return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) {
|
||||
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
|
||||
})(nodeInterop);
|
||||
}
|
||||
|
||||
function _interopRequireWildcard(obj, nodeInterop) {
|
||||
if (!nodeInterop && obj && obj.__esModule) {
|
||||
return obj;
|
||||
}
|
||||
if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
|
||||
return { default: obj };
|
||||
}
|
||||
var cache = _getRequireWildcardCache(nodeInterop);
|
||||
if (cache && cache.has(obj)) {
|
||||
return cache.get(obj);
|
||||
}
|
||||
var newObj = {};
|
||||
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
|
||||
for (var key in obj) {
|
||||
if (key !== 'default' && Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
|
||||
if (desc && (desc.get || desc.set)) {
|
||||
Object.defineProperty(newObj, key, desc);
|
||||
} else {
|
||||
newObj[key] = obj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
newObj.default = obj;
|
||||
if (cache) {
|
||||
cache.set(obj, newObj);
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
var _index = _interopRequireWildcard(require('./lib/server'));
|
||||
|
||||
Object.defineProperty(exports, '__esModule', {
|
||||
value: true,
|
||||
});
|
||||
var _exportNames = {};
|
||||
Object.defineProperty(exports, 'default', {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index.default;
|
||||
},
|
||||
});
|
||||
|
||||
Object.keys(_index).forEach(function (key) {
|
||||
if (key === 'default' || key === '__esModule') return;
|
||||
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
||||
if (key in exports && exports[key] === _index[key]) return;
|
||||
Object.defineProperty(exports, key, {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index[key];
|
||||
},
|
||||
});
|
||||
});
|
||||
module.exports = require('./lib/server/index.js');
|
||||
|
@ -1,8 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const MyProvider = React.memo((props) => {
|
||||
return <>{props.children}</>;
|
||||
});
|
||||
MyProvider.displayName = 'MyProvider';
|
||||
|
||||
export default MyProvider;
|
21
packages/core/cli/templates/plugin/src/client/index.tsx.tpl
Normal file
21
packages/core/cli/templates/plugin/src/client/index.tsx.tpl
Normal file
@ -0,0 +1,21 @@
|
||||
import { Plugin } from '@nocobase/client';
|
||||
|
||||
export class {{{pascalCaseName}}}Plugin extends Plugin {
|
||||
async afterAdd() {
|
||||
// this.app.pm.add()
|
||||
}
|
||||
|
||||
async beforeLoad() {}
|
||||
|
||||
// You can get and modify the app instance here
|
||||
async load() {
|
||||
console.log(this.app);
|
||||
// this.app.addComponents({})
|
||||
// this.app.addScopes({})
|
||||
// this.app.addProvider()
|
||||
// this.app.addProviders()
|
||||
// this.app.router.add()
|
||||
}
|
||||
}
|
||||
|
||||
export default {{{pascalCaseName}}}Plugin;
|
@ -2,4 +2,4 @@
|
||||
sidebar: false
|
||||
---
|
||||
|
||||
<code src="../src/application/demos/demo2/index.tsx"></code>
|
||||
<code src="../src/application/demos/demo3.tsx"></code>
|
||||
|
@ -8,92 +8,48 @@ order: 1
|
||||
|
||||
示例:
|
||||
|
||||
```tsx | pure
|
||||
const app = new Application();
|
||||
|
||||
app.use([MemoryRouter, { initialEntries: ['/'] }]);
|
||||
|
||||
app.use(({ children }) => {
|
||||
const location = useLocation();
|
||||
if (location.pathname === '/hello') {
|
||||
return <div>Hello NocoBase!</div>;
|
||||
}
|
||||
return children;
|
||||
});
|
||||
|
||||
export default app.compose();
|
||||
```
|
||||
|
||||
## RouteSwitch
|
||||
|
||||
稍微复杂的应用都会用到路由来管理前端的页面,如下:
|
||||
|
||||
```jsx
|
||||
/**
|
||||
* defaultShowCode: true
|
||||
* title: Router
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Route, Routes, Link, MemoryRouter as Router } from 'react-router-dom';
|
||||
|
||||
const Home = () => <h1>Home</h1>;
|
||||
const About = () => <h1>About</h1>;
|
||||
|
||||
const App = () => (
|
||||
<Router initialEntries={['/']}>
|
||||
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />}></Route>
|
||||
<Route path="/about" element={<About />}></Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
上述例子,组件经由路由转发,`/` 转发给 `Home`,`/about` 转发给 `About`。这种 JSX 的写法,对于熟悉 JSX 的开发来说,十分便捷,但需要开发来编写和维护,不符合 NocoBase 低代码、无代码的设计理念。所以将 Route 做了封装和配置化改造,如下:
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* defaultShowCode: true
|
||||
* title: RouteSwitch
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Link, MemoryRouter as Router } from 'react-router-dom';
|
||||
import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client';
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { Application } from '@nocobase/client';
|
||||
|
||||
const Home = () => <h1>Home</h1>;
|
||||
const About = () => <h1>About</h1>;
|
||||
|
||||
const routes: RouteRedirectProps[] = [
|
||||
{
|
||||
type: 'route',
|
||||
path: '/',
|
||||
component: 'Home',
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/about',
|
||||
component: 'About',
|
||||
},
|
||||
];
|
||||
const Layout = () => {
|
||||
return <div>
|
||||
<div><Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link></div>
|
||||
<Outlet />
|
||||
</div>
|
||||
}
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<RouteSwitchProvider components={{ Home, About }}>
|
||||
<Router initialEntries={['/']}>
|
||||
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
|
||||
<RouteSwitch routes={routes} />
|
||||
</Router>
|
||||
</RouteSwitchProvider>
|
||||
);
|
||||
};
|
||||
const app = new Application({
|
||||
router: {
|
||||
type: 'memory',
|
||||
initialEntries: ['/']
|
||||
}
|
||||
})
|
||||
|
||||
app.router.add('root', {
|
||||
element: <Layout />
|
||||
})
|
||||
|
||||
app.router.add('root.home', {
|
||||
path: '/',
|
||||
element: <Home />
|
||||
})
|
||||
|
||||
app.router.add('root.about', {
|
||||
path: '/about',
|
||||
element: <About />
|
||||
})
|
||||
|
||||
export default app.getRootComponent();
|
||||
```
|
||||
|
||||
- 由 RouteSwitchProvider 配置 components,由开发编写,以 Layout 或 Template 的方式提供给 RouteSwitch 使用。
|
||||
- 由 RouteSwitch 配置 routes,JSON 的方式,可以由后端获取,方便后续的动态化、无代码的支持。
|
||||
|
||||
## SchemaComponent
|
||||
|
||||
路由可以通过 JSON 的方式配置,可以注册诸多可供路由使用的组件模板,以方便各种场景支持,但是这些组件还是需要开发编写和维护,所以进一步将组件抽象,转换成配置化的方式。如:
|
||||
@ -459,101 +415,6 @@ export default function App() {
|
||||
}
|
||||
```
|
||||
|
||||
## RouteSwitch + SchemaComponent
|
||||
|
||||
当路由和组件都可以配置之后,可以进一步将二者结合,例子如下:
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* defaultShowCode: true
|
||||
* title: RouteSwitch + SchemaComponent
|
||||
*/
|
||||
import React, { useMemo, useEffect } from 'react';
|
||||
import { Link, MemoryRouter as Router } from 'react-router-dom';
|
||||
import {
|
||||
RouteRedirectProps,
|
||||
RouteSwitchProvider,
|
||||
RouteSwitch,
|
||||
useRoute,
|
||||
SchemaComponentProvider,
|
||||
SchemaComponent,
|
||||
useDesignable,
|
||||
useSchemaComponentContext,
|
||||
} from '@nocobase/client';
|
||||
import { Spin, Button } from 'antd';
|
||||
import { observer, Schema } from '@formily/react';
|
||||
|
||||
const Hello = observer(({ name }) => {
|
||||
const { patch, remove } = useDesignable();
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello {name}!</h1>
|
||||
<Button
|
||||
onClick={() => {
|
||||
patch('x-component-props.name', Math.random());
|
||||
}}
|
||||
>更新</Button>
|
||||
</div>
|
||||
)
|
||||
}, { displayName: 'Hello' });
|
||||
|
||||
const RouteSchemaComponent = (props) => {
|
||||
const route = useRoute();
|
||||
const { reset } = useSchemaComponentContext();
|
||||
useEffect(() => {
|
||||
reset();
|
||||
}, route.schema);
|
||||
return <SchemaComponent schema={route.schema}/>
|
||||
}
|
||||
|
||||
const routes: RouteRedirectProps[] = [
|
||||
{
|
||||
type: 'route',
|
||||
path: '/',
|
||||
component: 'RouteSchemaComponent',
|
||||
schema: {
|
||||
name: 'home',
|
||||
'x-component': 'Hello',
|
||||
'x-component-props': {
|
||||
name: 'Home',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/about',
|
||||
component: 'RouteSchemaComponent',
|
||||
schema: {
|
||||
name: 'home',
|
||||
'x-component': 'Hello',
|
||||
'x-component-props': {
|
||||
name: 'About',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<SchemaComponentProvider components={{ Hello }}>
|
||||
<RouteSwitchProvider components={{ RouteSchemaComponent }}>
|
||||
<Router initialEntries={['/']}>
|
||||
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
|
||||
<RouteSwitch routes={routes} />
|
||||
</Router>
|
||||
</RouteSwitchProvider>
|
||||
</SchemaComponentProvider>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
以上例子实现了路由和组件层面的配置化,在开发层面配置了两个组件:
|
||||
|
||||
- `<RouteSchemaComponent/>` 简易的可以在路由里配置 schema 的方案
|
||||
- `<Hello/>` 自定义的 Schema 组件
|
||||
|
||||
为了让大家更加能感受到 Schema 组件的不一样之处,例子添加了一个简易的随机更新 `x-component-props.name` 值的按钮,当路由切换后,更新后的 name 并不会被重置。这也是 Schema 组件的 Designable 的能力,可以任意的动态更新 schema 配置,实时更新,实时渲染。
|
||||
|
||||
## Designable
|
||||
|
||||
SchemaComponent 基于 Formily 的 SchemaField,Formily 提供了 [Designable](https://github.com/alibaba/designable) 来解决 Schema 的配置问题,但是这套方案:
|
||||
@ -781,7 +642,6 @@ const { data, loading } = useRequest();
|
||||
|
||||
客户端的扩展以 Providers 的形式存在,提供各种可供组件使用的 Context,可全局也可以局部使用。上文我们已经介绍了核心的三个 Providers:
|
||||
|
||||
- RouteSwitchProvider,提供配置路由所需的 Layout 和 Template 组件
|
||||
- SchemaComponentProvider,提供配置 Schema 所需的各种组件
|
||||
- ApiClientProvider,提供客户端 SDK
|
||||
|
||||
@ -800,16 +660,14 @@ const { data, loading } = useRequest();
|
||||
```tsx | pure
|
||||
<ApiClientProvider>
|
||||
<SchemaComponentProvider>
|
||||
<RouteSwitchProvider>
|
||||
{...}
|
||||
</RouteSwitchProvider>
|
||||
</SchemaComponentProvider>
|
||||
</ApiClientProvider>
|
||||
```
|
||||
|
||||
但是这样的方式不利于 Providers 的管理和扩展,为此提炼了 `compose()` 函数用于配置多个 providers,如下:
|
||||
|
||||
<code id='intro-demo2' defaultShowCode="true" titile="compose" src="../src/application/demos/demo1/index.tsx"></code>
|
||||
<code id='intro-demo2' defaultShowCode="true" titile="compose" src="../src/application/demos/demo1.tsx"></code>
|
||||
|
||||
## Application
|
||||
|
||||
|
@ -53,12 +53,12 @@
|
||||
"react-is": ">=18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dumi": "^2.2.0",
|
||||
"dumi-theme-nocobase": "^0.2.14",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@types/markdown-it": "12.2.3",
|
||||
"@types/markdown-it-highlightjs": "3.3.1",
|
||||
"axios-mock-adapter": "^1.20.0",
|
||||
"dumi": "^2.2.0",
|
||||
"dumi-theme-nocobase": "^0.2.9"
|
||||
"axios-mock-adapter": "^1.20.0"
|
||||
},
|
||||
"gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644"
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import { SchemaComponentOptions, useDesignable } from '../schema-component';
|
||||
|
||||
export const ACLContext = createContext<any>({});
|
||||
|
||||
// TODO: delete this,replace by `ACLPlugin`
|
||||
export const ACLProvider = (props) => {
|
||||
return (
|
||||
<SchemaComponentOptions
|
||||
|
@ -1,4 +1,22 @@
|
||||
export * from './ACLProvider';
|
||||
export * from './ACLShortcut';
|
||||
|
||||
import { Plugin } from '../application/Plugin';
|
||||
import {
|
||||
ACLActionProvider,
|
||||
ACLCollectionFieldProvider,
|
||||
ACLCollectionProvider,
|
||||
ACLMenuItemProvider,
|
||||
} from './ACLProvider';
|
||||
import './style.less';
|
||||
|
||||
export class ACLPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.addComponents({
|
||||
ACLCollectionFieldProvider,
|
||||
ACLActionProvider,
|
||||
ACLMenuItemProvider,
|
||||
ACLCollectionProvider,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import moment from 'moment';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAPIClient, useRequest } from '../api-client';
|
||||
import { Plugin } from '../application/Plugin';
|
||||
import { loadConstrueLocale } from './loadConstrueLocale';
|
||||
|
||||
export const AppLangContext = createContext<any>({});
|
||||
@ -50,3 +51,9 @@ export function AntdConfigProvider(props) {
|
||||
</AppLangContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export class AntdConfigPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.use(AntdConfigProvider, this.options?.config || {});
|
||||
}
|
||||
}
|
||||
|
@ -1,91 +0,0 @@
|
||||
import { i18n } from 'i18next';
|
||||
import { merge } from 'lodash';
|
||||
import get from 'lodash/get';
|
||||
import set from 'lodash/set';
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { APIClient, APIClientProvider } from '../api-client';
|
||||
import { Plugin } from './Plugin';
|
||||
import { PluginManager } from './PluginManager';
|
||||
import { Router } from './Router';
|
||||
import { AppComponent, defaultAppComponents } from './components';
|
||||
import { ApplicationOptions } from './types';
|
||||
|
||||
export class Application {
|
||||
providers: any[];
|
||||
router: Router;
|
||||
plugins: Map<string, Plugin>;
|
||||
scopes: Record<string, any>;
|
||||
i18n: i18n;
|
||||
apiClient: APIClient;
|
||||
components: any;
|
||||
pm: PluginManager;
|
||||
|
||||
constructor(protected _options: ApplicationOptions) {
|
||||
this.providers = [];
|
||||
this.plugins = new Map<string, Plugin>();
|
||||
this.scopes = merge(this.scopes, _options.scopes || {});
|
||||
this.components = merge(defaultAppComponents, _options.components || {});
|
||||
this.apiClient = new APIClient(_options.apiClient);
|
||||
this.router = new Router(_options.router, { app: this });
|
||||
this.pm = new PluginManager(this);
|
||||
this.useDefaultProviders();
|
||||
}
|
||||
|
||||
get options() {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
useDefaultProviders() {
|
||||
this.use([APIClientProvider, { apiClient: this.apiClient }]);
|
||||
this.use(I18nextProvider, { i18n: this.i18n });
|
||||
}
|
||||
|
||||
getPlugin(name: string) {
|
||||
return this.plugins.get(name);
|
||||
}
|
||||
|
||||
getComponent(name: string) {
|
||||
return get(this.components, name);
|
||||
}
|
||||
|
||||
renderComponent(name: string, props = {}) {
|
||||
return React.createElement(this.getComponent(name), props);
|
||||
}
|
||||
|
||||
registerComponent(name: string, component: any) {
|
||||
set(this.components, name, component);
|
||||
}
|
||||
|
||||
registerComponents(components: any) {
|
||||
Object.keys(components).forEach((name) => {
|
||||
this.registerComponent(name, components[name]);
|
||||
});
|
||||
}
|
||||
|
||||
registerScopes(scopes: Record<string, any>) {
|
||||
this.scopes = merge(this.scopes, scopes);
|
||||
}
|
||||
|
||||
use(component: any, props?: any) {
|
||||
this.providers.push(props ? [component, props] : component);
|
||||
}
|
||||
|
||||
async load() {
|
||||
return this.pm.load();
|
||||
}
|
||||
|
||||
getRootComponent() {
|
||||
return () => <AppComponent app={this} />;
|
||||
}
|
||||
|
||||
mount(selector: string) {
|
||||
const container = typeof selector === 'string' ? document.querySelector(selector) : selector;
|
||||
if (container) {
|
||||
const App = this.getRootComponent();
|
||||
const root = createRoot(container);
|
||||
root.render(<App />);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import { Application } from './Application';
|
||||
import { PluginOptions } from './types';
|
||||
|
||||
export class Plugin {
|
||||
constructor(protected _options: PluginOptions, protected app: Application) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
get options() {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this._options.name;
|
||||
}
|
||||
|
||||
get pm() {
|
||||
return this.app.pm;
|
||||
}
|
||||
|
||||
get router() {
|
||||
return this.app.router;
|
||||
}
|
||||
|
||||
async afterAdd() {}
|
||||
|
||||
async beforeLoad() {}
|
||||
|
||||
async load() {}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import { Application } from './Application';
|
||||
import { Plugin } from './Plugin';
|
||||
import { type PluginOptions } from './types';
|
||||
|
||||
export interface PluginManagerOptions {
|
||||
plugins: string[];
|
||||
}
|
||||
|
||||
type PluginNameOrClass = string | typeof Plugin;
|
||||
|
||||
export class PluginManager {
|
||||
protected pluginInstances: Map<string, Plugin>;
|
||||
protected pluginPrepares: Map<string, any>;
|
||||
|
||||
constructor(protected app: Application) {
|
||||
this.pluginInstances = new Map();
|
||||
this.pluginPrepares = new Map();
|
||||
this.addPresetPlugins();
|
||||
}
|
||||
|
||||
protected addPresetPlugins() {
|
||||
const { plugins } = this.app.options;
|
||||
for (const plugin of plugins) {
|
||||
if (typeof plugin === 'string') {
|
||||
this.prepare(plugin);
|
||||
} else {
|
||||
this.prepare(...plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prepare(nameOrClass: PluginNameOrClass, options?: PluginOptions) {
|
||||
let opts: any = {};
|
||||
if (typeof nameOrClass === 'string') {
|
||||
opts['name'] = nameOrClass;
|
||||
} else {
|
||||
opts = { ...options, Plugin: nameOrClass };
|
||||
}
|
||||
return this.pluginPrepares.set(opts.name, opts);
|
||||
}
|
||||
|
||||
async add(nameOrClass: PluginNameOrClass, options?: PluginOptions) {
|
||||
let opts: any = {};
|
||||
if (typeof nameOrClass === 'string') {
|
||||
opts['name'] = nameOrClass;
|
||||
} else {
|
||||
opts = { ...options, Plugin: nameOrClass };
|
||||
}
|
||||
const plugin = await this.makePlugin(opts);
|
||||
this.pluginInstances.set(plugin.name, plugin);
|
||||
await plugin.afterAdd();
|
||||
return plugin;
|
||||
}
|
||||
|
||||
async makePlugin(opts) {
|
||||
const { importPlugins } = this.app.options;
|
||||
let P: typeof Plugin = opts.Plugin;
|
||||
if (!P) {
|
||||
P = await importPlugins(opts.name);
|
||||
}
|
||||
if (!P) {
|
||||
throw new Error(`Plugin "${opts.name} " not found`);
|
||||
}
|
||||
console.log(opts, P);
|
||||
return new P(opts, this.app);
|
||||
}
|
||||
|
||||
async load() {
|
||||
for (const opts of this.pluginPrepares.values()) {
|
||||
const plugin = await this.makePlugin(opts);
|
||||
this.pluginInstances.set(plugin.name, plugin);
|
||||
await plugin.afterAdd();
|
||||
}
|
||||
|
||||
for (const plugin of this.pluginInstances.values()) {
|
||||
await plugin.beforeLoad();
|
||||
}
|
||||
|
||||
for (const plugin of this.pluginInstances.values()) {
|
||||
await plugin.load();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
import set from 'lodash/set';
|
||||
import { createBrowserRouter, createHashRouter, createMemoryRouter } from 'react-router-dom';
|
||||
import { Application } from './Application';
|
||||
import { RouterOptions } from './types';
|
||||
|
||||
export class Router {
|
||||
protected app: Application;
|
||||
protected routes: Map<string, any>;
|
||||
|
||||
constructor(protected options?: RouterOptions, protected context?: any) {
|
||||
this.routes = new Map<string, any>();
|
||||
this.app = context.app;
|
||||
}
|
||||
|
||||
getRoutes() {
|
||||
const routes = {};
|
||||
for (const [name, route] of this.routes) {
|
||||
set(routes, name.split('.').join('.children.'), route);
|
||||
}
|
||||
|
||||
const transform = (item) => {
|
||||
if (item.component) {
|
||||
item.Component = this.app.getComponent(item.component);
|
||||
}
|
||||
return item;
|
||||
};
|
||||
|
||||
const toArr = (items: any) => {
|
||||
return Object.values<any>(items || {}).map((item) => {
|
||||
if (item.children) {
|
||||
item.children = toArr(item.children);
|
||||
}
|
||||
return transform(item);
|
||||
});
|
||||
};
|
||||
return toArr(routes);
|
||||
}
|
||||
|
||||
createRouter() {
|
||||
const { type, ...opts } = this.options;
|
||||
const routes = this.getRoutes();
|
||||
if (routes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
switch (type) {
|
||||
case 'hash':
|
||||
return createHashRouter(routes, opts) as any;
|
||||
case 'browser':
|
||||
return createBrowserRouter(routes, opts) as any;
|
||||
case 'memory':
|
||||
return createMemoryRouter(routes, opts) as any;
|
||||
default:
|
||||
return createMemoryRouter(routes, opts) as any;
|
||||
}
|
||||
}
|
||||
|
||||
add(name: string, route: any) {
|
||||
this.routes.set(name, route);
|
||||
Object.keys(route.children || {}).forEach((key) => {
|
||||
this.routes.set(`${name}.${key}`, route.children[key]);
|
||||
});
|
||||
}
|
||||
|
||||
remove(name: string) {
|
||||
this.routes.delete(name);
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ApplicationContext } from '../context';
|
||||
import { useApp, useLoad } from '../hooks';
|
||||
|
||||
const Internal = React.memo(() => {
|
||||
const app = useApp();
|
||||
const loading = useLoad();
|
||||
if (loading) {
|
||||
return app.renderComponent('App.Spin');
|
||||
}
|
||||
return app.renderComponent('App.Main', {
|
||||
app,
|
||||
providers: app.providers,
|
||||
});
|
||||
});
|
||||
|
||||
export const AppComponent = (props) => {
|
||||
const { app } = props;
|
||||
return (
|
||||
<ApplicationContext.Provider value={app}>
|
||||
<Internal />
|
||||
</ApplicationContext.Provider>
|
||||
);
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Application } from '../Application';
|
||||
import { RouterProvider } from './RouterProvider';
|
||||
|
||||
export const MainComponent = React.memo((props: { app: Application; providers: any[] }) => {
|
||||
const { app, providers } = props;
|
||||
const router = useMemo(() => app.router.createRouter(), []);
|
||||
return <RouterProvider router={router} providers={providers} />;
|
||||
});
|
@ -1,119 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import type { RouterState } from '@remix-run/router';
|
||||
import React from 'react';
|
||||
import {
|
||||
UNSAFE_DataRouterContext as DataRouterContext,
|
||||
UNSAFE_DataRouterStateContext as DataRouterStateContext,
|
||||
UNSAFE_LocationContext as LocationContext,
|
||||
UNSAFE_RouteContext as RouteContext,
|
||||
Router,
|
||||
UNSAFE_useRoutesImpl as useRoutesImpl,
|
||||
type DataRouteObject,
|
||||
type RouterProviderProps,
|
||||
} from 'react-router';
|
||||
import { compose } from '../compose';
|
||||
|
||||
const START_TRANSITION = 'startTransition';
|
||||
|
||||
/**
|
||||
* Given a Remix Router instance, render the appropriate UI
|
||||
*/
|
||||
export function RouterProvider({
|
||||
fallbackElement,
|
||||
router,
|
||||
providers,
|
||||
}: RouterProviderProps & { providers?: any }): React.ReactElement {
|
||||
// Need to use a layout effect here so we are subscribed early enough to
|
||||
// pick up on any render-driven redirects/navigations (useEffect/<Navigate>)
|
||||
const [state, setStateImpl] = React.useState(router.state);
|
||||
const setState = React.useCallback(
|
||||
(newState: RouterState) => {
|
||||
START_TRANSITION in React ? React[START_TRANSITION](() => setStateImpl(newState)) : setStateImpl(newState);
|
||||
},
|
||||
[setStateImpl],
|
||||
);
|
||||
React.useLayoutEffect(() => router.subscribe(setState), [router, setState]);
|
||||
|
||||
const navigator = React.useMemo((): Navigator => {
|
||||
return {
|
||||
createHref: router.createHref,
|
||||
encodeLocation: router.encodeLocation,
|
||||
go: (n) => router.navigate(n),
|
||||
push: (to, state, opts) =>
|
||||
router.navigate(to, {
|
||||
state,
|
||||
preventScrollReset: opts?.preventScrollReset,
|
||||
}),
|
||||
replace: (to, state, opts) =>
|
||||
router.navigate(to, {
|
||||
replace: true,
|
||||
state,
|
||||
preventScrollReset: opts?.preventScrollReset,
|
||||
}),
|
||||
};
|
||||
}, [router]);
|
||||
|
||||
const basename = router.basename || '/';
|
||||
|
||||
const dataRouterContext = React.useMemo(
|
||||
() => ({
|
||||
router,
|
||||
navigator,
|
||||
static: false,
|
||||
basename,
|
||||
}),
|
||||
[router, navigator, basename],
|
||||
);
|
||||
const Providers = compose(...providers)((props) => <>{props.children}</>);
|
||||
// The fragment and {null} here are important! We need them to keep React 18's
|
||||
// useId happy when we are server-rendering since we may have a <script> here
|
||||
// containing the hydrated server-side staticContext (from StaticRouterProvider).
|
||||
// useId relies on the component tree structure to generate deterministic id's
|
||||
// so we need to ensure it remains the same on the client even though
|
||||
// we don't need the <script> tag
|
||||
return (
|
||||
<>
|
||||
<RouterContextCleaner>
|
||||
<DataRouterContext.Provider value={dataRouterContext}>
|
||||
<DataRouterStateContext.Provider value={state}>
|
||||
<Router
|
||||
basename={basename}
|
||||
location={state.location}
|
||||
navigationType={state.historyAction}
|
||||
navigator={navigator}
|
||||
>
|
||||
<Providers>
|
||||
{state.initialized ? <DataRoutes routes={router.routes} state={state} /> : fallbackElement}
|
||||
</Providers>
|
||||
</Router>
|
||||
</DataRouterStateContext.Provider>
|
||||
</DataRouterContext.Provider>
|
||||
</RouterContextCleaner>
|
||||
{null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function DataRoutes({
|
||||
routes,
|
||||
state,
|
||||
}: {
|
||||
routes: DataRouteObject[];
|
||||
state: RouterState;
|
||||
}): React.ReactElement | null {
|
||||
return useRoutesImpl(routes, undefined, state);
|
||||
}
|
||||
|
||||
export const RouterContextCleaner = (props) => {
|
||||
return (
|
||||
<RouteContext.Provider
|
||||
value={{
|
||||
outlet: null,
|
||||
matches: [],
|
||||
isDataRoute: false,
|
||||
}}
|
||||
>
|
||||
<LocationContext.Provider value={null as any}>{props.children}</LocationContext.Provider>
|
||||
</RouteContext.Provider>
|
||||
);
|
||||
};
|
@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import { MainComponent } from './MainComponent';
|
||||
|
||||
export * from './AppComponent';
|
||||
export * from './MainComponent';
|
||||
export * from './RouterProvider';
|
||||
|
||||
export const defaultAppComponents = {
|
||||
App: {
|
||||
Main: MainComponent,
|
||||
Spin: () => React.createElement('div', 'loading'),
|
||||
},
|
||||
};
|
@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const Blank = ({ children }) => children || null;
|
||||
|
||||
export const compose = (...components: any[]) => {
|
||||
const Root = [...components, Blank].reduce((parent, child) => {
|
||||
const [Parent, parentProps] = Array.isArray(parent) ? parent : [parent];
|
||||
const [Child, childProps] = Array.isArray(child) ? child : [child];
|
||||
return ({ children }) => (
|
||||
<Parent {...parentProps}>
|
||||
<Child {...childProps}>{children}</Child>
|
||||
</Parent>
|
||||
);
|
||||
});
|
||||
return (LastChild?: any) => (props?: any) => {
|
||||
return <Root>{LastChild && <LastChild {...props} />}</Root>;
|
||||
};
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
export * from './useApp';
|
||||
export * from './useLoad';
|
||||
export * from './useRouter';
|
@ -1,16 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useApp } from './useApp';
|
||||
|
||||
export const useLoad = () => {
|
||||
const app = useApp();
|
||||
const [loading, setLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
(async () => {
|
||||
await app.load();
|
||||
setLoading(false);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
return loading;
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
# Application V2
|
||||
|
||||
<code src="./demos/demo1.tsx">Demo</code>
|
@ -1,3 +0,0 @@
|
||||
export * from './Application';
|
||||
export * from './Plugin';
|
||||
export * from './compose';
|
@ -1,37 +0,0 @@
|
||||
import { Plugin } from './Plugin';
|
||||
|
||||
export interface HashRouterOptions {
|
||||
type: 'hash';
|
||||
basename?: string;
|
||||
// TODO: 补充 hash 参数
|
||||
}
|
||||
|
||||
export interface BrowserRouterOptions {
|
||||
type: 'browser';
|
||||
basename?: string;
|
||||
// TODO: 补充 browser 参数
|
||||
}
|
||||
|
||||
export interface MemoryRouterOptions {
|
||||
type: 'memory';
|
||||
basename?: string;
|
||||
// TODO: 补充 memory 参数
|
||||
}
|
||||
|
||||
export type RouterOptions = HashRouterOptions | BrowserRouterOptions | MemoryRouterOptions;
|
||||
|
||||
export interface PluginOptions {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type PluginNameOrClass = string | [typeof Plugin, PluginOptions];
|
||||
|
||||
export interface ApplicationOptions {
|
||||
apiClient?: any;
|
||||
// List of preset plugins
|
||||
plugins?: PluginNameOrClass[];
|
||||
components?: any;
|
||||
scopes?: Record<string, any>;
|
||||
router?: RouterOptions;
|
||||
importPlugins?: (name: string) => Promise<any>;
|
||||
}
|
@ -1,166 +1,158 @@
|
||||
import { Spin } from 'antd';
|
||||
import { APIClientOptions } from '@nocobase/sdk';
|
||||
import { i18n as i18next } from 'i18next';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import get from 'lodash/get';
|
||||
import merge from 'lodash/merge';
|
||||
import set from 'lodash/set';
|
||||
import React, { ComponentType, FC, ReactElement } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import { ACLProvider } from '../acl';
|
||||
import { AntdConfigProvider } from '../antd-config-provider';
|
||||
import { Link, Navigate, NavLink } from 'react-router-dom';
|
||||
import { APIClient, APIClientProvider } from '../api-client';
|
||||
import { SigninPage, SignupPage } from '../auth';
|
||||
import { SigninPageExtensionProvider } from '../auth/SigninPageExtension';
|
||||
import { BlockSchemaComponentProvider } from '../block-provider';
|
||||
import { RemoteDocumentTitleProvider } from '../document-title';
|
||||
import { i18n } from '../i18n';
|
||||
import { PinnedPluginListProvider } from '../plugin-manager';
|
||||
import PMProvider, { PluginManagerLink, SettingsCenterDropdown } from '../pm';
|
||||
import {
|
||||
AdminLayout,
|
||||
AuthLayout,
|
||||
RemoteRouteSwitchProvider,
|
||||
RouteSchemaComponent,
|
||||
RouteSwitch,
|
||||
useRoutes,
|
||||
} from '../route-switch';
|
||||
import {
|
||||
AntdSchemaComponentProvider,
|
||||
DesignableSwitch,
|
||||
MenuItemInitializers,
|
||||
SchemaComponentProvider,
|
||||
} from '../schema-component';
|
||||
import { ErrorFallback } from '../schema-component/antd/error-fallback';
|
||||
import { SchemaInitializerProvider } from '../schema-initializer';
|
||||
import { BlockTemplateDetails, BlockTemplatePage } from '../schema-templates';
|
||||
import { SystemSettingsProvider } from '../system-settings';
|
||||
import { compose } from './compose';
|
||||
import { AppComponent, BlankComponent, defaultAppComponents } from './components';
|
||||
import { PluginManager, PluginType } from './PluginManager';
|
||||
import { ComponentTypeAndString, RouterManager, RouterOptions } from './RouterManager';
|
||||
import { compose, normalizeContainer } from './utils';
|
||||
|
||||
export type ComponentAndProps<T = any> = [ComponentType, T];
|
||||
export interface ApplicationOptions {
|
||||
apiClient?: any;
|
||||
i18n?: any;
|
||||
plugins?: any[];
|
||||
apiClient?: APIClientOptions;
|
||||
i18n?: i18next;
|
||||
providers?: (ComponentType | ComponentAndProps)[];
|
||||
plugins?: PluginType[];
|
||||
components?: Record<string, ComponentType>;
|
||||
scopes?: Record<string, any>;
|
||||
router?: RouterOptions;
|
||||
dynamicImport?: any;
|
||||
}
|
||||
|
||||
export const getCurrentTimezone = () => {
|
||||
const timezoneOffset = new Date().getTimezoneOffset() / -60;
|
||||
const timezone = String(timezoneOffset).padStart(2, '0') + ':00';
|
||||
return (timezoneOffset > 0 ? '+' : '-') + timezone;
|
||||
};
|
||||
|
||||
export type PluginCallback = () => Promise<any>;
|
||||
|
||||
const App = React.memo((props: any) => {
|
||||
const C = compose(...props.providers)(() => {
|
||||
const routes = useRoutes();
|
||||
return <RouteSwitch routes={routes} />;
|
||||
});
|
||||
return <C />;
|
||||
});
|
||||
|
||||
App.displayName = 'App';
|
||||
|
||||
export class Application {
|
||||
providers = [];
|
||||
mainComponent = null;
|
||||
apiClient: APIClient;
|
||||
i18n: i18next;
|
||||
plugins: PluginCallback[] = [];
|
||||
options: ApplicationOptions;
|
||||
public providers: ComponentAndProps[] = [];
|
||||
public router: RouterManager;
|
||||
public scopes: Record<string, any> = {};
|
||||
public i18n: i18next;
|
||||
public apiClient: APIClient;
|
||||
public components: Record<string, ComponentType> = { ...defaultAppComponents };
|
||||
public pm: PluginManager;
|
||||
|
||||
constructor(options: ApplicationOptions) {
|
||||
this.options = options;
|
||||
this.apiClient = new APIClient({
|
||||
baseURL: process.env.API_BASE_URL,
|
||||
headers: {
|
||||
'X-Hostname': window?.location?.hostname,
|
||||
'X-Timezone': getCurrentTimezone(),
|
||||
},
|
||||
...options.apiClient,
|
||||
});
|
||||
constructor(protected options: ApplicationOptions = {}) {
|
||||
this.scopes = merge(this.scopes, options.scopes);
|
||||
this.components = merge(this.components, options.components);
|
||||
this.apiClient = new APIClient(options.apiClient);
|
||||
this.i18n = options.i18n || i18n;
|
||||
this.router = new RouterManager({
|
||||
...options.router,
|
||||
renderComponent: this.renderComponent.bind(this),
|
||||
});
|
||||
this.pm = new PluginManager(options.plugins, this);
|
||||
this.addDefaultProviders();
|
||||
this.addReactRouterComponents();
|
||||
this.addProviders(options.providers || []);
|
||||
}
|
||||
|
||||
private addDefaultProviders() {
|
||||
this.use(APIClientProvider, { apiClient: this.apiClient });
|
||||
this.use(I18nextProvider, { i18n: this.i18n });
|
||||
this.use(AntdConfigProvider, { remoteLocale: true });
|
||||
this.use(RemoteRouteSwitchProvider, {
|
||||
components: {
|
||||
AuthLayout,
|
||||
AdminLayout,
|
||||
RouteSchemaComponent,
|
||||
SigninPage,
|
||||
SignupPage,
|
||||
BlockTemplatePage,
|
||||
BlockTemplateDetails,
|
||||
},
|
||||
}
|
||||
|
||||
private addReactRouterComponents() {
|
||||
this.addComponents({
|
||||
Link,
|
||||
Navigate: Navigate as ComponentType,
|
||||
NavLink,
|
||||
});
|
||||
this.use(SystemSettingsProvider);
|
||||
this.use(PinnedPluginListProvider, {
|
||||
items: {
|
||||
ui: { order: 100, component: 'DesignableSwitch', pin: true, snippet: 'ui.*' },
|
||||
pm: { order: 200, component: 'PluginManagerLink', pin: true, snippet: 'pm' },
|
||||
sc: { order: 300, component: 'SettingsCenterDropdown', pin: true, snippet: 'pm.*' },
|
||||
},
|
||||
});
|
||||
this.use(SchemaComponentProvider, {
|
||||
components: { Link, NavLink, DesignableSwitch, PluginManagerLink, SettingsCenterDropdown },
|
||||
});
|
||||
this.use(SchemaInitializerProvider, {
|
||||
initializers: {
|
||||
MenuItemInitializers,
|
||||
},
|
||||
});
|
||||
this.use(BlockSchemaComponentProvider);
|
||||
this.use(AntdSchemaComponentProvider);
|
||||
this.use(SigninPageExtensionProvider);
|
||||
this.use(ACLProvider);
|
||||
this.use(RemoteDocumentTitleProvider);
|
||||
this.use(PMProvider);
|
||||
}
|
||||
|
||||
use(component, props?: any) {
|
||||
this.providers.push(props ? [component, props] : component);
|
||||
get dynamicImport() {
|
||||
return this.options.dynamicImport;
|
||||
}
|
||||
|
||||
main(mainComponent: any) {
|
||||
this.mainComponent = mainComponent;
|
||||
getComposeProviders() {
|
||||
const Providers = compose(...this.providers)(BlankComponent);
|
||||
Providers.displayName = 'Providers';
|
||||
return Providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
plugin(plugin: PluginCallback) {
|
||||
this.plugins.push(plugin);
|
||||
use<T = any>(component: ComponentType, props?: T) {
|
||||
return this.addProvider(component, props);
|
||||
}
|
||||
|
||||
handleErrors(error: any) {
|
||||
console.error(error);
|
||||
addProvider<T = any>(component: ComponentType, props?: T) {
|
||||
return this.providers.push([component, props]);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (props: any) => {
|
||||
const { plugins = [], dynamicImport } = this.options;
|
||||
const [loading, setLoading] = useState(false);
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
(async () => {
|
||||
const res = await this.apiClient.request({ url: 'app:getPlugins' });
|
||||
if (Array.isArray(res.data?.data)) {
|
||||
plugins.push(...res.data.data);
|
||||
}
|
||||
for (const plugin of plugins) {
|
||||
const pluginModule = await dynamicImport(plugin);
|
||||
this.use(pluginModule.default);
|
||||
}
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
addProviders(providers: (ComponentType | [ComponentType, any])[]) {
|
||||
providers.forEach((provider) => {
|
||||
if (Array.isArray(provider)) {
|
||||
this.addProvider(provider[0], provider[1]);
|
||||
} else {
|
||||
this.addProvider(provider);
|
||||
}
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback} onError={this.handleErrors}>
|
||||
<App providers={this.providers} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
return this.pm.load();
|
||||
}
|
||||
|
||||
getComponent<T = any>(Component: ComponentTypeAndString<T>, isShowError = true): ComponentType<T> | undefined {
|
||||
const showError = (msg: string) => isShowError && console.error(msg);
|
||||
if (!Component) {
|
||||
showError(`getComponent called with ${Component}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// ClassComponent or FunctionComponent
|
||||
if (typeof Component === 'function') return Component;
|
||||
|
||||
// Component is a string, try to get it from this.components
|
||||
if (typeof Component === 'string') {
|
||||
const res = get(this.components, Component) as ComponentType<T>;
|
||||
if (!res) {
|
||||
showError(`Component ${Component} not found`);
|
||||
return;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
showError(`Component ${Component} should be a string or a React component`);
|
||||
return;
|
||||
}
|
||||
|
||||
renderComponent<T extends {}>(Component: ComponentTypeAndString, props?: T): ReactElement {
|
||||
return React.createElement(this.getComponent(Component), props);
|
||||
}
|
||||
|
||||
addComponent(component: ComponentType, name?: string) {
|
||||
const componentName = name || component.displayName || component.name;
|
||||
if (!componentName) {
|
||||
console.error('Component must have a displayName or pass name as second argument');
|
||||
return;
|
||||
}
|
||||
set(this.components, componentName, component);
|
||||
}
|
||||
|
||||
addComponents(components: Record<string, ComponentType>) {
|
||||
Object.keys(components).forEach((name) => {
|
||||
this.addComponent(components[name], name);
|
||||
});
|
||||
}
|
||||
|
||||
addScopes(scopes: Record<string, any>) {
|
||||
this.scopes = merge(this.scopes, scopes);
|
||||
}
|
||||
|
||||
getRootComponent() {
|
||||
const Root: FC = () => <AppComponent app={this} />;
|
||||
return Root;
|
||||
}
|
||||
|
||||
mount(containerOrSelector: Element | ShadowRoot | string) {
|
||||
const container = normalizeContainer(containerOrSelector);
|
||||
if (!container) return;
|
||||
const App = this.getRootComponent();
|
||||
const root = createRoot(container);
|
||||
root.render(<App />);
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
22
packages/core/client/src/application/Plugin.ts
Normal file
22
packages/core/client/src/application/Plugin.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Application } from './Application';
|
||||
|
||||
export class Plugin<T = any> {
|
||||
constructor(protected options: T, protected app: Application) {
|
||||
this.options = options;
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
get pm() {
|
||||
return this.app.pm;
|
||||
}
|
||||
|
||||
get router() {
|
||||
return this.app.router;
|
||||
}
|
||||
|
||||
async afterAdd() {}
|
||||
|
||||
async beforeLoad() {}
|
||||
|
||||
async load() {}
|
||||
}
|
59
packages/core/client/src/application/PluginManager.ts
Normal file
59
packages/core/client/src/application/PluginManager.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import type { Application } from './Application';
|
||||
import type { Plugin } from './Plugin';
|
||||
|
||||
export type PluginOptions<T = any> = { name?: string; config?: T };
|
||||
export type PluginType<Opts = any> = typeof Plugin | [typeof Plugin, PluginOptions<Opts>];
|
||||
|
||||
export class PluginManager {
|
||||
protected pluginInstances: Map<typeof Plugin, Plugin> = new Map();
|
||||
protected pluginsAliases: Record<string, Plugin> = {};
|
||||
private loadStaticPlugin: Promise<void>;
|
||||
constructor(protected _plugins: PluginType[], protected app: Application) {
|
||||
this.app = app;
|
||||
this.loadStaticPlugin = this.initStaticPlugins(_plugins);
|
||||
}
|
||||
|
||||
private async initStaticPlugins(_plugins: PluginType[] = []) {
|
||||
for await (const plugin of _plugins) {
|
||||
const pluginClass = Array.isArray(plugin) ? plugin[0] : plugin;
|
||||
const opts = Array.isArray(plugin) ? plugin[1] : undefined;
|
||||
await this.add(pluginClass, opts);
|
||||
}
|
||||
}
|
||||
|
||||
async add<T = any>(plugin: typeof Plugin, opts: PluginOptions<T> = {}) {
|
||||
const instance = this.getInstance(plugin, opts);
|
||||
|
||||
this.pluginInstances.set(plugin, instance);
|
||||
|
||||
if (opts.name) {
|
||||
this.pluginsAliases[opts.name] = instance;
|
||||
}
|
||||
await instance.afterAdd();
|
||||
}
|
||||
|
||||
get<T extends typeof Plugin>(PluginClass: T): InstanceType<T>;
|
||||
get<T extends {}>(name: string): T;
|
||||
get(name: any) {
|
||||
if (typeof name === 'string') {
|
||||
return this.pluginsAliases[name];
|
||||
}
|
||||
return this.pluginInstances.get(name);
|
||||
}
|
||||
|
||||
private getInstance<T>(plugin: typeof Plugin, opts?: T) {
|
||||
return new plugin(opts, this.app);
|
||||
}
|
||||
|
||||
async load() {
|
||||
await this.loadStaticPlugin;
|
||||
|
||||
for (const plugin of this.pluginInstances.values()) {
|
||||
await plugin.beforeLoad();
|
||||
}
|
||||
|
||||
for (const plugin of this.pluginInstances.values()) {
|
||||
await plugin.load();
|
||||
}
|
||||
}
|
||||
}
|
139
packages/core/client/src/application/RouterManager.tsx
Normal file
139
packages/core/client/src/application/RouterManager.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import set from 'lodash/set';
|
||||
import React, { ComponentType } from 'react';
|
||||
import {
|
||||
BrowserRouter,
|
||||
BrowserRouterProps,
|
||||
HashRouter,
|
||||
HashRouterProps,
|
||||
MemoryRouter,
|
||||
MemoryRouterProps,
|
||||
RouteObject,
|
||||
useRoutes,
|
||||
} from 'react-router-dom';
|
||||
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;
|
||||
};
|
||||
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> = {};
|
||||
|
||||
constructor(protected options: RouterOptions) {
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
setType(type: RouterOptions['type']) {
|
||||
this.options.type = type;
|
||||
}
|
||||
|
||||
setBasename(basename: string) {
|
||||
this.options.basename = basename;
|
||||
}
|
||||
|
||||
getRoutes(): 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.'), 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.options.renderComponent ? this.options.renderComponent(Component) : 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);
|
||||
}
|
||||
|
||||
getRouterComponent() {
|
||||
const { type = 'browser', ...opts } = this.options || {};
|
||||
const Routers = {
|
||||
hash: HashRouter,
|
||||
browser: BrowserRouter,
|
||||
memory: MemoryRouter,
|
||||
};
|
||||
|
||||
const ReactRouter = Routers[type];
|
||||
|
||||
const RenderRoutes = () => {
|
||||
const routes = this.getRoutes();
|
||||
const element = useRoutes(routes);
|
||||
return element;
|
||||
};
|
||||
|
||||
const RenderRouter: React.FC<{ BaseLayout?: ComponentType }> = ({ BaseLayout = BlankComponent }) => {
|
||||
return (
|
||||
<RouterContextCleaner>
|
||||
<ReactRouter {...opts}>
|
||||
<BaseLayout>
|
||||
<RenderRoutes />
|
||||
</BaseLayout>
|
||||
</ReactRouter>
|
||||
</RouterContextCleaner>
|
||||
);
|
||||
};
|
||||
|
||||
return RenderRouter;
|
||||
}
|
||||
|
||||
add(name: string, route: RouteType) {
|
||||
this.routes[name] = route;
|
||||
}
|
||||
|
||||
remove(name: string) {
|
||||
delete this.routes[name];
|
||||
}
|
||||
}
|
||||
|
||||
export function createRouterManager(options?: RouterOptions) {
|
||||
return new RouterManager(options);
|
||||
}
|
@ -0,0 +1,340 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { render, screen, sleep, userEvent } from 'testUtils';
|
||||
import { describe } from 'vitest';
|
||||
import { Application } from '../Application';
|
||||
import { Plugin } from '../Plugin';
|
||||
|
||||
describe('Application', () => {
|
||||
const router: any = { type: 'memory', initialEntries: ['/'] };
|
||||
const initialComponentsLength = 6;
|
||||
const initialProvidersLength = 2;
|
||||
it('basic', () => {
|
||||
const app = new Application({ router });
|
||||
expect(app.i18n).toBeDefined();
|
||||
expect(app.apiClient).toBeDefined();
|
||||
expect(app.components).toBeDefined();
|
||||
expect(app.pm).toBeDefined();
|
||||
expect(app.providers).toBeDefined();
|
||||
expect(app.router).toBeDefined();
|
||||
expect(app.scopes).toBeDefined();
|
||||
expect(app.providers).toHaveLength(initialProvidersLength);
|
||||
expect(Object.keys(app.components)).toHaveLength(initialComponentsLength);
|
||||
});
|
||||
|
||||
describe('components', () => {
|
||||
const Hello = () => <div>Hello</div>;
|
||||
Hello.displayName = 'Hello';
|
||||
|
||||
it('initial', () => {
|
||||
const app = new Application({ router, components: { Hello } });
|
||||
expect(app.components.Hello).toBe(Hello);
|
||||
});
|
||||
|
||||
it('addComponents', () => {
|
||||
const app = new Application({ router });
|
||||
app.addComponents({ Hello });
|
||||
expect(app.components.Hello).toBe(Hello);
|
||||
});
|
||||
|
||||
it('addComponent', () => {
|
||||
const app = new Application({ router });
|
||||
app.addComponent(Hello);
|
||||
expect(app.components.Hello).toBe(Hello);
|
||||
|
||||
app.addComponent(Hello, 'Hello2');
|
||||
expect(app.components.Hello2).toBe(Hello);
|
||||
});
|
||||
|
||||
it('addComponents without name, should error', () => {
|
||||
const app = new Application({ router });
|
||||
|
||||
const fn = vitest.fn();
|
||||
const originalConsoleError = console.error;
|
||||
console.error = fn;
|
||||
app.addComponent(() => <div>123</div>);
|
||||
expect(fn).toBeCalled();
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
describe('getComponent', () => {
|
||||
let originalConsoleError: any;
|
||||
beforeEach(() => {
|
||||
originalConsoleError = console.error;
|
||||
});
|
||||
afterEach(() => {
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
it('arg is Class component', () => {
|
||||
class Foo extends Component {
|
||||
render() {
|
||||
return <div></div>;
|
||||
}
|
||||
}
|
||||
const app = new Application({ router });
|
||||
expect(app.getComponent(Foo)).toBe(Foo);
|
||||
});
|
||||
|
||||
it('arg is Function component', () => {
|
||||
const Foo = () => <div></div>;
|
||||
const app = new Application({ router });
|
||||
expect(app.getComponent(Foo)).toBe(Foo);
|
||||
});
|
||||
|
||||
it('arg is string', () => {
|
||||
const Foo = () => <div></div>;
|
||||
const app = new Application({ router, components: { Foo } });
|
||||
expect(app.getComponent('Foo')).toBe(Foo);
|
||||
});
|
||||
|
||||
it('arg is string, but not found in components', () => {
|
||||
const app = new Application({ router });
|
||||
const fn = vitest.fn();
|
||||
console.error = fn;
|
||||
expect(app.getComponent('Foo')).toBeUndefined();
|
||||
expect(fn).toBeCalled();
|
||||
});
|
||||
|
||||
it('arg is null or undefined', () => {
|
||||
const app = new Application({ router });
|
||||
const fn = vitest.fn();
|
||||
console.error = fn;
|
||||
expect(app.getComponent(null)).toBeUndefined();
|
||||
expect(fn).toBeCalled();
|
||||
});
|
||||
|
||||
it('arg is other types', () => {
|
||||
const app = new Application({ router });
|
||||
const fn = vitest.fn();
|
||||
console.error = fn;
|
||||
expect(app.getComponent({} as any)).toBeUndefined();
|
||||
expect(fn).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('renderComponent', () => {
|
||||
const Foo = (props) => <div>{props.name}</div>;
|
||||
const app = new Application({ router, components: { Foo } });
|
||||
expect(app.renderComponent('Foo', { name: 'bar' })).toEqual(<Foo name="bar" />);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scopes', () => {
|
||||
it('initial', () => {
|
||||
const scopes = { foo: 'bar' };
|
||||
const app = new Application({ router, scopes });
|
||||
expect(app.scopes).toEqual(scopes);
|
||||
});
|
||||
|
||||
it('addScopes', () => {
|
||||
const app = new Application({ router });
|
||||
app.addScopes({ foo: 'bar' });
|
||||
expect(app.scopes).toEqual({ foo: 'bar' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('providers', () => {
|
||||
const Hello = (props) => <div>Hello {props.children}</div>;
|
||||
const World = (props) => (
|
||||
<div>
|
||||
<div>World</div>
|
||||
<div>{props.name}</div>
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
const Foo = (props) => <div>Foo {props.children}</div>;
|
||||
|
||||
it('initial', () => {
|
||||
const app = new Application({ router, providers: [Hello, [World, { name: 'aaa' }]] });
|
||||
expect(app.providers.slice(initialProvidersLength)).toEqual([
|
||||
[Hello, undefined],
|
||||
[World, { name: 'aaa' }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('addProviders', () => {
|
||||
const app = new Application({ router, providers: [Hello] });
|
||||
app.addProviders([[World, { name: 'aaa' }], Foo]);
|
||||
expect(app.providers.slice(initialProvidersLength)).toEqual([
|
||||
[Hello, undefined],
|
||||
[World, { name: 'aaa' }],
|
||||
[Foo, undefined],
|
||||
]);
|
||||
});
|
||||
|
||||
it('addProvider', () => {
|
||||
const app = new Application({ router, providers: [Hello] });
|
||||
app.addProvider(World, { name: 'aaa' });
|
||||
expect(app.providers.slice(initialProvidersLength)).toEqual([
|
||||
[Hello, undefined],
|
||||
[World, { name: 'aaa' }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('use', () => {
|
||||
const app = new Application({ router, providers: [Hello] });
|
||||
app.use(World, { name: 'aaa' });
|
||||
expect(app.providers.slice(initialProvidersLength)).toEqual([
|
||||
[Hello, undefined],
|
||||
[World, { name: 'aaa' }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('getComposeProviders', () => {
|
||||
const app = new Application({ router, providers: [Hello, [World, { name: 'aaa' }]] });
|
||||
const Providers = app.getComposeProviders();
|
||||
render(
|
||||
<Providers>
|
||||
<Foo />
|
||||
</Providers>,
|
||||
);
|
||||
expect(screen.getByText('Hello')).toBeInTheDocument();
|
||||
expect(screen.getByText('World')).toBeInTheDocument();
|
||||
expect(screen.getByText('aaa')).toBeInTheDocument();
|
||||
expect(screen.getByText('Foo')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('render', () => {
|
||||
it('getRootComponent', async () => {
|
||||
const app = new Application({
|
||||
router,
|
||||
});
|
||||
const Layout = () => (
|
||||
<div>
|
||||
<div>
|
||||
<Link to="/">Home</Link>
|
||||
<Link to="/about">About</Link>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
const Home = () => {
|
||||
return <div>HomeComponent</div>;
|
||||
};
|
||||
const About = () => {
|
||||
return <div>AboutComponent</div>;
|
||||
};
|
||||
|
||||
const HelloProvider = ({ children }) => {
|
||||
return <div>Hello {children}</div>;
|
||||
};
|
||||
|
||||
app.router.add('root', {
|
||||
element: <Layout />,
|
||||
});
|
||||
app.router.add('root.home', {
|
||||
path: '/',
|
||||
element: <Home />,
|
||||
});
|
||||
app.router.add('root.about', {
|
||||
path: '/about',
|
||||
element: <About />,
|
||||
});
|
||||
app.addProviders([HelloProvider]);
|
||||
|
||||
const Root = app.getRootComponent();
|
||||
render(<Root />);
|
||||
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
await sleep(10);
|
||||
expect(screen.getByText('HomeComponent')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText('About'));
|
||||
expect(screen.getByText('AboutComponent')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('mount', async () => {
|
||||
const Hello = () => <div>Hello</div>;
|
||||
const app = new Application({
|
||||
router,
|
||||
providers: [Hello],
|
||||
});
|
||||
|
||||
render(<div id="app"></div>);
|
||||
const root = app.mount('#app');
|
||||
expect(root).toBeDefined();
|
||||
|
||||
await sleep(10);
|
||||
expect(screen.getByText('Hello')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('mount root error', () => {
|
||||
const app = new Application({
|
||||
router,
|
||||
});
|
||||
const originalConsoleWarn = console.warn;
|
||||
|
||||
const fn = vitest.fn();
|
||||
console.warn = fn;
|
||||
app.mount('#app');
|
||||
expect(fn).toBeCalled();
|
||||
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
it('plugin load error', async () => {
|
||||
const app = new Application({
|
||||
router,
|
||||
});
|
||||
class DemoPlugin extends Plugin {
|
||||
async load() {
|
||||
throw new Error('error');
|
||||
}
|
||||
}
|
||||
app.pm.add(DemoPlugin, { name: 'demo' });
|
||||
|
||||
const Root = app.getRootComponent();
|
||||
render(<Root />);
|
||||
|
||||
await sleep(10);
|
||||
expect(screen.getByText('Load Plugin Error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('replace Component', async () => {
|
||||
const AppSpin = () => <div>AppSpin</div>;
|
||||
const AppMain = () => <div>AppMain</div>;
|
||||
const app = new Application({
|
||||
router,
|
||||
components: { AppSpin, AppMain },
|
||||
});
|
||||
const Root = app.getRootComponent();
|
||||
render(<Root />);
|
||||
expect(screen.getByText('AppSpin')).toBeInTheDocument();
|
||||
await sleep(10);
|
||||
expect(screen.getByText('AppMain')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('render component error', async () => {
|
||||
const app = new Application({
|
||||
router,
|
||||
});
|
||||
|
||||
const ErrorFallback = () => {
|
||||
return <div>ErrorFallback</div>;
|
||||
};
|
||||
const Foo = () => {
|
||||
throw new Error('error');
|
||||
return null;
|
||||
};
|
||||
app.use(Foo);
|
||||
app.addComponents({
|
||||
ErrorFallback,
|
||||
});
|
||||
|
||||
const originalConsoleWarn = console.error;
|
||||
const fn = vitest.fn();
|
||||
console.error = fn;
|
||||
|
||||
const Root = app.getRootComponent();
|
||||
render(<Root />);
|
||||
await sleep(10);
|
||||
expect(fn).toBeCalled();
|
||||
|
||||
expect(screen.getByText('ErrorFallback')).toBeInTheDocument();
|
||||
screen.debug();
|
||||
|
||||
console.error = originalConsoleWarn;
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,99 @@
|
||||
import { Application } from '../Application';
|
||||
import { Plugin } from '../Plugin';
|
||||
|
||||
describe('Plugin', () => {
|
||||
it('lifecycle', async () => {
|
||||
const afterAdd = vitest.fn();
|
||||
const beforeLoad = vitest.fn();
|
||||
const load = vitest.fn();
|
||||
class DemoPlugin extends Plugin {
|
||||
async afterAdd() {
|
||||
afterAdd();
|
||||
}
|
||||
async beforeLoad() {
|
||||
beforeLoad();
|
||||
}
|
||||
async load() {
|
||||
load();
|
||||
}
|
||||
}
|
||||
const app = new Application({
|
||||
plugins: [[DemoPlugin, { name: 'demo1' }]],
|
||||
});
|
||||
await app.load();
|
||||
|
||||
expect(afterAdd).toBeCalledTimes(1);
|
||||
expect(beforeLoad).toBeCalledTimes(1);
|
||||
expect(load).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PluginManager', () => {
|
||||
it('static Plugins', async () => {
|
||||
const fn1 = vitest.fn();
|
||||
class Demo1Plugin extends Plugin {
|
||||
async load() {
|
||||
fn1();
|
||||
}
|
||||
}
|
||||
const fn2 = vitest.fn();
|
||||
const config = { a: 1 };
|
||||
class Demo2Plugin extends Plugin {
|
||||
async load() {
|
||||
fn2(this.options.config);
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Application({
|
||||
plugins: [
|
||||
[Demo1Plugin, { name: 'demo1' }],
|
||||
[Demo2Plugin, { name: 'demo2', config }],
|
||||
],
|
||||
});
|
||||
await app.load();
|
||||
|
||||
expect(fn1).toBeCalledTimes(1);
|
||||
expect(fn2).toBeCalledTimes(1);
|
||||
expect(fn2).toBeCalledWith(config);
|
||||
});
|
||||
|
||||
it('dynamic Plugins', async () => {
|
||||
const fn2 = vitest.fn();
|
||||
const config = { a: 1 };
|
||||
class Demo2 extends Plugin {
|
||||
async load() {
|
||||
fn2(this.options.config);
|
||||
}
|
||||
}
|
||||
|
||||
class Demo1Plugin extends Plugin {
|
||||
async afterAdd() {
|
||||
this.app.pm.add(Demo2, { name: 'demo2', config });
|
||||
}
|
||||
}
|
||||
const app = new Application({
|
||||
plugins: [[Demo1Plugin, { name: 'demo1' }]],
|
||||
});
|
||||
await app.load();
|
||||
expect(fn2).toBeCalledTimes(1);
|
||||
expect(fn2).toBeCalledWith(config);
|
||||
});
|
||||
|
||||
it('getter', async () => {
|
||||
class DemoPlugin extends Plugin {
|
||||
async afterAdd() {
|
||||
expect(this.pm).toBe(this.app.pm);
|
||||
expect(this.router).toBe(this.app.router);
|
||||
}
|
||||
}
|
||||
const app = new Application({ plugins: [[DemoPlugin, { name: 'demo' }]] });
|
||||
await app.load();
|
||||
});
|
||||
|
||||
it('get', async () => {
|
||||
class DemoPlugin extends Plugin { }
|
||||
const app = new Application({ plugins: [[DemoPlugin, { name: 'demo' }]] });
|
||||
await app.load();
|
||||
expect(app.pm.get('demo')).toBeInstanceOf(DemoPlugin);
|
||||
});
|
||||
});
|
@ -0,0 +1,198 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
import { render, screen, userEvent } from 'testUtils';
|
||||
import { beforeAll } from 'vitest';
|
||||
import { Application } from '../Application';
|
||||
import { RouterManager, RouteType } from '../RouterManager';
|
||||
|
||||
describe('Router', () => {
|
||||
let app: Application;
|
||||
beforeAll(() => {
|
||||
app = new Application();
|
||||
});
|
||||
|
||||
describe('add routes', () => {
|
||||
let router: RouterManager;
|
||||
|
||||
beforeEach(() => {
|
||||
router = new RouterManager({ type: 'memory', initialEntries: ['/'] });
|
||||
});
|
||||
|
||||
it('basic', () => {
|
||||
const route1: RouteType = {
|
||||
path: '/',
|
||||
element: <div />,
|
||||
};
|
||||
router.add('test', route1);
|
||||
expect(router.getRoutes()).toHaveLength(1);
|
||||
expect(router.getRoutes()).toEqual([route1]);
|
||||
|
||||
const route2: RouteType = {
|
||||
path: '/test2',
|
||||
element: <div />,
|
||||
};
|
||||
|
||||
router.add('test2', route2);
|
||||
expect(router.getRoutes()).toHaveLength(2);
|
||||
expect(router.getRoutes()).toEqual([route1, route2]);
|
||||
});
|
||||
|
||||
it('nested route', () => {
|
||||
const route1: RouteType = {
|
||||
path: '/',
|
||||
element: <div />,
|
||||
};
|
||||
const route2: RouteType = {
|
||||
path: '/test2',
|
||||
element: <div />,
|
||||
};
|
||||
|
||||
router.add('test', route1);
|
||||
router.add('test.test2', route2);
|
||||
expect(router.getRoutes()).toEqual([{ ...route1, children: [route2] }]);
|
||||
});
|
||||
|
||||
it('nested route with empty parent', () => {
|
||||
const route1: RouteType = {
|
||||
path: '/',
|
||||
element: <div />,
|
||||
};
|
||||
const route2: RouteType = {
|
||||
path: '/test2',
|
||||
element: <div />,
|
||||
};
|
||||
const route3: RouteType = {
|
||||
path: '/test3',
|
||||
element: <div />,
|
||||
};
|
||||
|
||||
router.add('test', route1);
|
||||
router.add('test.empty.test2', route2);
|
||||
router.add('test.empty2.empty3.test3', route3);
|
||||
expect(router.getRoutes()).toEqual([{ ...route1, children: [route2, route3] }]);
|
||||
});
|
||||
|
||||
it('Component', () => {
|
||||
const Hello = () => <div></div>;
|
||||
const route: RouteType = {
|
||||
path: '/',
|
||||
Component: Hello,
|
||||
};
|
||||
router.add('test', route);
|
||||
expect(router.getRoutes()).toEqual([{ path: '/', element: <Hello />, children: undefined }]);
|
||||
});
|
||||
|
||||
it('Component is string', () => {
|
||||
const router = new RouterManager({
|
||||
type: 'memory',
|
||||
initialEntries: ['/'],
|
||||
renderComponent: app.renderComponent.bind(app),
|
||||
});
|
||||
const Hello = () => <div></div>;
|
||||
app.addComponents({ Hello });
|
||||
const route: RouteType = {
|
||||
path: '/',
|
||||
Component: 'Hello',
|
||||
};
|
||||
router.add('test', route);
|
||||
expect(router.getRoutes()).toEqual([{ path: '/', element: <Hello />, children: undefined }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
let router: RouterManager;
|
||||
|
||||
beforeEach(() => {
|
||||
router = new RouterManager({ type: 'memory', initialEntries: ['/'] });
|
||||
});
|
||||
it('basic', () => {
|
||||
const route1: RouteType = {
|
||||
path: '/',
|
||||
element: <div />,
|
||||
};
|
||||
router.add('test', route1);
|
||||
expect(router.getRoutes()).toEqual([route1]);
|
||||
router.remove('test');
|
||||
expect(router.getRoutes()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRouterComponent', () => {
|
||||
it('basic', async () => {
|
||||
const router = new RouterManager({ type: 'memory', initialEntries: ['/'] });
|
||||
const Layout = () => {
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Link to="/">Home</Link>
|
||||
<Link to="/about">About</Link>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
router.add('root', {
|
||||
element: <Layout />,
|
||||
});
|
||||
router.add('root.home', {
|
||||
path: '/',
|
||||
element: <div>HomeComponent</div>,
|
||||
});
|
||||
router.add('root.about', {
|
||||
path: '/about',
|
||||
element: <div>AboutComponent</div>,
|
||||
});
|
||||
const RouterComponent = router.getRouterComponent();
|
||||
render(<RouterComponent />);
|
||||
expect(screen.getByText('HomeComponent')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText('About'));
|
||||
expect(screen.getByText('AboutComponent')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('BaseLayout', () => {
|
||||
const router = new RouterManager({ type: 'memory', initialEntries: ['/'] });
|
||||
router.add('home', {
|
||||
path: '/',
|
||||
element: <div>HomeComponent</div>,
|
||||
});
|
||||
const RouterComponent = router.getRouterComponent();
|
||||
const BaseLayout: FC = (props) => {
|
||||
return <div>BaseLayout {props.children}</div>;
|
||||
};
|
||||
render(<RouterComponent BaseLayout={BaseLayout} />);
|
||||
expect(screen.getByText('HomeComponent')).toBeInTheDocument();
|
||||
expect(screen.getByText('BaseLayout')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('nested router', () => {
|
||||
const router = new RouterManager({ type: 'memory', initialEntries: ['/'] });
|
||||
|
||||
const Test = () => {
|
||||
const router2 = new RouterManager({ type: 'memory', initialEntries: ['/'] });
|
||||
router2.add('rooter2', {
|
||||
path: '/',
|
||||
element: <div>Router2</div>,
|
||||
});
|
||||
const RouterComponent = router2.getRouterComponent();
|
||||
return (
|
||||
<div>
|
||||
Router1
|
||||
<RouterComponent />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
router.add('home', {
|
||||
path: '/',
|
||||
element: <Test />,
|
||||
});
|
||||
|
||||
const RouterComponent = router.getRouterComponent();
|
||||
render(<RouterComponent />);
|
||||
|
||||
expect(screen.getByText('Router1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Router2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,77 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`compose > case 1 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<h1>
|
||||
A
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`compose > case 2 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<h1>
|
||||
A
|
||||
</h1>
|
||||
<div>
|
||||
<h1>
|
||||
B
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`compose > case 3 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<h1>
|
||||
A
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`compose > case 4 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<h1>
|
||||
A
|
||||
</h1>
|
||||
<div>
|
||||
<h1>
|
||||
B
|
||||
</h1>
|
||||
<div>
|
||||
<h1>
|
||||
C
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`compose > case 5 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<h1>
|
||||
A
|
||||
</h1>
|
||||
<div>
|
||||
<h1>
|
||||
B
|
||||
1
|
||||
</h1>
|
||||
<div>
|
||||
<h1>
|
||||
C
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -1,95 +0,0 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { compose } from '../compose';
|
||||
|
||||
describe('compose', () => {
|
||||
it('case 1', () => {
|
||||
const A: React.FC = (props) => (
|
||||
<div>
|
||||
<h1>A</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const App = compose(A)();
|
||||
const { container } = render(<App />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('case 2', () => {
|
||||
const A: React.FC = (props) => (
|
||||
<div>
|
||||
<h1>A</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const B: React.FC = (props) => (
|
||||
<div>
|
||||
<h1>B</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const App = compose(A)(B);
|
||||
const { container } = render(<App />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('case 3', () => {
|
||||
const A: React.FC = (props) => (
|
||||
<div>
|
||||
<h1>A</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const App = compose([A])();
|
||||
const { container } = render(<App />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('case 4', () => {
|
||||
const A: React.FC = (props) => (
|
||||
<div>
|
||||
<h1>A</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const B: React.FC = (props) => (
|
||||
<div>
|
||||
<h1>B</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const C: React.FC = (props) => (
|
||||
<div>
|
||||
<h1>C</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const App = compose(A, B)(C);
|
||||
const { container } = render(<App />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('case 5', () => {
|
||||
const A: React.FC<any> = (props) => (
|
||||
<div>
|
||||
<h1>A {props.name}</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const B: React.FC<any> = (props) => (
|
||||
<div>
|
||||
<h1>B {props.name}</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const C: React.FC<any> = (props) => (
|
||||
<div>
|
||||
<h1>C</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
const App = compose(A, [B, { name: '1' }])(C);
|
||||
const { container } = render(<App />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import { render, sleep } from 'testUtils';
|
||||
import { describe } from 'vitest';
|
||||
import { Plugin } from '../Plugin'
|
||||
import { Application } from '../Application';
|
||||
import { useApp, useRouter, usePlugin } from '../hooks';
|
||||
|
||||
describe('Application Hooks', () => {
|
||||
describe('useApp', () => {
|
||||
it('should return the application instance', async () => {
|
||||
const app = new Application();
|
||||
const Hello = () => {
|
||||
const app1 = useApp();
|
||||
expect(app).toBe(app1);
|
||||
return null;
|
||||
};
|
||||
app.addProviders([Hello]);
|
||||
const Root = app.getRootComponent();
|
||||
render(<Root />);
|
||||
|
||||
await sleep(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useRouter', () => {
|
||||
it('should return the router instance', async () => {
|
||||
const app = new Application();
|
||||
const Hello = () => {
|
||||
const router = useRouter();
|
||||
expect(app.router).toBe(router);
|
||||
return null;
|
||||
};
|
||||
app.addProviders([Hello]);
|
||||
const Root = app.getRootComponent();
|
||||
render(<Root />);
|
||||
|
||||
await sleep(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('usePlugin', () => {
|
||||
it('should return the plugin instance', async () => {
|
||||
class DemoPlugin extends Plugin {
|
||||
test = 'test';
|
||||
}
|
||||
const Hello = () => {
|
||||
const demo = usePlugin<{ test: string }>('demo');
|
||||
const demo2 = usePlugin(DemoPlugin);
|
||||
expect(demo).toBeInstanceOf(DemoPlugin);
|
||||
expect(demo2).toBeInstanceOf(DemoPlugin);
|
||||
expect(demo.test).toBe('test');
|
||||
expect(demo2.test).toBe('test');
|
||||
return null;
|
||||
};
|
||||
const app = new Application({ plugins: [[DemoPlugin, { name: 'demo' }]], providers: [Hello] });
|
||||
const Root = app.getRootComponent();
|
||||
render(<Root />);
|
||||
|
||||
await sleep(10);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from 'testUtils';
|
||||
import { describe } from 'vitest';
|
||||
import { compose, normalizeContainer } from '../utils';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('normalizeContainer', () => {
|
||||
let originalConsoleWarn: any;
|
||||
beforeEach(() => {
|
||||
originalConsoleWarn = console.warn;
|
||||
});
|
||||
afterEach(() => {
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
it('when container is undefined or null, return null', () => {
|
||||
const fn = vitest.fn();
|
||||
console.warn = fn;
|
||||
expect(normalizeContainer(undefined)).toBeNull();
|
||||
expect(fn).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('when container is string and can not find element, return null', () => {
|
||||
const fn = vitest.fn();
|
||||
console.warn = fn;
|
||||
expect(normalizeContainer('#app')).toBeNull();
|
||||
expect(fn).toBeCalledTimes(1);
|
||||
});
|
||||
it('when container is string and can find element, return element', () => {
|
||||
render(<div id="app">App</div>);
|
||||
expect(normalizeContainer('#app')).toBeInTheDocument();
|
||||
});
|
||||
it('when container is element, return element', () => {
|
||||
render(<div id="app">App</div>);
|
||||
const element = document.getElementById('#app');
|
||||
expect(normalizeContainer(element)).toBe(element);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compose', () => {
|
||||
const Hello = ({ children }: any) => <div>Hello {children}</div>;
|
||||
const World = ({ name, children }: any) => (
|
||||
<div>
|
||||
<div>World</div>
|
||||
<div>{name}</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
const Foo = () => <div>Foo</div>;
|
||||
|
||||
it('compose without LastComponent', () => {
|
||||
const composeFn = compose([Hello, undefined], [World, { name: 'aaa' }]);
|
||||
const Compose = composeFn();
|
||||
render(<Compose />);
|
||||
expect(screen.getByText('Hello')).toBeInTheDocument();
|
||||
expect(screen.getByText('aaa')).toBeInTheDocument();
|
||||
expect(screen.getByText('World')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('compose with LastComponent', () => {
|
||||
const composeFn = compose([Hello, undefined]);
|
||||
const Compose = composeFn(Foo);
|
||||
render(<Compose />);
|
||||
expect(screen.getByText('Hello')).toBeInTheDocument();
|
||||
expect(screen.getByText('Foo')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,25 @@
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
||||
import type { Application } from '../Application';
|
||||
import { ApplicationContext } from '../context';
|
||||
import { useAppPluginLoad } from '../hooks';
|
||||
|
||||
export interface AppComponentProps {
|
||||
app: Application;
|
||||
}
|
||||
|
||||
export const AppComponent: FC<AppComponentProps> = (props) => {
|
||||
const { app } = props;
|
||||
const { loading, error } = useAppPluginLoad(app);
|
||||
const handleErrors = useCallback((error: Error, info: { componentStack: string }) => {
|
||||
console.error(error, info);
|
||||
}, []);
|
||||
if (loading) return app.renderComponent('AppSpin', { app });
|
||||
if (error) return app.renderComponent('AppError', { app, error });
|
||||
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={app.getComponent<FallbackProps>('ErrorFallback')} onError={handleErrors}>
|
||||
<ApplicationContext.Provider value={app}>{app.renderComponent('AppMain')}</ApplicationContext.Provider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
|
||||
export const BlankComponent: FC<{ children?: ReactNode }> = ({ children }) => <>{children}</>;
|
@ -0,0 +1,11 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useApp } from '../hooks';
|
||||
|
||||
export const MainComponent = React.memo(() => {
|
||||
const app = useApp();
|
||||
const Router = useMemo(() => app.router.getRouterComponent(), [app]);
|
||||
const Providers = useMemo(() => app.getComposeProviders(), [app]);
|
||||
return <Router BaseLayout={Providers} />;
|
||||
});
|
||||
|
||||
MainComponent.displayName = 'MainComponent';
|
@ -0,0 +1,17 @@
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import { UNSAFE_LocationContext, UNSAFE_RouteContext } from 'react-router-dom';
|
||||
|
||||
export const RouterContextCleaner: FC<{ children?: ReactNode }> = React.memo((props) => {
|
||||
return (
|
||||
<UNSAFE_RouteContext.Provider
|
||||
value={{
|
||||
outlet: null,
|
||||
matches: [],
|
||||
isDataRoute: false,
|
||||
}}
|
||||
>
|
||||
<UNSAFE_LocationContext.Provider value={null}>{props.children}</UNSAFE_LocationContext.Provider>
|
||||
</UNSAFE_RouteContext.Provider>
|
||||
);
|
||||
});
|
||||
RouterContextCleaner.displayName = 'RouterContextCleaner';
|
@ -0,0 +1,16 @@
|
||||
import React, { FC } from 'react';
|
||||
import { MainComponent } from './MainComponent';
|
||||
|
||||
const Loading: FC = () => <div>Loading...</div>;
|
||||
const AppError: FC<{ error: Error }> = ({ error }) => (
|
||||
<div>
|
||||
<div>Load Plugin Error</div>
|
||||
{error?.message}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const defaultAppComponents = {
|
||||
AppMain: MainComponent,
|
||||
AppSpin: Loading,
|
||||
AppError: AppError,
|
||||
};
|
4
packages/core/client/src/application/components/index.ts
Normal file
4
packages/core/client/src/application/components/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './AppComponent';
|
||||
export * from './BlankComponent';
|
||||
export * from './defaultComponents';
|
||||
export * from './RouterContextCleaner';
|
@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const Blank = ({ children }) => children || null;
|
||||
|
||||
export const compose = (...components: any[]) => {
|
||||
const Root = [...components, Blank].reduce((parent, child) => {
|
||||
const [Parent, parentProps] = Array.isArray(parent) ? parent : [parent];
|
||||
const [Child, childProps] = Array.isArray(child) ? child : [child];
|
||||
return ({ children }) => (
|
||||
<Parent {...parentProps}>
|
||||
<Child {...childProps}>{children}</Child>
|
||||
</Parent>
|
||||
);
|
||||
});
|
||||
return (LastChild?: any) => (props?: any) => {
|
||||
return <Root>{LastChild && <LastChild {...props} />}</Root>;
|
||||
};
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { createContext } from 'react';
|
||||
import { Application } from './Application';
|
||||
import type { Application } from './Application';
|
||||
|
||||
export const ApplicationContext = createContext<Application>(null as any);
|
||||
export const ApplicationContext = createContext<Application>(null);
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { FC } from 'react';
|
||||
import { Link, Outlet, useLocation } from 'react-router-dom';
|
||||
import { Application } from '../Application';
|
||||
import { Plugin } from '../Plugin';
|
||||
@ -15,6 +15,10 @@ const Root = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const Home = () => {
|
||||
return <div>this is Home</div>;
|
||||
};
|
||||
|
||||
const Team = () => {
|
||||
return (
|
||||
<div>
|
||||
@ -35,7 +39,7 @@ class Test1Plugin extends Plugin {
|
||||
async load() {
|
||||
this.router.add('root.team', {
|
||||
path: 'team',
|
||||
component: 'Team',
|
||||
Component: 'Team',
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -44,21 +48,30 @@ class Test2Plugin extends Plugin {
|
||||
async load() {
|
||||
this.router.add('root.about', {
|
||||
path: 'about',
|
||||
component: 'About',
|
||||
Component: 'About',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class NocobasePresetPlugin extends Plugin {
|
||||
async afterAdd() {
|
||||
await this.pm.add('test1');
|
||||
await this.pm.add(Test2Plugin, { name: 'test2' });
|
||||
// mock load remote plugin
|
||||
await this.addRemotePlugin();
|
||||
}
|
||||
|
||||
async addRemotePlugin() {
|
||||
await this.pm.add(Test1Plugin);
|
||||
await this.pm.add(Test2Plugin);
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.router.add('root', {
|
||||
path: '/',
|
||||
component: 'Root',
|
||||
Component: 'Root',
|
||||
});
|
||||
this.router.add('root.home', {
|
||||
path: '/',
|
||||
Component: 'Home',
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -70,21 +83,18 @@ const app = new Application({
|
||||
router: {
|
||||
type: 'hash',
|
||||
},
|
||||
plugins: [[NocobasePresetPlugin, { name: 'nocobase' }]],
|
||||
importPlugins: async (name) => {
|
||||
return {
|
||||
test1: Test1Plugin,
|
||||
}[name];
|
||||
},
|
||||
components: { Root, Team, About },
|
||||
plugins: [NocobasePresetPlugin],
|
||||
components: { Root, Home, Team, About },
|
||||
});
|
||||
|
||||
app.use((props) => {
|
||||
const HelloProvider: FC = (props) => {
|
||||
const location = useLocation();
|
||||
if (location.pathname === '/hello') {
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
return props.children;
|
||||
});
|
||||
return <>{props.children}</>;
|
||||
};
|
||||
|
||||
app.use(HelloProvider);
|
||||
|
||||
export default app.getRootComponent();
|
@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useDesignable } from '@nocobase/client';
|
||||
import { Button } from 'antd';
|
||||
import { observer } from '@formily/react';
|
||||
|
||||
export const Hello: React.FC<any> = observer(
|
||||
({ name }) => {
|
||||
const { patch, remove } = useDesignable();
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello {name}!</h1>
|
||||
<Button
|
||||
onClick={() => {
|
||||
patch('x-component-props.name', Math.random());
|
||||
}}
|
||||
>
|
||||
更新
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ displayName: 'Hello' },
|
||||
);
|
@ -1,7 +0,0 @@
|
||||
import React from 'react';
|
||||
import { SchemaComponent, useRoute } from '@nocobase/client';
|
||||
|
||||
export const RouteSchemaComponent = () => {
|
||||
const route = useRoute();
|
||||
return <SchemaComponent schema={route.schema} />;
|
||||
};
|
@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link, MemoryRouter } from 'react-router-dom';
|
||||
import { RouteSwitchProvider, RouteSwitch, SchemaComponentProvider, compose } from '@nocobase/client';
|
||||
import { Hello } from './Hello';
|
||||
import { RouteSchemaComponent } from './RouteSchemaComponent';
|
||||
import routes from './routes';
|
||||
|
||||
const providers = [
|
||||
[MemoryRouter, { initialEntries: ['/'] }],
|
||||
[SchemaComponentProvider, { components: { Hello } }],
|
||||
[RouteSwitchProvider, { components: { RouteSchemaComponent } }],
|
||||
];
|
||||
|
||||
const App = compose(...providers)(() => {
|
||||
return (
|
||||
<div>
|
||||
<Link to={'/'}>Home</Link>,<Link to={'/about'}>About</Link>
|
||||
<RouteSwitch routes={routes} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default App;
|
@ -1,28 +0,0 @@
|
||||
import { RouteRedirectProps } from '@nocobase/client';
|
||||
|
||||
export default [
|
||||
{
|
||||
type: 'route',
|
||||
path: '/',
|
||||
component: 'RouteSchemaComponent',
|
||||
schema: {
|
||||
name: 'home',
|
||||
'x-component': 'Hello',
|
||||
'x-component-props': {
|
||||
name: 'Home',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/about',
|
||||
component: 'RouteSchemaComponent',
|
||||
schema: {
|
||||
name: 'home',
|
||||
'x-component': 'Hello',
|
||||
'x-component-props': {
|
||||
name: 'About',
|
||||
},
|
||||
},
|
||||
},
|
||||
] as Array<RouteRedirectProps>;
|
99
packages/core/client/src/application/demos/demo2.tsx
Normal file
99
packages/core/client/src/application/demos/demo2.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Link, Navigate, Outlet, useParams } from 'react-router-dom';
|
||||
import { Application } from '../Application';
|
||||
import { useApp } from '../hooks';
|
||||
import { Plugin } from '../Plugin';
|
||||
import { RouterManager } from '../RouterManager';
|
||||
|
||||
const Root = () => {
|
||||
return (
|
||||
<div>
|
||||
<Link to={'/'}>Home</Link>|<Link to={'/admin'}>Admin</Link>|<Link to={'/admin/setting'}>Admin Setting</Link>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Home = () => {
|
||||
return <div>Home</div>;
|
||||
};
|
||||
|
||||
const Admin = () => {
|
||||
return (
|
||||
<div>
|
||||
<div>Admin</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MobileLayout = () => {
|
||||
return (
|
||||
<div>
|
||||
<div>MobileLayout</div>
|
||||
<Link to={'/mobile/123'}>Mobile 123</Link>|<Link to={'/mobile/456'}>Mobile 456</Link>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MobilePage = () => {
|
||||
const { id } = useParams();
|
||||
|
||||
return <div>id: {id}</div>;
|
||||
};
|
||||
|
||||
const AdminSetting = () => {
|
||||
const app = useApp();
|
||||
const MobileRouter = useMemo(() => {
|
||||
const router = new RouterManager({ type: 'memory', initialEntries: ['/'] }, app);
|
||||
router.add('mobile', {
|
||||
element: <MobileLayout />,
|
||||
});
|
||||
router.add('mobile.index', {
|
||||
path: '/',
|
||||
element: <Navigate replace to="/mobile/123" />,
|
||||
});
|
||||
router.add('mobile.page', {
|
||||
path: '/mobile/:id',
|
||||
element: <MobilePage />,
|
||||
});
|
||||
|
||||
return router.getRouterComponent();
|
||||
}, [app]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>AdminSetting</div>
|
||||
<MobileRouter />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
class NocobasePresetPlugin extends Plugin {
|
||||
async load() {
|
||||
this.router.add('root', {
|
||||
path: '/',
|
||||
element: <Root />,
|
||||
});
|
||||
this.router.add('root.home', {
|
||||
path: '/',
|
||||
element: <Home />,
|
||||
});
|
||||
this.router.add('root.admin', {
|
||||
path: '/admin',
|
||||
element: <Admin />,
|
||||
});
|
||||
this.router.add('root.admin.setting', {
|
||||
path: '/admin/setting',
|
||||
element: <AdminSetting />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Application({
|
||||
router: { type: 'hash' },
|
||||
plugins: [NocobasePresetPlugin],
|
||||
});
|
||||
|
||||
export default app.getRootComponent();
|
@ -1,10 +0,0 @@
|
||||
import { APIClient } from '@nocobase/client';
|
||||
import mock from './mock';
|
||||
|
||||
const apiClient = new APIClient({
|
||||
baseURL: `${location.protocol}//${location.host}/api/`,
|
||||
});
|
||||
|
||||
mock(apiClient);
|
||||
|
||||
export default apiClient;
|
@ -1,58 +0,0 @@
|
||||
import {
|
||||
AdminLayout,
|
||||
AntdConfigProvider,
|
||||
AntdSchemaComponentProvider,
|
||||
APIClientProvider,
|
||||
AuthLayout,
|
||||
CollectionManagerProvider,
|
||||
compose,
|
||||
DesignableSwitch,
|
||||
DocumentTitleProvider,
|
||||
i18n,
|
||||
RouteSchemaComponent,
|
||||
RouteSwitch,
|
||||
RouteSwitchProvider,
|
||||
SchemaComponentProvider,
|
||||
SchemaInitializerProvider,
|
||||
SigninPage,
|
||||
SignupPage,
|
||||
SystemSettingsProvider,
|
||||
useRequest,
|
||||
} from '@nocobase/client';
|
||||
import { Spin } from 'antd';
|
||||
import React from 'react';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Link, MemoryRouter, NavLink } from 'react-router-dom';
|
||||
import apiClient from './apiClient';
|
||||
|
||||
const providers = [
|
||||
// [HashRouter],
|
||||
[MemoryRouter, { initialEntries: ['/'] }],
|
||||
[APIClientProvider, { apiClient }],
|
||||
[I18nextProvider, { i18n }],
|
||||
[AntdConfigProvider, { remoteLocale: true }],
|
||||
SystemSettingsProvider,
|
||||
[{ components: { DesignableSwitch } }],
|
||||
[SchemaComponentProvider, { components: { Link, NavLink } }],
|
||||
CollectionManagerProvider,
|
||||
AntdSchemaComponentProvider,
|
||||
SchemaInitializerProvider,
|
||||
[DocumentTitleProvider, { addonAfter: 'NocoBase' }],
|
||||
[RouteSwitchProvider, { components: { AuthLayout, AdminLayout, RouteSchemaComponent, SigninPage, SignupPage } }],
|
||||
];
|
||||
|
||||
const App = compose(...providers)(() => {
|
||||
const { data, loading } = useRequest({
|
||||
url: 'routes:getAccessible',
|
||||
});
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<RouteSwitch routes={data?.data || []} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default App;
|
@ -1,197 +0,0 @@
|
||||
import { uid } from '@formily/shared';
|
||||
import { APIClient } from '@nocobase/client';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
export default (apiClient: APIClient) => {
|
||||
const mock = new MockAdapter(apiClient.axios);
|
||||
|
||||
mock.onGet('/app:getLang').reply(200, {
|
||||
data: { lang: 'en-US' },
|
||||
});
|
||||
|
||||
mock.onGet('/systemSettings:get/1').reply(200, {
|
||||
data: {
|
||||
title: 'NocoBase',
|
||||
},
|
||||
});
|
||||
|
||||
const jsonSchema = {
|
||||
qqzzjakwkwl: {
|
||||
name: 'qqzzjakwkwl',
|
||||
type: 'void',
|
||||
'x-component': 'Menu',
|
||||
'x-component-props': {
|
||||
mode: 'mix',
|
||||
theme: 'dark',
|
||||
// defaultSelectedUid: 'u8',
|
||||
onSelect: '{{ onSelect }}',
|
||||
sideMenuRefScopeKey: 'sideMenuRef',
|
||||
},
|
||||
properties: {
|
||||
item3: {
|
||||
type: 'void',
|
||||
title: 'SubMenu u3',
|
||||
'x-uid': 'u3',
|
||||
'x-component': 'Menu.SubMenu',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
item6: {
|
||||
type: 'void',
|
||||
title: 'SubMenu u6',
|
||||
'x-uid': 'u6',
|
||||
'x-component': 'Menu.SubMenu',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
item7: {
|
||||
type: 'void',
|
||||
title: 'Menu Item u7',
|
||||
'x-uid': 'u7',
|
||||
'x-component': 'Menu.Item',
|
||||
'x-component-props': {},
|
||||
},
|
||||
item8: {
|
||||
type: 'void',
|
||||
title: 'Menu Item u8',
|
||||
'x-uid': 'u8',
|
||||
'x-component': 'Menu.Item',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
},
|
||||
item4: {
|
||||
type: 'void',
|
||||
title: 'Menu Item u4',
|
||||
'x-uid': 'u4',
|
||||
'x-component': 'Menu.Item',
|
||||
'x-component-props': {},
|
||||
},
|
||||
item5: {
|
||||
type: 'void',
|
||||
title: 'Menu Item u5',
|
||||
'x-uid': 'u5',
|
||||
'x-component': 'Menu.Item',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
},
|
||||
item1: {
|
||||
type: 'void',
|
||||
title: 'Menu Item u1',
|
||||
'x-uid': 'u1',
|
||||
'x-component': 'Menu.Item',
|
||||
'x-component-props': {},
|
||||
},
|
||||
item2: {
|
||||
type: 'void',
|
||||
title: 'Menu Item u2',
|
||||
'x-uid': 'u2',
|
||||
'x-component': 'Menu.Item',
|
||||
'x-component-props': {},
|
||||
},
|
||||
item9: {
|
||||
type: 'void',
|
||||
title: 'SubMenu u9',
|
||||
'x-uid': 'u9',
|
||||
'x-component': 'Menu.SubMenu',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
item10: {
|
||||
type: 'void',
|
||||
title: 'Menu Item u10',
|
||||
'x-uid': 'u10',
|
||||
'x-component': 'Menu.Item',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mock.onGet(/\/uiSchemas\:getJsonSchema\/(\w+)/).reply(function (config) {
|
||||
const name = config.url.split('/').pop();
|
||||
console.log(name);
|
||||
if (jsonSchema[name]) {
|
||||
return [200, { data: jsonSchema[name] }];
|
||||
}
|
||||
const response = {
|
||||
data: {
|
||||
type: 'void',
|
||||
name: name,
|
||||
'x-uid': name,
|
||||
'x-component': 'Page',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
name: 'grid1',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'Grid.AddBlockItem',
|
||||
'x-uid': uid(),
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return [200, response];
|
||||
});
|
||||
|
||||
mock.onGet(/\/uiSchemas\:getProperties\/(\w+)/).reply(function (config) {
|
||||
// const name = config.url.split('/').pop();
|
||||
// console.log(name);
|
||||
// if (jsonSchema[name]) {
|
||||
// return [200, { data: jsonSchema[name] }];
|
||||
// }
|
||||
const response = {
|
||||
data: {
|
||||
type: 'void',
|
||||
name: uid(),
|
||||
'x-uid': uid(),
|
||||
'x-component': 'Page',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
name: 'grid1',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'Grid.AddBlockItem',
|
||||
'x-uid': uid(),
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return [200, response];
|
||||
});
|
||||
|
||||
mock.onGet('/routes:getAccessible').reply(200, {
|
||||
data: [
|
||||
{
|
||||
type: 'redirect',
|
||||
from: '/',
|
||||
to: '/admin',
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
uiSchemaUid: 'qqzzjakwkwl',
|
||||
path: '/admin/:name?',
|
||||
component: 'AdminLayout',
|
||||
title: 'NocoBase Admin',
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
component: 'AuthLayout',
|
||||
routes: [
|
||||
{
|
||||
type: 'route',
|
||||
path: '/signin',
|
||||
component: 'SigninPage',
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/signup',
|
||||
component: 'SignupPage',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
10
packages/core/client/src/application/demos/demo3.tsx
Normal file
10
packages/core/client/src/application/demos/demo3.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { Application, NocoBaseBuildInPlugin } from '@nocobase/client';
|
||||
|
||||
export const app = new Application({
|
||||
apiClient: {
|
||||
baseURL: process.env.API_BASE_URL,
|
||||
},
|
||||
plugins: [NocoBaseBuildInPlugin],
|
||||
});
|
||||
|
||||
export default app.getRootComponent();
|
4
packages/core/client/src/application/hooks/index.ts
Normal file
4
packages/core/client/src/application/hooks/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './useApp';
|
||||
export * from './useAppPluginLoad';
|
||||
export * from './usePlugin';
|
||||
export * from './useRouter';
|
@ -1,6 +1,7 @@
|
||||
import { useContext } from 'react';
|
||||
import type { Application } from '../Application';
|
||||
import { ApplicationContext } from '../context';
|
||||
|
||||
export const useApp = () => {
|
||||
return useContext(ApplicationContext);
|
||||
return useContext(ApplicationContext) || ({} as Application);
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { Application } from '../Application';
|
||||
|
||||
export function useAppPluginLoad(app: Application) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
useEffect(() => {
|
||||
async function run() {
|
||||
try {
|
||||
await app.load();
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
run();
|
||||
}, [app]);
|
||||
return { loading, error };
|
||||
}
|
9
packages/core/client/src/application/hooks/usePlugin.ts
Normal file
9
packages/core/client/src/application/hooks/usePlugin.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Plugin } from '../Plugin';
|
||||
import { useApp } from './useApp';
|
||||
|
||||
export function usePlugin<T extends typeof Plugin>(plugin: T): InstanceType<T>;
|
||||
export function usePlugin<T extends {}>(name: string): T;
|
||||
export function usePlugin(name: any) {
|
||||
const app = useApp();
|
||||
return app.pm.get(name);
|
||||
}
|
@ -4,10 +4,389 @@ group:
|
||||
order: 1
|
||||
---
|
||||
|
||||
# Application <Badge>待定</Badge>
|
||||
# Application V2
|
||||
|
||||
<img src="https://nocobase.oss-cn-beijing.aliyuncs.com/5be7ebc2f47effef85be7a0c75cf76f9.png" style="max-width: 800px;" />
|
||||
|
||||
## compose
|
||||
## Usage
|
||||
|
||||
<code src="./demos/demo1/index.tsx"></code>
|
||||
### 基础用法
|
||||
|
||||
<code src="./demos/demo1.tsx">Demo1</code>
|
||||
|
||||
### 嵌套 Router
|
||||
|
||||
<code src="./demos/demo2.tsx">Demo2</code>
|
||||
|
||||
## API
|
||||
|
||||
Application 提供了强大的功能,包括:
|
||||
|
||||
- 组件管理
|
||||
- 路由管理
|
||||
- scopes 管理
|
||||
- providers 管理
|
||||
- 插件管理
|
||||
|
||||
### 组件管理
|
||||
|
||||
通过在 `Application` 实例上添加组件,可以在后续的路由或者 `schema` 中以字符串的方式使用。
|
||||
|
||||
#### 添加组件
|
||||
|
||||
```tsx | pure
|
||||
const Hello = () => <div>Hello</div>;
|
||||
const World = () => <div>Wold</div>;
|
||||
|
||||
// 初始化时添加组件
|
||||
const app = new Application({
|
||||
components: {
|
||||
Hello,
|
||||
World
|
||||
}
|
||||
});
|
||||
|
||||
// 通过实例添加多个组件
|
||||
app.addComponents({
|
||||
Hello,
|
||||
World
|
||||
});
|
||||
|
||||
// 添加单个组件
|
||||
app.addComponent(Hello, 'Hello');
|
||||
```
|
||||
|
||||
在插件中添加组件。
|
||||
|
||||
```tsx | pure
|
||||
import { Application, Plugin } from '@nocobase/client';
|
||||
const Hello = () => <div>Hello</div>;
|
||||
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.addComponents({
|
||||
Hello
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Application({
|
||||
plugins: [MyPlugin]
|
||||
});
|
||||
```
|
||||
|
||||
#### 获取组件
|
||||
|
||||
```tsx | pure
|
||||
const app = new Application({
|
||||
components: {
|
||||
Hello
|
||||
}
|
||||
});
|
||||
|
||||
const Hello = app.component('Hello');
|
||||
```
|
||||
|
||||
#### 渲染组件
|
||||
|
||||
```tsx | pure
|
||||
const Test = (props) => <div>{props.name}</div>;
|
||||
const app = new Application({
|
||||
components: {
|
||||
Test
|
||||
}
|
||||
});
|
||||
|
||||
const Hello = () => {
|
||||
return <div>
|
||||
{app.renderComponent('Test', { name: 'foo' })}
|
||||
</div>
|
||||
};
|
||||
```
|
||||
|
||||
### 路由管理
|
||||
|
||||
提供了路由的增删改查功能。
|
||||
|
||||
#### 初始化路由
|
||||
|
||||
```tsx | pure
|
||||
const app = new Application({
|
||||
router: {
|
||||
type: 'history' // 默认是 history 类型
|
||||
}
|
||||
});
|
||||
|
||||
// 其他类型
|
||||
const app = new Application({
|
||||
router: {
|
||||
type: 'hash'
|
||||
}
|
||||
});
|
||||
|
||||
const app = new Application({
|
||||
router: {
|
||||
type: 'memory',
|
||||
initialEntries: ['/', '/foo']
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 添加路由
|
||||
|
||||
```tsx | pure
|
||||
const app = new Application();
|
||||
|
||||
// 通过 add 方法添加路由
|
||||
// 第一个是 name,第二个是路由配置
|
||||
app.router.add('home', {
|
||||
path: '/',
|
||||
element: <div>Home</div>
|
||||
});
|
||||
|
||||
app.router.add('about', {
|
||||
path: '/about',
|
||||
element: <div>About</div>
|
||||
});
|
||||
```
|
||||
|
||||
嵌套路由。
|
||||
|
||||
```tsx | pure
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
|
||||
const app = new Application();
|
||||
const Root = () => <div>
|
||||
<div>
|
||||
<Link to='/'>Home</Link>
|
||||
<Link to='/about'>About</Link>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>;
|
||||
|
||||
app.router.add('root', {
|
||||
element: <Layout />
|
||||
});
|
||||
|
||||
// 通过 . 区分路由层级
|
||||
app.router.add('root.home', {
|
||||
path: '/',
|
||||
element: <div>Home</div>
|
||||
});
|
||||
app.router.add('root.about', {
|
||||
path: '/about',
|
||||
element: <div>About</div>
|
||||
});
|
||||
|
||||
const AdminLayout = () => {
|
||||
return <div>
|
||||
<div>
|
||||
<Link to='/admin'>Admin Home</Link>
|
||||
<Link to='/admin/user'>Admin User</Link>
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
}
|
||||
|
||||
app.router.add('root.admin', {
|
||||
path: '/admin',
|
||||
element: <AdminLayout />
|
||||
});
|
||||
app.router.add('root.admin.home', {
|
||||
path: '/admin',
|
||||
element: <div>Admin Home</div>
|
||||
});
|
||||
app.router.add('root.admin.about', {
|
||||
path: '/admin/user',
|
||||
element: <div>Admin User</div>
|
||||
});
|
||||
```
|
||||
|
||||
支持 `Component` is 是字符串类型。
|
||||
|
||||
```tsx | pure
|
||||
const Hello = () => <div>Hello</div>;
|
||||
const World = () => <div>World</div>;
|
||||
|
||||
app.router.add('hello', {
|
||||
path: '/hello',
|
||||
Component: Hello
|
||||
});
|
||||
|
||||
// 先通过 addComponents 添加组件
|
||||
app.addComponents({ World });
|
||||
app.router.add('hello', {
|
||||
path: '/hello',
|
||||
Component: 'World' // 路由上使用 Component 字符串
|
||||
})
|
||||
```
|
||||
|
||||
在插件中添加路由。
|
||||
|
||||
```tsx | pure
|
||||
import { Application, Plugin } from '@nocobase/client';
|
||||
const Hello = () => <div>Hello</div>;
|
||||
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.router.add('hello', {
|
||||
path: '/hello',
|
||||
element: <Hello />
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Application({
|
||||
plugins: [MyPlugin]
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
#### 删除路由
|
||||
|
||||
```tsx | pure
|
||||
const app = new Application();
|
||||
app.router.add('home', {
|
||||
path: '/',
|
||||
element: <div>Home</div>
|
||||
});
|
||||
|
||||
// 通过 name 删除路由
|
||||
app.router.remove('home');
|
||||
```
|
||||
|
||||
### Scopes 管理
|
||||
|
||||
```tsx | pure
|
||||
const scopes = { foo: 'xxx' };
|
||||
// initial Application with scopes
|
||||
const app = new Application({ scopes });
|
||||
|
||||
// add multiple scopes
|
||||
app.addScopes({ bar: 'xxx' });
|
||||
```
|
||||
|
||||
### Providers 管理
|
||||
|
||||
```tsx | pure
|
||||
// Provider must render props.children
|
||||
const Hello = (props) => <div>Hello {props.children}</div>;
|
||||
const World = (props) => (
|
||||
<div>
|
||||
World {props.name} {props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
// initial Application with providers
|
||||
const app = new Application({
|
||||
providers: [Hello, [World, { name: 'aaa' }]]
|
||||
});
|
||||
```
|
||||
|
||||
It will render:
|
||||
|
||||
```tsx | pure
|
||||
<Hello><World name='aaa'>{routes}</World></Hello>
|
||||
```
|
||||
|
||||
```tsx | pure
|
||||
// add multiple providers
|
||||
app.addProviders([Hello, [World, { name: 'bbb' }]]);
|
||||
|
||||
// add single provider
|
||||
app.addProvider(Hello);
|
||||
app.addProvider(World, { name: 'ccc' });
|
||||
```
|
||||
|
||||
add provider in plugin.
|
||||
|
||||
```tsx | pure
|
||||
import { Application, Plugin } from '@nocobase/client';
|
||||
const Hello = (props) => <div>Hello {props.children}</div>;
|
||||
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.addProvider(Hello);
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Application({
|
||||
plugins: [MyPlugin]
|
||||
});
|
||||
```
|
||||
|
||||
### 插件管理
|
||||
|
||||
```tsx | pure
|
||||
import { Plugin } from '@nocobase/client';
|
||||
|
||||
class MyPlugin extends Plugin {
|
||||
async afterAdd() {
|
||||
// You can load other plugins here.
|
||||
// this.app.pm.add(OtherPlugin)
|
||||
}
|
||||
|
||||
async load() {
|
||||
// modify app
|
||||
// this.app.router.add('hello', { xx })
|
||||
// this.app.addComponents({ Hello })
|
||||
// this.app.addProviders([Hello])
|
||||
// this.app.addScopes({ foo: 'xxx' })
|
||||
}
|
||||
|
||||
async afterLoad() {
|
||||
// do something
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
load other plugin.
|
||||
|
||||
```tsx | pure
|
||||
|
||||
class HelloPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.router.add('hello', {
|
||||
path: '/hello',
|
||||
element: <div>Hello</div>
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const World = () => <div>World</div>;
|
||||
class WorldPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.addComponents({
|
||||
World
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class MyPlugin extends Plugin {
|
||||
async afterAdd() {
|
||||
this.app.pm.add(HelloPlugin);
|
||||
this.app.pm.add(WorldPlugin);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 渲染
|
||||
|
||||
#### Root Component
|
||||
|
||||
```tsx | pure
|
||||
const RootComponent = app.getRootComponent();
|
||||
|
||||
const App = () => {
|
||||
return <div>
|
||||
My other logic
|
||||
<RootComponent />
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
#### mount
|
||||
|
||||
```tsx | pure
|
||||
app.mount('#app');
|
||||
```
|
||||
|
5
packages/core/client/src/application/index.ts
Normal file
5
packages/core/client/src/application/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './Application';
|
||||
export * from './hooks';
|
||||
export * from './Plugin';
|
||||
export * from './RouterManager';
|
||||
export * from './utils';
|
@ -1,2 +0,0 @@
|
||||
export * from './Application';
|
||||
export * from './compose';
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user