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
|
||||||
**/.dumi/tmp-test
|
**/.dumi/tmp-test
|
||||||
**/.dumi/tmp-production
|
**/.dumi/tmp-production
|
||||||
|
packages/core/cli/templates/plugin/src/client/*.tpl
|
||||||
|
@ -34,6 +34,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"@typescript-eslint/no-this-alias": "off",
|
||||||
|
"@typescript-eslint/ban-types": "off",
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
"no-empty-function": "off",
|
"no-empty-function": "off",
|
||||||
|
@ -13,3 +13,4 @@ packages/core/client/src/locale/*
|
|||||||
**/.dumi/tmp
|
**/.dumi/tmp
|
||||||
**/.dumi/tmp-test
|
**/.dumi/tmp-test
|
||||||
**/.dumi/tmp-production
|
**/.dumi/tmp-production
|
||||||
|
packages/core/cli/templates/plugin/src/client/*.tpl
|
||||||
|
@ -85,6 +85,7 @@ const sidebar = {
|
|||||||
// '/welcome/release/index',
|
// '/welcome/release/index',
|
||||||
// '/welcome/release/v08-changelog',
|
// '/welcome/release/v08-changelog',
|
||||||
'/welcome/release/v10-changelog',
|
'/welcome/release/v10-changelog',
|
||||||
|
'/welcome/release/v11-changelog',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -260,7 +261,7 @@ const sidebar = {
|
|||||||
children: [
|
children: [
|
||||||
// '/api/client',
|
// '/api/client',
|
||||||
'/api/client/application',
|
'/api/client/application',
|
||||||
'/api/client/route-switch',
|
'/api/client/router',
|
||||||
{
|
{
|
||||||
title: 'SchemaDesigner',
|
title: 'SchemaDesigner',
|
||||||
'title.zh-CN': 'SchemaDesigner',
|
'title.zh-CN': 'SchemaDesigner',
|
||||||
|
@ -32,7 +32,6 @@ Add Providers, build-in Providers are:
|
|||||||
- APIClientProvider
|
- APIClientProvider
|
||||||
- I18nextProvider
|
- I18nextProvider
|
||||||
- AntdConfigProvider
|
- AntdConfigProvider
|
||||||
- RemoteRouteSwitchProvider
|
|
||||||
- SystemSettingsProvider
|
- SystemSettingsProvider
|
||||||
- PluginManagerProvider
|
- PluginManagerProvider
|
||||||
- SchemaComponentProvider
|
- 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
|
- APIClientProvider
|
||||||
- I18nextProvider
|
- I18nextProvider
|
||||||
- AntdConfigProvider
|
- AntdConfigProvider
|
||||||
- RemoteRouteSwitchProvider
|
|
||||||
- SystemSettingsProvider
|
- SystemSettingsProvider
|
||||||
- PluginManagerProvider
|
- PluginManagerProvider
|
||||||
- SchemaComponentProvider
|
- SchemaComponentProvider
|
||||||
|
@ -1,62 +1,66 @@
|
|||||||
# UI Routing
|
# 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
|
```tsx
|
||||||
/**
|
/**
|
||||||
* defaultShowCode: true
|
* defaultShowCode: true
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, MemoryRouter as Router } from 'react-router-dom';
|
import { Link, Outlet } from 'react-router-dom';
|
||||||
import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client';
|
import { Application } from '@nocobase/client';
|
||||||
|
|
||||||
const Home = () => <h1>Home</h1>;
|
const Home = () => <h1>Home</h1>;
|
||||||
const About = () => <h1>About</h1>;
|
const About = () => <h1>About</h1>;
|
||||||
|
|
||||||
const routes: RouteRedirectProps[] = [
|
const Layout = () => {
|
||||||
{
|
return <div>
|
||||||
type: 'route',
|
<div><Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link></div>
|
||||||
path: '/',
|
<Outlet />
|
||||||
component: 'Home',
|
</div>
|
||||||
},
|
}
|
||||||
{
|
|
||||||
type: 'route',
|
|
||||||
path: '/about',
|
|
||||||
component: 'About',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default () => {
|
const app = new Application({
|
||||||
return (
|
router: {
|
||||||
<RouteSwitchProvider components={{ Home, About }}>
|
type: 'memory',
|
||||||
<Router initialEntries={['/']}>
|
initialEntries: ['/']
|
||||||
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
|
}
|
||||||
<RouteSwitch routes={routes} />
|
})
|
||||||
</Router>
|
|
||||||
</RouteSwitchProvider>
|
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.
|
In a full NocoBase application, the Route can be extended in a similar way as follows.
|
||||||
|
|
||||||
```tsx | pure
|
```tsx | pure
|
||||||
import { RouteSwitchContext } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import React, { useContext } from 'react';
|
|
||||||
|
|
||||||
const HelloWorld = () => {
|
class MyPlugin extends Plugin {
|
||||||
return <div>Hello ui router</div>;
|
async load() {
|
||||||
};
|
// add
|
||||||
|
this.app.router.add('hello', {
|
||||||
|
path: '/hello',
|
||||||
|
element: <div>hello</div>,
|
||||||
|
})
|
||||||
|
|
||||||
export default React.memo((props) => {
|
// remove
|
||||||
const ctx = useContext(RouteSwitchContext);
|
this.app.router.remove('hello');
|
||||||
ctx.routes.push({
|
}
|
||||||
type: 'route',
|
}
|
||||||
path: '/hello-world',
|
|
||||||
component: HelloWorld,
|
|
||||||
});
|
|
||||||
return <RouteSwitchContext.Provider value={ctx}>{props.children}</RouteSwitchContext.Provider>;
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
See [packages/samples/custom-page](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-page) for the full example
|
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
|
- APIClientProvider
|
||||||
- I18nextProvider
|
- I18nextProvider
|
||||||
- AntdConfigProvider
|
- AntdConfigProvider
|
||||||
- RemoteRouteSwitchProvider
|
|
||||||
- SystemSettingsProvider
|
- SystemSettingsProvider
|
||||||
- PluginManagerProvider
|
- PluginManagerProvider
|
||||||
- SchemaComponentProvider
|
- 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;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 完整示例
|
|
||||||
|
|
||||||
```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
|
- APIClientProvider
|
||||||
- I18nextProvider
|
- I18nextProvider
|
||||||
- AntdConfigProvider
|
- AntdConfigProvider
|
||||||
- RemoteRouteSwitchProvider
|
|
||||||
- SystemSettingsProvider
|
- SystemSettingsProvider
|
||||||
- PluginManagerProvider
|
- PluginManagerProvider
|
||||||
- SchemaComponentProvider
|
- SchemaComponentProvider
|
||||||
|
@ -1,62 +1,66 @@
|
|||||||
# UI Routing
|
# 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
|
```tsx
|
||||||
/**
|
/**
|
||||||
* defaultShowCode: true
|
* defaultShowCode: true
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, MemoryRouter as Router } from 'react-router-dom';
|
import { Link, Outlet } from 'react-router-dom';
|
||||||
import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client';
|
import { Application } from '@nocobase/client';
|
||||||
|
|
||||||
const Home = () => <h1>Home</h1>;
|
const Home = () => <h1>Home</h1>;
|
||||||
const About = () => <h1>About</h1>;
|
const About = () => <h1>About</h1>;
|
||||||
|
|
||||||
const routes: RouteRedirectProps[] = [
|
const Layout = () => {
|
||||||
{
|
return <div>
|
||||||
type: 'route',
|
<div><Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link></div>
|
||||||
path: '/',
|
<Outlet />
|
||||||
component: 'Home',
|
</div>
|
||||||
},
|
}
|
||||||
{
|
|
||||||
type: 'route',
|
|
||||||
path: '/about',
|
|
||||||
component: 'About',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default () => {
|
const app = new Application({
|
||||||
return (
|
router: {
|
||||||
<RouteSwitchProvider components={{ Home, About }}>
|
type: 'memory',
|
||||||
<Router initialEntries={['/']}>
|
initialEntries: ['/']
|
||||||
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
|
}
|
||||||
<RouteSwitch routes={routes} />
|
})
|
||||||
</Router>
|
|
||||||
</RouteSwitchProvider>
|
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.
|
In a full NocoBase application, the Route can be extended in a similar way as follows.
|
||||||
|
|
||||||
```tsx | pure
|
```tsx | pure
|
||||||
import { RouteSwitchContext } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import React, { useContext } from 'react';
|
|
||||||
|
|
||||||
const HelloWorld = () => {
|
class MyPlugin extends Plugin {
|
||||||
return <div>Hello ui router</div>;
|
async load() {
|
||||||
};
|
// add
|
||||||
|
this.app.router.add('hello', {
|
||||||
|
path: '/hello',
|
||||||
|
element: <div>hello</div>,
|
||||||
|
})
|
||||||
|
|
||||||
export default React.memo((props) => {
|
// remove
|
||||||
const ctx = useContext(RouteSwitchContext);
|
this.app.router.remove('hello');
|
||||||
ctx.routes.push({
|
}
|
||||||
type: 'route',
|
}
|
||||||
path: '/hello-world',
|
|
||||||
component: HelloWorld,
|
|
||||||
});
|
|
||||||
return <RouteSwitchContext.Provider value={ctx}>{props.children}</RouteSwitchContext.Provider>;
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
See [packages/samples/custom-page](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-page) for the full example
|
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
|
- APIClientProvider
|
||||||
- I18nextProvider
|
- I18nextProvider
|
||||||
- AntdConfigProvider
|
- AntdConfigProvider
|
||||||
- RemoteRouteSwitchProvider
|
|
||||||
- SystemSettingsProvider
|
- SystemSettingsProvider
|
||||||
- PluginManagerProvider
|
- PluginManagerProvider
|
||||||
- SchemaComponentProvider
|
- 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;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 完整示例
|
|
||||||
|
|
||||||
```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
|
- APIClientProvider
|
||||||
- I18nextProvider
|
- I18nextProvider
|
||||||
- AntdConfigProvider
|
- AntdConfigProvider
|
||||||
- RemoteRouteSwitchProvider
|
|
||||||
- SystemSettingsProvider
|
- SystemSettingsProvider
|
||||||
- PluginManagerProvider
|
- PluginManagerProvider
|
||||||
- SchemaComponentProvider
|
- SchemaComponentProvider
|
||||||
|
@ -1,62 +1,66 @@
|
|||||||
# UI 路由
|
# 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
|
```tsx
|
||||||
/**
|
/**
|
||||||
* defaultShowCode: true
|
* defaultShowCode: true
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, MemoryRouter as Router } from 'react-router-dom';
|
import { Link, Outlet } from 'react-router-dom';
|
||||||
import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client';
|
import { Application } from '@nocobase/client';
|
||||||
|
|
||||||
const Home = () => <h1>Home</h1>;
|
const Home = () => <h1>Home</h1>;
|
||||||
const About = () => <h1>About</h1>;
|
const About = () => <h1>About</h1>;
|
||||||
|
|
||||||
const routes: RouteRedirectProps[] = [
|
const Layout = () => {
|
||||||
{
|
return <div>
|
||||||
type: 'route',
|
<div><Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link></div>
|
||||||
path: '/',
|
<Outlet />
|
||||||
component: 'Home',
|
</div>
|
||||||
},
|
}
|
||||||
{
|
|
||||||
type: 'route',
|
|
||||||
path: '/about',
|
|
||||||
component: 'About',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default () => {
|
const app = new Application({
|
||||||
return (
|
router: {
|
||||||
<RouteSwitchProvider components={{ Home, About }}>
|
type: 'memory',
|
||||||
<Router initialEntries={['/']}>
|
initialEntries: ['/']
|
||||||
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
|
}
|
||||||
<RouteSwitch routes={routes} />
|
})
|
||||||
</Router>
|
|
||||||
</RouteSwitchProvider>
|
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:
|
在完整的 NocoBase 应用里,可以类似以下的的方式扩展 Route:
|
||||||
|
|
||||||
```tsx | pure
|
```tsx | pure
|
||||||
import { RouteSwitchContext } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import React, { useContext } from 'react';
|
|
||||||
|
|
||||||
const HelloWorld = () => {
|
class MyPlugin extends Plugin {
|
||||||
return <div>Hello ui router</div>;
|
async load() {
|
||||||
};
|
// 添加一条路由
|
||||||
|
this.app.router.add('hello', {
|
||||||
|
path: '/hello',
|
||||||
|
element: <div>hello</div>,
|
||||||
|
})
|
||||||
|
|
||||||
export default React.memo((props) => {
|
// 删除一条路由
|
||||||
const ctx = useContext(RouteSwitchContext);
|
this.app.router.remove('hello');
|
||||||
ctx.routes.push({
|
}
|
||||||
type: 'route',
|
}
|
||||||
path: '/hello-world',
|
|
||||||
component: HelloWorld,
|
|
||||||
});
|
|
||||||
return <RouteSwitchContext.Provider value={ctx}>{props.children}</RouteSwitchContext.Provider>;
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
完整示例查看 [packages/samples/custom-page](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-page)
|
完整示例查看 [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": {
|
"devDependencies": {
|
||||||
|
"commander": "^9.2.0",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
@ -57,7 +58,7 @@
|
|||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
"auto-changelog": "^2.4.0",
|
"auto-changelog": "^2.4.0",
|
||||||
"dumi": "^2.2.0",
|
"dumi": "^2.2.0",
|
||||||
"dumi-theme-nocobase": "^0.2.12",
|
"dumi-theme-nocobase": "^0.2.14",
|
||||||
"ghooks": "^2.0.4",
|
"ghooks": "^2.0.4",
|
||||||
"jsdom-worker": "^0.3.0",
|
"jsdom-worker": "^0.3.0",
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
|
@ -20,8 +20,7 @@ export default defineConfig({
|
|||||||
{ rel: 'stylesheet', href: '/global.css' },
|
{ rel: 'stylesheet', href: '/global.css' },
|
||||||
],
|
],
|
||||||
headScripts: [
|
headScripts: [
|
||||||
'/browser-checker.js',
|
'/browser-checker.js'
|
||||||
'/set-router.js',
|
|
||||||
],
|
],
|
||||||
hash: true,
|
hash: true,
|
||||||
alias: {
|
alias: {
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
var match = location.pathname.match(/^\/apps\/([^/]*)\//);
|
|
||||||
window.routerBase = match ? match[0] : "/";
|
|
@ -1,5 +1,6 @@
|
|||||||
import '@/theme';
|
import '@/theme';
|
||||||
import { Application } from '@nocobase/client';
|
import { Application } from '@nocobase/client';
|
||||||
|
import { NocoBaseClientPresetPlugin } from '@nocobase/preset-nocobase/client';
|
||||||
|
|
||||||
export const app = new Application({
|
export const app = new Application({
|
||||||
apiClient: {
|
apiClient: {
|
||||||
@ -8,6 +9,7 @@ export const app = new Application({
|
|||||||
dynamicImport: (name: string) => {
|
dynamicImport: (name: string) => {
|
||||||
return import(`../plugins/${name}`);
|
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 registerBabel from './registerBabel';
|
||||||
import rollup from './rollup';
|
import rollup from './rollup';
|
||||||
import { Dispose, IBundleOptions, IBundleTypeOutput, ICjs, IEsm, IOpts } from './types';
|
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[] {
|
export function getBundleOpts(opts: IOpts): IBundleOptions[] {
|
||||||
const { cwd, buildArgs = {}, rootConfig = {} } = opts;
|
const { cwd, buildArgs = {}, rootConfig = {} } = opts;
|
||||||
const entry = getExistFile({
|
const entry = getExistFiles({
|
||||||
cwd,
|
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,
|
returnRelative: true,
|
||||||
});
|
});
|
||||||
const userConfig = getUserConfig({ cwd, customPath: buildArgs.config });
|
const userConfig = getUserConfig({ cwd, customPath: buildArgs.config });
|
||||||
|
@ -5,7 +5,7 @@ import signale from 'signale';
|
|||||||
import slash from 'slash2';
|
import slash from 'slash2';
|
||||||
import schema from './schema';
|
import schema from './schema';
|
||||||
import { IBundleOptions } from './types';
|
import { IBundleOptions } from './types';
|
||||||
import { getExistFile } from './utils';
|
import { getExistFiles } from './utils';
|
||||||
|
|
||||||
function testDefault(obj) {
|
function testDefault(obj) {
|
||||||
return obj.default || obj;
|
return obj.default || obj;
|
||||||
@ -51,7 +51,7 @@ export default function({ cwd, customPath }: { cwd: string; customPath?: string
|
|||||||
|
|
||||||
const configFile =
|
const configFile =
|
||||||
finalPath ||
|
finalPath ||
|
||||||
getExistFile({
|
getExistFiles({
|
||||||
cwd,
|
cwd,
|
||||||
files: CONFIG_FILES,
|
files: CONFIG_FILES,
|
||||||
returnRelative: false,
|
returnRelative: false,
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import { join } from 'path';
|
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) {
|
for (const file of files) {
|
||||||
const absFilePath = join(cwd, file);
|
const absFilePath = join(cwd, file);
|
||||||
if (existsSync(absFilePath)) {
|
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';
|
export { getLernaPackages } from './getLernaPackages';
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
// @ts-nocheck
|
export * from './src/client';
|
||||||
export * from './lib/client';
|
export { default } from './src/client';
|
||||||
export { default } from './lib/client';
|
|
||||||
|
@ -1,65 +1 @@
|
|||||||
'use strict';
|
module.exports = require('./lib/client/index.js');
|
||||||
|
|
||||||
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];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
@ -2,6 +2,17 @@
|
|||||||
"name": "{{{packageName}}}",
|
"name": "{{{packageName}}}",
|
||||||
"version": "{{{packageVersion}}}",
|
"version": "{{{packageVersion}}}",
|
||||||
"main": "lib/server/index.js",
|
"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": {
|
"devDependencies": {
|
||||||
"@nocobase/server": "{{{nocobaseVersion}}}",
|
"@nocobase/server": "{{{nocobaseVersion}}}",
|
||||||
"@nocobase/test": "{{{nocobaseVersion}}}"
|
"@nocobase/test": "{{{nocobaseVersion}}}"
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
// @ts-nocheck
|
export * from './src/server';
|
||||||
export * from './lib/server';
|
export { default } from './src/server';
|
||||||
export { default } from './lib/server';
|
|
||||||
|
@ -1,65 +1 @@
|
|||||||
'use strict';
|
module.exports = require('./lib/server/index.js');
|
||||||
|
|
||||||
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];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
@ -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
|
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
|
```tsx
|
||||||
/**
|
/**
|
||||||
* defaultShowCode: true
|
* defaultShowCode: true
|
||||||
* title: RouteSwitch
|
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, MemoryRouter as Router } from 'react-router-dom';
|
import { Link, Outlet } from 'react-router-dom';
|
||||||
import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client';
|
import { Application } from '@nocobase/client';
|
||||||
|
|
||||||
const Home = () => <h1>Home</h1>;
|
const Home = () => <h1>Home</h1>;
|
||||||
const About = () => <h1>About</h1>;
|
const About = () => <h1>About</h1>;
|
||||||
|
|
||||||
const routes: RouteRedirectProps[] = [
|
const Layout = () => {
|
||||||
{
|
return <div>
|
||||||
type: 'route',
|
<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: '/',
|
path: '/',
|
||||||
component: 'Home',
|
element: <Home />
|
||||||
},
|
})
|
||||||
{
|
|
||||||
type: 'route',
|
app.router.add('root.about', {
|
||||||
path: '/about',
|
path: '/about',
|
||||||
component: 'About',
|
element: <About />
|
||||||
},
|
})
|
||||||
];
|
|
||||||
|
|
||||||
export default () => {
|
export default app.getRootComponent();
|
||||||
return (
|
|
||||||
<RouteSwitchProvider components={{ Home, About }}>
|
|
||||||
<Router initialEntries={['/']}>
|
|
||||||
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
|
|
||||||
<RouteSwitch routes={routes} />
|
|
||||||
</Router>
|
|
||||||
</RouteSwitchProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- 由 RouteSwitchProvider 配置 components,由开发编写,以 Layout 或 Template 的方式提供给 RouteSwitch 使用。
|
|
||||||
- 由 RouteSwitch 配置 routes,JSON 的方式,可以由后端获取,方便后续的动态化、无代码的支持。
|
|
||||||
|
|
||||||
## SchemaComponent
|
## SchemaComponent
|
||||||
|
|
||||||
路由可以通过 JSON 的方式配置,可以注册诸多可供路由使用的组件模板,以方便各种场景支持,但是这些组件还是需要开发编写和维护,所以进一步将组件抽象,转换成配置化的方式。如:
|
路由可以通过 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
|
## Designable
|
||||||
|
|
||||||
SchemaComponent 基于 Formily 的 SchemaField,Formily 提供了 [Designable](https://github.com/alibaba/designable) 来解决 Schema 的配置问题,但是这套方案:
|
SchemaComponent 基于 Formily 的 SchemaField,Formily 提供了 [Designable](https://github.com/alibaba/designable) 来解决 Schema 的配置问题,但是这套方案:
|
||||||
@ -781,7 +642,6 @@ const { data, loading } = useRequest();
|
|||||||
|
|
||||||
客户端的扩展以 Providers 的形式存在,提供各种可供组件使用的 Context,可全局也可以局部使用。上文我们已经介绍了核心的三个 Providers:
|
客户端的扩展以 Providers 的形式存在,提供各种可供组件使用的 Context,可全局也可以局部使用。上文我们已经介绍了核心的三个 Providers:
|
||||||
|
|
||||||
- RouteSwitchProvider,提供配置路由所需的 Layout 和 Template 组件
|
|
||||||
- SchemaComponentProvider,提供配置 Schema 所需的各种组件
|
- SchemaComponentProvider,提供配置 Schema 所需的各种组件
|
||||||
- ApiClientProvider,提供客户端 SDK
|
- ApiClientProvider,提供客户端 SDK
|
||||||
|
|
||||||
@ -800,16 +660,14 @@ const { data, loading } = useRequest();
|
|||||||
```tsx | pure
|
```tsx | pure
|
||||||
<ApiClientProvider>
|
<ApiClientProvider>
|
||||||
<SchemaComponentProvider>
|
<SchemaComponentProvider>
|
||||||
<RouteSwitchProvider>
|
|
||||||
{...}
|
{...}
|
||||||
</RouteSwitchProvider>
|
|
||||||
</SchemaComponentProvider>
|
</SchemaComponentProvider>
|
||||||
</ApiClientProvider>
|
</ApiClientProvider>
|
||||||
```
|
```
|
||||||
|
|
||||||
但是这样的方式不利于 Providers 的管理和扩展,为此提炼了 `compose()` 函数用于配置多个 providers,如下:
|
但是这样的方式不利于 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
|
## Application
|
||||||
|
|
||||||
|
@ -53,12 +53,12 @@
|
|||||||
"react-is": ">=18.0.0"
|
"react-is": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"dumi": "^2.2.0",
|
||||||
|
"dumi-theme-nocobase": "^0.2.14",
|
||||||
"@testing-library/react": "^12.1.2",
|
"@testing-library/react": "^12.1.2",
|
||||||
"@types/markdown-it": "12.2.3",
|
"@types/markdown-it": "12.2.3",
|
||||||
"@types/markdown-it-highlightjs": "3.3.1",
|
"@types/markdown-it-highlightjs": "3.3.1",
|
||||||
"axios-mock-adapter": "^1.20.0",
|
"axios-mock-adapter": "^1.20.0"
|
||||||
"dumi": "^2.2.0",
|
|
||||||
"dumi-theme-nocobase": "^0.2.9"
|
|
||||||
},
|
},
|
||||||
"gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644"
|
"gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644"
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import { SchemaComponentOptions, useDesignable } from '../schema-component';
|
|||||||
|
|
||||||
export const ACLContext = createContext<any>({});
|
export const ACLContext = createContext<any>({});
|
||||||
|
|
||||||
|
// TODO: delete this,replace by `ACLPlugin`
|
||||||
export const ACLProvider = (props) => {
|
export const ACLProvider = (props) => {
|
||||||
return (
|
return (
|
||||||
<SchemaComponentOptions
|
<SchemaComponentOptions
|
||||||
|
@ -1,4 +1,22 @@
|
|||||||
export * from './ACLProvider';
|
export * from './ACLProvider';
|
||||||
export * from './ACLShortcut';
|
export * from './ACLShortcut';
|
||||||
|
|
||||||
|
import { Plugin } from '../application/Plugin';
|
||||||
|
import {
|
||||||
|
ACLActionProvider,
|
||||||
|
ACLCollectionFieldProvider,
|
||||||
|
ACLCollectionProvider,
|
||||||
|
ACLMenuItemProvider,
|
||||||
|
} from './ACLProvider';
|
||||||
import './style.less';
|
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 React, { createContext, useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAPIClient, useRequest } from '../api-client';
|
import { useAPIClient, useRequest } from '../api-client';
|
||||||
|
import { Plugin } from '../application/Plugin';
|
||||||
import { loadConstrueLocale } from './loadConstrueLocale';
|
import { loadConstrueLocale } from './loadConstrueLocale';
|
||||||
|
|
||||||
export const AppLangContext = createContext<any>({});
|
export const AppLangContext = createContext<any>({});
|
||||||
@ -50,3 +51,9 @@ export function AntdConfigProvider(props) {
|
|||||||
</AppLangContext.Provider>
|
</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 { i18n as i18next } from 'i18next';
|
||||||
import React, { useEffect, useState } from 'react';
|
import get from 'lodash/get';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
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 { I18nextProvider } from 'react-i18next';
|
||||||
import { Link, NavLink } from 'react-router-dom';
|
import { Link, Navigate, NavLink } from 'react-router-dom';
|
||||||
import { ACLProvider } from '../acl';
|
|
||||||
import { AntdConfigProvider } from '../antd-config-provider';
|
|
||||||
import { APIClient, APIClientProvider } from '../api-client';
|
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 { i18n } from '../i18n';
|
||||||
import { PinnedPluginListProvider } from '../plugin-manager';
|
import { AppComponent, BlankComponent, defaultAppComponents } from './components';
|
||||||
import PMProvider, { PluginManagerLink, SettingsCenterDropdown } from '../pm';
|
import { PluginManager, PluginType } from './PluginManager';
|
||||||
import {
|
import { ComponentTypeAndString, RouterManager, RouterOptions } from './RouterManager';
|
||||||
AdminLayout,
|
import { compose, normalizeContainer } from './utils';
|
||||||
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';
|
|
||||||
|
|
||||||
|
export type ComponentAndProps<T = any> = [ComponentType, T];
|
||||||
export interface ApplicationOptions {
|
export interface ApplicationOptions {
|
||||||
apiClient?: any;
|
apiClient?: APIClientOptions;
|
||||||
i18n?: any;
|
i18n?: i18next;
|
||||||
plugins?: any[];
|
providers?: (ComponentType | ComponentAndProps)[];
|
||||||
|
plugins?: PluginType[];
|
||||||
|
components?: Record<string, ComponentType>;
|
||||||
|
scopes?: Record<string, any>;
|
||||||
|
router?: RouterOptions;
|
||||||
dynamicImport?: any;
|
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 {
|
export class Application {
|
||||||
providers = [];
|
public providers: ComponentAndProps[] = [];
|
||||||
mainComponent = null;
|
public router: RouterManager;
|
||||||
apiClient: APIClient;
|
public scopes: Record<string, any> = {};
|
||||||
i18n: i18next;
|
public i18n: i18next;
|
||||||
plugins: PluginCallback[] = [];
|
public apiClient: APIClient;
|
||||||
options: ApplicationOptions;
|
public components: Record<string, ComponentType> = { ...defaultAppComponents };
|
||||||
|
public pm: PluginManager;
|
||||||
|
|
||||||
constructor(options: ApplicationOptions) {
|
constructor(protected options: ApplicationOptions = {}) {
|
||||||
this.options = options;
|
this.scopes = merge(this.scopes, options.scopes);
|
||||||
this.apiClient = new APIClient({
|
this.components = merge(this.components, options.components);
|
||||||
baseURL: process.env.API_BASE_URL,
|
this.apiClient = new APIClient(options.apiClient);
|
||||||
headers: {
|
|
||||||
'X-Hostname': window?.location?.hostname,
|
|
||||||
'X-Timezone': getCurrentTimezone(),
|
|
||||||
},
|
|
||||||
...options.apiClient,
|
|
||||||
});
|
|
||||||
this.i18n = options.i18n || i18n;
|
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(APIClientProvider, { apiClient: this.apiClient });
|
||||||
this.use(I18nextProvider, { i18n: this.i18n });
|
this.use(I18nextProvider, { i18n: this.i18n });
|
||||||
this.use(AntdConfigProvider, { remoteLocale: true });
|
}
|
||||||
this.use(RemoteRouteSwitchProvider, {
|
|
||||||
components: {
|
private addReactRouterComponents() {
|
||||||
AuthLayout,
|
this.addComponents({
|
||||||
AdminLayout,
|
Link,
|
||||||
RouteSchemaComponent,
|
Navigate: Navigate as ComponentType,
|
||||||
SigninPage,
|
NavLink,
|
||||||
SignupPage,
|
|
||||||
BlockTemplatePage,
|
|
||||||
BlockTemplateDetails,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
this.use(SystemSettingsProvider);
|
}
|
||||||
this.use(PinnedPluginListProvider, {
|
|
||||||
items: {
|
get dynamicImport() {
|
||||||
ui: { order: 100, component: 'DesignableSwitch', pin: true, snippet: 'ui.*' },
|
return this.options.dynamicImport;
|
||||||
pm: { order: 200, component: 'PluginManagerLink', pin: true, snippet: 'pm' },
|
}
|
||||||
sc: { order: 300, component: 'SettingsCenterDropdown', pin: true, snippet: 'pm.*' },
|
|
||||||
},
|
getComposeProviders() {
|
||||||
|
const Providers = compose(...this.providers)(BlankComponent);
|
||||||
|
Providers.displayName = 'Providers';
|
||||||
|
return Providers;
|
||||||
|
}
|
||||||
|
|
||||||
|
use<T = any>(component: ComponentType, props?: T) {
|
||||||
|
return this.addProvider(component, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
addProvider<T = any>(component: ComponentType, props?: T) {
|
||||||
|
return this.providers.push([component, props]);
|
||||||
|
}
|
||||||
|
|
||||||
|
addProviders(providers: (ComponentType | [ComponentType, any])[]) {
|
||||||
|
providers.forEach((provider) => {
|
||||||
|
if (Array.isArray(provider)) {
|
||||||
|
this.addProvider(provider[0], provider[1]);
|
||||||
|
} else {
|
||||||
|
this.addProvider(provider);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.use(SchemaComponentProvider, {
|
}
|
||||||
components: { Link, NavLink, DesignableSwitch, PluginManagerLink, SettingsCenterDropdown },
|
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
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) {
|
addScopes(scopes: Record<string, any>) {
|
||||||
this.providers.push(props ? [component, props] : component);
|
this.scopes = merge(this.scopes, scopes);
|
||||||
}
|
}
|
||||||
|
|
||||||
main(mainComponent: any) {
|
getRootComponent() {
|
||||||
this.mainComponent = mainComponent;
|
const Root: FC = () => <AppComponent app={this} />;
|
||||||
|
return Root;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
mount(containerOrSelector: Element | ShadowRoot | string) {
|
||||||
* TODO
|
const container = normalizeContainer(containerOrSelector);
|
||||||
*/
|
if (!container) return;
|
||||||
plugin(plugin: PluginCallback) {
|
const App = this.getRootComponent();
|
||||||
this.plugins.push(plugin);
|
const root = createRoot(container);
|
||||||
}
|
root.render(<App />);
|
||||||
|
return root;
|
||||||
handleErrors(error: any) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback} onError={this.handleErrors}>
|
|
||||||
<App providers={this.providers} />
|
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 { 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 { Link, Outlet, useLocation } from 'react-router-dom';
|
||||||
import { Application } from '../Application';
|
import { Application } from '../Application';
|
||||||
import { Plugin } from '../Plugin';
|
import { Plugin } from '../Plugin';
|
||||||
@ -15,6 +15,10 @@ const Root = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Home = () => {
|
||||||
|
return <div>this is Home</div>;
|
||||||
|
};
|
||||||
|
|
||||||
const Team = () => {
|
const Team = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -35,7 +39,7 @@ class Test1Plugin extends Plugin {
|
|||||||
async load() {
|
async load() {
|
||||||
this.router.add('root.team', {
|
this.router.add('root.team', {
|
||||||
path: 'team',
|
path: 'team',
|
||||||
component: 'Team',
|
Component: 'Team',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -44,21 +48,30 @@ class Test2Plugin extends Plugin {
|
|||||||
async load() {
|
async load() {
|
||||||
this.router.add('root.about', {
|
this.router.add('root.about', {
|
||||||
path: 'about',
|
path: 'about',
|
||||||
component: 'About',
|
Component: 'About',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NocobasePresetPlugin extends Plugin {
|
class NocobasePresetPlugin extends Plugin {
|
||||||
async afterAdd() {
|
async afterAdd() {
|
||||||
await this.pm.add('test1');
|
// mock load remote plugin
|
||||||
await this.pm.add(Test2Plugin, { name: 'test2' });
|
await this.addRemotePlugin();
|
||||||
|
}
|
||||||
|
|
||||||
|
async addRemotePlugin() {
|
||||||
|
await this.pm.add(Test1Plugin);
|
||||||
|
await this.pm.add(Test2Plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
this.router.add('root', {
|
this.router.add('root', {
|
||||||
path: '/',
|
path: '/',
|
||||||
component: 'Root',
|
Component: 'Root',
|
||||||
|
});
|
||||||
|
this.router.add('root.home', {
|
||||||
|
path: '/',
|
||||||
|
Component: 'Home',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,21 +83,18 @@ const app = new Application({
|
|||||||
router: {
|
router: {
|
||||||
type: 'hash',
|
type: 'hash',
|
||||||
},
|
},
|
||||||
plugins: [[NocobasePresetPlugin, { name: 'nocobase' }]],
|
plugins: [NocobasePresetPlugin],
|
||||||
importPlugins: async (name) => {
|
components: { Root, Home, Team, About },
|
||||||
return {
|
|
||||||
test1: Test1Plugin,
|
|
||||||
}[name];
|
|
||||||
},
|
|
||||||
components: { Root, Team, About },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use((props) => {
|
const HelloProvider: FC = (props) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
if (location.pathname === '/hello') {
|
if (location.pathname === '/hello') {
|
||||||
return <div>Hello</div>;
|
return <div>Hello</div>;
|
||||||
}
|
}
|
||||||
return props.children;
|
return <>{props.children}</>;
|
||||||
});
|
};
|
||||||
|
|
||||||
|
app.use(HelloProvider);
|
||||||
|
|
||||||
export default app.getRootComponent();
|
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 { useContext } from 'react';
|
||||||
|
import type { Application } from '../Application';
|
||||||
import { ApplicationContext } from '../context';
|
import { ApplicationContext } from '../context';
|
||||||
|
|
||||||
export const useApp = () => {
|
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
|
order: 1
|
||||||
---
|
---
|
||||||
|
|
||||||
# Application <Badge>待定</Badge>
|
# Application V2
|
||||||
|
|
||||||
<img src="https://nocobase.oss-cn-beijing.aliyuncs.com/5be7ebc2f47effef85be7a0c75cf76f9.png" style="max-width: 800px;" />
|
<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