refactor: plugin settings manager (#2712)
* feat: add settingsCenter * fix: style bug * chore: optimized code * refactor: settingCenter Auth * feat: add aclSnippet option * refactor: all plugin's setting center api * feat: add plugin with name * docs: add settings-center doc * fix: settings center menu sort by name * fix: change setting center layout * fix: change hello sort * test: add SettingsCenter.ts test case * fix: bug * fix: acl bug * fix: bug * fix: bug and 404 page * fix: test bug * fix: test bug * fix: bug * fix: locale * fix: styling * fix: rename settingsCenter to pluginSettingsManager * fix: styling * fix: e2e bug * fix: e2e bug * fix: locale * feat: update docs * fix: update --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
454db91827
commit
35b06cbfa0
10
README.md
10
README.md
@ -8,11 +8,11 @@ NocoBase is in early stage of development and is subject to frequent changes, pl
|
|||||||
|
|
||||||
## Recent major updates
|
## Recent major updates
|
||||||
|
|
||||||
- [v0.14: New plugin manager, supports adding plugins through UI - 2023/09/11](https://docs.nocobase.com/welcome/release/v14-changelog)
|
- [v0.15: New plugin settings manager - 2023/11/13](https://blog.nocobase.com/posts/release-v015/)
|
||||||
- [v0.13: New application status flow - 2023/08/24](https://docs.nocobase.com/welcome/release/v13-changelog)
|
- [v0.14: New plugin manager, supports adding plugins through UI - 2023/09/11](https://blog.nocobase.com/posts/release-v014/)
|
||||||
- [v0.12: New plugin build tool - 2023/08/01](https://docs.nocobase.com/welcome/release/v12-changelog)
|
- [v0.13: New application status flow - 2023/08/24](https://blog.nocobase.com/posts/release-v013/)
|
||||||
- [v0.11: New client application, plugin and router - 2023/07/08](http://docs.nocobase.com/welcome/release/v11-changelog)
|
- [v0.12: New plugin build tool - 2023/08/01](https://blog.nocobase.com/posts/release-v012/)
|
||||||
- [v0.10: Update instructions - 2023/06/23](http://docs.nocobase.com/welcome/release/v10-changelog)
|
- [v0.11: New client application, plugin and router - 2023/07/08](https://blog.nocobase.com/posts/release-v011/)
|
||||||
|
|
||||||
## What is NocoBase
|
## What is NocoBase
|
||||||
|
|
||||||
|
@ -8,11 +8,11 @@ NocoBase 正处在早期开发阶段,可能变动频繁,请谨慎用于生
|
|||||||
|
|
||||||
## 最近重要更新
|
## 最近重要更新
|
||||||
|
|
||||||
- [v0.14:全新的插件管理器,支持通过界面添加插件 - 2023/09/11](https://docs-cn.nocobase.com/welcome/release/v14-changelog)
|
- [v0.15:全新的插件设置中心 - 2023/11/13](https://blog-cn.nocobase.com/posts/release-v015/)
|
||||||
- [v0.13: 全新的应用状态流转 - 2023/08/24](https://docs-cn.nocobase.com/welcome/release/v13-changelog)
|
- [v0.14:全新的插件管理器,支持通过界面添加插件 - 2023/09/11](https://blog-cn.nocobase.com/posts/release-v014/)
|
||||||
- [v0.12: 全新的插件构建工具 - 2023/08/01](https://docs-cn.nocobase.com/welcome/release/v12-changelog)
|
- [v0.13: 全新的应用状态流转 - 2023/08/24](https://blog-cn.nocobase.com/posts/release-v013/)
|
||||||
- [v0.11: 全新的客户端 Application、Plugin 和 Router - 2023/07/08](https://docs-cn.nocobase.com/welcome/release/v11-changelog)
|
- [v0.12: 全新的插件构建工具 - 2023/08/01](https://blog-cn.nocobase.com/posts/release-v012/)
|
||||||
- [v0.10: Update instructions - 2023/06/23](https://docs-cn.nocobase.com/welcome/release/v10-changelog)
|
- [v0.11: 全新的客户端 Application、Plugin 和 Router - 2023/07/08](https://blog-cn.nocobase.com/posts/release-v011/)
|
||||||
|
|
||||||
## NocoBase 是什么
|
## NocoBase 是什么
|
||||||
|
|
||||||
|
@ -200,7 +200,7 @@ const sidebar = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
'/development/client/ui-router',
|
'/development/client/ui-router',
|
||||||
'/development/client/settings-center',
|
'/development/client/plugin-settings',
|
||||||
'/development/client/i18n',
|
'/development/client/i18n',
|
||||||
'/development/client/test',
|
'/development/client/test',
|
||||||
],
|
],
|
||||||
|
75
docs/en-US/development/client/plugin-settings.md
Normal file
75
docs/en-US/development/client/plugin-settings.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# Plugin Settings Manager
|
||||||
|
|
||||||
|
<img src="./plugin-settings/settings-tab.jpg" style="max-width: 100%;"/>
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```tsx | pure
|
||||||
|
import { Plugin } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const HelloSettingPage = () => <div>Hello Setting page</div>;
|
||||||
|
|
||||||
|
export class HelloPlugin extends Plugin {
|
||||||
|
async load() {
|
||||||
|
this.app.pluginSettingsManager.add('hello', {
|
||||||
|
title: 'Hello', // menu title and page title
|
||||||
|
icon: 'ApiOutlined', // menu icon
|
||||||
|
Component: HelloSettingPage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Level Routes
|
||||||
|
|
||||||
|
```tsx | pure
|
||||||
|
import { Outlet } from 'react-router-dom'
|
||||||
|
const SettingPageLayout = () => <div> <div>This</div> public part, the following is the outlet of the sub-route: <div><Outlet /></div></div>;
|
||||||
|
|
||||||
|
class HelloPlugin extends Plugin {
|
||||||
|
async load() {
|
||||||
|
this.app.pluginSettingsManager.add('hello', {
|
||||||
|
title: 'HelloWorld',
|
||||||
|
icon: '',
|
||||||
|
Component: SettingPageLayout
|
||||||
|
})
|
||||||
|
|
||||||
|
this.app.pluginSettingsManager.add('hello.demo1', {
|
||||||
|
title: 'Demo1 Page',
|
||||||
|
Component: () => <div>Demo1 Page Content</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
this.app.pluginSettingsManager.add('hello.demo2', {
|
||||||
|
title: 'Demo2 Page',
|
||||||
|
Component: () => <div>Demo2 Page Content</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Route Path
|
||||||
|
|
||||||
|
If you want to get the jump link of the setting page, you can get it through the `getRoutePath` method.
|
||||||
|
|
||||||
|
```tsx | pure
|
||||||
|
import { useApp } from '@nocobase/client'
|
||||||
|
|
||||||
|
const app = useApp();
|
||||||
|
app.pluginSettingsManager.getRoutePath('hello'); // /admin/settings/hello
|
||||||
|
app.pluginSettingsManager.getRoutePath('hello.demo1'); // /admin/settings/hello/demo1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Config
|
||||||
|
|
||||||
|
If you want to get the added configuration (already filtered by permissions), you can get it through the `get` method.
|
||||||
|
|
||||||
|
```tsx | pure
|
||||||
|
const app = useApp();
|
||||||
|
app.pluginSettingsManager.get('hello'); // { title: 'HelloWorld', icon: '', Component: HelloSettingPage, children: [{...}] }
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
See [samples/hello](https://github.com/nocobase/nocobase/blob/main/packages/plugins/%40nocobase/plugin-sample-hello/src/client/index.tsx) for full examples.
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
@ -1,33 +0,0 @@
|
|||||||
# Settings Center
|
|
||||||
|
|
||||||
<img src="./settings-center/settings-tab.jpg" style="max-width: 100%;"/>
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
```tsx | pure
|
|
||||||
import { SettingsCenterProvider } from '@nocobase/client';
|
|
||||||
import React, { useContext } from 'react';
|
|
||||||
|
|
||||||
const HelloTab => () => <div>Hello Tab</div>;
|
|
||||||
|
|
||||||
export default React.memo((props) => {
|
|
||||||
return (
|
|
||||||
<SettingsCenterProvider
|
|
||||||
settings={{
|
|
||||||
'sample-hello': {
|
|
||||||
title: 'Hello',
|
|
||||||
icon: 'ApiOutlined',
|
|
||||||
tabs: {
|
|
||||||
tab1: {
|
|
||||||
title: 'Hello Tab',
|
|
||||||
component: HelloTab,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>{props.children}</SettingsCenterProvider>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
See [samples/hello](https://github.com/nocobase/nocobase/tree/develop/packages/samples/hello) for full examples.
|
|
@ -18,7 +18,7 @@ No-code Users can manage the activation and deactivation of local plugins throug
|
|||||||
|
|
||||||
<img src="./index/pm-ui.jpg" style="max-width: 800px;" />
|
<img src="./index/pm-ui.jpg" style="max-width: 800px;" />
|
||||||
|
|
||||||
Developers can also manage the complete plugin process by way of the CLI:
|
Developers can also manage the complete plugin process by way of the CLI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create the plugin
|
# Create the plugin
|
||||||
@ -47,7 +47,7 @@ Whether it is generic functionality or personalization, it is recommended to wri
|
|||||||
Distribution of modules.
|
Distribution of modules.
|
||||||
|
|
||||||
- Server
|
- Server
|
||||||
- Collections & Fields: mainly used for system table configuration. Business tables are recommended to be configured in "Settings Center - Collection manager".
|
- Collections & Fields: mainly used for system table configuration. Business tables are recommended to be configured in "Plugin Settings Manager - Collection manager".
|
||||||
- Resources & Actions: Mainly used to extend the Action API
|
- Resources & Actions: Mainly used to extend the Action API
|
||||||
- Middleware: Middleware
|
- Middleware: Middleware
|
||||||
- Events: Events
|
- Events: Events
|
||||||
@ -57,5 +57,5 @@ Distribution of modules.
|
|||||||
- Client
|
- Client
|
||||||
- UI Schema Designer: Page Designer
|
- UI Schema Designer: Page Designer
|
||||||
- UI Router: When there is a need for custom pages
|
- UI Router: When there is a need for custom pages
|
||||||
- Settings Center: Provides configuration pages for plugins
|
- Plugin Settings Manager: Provides configuration pages for plugins
|
||||||
- I18n: Client side internationalization
|
- I18n: Client side internationalization
|
||||||
|
@ -76,7 +76,7 @@ yarn pm remove hello
|
|||||||
- Client
|
- Client
|
||||||
- UI Schema Designer:页面设计器
|
- UI Schema Designer:页面设计器
|
||||||
- UI Router:有自定义页面需求时
|
- UI Router:有自定义页面需求时
|
||||||
- Settings Center:为插件提供配置页面
|
- Plugin Settings Manager:为插件提供配置页面
|
||||||
- I18n:客户端国际化
|
- I18n:客户端国际化
|
||||||
- Devtools
|
- Devtools
|
||||||
- Commands:自定义命令行
|
- Commands:自定义命令行
|
||||||
@ -113,8 +113,6 @@ yarn pm remove hello
|
|||||||
- UI Router
|
- UI Router
|
||||||
- RouteSwitchProvider
|
- RouteSwitchProvider
|
||||||
- RouteSwitch
|
- RouteSwitch
|
||||||
- Settings Center
|
|
||||||
- SettingsCenterProvider
|
|
||||||
- I18n
|
- I18n
|
||||||
- app.i18n
|
- app.i18n
|
||||||
- useTranslation
|
- useTranslation
|
||||||
|
@ -6,7 +6,7 @@ Starting with v0.8, NocoBase begins to provide an available plugin manager and d
|
|||||||
|
|
||||||
- UI Editor
|
- UI Editor
|
||||||
- Plugin Manager
|
- Plugin Manager
|
||||||
- Settings Center
|
- Plugin Settings Manager
|
||||||
- Personal Center
|
- Personal Center
|
||||||
|
|
||||||
<img src="./v08-changelog/topright.jpg" style="max-width: 500px;" />
|
<img src="./v08-changelog/topright.jpg" style="max-width: 500px;" />
|
||||||
|
75
docs/zh-CN/development/client/plugin-settings.md
Normal file
75
docs/zh-CN/development/client/plugin-settings.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# 配置中心
|
||||||
|
|
||||||
|
<img src="./plugin-settings/settings-tab.jpg" style="max-width: 100%;"/>
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
### 基础用法
|
||||||
|
|
||||||
|
```tsx | pure
|
||||||
|
import { Plugin } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const HelloSettingPage = () => <div>Hello Setting page</div>;
|
||||||
|
|
||||||
|
export class HelloPlugin extends Plugin {
|
||||||
|
async load() {
|
||||||
|
this.app.pluginSettingsManager.add('hello', {
|
||||||
|
title: 'Hello', // 设置页面的标题和菜单名称
|
||||||
|
icon: 'ApiOutlined', // 设置页面菜单图标
|
||||||
|
Component: HelloSettingPage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 多层级路由
|
||||||
|
|
||||||
|
```tsx | pure
|
||||||
|
import { Outlet } from 'react-router-dom'
|
||||||
|
const SettingPageLayout = () => <div>公共部分,下面是子路由的出口: <div><Outlet /></div></div>;
|
||||||
|
|
||||||
|
class HelloPlugin extends Plugin {
|
||||||
|
async load() {
|
||||||
|
this.app.pluginSettingsManager.add('hello', {
|
||||||
|
title: 'HelloWorld', // 设置页面的标题和菜单名称
|
||||||
|
icon: '', // 菜单图标
|
||||||
|
Component: SettingPageLayout
|
||||||
|
})
|
||||||
|
|
||||||
|
this.app.pluginSettingsManager.add('hello.demo1', {
|
||||||
|
title: 'Demo1 Page',
|
||||||
|
Component: () => <div>Demo1 Page Content</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
this.app.pluginSettingsManager.add('hello.demo2', {
|
||||||
|
title: 'Demo2 Page',
|
||||||
|
Component: () => <div>Demo2 Page Content</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 获取路由路径
|
||||||
|
|
||||||
|
|
||||||
|
如果想获取设置页面的跳转链接,可以通过 `getRoutePath` 方法获取。
|
||||||
|
|
||||||
|
```tsx | pure
|
||||||
|
import { useApp } from '@nocobase/client'
|
||||||
|
|
||||||
|
const app = useApp();
|
||||||
|
app.pluginSettingsManager.getRoutePath('hello'); // /admin/settings/hello
|
||||||
|
app.pluginSettingsManager.getRoutePath('hello.demo1'); // /admin/settings/hello/demo1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 获取配置
|
||||||
|
|
||||||
|
如果想获取添加的配置(已进行权限过滤),可以通过 `get` 方法获取。
|
||||||
|
|
||||||
|
```tsx | pure
|
||||||
|
const app = useApp();
|
||||||
|
app.pluginSettingsManager.get('hello'); // { title: 'HelloWorld', icon: '', Component: HelloSettingPage, children: [{...}] }
|
||||||
|
```
|
||||||
|
|
||||||
|
完整示例查看 [samples/hello](https://github.com/nocobase/nocobase/blob/main/packages/plugins/%40nocobase/plugin-sample-hello/src/client/index.tsx)。
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
@ -1,33 +0,0 @@
|
|||||||
# 配置中心
|
|
||||||
|
|
||||||
<img src="./settings-center/settings-tab.jpg" style="max-width: 100%;"/>
|
|
||||||
|
|
||||||
## 示例
|
|
||||||
|
|
||||||
```tsx | pure
|
|
||||||
import { SettingsCenterProvider } from '@nocobase/client';
|
|
||||||
import React, { useContext } from 'react';
|
|
||||||
|
|
||||||
const HelloTab => () => <div>Hello Tab</div>;
|
|
||||||
|
|
||||||
export default React.memo((props) => {
|
|
||||||
return (
|
|
||||||
<SettingsCenterProvider
|
|
||||||
settings={{
|
|
||||||
'sample-hello': {
|
|
||||||
title: 'Hello',
|
|
||||||
icon: 'ApiOutlined',
|
|
||||||
tabs: {
|
|
||||||
tab1: {
|
|
||||||
title: 'Hello Tab',
|
|
||||||
component: HelloTab,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>{props.children}</SettingsCenterProvider>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
完整示例查看 [samples/hello](https://github.com/nocobase/nocobase/tree/develop/packages/samples/hello)。
|
|
@ -57,6 +57,6 @@ yarn pm remove hello
|
|||||||
- Client
|
- Client
|
||||||
- UI Schema Designer:页面设计器
|
- UI Schema Designer:页面设计器
|
||||||
- UI Router:有自定义页面需求时
|
- UI Router:有自定义页面需求时
|
||||||
- Settings Center:为插件提供配置页面
|
- Plugin Settings Manager:为插件提供配置页面
|
||||||
- I18n:客户端国际化
|
- I18n:客户端国际化
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ yarn pm remove hello
|
|||||||
- Client
|
- Client
|
||||||
- UI Schema Designer:页面设计器
|
- UI Schema Designer:页面设计器
|
||||||
- UI Router:有自定义页面需求时
|
- UI Router:有自定义页面需求时
|
||||||
- Settings Center:为插件提供配置页面
|
- Plugin Settings Manager:为插件提供配置页面
|
||||||
- I18n:客户端国际化
|
- I18n:客户端国际化
|
||||||
- Devtools
|
- Devtools
|
||||||
- Commands:自定义命令行
|
- Commands:自定义命令行
|
||||||
@ -113,8 +113,6 @@ yarn pm remove hello
|
|||||||
- UI Router
|
- UI Router
|
||||||
- RouteSwitchProvider
|
- RouteSwitchProvider
|
||||||
- RouteSwitch
|
- RouteSwitch
|
||||||
- Settings Center
|
|
||||||
- SettingsCenterProvider
|
|
||||||
- I18n
|
- I18n
|
||||||
- app.i18n
|
- app.i18n
|
||||||
- useTranslation
|
- useTranslation
|
||||||
|
@ -46,7 +46,7 @@ export default defineConfig({
|
|||||||
link: '/apis/api-client',
|
link: '/apis/api-client',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'SettingsCenter',
|
title: 'PluginSettingsManager',
|
||||||
link: '#',
|
link: '#',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -552,8 +552,8 @@ test.describe('blcok template', () => {
|
|||||||
await page.locator('.ant-drawer-mask').click();
|
await page.locator('.ant-drawer-mask').click();
|
||||||
|
|
||||||
//删除模板
|
//删除模板
|
||||||
await page.getByTestId('settings-center-button').click();
|
await page.getByTestId('plugin-settings-button').click();
|
||||||
await page.getByRole('button', { name: 'All plugin settings' }).click();
|
await page.getByLabel('ui-schema-storage').click();
|
||||||
await page.getByRole('menuitem', { name: 'layout Block templates' }).click();
|
await page.getByRole('menuitem', { name: 'layout Block templates' }).click();
|
||||||
await page.getByLabel('action-Action.Link-Delete-destroy-uiSchemaTemplates-table-Users_Form').click();
|
await page.getByLabel('action-Action.Link-Delete-destroy-uiSchemaTemplates-table-Users_Form').click();
|
||||||
await page.getByRole('button', { name: 'OK' }).click();
|
await page.getByRole('button', { name: 'OK' }).click();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { expect, test } from '@nocobase/test/client';
|
import { expect, test } from '@nocobase/test/client';
|
||||||
|
|
||||||
async function waitForModalToBeHidden(page) {
|
async function waitForModalToBeHidden(page) {
|
||||||
|
test.slow();
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const modal = document.querySelector('.ant-modal');
|
const modal = document.querySelector('.ant-modal');
|
||||||
if (modal) {
|
if (modal) {
|
||||||
@ -57,11 +58,12 @@ test.describe('remove plugin', () => {
|
|||||||
await page.getByPlaceholder('Search plugin').fill('Hello');
|
await page.getByPlaceholder('Search plugin').fill('Hello');
|
||||||
await expect(page.getByLabel('Hello')).toBeVisible();
|
await expect(page.getByLabel('Hello')).toBeVisible();
|
||||||
const isActive = await page.getByLabel('Hello').getByLabel('enable').isChecked();
|
const isActive = await page.getByLabel('Hello').getByLabel('enable').isChecked();
|
||||||
await expect(isActive).toBe(false);
|
expect(isActive).toBe(false);
|
||||||
//将hello插件remove
|
//将hello插件remove
|
||||||
await page.getByLabel('Hello').getByText('Remove').click();
|
await page.getByLabel('Hello').getByText('Remove').click();
|
||||||
await page.getByRole('button', { name: 'Yes' }).click();
|
await page.getByRole('button', { name: 'Yes' }).click();
|
||||||
//等待页面刷新结束
|
//等待页面刷新结束
|
||||||
|
await waitForModalToBeHidden(page);
|
||||||
await page.waitForLoadState('load');
|
await page.waitForLoadState('load');
|
||||||
await page.getByPlaceholder('Search plugin').fill('hello');
|
await page.getByPlaceholder('Search plugin').fill('hello');
|
||||||
await expect(page.getByLabel('Hello')).not.toBeVisible();
|
await expect(page.getByLabel('Hello')).not.toBeVisible();
|
||||||
@ -102,7 +104,7 @@ test.describe('enable & disabled plugin', () => {
|
|||||||
await expect(page.getByLabel('Hello')).toBeVisible();
|
await expect(page.getByLabel('Hello')).toBeVisible();
|
||||||
const isActive = await page.getByLabel('Hello').getByLabel('enable').isChecked();
|
const isActive = await page.getByLabel('Hello').getByLabel('enable').isChecked();
|
||||||
expect(isActive).toBe(false);
|
expect(isActive).toBe(false);
|
||||||
//激活插件
|
// 激活插件
|
||||||
await page.getByLabel('Hello').getByLabel('enable').click();
|
await page.getByLabel('Hello').getByLabel('enable').click();
|
||||||
await page.waitForTimeout(1000); // 等待1秒钟
|
await page.waitForTimeout(1000); // 等待1秒钟
|
||||||
//等待弹窗消失和页面刷新结束
|
//等待弹窗消失和页面刷新结束
|
||||||
|
@ -9,6 +9,7 @@ import { useCollection, useCollectionManager } from '../collection-manager';
|
|||||||
import { useResourceActionContext } from '../collection-manager/ResourceActionProvider';
|
import { useResourceActionContext } from '../collection-manager/ResourceActionProvider';
|
||||||
import { useRecord } from '../record-provider';
|
import { useRecord } from '../record-provider';
|
||||||
import { SchemaComponentOptions, useDesignable } from '../schema-component';
|
import { SchemaComponentOptions, useDesignable } from '../schema-component';
|
||||||
|
import { useApp } from '../application';
|
||||||
|
|
||||||
export const ACLContext = createContext<any>({});
|
export const ACLContext = createContext<any>({});
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ export const ACLRolesCheckProvider = (props) => {
|
|||||||
const { setDesignable } = useDesignable();
|
const { setDesignable } = useDesignable();
|
||||||
const { render } = useAppSpin();
|
const { render } = useAppSpin();
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
|
const app = useApp();
|
||||||
const result = useRequest<{
|
const result = useRequest<{
|
||||||
data: {
|
data: {
|
||||||
snippets: string[];
|
snippets: string[];
|
||||||
@ -57,6 +59,7 @@ export const ACLRolesCheckProvider = (props) => {
|
|||||||
if (data?.data?.role !== api.auth.role) {
|
if (data?.data?.role !== api.auth.role) {
|
||||||
api.auth.setRole(data?.data?.role);
|
api.auth.setRole(data?.data?.role);
|
||||||
}
|
}
|
||||||
|
app.pluginSettingsManager.setAclSnippets(data?.data?.snippets || []);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { Checkbox, message, Table } from 'antd';
|
import { Checkbox, message, Table } from 'antd';
|
||||||
import React, { createContext, useContext } from 'react';
|
import React, { createContext, useContext, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAPIClient, useRequest } from '../../api-client';
|
import { useAPIClient, useRequest } from '../../api-client';
|
||||||
import { SettingsCenterContext } from '../../pm';
|
import { SettingsCenterContext } from '../../pm';
|
||||||
import { useRecord } from '../../record-provider';
|
import { useRecord } from '../../record-provider';
|
||||||
import { useCompile } from '../../schema-component';
|
|
||||||
import { useStyles } from '../style';
|
import { useStyles } from '../style';
|
||||||
|
import { useApp } from '../../application';
|
||||||
|
import { useCompile } from '../../schema-component';
|
||||||
|
|
||||||
const getParentKeys = (tree, func, path = []) => {
|
const getParentKeys = (tree, func, path = []) => {
|
||||||
if (!tree) return [];
|
if (!tree) return [];
|
||||||
@ -22,7 +23,7 @@ const getParentKeys = (tree, func, path = []) => {
|
|||||||
};
|
};
|
||||||
const getChildrenKeys = (data = [], arr = []) => {
|
const getChildrenKeys = (data = [], arr = []) => {
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
arr.push(item.key);
|
arr.push(item.aclSnippet);
|
||||||
if (item.children && item.children.length) getChildrenKeys(item.children, arr);
|
if (item.children && item.children.length) getChildrenKeys(item.children, arr);
|
||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
@ -35,62 +36,40 @@ export const SettingCenterProvider = (props) => {
|
|||||||
return <SettingMenuContext.Provider value={configureItems}>{props.children}</SettingMenuContext.Provider>;
|
return <SettingMenuContext.Provider value={configureItems}>{props.children}</SettingMenuContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatPluginTabs = (data) => {
|
|
||||||
const tabs = [];
|
|
||||||
for (const key in data) {
|
|
||||||
const plugin = data?.[key];
|
|
||||||
for (const tabKey in plugin?.tabs || {}) {
|
|
||||||
const tab = plugin?.tabs[tabKey];
|
|
||||||
tabs.push({
|
|
||||||
pluginTitle: plugin.title,
|
|
||||||
...tab,
|
|
||||||
key: `pm.${key}.${tabKey}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tabs;
|
|
||||||
const arr: any[] = Object.entries(data);
|
|
||||||
const pluginsTabs = [];
|
|
||||||
console.log(tabs);
|
|
||||||
arr.forEach((v) => {
|
|
||||||
const children = Object.entries(v[1].tabs).map((k: any) => {
|
|
||||||
return {
|
|
||||||
key: 'pm.' + v[0] + '.' + k[0],
|
|
||||||
title: k[1].title,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
pluginsTabs.push({
|
|
||||||
title: v[1].title,
|
|
||||||
key: 'pm.' + v[0],
|
|
||||||
children,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return pluginsTabs;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SettingsCenterConfigure = () => {
|
export const SettingsCenterConfigure = () => {
|
||||||
|
const app = useApp();
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const pluginTags = useContext(SettingMenuContext);
|
|
||||||
const items: any[] = (pluginTags && formatPluginTabs(pluginTags)) || [];
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { loading, refresh, data } = useRequest<{
|
const settings = app.pluginSettingsManager.getList(false);
|
||||||
data: any;
|
const allAclSnippets = app.pluginSettingsManager.getAclSnippets();
|
||||||
}>({
|
const [snippets, setSnippets] = useState<string[]>([]);
|
||||||
resource: 'roles.snippets',
|
const allChecked = useMemo(
|
||||||
resourceOf: record.name,
|
() => snippets.includes('pm.*') && snippets.every((item) => !item.startsWith('!pm.')),
|
||||||
action: 'list',
|
[snippets],
|
||||||
params: {
|
);
|
||||||
paginate: false,
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { loading, refresh } = useRequest(
|
||||||
|
{
|
||||||
|
resource: 'roles.snippets',
|
||||||
|
resourceOf: record.name,
|
||||||
|
action: 'list',
|
||||||
|
params: {
|
||||||
|
paginate: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
onSuccess(data) {
|
||||||
|
setSnippets(data?.data || []);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
const resource = api.resource('roles.snippets', record.name);
|
const resource = api.resource('roles.snippets', record.name);
|
||||||
const handleChange = async (checked, record) => {
|
const handleChange = async (checked, record) => {
|
||||||
const childrenKeys = getChildrenKeys(record?.children, []);
|
const childrenKeys = getChildrenKeys(record?.children, []);
|
||||||
const totalKeys = childrenKeys.concat(record.key);
|
const totalKeys = childrenKeys.concat(record.aclSnippet);
|
||||||
if (!checked) {
|
if (!checked) {
|
||||||
await resource.remove({
|
await resource.remove({
|
||||||
values: totalKeys.map((v) => '!' + v),
|
values: totalKeys.map((v) => '!' + v),
|
||||||
@ -104,40 +83,54 @@ export const SettingsCenterConfigure = () => {
|
|||||||
}
|
}
|
||||||
message.success(t('Saved successfully'));
|
message.success(t('Saved successfully'));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
items?.length && (
|
<Table
|
||||||
<Table
|
className={styles}
|
||||||
className={styles}
|
loading={loading}
|
||||||
loading={loading}
|
rowKey={'key'}
|
||||||
rowKey={'key'}
|
pagination={false}
|
||||||
pagination={false}
|
expandable={{
|
||||||
columns={[
|
defaultExpandAllRows: true,
|
||||||
{
|
}}
|
||||||
dataIndex: 'title',
|
columns={[
|
||||||
title: t('Plugin tab name'),
|
{
|
||||||
render: (value) => {
|
dataIndex: 'title',
|
||||||
return compile(value);
|
title: t('Plugin name'),
|
||||||
},
|
render: (value) => {
|
||||||
|
return compile(value);
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
dataIndex: 'pluginTitle',
|
{
|
||||||
title: t('Plugin name'),
|
dataIndex: 'accessible',
|
||||||
render: (value) => {
|
title: (
|
||||||
return compile(value);
|
<>
|
||||||
},
|
<Checkbox
|
||||||
|
checked={allChecked}
|
||||||
|
onChange={async () => {
|
||||||
|
const values = allAclSnippets.map((v) => '!' + v);
|
||||||
|
if (!allChecked) {
|
||||||
|
await resource.remove({
|
||||||
|
values,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await resource.add({
|
||||||
|
values,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
message.success(t('Saved successfully'));
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
{t('Accessible')}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
render: (_, record) => {
|
||||||
|
const checked = !snippets.includes('!' + record.aclSnippet);
|
||||||
|
return <Checkbox checked={checked} onChange={() => handleChange(checked, record)} />;
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
dataIndex: 'accessible',
|
]}
|
||||||
title: t('Accessible'),
|
dataSource={settings}
|
||||||
render: (_, record) => {
|
/>
|
||||||
const checked = !data?.data?.includes('!' + record.key);
|
|
||||||
return !record.children && <Checkbox checked={checked} onChange={() => handleChange(checked, record)} />;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
dataSource={items}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -8,18 +8,22 @@ import React, { ComponentType, FC, ReactElement } from 'react';
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import { Link, NavLink, Navigate } from 'react-router-dom';
|
import { Link, NavLink, Navigate } from 'react-router-dom';
|
||||||
import { APIClient, APIClientProvider } from '../api-client';
|
|
||||||
import { i18n } from '../i18n';
|
|
||||||
import type { Plugin } from './Plugin';
|
|
||||||
import { PluginManager, PluginType } from './PluginManager';
|
import { PluginManager, PluginType } from './PluginManager';
|
||||||
import { ComponentTypeAndString, RouterManager, RouterOptions } from './RouterManager';
|
import { ComponentTypeAndString, RouterManager, RouterOptions } from './RouterManager';
|
||||||
import { WebSocketClient, WebSocketClientOptions } from './WebSocketClient';
|
import { WebSocketClient, WebSocketClientOptions } from './WebSocketClient';
|
||||||
|
import { PluginSettingsManager } from './PluginSettingsManager';
|
||||||
|
|
||||||
|
import { APIClient, APIClientProvider } from '../api-client';
|
||||||
|
import { i18n } from '../i18n';
|
||||||
import { AppComponent, BlankComponent, defaultAppComponents } from './components';
|
import { AppComponent, BlankComponent, defaultAppComponents } from './components';
|
||||||
import { compose, normalizeContainer } from './utils';
|
import { compose, normalizeContainer } from './utils';
|
||||||
import { defineGlobalDeps } from './utils/globalDeps';
|
import { defineGlobalDeps } from './utils/globalDeps';
|
||||||
import type { RequireJS } from './utils/requirejs';
|
|
||||||
import { getRequireJs } from './utils/requirejs';
|
import { getRequireJs } from './utils/requirejs';
|
||||||
|
|
||||||
|
import type { RequireJS } from './utils/requirejs';
|
||||||
|
import type { Plugin } from './Plugin';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
define: RequireJS['define'];
|
define: RequireJS['define'];
|
||||||
@ -50,6 +54,7 @@ export class Application {
|
|||||||
public apiClient: APIClient;
|
public apiClient: APIClient;
|
||||||
public components: Record<string, ComponentType> = { ...defaultAppComponents };
|
public components: Record<string, ComponentType> = { ...defaultAppComponents };
|
||||||
public pm: PluginManager;
|
public pm: PluginManager;
|
||||||
|
public pluginSettingsManager: PluginSettingsManager;
|
||||||
public devDynamicImport: DevDynamicImport;
|
public devDynamicImport: DevDynamicImport;
|
||||||
public requirejs: RequireJS;
|
public requirejs: RequireJS;
|
||||||
public notification;
|
public notification;
|
||||||
@ -57,6 +62,9 @@ export class Application {
|
|||||||
maintained = false;
|
maintained = false;
|
||||||
maintaining = false;
|
maintaining = false;
|
||||||
error = null;
|
error = null;
|
||||||
|
get pluginManager() {
|
||||||
|
return this.pm;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(protected options: ApplicationOptions = {}) {
|
constructor(protected options: ApplicationOptions = {}) {
|
||||||
this.initRequireJs();
|
this.initRequireJs();
|
||||||
@ -81,6 +89,8 @@ export class Application {
|
|||||||
this.addReactRouterComponents();
|
this.addReactRouterComponents();
|
||||||
this.addProviders(options.providers || []);
|
this.addProviders(options.providers || []);
|
||||||
this.ws = new WebSocketClient(options.ws);
|
this.ws = new WebSocketClient(options.ws);
|
||||||
|
this.pluginSettingsManager = new PluginSettingsManager(this);
|
||||||
|
this.addRoutes();
|
||||||
}
|
}
|
||||||
|
|
||||||
private initRequireJs() {
|
private initRequireJs() {
|
||||||
@ -102,6 +112,13 @@ export class Application {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addRoutes() {
|
||||||
|
this.router.add('not-found', {
|
||||||
|
path: '*',
|
||||||
|
Component: this.components['AppNotFound'] || BlankComponent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getComposeProviders() {
|
getComposeProviders() {
|
||||||
const Providers = compose(...this.providers)(BlankComponent);
|
const Providers = compose(...this.providers)(BlankComponent);
|
||||||
Providers.displayName = 'Providers';
|
Providers.displayName = 'Providers';
|
||||||
|
141
packages/core/client/src/application/PluginSettingsManager.ts
Normal file
141
packages/core/client/src/application/PluginSettingsManager.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { set } from 'lodash';
|
||||||
|
import { createElement } from 'react';
|
||||||
|
|
||||||
|
import { Icon } from '../icon';
|
||||||
|
import type { Application } from './Application';
|
||||||
|
import type { RouteType } from './RouterManager';
|
||||||
|
|
||||||
|
export const ADMIN_SETTINGS_KEY = 'admin.settings.';
|
||||||
|
export const ADMIN_SETTINGS_PATH = '/admin/settings/';
|
||||||
|
export const SNIPPET_PREFIX = 'pm.';
|
||||||
|
|
||||||
|
export interface PluginSettingsManagerSettingOptionsType {
|
||||||
|
title: string;
|
||||||
|
Component: RouteType['Component'];
|
||||||
|
icon?: string;
|
||||||
|
/**
|
||||||
|
* sort, the smaller the number, the higher the priority
|
||||||
|
* @default 0
|
||||||
|
*/
|
||||||
|
sort?: number;
|
||||||
|
isBookmark?: boolean;
|
||||||
|
aclSnippet?: string;
|
||||||
|
[index: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginSettingsPageType {
|
||||||
|
label?: string;
|
||||||
|
title: string;
|
||||||
|
key: string;
|
||||||
|
icon: any;
|
||||||
|
path: string;
|
||||||
|
sort?: number;
|
||||||
|
name?: string;
|
||||||
|
pluginName?: string;
|
||||||
|
isBookmark?: boolean;
|
||||||
|
children?: PluginSettingsPageType[];
|
||||||
|
[index: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PluginSettingsManager {
|
||||||
|
protected settings: Record<string, PluginSettingsManagerSettingOptionsType> = {};
|
||||||
|
protected aclSnippets: string[] = [];
|
||||||
|
|
||||||
|
constructor(protected app: Application) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAclSnippets(aclSnippets: string[]) {
|
||||||
|
this.aclSnippets = aclSnippets;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAclSnippet(name: string) {
|
||||||
|
const setting = this.settings[name];
|
||||||
|
return setting?.aclSnippet ? setting.aclSnippet : `${SNIPPET_PREFIX}${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRouteName(name: string) {
|
||||||
|
return `${ADMIN_SETTINGS_KEY}${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoutePath(name: string) {
|
||||||
|
return `${ADMIN_SETTINGS_PATH}${name.replaceAll('.', '/')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(name: string, options: PluginSettingsManagerSettingOptionsType) {
|
||||||
|
const nameArr = name.split('.');
|
||||||
|
const pluginName = nameArr[0];
|
||||||
|
this.settings[name] = { ...options, name, pluginName };
|
||||||
|
|
||||||
|
// add children
|
||||||
|
if (nameArr.length > 1) {
|
||||||
|
set(this.settings, nameArr.join('.children.'), this.settings[name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add route
|
||||||
|
this.app.router.add(this.getRouteName(name), {
|
||||||
|
path: this.getRoutePath(name),
|
||||||
|
Component: options.Component,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(name: string) {
|
||||||
|
// delete self and children
|
||||||
|
Object.keys(this.settings).forEach((key) => {
|
||||||
|
if (key.startsWith(name)) {
|
||||||
|
delete this.settings[key];
|
||||||
|
this.app.router.remove(`${ADMIN_SETTINGS_KEY}${key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAuth(name: string) {
|
||||||
|
return this.aclSnippets.includes(`!${this.getAclSnippet(name)}`) === false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSetting(name: string) {
|
||||||
|
return this.settings[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
has(name: string) {
|
||||||
|
const hasAuth = this.hasAuth(name);
|
||||||
|
if (!hasAuth) return false;
|
||||||
|
return !!this.getSetting(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(name: string, filterAuth = true): PluginSettingsPageType {
|
||||||
|
const isAllow = this.hasAuth(name);
|
||||||
|
const pluginSetting = this.getSetting(name);
|
||||||
|
if ((filterAuth && !isAllow) || !pluginSetting) return null;
|
||||||
|
const children = Object.keys(pluginSetting.children || {})
|
||||||
|
.sort((a, b) => a.localeCompare(b)) // sort by name
|
||||||
|
.map((key) => this.get(pluginSetting.children[key].name, filterAuth))
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((a, b) => (a.sort || 0) - (b.sort || 0));
|
||||||
|
const { title, icon, aclSnippet, ...others } = pluginSetting;
|
||||||
|
return {
|
||||||
|
...others,
|
||||||
|
aclSnippet: this.getAclSnippet(name),
|
||||||
|
title,
|
||||||
|
isAllow,
|
||||||
|
label: title,
|
||||||
|
icon: typeof icon === 'string' ? createElement(Icon, { type: icon }) : icon,
|
||||||
|
path: this.getRoutePath(name),
|
||||||
|
key: name,
|
||||||
|
children: children.length ? children : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getList(filterAuth = true): PluginSettingsPageType[] {
|
||||||
|
return Object.keys(this.settings)
|
||||||
|
.filter((item) => !item.includes('.')) // top level
|
||||||
|
.sort((a, b) => a.localeCompare(b)) // sort by name
|
||||||
|
.map((name) => this.get(name, filterAuth))
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((a, b) => (a.sort || 0) - (b.sort || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
getAclSnippets() {
|
||||||
|
return Object.keys(this.settings).map((name) => this.getAclSnippet(name));
|
||||||
|
}
|
||||||
|
}
|
@ -16,7 +16,7 @@ describe('Application', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const router: any = { type: 'memory', initialEntries: ['/'] };
|
const router: any = { type: 'memory', initialEntries: ['/'] };
|
||||||
const initialComponentsLength = 6;
|
const initialComponentsLength = 7;
|
||||||
const initialProvidersLength = 2;
|
const initialProvidersLength = 2;
|
||||||
it('basic', () => {
|
it('basic', () => {
|
||||||
const app = new Application({ router });
|
const app = new Application({ router });
|
||||||
|
@ -0,0 +1,142 @@
|
|||||||
|
import { Application } from '../Application';
|
||||||
|
import axios from 'axios';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
|
||||||
|
describe('PluginSettingsManager', () => {
|
||||||
|
let app: Application;
|
||||||
|
|
||||||
|
const test = {
|
||||||
|
title: 'test title',
|
||||||
|
Component: () => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const test1 = {
|
||||||
|
title: 'test1 title',
|
||||||
|
Component: () => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const test2 = {
|
||||||
|
title: 'test2 title',
|
||||||
|
Component: () => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
const mock = new MockAdapter(axios);
|
||||||
|
mock.onGet('pm:listEnabled').reply(200, {
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
app = new Application({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('basic use', () => {
|
||||||
|
const name = 'test';
|
||||||
|
|
||||||
|
app.pluginSettingsManager.add(name, test);
|
||||||
|
|
||||||
|
const settingRes = { ...test, name };
|
||||||
|
const getRes = {
|
||||||
|
...test,
|
||||||
|
name,
|
||||||
|
label: test.title,
|
||||||
|
path: '/admin/settings/test',
|
||||||
|
isAllow: true,
|
||||||
|
aclSnippet: 'pm.test',
|
||||||
|
key: name,
|
||||||
|
children: undefined,
|
||||||
|
};
|
||||||
|
expect(app.pluginSettingsManager.getSetting('test')).toContain(settingRes);
|
||||||
|
expect(app.pluginSettingsManager.get('test')).toContain(getRes);
|
||||||
|
expect(app.pluginSettingsManager.hasAuth('test')).toBeTruthy();
|
||||||
|
const list = app.pluginSettingsManager.getList();
|
||||||
|
expect(list.length).toBe(1);
|
||||||
|
expect(list[0]).toContain(getRes);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multi', () => {
|
||||||
|
app.pluginSettingsManager.add('test1', test1);
|
||||||
|
app.pluginSettingsManager.add('test2', test2);
|
||||||
|
expect(app.pluginSettingsManager.get('test1')).toContain(test1);
|
||||||
|
expect(app.pluginSettingsManager.get('test2')).toContain(test2);
|
||||||
|
|
||||||
|
const list = app.pluginSettingsManager.getList();
|
||||||
|
expect(list.length).toBe(2);
|
||||||
|
expect(list[0]).toContain(test1);
|
||||||
|
expect(list[1]).toContain(test2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nested', () => {
|
||||||
|
app.pluginSettingsManager.add('test1', test1);
|
||||||
|
app.pluginSettingsManager.add('test1.test2', test2);
|
||||||
|
expect(app.pluginSettingsManager.get('test1')).toContain(test1);
|
||||||
|
expect(app.pluginSettingsManager.get('test1.test2')).toContain(test2);
|
||||||
|
expect(app.pluginSettingsManager.get('test1').children.length).toBe(1);
|
||||||
|
expect(app.pluginSettingsManager.get('test1').children[0]).toContain(test2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('remove', () => {
|
||||||
|
app.pluginSettingsManager.add('test1', test1);
|
||||||
|
app.pluginSettingsManager.add('test1.test2', test2);
|
||||||
|
|
||||||
|
app.pluginSettingsManager.remove('test1');
|
||||||
|
expect(app.pluginSettingsManager.get('test1')).toBeFalsy();
|
||||||
|
expect(app.pluginSettingsManager.get('test1.test2')).toBeFalsy();
|
||||||
|
expect(app.pluginSettingsManager.getList().length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('acl', () => {
|
||||||
|
app.pluginSettingsManager.setAclSnippets(['!pm.test']);
|
||||||
|
app.pluginSettingsManager.add('test', test);
|
||||||
|
expect(app.pluginSettingsManager.get('test')).toBeFalsy();
|
||||||
|
expect(app.pluginSettingsManager.hasAuth('test')).toBeFalsy();
|
||||||
|
expect(app.pluginSettingsManager.get('test', false)).toContain({ ...test, isAllow: false });
|
||||||
|
|
||||||
|
expect(app.pluginSettingsManager.getList().length).toBe(0);
|
||||||
|
expect(app.pluginSettingsManager.getList(false).length).toBe(1);
|
||||||
|
expect(app.pluginSettingsManager.getList(false)[0]).toContain({ ...test, isAllow: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has', () => {
|
||||||
|
app.pluginSettingsManager.add('test', test);
|
||||||
|
expect(app.pluginSettingsManager.has('test')).toBeTruthy();
|
||||||
|
expect(app.pluginSettingsManager.has('test1')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getAclSnippet', () => {
|
||||||
|
app.pluginSettingsManager.add('test1', test1);
|
||||||
|
app.pluginSettingsManager.add('test2', {
|
||||||
|
...test2,
|
||||||
|
aclSnippet: 'any.string',
|
||||||
|
});
|
||||||
|
expect(app.pluginSettingsManager.getAclSnippet('test1')).toBe('pm.test1');
|
||||||
|
expect(app.pluginSettingsManager.getAclSnippet('test2')).toBe('any.string');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getRouteName', () => {
|
||||||
|
app.pluginSettingsManager.add('test1', test1);
|
||||||
|
app.pluginSettingsManager.add('test1.test2', test2);
|
||||||
|
expect(app.pluginSettingsManager.getRouteName('test1')).toBe('admin.settings.test1');
|
||||||
|
expect(app.pluginSettingsManager.getRouteName('test1.test2')).toBe('admin.settings.test1.test2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getRoutePath', () => {
|
||||||
|
app.pluginSettingsManager.add('test1', test1);
|
||||||
|
app.pluginSettingsManager.add('test1.test2', test2);
|
||||||
|
expect(app.pluginSettingsManager.getRoutePath('test1')).toBe('/admin/settings/test1');
|
||||||
|
expect(app.pluginSettingsManager.getRoutePath('test1.test2')).toBe('/admin/settings/test1/test2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('router', () => {
|
||||||
|
app.pluginSettingsManager.add('test1', test1);
|
||||||
|
app.pluginSettingsManager.add('test1.test2', test2);
|
||||||
|
expect(app.router.getRoutes()[0]).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"children": undefined,
|
||||||
|
"element": <AppNotFound />,
|
||||||
|
"path": "*",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
@ -9,8 +9,11 @@ const AppError: FC<{ error: Error }> = ({ error }) => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const AppNotFound: FC = () => <div>Not Found</div>;
|
||||||
|
|
||||||
export const defaultAppComponents = {
|
export const defaultAppComponents = {
|
||||||
AppMain: MainComponent,
|
AppMain: MainComponent,
|
||||||
AppSpin: Loading,
|
AppSpin: Loading,
|
||||||
AppError: AppError,
|
AppError: AppError,
|
||||||
|
AppNotFound: AppNotFound,
|
||||||
};
|
};
|
||||||
|
@ -27,6 +27,7 @@ Application 提供了强大的功能,包括:
|
|||||||
- scopes 管理
|
- scopes 管理
|
||||||
- providers 管理
|
- providers 管理
|
||||||
- 插件管理
|
- 插件管理
|
||||||
|
- 插件设置页面管理
|
||||||
|
|
||||||
### 组件管理
|
### 组件管理
|
||||||
|
|
||||||
@ -370,6 +371,76 @@ class MyPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 插件设置页面管理
|
||||||
|
|
||||||
|
#### 基础用法
|
||||||
|
|
||||||
|
```tsx | pure
|
||||||
|
import { Plugin } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const HelloSettingPage = () => <div>Hello Setting page</div>;
|
||||||
|
|
||||||
|
export class HelloPlugin extends Plugin {
|
||||||
|
async load() {
|
||||||
|
this.app.pluginSettingsManager.add('hello', {
|
||||||
|
title: 'Hello', // 设置页面的标题和菜单名称
|
||||||
|
icon: 'ApiOutlined', // 设置页面菜单图标
|
||||||
|
Component: HelloSettingPage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 多层级路由
|
||||||
|
|
||||||
|
```tsx | pure
|
||||||
|
import { Outlet } from 'react-router-dom'
|
||||||
|
const SettingPageLayout = () => <div>公共部分,下面是子路由的出口: <div><Outlet /></div></div>;
|
||||||
|
|
||||||
|
class HelloPlugin extends Plugin {
|
||||||
|
async load() {
|
||||||
|
this.app.pluginSettingsManager.add('hello', {
|
||||||
|
title: 'HelloWorld', // 设置页面的标题和菜单名称
|
||||||
|
icon: '', // 菜单图标
|
||||||
|
Component: SettingPageLayout
|
||||||
|
})
|
||||||
|
|
||||||
|
this.app.pluginSettingsManager.add('hello.demo1', {
|
||||||
|
title: 'Demo1 Page',
|
||||||
|
Component: () => <div>Demo1 Page Content</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
this.app.pluginSettingsManager.add('hello.demo2', {
|
||||||
|
title: 'Demo2 Page',
|
||||||
|
Component: () => <div>Demo2 Page Content</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 获取路由路径
|
||||||
|
|
||||||
|
|
||||||
|
如果想获取设置页面的跳转链接,可以通过 `getRoutePath` 方法获取。
|
||||||
|
|
||||||
|
```tsx | pure
|
||||||
|
import { useApp } from '@nocobase/client'
|
||||||
|
|
||||||
|
const app = useApp();
|
||||||
|
app.pluginSettingsManager.getRoutePath('hello'); // /admin/settings/hello
|
||||||
|
app.pluginSettingsManager.getRoutePath('hello.demo1'); // /admin/settings/hello/demo1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 获取配置
|
||||||
|
|
||||||
|
如果想获取添加的配置(已进行权限过滤),可以通过 `get` 方法获取。
|
||||||
|
|
||||||
|
```tsx | pure
|
||||||
|
const app = useApp();
|
||||||
|
app.pluginSettingsManager.get('hello'); // { title: 'HelloWorld', icon: '', Component: HelloSettingPage, children: [{...}] }
|
||||||
|
```
|
||||||
|
|
||||||
### 渲染
|
### 渲染
|
||||||
|
|
||||||
#### Root Component
|
#### Root Component
|
||||||
|
@ -3,3 +3,4 @@ export * from './hooks';
|
|||||||
export * from './Plugin';
|
export * from './Plugin';
|
||||||
export * from './RouterManager';
|
export * from './RouterManager';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
export * from './PluginSettingsManager';
|
||||||
|
@ -38,6 +38,7 @@ export default {
|
|||||||
"Unconnected": "Unconnected",
|
"Unconnected": "Unconnected",
|
||||||
"System settings": "System settings",
|
"System settings": "System settings",
|
||||||
"System title": "System title",
|
"System title": "System title",
|
||||||
|
"Settings": "Settings",
|
||||||
"Logo": "Logo",
|
"Logo": "Logo",
|
||||||
"Add menu item": "Add menu item",
|
"Add menu item": "Add menu item",
|
||||||
"Page": "Page",
|
"Page": "Page",
|
||||||
|
@ -42,6 +42,7 @@ export default {
|
|||||||
'System settings': '系统设置',
|
'System settings': '系统设置',
|
||||||
'System title': '系统名称',
|
'System title': '系统名称',
|
||||||
Setting: '设置',
|
Setting: '设置',
|
||||||
|
Settings: '设置',
|
||||||
Enable: '启用',
|
Enable: '启用',
|
||||||
Disable: '禁用',
|
Disable: '禁用',
|
||||||
On: '启用',
|
On: '启用',
|
||||||
|
@ -3,7 +3,7 @@ import { css } from '@emotion/css';
|
|||||||
import { observer } from '@formily/reactive-react';
|
import { observer } from '@formily/reactive-react';
|
||||||
import { Button, Modal, Result, Spin } from 'antd';
|
import { Button, Modal, Result, Spin } from 'antd';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate, useNavigate } from 'react-router-dom';
|
||||||
import { ACLPlugin } from '../acl';
|
import { ACLPlugin } from '../acl';
|
||||||
import { Application } from '../application';
|
import { Application } from '../application';
|
||||||
import { Plugin } from '../application/Plugin';
|
import { Plugin } from '../application/Plugin';
|
||||||
@ -188,6 +188,22 @@ const AppMaintainingDialog: FC<{ app: Application; error: Error }> = observer(({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const AppNotFound = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<Result
|
||||||
|
status="404"
|
||||||
|
title="404"
|
||||||
|
subTitle="Sorry, the page you visited does not exist."
|
||||||
|
extra={
|
||||||
|
<Button onClick={() => navigate('/', { replace: true })} type="primary">
|
||||||
|
Back Home
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export class NocoBaseBuildInPlugin extends Plugin {
|
export class NocoBaseBuildInPlugin extends Plugin {
|
||||||
async afterAdd() {
|
async afterAdd() {
|
||||||
this.app.addComponents({
|
this.app.addComponents({
|
||||||
@ -195,6 +211,7 @@ export class NocoBaseBuildInPlugin extends Plugin {
|
|||||||
AppError,
|
AppError,
|
||||||
AppMaintaining,
|
AppMaintaining,
|
||||||
AppMaintainingDialog,
|
AppMaintainingDialog,
|
||||||
|
AppNotFound,
|
||||||
});
|
});
|
||||||
await this.addPlugins();
|
await this.addPlugins();
|
||||||
}
|
}
|
||||||
@ -216,6 +233,11 @@ export class NocoBaseBuildInPlugin extends Plugin {
|
|||||||
element: <Navigate replace to="/admin" />,
|
element: <Navigate replace to="/admin" />,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.router.add('not-found', {
|
||||||
|
path: '*',
|
||||||
|
Component: AppNotFound,
|
||||||
|
});
|
||||||
|
|
||||||
this.router.add('admin', {
|
this.router.add('admin', {
|
||||||
path: '/admin',
|
path: '/admin',
|
||||||
Component: 'AdminLayout',
|
Component: 'AdminLayout',
|
||||||
|
@ -7,6 +7,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { DeleteOutlined, ReadOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons';
|
import { DeleteOutlined, ReadOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { useAPIClient } from '../api-client';
|
import { useAPIClient } from '../api-client';
|
||||||
|
import { useApp } from '../application';
|
||||||
import { PluginDetail } from './PluginDetail';
|
import { PluginDetail } from './PluginDetail';
|
||||||
import { PluginUpgradeModal } from './PluginForm/modal/PluginUpgradeModal';
|
import { PluginUpgradeModal } from './PluginForm/modal/PluginUpgradeModal';
|
||||||
import { useStyles } from './style';
|
import { useStyles } from './style';
|
||||||
@ -18,6 +19,7 @@ interface IPluginInfo extends IPluginCard {
|
|||||||
|
|
||||||
function PluginInfo(props: IPluginInfo) {
|
function PluginInfo(props: IPluginInfo) {
|
||||||
const { data, onClick } = props;
|
const { data, onClick } = props;
|
||||||
|
const app = useApp();
|
||||||
const { name, displayName, isCompatible, packageName, updatable, builtIn, enabled, description, type, error } = data;
|
const { name, displayName, isCompatible, packageName, updatable, builtIn, enabled, description, type, error } = data;
|
||||||
const { styles, theme } = useStyles();
|
const { styles, theme } = useStyles();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -28,7 +30,6 @@ function PluginInfo(props: IPluginInfo) {
|
|||||||
const [enabledVal, setEnabledVal] = useState(enabled);
|
const [enabledVal, setEnabledVal] = useState(enabled);
|
||||||
const reload = () => window.location.reload();
|
const reload = () => window.location.reload();
|
||||||
const title = displayName || name || packageName;
|
const title = displayName || name || packageName;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showUploadForm && (
|
{showUploadForm && (
|
||||||
@ -92,14 +93,16 @@ function PluginInfo(props: IPluginInfo) {
|
|||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{enabled ? (
|
{enabled ? (
|
||||||
<a
|
app.pluginSettingsManager.has(name) && (
|
||||||
onClick={(e) => {
|
<a
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
navigate(`/admin/settings/${name}`);
|
e.stopPropagation();
|
||||||
}}
|
navigate(app.pluginSettingsManager.getRoutePath(name));
|
||||||
>
|
}}
|
||||||
<SettingOutlined /> {t('Setting')}
|
>
|
||||||
</a>
|
<SettingOutlined /> {t('Settings')}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
key={'delete'}
|
key={'delete'}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { ApiOutlined, SettingOutlined } from '@ant-design/icons';
|
import { ApiOutlined, SettingOutlined } from '@ant-design/icons';
|
||||||
import { Button, Dropdown, MenuProps, Tooltip } from 'antd';
|
import { css } from '@emotion/css';
|
||||||
import _ from 'lodash';
|
import { Button, Card, Popover, Tooltip } from 'antd';
|
||||||
import React, { useContext, useMemo, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useACLRoleContext } from '../acl/ACLProvider';
|
import { useApp } from '../application';
|
||||||
import { ActionContextProvider, useCompile } from '../schema-component';
|
import { ActionContextProvider, useCompile } from '../schema-component';
|
||||||
import { useToken } from '../style';
|
import { useToken } from '../style';
|
||||||
import { SettingsCenterContext, getPluginsTabs } from './index';
|
|
||||||
|
|
||||||
export const PluginManagerLink = () => {
|
export const PluginManagerLink = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -27,56 +26,85 @@ export const PluginManagerLink = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBookmarkTabs = _.memoize((data) => {
|
|
||||||
const bookmarkTabs = [];
|
|
||||||
data.forEach((plugin) => {
|
|
||||||
const tabs = plugin.tabs;
|
|
||||||
tabs.forEach((tab) => {
|
|
||||||
tab.isBookmark && tab.isAllow && bookmarkTabs.push({ ...tab, path: `${plugin.key}/${tab.key}` });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return bookmarkTabs;
|
|
||||||
});
|
|
||||||
export const SettingsCenterDropdown = () => {
|
export const SettingsCenterDropdown = () => {
|
||||||
const { snippets = [] } = useACLRoleContext();
|
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const { t } = useTranslation();
|
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const navigate = useNavigate();
|
const { t } = useTranslation();
|
||||||
const itemData = useContext(SettingsCenterContext);
|
|
||||||
const { token } = useToken();
|
const { token } = useToken();
|
||||||
const pluginsTabs = getPluginsTabs(itemData, snippets);
|
const navigate = useNavigate();
|
||||||
const bookmarkTabs = getBookmarkTabs(pluginsTabs);
|
const app = useApp();
|
||||||
const menu = useMemo<MenuProps>(() => {
|
const settings = app.pluginSettingsManager.getList();
|
||||||
return {
|
const [open, setOpen] = useState(false);
|
||||||
items: [
|
|
||||||
...bookmarkTabs.map((tab) => ({
|
|
||||||
role: 'button',
|
|
||||||
key: `/admin/settings/${tab.path}`,
|
|
||||||
label: compile(tab.title),
|
|
||||||
})),
|
|
||||||
{ type: 'divider' },
|
|
||||||
{
|
|
||||||
role: 'button',
|
|
||||||
key: '/admin/settings',
|
|
||||||
label: t('All plugin settings'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onClick({ key }) {
|
|
||||||
navigate(key);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [bookmarkTabs]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionContextProvider value={{ visible, setVisible }}>
|
<ActionContextProvider value={{ visible, setVisible }}>
|
||||||
<Dropdown placement="bottom" menu={menu}>
|
<Popover
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setOpen(open);
|
||||||
|
}}
|
||||||
|
arrow={false}
|
||||||
|
content={
|
||||||
|
<div style={{ maxWidth: '21rem' }}>
|
||||||
|
<Card
|
||||||
|
bordered={false}
|
||||||
|
className={css`
|
||||||
|
box-shadow: none;
|
||||||
|
`}
|
||||||
|
style={{ boxShadow: 'none' }}
|
||||||
|
>
|
||||||
|
{settings.map((setting) => (
|
||||||
|
<Card.Grid
|
||||||
|
className={css`
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
&:hover {
|
||||||
|
border-radius: ${token.borderRadius}px;
|
||||||
|
background: rgba(0, 0, 0, 0.045);
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
key={setting.pluginName}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
aria-label={setting.name}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen(false);
|
||||||
|
navigate(setting.path);
|
||||||
|
}}
|
||||||
|
title={compile(setting.title)}
|
||||||
|
style={{ display: 'block', color: 'inherit', padding: token.marginSM }}
|
||||||
|
href={setting.path}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '1.2rem', textAlign: 'center', marginBottom: '0.3rem' }}>
|
||||||
|
{setting.icon || <SettingOutlined />}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
fontSize: token.fontSizeSM,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{compile(setting.title)}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Card.Grid>
|
||||||
|
))}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
data-testid="settings-center-button"
|
data-testid="plugin-settings-button"
|
||||||
icon={<SettingOutlined style={{ color: token.colorTextHeaderMenu }} />}
|
icon={<SettingOutlined style={{ color: token.colorTextHeaderMenu }} />}
|
||||||
// title={t('All plugin settings')}
|
// title={t('All plugin settings')}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Popover>
|
||||||
</ActionContextProvider>
|
</ActionContextProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,135 +1,97 @@
|
|||||||
export * from './PluginManagerLink';
|
|
||||||
import { PageHeader } from '@ant-design/pro-layout';
|
import { PageHeader } from '@ant-design/pro-layout';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { Layout, Menu, Result, Tabs } from 'antd';
|
import { Layout, Menu, Result } from 'antd';
|
||||||
import _, { sortBy } from 'lodash';
|
import _, { get } from 'lodash';
|
||||||
import React, { createContext, useContext, useMemo } from 'react';
|
import React, { createContext, useCallback, useMemo } from 'react';
|
||||||
import { Navigate, useNavigate, useParams } from 'react-router-dom';
|
import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useACLRoleContext } from '../acl/ACLProvider';
|
|
||||||
import { ACLPane } from '../acl/ACLShortcut';
|
|
||||||
import { CollectionManagerPane } from '../collection-manager';
|
|
||||||
import { Icon } from '../icon';
|
|
||||||
import { useCompile } from '../schema-component';
|
|
||||||
import { BlockTemplatesPane } from '../schema-templates';
|
|
||||||
import { SystemSettingsPane } from '../system-settings';
|
|
||||||
import { useStyles } from './style';
|
import { useStyles } from './style';
|
||||||
|
import { ADMIN_SETTINGS_PATH, PluginSettingsPageType, useApp } from '../application';
|
||||||
|
import { useCompile } from '../schema-component';
|
||||||
|
|
||||||
export const SettingsCenterContext = createContext<any>({});
|
export const SettingsCenterContext = createContext<any>({});
|
||||||
|
|
||||||
export const settings = {
|
function getMenuItems(list: PluginSettingsPageType[]) {
|
||||||
acl: {
|
return list.map((item) => {
|
||||||
title: '{{t("ACL")}}',
|
|
||||||
icon: 'LockOutlined',
|
|
||||||
tabs: {
|
|
||||||
roles: {
|
|
||||||
isBookmark: true,
|
|
||||||
title: '{{t("Roles & Permissions")}}',
|
|
||||||
component: () => <ACLPane />,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'ui-schema-storage': {
|
|
||||||
title: '{{t("Block templates")}}',
|
|
||||||
icon: 'LayoutOutlined',
|
|
||||||
tabs: {
|
|
||||||
'block-templates': {
|
|
||||||
title: '{{t("Block templates")}}',
|
|
||||||
component: BlockTemplatesPane,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'collection-manager': {
|
|
||||||
icon: 'DatabaseOutlined',
|
|
||||||
title: '{{t("Collection manager")}}',
|
|
||||||
tabs: {
|
|
||||||
collections: {
|
|
||||||
isBookmark: true,
|
|
||||||
title: '{{t("Collections & Fields")}}',
|
|
||||||
component: CollectionManagerPane,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'system-settings': {
|
|
||||||
icon: 'SettingOutlined',
|
|
||||||
title: '{{t("System settings")}}',
|
|
||||||
tabs: {
|
|
||||||
'system-settings': {
|
|
||||||
isBookmark: true,
|
|
||||||
title: '{{t("System settings")}}',
|
|
||||||
component: SystemSettingsPane,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPluginsTabs = _.memoize((items, snippets) => {
|
|
||||||
const pluginsTabs = Object.keys(items).map((plugin) => {
|
|
||||||
const tabsObj = items[plugin].tabs;
|
|
||||||
const tabs = sortBy(
|
|
||||||
Object.keys(tabsObj).map((tab) => {
|
|
||||||
return {
|
|
||||||
key: tab,
|
|
||||||
...tabsObj[tab],
|
|
||||||
isAllow: snippets.includes('pm.*') && !snippets?.includes(`!pm.${plugin}.${tab}`),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
(o) => !o.isAllow,
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
...items[plugin],
|
key: item.name,
|
||||||
key: plugin,
|
label: item.label,
|
||||||
tabs,
|
title: item.title,
|
||||||
isAllow: !tabs.every((v) => !v.isAllow),
|
icon: item.icon,
|
||||||
|
children: item.children?.length ? getMenuItems(item.children) : undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return sortBy(pluginsTabs, (o) => !o.isAllow);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
export const SettingsCenter = () => {
|
export const SettingsCenterComponent = () => {
|
||||||
const { styles } = useStyles();
|
const { styles, theme } = useStyles();
|
||||||
const { snippets = [] } = useACLRoleContext();
|
const app = useApp();
|
||||||
const params = useParams<any>();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const items = useContext(SettingsCenterContext);
|
const location = useLocation();
|
||||||
const pluginsTabs = getPluginsTabs(items, snippets);
|
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const firstUri = useMemo(() => {
|
const settings = useMemo(() => {
|
||||||
const pluginName = pluginsTabs[0].key;
|
const list = app.pluginSettingsManager.getList();
|
||||||
const tabName = pluginsTabs[0].tabs[0].key;
|
// compile title
|
||||||
return `/admin/settings/${pluginName}/${tabName}`;
|
function traverse(settings: PluginSettingsPageType[]) {
|
||||||
}, [pluginsTabs]);
|
settings.forEach((item) => {
|
||||||
const { pluginName, tabName } = params;
|
item.title = compile(item.title);
|
||||||
const activePlugin = pluginsTabs.find((v) => v.key === pluginName);
|
item.label = compile(item.title);
|
||||||
const aclPluginTabCheck = activePlugin?.isAllow && activePlugin.tabs.find((v) => v.key === tabName)?.isAllow;
|
if (item.children?.length) {
|
||||||
if (!pluginName) {
|
traverse(item.children);
|
||||||
return <Navigate replace to={firstUri} />;
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
traverse(list);
|
||||||
|
return list;
|
||||||
|
}, [app.pluginSettingsManager, compile]);
|
||||||
|
const getFirstDeepChildPath = useCallback((settings: PluginSettingsPageType[]) => {
|
||||||
|
if (!settings || !settings.length) {
|
||||||
|
return '/admin';
|
||||||
|
}
|
||||||
|
const first = settings[0];
|
||||||
|
if (first.children?.length) {
|
||||||
|
return getFirstDeepChildPath(first.children);
|
||||||
|
}
|
||||||
|
return first.path;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const settingsMapByPath = useMemo<Record<string, PluginSettingsPageType>>(() => {
|
||||||
|
const map = {};
|
||||||
|
const traverse = (settings: PluginSettingsPageType[]) => {
|
||||||
|
settings.forEach((item) => {
|
||||||
|
map[item.path] = item;
|
||||||
|
if (item.children?.length) {
|
||||||
|
traverse(item.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
traverse(settings);
|
||||||
|
return map;
|
||||||
|
}, [settings]);
|
||||||
|
|
||||||
|
const currentSetting = useMemo(() => settingsMapByPath[location.pathname], [location.pathname, settingsMapByPath]);
|
||||||
|
const currentPlugin = useMemo(() => {
|
||||||
|
if (!currentSetting) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return settings.find((item) => item.name === currentSetting.pluginName);
|
||||||
|
}, [currentSetting, settings]);
|
||||||
|
|
||||||
|
const sidebarMenus = useMemo(() => {
|
||||||
|
return getMenuItems(settings.map((item) => ({ ...item, children: null })));
|
||||||
|
}, [settings]);
|
||||||
|
|
||||||
|
if (!currentSetting || location.pathname === ADMIN_SETTINGS_PATH || location.pathname === ADMIN_SETTINGS_PATH + '/') {
|
||||||
|
return <Navigate replace to={getFirstDeepChildPath(settings)} />;
|
||||||
}
|
}
|
||||||
if (!items[pluginName]) {
|
if (location.pathname === currentPlugin.path && currentPlugin.children?.length > 0) {
|
||||||
return <Navigate replace to={firstUri} />;
|
return <Navigate replace to={getFirstDeepChildPath(currentPlugin.children)} />;
|
||||||
}
|
}
|
||||||
if (!tabName) {
|
|
||||||
const firstTabName = Object.keys(items[pluginName]?.tabs).shift();
|
|
||||||
return <Navigate replace to={`/admin/settings/${pluginName}/${firstTabName}`} />;
|
|
||||||
}
|
|
||||||
const component = items[pluginName]?.tabs?.[tabName]?.component;
|
|
||||||
const plugin: any = pluginsTabs.find((v) => v.key === pluginName);
|
|
||||||
const menuItems: any = pluginsTabs
|
|
||||||
.filter((plugin) => plugin.isAllow)
|
|
||||||
.map((plugin) => {
|
|
||||||
return {
|
|
||||||
label: compile(plugin.title),
|
|
||||||
key: plugin.key,
|
|
||||||
icon: plugin.icon ? <Icon type={plugin.icon} /> : null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Layout.Sider
|
<Layout.Sider
|
||||||
className={css`
|
className={css`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
/* position: fixed;
|
|
||||||
padding-top: 46px; */
|
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
background: rgba(0, 0, 0, 0);
|
background: rgba(0, 0, 0, 0);
|
||||||
@ -144,44 +106,46 @@ export const SettingsCenter = () => {
|
|||||||
theme={'light'}
|
theme={'light'}
|
||||||
>
|
>
|
||||||
<Menu
|
<Menu
|
||||||
selectedKeys={[pluginName]}
|
selectedKeys={[currentSetting?.pluginName]}
|
||||||
style={{ height: 'calc(100vh - 46px)', overflowY: 'auto', overflowX: 'hidden' }}
|
style={{ height: 'calc(100vh - 46px)', overflowY: 'auto', overflowX: 'hidden' }}
|
||||||
onClick={(e) => {
|
onClick={({ key }) => {
|
||||||
const item = items[e.key];
|
const plugin = settings.find((item) => item.name === key);
|
||||||
const tabKey = Object.keys(item.tabs).shift();
|
if (plugin.children?.length) {
|
||||||
navigate(`/admin/settings/${e.key}/${tabKey}`);
|
return navigate(getFirstDeepChildPath(plugin.children));
|
||||||
|
} else {
|
||||||
|
return navigate(plugin.path);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
items={menuItems as any}
|
items={sidebarMenus}
|
||||||
/>
|
/>
|
||||||
</Layout.Sider>
|
</Layout.Sider>
|
||||||
<Layout.Content>
|
<Layout.Content>
|
||||||
{aclPluginTabCheck && (
|
{currentSetting && (
|
||||||
<PageHeader
|
<PageHeader
|
||||||
className={styles.pageHeader}
|
className={styles.pageHeader}
|
||||||
|
style={{
|
||||||
|
paddingBottom: currentPlugin.children?.length > 0 ? 0 : theme.paddingSM,
|
||||||
|
}}
|
||||||
ghost={false}
|
ghost={false}
|
||||||
title={compile(items[pluginName]?.title)}
|
title={currentPlugin.title}
|
||||||
footer={
|
footer={
|
||||||
<Tabs
|
currentPlugin.children?.length > 0 && (
|
||||||
activeKey={tabName}
|
<Menu
|
||||||
onChange={(activeKey) => {
|
style={{ marginLeft: -theme.margin }}
|
||||||
navigate(`/admin/settings/${pluginName}/${activeKey}`);
|
onClick={({ key }) => {
|
||||||
}}
|
navigate(app.pluginSettingsManager.getRoutePath(key));
|
||||||
items={plugin.tabs?.map((tab) => {
|
}}
|
||||||
if (!tab.isAllow) {
|
selectedKeys={[currentSetting?.name]}
|
||||||
return null;
|
mode="horizontal"
|
||||||
}
|
items={getMenuItems(currentPlugin.children)}
|
||||||
return {
|
></Menu>
|
||||||
label: compile(tab?.title),
|
)
|
||||||
key: tab.key,
|
|
||||||
};
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className={styles.pageContent}>
|
<div className={styles.pageContent}>
|
||||||
{aclPluginTabCheck ? (
|
{currentSetting ? (
|
||||||
component && React.createElement(component)
|
<Outlet />
|
||||||
) : (
|
) : (
|
||||||
<Result status="404" title="404" subTitle="Sorry, the page you visited does not exist." />
|
<Result status="404" title="404" subTitle="Sorry, the page you visited does not exist." />
|
||||||
)}
|
)}
|
||||||
@ -191,15 +155,3 @@ export const SettingsCenter = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsCenterProvider = (props) => {
|
|
||||||
const { settings = {} } = props;
|
|
||||||
const items = useContext(SettingsCenterContext);
|
|
||||||
return (
|
|
||||||
<SettingsCenterContext.Provider value={{ ...items, ...settings }}>{props.children}</SettingsCenterContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PMProvider = (props) => {
|
|
||||||
return <SettingsCenterProvider settings={settings}>{props.children}</SettingsCenterProvider>;
|
|
||||||
};
|
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Plugin } from '../application/Plugin';
|
import { Plugin } from '../application/Plugin';
|
||||||
import { PluginManagerLink, SettingsCenterDropdown } from './PluginManagerLink';
|
import { PluginManagerLink, SettingsCenterDropdown } from './PluginManagerLink';
|
||||||
import { PMProvider, SettingsCenter } from './PluginSetting';
|
import { SettingsCenterComponent } from './PluginSetting';
|
||||||
import { PluginManager } from './PluginManager';
|
import { PluginManager } from './PluginManager';
|
||||||
|
import { ACLPane } from '../acl/ACLShortcut';
|
||||||
|
import { CollectionManagerPane } from '../collection-manager';
|
||||||
|
import { BlockTemplatesPane } from '../schema-templates';
|
||||||
|
import { SystemSettingsPane } from '../system-settings';
|
||||||
|
import { ADMIN_SETTINGS_PATH } from '../application';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
export * from './PluginManagerLink';
|
export * from './PluginManagerLink';
|
||||||
export * from './PluginSetting';
|
export * from './PluginSetting';
|
||||||
@ -12,7 +18,43 @@ export class PMPlugin extends Plugin {
|
|||||||
async load() {
|
async load() {
|
||||||
this.addComponents();
|
this.addComponents();
|
||||||
this.addRoutes();
|
this.addRoutes();
|
||||||
this.app.use(PMProvider);
|
this.addSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
addSettings() {
|
||||||
|
this.app.pluginSettingsManager.add('acl', {
|
||||||
|
title: '{{t("ACL")}}',
|
||||||
|
icon: 'LockOutlined',
|
||||||
|
Component: ACLPane,
|
||||||
|
aclSnippet: 'pm.acl.roles',
|
||||||
|
isBookmark: true,
|
||||||
|
});
|
||||||
|
this.app.pluginSettingsManager.add('ui-schema-storage', {
|
||||||
|
title: '{{t("Block templates")}}',
|
||||||
|
icon: 'LayoutOutlined',
|
||||||
|
Component: BlockTemplatesPane,
|
||||||
|
isBookmark: true,
|
||||||
|
aclSnippet: 'pm.ui-schema-storage.block-templates',
|
||||||
|
});
|
||||||
|
this.app.pluginSettingsManager.add('system-settings', {
|
||||||
|
icon: 'SettingOutlined',
|
||||||
|
title: '{{t("System settings")}}',
|
||||||
|
Component: SystemSettingsPane,
|
||||||
|
isBookmark: true,
|
||||||
|
aclSnippet: 'pm.system-settings.system-settings',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.pluginSettingsManager.add('collection-manager', {
|
||||||
|
icon: 'DatabaseOutlined',
|
||||||
|
title: '{{t("Collection manager")}}',
|
||||||
|
Component: () => <Outlet />,
|
||||||
|
isBookmark: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.pluginSettingsManager.add('collection-manager.collections', {
|
||||||
|
title: '{{t("Collections & Fields")}}',
|
||||||
|
Component: CollectionManagerPane,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addComponents() {
|
addComponents() {
|
||||||
@ -36,17 +78,9 @@ export class PMPlugin extends Plugin {
|
|||||||
element: <PluginManager />,
|
element: <PluginManager />,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.app.router.add('admin.settings.list', {
|
this.app.router.add('admin.settings', {
|
||||||
path: '/admin/settings',
|
path: ADMIN_SETTINGS_PATH,
|
||||||
element: <SettingsCenter />,
|
element: <SettingsCenterComponent />,
|
||||||
});
|
|
||||||
this.app.router.add('admin.settings.pluginName', {
|
|
||||||
path: '/admin/settings/:pluginName',
|
|
||||||
element: <SettingsCenter />,
|
|
||||||
});
|
|
||||||
this.app.router.add('admin.settings.pluginName-tabName', {
|
|
||||||
path: '/admin/settings/:pluginName/:tabName',
|
|
||||||
element: <SettingsCenter />,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,9 @@ export const useStyles = createStyles(({ token }) => {
|
|||||||
cursor: 'not-allowed',
|
cursor: 'not-allowed',
|
||||||
},
|
},
|
||||||
pageHeader: {
|
pageHeader: {
|
||||||
paddingBottom: 0,
|
|
||||||
backgroundColor: token.colorBgContainer,
|
backgroundColor: token.colorBgContainer,
|
||||||
paddingTop: token.paddingSM,
|
paddingTop: token.paddingSM,
|
||||||
|
paddingBottom: 0,
|
||||||
paddingInline: token.paddingLG,
|
paddingInline: token.paddingLG,
|
||||||
'.ant-page-header-footer': { marginBlockStart: '0' },
|
'.ant-page-header-footer': { marginBlockStart: '0' },
|
||||||
'& .ant-tabs-nav': {
|
'& .ant-tabs-nav': {
|
||||||
|
@ -25,7 +25,7 @@ export const ColorSelect = connect(
|
|||||||
return (
|
return (
|
||||||
<Select {...props}>
|
<Select {...props}>
|
||||||
{Object.keys(colors).map((color) => (
|
{Object.keys(colors).map((color) => (
|
||||||
<Select.Option value={color}>
|
<Select.Option key={color} value={color}>
|
||||||
<Tag color={color}>{compile(colors[color] || colors.default)}</Tag>
|
<Tag color={color}>{compile(colors[color] || colors.default)}</Tag>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
))}
|
))}
|
||||||
|
@ -50,18 +50,18 @@ export const Sortable = (props: any) => {
|
|||||||
|
|
||||||
const useSortableItemProps = (props) => {
|
const useSortableItemProps = (props) => {
|
||||||
const id = useSortableItemId(props);
|
const id = useSortableItemId(props);
|
||||||
|
const schema = useFieldSchema();
|
||||||
if (props.schema) {
|
if (props.schema) {
|
||||||
return { ...props, id };
|
return { ...props, id };
|
||||||
}
|
}
|
||||||
const schema = useFieldSchema();
|
|
||||||
return { ...props, id, schema };
|
return { ...props, id, schema };
|
||||||
};
|
};
|
||||||
|
|
||||||
const useSortableItemId = (props) => {
|
const useSortableItemId = (props) => {
|
||||||
|
const field = useField();
|
||||||
if (props.id) {
|
if (props.id) {
|
||||||
return props.id;
|
return props.id;
|
||||||
}
|
}
|
||||||
const field = useField();
|
|
||||||
return field.address.toString();
|
return field.address.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { RightOutlined } from '@ant-design/icons';
|
import { RightOutlined } from '@ant-design/icons';
|
||||||
import { Plugin, SettingsCenterProvider } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import { Button, Tooltip } from 'antd';
|
import { Button, Tooltip } from 'antd';
|
||||||
import { createStyles } from 'antd-style';
|
import { createStyles } from 'antd-style';
|
||||||
import React, { lazy } from 'react';
|
import React, { lazy } from 'react';
|
||||||
import { useTranslation } from '../locale';
|
import { NAMESPACE } from '../locale';
|
||||||
|
|
||||||
const DOCUMENTATION_PATH = '/api-documentation';
|
const DOCUMENTATION_PATH = '/api-documentation';
|
||||||
const Documentation = lazy(() => import('./Document'));
|
const Documentation = lazy(() => import('./Document'));
|
||||||
@ -37,32 +37,15 @@ const SCDocumentation = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const APIDocumentationProvider = React.memo((props) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<SettingsCenterProvider
|
|
||||||
settings={{
|
|
||||||
['api-doc']: {
|
|
||||||
title: t('API documentation'),
|
|
||||||
icon: 'BookOutlined',
|
|
||||||
tabs: {
|
|
||||||
documentation: {
|
|
||||||
title: t('Documentation'),
|
|
||||||
component: SCDocumentation,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</SettingsCenterProvider>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
APIDocumentationProvider.displayName = 'APIDocumentationProvider';
|
|
||||||
|
|
||||||
export class APIDocumentationPlugin extends Plugin {
|
export class APIDocumentationPlugin extends Plugin {
|
||||||
async load() {
|
async load() {
|
||||||
this.app.use(APIDocumentationProvider);
|
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||||
|
title: `{{t("API documentation", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
icon: 'BookOutlined',
|
||||||
|
Component: SCDocumentation,
|
||||||
|
aclSnippet: 'pm.api-doc.documentation',
|
||||||
|
});
|
||||||
|
|
||||||
this.app.router.add('api-documentation', {
|
this.app.router.add('api-documentation', {
|
||||||
path: DOCUMENTATION_PATH,
|
path: DOCUMENTATION_PATH,
|
||||||
Component: Documentation,
|
Component: Documentation,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
'API documentation': 'Api 文档',
|
'API documentation': 'API 文档',
|
||||||
'Documentation': '文档',
|
'Documentation': '文档',
|
||||||
'Select a definition': '选择端点',
|
'Select a definition': '选择端点',
|
||||||
};
|
};
|
||||||
|
@ -1,34 +1,15 @@
|
|||||||
import { Plugin, SchemaComponentOptions, SettingsCenterProvider } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import React from 'react';
|
import { NAMESPACE } from '../constants';
|
||||||
import { Configuration } from './Configuration';
|
import { Configuration } from './Configuration';
|
||||||
import { useTranslation } from './locale';
|
|
||||||
|
|
||||||
const ApiKeysProvider = React.memo((props) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<SettingsCenterProvider
|
|
||||||
settings={{
|
|
||||||
['api-keys']: {
|
|
||||||
title: t('API keys'),
|
|
||||||
icon: 'EnvironmentOutlined',
|
|
||||||
tabs: {
|
|
||||||
configuration: {
|
|
||||||
title: t('Keys manager'),
|
|
||||||
component: Configuration,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SchemaComponentOptions components={{}}>{props.children}</SchemaComponentOptions>
|
|
||||||
</SettingsCenterProvider>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
ApiKeysProvider.displayName = 'ApiKeysProvider';
|
|
||||||
|
|
||||||
class APIKeysPlugin extends Plugin {
|
class APIKeysPlugin extends Plugin {
|
||||||
async load() {
|
async load() {
|
||||||
this.app.addProvider(ApiKeysProvider);
|
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||||
|
icon: 'KeyOutlined',
|
||||||
|
title: '{{t("API keys", {"ns": "api-keys"})}}',
|
||||||
|
Component: Configuration,
|
||||||
|
aclSnippet: 'pm.api-keys.configuration',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ export default {
|
|||||||
namespace: 'api-keys',
|
namespace: 'api-keys',
|
||||||
duplicator: 'optional',
|
duplicator: 'optional',
|
||||||
name: 'apiKeys',
|
name: 'apiKeys',
|
||||||
title: '{{t("API keys")}}',
|
title: '{{t("API keys", {"ns": "api-keys"})}}',
|
||||||
sortable: 'sort',
|
sortable: 'sort',
|
||||||
model: 'ApiKeyModel',
|
model: 'ApiKeyModel',
|
||||||
createdBy: true,
|
createdBy: true,
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
const locale = {
|
const locale = {
|
||||||
'API key created successfully': 'API key 创建成功',
|
'API keys': 'API 密钥',
|
||||||
|
'API key created successfully': 'API 密钥创建成功',
|
||||||
'Make sure to copy your personal access key now as you will not be able to see this again.':
|
'Make sure to copy your personal access key now as you will not be able to see this again.':
|
||||||
'请确保现在复制你的个人访问密钥,因为你将无法再次看到这个密钥。',
|
'请确保现在复制你的个人访问密钥,因为你将无法再次看到这个密钥。',
|
||||||
'Key name': '密钥名称',
|
'Key name': '密钥名称',
|
||||||
Expiration: '过期时间',
|
Expiration: '过期时间',
|
||||||
'Delete API key': '删除 API key',
|
'Delete API key': '删除 API 密钥',
|
||||||
Role: '角色',
|
Role: '角色',
|
||||||
'Keys manager': '密钥管理',
|
'Keys manager': '密钥管理',
|
||||||
'Created at': '创建时间',
|
'Created at': '创建时间',
|
||||||
'Add API key': '添加 API key',
|
'Add API key': '添加 API 密钥',
|
||||||
Never: '永不',
|
Never: '永不',
|
||||||
Custom: '自定义',
|
Custom: '自定义',
|
||||||
'Never expires': '永不过期',
|
'Never expires': '永不过期',
|
||||||
|
@ -1,41 +1,20 @@
|
|||||||
import {
|
import { OptionsComponentProvider, SigninPageProvider, SignupPageProvider } from '@nocobase/client';
|
||||||
OptionsComponentProvider,
|
|
||||||
SettingsCenterProvider,
|
|
||||||
SigninPageProvider,
|
|
||||||
SignupPageProvider,
|
|
||||||
} from '@nocobase/client';
|
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Authenticator } from './settings/Authenticator';
|
|
||||||
import SigninPage from './basic/SigninPage';
|
|
||||||
import { presetAuthType } from '../preset';
|
import { presetAuthType } from '../preset';
|
||||||
|
import { Options } from './basic/Options';
|
||||||
|
import SigninPage from './basic/SigninPage';
|
||||||
import SignupPage from './basic/SignupPage';
|
import SignupPage from './basic/SignupPage';
|
||||||
import { useAuthTranslation } from './locale';
|
import { useAuthTranslation } from './locale';
|
||||||
import { Options } from './basic/Options';
|
|
||||||
|
|
||||||
export const AuthPluginProvider: FC = (props) => {
|
export const AuthPluginProvider: FC = (props) => {
|
||||||
const { t } = useAuthTranslation();
|
const { t } = useAuthTranslation();
|
||||||
return (
|
return (
|
||||||
<SettingsCenterProvider
|
<OptionsComponentProvider authType={presetAuthType} component={Options}>
|
||||||
settings={{
|
<SigninPageProvider authType={presetAuthType} tabTitle={t('Sign in via password')} component={SigninPage}>
|
||||||
auth: {
|
<SignupPageProvider authType={presetAuthType} component={SignupPage}>
|
||||||
title: t('Authentication'),
|
{props.children}
|
||||||
icon: 'LoginOutlined',
|
</SignupPageProvider>
|
||||||
tabs: {
|
</SigninPageProvider>
|
||||||
authenticators: {
|
</OptionsComponentProvider>
|
||||||
title: t('Authenticators'),
|
|
||||||
component: () => <Authenticator />,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<OptionsComponentProvider authType={presetAuthType} component={Options}>
|
|
||||||
<SigninPageProvider authType={presetAuthType} tabTitle={t('Sign in via password')} component={SigninPage}>
|
|
||||||
<SignupPageProvider authType={presetAuthType} component={SignupPage}>
|
|
||||||
{props.children}
|
|
||||||
</SignupPageProvider>
|
|
||||||
</SigninPageProvider>
|
|
||||||
</OptionsComponentProvider>
|
|
||||||
</SettingsCenterProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
import { Plugin } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import { AuthPluginProvider } from './AuthPluginProvider';
|
import { AuthPluginProvider } from './AuthPluginProvider';
|
||||||
import { AuthProvider } from './AuthProvider';
|
import { AuthProvider } from './AuthProvider';
|
||||||
|
import { NAMESPACE } from './locale';
|
||||||
|
import { Authenticator } from './settings/Authenticator';
|
||||||
|
|
||||||
export class AuthPlugin extends Plugin {
|
export class AuthPlugin extends Plugin {
|
||||||
async load() {
|
async load() {
|
||||||
|
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||||
|
icon: 'LoginOutlined',
|
||||||
|
title: `{{t("Authentication", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
Component: Authenticator,
|
||||||
|
aclSnippet: 'pm.auth.authenticators',
|
||||||
|
});
|
||||||
this.app.providers.unshift([AuthProvider, {}]);
|
this.app.providers.unshift([AuthProvider, {}]);
|
||||||
this.app.use(AuthPluginProvider);
|
this.app.use(AuthPluginProvider);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import {
|
|||||||
Plugin,
|
Plugin,
|
||||||
SchemaComponentOptions,
|
SchemaComponentOptions,
|
||||||
SchemaInitializerContext,
|
SchemaInitializerContext,
|
||||||
SettingsCenterProvider,
|
|
||||||
useAPIClient,
|
useAPIClient,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import JSON5 from 'json5';
|
import JSON5 from 'json5';
|
||||||
@ -13,7 +12,7 @@ import { ChartBlockEngine } from './ChartBlockEngine';
|
|||||||
import { ChartBlockInitializer } from './ChartBlockInitializer';
|
import { ChartBlockInitializer } from './ChartBlockInitializer';
|
||||||
import { ChartQueryMetadataProvider } from './ChartQueryMetadataProvider';
|
import { ChartQueryMetadataProvider } from './ChartQueryMetadataProvider';
|
||||||
import './Icons';
|
import './Icons';
|
||||||
import { lang } from './locale';
|
import { lang, NAMESPACE } from './locale';
|
||||||
import { CustomSelect } from './select';
|
import { CustomSelect } from './select';
|
||||||
import { QueriesTable } from './settings/QueriesTable';
|
import { QueriesTable } from './settings/QueriesTable';
|
||||||
|
|
||||||
@ -53,7 +52,7 @@ const ChartsProvider = React.memo((props) => {
|
|||||||
key: 'chart',
|
key: 'chart',
|
||||||
type: 'item',
|
type: 'item',
|
||||||
icon: 'PieChartOutlined',
|
icon: 'PieChartOutlined',
|
||||||
title: '{{t("Chart (Old)",{ns:"charts"})}}',
|
title: `{{t("Chart (Old)", { ns: "${NAMESPACE}" })}}`,
|
||||||
component: 'ChartBlockInitializer',
|
component: 'ChartBlockInitializer',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -78,27 +77,12 @@ const ChartsProvider = React.memo((props) => {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<ChartQueryMetadataProvider>
|
<ChartQueryMetadataProvider>
|
||||||
<SettingsCenterProvider
|
<SchemaComponentOptions
|
||||||
settings={{
|
scope={{ validateSQL }}
|
||||||
charts: {
|
components={{ CustomSelect, ChartBlockInitializer, ChartBlockEngine }}
|
||||||
title: '{{t("Charts", {ns:"charts"})}}',
|
|
||||||
icon: 'PieChartOutlined',
|
|
||||||
tabs: {
|
|
||||||
queries: {
|
|
||||||
title: '{{t("Queries", {ns:"charts"})}}',
|
|
||||||
component: QueriesTable,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<SchemaComponentOptions
|
<SchemaInitializerContext.Provider value={items}>{props.children}</SchemaInitializerContext.Provider>
|
||||||
scope={{ validateSQL }}
|
</SchemaComponentOptions>
|
||||||
components={{ CustomSelect, ChartBlockInitializer, ChartBlockEngine }}
|
|
||||||
>
|
|
||||||
<SchemaInitializerContext.Provider value={items}>{props.children}</SchemaInitializerContext.Provider>
|
|
||||||
</SchemaComponentOptions>
|
|
||||||
</SettingsCenterProvider>
|
|
||||||
</ChartQueryMetadataProvider>
|
</ChartQueryMetadataProvider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -110,6 +94,12 @@ export class ChartsPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
async load() {
|
async load() {
|
||||||
this.app.use(ChartsProvider);
|
this.app.use(ChartsProvider);
|
||||||
|
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||||
|
title: `{{t("Charts", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
icon: 'PieChartOutlined',
|
||||||
|
Component: QueriesTable,
|
||||||
|
aclSnippet: 'pm.charts.queries',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
import { SettingsCenterProvider } from '@nocobase/client';
|
|
||||||
import { Card } from 'antd';
|
|
||||||
import React, { FC } from 'react';
|
|
||||||
|
|
||||||
const DuplicatorPanel = () => {
|
|
||||||
return (
|
|
||||||
<Card bordered={false}>
|
|
||||||
<div>hello world</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DuplicatorProvider: FC = function (props) {
|
|
||||||
return (
|
|
||||||
<SettingsCenterProvider
|
|
||||||
settings={
|
|
||||||
{
|
|
||||||
// duplicator: {
|
|
||||||
// title: '应用导入导出',
|
|
||||||
// icon: 'CloudDownloadOutlined',
|
|
||||||
// tabs: {
|
|
||||||
// tab1: {
|
|
||||||
// title: '应用导入导出',
|
|
||||||
// component: DuplicatorPanel,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</SettingsCenterProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
DuplicatorProvider.displayName = 'DuplicatorProvider';
|
|
@ -1,10 +1,7 @@
|
|||||||
import { Plugin } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import { DuplicatorProvider } from './DuplicatorProvider';
|
|
||||||
|
|
||||||
export class DuplicatorPlugin extends Plugin {
|
export class DuplicatorPlugin extends Plugin {
|
||||||
async load() {
|
async load() {}
|
||||||
this.app.use(DuplicatorProvider);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DuplicatorPlugin;
|
export default DuplicatorPlugin;
|
||||||
|
@ -6,16 +6,13 @@ import {
|
|||||||
SchemaComponentOptions,
|
SchemaComponentOptions,
|
||||||
SchemaInitializerContext,
|
SchemaInitializerContext,
|
||||||
SchemaInitializerProvider,
|
SchemaInitializerProvider,
|
||||||
SettingsCenterProvider,
|
|
||||||
useCollection,
|
useCollection,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { forEach } from '@nocobase/utils/client';
|
import { forEach } from '@nocobase/utils/client';
|
||||||
import React, { FC, useContext } from 'react';
|
import React, { FC, useContext } from 'react';
|
||||||
import { FileStoragePane } from './FileStorage';
|
|
||||||
import * as hooks from './hooks';
|
import * as hooks from './hooks';
|
||||||
import * as initializers from './initializers';
|
import * as initializers from './initializers';
|
||||||
import { attachment } from './interfaces/attachment';
|
import { attachment } from './interfaces/attachment';
|
||||||
import { NAMESPACE } from './locale';
|
|
||||||
import * as templates from './templates';
|
import * as templates from './templates';
|
||||||
|
|
||||||
// 注册之后就可以在 Crete collection 按钮中选择创建了
|
// 注册之后就可以在 Crete collection 按钮中选择创建了
|
||||||
@ -52,33 +49,18 @@ export const FileManagerProvider: FC = (props) => {
|
|||||||
const ctx = useContext(PluginManagerContext);
|
const ctx = useContext(PluginManagerContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsCenterProvider
|
<PluginManagerContext.Provider
|
||||||
settings={{
|
value={{
|
||||||
'file-manager': {
|
components: {
|
||||||
title: `{{t("File manager", { ns: "${NAMESPACE}" })}}`,
|
...ctx?.components,
|
||||||
icon: 'FileOutlined',
|
|
||||||
tabs: {
|
|
||||||
storages: {
|
|
||||||
title: `{{t("File storage", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
component: FileStoragePane,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PluginManagerContext.Provider
|
<CollectionManagerProvider interfaces={{ attachment }}>
|
||||||
value={{
|
<SchemaComponentOptions scope={hooks}>
|
||||||
components: {
|
<SchemaInitializerProvider components={initializers}>{props.children}</SchemaInitializerProvider>
|
||||||
...ctx?.components,
|
</SchemaComponentOptions>
|
||||||
},
|
</CollectionManagerProvider>
|
||||||
}}
|
</PluginManagerContext.Provider>
|
||||||
>
|
|
||||||
<CollectionManagerProvider interfaces={{ attachment }}>
|
|
||||||
<SchemaComponentOptions scope={hooks}>
|
|
||||||
<SchemaInitializerProvider components={initializers}>{props.children}</SchemaInitializerProvider>
|
|
||||||
</SchemaComponentOptions>
|
|
||||||
</CollectionManagerProvider>
|
|
||||||
</PluginManagerContext.Provider>
|
|
||||||
</SettingsCenterProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -12,7 +12,7 @@ test.describe('file manager', () => {
|
|||||||
// 1、前置条件:已登录
|
// 1、前置条件:已登录
|
||||||
|
|
||||||
// 2、测试步骤:进入“文件管理器”-“新建”按钮,填写表单,点击“确定”按钮
|
// 2、测试步骤:进入“文件管理器”-“新建”按钮,填写表单,点击“确定”按钮
|
||||||
await page.goto('/admin/settings/file-manager/storages');
|
await page.goto('/admin/settings/file-manager');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
await page.getByRole('button', { name: 'plus Add new' }).hover();
|
await page.getByRole('button', { name: 'plus Add new' }).hover();
|
||||||
await page.getByRole('menuitem', { name: 'Local storage' }).click();
|
await page.getByRole('menuitem', { name: 'Local storage' }).click();
|
||||||
|
@ -10,7 +10,7 @@ test.describe('File manager', () => {
|
|||||||
let caseTitle = 'edit local storage title';
|
let caseTitle = 'edit local storage title';
|
||||||
|
|
||||||
// 1、前置条件:1.1已登录;1.2存在一个文件管理器
|
// 1、前置条件:1.1已登录;1.2存在一个文件管理器
|
||||||
await page.goto('/admin/settings/file-manager/storages');
|
await page.goto('/admin/settings/file-manager');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
await page.getByRole('button', { name: 'plus Add new' }).hover();
|
await page.getByRole('button', { name: 'plus Add new' }).hover();
|
||||||
await page.getByRole('menuitem', { name: 'Local storage' }).click();
|
await page.getByRole('menuitem', { name: 'Local storage' }).click();
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { Plugin } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import { FileManagerProvider } from './FileManagerProvider';
|
import { FileManagerProvider } from './FileManagerProvider';
|
||||||
|
import { FileStoragePane } from './FileStorage';
|
||||||
|
import { NAMESPACE } from './locale';
|
||||||
import { storageTypes } from './schemas/storageTypes';
|
import { storageTypes } from './schemas/storageTypes';
|
||||||
|
|
||||||
export class FileManagerPlugin extends Plugin {
|
export class FileManagerPlugin extends Plugin {
|
||||||
@ -7,6 +9,12 @@ export class FileManagerPlugin extends Plugin {
|
|||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
this.app.use(FileManagerProvider);
|
this.app.use(FileManagerProvider);
|
||||||
|
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||||
|
title: `{{t("File manager", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
icon: 'FileOutlined',
|
||||||
|
Component: FileStoragePane,
|
||||||
|
aclSnippet: 'pm.file-manager.storages',
|
||||||
|
});
|
||||||
Object.values(storageTypes).forEach((storageType) => {
|
Object.values(storageTypes).forEach((storageType) => {
|
||||||
this.registerStorageType(storageType.name, storageType);
|
this.registerStorageType(storageType.name, storageType);
|
||||||
});
|
});
|
||||||
|
@ -1,30 +1,19 @@
|
|||||||
import { PluginManagerContext, SettingsCenterContext, SettingsCenterProvider } from '@nocobase/client';
|
import { PluginManagerContext } from '@nocobase/client';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { GraphCollectionPane } from './GraphCollectionShortcut';
|
|
||||||
import { useGCMTranslation } from './utils';
|
|
||||||
|
|
||||||
export const GraphCollectionProvider = React.memo((props) => {
|
export const GraphCollectionProvider = React.memo((props) => {
|
||||||
const ctx = useContext(PluginManagerContext);
|
const ctx = useContext(PluginManagerContext);
|
||||||
const { t } = useGCMTranslation();
|
|
||||||
const items = useContext(SettingsCenterContext);
|
|
||||||
|
|
||||||
items['collection-manager']['tabs']['graph'] = {
|
|
||||||
title: t('Graphical interface'),
|
|
||||||
component: GraphCollectionPane,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsCenterProvider settings={items}>
|
<PluginManagerContext.Provider
|
||||||
<PluginManagerContext.Provider
|
value={{
|
||||||
value={{
|
components: {
|
||||||
components: {
|
...ctx?.components,
|
||||||
...ctx?.components,
|
},
|
||||||
},
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{props.children}
|
||||||
{props.children}
|
</PluginManagerContext.Provider>
|
||||||
</PluginManagerContext.Provider>
|
|
||||||
</SettingsCenterProvider>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
GraphCollectionProvider.displayName = 'GraphCollectionProvider';
|
GraphCollectionProvider.displayName = 'GraphCollectionProvider';
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
import { Plugin } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import { GraphCollectionProvider } from './GraphCollectionProvider';
|
import { GraphCollectionProvider } from './GraphCollectionProvider';
|
||||||
|
import { GraphCollectionPane } from './GraphCollectionShortcut';
|
||||||
|
import { NAMESPACE } from './locale';
|
||||||
|
|
||||||
export class GraphCollectionPlugin extends Plugin {
|
export class GraphCollectionPlugin extends Plugin {
|
||||||
async load() {
|
async load() {
|
||||||
this.app.use(GraphCollectionProvider);
|
this.app.use(GraphCollectionProvider);
|
||||||
|
|
||||||
|
this.app.pluginSettingsManager.add('collection-manager.graph', {
|
||||||
|
title: `{{t("Graphical interface", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
Component: GraphCollectionPane,
|
||||||
|
aclSnippet: 'pm.collection-manager.graph',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1 @@
|
|||||||
// export { default as enUS } from './en-US';
|
export const NAMESPACE = 'graph-collection-manager';
|
||||||
// export { default as zhCN } from './zh-CN';
|
|
||||||
// export { default as jaJP } from './ja-JP';
|
|
||||||
|
@ -1,30 +1,14 @@
|
|||||||
import { Plugin, SettingsCenterProvider } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import React from 'react';
|
|
||||||
import { Localization } from './Localization';
|
import { Localization } from './Localization';
|
||||||
import { useLocalTranslation } from './locale';
|
import { NAMESPACE } from './locale';
|
||||||
|
|
||||||
export class LocalizationManagementPlugin extends Plugin {
|
export class LocalizationManagementPlugin extends Plugin {
|
||||||
async load() {
|
async load() {
|
||||||
this.app.use((props) => {
|
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||||
const { t } = useLocalTranslation();
|
title: `{{t("Localization management", { ns: "${NAMESPACE}" })}}`,
|
||||||
return (
|
icon: 'GlobalOutlined',
|
||||||
<SettingsCenterProvider
|
Component: Localization,
|
||||||
settings={{
|
aclSnippet: 'pm.localization-management.localization',
|
||||||
['localization-management']: {
|
|
||||||
title: t('Localization management'),
|
|
||||||
icon: 'GlobalOutlined',
|
|
||||||
tabs: {
|
|
||||||
localization: {
|
|
||||||
title: t('Translations'),
|
|
||||||
component: () => <Localization />,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</SettingsCenterProvider>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import AMapLoader from '@amap/amap-jsapi-loader';
|
import AMapLoader from '@amap/amap-jsapi-loader';
|
||||||
import '@amap/amap-jsapi-types';
|
import '@amap/amap-jsapi-types';
|
||||||
import { SyncOutlined } from '@ant-design/icons';
|
import { SyncOutlined } from '@ant-design/icons';
|
||||||
import { useField, useFieldSchema } from '@formily/react';
|
import { useFieldSchema } from '@formily/react';
|
||||||
import { css, useCollection } from '@nocobase/client';
|
import { css, useApp, useCollection } from '@nocobase/client';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import { Alert, App, Button, Spin } from 'antd';
|
import { Alert, App, Button, Spin } from 'antd';
|
||||||
import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||||
@ -373,11 +373,13 @@ export const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapCompone
|
|||||||
errMessage,
|
errMessage,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const app = useApp();
|
||||||
|
|
||||||
if (!accessKey || errMessage) {
|
if (!accessKey || errMessage) {
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
action={
|
action={
|
||||||
<Button type="primary" onClick={() => navigate('/admin/settings/map/configuration')}>
|
<Button type="primary" onClick={() => navigate(app.pluginSettingsManager.getRoutePath('map'))}>
|
||||||
{t('Go to the configuration page')}
|
{t('Go to the configuration page')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { SyncOutlined } from '@ant-design/icons';
|
import { SyncOutlined } from '@ant-design/icons';
|
||||||
import { useFieldSchema } from '@formily/react';
|
import { useFieldSchema } from '@formily/react';
|
||||||
import { Loader } from '@googlemaps/js-api-loader';
|
import { Loader } from '@googlemaps/js-api-loader';
|
||||||
import { css, useAPIClient, useCollection } from '@nocobase/client';
|
import { css, useAPIClient, useApp, useCollection } from '@nocobase/client';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
import { Alert, App, Button, Spin } from 'antd';
|
import { Alert, App, Button, Spin } from 'antd';
|
||||||
import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||||
@ -369,12 +369,16 @@ export const GoogleMapsComponent = React.forwardRef<GoogleMapForwardedRefProps,
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
const app = useApp();
|
||||||
|
|
||||||
if (!accessKey || errMessage) {
|
if (!accessKey || errMessage) {
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
action={
|
action={
|
||||||
<Button type="primary" onClick={() => navigate('/admin/settings/map/configuration?tab=google')}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => navigate(app.pluginSettingsManager.getRoutePath('map') + '?tab=google')}
|
||||||
|
>
|
||||||
{t('Go to the configuration page')}
|
{t('Go to the configuration page')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
@ -1,45 +1,23 @@
|
|||||||
import {
|
import { CollectionManagerContext, CurrentAppInfoProvider, Plugin, SchemaComponentOptions } from '@nocobase/client';
|
||||||
CollectionManagerContext,
|
|
||||||
CurrentAppInfoProvider,
|
|
||||||
Plugin,
|
|
||||||
SchemaComponentOptions,
|
|
||||||
SettingsCenterProvider,
|
|
||||||
} from '@nocobase/client';
|
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { MapBlockOptions } from './block';
|
import { MapBlockOptions } from './block';
|
||||||
import { Configuration, Map } from './components';
|
import { Configuration, Map } from './components';
|
||||||
import { interfaces } from './fields';
|
import { interfaces } from './fields';
|
||||||
import { MapInitializer } from './initialize';
|
import { MapInitializer } from './initialize';
|
||||||
import { useMapTranslation } from './locale';
|
import { NAMESPACE } from './locale';
|
||||||
|
|
||||||
const MapProvider = React.memo((props) => {
|
const MapProvider = React.memo((props) => {
|
||||||
const ctx = useContext(CollectionManagerContext);
|
const ctx = useContext(CollectionManagerContext);
|
||||||
const { t } = useMapTranslation();
|
|
||||||
return (
|
return (
|
||||||
<CurrentAppInfoProvider>
|
<CurrentAppInfoProvider>
|
||||||
<MapInitializer>
|
<MapInitializer>
|
||||||
<SettingsCenterProvider
|
<SchemaComponentOptions components={{ Map }}>
|
||||||
settings={{
|
<MapBlockOptions>
|
||||||
map: {
|
<CollectionManagerContext.Provider value={{ ...ctx, interfaces: { ...ctx.interfaces, ...interfaces } }}>
|
||||||
title: t('Map Manager'),
|
{props.children}
|
||||||
icon: 'EnvironmentOutlined',
|
</CollectionManagerContext.Provider>
|
||||||
tabs: {
|
</MapBlockOptions>
|
||||||
configuration: {
|
</SchemaComponentOptions>
|
||||||
title: t('Configuration'),
|
|
||||||
component: Configuration,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SchemaComponentOptions components={{ Map }}>
|
|
||||||
<MapBlockOptions>
|
|
||||||
<CollectionManagerContext.Provider value={{ ...ctx, interfaces: { ...ctx.interfaces, ...interfaces } }}>
|
|
||||||
{props.children}
|
|
||||||
</CollectionManagerContext.Provider>
|
|
||||||
</MapBlockOptions>
|
|
||||||
</SchemaComponentOptions>
|
|
||||||
</SettingsCenterProvider>
|
|
||||||
</MapInitializer>
|
</MapInitializer>
|
||||||
</CurrentAppInfoProvider>
|
</CurrentAppInfoProvider>
|
||||||
);
|
);
|
||||||
@ -49,6 +27,12 @@ MapProvider.displayName = 'MapProvider';
|
|||||||
export class MapPlugin extends Plugin {
|
export class MapPlugin extends Plugin {
|
||||||
async load() {
|
async load() {
|
||||||
this.app.use(MapProvider);
|
this.app.use(MapProvider);
|
||||||
|
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||||
|
title: `{{t("Map Manager", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
icon: 'EnvironmentOutlined',
|
||||||
|
Component: Configuration,
|
||||||
|
aclSnippet: 'pm.map.configuration',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
import { SettingsCenterProvider } from '@nocobase/client';
|
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { AppConfiguration, InterfaceConfiguration } from './configuration';
|
|
||||||
import { isJSBridge } from './core/bridge';
|
import { isJSBridge } from './core/bridge';
|
||||||
import { useTranslation } from './locale';
|
|
||||||
|
|
||||||
export const MobileClientProvider = React.memo((props) => {
|
export const MobileClientProvider = React.memo((props) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigation = useNavigate();
|
const navigation = useNavigate();
|
||||||
|
|
||||||
@ -16,26 +12,5 @@ export const MobileClientProvider = React.memo((props) => {
|
|||||||
}
|
}
|
||||||
}, [location.pathname, navigation]);
|
}, [location.pathname, navigation]);
|
||||||
|
|
||||||
return (
|
return <>{props.children}</>;
|
||||||
<SettingsCenterProvider
|
|
||||||
settings={{
|
|
||||||
['mobile-client']: {
|
|
||||||
title: t('Mobile Client-side'),
|
|
||||||
icon: 'MobileOutlined',
|
|
||||||
tabs: {
|
|
||||||
interface: {
|
|
||||||
title: t('Interface Configuration'),
|
|
||||||
component: InterfaceConfiguration,
|
|
||||||
},
|
|
||||||
app: {
|
|
||||||
title: t('App Configuration'),
|
|
||||||
component: AppConfiguration,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</SettingsCenterProvider>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
@ -1,17 +1,38 @@
|
|||||||
import { createRouterManager, Plugin, RouterManager, RouteSchemaComponent } from '@nocobase/client';
|
import { createRouterManager, Plugin, RouterManager, RouteSchemaComponent } from '@nocobase/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate, Outlet } from 'react-router-dom';
|
||||||
import { MobileClientProvider } from './MobileClientProvider';
|
import { MobileClientProvider } from './MobileClientProvider';
|
||||||
import MApplication from './router/Application';
|
import MApplication from './router/Application';
|
||||||
|
import { AppConfiguration, InterfaceConfiguration } from './configuration';
|
||||||
|
import { NAMESPACE } from './locale';
|
||||||
|
|
||||||
export class MobileClientPlugin extends Plugin {
|
export class MobileClientPlugin extends Plugin {
|
||||||
public mobileRouter: RouterManager;
|
public mobileRouter: RouterManager;
|
||||||
async load() {
|
async load() {
|
||||||
this.setMobileRouter();
|
this.setMobileRouter();
|
||||||
this.addRoutes();
|
this.addRoutes();
|
||||||
|
this.addSettings();
|
||||||
this.app.use(MobileClientProvider);
|
this.app.use(MobileClientProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSettings() {
|
||||||
|
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||||
|
title: `{{t("Mobile Client-side", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
icon: 'MobileOutlined',
|
||||||
|
Component: () => <Outlet />,
|
||||||
|
});
|
||||||
|
this.app.pluginSettingsManager.add(`${NAMESPACE}.interface`, {
|
||||||
|
title: `{{t("Interface Configuration", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
Component: InterfaceConfiguration,
|
||||||
|
sort: 1,
|
||||||
|
});
|
||||||
|
this.app.pluginSettingsManager.add(`${NAMESPACE}.app`, {
|
||||||
|
title: `{{t("App Configuration", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
Component: AppConfiguration,
|
||||||
|
sort: 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setMobileRouter() {
|
setMobileRouter() {
|
||||||
const router = createRouterManager({ type: 'hash' });
|
const router = createRouterManager({ type: 'hash' });
|
||||||
router.add('root', {
|
router.add('root', {
|
||||||
|
@ -1,14 +1,7 @@
|
|||||||
import {
|
import { Icon, PinnedPluginListProvider, SchemaComponentOptions, useApp, useRequest } from '@nocobase/client';
|
||||||
Icon,
|
|
||||||
PinnedPluginListProvider,
|
|
||||||
SchemaComponentOptions,
|
|
||||||
SettingsCenterProvider,
|
|
||||||
useRequest,
|
|
||||||
} from '@nocobase/client';
|
|
||||||
import { Button, Dropdown } from 'antd';
|
import { Button, Dropdown } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { AppManager } from './AppManager';
|
|
||||||
import { AppNameInput } from './AppNameInput';
|
import { AppNameInput } from './AppNameInput';
|
||||||
import { usePluginUtils } from './utils';
|
import { usePluginUtils } from './utils';
|
||||||
|
|
||||||
@ -25,6 +18,7 @@ const MultiAppManager = () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const { t } = usePluginUtils();
|
const { t } = usePluginUtils();
|
||||||
|
const app = useApp();
|
||||||
const items = [
|
const items = [
|
||||||
...(data?.data || []).map((app) => {
|
...(data?.data || []).map((app) => {
|
||||||
let link = `/apps/${app.name}/admin/`;
|
let link = `/apps/${app.name}/admin/`;
|
||||||
@ -42,7 +36,7 @@ const MultiAppManager = () => {
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
key: '.manager',
|
key: '.manager',
|
||||||
label: <Link to="/admin/settings/multi-app-manager/applications">{t('Manage applications')}</Link>,
|
label: <Link to={app.pluginSettingsManager.getRoutePath('multi-app-manager')}>{t('Manage applications')}</Link>,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
@ -58,35 +52,13 @@ const MultiAppManager = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MultiAppManagerProvider = (props) => {
|
export const MultiAppManagerProvider = (props) => {
|
||||||
const { t } = usePluginUtils();
|
|
||||||
return (
|
return (
|
||||||
<PinnedPluginListProvider
|
<PinnedPluginListProvider
|
||||||
items={{
|
items={{
|
||||||
am: { order: 201, component: 'MultiAppManager', pin: true },
|
am: { order: 201, component: 'MultiAppManager', pin: true },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SchemaComponentOptions components={{ MultiAppManager, AppNameInput }}>
|
<SchemaComponentOptions components={{ MultiAppManager, AppNameInput }}>{props.children}</SchemaComponentOptions>
|
||||||
<SettingsCenterProvider
|
|
||||||
settings={{
|
|
||||||
'multi-app-manager': {
|
|
||||||
title: t('Multi-app manager'),
|
|
||||||
icon: 'AppstoreOutlined',
|
|
||||||
tabs: {
|
|
||||||
applications: {
|
|
||||||
title: t('Applications'),
|
|
||||||
component: () => <AppManager />,
|
|
||||||
},
|
|
||||||
// settings: {
|
|
||||||
// title: 'Settings',
|
|
||||||
// component: () => <Settings />,
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</SettingsCenterProvider>
|
|
||||||
</SchemaComponentOptions>
|
|
||||||
</PinnedPluginListProvider>
|
</PinnedPluginListProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,18 @@
|
|||||||
import { Plugin } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import { MultiAppManagerProvider } from './MultiAppManagerProvider';
|
import { MultiAppManagerProvider } from './MultiAppManagerProvider';
|
||||||
|
import { AppManager } from './AppManager';
|
||||||
|
import { NAMESPACE } from '../locale';
|
||||||
|
|
||||||
export class MultiAppManagerPlugin extends Plugin {
|
export class MultiAppManagerPlugin extends Plugin {
|
||||||
async load() {
|
async load() {
|
||||||
this.app.use(MultiAppManagerProvider);
|
this.app.use(MultiAppManagerProvider);
|
||||||
|
|
||||||
|
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||||
|
title: `{{t("Multi-app manager", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
icon: 'AppstoreOutlined',
|
||||||
|
Component: AppManager,
|
||||||
|
aclSnippet: 'pm.multi-app-manager.applications',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export const NAMESPACE = 'multi-app-manager';
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@nocobase/plugin-oidc",
|
"name": "@nocobase/plugin-oidc",
|
||||||
"displayName": "OIDC (OpenID Connect) auth - SSO login",
|
"displayName": "OIDC auth - SSO login",
|
||||||
"displayName.zh-CN": "OIDC (OpenID Connect) 认证 - SSO 登录",
|
"displayName.zh-CN": "OIDC 认证 - SSO 登录",
|
||||||
"description": "OIDC (OpenID Connect) authentication for NocoBase",
|
"description": "OIDC (OpenID Connect) authentication for NocoBase",
|
||||||
"description.zh-CN": "OIDC (OpenID Connect) authentication for NocoBase",
|
"description.zh-CN": "OIDC (OpenID Connect) authentication for NocoBase",
|
||||||
"version": "0.14.0-alpha.8",
|
"version": "0.14.0-alpha.8",
|
||||||
|
@ -1,11 +1,5 @@
|
|||||||
import { TableOutlined } from '@ant-design/icons';
|
import { TableOutlined } from '@ant-design/icons';
|
||||||
import {
|
import { Plugin, SchemaComponentOptions, SchemaInitializer, SchemaInitializerContext, useApp } from '@nocobase/client';
|
||||||
Plugin,
|
|
||||||
SchemaComponentOptions,
|
|
||||||
SchemaInitializer,
|
|
||||||
SchemaInitializerContext,
|
|
||||||
SettingsCenterProvider,
|
|
||||||
} from '@nocobase/client';
|
|
||||||
import { Card } from 'antd';
|
import { Card } from 'antd';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -56,31 +50,30 @@ const HelloProvider = React.memo((props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsCenterProvider
|
<SchemaComponentOptions components={{ HelloDesigner, HelloBlockInitializer }}>
|
||||||
settings={{
|
<SchemaInitializerContext.Provider value={items}>{props.children}</SchemaInitializerContext.Provider>
|
||||||
'sample-hello': {
|
</SchemaComponentOptions>
|
||||||
title: 'Hello',
|
|
||||||
icon: 'ApiOutlined',
|
|
||||||
tabs: {
|
|
||||||
tab1: {
|
|
||||||
title: 'Hello tab',
|
|
||||||
component: () => <Card bordered={false}>Hello Settings</Card>,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SchemaComponentOptions components={{ HelloDesigner, HelloBlockInitializer }}>
|
|
||||||
<SchemaInitializerContext.Provider value={items}>{props.children}</SchemaInitializerContext.Provider>
|
|
||||||
</SchemaComponentOptions>
|
|
||||||
</SettingsCenterProvider>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
HelloProvider.displayName = 'HelloProvider';
|
HelloProvider.displayName = 'HelloProvider';
|
||||||
|
|
||||||
|
const HelloPluginSettingPage = () => {
|
||||||
|
return (
|
||||||
|
<Card bordered={false}>
|
||||||
|
<div>Hello plugin setting page</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
class HelloPlugin extends Plugin {
|
class HelloPlugin extends Plugin {
|
||||||
async load() {
|
async load() {
|
||||||
this.app.addProvider(HelloProvider);
|
this.app.addProvider(HelloProvider);
|
||||||
|
this.app.pluginSettingsManager.add('sample-hello', {
|
||||||
|
title: 'Hello',
|
||||||
|
icon: 'ApiOutlined',
|
||||||
|
Component: HelloPluginSettingPage,
|
||||||
|
sort: 100,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { i18n, Plugin, PluginManagerContext, SettingsCenterProvider } from '@nocobase/client';
|
import { i18n, Plugin, PluginManagerContext } from '@nocobase/client';
|
||||||
import { Select } from 'antd';
|
import { Select } from 'antd';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const ns = '@nocobase/plugin-sample-shop-i18n';
|
const NAMESPACE = 'sample-shop-i18n';
|
||||||
|
|
||||||
i18n.addResources('zh-CN', ns, {
|
i18n.addResources('zh-CN', NAMESPACE, {
|
||||||
Shop: '店铺',
|
Shop: '店铺',
|
||||||
I18n: '国际化',
|
I18n: '国际化',
|
||||||
Pending: '已下单',
|
Pending: '已下单',
|
||||||
@ -23,7 +23,7 @@ const ORDER_STATUS_LIST = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function OrderStatusSelect() {
|
function OrderStatusSelect() {
|
||||||
const { t } = useTranslation(ns);
|
const { t } = useTranslation(NAMESPACE);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select style={{ minWidth: '8em' }}>
|
<Select style={{ minWidth: '8em' }}>
|
||||||
@ -40,36 +40,26 @@ const ShopI18nProvider = React.memo((props) => {
|
|||||||
const ctx = useContext(PluginManagerContext);
|
const ctx = useContext(PluginManagerContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsCenterProvider
|
<PluginManagerContext.Provider
|
||||||
settings={{
|
value={{
|
||||||
workflow: {
|
components: {
|
||||||
icon: 'ShopOutlined',
|
...ctx?.components,
|
||||||
title: `{{t("Shop", { ns: "${ns}" })}}`,
|
|
||||||
tabs: {
|
|
||||||
workflows: {
|
|
||||||
title: `{{t("I18n", { ns: "${ns}" })}}`,
|
|
||||||
component: OrderStatusSelect,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PluginManagerContext.Provider
|
{props.children}
|
||||||
value={{
|
</PluginManagerContext.Provider>
|
||||||
components: {
|
|
||||||
...ctx?.components,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</PluginManagerContext.Provider>
|
|
||||||
</SettingsCenterProvider>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
class ShopI18nPlugin extends Plugin {
|
class ShopI18nPlugin extends Plugin {
|
||||||
async load() {
|
async load() {
|
||||||
this.app.addProvider(ShopI18nProvider);
|
this.app.addProvider(ShopI18nProvider);
|
||||||
|
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||||
|
title: `{{t("Shop", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
icon: 'ShopOutlined',
|
||||||
|
Component: OrderStatusSelect,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,4 @@
|
|||||||
import {
|
import { Plugin, createStyles, defaultTheme, useCurrentUserSettingsMenu, useGlobalTheme } from '@nocobase/client';
|
||||||
Plugin,
|
|
||||||
SettingsCenterProvider,
|
|
||||||
createStyles,
|
|
||||||
defaultTheme,
|
|
||||||
useCurrentUserSettingsMenu,
|
|
||||||
useGlobalTheme,
|
|
||||||
} from '@nocobase/client';
|
|
||||||
import { ConfigProvider } from 'antd';
|
import { ConfigProvider } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
@ -15,7 +8,7 @@ import ThemeList from './components/ThemeList';
|
|||||||
import { ThemeListProvider } from './components/ThemeListProvider';
|
import { ThemeListProvider } from './components/ThemeListProvider';
|
||||||
import CustomTheme from './components/theme-editor';
|
import CustomTheme from './components/theme-editor';
|
||||||
import { useThemeSettings } from './hooks/useThemeSettings';
|
import { useThemeSettings } from './hooks/useThemeSettings';
|
||||||
import { useTranslation } from './locale';
|
import { NAMESPACE } from './locale';
|
||||||
|
|
||||||
const useStyles = createStyles(({ css, token }) => {
|
const useStyles = createStyles(({ css, token }) => {
|
||||||
return {
|
return {
|
||||||
@ -38,7 +31,6 @@ const useStyles = createStyles(({ css, token }) => {
|
|||||||
const CustomThemeProvider = React.memo((props) => {
|
const CustomThemeProvider = React.memo((props) => {
|
||||||
const { addMenuItem } = useCurrentUserSettingsMenu();
|
const { addMenuItem } = useCurrentUserSettingsMenu();
|
||||||
const themeItem = useThemeSettings();
|
const themeItem = useThemeSettings();
|
||||||
const { t } = useTranslation();
|
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const { theme, setTheme } = useGlobalTheme();
|
const { theme, setTheme } = useGlobalTheme();
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
@ -48,21 +40,6 @@ const CustomThemeProvider = React.memo((props) => {
|
|||||||
addMenuItem(themeItem, { before: 'divider_3' });
|
addMenuItem(themeItem, { before: 'divider_3' });
|
||||||
}, [addMenuItem, themeItem]);
|
}, [addMenuItem, themeItem]);
|
||||||
|
|
||||||
const settings = useMemo(() => {
|
|
||||||
return {
|
|
||||||
'theme-editor': {
|
|
||||||
title: t('Theme editor'),
|
|
||||||
icon: 'BgColorsOutlined',
|
|
||||||
tabs: {
|
|
||||||
themes: {
|
|
||||||
title: t('Themes'),
|
|
||||||
component: ThemeList,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const contentStyle = useMemo(() => {
|
const contentStyle = useMemo(() => {
|
||||||
return open
|
return open
|
||||||
? { transform: 'rotate(0)', flexGrow: 1, width: 0, height: '100%' }
|
? { transform: 'rotate(0)', flexGrow: 1, width: 0, height: '100%' }
|
||||||
@ -89,7 +66,7 @@ const CustomThemeProvider = React.memo((props) => {
|
|||||||
<ThemeListProvider>
|
<ThemeListProvider>
|
||||||
<InitializeTheme>
|
<InitializeTheme>
|
||||||
<ThemeEditorProvider open={open} setOpen={setOpen}>
|
<ThemeEditorProvider open={open} setOpen={setOpen}>
|
||||||
<SettingsCenterProvider settings={settings}>{editor}</SettingsCenterProvider>
|
{editor}
|
||||||
</ThemeEditorProvider>
|
</ThemeEditorProvider>
|
||||||
</InitializeTheme>
|
</InitializeTheme>
|
||||||
</ThemeListProvider>
|
</ThemeListProvider>
|
||||||
@ -101,6 +78,12 @@ CustomThemeProvider.displayName = 'CustomThemeProvider';
|
|||||||
export class ThemeEditorPlugin extends Plugin {
|
export class ThemeEditorPlugin extends Plugin {
|
||||||
async load() {
|
async load() {
|
||||||
this.app.use(CustomThemeProvider);
|
this.app.use(CustomThemeProvider);
|
||||||
|
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||||
|
title: `{{t("Theme editor", {ns:"${NAMESPACE}"})}}`,
|
||||||
|
icon: 'BgColorsOutlined',
|
||||||
|
Component: ThemeList,
|
||||||
|
aclSnippet: 'pm.theme-editor.themes',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,39 +1,19 @@
|
|||||||
import React, { FC, useContext } from 'react';
|
import React, { FC, useContext } from 'react';
|
||||||
|
|
||||||
import { PluginManagerContext, SettingsCenterProvider } from '@nocobase/client';
|
import { PluginManagerContext } from '@nocobase/client';
|
||||||
|
|
||||||
import { NAMESPACE } from './locale';
|
|
||||||
|
|
||||||
import { VerificationProviders } from './VerificationProviders';
|
|
||||||
|
|
||||||
export { default as verificationProviderTypes } from './providerTypes';
|
export { default as verificationProviderTypes } from './providerTypes';
|
||||||
|
|
||||||
export const VerificationProvider: FC = (props) => {
|
export const VerificationProvider: FC = (props) => {
|
||||||
const ctx = useContext(PluginManagerContext);
|
const ctx = useContext(PluginManagerContext);
|
||||||
return (
|
return (
|
||||||
<SettingsCenterProvider
|
<PluginManagerContext.Provider
|
||||||
settings={{
|
value={{
|
||||||
verification: {
|
components: {
|
||||||
icon: 'CheckCircleOutlined',
|
...ctx?.components,
|
||||||
title: `{{t("Verification", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
tabs: {
|
|
||||||
providers: {
|
|
||||||
title: `{{t("Verification providers", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
component: VerificationProviders,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PluginManagerContext.Provider
|
{props.children}
|
||||||
value={{
|
</PluginManagerContext.Provider>
|
||||||
components: {
|
|
||||||
...ctx?.components,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</PluginManagerContext.Provider>
|
|
||||||
</SettingsCenterProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
import { Plugin } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import { VerificationProvider } from './VerificationProvider';
|
import { VerificationProvider } from './VerificationProvider';
|
||||||
|
import { VerificationProviders } from './VerificationProviders';
|
||||||
|
import { NAMESPACE } from './locale';
|
||||||
|
|
||||||
export class VerificationPlugin extends Plugin {
|
export class VerificationPlugin extends Plugin {
|
||||||
async load() {
|
async load() {
|
||||||
this.app.use(VerificationProvider);
|
this.app.use(VerificationProvider);
|
||||||
|
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||||
|
icon: 'CheckCircleOutlined',
|
||||||
|
title: `{{t("Verification", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
Component: VerificationProviders,
|
||||||
|
aclSnippet: 'pm.verification.providers',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
cx,
|
cx,
|
||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
useAPIClient,
|
useAPIClient,
|
||||||
|
useApp,
|
||||||
useCompile,
|
useCompile,
|
||||||
useDocumentTitle,
|
useDocumentTitle,
|
||||||
useResourceActionContext,
|
useResourceActionContext,
|
||||||
@ -20,6 +21,7 @@ import useStyles from './style';
|
|||||||
import { linkNodes } from './utils';
|
import { linkNodes } from './utils';
|
||||||
import { DownOutlined } from '@ant-design/icons';
|
import { DownOutlined } from '@ant-design/icons';
|
||||||
import { StatusButton } from './components/StatusButton';
|
import { StatusButton } from './components/StatusButton';
|
||||||
|
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './constant';
|
||||||
|
|
||||||
function attachJobs(nodes, jobs: any[] = []): void {
|
function attachJobs(nodes, jobs: any[] = []): void {
|
||||||
const nodesMap = new Map();
|
const nodesMap = new Map();
|
||||||
@ -165,7 +167,7 @@ function ExecutionsDropdown(props) {
|
|||||||
const onClick = useCallback(
|
const onClick = useCallback(
|
||||||
({ key }) => {
|
({ key }) => {
|
||||||
if (key != execution.id) {
|
if (key != execution.id) {
|
||||||
navigate(`/admin/settings/workflow/executions/${key}`);
|
navigate(getWorkflowExecutionsPath(key));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[execution],
|
[execution],
|
||||||
@ -208,6 +210,7 @@ export function ExecutionCanvas() {
|
|||||||
const { data, loading } = useResourceActionContext();
|
const { data, loading } = useResourceActionContext();
|
||||||
const { setTitle } = useDocumentTitle();
|
const { setTitle } = useDocumentTitle();
|
||||||
const [viewJob, setViewJob] = useState(null);
|
const [viewJob, setViewJob] = useState(null);
|
||||||
|
const app = useApp();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { workflow } = data?.data ?? {};
|
const { workflow } = data?.data ?? {};
|
||||||
setTitle?.(`${workflow?.title ? `${workflow.title} - ` : ''}${lang('Execution history')}`);
|
setTitle?.(`${workflow?.title ? `${workflow.title} - ` : ''}${lang('Execution history')}`);
|
||||||
@ -244,8 +247,8 @@ export function ExecutionCanvas() {
|
|||||||
<header>
|
<header>
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
items={[
|
items={[
|
||||||
{ title: <Link to={`/admin/settings/workflow/workflows`}>{lang('Workflow')}</Link> },
|
{ title: <Link to={app.pluginSettingsManager.getRoutePath('workflow')}>{lang('Workflow')}</Link> },
|
||||||
{ title: <Link to={`/admin/settings/workflow/workflows/${workflow.id}`}>{workflow.title}</Link> },
|
{ title: <Link to={getWorkflowDetailPath(workflow.id)}>{workflow.title}</Link> },
|
||||||
{ title: <ExecutionsDropdown /> },
|
{ title: <ExecutionsDropdown /> },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
@ -3,13 +3,14 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { useActionContext, useRecord } from '@nocobase/client';
|
import { useActionContext, useRecord } from '@nocobase/client';
|
||||||
|
import { getWorkflowExecutionsPath } from './constant';
|
||||||
|
|
||||||
export const ExecutionLink = () => {
|
export const ExecutionLink = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { id } = useRecord();
|
const { id } = useRecord();
|
||||||
const { setVisible } = useActionContext();
|
const { setVisible } = useActionContext();
|
||||||
return (
|
return (
|
||||||
<Link to={`/admin/settings/workflow/executions/${id}`} onClick={() => setVisible(false)}>
|
<Link to={getWorkflowExecutionsPath(id)} onClick={() => setVisible(false)}>
|
||||||
{t('View')}
|
{t('View')}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
ResourceActionProvider,
|
ResourceActionProvider,
|
||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
cx,
|
cx,
|
||||||
|
useApp,
|
||||||
useDocumentTitle,
|
useDocumentTitle,
|
||||||
useResourceActionContext,
|
useResourceActionContext,
|
||||||
useResourceContext,
|
useResourceContext,
|
||||||
@ -21,6 +22,7 @@ import { lang } from './locale';
|
|||||||
import { executionSchema } from './schemas/executions';
|
import { executionSchema } from './schemas/executions';
|
||||||
import useStyles from './style';
|
import useStyles from './style';
|
||||||
import { linkNodes } from './utils';
|
import { linkNodes } from './utils';
|
||||||
|
import { getWorkflowDetailPath } from './constant';
|
||||||
|
|
||||||
function ExecutionResourceProvider({ request, filter = {}, ...others }) {
|
function ExecutionResourceProvider({ request, filter = {}, ...others }) {
|
||||||
const { workflow } = useFlowContext();
|
const { workflow } = useFlowContext();
|
||||||
@ -44,6 +46,7 @@ function ExecutionResourceProvider({ request, filter = {}, ...others }) {
|
|||||||
export function WorkflowCanvas() {
|
export function WorkflowCanvas() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const app = useApp();
|
||||||
const { data, refresh, loading } = useResourceActionContext();
|
const { data, refresh, loading } = useResourceActionContext();
|
||||||
const { resource } = useResourceContext();
|
const { resource } = useResourceContext();
|
||||||
const { setTitle } = useDocumentTitle();
|
const { setTitle } = useDocumentTitle();
|
||||||
@ -67,7 +70,7 @@ export function WorkflowCanvas() {
|
|||||||
|
|
||||||
function onSwitchVersion({ key }) {
|
function onSwitchVersion({ key }) {
|
||||||
if (key != workflow.id) {
|
if (key != workflow.id) {
|
||||||
navigate(`/admin/settings/workflow/workflows/${key}`);
|
navigate(getWorkflowDetailPath(key));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +95,7 @@ export function WorkflowCanvas() {
|
|||||||
});
|
});
|
||||||
message.success(t('Operation succeeded'));
|
message.success(t('Operation succeeded'));
|
||||||
|
|
||||||
navigate(`/admin/settings/workflow/workflows/${revision.id}`);
|
navigate(`/admin/workflow/workflows/${revision.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDelete() {
|
async function onDelete() {
|
||||||
@ -110,8 +113,8 @@ export function WorkflowCanvas() {
|
|||||||
|
|
||||||
navigate(
|
navigate(
|
||||||
workflow.current
|
workflow.current
|
||||||
? '/admin/settings/workflow/workflows'
|
? app.pluginSettingsManager.getRoutePath('workflow')
|
||||||
: `/admin/settings/workflow/workflows/${revisions.find((item) => item.current)?.id}`,
|
: getWorkflowDetailPath(revisions.find((item) => item.current)?.id),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -147,7 +150,7 @@ export function WorkflowCanvas() {
|
|||||||
<header>
|
<header>
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
items={[
|
items={[
|
||||||
{ title: <Link to={`/admin/settings/workflow/workflows`}>{lang('Workflow')}</Link> },
|
{ title: <Link to={app.pluginSettingsManager.getRoutePath('workflow')}>{lang('Workflow')}</Link> },
|
||||||
{ title: <strong>{workflow.title}</strong> },
|
{ title: <strong>{workflow.title}</strong> },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { getWorkflowDetailPath } from './constant';
|
||||||
import { useActionContext, useGetAriaLabelOfAction, useRecord } from '@nocobase/client';
|
import { useActionContext, useGetAriaLabelOfAction, useRecord } from '@nocobase/client';
|
||||||
|
|
||||||
export const WorkflowLink = () => {
|
export const WorkflowLink = () => {
|
||||||
@ -11,7 +12,7 @@ export const WorkflowLink = () => {
|
|||||||
const { getAriaLabel } = useGetAriaLabelOfAction('Configure');
|
const { getAriaLabel } = useGetAriaLabelOfAction('Configure');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link aria-label={getAriaLabel()} to={`/admin/settings/workflow/workflows/${id}`} onClick={() => setVisible(false)}>
|
<Link aria-label={getAriaLabel()} to={getWorkflowDetailPath(id)} onClick={() => setVisible(false)}>
|
||||||
{t('Configure')}
|
{t('Configure')}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,6 @@ import {
|
|||||||
PluginManagerContext,
|
PluginManagerContext,
|
||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
SchemaComponentContext,
|
SchemaComponentContext,
|
||||||
SettingsCenterProvider,
|
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { Card } from 'antd';
|
import { Card } from 'antd';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
@ -12,7 +11,6 @@ import { ExecutionResourceProvider } from './ExecutionResourceProvider';
|
|||||||
import { WorkflowLink } from './WorkflowLink';
|
import { WorkflowLink } from './WorkflowLink';
|
||||||
import OpenDrawer from './components/OpenDrawer';
|
import OpenDrawer from './components/OpenDrawer';
|
||||||
import expressionField from './interfaces/expression';
|
import expressionField from './interfaces/expression';
|
||||||
import { lang } from './locale';
|
|
||||||
import { instructions } from './nodes';
|
import { instructions } from './nodes';
|
||||||
import { workflowSchema } from './schemas/workflows';
|
import { workflowSchema } from './schemas/workflows';
|
||||||
import { getTriggersOptions, triggers } from './triggers';
|
import { getTriggersOptions, triggers } from './triggers';
|
||||||
@ -26,7 +24,7 @@ export function useWorkflowContext() {
|
|||||||
return useContext(WorkflowContext);
|
return useContext(WorkflowContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkflowPane() {
|
export function WorkflowPane() {
|
||||||
const ctx = useContext(SchemaComponentContext);
|
const ctx = useContext(SchemaComponentContext);
|
||||||
return (
|
return (
|
||||||
<Card bordered={false}>
|
<Card bordered={false}>
|
||||||
@ -53,41 +51,24 @@ export const WorkflowProvider = (props) => {
|
|||||||
const pmCtx = useContext(PluginManagerContext);
|
const pmCtx = useContext(PluginManagerContext);
|
||||||
const cmCtx = useContext(CollectionManagerContext);
|
const cmCtx = useContext(CollectionManagerContext);
|
||||||
return (
|
return (
|
||||||
<SettingsCenterProvider
|
<PluginManagerContext.Provider
|
||||||
settings={{
|
value={{
|
||||||
workflow: {
|
components: {
|
||||||
icon: 'PartitionOutlined',
|
...pmCtx?.components,
|
||||||
// title: `{{t("Workflow", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
title: lang('Workflow'),
|
|
||||||
tabs: {
|
|
||||||
workflows: {
|
|
||||||
isBookmark: true,
|
|
||||||
title: lang('Workflow'),
|
|
||||||
component: WorkflowPane,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PluginManagerContext.Provider
|
<CollectionManagerContext.Provider
|
||||||
value={{
|
value={{
|
||||||
components: {
|
...cmCtx,
|
||||||
...pmCtx?.components,
|
interfaces: {
|
||||||
|
...cmCtx.interfaces,
|
||||||
|
expression: expressionField,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CollectionManagerContext.Provider
|
<WorkflowContext.Provider value={{ triggers, instructions }}>{props.children}</WorkflowContext.Provider>
|
||||||
value={{
|
</CollectionManagerContext.Provider>
|
||||||
...cmCtx,
|
</PluginManagerContext.Provider>
|
||||||
interfaces: {
|
|
||||||
...cmCtx.interfaces,
|
|
||||||
expression: expressionField,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<WorkflowContext.Provider value={{ triggers, instructions }}>{props.children}</WorkflowContext.Provider>
|
|
||||||
</CollectionManagerContext.Provider>
|
|
||||||
</PluginManagerContext.Provider>
|
|
||||||
</SettingsCenterProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -12,7 +12,7 @@ test.describe('workflow manage', () => {
|
|||||||
// 1、前置条件:已登录
|
// 1、前置条件:已登录
|
||||||
|
|
||||||
// 2、测试步骤:进入“工作流管理”-“新建”按钮,填写表单,点击“确定”按钮
|
// 2、测试步骤:进入“工作流管理”-“新建”按钮,填写表单,点击“确定”按钮
|
||||||
await page.goto('/admin/settings/workflow/workflows');
|
await page.goto('/admin/settings/workflow');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
await page.getByLabel('action-Action-Add new-workflows').click();
|
await page.getByLabel('action-Action-Add new-workflows').click();
|
||||||
const createWorkFlow = new CreateWorkFlow(page);
|
const createWorkFlow = new CreateWorkFlow(page);
|
||||||
|
@ -10,7 +10,7 @@ test.describe('workflow manage', () => {
|
|||||||
const caseTitle = 'edit from event name';
|
const caseTitle = 'edit from event name';
|
||||||
|
|
||||||
// 1、前置条件:1.1、已登录,1.2、存在一个工作流
|
// 1、前置条件:1.1、已登录,1.2、存在一个工作流
|
||||||
await page.goto('/admin/settings/workflow/workflows');
|
await page.goto('/admin/settings/workflow');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
await page.getByLabel('action-Action-Add new-workflows').click();
|
await page.getByLabel('action-Action-Add new-workflows').click();
|
||||||
const createWorkFlow = new CreateWorkFlow(page);
|
const createWorkFlow = new CreateWorkFlow(page);
|
||||||
|
@ -24,7 +24,7 @@ test.describe('trigger collection events', () => {
|
|||||||
|
|
||||||
const newPage = mockPage(appendJsonCollectionName(e2e_GeneralFormsTable, appendText));
|
const newPage = mockPage(appendJsonCollectionName(e2e_GeneralFormsTable, appendText));
|
||||||
//配置工作流
|
//配置工作流
|
||||||
await page.goto('/admin/settings/workflow/workflows');
|
await page.goto('/admin/settings/workflow');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
await page.getByLabel('action-Action-Add new-workflows').click();
|
await page.getByLabel('action-Action-Add new-workflows').click();
|
||||||
const createWorkFlow = new CreateWorkFlow(page);
|
const createWorkFlow = new CreateWorkFlow(page);
|
||||||
@ -85,7 +85,7 @@ test.describe('trigger collection events', () => {
|
|||||||
|
|
||||||
// 3、预期结果:数据添加成功,工作流成功触发
|
// 3、预期结果:数据添加成功,工作流成功触发
|
||||||
await expect(page.getByText(fieldData)).toBeVisible();
|
await expect(page.getByText(fieldData)).toBeVisible();
|
||||||
await page.goto('/admin/settings/workflow/workflows');
|
await page.goto('/admin/settings/workflow');
|
||||||
await expect(page.getByRole('table').locator('a').filter({ hasText: '1' })).toBeVisible();
|
await expect(page.getByRole('table').locator('a').filter({ hasText: '1' })).toBeVisible();
|
||||||
|
|
||||||
// 4、后置处理:删除工作流
|
// 4、后置处理:删除工作流
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
export const getWorkflowDetailPath = (id: string | number) => `/admin/workflow/workflows/${id}`;
|
||||||
|
export const getWorkflowExecutionsPath = (id: string | number) => `/admin/workflow/executions/${id}`;
|
@ -10,13 +10,15 @@ import { Plugin } from '@nocobase/client';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ExecutionPage } from './ExecutionPage';
|
import { ExecutionPage } from './ExecutionPage';
|
||||||
import { WorkflowPage } from './WorkflowPage';
|
import { WorkflowPage } from './WorkflowPage';
|
||||||
import { WorkflowProvider } from './WorkflowProvider';
|
import { WorkflowPane, WorkflowProvider } from './WorkflowProvider';
|
||||||
import { DynamicExpression } from './components/DynamicExpression';
|
import { DynamicExpression } from './components/DynamicExpression';
|
||||||
import { triggers, useTrigger, getTriggersOptions } from './triggers';
|
import { triggers, useTrigger, getTriggersOptions } from './triggers';
|
||||||
import { instructions } from './nodes';
|
import { instructions } from './nodes';
|
||||||
import { WorkflowTodo } from './nodes/manual/WorkflowTodo';
|
import { WorkflowTodo } from './nodes/manual/WorkflowTodo';
|
||||||
import { WorkflowTodoBlockInitializer } from './nodes/manual/WorkflowTodoBlockInitializer';
|
import { WorkflowTodoBlockInitializer } from './nodes/manual/WorkflowTodoBlockInitializer';
|
||||||
import { useTriggerWorkflowsActionProps } from './triggers/form';
|
import { useTriggerWorkflowsActionProps } from './triggers/form';
|
||||||
|
import { NAMESPACE } from './locale';
|
||||||
|
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './constant';
|
||||||
|
|
||||||
export class WorkflowPlugin extends Plugin {
|
export class WorkflowPlugin extends Plugin {
|
||||||
triggers = triggers;
|
triggers = triggers;
|
||||||
@ -28,6 +30,12 @@ export class WorkflowPlugin extends Plugin {
|
|||||||
this.addScopes();
|
this.addScopes();
|
||||||
this.addComponents();
|
this.addComponents();
|
||||||
this.app.addProvider(WorkflowProvider);
|
this.app.addProvider(WorkflowProvider);
|
||||||
|
this.app.pluginSettingsManager.add(NAMESPACE, {
|
||||||
|
icon: 'PartitionOutlined',
|
||||||
|
title: `{{t("Workflow", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
Component: WorkflowPane,
|
||||||
|
aclSnippet: 'pm.workflow.workflows',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addScopes() {
|
addScopes() {
|
||||||
@ -47,12 +55,12 @@ export class WorkflowPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addRoutes() {
|
addRoutes() {
|
||||||
this.app.router.add('admin.settings.workflow.workflows.id', {
|
this.app.router.add('admin.workflow.workflows.id', {
|
||||||
path: '/admin/settings/workflow/workflows/:id',
|
path: getWorkflowDetailPath(':id'),
|
||||||
element: <WorkflowPage />,
|
element: <WorkflowPage />,
|
||||||
});
|
});
|
||||||
this.app.router.add('admin.settings.workflow.executions.id', {
|
this.app.router.add('admin.workflow.executions.id', {
|
||||||
path: '/admin/settings/workflow/executions/:id',
|
path: getWorkflowExecutionsPath(':id'),
|
||||||
element: <ExecutionPage />,
|
element: <ExecutionPage />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { ExecutionStatusOptions } from '../constants';
|
|||||||
import { NAMESPACE } from '../locale';
|
import { NAMESPACE } from '../locale';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
|
import { getWorkflowDetailPath } from '../constant';
|
||||||
|
|
||||||
export const executionCollection = {
|
export const executionCollection = {
|
||||||
name: 'executions',
|
name: 'executions',
|
||||||
@ -32,12 +33,7 @@ export const executionCollection = {
|
|||||||
title: `{{t("Version", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Version", { ns: "${NAMESPACE}" })}}`,
|
||||||
['x-component']({ value }) {
|
['x-component']({ value }) {
|
||||||
const { setVisible } = useActionContext();
|
const { setVisible } = useActionContext();
|
||||||
return (
|
return <Link to={getWorkflowDetailPath(value)} onClick={() => setVisible(false)}>{`#${value}`}</Link>;
|
||||||
<Link
|
|
||||||
to={`/admin/settings/workflow/workflows/${value}`}
|
|
||||||
onClick={() => setVisible(false)}
|
|
||||||
>{`#${value}`}</Link>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
} as ISchema,
|
} as ISchema,
|
||||||
},
|
},
|
||||||
|
@ -24,7 +24,15 @@
|
|||||||
"module": "commonjs"
|
"module": "commonjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["packages/**/*", ".dumi/**/*", ".dumirc.ts", "scripts/*", "playwright.config.ts", "vitest.config.ts"],
|
"include": [
|
||||||
|
"packages/**/*",
|
||||||
|
".dumi/**/*",
|
||||||
|
".dumirc.ts",
|
||||||
|
"scripts/*",
|
||||||
|
"playwright.config.ts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"jest.setupAfterEnv.ts"
|
||||||
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"packages/**/node_modules",
|
"packages/**/node_modules",
|
||||||
"packages/**/dist",
|
"packages/**/dist",
|
||||||
|
Loading…
Reference in New Issue
Block a user