diff --git a/.eslintignore b/.eslintignore index aa5a3a110..e6e7eedb6 100755 --- a/.eslintignore +++ b/.eslintignore @@ -23,3 +23,4 @@ packages/core/database/src/sql-parser/index.js **/.dumi/tmp **/.dumi/tmp-test **/.dumi/tmp-production +packages/core/cli/templates/plugin/src/client/*.tpl diff --git a/.eslintrc b/.eslintrc index 805d21c4d..d23c6be12 100755 --- a/.eslintrc +++ b/.eslintrc @@ -34,6 +34,8 @@ } }, "rules": { + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/ban-types": "off", "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off", "no-empty-function": "off", diff --git a/.prettierignore b/.prettierignore index 2899bee96..439b62d59 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,3 +13,4 @@ packages/core/client/src/locale/* **/.dumi/tmp **/.dumi/tmp-test **/.dumi/tmp-production +packages/core/cli/templates/plugin/src/client/*.tpl diff --git a/docs/config.ts b/docs/config.ts index 4d1c192e9..ce2345eda 100644 --- a/docs/config.ts +++ b/docs/config.ts @@ -85,6 +85,7 @@ const sidebar = { // '/welcome/release/index', // '/welcome/release/v08-changelog', '/welcome/release/v10-changelog', + '/welcome/release/v11-changelog', ], }, { @@ -260,7 +261,7 @@ const sidebar = { children: [ // '/api/client', '/api/client/application', - '/api/client/route-switch', + '/api/client/router', { title: 'SchemaDesigner', 'title.zh-CN': 'SchemaDesigner', diff --git a/docs/en-US/api/client/application.md b/docs/en-US/api/client/application.md index ae237311f..38387db90 100644 --- a/docs/en-US/api/client/application.md +++ b/docs/en-US/api/client/application.md @@ -32,7 +32,6 @@ Add Providers, build-in Providers are: - APIClientProvider - I18nextProvider - AntdConfigProvider -- RemoteRouteSwitchProvider - SystemSettingsProvider - PluginManagerProvider - SchemaComponentProvider diff --git a/docs/en-US/api/client/route-switch.md b/docs/en-US/api/client/route-switch.md deleted file mode 100644 index 8af0f744a..000000000 --- a/docs/en-US/api/client/route-switch.md +++ /dev/null @@ -1,74 +0,0 @@ -# RouteSwitch - -## `` - -```ts -interface RouteSwitchProviderProps { - components?: ReactComponent; - routes?: RouteRedirectProps[]; -} -``` - -## `` - -```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 = () =>

Home

; -const About = () =>

About

; - -const routes: RouteRedirectProps[] = [ - { - type: 'route', - path: '/', - component: 'Home', - }, - { - type: 'route', - path: '/about', - component: 'About', - }, -]; - -export default () => { - return ( - - - Home, About - - - - ); -}; -``` diff --git a/docs/en-US/api/client/router.md b/docs/en-US/api/client/router.md new file mode 100644 index 000000000..38436ea7c --- /dev/null +++ b/docs/en-US/api/client/router.md @@ -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
Hello
+} + +// first argument is `name` of route, second argument is `RouteObject` +app.router.add('root', { + path: '/', + element: +}) + +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
+ Home + about + + +
+} + +const Home = () => { + return
Home
+} + +const About = () => { + return
About
+} + +app.router.add('root', { + element: +}) +app.router.add('root.home', { + path: '/home', + element: +}) +app.router.add('root.about', { + path: '/about', + element: +}) +``` + +It will generate the following routes: + +```tsx | pure +{ + element: , + children: [ + { + path: '/home', + element: + }, + { + path: '/about', + element: + } + ] +} +``` + +### 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:
hello
, + }) + + // 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 = () =>

Home

; +const About = () =>

About

; + +const Layout = () => { + return
+
Home, About
+ +
+} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'] + } +}) + +app.router.add('root', { + element: +}) + +app.router.add('root.home', { + path: '/', + element: +}) + +app.router.add('root.about', { + path: '/about', + element: +}) + +export default app.getRootComponent(); +``` diff --git a/docs/en-US/development/client/index.md b/docs/en-US/development/client/index.md index 9f51ef40a..6080eff9a 100644 --- a/docs/en-US/development/client/index.md +++ b/docs/en-US/development/client/index.md @@ -7,7 +7,6 @@ Most of the extensions for the NocoBase client are provided as Providers. - APIClientProvider - I18nextProvider - AntdConfigProvider -- RemoteRouteSwitchProvider - SystemSettingsProvider - PluginManagerProvider - SchemaComponentProvider diff --git a/docs/en-US/development/client/ui-router.md b/docs/en-US/development/client/ui-router.md index bd27b1994..ace071fb2 100644 --- a/docs/en-US/development/client/ui-router.md +++ b/docs/en-US/development/client/ui-router.md @@ -1,62 +1,66 @@ # UI Routing -NocoBase Client's Router is based on [React Router](https://v5.reactrouter.com/web/guides/quick-start) and can be configured via `` to configure ui routes with the following example. +NocoBase Client's Router is based on [React Router](https://v5.reactrouter.com/web/guides/quick-start) and can be configured via `app.router` to configure ui routes with the following example. ```tsx /** * defaultShowCode: true */ import React from 'react'; -import { Link, MemoryRouter as Router } from 'react-router-dom'; -import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client'; +import { Link, Outlet } from 'react-router-dom'; +import { Application } from '@nocobase/client'; const Home = () =>

