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
+}
+
+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
+}
-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
+}
+
+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
+}
-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
+}
+
+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
+}
-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
+}
-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}!
- {
- patch('x-component-props.name', Math.random());
- }}
- >更新
-
- )
-}, { 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