Home

; const About = () =>

About

; -const routes: RouteRedirectProps[] = [ - { - type: 'route', - path: '/', - component: 'Home', - }, - { - type: 'route', - path: '/about', - component: 'About', - }, -]; +const Layout = () => { + return
+
Home, About
+ +
+} -export default () => { - return ( - - - Home, About - - - - ); -}; +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'] + } +}) + +app.router.add('root', { + element: +}) + +app.router.add('root.home', { + path: '/', + element: +}) + +app.router.add('root.about', { + path: '/about', + element: +}) + +export default app.getRootComponent(); ``` In a full NocoBase application, the Route can be extended in a similar way as follows. ```tsx | pure -import { RouteSwitchContext } from '@nocobase/client'; -import React, { useContext } from 'react'; +import { Plugin } from '@nocobase/client'; -const HelloWorld = () => { - return
Hello ui router
; -}; +class MyPlugin extends Plugin { + async load() { + // add + this.app.router.add('hello', { + path: '/hello', + element:
hello
, + }) -export default React.memo((props) => { - const ctx = useContext(RouteSwitchContext); - ctx.routes.push({ - type: 'route', - path: '/hello-world', - component: HelloWorld, - }); - return {props.children}; -}); + // remove + this.app.router.remove('hello'); + } +} ``` See [packages/samples/custom-page](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-page) for the full example diff --git a/docs/en-US/welcome/release/v11-changelog.md b/docs/en-US/welcome/release/v11-changelog.md new file mode 100644 index 000000000..928faef47 --- /dev/null +++ b/docs/en-US/welcome/release/v11-changelog.md @@ -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
+ {props.children} +
; +} + +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
+ {props.children} +
; +} + ++ 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
+ + {props.children} + +
+} +``` + +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
+- + {props.children} +- +
+} + ++ 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). diff --git a/docs/tr-TR/api/client/application.md b/docs/tr-TR/api/client/application.md index a7f126b4a..7f8ac6e03 100644 --- a/docs/tr-TR/api/client/application.md +++ b/docs/tr-TR/api/client/application.md @@ -32,7 +32,6 @@ const app = new Application({ - APIClientProvider - I18nextProvider - AntdConfigProvider -- RemoteRouteSwitchProvider - SystemSettingsProvider - PluginManagerProvider - SchemaComponentProvider @@ -59,4 +58,4 @@ export const app = new Application({ }); export default app.render(); -``` \ No newline at end of file +``` diff --git a/docs/tr-TR/api/client/route-switch.md b/docs/tr-TR/api/client/route-switch.md deleted file mode 100644 index 6739d10a1..000000000 --- a/docs/tr-TR/api/client/route-switch.md +++ /dev/null @@ -1,74 +0,0 @@ -# RouteSwitch - -## `` - -```ts -interface RouteSwitchProviderProps { - components?: ReactComponent; - routes?: RouteRedirectProps[]; -} -``` - -## `` - -```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 = () =>

Home

; -const About = () =>

About

; - -const routes: RouteRedirectProps[] = [ - { - type: 'route', - path: '/', - component: 'Home', - }, - { - type: 'route', - path: '/about', - component: 'About', - }, -]; - -export default () => { - return ( - - - Home, About - - - - ); -}; -``` diff --git a/docs/tr-TR/api/client/router.md b/docs/tr-TR/api/client/router.md new file mode 100644 index 000000000..38436ea7c --- /dev/null +++ b/docs/tr-TR/api/client/router.md @@ -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
Hello
+} + +// first argument is `name` of route, second argument is `RouteObject` +app.router.add('root', { + path: '/', + element: +}) + +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
+ Home + about + + +
+} + +const Home = () => { + return
Home
+} + +const About = () => { + return
About
+} + +app.router.add('root', { + element: +}) +app.router.add('root.home', { + path: '/home', + element: +}) +app.router.add('root.about', { + path: '/about', + element: +}) +``` + +It will generate the following routes: + +```tsx | pure +{ + element: , + children: [ + { + path: '/home', + element: + }, + { + path: '/about', + element: + } + ] +} +``` + +### 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:
hello
, + }) + + // 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 = () =>

Home

; +const About = () =>

About

; + +const Layout = () => { + return
+
Home, About
+ +
+} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'] + } +}) + +app.router.add('root', { + element: +}) + +app.router.add('root.home', { + path: '/', + element: +}) + +app.router.add('root.about', { + path: '/about', + element: +}) + +export default app.getRootComponent(); +``` diff --git a/docs/tr-TR/development/client/index.md b/docs/tr-TR/development/client/index.md index 83983e138..ce958ea31 100644 --- a/docs/tr-TR/development/client/index.md +++ b/docs/tr-TR/development/client/index.md @@ -7,7 +7,6 @@ Most of the extensions for the NocoBase client are provided as Providers. - APIClientProvider - I18nextProvider - AntdConfigProvider -- RemoteRouteSwitchProvider - SystemSettingsProvider - PluginManagerProvider - SchemaComponentProvider diff --git a/docs/tr-TR/development/client/ui-router.md b/docs/tr-TR/development/client/ui-router.md index bd27b1994..ace071fb2 100644 --- a/docs/tr-TR/development/client/ui-router.md +++ b/docs/tr-TR/development/client/ui-router.md @@ -1,62 +1,66 @@ # UI Routing -NocoBase Client's Router is based on [React Router](https://v5.reactrouter.com/web/guides/quick-start) and can be configured via `` to configure ui routes with the following example. +NocoBase Client's Router is based on [React Router](https://v5.reactrouter.com/web/guides/quick-start) and can be configured via `app.router` to configure ui routes with the following example. ```tsx /** * defaultShowCode: true */ import React from 'react'; -import { Link, MemoryRouter as Router } from 'react-router-dom'; -import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client'; +import { Link, Outlet } from 'react-router-dom'; +import { Application } from '@nocobase/client'; const Home = () =>

Home

; const About = () =>

About

; -const routes: RouteRedirectProps[] = [ - { - type: 'route', - path: '/', - component: 'Home', - }, - { - type: 'route', - path: '/about', - component: 'About', - }, -]; +const Layout = () => { + return
+
Home, About
+ +
+} -export default () => { - return ( - - - Home, About - - - - ); -}; +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'] + } +}) + +app.router.add('root', { + element: +}) + +app.router.add('root.home', { + path: '/', + element: +}) + +app.router.add('root.about', { + path: '/about', + element: +}) + +export default app.getRootComponent(); ``` In a full NocoBase application, the Route can be extended in a similar way as follows. ```tsx | pure -import { RouteSwitchContext } from '@nocobase/client'; -import React, { useContext } from 'react'; +import { Plugin } from '@nocobase/client'; -const HelloWorld = () => { - return
Hello ui router
; -}; +class MyPlugin extends Plugin { + async load() { + // add + this.app.router.add('hello', { + path: '/hello', + element:
hello
, + }) -export default React.memo((props) => { - const ctx = useContext(RouteSwitchContext); - ctx.routes.push({ - type: 'route', - path: '/hello-world', - component: HelloWorld, - }); - return {props.children}; -}); + // remove + this.app.router.remove('hello'); + } +} ``` See [packages/samples/custom-page](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-page) for the full example diff --git a/docs/tr-TR/welcome/release/v11-changelog.md b/docs/tr-TR/welcome/release/v11-changelog.md new file mode 100644 index 000000000..928faef47 --- /dev/null +++ b/docs/tr-TR/welcome/release/v11-changelog.md @@ -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
+ {props.children} +
; +} + +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
+ {props.children} +
; +} + ++ 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
+ + {props.children} + +
+} +``` + +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
+- + {props.children} +- +
+} + ++ 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). diff --git a/docs/zh-CN/api/client/application.md b/docs/zh-CN/api/client/application.md index a7f126b4a..7f8ac6e03 100644 --- a/docs/zh-CN/api/client/application.md +++ b/docs/zh-CN/api/client/application.md @@ -32,7 +32,6 @@ const app = new Application({ - APIClientProvider - I18nextProvider - AntdConfigProvider -- RemoteRouteSwitchProvider - SystemSettingsProvider - PluginManagerProvider - SchemaComponentProvider @@ -59,4 +58,4 @@ export const app = new Application({ }); export default app.render(); -``` \ No newline at end of file +``` diff --git a/docs/zh-CN/api/client/route-switch.md b/docs/zh-CN/api/client/route-switch.md deleted file mode 100644 index 6739d10a1..000000000 --- a/docs/zh-CN/api/client/route-switch.md +++ /dev/null @@ -1,74 +0,0 @@ -# RouteSwitch - -## `` - -```ts -interface RouteSwitchProviderProps { - components?: ReactComponent; - routes?: RouteRedirectProps[]; -} -``` - -## `` - -```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 = () =>

Home

; -const About = () =>

About

; - -const routes: RouteRedirectProps[] = [ - { - type: 'route', - path: '/', - component: 'Home', - }, - { - type: 'route', - path: '/about', - component: 'About', - }, -]; - -export default () => { - return ( - - - Home, About - - - - ); -}; -``` diff --git a/docs/zh-CN/api/client/router.md b/docs/zh-CN/api/client/router.md new file mode 100644 index 000000000..14f2e4956 --- /dev/null +++ b/docs/zh-CN/api/client/router.md @@ -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
Hello
+} + +// 第一个参数是名称, 第二个参数是 `RouteObject` +app.router.add('root', { + path: '/', + element: +}) + +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
+ Home + about + + +
+} + +const Home = () => { + return
Home
+} + +const About = () => { + return
About
+} + +app.router.add('root', { + element: +}) +app.router.add('root.home', { + path: '/home', + element: +}) +app.router.add('root.about', { + path: '/about', + element: +}) +``` + +它将会被渲染为如下形式: + +```tsx | pure +{ + element: , + children: [ + { + path: '/home', + element: + }, + { + path: '/about', + element: + } + ] +} +``` + +### 删除路由 + +```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:
hello
, + }) + + // 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 = () =>

Home

; +const About = () =>

About

; + +const Layout = () => { + return
+
Home, About
+ +
+} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'] + } +}) + +app.router.add('root', { + element: +}) + +app.router.add('root.home', { + path: '/', + element: +}) + +app.router.add('root.about', { + path: '/about', + element: +}) + +export default app.getRootComponent(); +``` diff --git a/docs/zh-CN/development/client/index.md b/docs/zh-CN/development/client/index.md index d33ac8b86..c1e257850 100644 --- a/docs/zh-CN/development/client/index.md +++ b/docs/zh-CN/development/client/index.md @@ -7,7 +7,6 @@ NocoBase 客户端的扩展大多以 Provider 的形式提供。 - APIClientProvider - I18nextProvider - AntdConfigProvider -- RemoteRouteSwitchProvider - SystemSettingsProvider - PluginManagerProvider - SchemaComponentProvider diff --git a/docs/zh-CN/development/client/ui-router.md b/docs/zh-CN/development/client/ui-router.md index a4c40946e..b6ace0db3 100644 --- a/docs/zh-CN/development/client/ui-router.md +++ b/docs/zh-CN/development/client/ui-router.md @@ -1,62 +1,66 @@ # UI 路由 -NocoBase Client 的 Router 基于 [React Router](https://v5.reactrouter.com/web/guides/quick-start),可以通过 `` 来配置 ui routes,例子如下: +NocoBase Client 的 Router 基于 [React Router](https://v5.reactrouter.com/web/guides/quick-start),可以通过 `app.router` 来配置 ui routes,例子如下: ```tsx /** * defaultShowCode: true */ import React from 'react'; -import { Link, MemoryRouter as Router } from 'react-router-dom'; -import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client'; +import { Link, Outlet } from 'react-router-dom'; +import { Application } from '@nocobase/client'; const Home = () =>

Home

; const About = () =>

About

; -const routes: RouteRedirectProps[] = [ - { - type: 'route', - path: '/', - component: 'Home', - }, - { - type: 'route', - path: '/about', - component: 'About', - }, -]; +const Layout = () => { + return
+
Home, About
+ +
+} -export default () => { - return ( - - - Home, About - - - - ); -}; +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'] + } +}) + +app.router.add('root', { + element: +}) + +app.router.add('root.home', { + path: '/', + element: +}) + +app.router.add('root.about', { + path: '/about', + element: +}) + +export default app.getRootComponent(); ``` 在完整的 NocoBase 应用里,可以类似以下的的方式扩展 Route: ```tsx | pure -import { RouteSwitchContext } from '@nocobase/client'; -import React, { useContext } from 'react'; +import { Plugin } from '@nocobase/client'; -const HelloWorld = () => { - return
Hello ui router
; -}; +class MyPlugin extends Plugin { + async load() { + // 添加一条路由 + this.app.router.add('hello', { + path: '/hello', + element:
hello
, + }) -export default React.memo((props) => { - const ctx = useContext(RouteSwitchContext); - ctx.routes.push({ - type: 'route', - path: '/hello-world', - component: HelloWorld, - }); - return {props.children}; -}); + // 删除一条路由 + this.app.router.remove('hello'); + } +} ``` 完整示例查看 [packages/samples/custom-page](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-page) diff --git a/docs/zh-CN/welcome/release/v11-changelog.md b/docs/zh-CN/welcome/release/v11-changelog.md new file mode 100644 index 000000000..1eacd3e0d --- /dev/null +++ b/docs/zh-CN/welcome/release/v11-changelog.md @@ -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
+ {props.children} +
; +} + +export default HelloProvider +``` + +现在需要改为插件的方式,例如: + +```diff | pure ++import { Plugin } from '@nocobase/client' + +const HelloProvider = (props) => { + // do something logic + return
+ {props.children} +
; +} + ++ 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
+ + {props.children} + +
+} +``` + +需要改为: + +```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
+- + {props.children} +- +
+} + ++ 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` diff --git a/package.json b/package.json index 0e405358c..ec6667d5e 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ } }, "devDependencies": { + "commander": "^9.2.0", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "react": "^18.0.0", @@ -57,7 +58,7 @@ "@vitejs/plugin-react": "^4.0.0", "auto-changelog": "^2.4.0", "dumi": "^2.2.0", - "dumi-theme-nocobase": "^0.2.12", + "dumi-theme-nocobase": "^0.2.14", "ghooks": "^2.0.4", "jsdom-worker": "^0.3.0", "prettier": "^2.2.1", diff --git a/packages/app/client/.umirc.ts b/packages/app/client/.umirc.ts index 31708ed5b..2cecc1d2f 100644 --- a/packages/app/client/.umirc.ts +++ b/packages/app/client/.umirc.ts @@ -20,8 +20,7 @@ export default defineConfig({ { rel: 'stylesheet', href: '/global.css' }, ], headScripts: [ - '/browser-checker.js', - '/set-router.js', + '/browser-checker.js' ], hash: true, alias: { diff --git a/packages/app/client/public/set-router.js b/packages/app/client/public/set-router.js deleted file mode 100644 index 6e9c95382..000000000 --- a/packages/app/client/public/set-router.js +++ /dev/null @@ -1,2 +0,0 @@ -var match = location.pathname.match(/^\/apps\/([^/]*)\//); -window.routerBase = match ? match[0] : "/"; \ No newline at end of file diff --git a/packages/app/client/src/pages/index.tsx b/packages/app/client/src/pages/index.tsx index 56a28e6ca..98a45bf37 100644 --- a/packages/app/client/src/pages/index.tsx +++ b/packages/app/client/src/pages/index.tsx @@ -1,5 +1,6 @@ import '@/theme'; import { Application } from '@nocobase/client'; +import { NocoBaseClientPresetPlugin } from '@nocobase/preset-nocobase/client'; export const app = new Application({ apiClient: { @@ -8,6 +9,7 @@ export const app = new Application({ dynamicImport: (name: string) => { return import(`../plugins/${name}`); }, + plugins: [NocoBaseClientPresetPlugin], }); -export default app.render(); +export default app.getRootComponent(); diff --git a/packages/app/client/src/plugins/acl.ts b/packages/app/client/src/plugins/acl.ts new file mode 100644 index 000000000..a31541bb8 --- /dev/null +++ b/packages/app/client/src/plugins/acl.ts @@ -0,0 +1,2 @@ +export { default } from '@nocobase/plugin-acl/client'; + diff --git a/packages/app/client/src/plugins/client.ts b/packages/app/client/src/plugins/client.ts new file mode 100644 index 000000000..af9dc0350 --- /dev/null +++ b/packages/app/client/src/plugins/client.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-client/client'; diff --git a/packages/app/client/src/plugins/collection-manager.ts b/packages/app/client/src/plugins/collection-manager.ts new file mode 100644 index 000000000..c8315a7db --- /dev/null +++ b/packages/app/client/src/plugins/collection-manager.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-collection-manager/client'; diff --git a/packages/app/client/src/plugins/error-handler.ts b/packages/app/client/src/plugins/error-handler.ts new file mode 100644 index 000000000..25b0dffe7 --- /dev/null +++ b/packages/app/client/src/plugins/error-handler.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-error-handler/client'; diff --git a/packages/app/client/src/plugins/system-settings.ts b/packages/app/client/src/plugins/system-settings.ts new file mode 100644 index 000000000..60bc887b9 --- /dev/null +++ b/packages/app/client/src/plugins/system-settings.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-system-settings/client'; diff --git a/packages/app/client/src/plugins/ui-routes-storage.ts b/packages/app/client/src/plugins/ui-routes-storage.ts new file mode 100644 index 000000000..f885e0faa --- /dev/null +++ b/packages/app/client/src/plugins/ui-routes-storage.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-ui-routes-storage/client'; diff --git a/packages/app/client/src/plugins/ui-schema-storage.ts b/packages/app/client/src/plugins/ui-schema-storage.ts new file mode 100644 index 000000000..a0c77eec9 --- /dev/null +++ b/packages/app/client/src/plugins/ui-schema-storage.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-ui-schema-storage/client'; diff --git a/packages/core/build/src/build.ts b/packages/core/build/src/build.ts index 49b396758..b4ac84b6d 100755 --- a/packages/core/build/src/build.ts +++ b/packages/core/build/src/build.ts @@ -11,13 +11,24 @@ import randomColor from './randomColor'; import registerBabel from './registerBabel'; import rollup from './rollup'; import { Dispose, IBundleOptions, IBundleTypeOutput, ICjs, IEsm, IOpts } from './types'; -import { getExistFile, getLernaPackages } from './utils'; +import { getExistFiles, getLernaPackages } from './utils'; export function getBundleOpts(opts: IOpts): IBundleOptions[] { const { cwd, buildArgs = {}, rootConfig = {} } = opts; - const entry = getExistFile({ + const entry = getExistFiles({ cwd, - files: ['src/index.tsx', 'src/index.ts', 'src/index.jsx', 'src/index.js'], + files: [ + 'src/index.tsx', + 'src/index.ts', + 'src/index.jsx', + 'src/index.js', + 'src/server/index.ts', + 'src/server/index.js', + 'src/client/index.js', + 'src/client/index.ts', + 'src/client/index.tsx' + ], + onlyOne: false, returnRelative: true, }); const userConfig = getUserConfig({ cwd, customPath: buildArgs.config }); diff --git a/packages/core/build/src/getUserConfig.ts b/packages/core/build/src/getUserConfig.ts index 02cb05f7f..98aed9187 100755 --- a/packages/core/build/src/getUserConfig.ts +++ b/packages/core/build/src/getUserConfig.ts @@ -5,7 +5,7 @@ import signale from 'signale'; import slash from 'slash2'; import schema from './schema'; import { IBundleOptions } from './types'; -import { getExistFile } from './utils'; +import { getExistFiles } from './utils'; function testDefault(obj) { return obj.default || obj; @@ -51,7 +51,7 @@ export default function({ cwd, customPath }: { cwd: string; customPath?: string const configFile = finalPath || - getExistFile({ + getExistFiles({ cwd, files: CONFIG_FILES, returnRelative: false, diff --git a/packages/core/build/src/utils/index.ts b/packages/core/build/src/utils/index.ts index 4784d57c6..52a875006 100755 --- a/packages/core/build/src/utils/index.ts +++ b/packages/core/build/src/utils/index.ts @@ -1,13 +1,17 @@ import { existsSync } from 'fs'; import { join } from 'path'; -export function getExistFile({ cwd, files, returnRelative }) { +export function getExistFiles({ cwd, files, returnRelative, onlyOne = true }) { + const res = []; for (const file of files) { const absFilePath = join(cwd, file); if (existsSync(absFilePath)) { - return returnRelative ? file : absFilePath; + const filePath = returnRelative ? file : absFilePath; + res.push(filePath); } } + + return onlyOne ? res[0] : res; // undefined or string[] } export { getLernaPackages } from './getLernaPackages'; diff --git a/packages/core/cli/templates/plugin/client.d.ts b/packages/core/cli/templates/plugin/client.d.ts index bd53a2f77..d515d1feb 100755 --- a/packages/core/cli/templates/plugin/client.d.ts +++ b/packages/core/cli/templates/plugin/client.d.ts @@ -1,3 +1,3 @@ -// @ts-nocheck -export * from './lib/client'; -export { default } from './lib/client'; +export * from './src/client'; +export { default } from './src/client'; + diff --git a/packages/core/cli/templates/plugin/client.js b/packages/core/cli/templates/plugin/client.js index c83e7e450..3c39956ca 100755 --- a/packages/core/cli/templates/plugin/client.js +++ b/packages/core/cli/templates/plugin/client.js @@ -1,65 +1 @@ -'use strict'; - -function _getRequireWildcardCache(nodeInterop) { - if (typeof WeakMap !== 'function') return null; - var cacheBabelInterop = new WeakMap(); - var cacheNodeInterop = new WeakMap(); - return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { - return nodeInterop ? cacheNodeInterop : cacheBabelInterop; - })(nodeInterop); -} - -function _interopRequireWildcard(obj, nodeInterop) { - if (!nodeInterop && obj && obj.__esModule) { - return obj; - } - if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) { - return { default: obj }; - } - var cache = _getRequireWildcardCache(nodeInterop); - if (cache && cache.has(obj)) { - return cache.get(obj); - } - var newObj = {}; - var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; - for (var key in obj) { - if (key !== 'default' && Object.prototype.hasOwnProperty.call(obj, key)) { - var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; - if (desc && (desc.get || desc.set)) { - Object.defineProperty(newObj, key, desc); - } else { - newObj[key] = obj[key]; - } - } - } - newObj.default = obj; - if (cache) { - cache.set(obj, newObj); - } - return newObj; -} - -var _index = _interopRequireWildcard(require('./lib/client')); - -Object.defineProperty(exports, '__esModule', { - value: true, -}); -var _exportNames = {}; -Object.defineProperty(exports, 'default', { - enumerable: true, - get: function get() { - return _index.default; - }, -}); - -Object.keys(_index).forEach(function (key) { - if (key === 'default' || key === '__esModule') return; - if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; - if (key in exports && exports[key] === _index[key]) return; - Object.defineProperty(exports, key, { - enumerable: true, - get: function get() { - return _index[key]; - }, - }); -}); +module.exports = require('./lib/client/index.js'); diff --git a/packages/core/cli/templates/plugin/package.json.tpl b/packages/core/cli/templates/plugin/package.json.tpl index 2eb499b1f..aeb78e6f0 100644 --- a/packages/core/cli/templates/plugin/package.json.tpl +++ b/packages/core/cli/templates/plugin/package.json.tpl @@ -2,6 +2,17 @@ "name": "{{{packageName}}}", "version": "{{{packageVersion}}}", "main": "lib/server/index.js", + "files": [ + "lib", + "src", + "README.md", + "README.zh-CN.md", + "CHANGELOG.md", + "server.js", + "server.d.ts", + "client.js", + "client.d.ts" + ], "devDependencies": { "@nocobase/server": "{{{nocobaseVersion}}}", "@nocobase/test": "{{{nocobaseVersion}}}" diff --git a/packages/core/cli/templates/plugin/server.d.ts b/packages/core/cli/templates/plugin/server.d.ts index 4d922a91b..b55e6f70d 100755 --- a/packages/core/cli/templates/plugin/server.d.ts +++ b/packages/core/cli/templates/plugin/server.d.ts @@ -1,3 +1,3 @@ -// @ts-nocheck -export * from './lib/server'; -export { default } from './lib/server'; +export * from './src/server'; +export { default } from './src/server'; + diff --git a/packages/core/cli/templates/plugin/server.js b/packages/core/cli/templates/plugin/server.js index 4f16903a6..adcc9d90d 100755 --- a/packages/core/cli/templates/plugin/server.js +++ b/packages/core/cli/templates/plugin/server.js @@ -1,65 +1 @@ -'use strict'; - -function _getRequireWildcardCache(nodeInterop) { - if (typeof WeakMap !== 'function') return null; - var cacheBabelInterop = new WeakMap(); - var cacheNodeInterop = new WeakMap(); - return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { - return nodeInterop ? cacheNodeInterop : cacheBabelInterop; - })(nodeInterop); -} - -function _interopRequireWildcard(obj, nodeInterop) { - if (!nodeInterop && obj && obj.__esModule) { - return obj; - } - if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) { - return { default: obj }; - } - var cache = _getRequireWildcardCache(nodeInterop); - if (cache && cache.has(obj)) { - return cache.get(obj); - } - var newObj = {}; - var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; - for (var key in obj) { - if (key !== 'default' && Object.prototype.hasOwnProperty.call(obj, key)) { - var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; - if (desc && (desc.get || desc.set)) { - Object.defineProperty(newObj, key, desc); - } else { - newObj[key] = obj[key]; - } - } - } - newObj.default = obj; - if (cache) { - cache.set(obj, newObj); - } - return newObj; -} - -var _index = _interopRequireWildcard(require('./lib/server')); - -Object.defineProperty(exports, '__esModule', { - value: true, -}); -var _exportNames = {}; -Object.defineProperty(exports, 'default', { - enumerable: true, - get: function get() { - return _index.default; - }, -}); - -Object.keys(_index).forEach(function (key) { - if (key === 'default' || key === '__esModule') return; - if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; - if (key in exports && exports[key] === _index[key]) return; - Object.defineProperty(exports, key, { - enumerable: true, - get: function get() { - return _index[key]; - }, - }); -}); +module.exports = require('./lib/server/index.js'); diff --git a/packages/core/cli/templates/plugin/src/client/index.tsx b/packages/core/cli/templates/plugin/src/client/index.tsx deleted file mode 100644 index e99af76e1..000000000 --- a/packages/core/cli/templates/plugin/src/client/index.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; - -const MyProvider = React.memo((props) => { - return <>{props.children}; -}); -MyProvider.displayName = 'MyProvider'; - -export default MyProvider; diff --git a/packages/core/cli/templates/plugin/src/client/index.tsx.tpl b/packages/core/cli/templates/plugin/src/client/index.tsx.tpl new file mode 100644 index 000000000..3b509bc2d --- /dev/null +++ b/packages/core/cli/templates/plugin/src/client/index.tsx.tpl @@ -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; diff --git a/packages/core/client/docs/develop.md b/packages/core/client/docs/develop.md index 78c8cf4f7..72c1138e2 100644 --- a/packages/core/client/docs/develop.md +++ b/packages/core/client/docs/develop.md @@ -2,4 +2,4 @@ sidebar: false --- - + diff --git a/packages/core/client/docs/intro.md b/packages/core/client/docs/intro.md index 1951d0768..526b115ba 100644 --- a/packages/core/client/docs/intro.md +++ b/packages/core/client/docs/intro.md @@ -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
Hello NocoBase!
; - } - 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 = () =>

Home

; -const About = () =>

About

; - -const App = () => ( - - Home, About - - }> - }> - - -); - -export default App; -``` - -上述例子,组件经由路由转发,`/` 转发给 `Home`,`/about` 转发给 `About`。这种 JSX 的写法,对于熟悉 JSX 的开发来说,十分便捷,但需要开发来编写和维护,不符合 NocoBase 低代码、无代码的设计理念。所以将 Route 做了封装和配置化改造,如下: - ```tsx /** * defaultShowCode: true - * title: RouteSwitch */ import React from 'react'; -import { Link, MemoryRouter as Router } from 'react-router-dom'; -import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client'; +import { Link, Outlet } from 'react-router-dom'; +import { Application } from '@nocobase/client'; const Home = () =>

Home

; const About = () =>

About

; -const routes: RouteRedirectProps[] = [ - { - type: 'route', - path: '/', - component: 'Home', - }, - { - type: 'route', - path: '/about', - component: 'About', - }, -]; +const Layout = () => { + return
+
Home, About
+ +
+} -export default () => { - return ( - - - Home, About - - - - ); -}; +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'] + } +}) + +app.router.add('root', { + element: +}) + +app.router.add('root.home', { + path: '/', + element: +}) + +app.router.add('root.about', { + path: '/about', + element: +}) + +export default app.getRootComponent(); ``` -- 由 RouteSwitchProvider 配置 components,由开发编写,以 Layout 或 Template 的方式提供给 RouteSwitch 使用。 -- 由 RouteSwitch 配置 routes,JSON 的方式,可以由后端获取,方便后续的动态化、无代码的支持。 - ## SchemaComponent 路由可以通过 JSON 的方式配置,可以注册诸多可供路由使用的组件模板,以方便各种场景支持,但是这些组件还是需要开发编写和维护,所以进一步将组件抽象,转换成配置化的方式。如: @@ -459,101 +415,6 @@ export default function App() { } ``` -## RouteSwitch + SchemaComponent - -当路由和组件都可以配置之后,可以进一步将二者结合,例子如下: - -```tsx -/** - * defaultShowCode: true - * title: RouteSwitch + SchemaComponent - */ -import React, { useMemo, useEffect } from 'react'; -import { Link, MemoryRouter as Router } from 'react-router-dom'; -import { - RouteRedirectProps, - RouteSwitchProvider, - RouteSwitch, - useRoute, - SchemaComponentProvider, - SchemaComponent, - useDesignable, - useSchemaComponentContext, -} from '@nocobase/client'; -import { Spin, Button } from 'antd'; -import { observer, Schema } from '@formily/react'; - -const Hello = observer(({ name }) => { - const { patch, remove } = useDesignable(); - return ( -
-

Hello {name}!

- -
- ) -}, { displayName: 'Hello' }); - -const RouteSchemaComponent = (props) => { - const route = useRoute(); - const { reset } = useSchemaComponentContext(); - useEffect(() => { - reset(); - }, route.schema); - return -} - -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 ( - - - - Home, About - - - - - ); -}; -``` - -以上例子实现了路由和组件层面的配置化,在开发层面配置了两个组件: - -- `` 简易的可以在路由里配置 schema 的方案 -- `` 自定义的 Schema 组件 - -为了让大家更加能感受到 Schema 组件的不一样之处,例子添加了一个简易的随机更新 `x-component-props.name` 值的按钮,当路由切换后,更新后的 name 并不会被重置。这也是 Schema 组件的 Designable 的能力,可以任意的动态更新 schema 配置,实时更新,实时渲染。 - ## Designable SchemaComponent 基于 Formily 的 SchemaField,Formily 提供了 [Designable](https://github.com/alibaba/designable) 来解决 Schema 的配置问题,但是这套方案: @@ -781,7 +642,6 @@ const { data, loading } = useRequest(); 客户端的扩展以 Providers 的形式存在,提供各种可供组件使用的 Context,可全局也可以局部使用。上文我们已经介绍了核心的三个 Providers: -- RouteSwitchProvider,提供配置路由所需的 Layout 和 Template 组件 - SchemaComponentProvider,提供配置 Schema 所需的各种组件 - ApiClientProvider,提供客户端 SDK @@ -800,16 +660,14 @@ const { data, loading } = useRequest(); ```tsx | pure - {...} - ``` 但是这样的方式不利于 Providers 的管理和扩展,为此提炼了 `compose()` 函数用于配置多个 providers,如下: - + ## Application diff --git a/packages/core/client/package.json b/packages/core/client/package.json index 5441f679b..31965ff65 100644 --- a/packages/core/client/package.json +++ b/packages/core/client/package.json @@ -53,12 +53,12 @@ "react-is": ">=18.0.0" }, "devDependencies": { + "dumi": "^2.2.0", + "dumi-theme-nocobase": "^0.2.14", "@testing-library/react": "^12.1.2", "@types/markdown-it": "12.2.3", "@types/markdown-it-highlightjs": "3.3.1", - "axios-mock-adapter": "^1.20.0", - "dumi": "^2.2.0", - "dumi-theme-nocobase": "^0.2.9" + "axios-mock-adapter": "^1.20.0" }, "gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644" } diff --git a/packages/core/client/src/acl/ACLProvider.tsx b/packages/core/client/src/acl/ACLProvider.tsx index b41568f11..fc8486b2f 100644 --- a/packages/core/client/src/acl/ACLProvider.tsx +++ b/packages/core/client/src/acl/ACLProvider.tsx @@ -12,6 +12,7 @@ import { SchemaComponentOptions, useDesignable } from '../schema-component'; export const ACLContext = createContext({}); +// TODO: delete this,replace by `ACLPlugin` export const ACLProvider = (props) => { return ( ({}); @@ -50,3 +51,9 @@ export function AntdConfigProvider(props) { ); } + +export class AntdConfigPlugin extends Plugin { + async load() { + this.app.use(AntdConfigProvider, this.options?.config || {}); + } +} diff --git a/packages/core/client/src/application-v2/Application.tsx b/packages/core/client/src/application-v2/Application.tsx deleted file mode 100644 index 6dbfaafd3..000000000 --- a/packages/core/client/src/application-v2/Application.tsx +++ /dev/null @@ -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; - scopes: Record; - i18n: i18n; - apiClient: APIClient; - components: any; - pm: PluginManager; - - constructor(protected _options: ApplicationOptions) { - this.providers = []; - this.plugins = new Map(); - 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) { - 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 () => ; - } - - mount(selector: string) { - const container = typeof selector === 'string' ? document.querySelector(selector) : selector; - if (container) { - const App = this.getRootComponent(); - const root = createRoot(container); - root.render(); - } - } -} diff --git a/packages/core/client/src/application-v2/Plugin.ts b/packages/core/client/src/application-v2/Plugin.ts deleted file mode 100644 index 9abf92693..000000000 --- a/packages/core/client/src/application-v2/Plugin.ts +++ /dev/null @@ -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() {} -} diff --git a/packages/core/client/src/application-v2/PluginManager.ts b/packages/core/client/src/application-v2/PluginManager.ts deleted file mode 100644 index 32f6221d3..000000000 --- a/packages/core/client/src/application-v2/PluginManager.ts +++ /dev/null @@ -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; - protected pluginPrepares: Map; - - 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(); - } - } -} diff --git a/packages/core/client/src/application-v2/Router.ts b/packages/core/client/src/application-v2/Router.ts deleted file mode 100644 index 5886bd542..000000000 --- a/packages/core/client/src/application-v2/Router.ts +++ /dev/null @@ -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; - - constructor(protected options?: RouterOptions, protected context?: any) { - this.routes = new Map(); - 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(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); - } -} diff --git a/packages/core/client/src/application-v2/components/AppComponent.tsx b/packages/core/client/src/application-v2/components/AppComponent.tsx deleted file mode 100644 index 7b7de37aa..000000000 --- a/packages/core/client/src/application-v2/components/AppComponent.tsx +++ /dev/null @@ -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 ( - - - - ); -}; diff --git a/packages/core/client/src/application-v2/components/MainComponent.tsx b/packages/core/client/src/application-v2/components/MainComponent.tsx deleted file mode 100644 index ae9a046f0..000000000 --- a/packages/core/client/src/application-v2/components/MainComponent.tsx +++ /dev/null @@ -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 ; -}); diff --git a/packages/core/client/src/application-v2/components/RouterProvider.tsx b/packages/core/client/src/application-v2/components/RouterProvider.tsx deleted file mode 100644 index 06131985e..000000000 --- a/packages/core/client/src/application-v2/components/RouterProvider.tsx +++ /dev/null @@ -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/) - 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