diff --git a/README.md b/README.md
index 1c9bdb042..6fa45117d 100644
--- a/README.md
+++ b/README.md
@@ -8,11 +8,11 @@ NocoBase is in early stage of development and is subject to frequent changes, pl
## 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.13: New application status flow - 2023/08/24](https://docs.nocobase.com/welcome/release/v13-changelog)
-- [v0.12: New plugin build tool - 2023/08/01](https://docs.nocobase.com/welcome/release/v12-changelog)
-- [v0.11: New client application, plugin and router - 2023/07/08](http://docs.nocobase.com/welcome/release/v11-changelog)
-- [v0.10: Update instructions - 2023/06/23](http://docs.nocobase.com/welcome/release/v10-changelog)
+- [v0.15: New plugin settings manager - 2023/11/13](https://blog.nocobase.com/posts/release-v015/)
+- [v0.14: New plugin manager, supports adding plugins through UI - 2023/09/11](https://blog.nocobase.com/posts/release-v014/)
+- [v0.13: New application status flow - 2023/08/24](https://blog.nocobase.com/posts/release-v013/)
+- [v0.12: New plugin build tool - 2023/08/01](https://blog.nocobase.com/posts/release-v012/)
+- [v0.11: New client application, plugin and router - 2023/07/08](https://blog.nocobase.com/posts/release-v011/)
## What is NocoBase
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 8669299ac..9ed5a7077 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -8,11 +8,11 @@ NocoBase 正处在早期开发阶段,可能变动频繁,请谨慎用于生
## 最近重要更新
-- [v0.14:全新的插件管理器,支持通过界面添加插件 - 2023/09/11](https://docs-cn.nocobase.com/welcome/release/v14-changelog)
-- [v0.13: 全新的应用状态流转 - 2023/08/24](https://docs-cn.nocobase.com/welcome/release/v13-changelog)
-- [v0.12: 全新的插件构建工具 - 2023/08/01](https://docs-cn.nocobase.com/welcome/release/v12-changelog)
-- [v0.11: 全新的客户端 Application、Plugin 和 Router - 2023/07/08](https://docs-cn.nocobase.com/welcome/release/v11-changelog)
-- [v0.10: Update instructions - 2023/06/23](https://docs-cn.nocobase.com/welcome/release/v10-changelog)
+- [v0.15:全新的插件设置中心 - 2023/11/13](https://blog-cn.nocobase.com/posts/release-v015/)
+- [v0.14:全新的插件管理器,支持通过界面添加插件 - 2023/09/11](https://blog-cn.nocobase.com/posts/release-v014/)
+- [v0.13: 全新的应用状态流转 - 2023/08/24](https://blog-cn.nocobase.com/posts/release-v013/)
+- [v0.12: 全新的插件构建工具 - 2023/08/01](https://blog-cn.nocobase.com/posts/release-v012/)
+- [v0.11: 全新的客户端 Application、Plugin 和 Router - 2023/07/08](https://blog-cn.nocobase.com/posts/release-v011/)
## NocoBase 是什么
diff --git a/docs/config.ts b/docs/config.ts
index 287d7ab14..07b6e2ae0 100644
--- a/docs/config.ts
+++ b/docs/config.ts
@@ -200,7 +200,7 @@ const sidebar = {
],
},
'/development/client/ui-router',
- '/development/client/settings-center',
+ '/development/client/plugin-settings',
'/development/client/i18n',
'/development/client/test',
],
diff --git a/docs/en-US/development/client/plugin-settings.md b/docs/en-US/development/client/plugin-settings.md
new file mode 100644
index 000000000..f938bbec3
--- /dev/null
+++ b/docs/en-US/development/client/plugin-settings.md
@@ -0,0 +1,75 @@
+# Plugin Settings Manager
+
+
+
+## Example
+
+### Basic Usage
+
+```tsx | pure
+import { Plugin } from '@nocobase/client';
+import React from 'react';
+
+const HelloSettingPage = () =>
Hello Setting page
;
+
+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 = () => This
public part, the following is the outlet of the sub-route:
;
+
+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: () => Demo1 Page Content
+ })
+
+ this.app.pluginSettingsManager.add('hello.demo2', {
+ title: 'Demo2 Page',
+ Component: () => Demo2 Page Content
+ })
+ }
+}
+```
+
+### 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.
diff --git a/docs/en-US/development/client/settings-center/settings-tab.jpg b/docs/en-US/development/client/plugin-settings/settings-tab.jpg
similarity index 100%
rename from docs/en-US/development/client/settings-center/settings-tab.jpg
rename to docs/en-US/development/client/plugin-settings/settings-tab.jpg
diff --git a/docs/en-US/development/client/settings-center.md b/docs/en-US/development/client/settings-center.md
deleted file mode 100644
index 848bb7dd7..000000000
--- a/docs/en-US/development/client/settings-center.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Settings Center
-
-
-
-## Example
-
-```tsx | pure
-import { SettingsCenterProvider } from '@nocobase/client';
-import React, { useContext } from 'react';
-
-const HelloTab => () => Hello Tab
;
-
-export default React.memo((props) => {
- return (
- {props.children}
- );
-});
-```
-
-See [samples/hello](https://github.com/nocobase/nocobase/tree/develop/packages/samples/hello) for full examples.
diff --git a/docs/en-US/development/index.md b/docs/en-US/development/index.md
index 5e32b1139..fc9639c95 100644
--- a/docs/en-US/development/index.md
+++ b/docs/en-US/development/index.md
@@ -18,7 +18,7 @@ No-code Users can manage the activation and deactivation of local plugins throug
-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
# Create the plugin
@@ -47,7 +47,7 @@ Whether it is generic functionality or personalization, it is recommended to wri
Distribution of modules.
- 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
- Middleware: Middleware
- Events: Events
@@ -57,5 +57,5 @@ Distribution of modules.
- Client
- UI Schema Designer: Page Designer
- 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
diff --git a/docs/en-US/development/learning-guide.md b/docs/en-US/development/learning-guide.md
index cc5744757..115402259 100644
--- a/docs/en-US/development/learning-guide.md
+++ b/docs/en-US/development/learning-guide.md
@@ -76,7 +76,7 @@ yarn pm remove hello
- Client
- UI Schema Designer:页面设计器
- UI Router:有自定义页面需求时
- - Settings Center:为插件提供配置页面
+ - Plugin Settings Manager:为插件提供配置页面
- I18n:客户端国际化
- Devtools
- Commands:自定义命令行
@@ -113,8 +113,6 @@ yarn pm remove hello
- UI Router
- RouteSwitchProvider
- RouteSwitch
- - Settings Center
- - SettingsCenterProvider
- I18n
- app.i18n
- useTranslation
diff --git a/docs/en-US/welcome/release/v08-changelog.md b/docs/en-US/welcome/release/v08-changelog.md
index 24fc16581..1d302a59a 100644
--- a/docs/en-US/welcome/release/v08-changelog.md
+++ b/docs/en-US/welcome/release/v08-changelog.md
@@ -6,7 +6,7 @@ Starting with v0.8, NocoBase begins to provide an available plugin manager and d
- UI Editor
- Plugin Manager
-- Settings Center
+- Plugin Settings Manager
- Personal Center
diff --git a/docs/zh-CN/development/client/plugin-settings.md b/docs/zh-CN/development/client/plugin-settings.md
new file mode 100644
index 000000000..73dc392a9
--- /dev/null
+++ b/docs/zh-CN/development/client/plugin-settings.md
@@ -0,0 +1,75 @@
+# 配置中心
+
+
+
+## 示例
+
+### 基础用法
+
+```tsx | pure
+import { Plugin } from '@nocobase/client';
+import React from 'react';
+
+const HelloSettingPage = () => Hello Setting page
;
+
+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 = () => ;
+
+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: () => Demo1 Page Content
+ })
+
+ this.app.pluginSettingsManager.add('hello.demo2', {
+ title: 'Demo2 Page',
+ Component: () => Demo2 Page Content
+ })
+ }
+}
+```
+
+### 获取路由路径
+
+
+如果想获取设置页面的跳转链接,可以通过 `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)。
diff --git a/docs/zh-CN/development/client/settings-center/settings-tab.jpg b/docs/zh-CN/development/client/plugin-settings/settings-tab.jpg
similarity index 100%
rename from docs/zh-CN/development/client/settings-center/settings-tab.jpg
rename to docs/zh-CN/development/client/plugin-settings/settings-tab.jpg
diff --git a/docs/zh-CN/development/client/settings-center.md b/docs/zh-CN/development/client/settings-center.md
deleted file mode 100644
index 7639b793e..000000000
--- a/docs/zh-CN/development/client/settings-center.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# 配置中心
-
-
-
-## 示例
-
-```tsx | pure
-import { SettingsCenterProvider } from '@nocobase/client';
-import React, { useContext } from 'react';
-
-const HelloTab => () => Hello Tab
;
-
-export default React.memo((props) => {
- return (
- {props.children}
- );
-});
-```
-
-完整示例查看 [samples/hello](https://github.com/nocobase/nocobase/tree/develop/packages/samples/hello)。
\ No newline at end of file
diff --git a/docs/zh-CN/development/index.md b/docs/zh-CN/development/index.md
index bda2bc154..72b0ba32e 100644
--- a/docs/zh-CN/development/index.md
+++ b/docs/zh-CN/development/index.md
@@ -57,6 +57,6 @@ yarn pm remove hello
- Client
- UI Schema Designer:页面设计器
- UI Router:有自定义页面需求时
- - Settings Center:为插件提供配置页面
+ - Plugin Settings Manager:为插件提供配置页面
- I18n:客户端国际化
diff --git a/docs/zh-CN/development/learning-guide.md b/docs/zh-CN/development/learning-guide.md
index cc5744757..115402259 100644
--- a/docs/zh-CN/development/learning-guide.md
+++ b/docs/zh-CN/development/learning-guide.md
@@ -76,7 +76,7 @@ yarn pm remove hello
- Client
- UI Schema Designer:页面设计器
- UI Router:有自定义页面需求时
- - Settings Center:为插件提供配置页面
+ - Plugin Settings Manager:为插件提供配置页面
- I18n:客户端国际化
- Devtools
- Commands:自定义命令行
@@ -113,8 +113,6 @@ yarn pm remove hello
- UI Router
- RouteSwitchProvider
- RouteSwitch
- - Settings Center
- - SettingsCenterProvider
- I18n
- app.i18n
- useTranslation
diff --git a/packages/core/client/.dumirc.ts b/packages/core/client/.dumirc.ts
index 7be56e346..6821e2b94 100644
--- a/packages/core/client/.dumirc.ts
+++ b/packages/core/client/.dumirc.ts
@@ -46,7 +46,7 @@ export default defineConfig({
link: '/apis/api-client',
},
{
- title: 'SettingsCenter',
+ title: 'PluginSettingsManager',
link: '#',
},
],
diff --git a/packages/core/client/src/__tests__/e2e/block.test.ts b/packages/core/client/src/__tests__/e2e/block.test.ts
index 28abbae63..9bdd6f65f 100644
--- a/packages/core/client/src/__tests__/e2e/block.test.ts
+++ b/packages/core/client/src/__tests__/e2e/block.test.ts
@@ -552,8 +552,8 @@ test.describe('blcok template', () => {
await page.locator('.ant-drawer-mask').click();
//删除模板
- await page.getByTestId('settings-center-button').click();
- await page.getByRole('button', { name: 'All plugin settings' }).click();
+ await page.getByTestId('plugin-settings-button').click();
+ await page.getByLabel('ui-schema-storage').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.getByRole('button', { name: 'OK' }).click();
diff --git a/packages/core/client/src/__tests__/e2e/pm.test.ts b/packages/core/client/src/__tests__/e2e/pm.test.ts
index 8e82ebedc..463492bd9 100644
--- a/packages/core/client/src/__tests__/e2e/pm.test.ts
+++ b/packages/core/client/src/__tests__/e2e/pm.test.ts
@@ -1,6 +1,7 @@
import { expect, test } from '@nocobase/test/client';
async function waitForModalToBeHidden(page) {
+ test.slow();
await page.waitForFunction(() => {
const modal = document.querySelector('.ant-modal');
if (modal) {
@@ -57,11 +58,12 @@ test.describe('remove plugin', () => {
await page.getByPlaceholder('Search plugin').fill('Hello');
await expect(page.getByLabel('Hello')).toBeVisible();
const isActive = await page.getByLabel('Hello').getByLabel('enable').isChecked();
- await expect(isActive).toBe(false);
+ expect(isActive).toBe(false);
//将hello插件remove
await page.getByLabel('Hello').getByText('Remove').click();
await page.getByRole('button', { name: 'Yes' }).click();
//等待页面刷新结束
+ await waitForModalToBeHidden(page);
await page.waitForLoadState('load');
await page.getByPlaceholder('Search plugin').fill('hello');
await expect(page.getByLabel('Hello')).not.toBeVisible();
@@ -102,7 +104,7 @@ test.describe('enable & disabled plugin', () => {
await expect(page.getByLabel('Hello')).toBeVisible();
const isActive = await page.getByLabel('Hello').getByLabel('enable').isChecked();
expect(isActive).toBe(false);
- //激活插件
+ // 激活插件
await page.getByLabel('Hello').getByLabel('enable').click();
await page.waitForTimeout(1000); // 等待1秒钟
//等待弹窗消失和页面刷新结束
diff --git a/packages/core/client/src/acl/ACLProvider.tsx b/packages/core/client/src/acl/ACLProvider.tsx
index 64f2318f8..c8f7b69df 100644
--- a/packages/core/client/src/acl/ACLProvider.tsx
+++ b/packages/core/client/src/acl/ACLProvider.tsx
@@ -9,6 +9,7 @@ import { useCollection, useCollectionManager } from '../collection-manager';
import { useResourceActionContext } from '../collection-manager/ResourceActionProvider';
import { useRecord } from '../record-provider';
import { SchemaComponentOptions, useDesignable } from '../schema-component';
+import { useApp } from '../application';
export const ACLContext = createContext({});
@@ -35,6 +36,7 @@ export const ACLRolesCheckProvider = (props) => {
const { setDesignable } = useDesignable();
const { render } = useAppSpin();
const api = useAPIClient();
+ const app = useApp();
const result = useRequest<{
data: {
snippets: string[];
@@ -57,6 +59,7 @@ export const ACLRolesCheckProvider = (props) => {
if (data?.data?.role !== api.auth.role) {
api.auth.setRole(data?.data?.role);
}
+ app.pluginSettingsManager.setAclSnippets(data?.data?.snippets || []);
},
},
);
diff --git a/packages/core/client/src/acl/Configuration/ConfigureCenter.tsx b/packages/core/client/src/acl/Configuration/ConfigureCenter.tsx
index e22392e8b..d2f3769a8 100644
--- a/packages/core/client/src/acl/Configuration/ConfigureCenter.tsx
+++ b/packages/core/client/src/acl/Configuration/ConfigureCenter.tsx
@@ -1,11 +1,12 @@
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 { useAPIClient, useRequest } from '../../api-client';
import { SettingsCenterContext } from '../../pm';
import { useRecord } from '../../record-provider';
-import { useCompile } from '../../schema-component';
import { useStyles } from '../style';
+import { useApp } from '../../application';
+import { useCompile } from '../../schema-component';
const getParentKeys = (tree, func, path = []) => {
if (!tree) return [];
@@ -22,7 +23,7 @@ const getParentKeys = (tree, func, path = []) => {
};
const getChildrenKeys = (data = [], arr = []) => {
for (const item of data) {
- arr.push(item.key);
+ arr.push(item.aclSnippet);
if (item.children && item.children.length) getChildrenKeys(item.children, arr);
}
return arr;
@@ -35,62 +36,40 @@ export const SettingCenterProvider = (props) => {
return {props.children} ;
};
-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 = () => {
+ const app = useApp();
const { styles } = useStyles();
const record = useRecord();
const api = useAPIClient();
- const pluginTags = useContext(SettingMenuContext);
- const items: any[] = (pluginTags && formatPluginTabs(pluginTags)) || [];
- const { t } = useTranslation();
const compile = useCompile();
- const { loading, refresh, data } = useRequest<{
- data: any;
- }>({
- resource: 'roles.snippets',
- resourceOf: record.name,
- action: 'list',
- params: {
- paginate: false,
+ const settings = app.pluginSettingsManager.getList(false);
+ const allAclSnippets = app.pluginSettingsManager.getAclSnippets();
+ const [snippets, setSnippets] = useState([]);
+ const allChecked = useMemo(
+ () => snippets.includes('pm.*') && snippets.every((item) => !item.startsWith('!pm.')),
+ [snippets],
+ );
+
+ 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 handleChange = async (checked, record) => {
const childrenKeys = getChildrenKeys(record?.children, []);
- const totalKeys = childrenKeys.concat(record.key);
+ const totalKeys = childrenKeys.concat(record.aclSnippet);
if (!checked) {
await resource.remove({
values: totalKeys.map((v) => '!' + v),
@@ -104,40 +83,54 @@ export const SettingsCenterConfigure = () => {
}
message.success(t('Saved successfully'));
};
-
return (
- items?.length && (
- {
- return compile(value);
- },
+ {
+ return compile(value);
},
- {
- dataIndex: 'pluginTitle',
- title: t('Plugin name'),
- render: (value) => {
- return compile(value);
- },
+ },
+ {
+ dataIndex: 'accessible',
+ title: (
+ <>
+ {
+ 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 handleChange(checked, record)} />;
},
- {
- dataIndex: 'accessible',
- title: t('Accessible'),
- render: (_, record) => {
- const checked = !data?.data?.includes('!' + record.key);
- return !record.children && handleChange(checked, record)} />;
- },
- },
- ]}
- dataSource={items}
- />
- )
+ },
+ ]}
+ dataSource={settings}
+ />
);
};
diff --git a/packages/core/client/src/application/Application.tsx b/packages/core/client/src/application/Application.tsx
index 95b0d7ee3..4cdcb8632 100644
--- a/packages/core/client/src/application/Application.tsx
+++ b/packages/core/client/src/application/Application.tsx
@@ -8,18 +8,22 @@ import React, { ComponentType, FC, ReactElement } from 'react';
import { createRoot } from 'react-dom/client';
import { I18nextProvider } from 'react-i18next';
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 { ComponentTypeAndString, RouterManager, RouterOptions } from './RouterManager';
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 { compose, normalizeContainer } from './utils';
import { defineGlobalDeps } from './utils/globalDeps';
-import type { RequireJS } from './utils/requirejs';
import { getRequireJs } from './utils/requirejs';
+import type { RequireJS } from './utils/requirejs';
+import type { Plugin } from './Plugin';
+
declare global {
interface Window {
define: RequireJS['define'];
@@ -50,6 +54,7 @@ export class Application {
public apiClient: APIClient;
public components: Record = { ...defaultAppComponents };
public pm: PluginManager;
+ public pluginSettingsManager: PluginSettingsManager;
public devDynamicImport: DevDynamicImport;
public requirejs: RequireJS;
public notification;
@@ -57,6 +62,9 @@ export class Application {
maintained = false;
maintaining = false;
error = null;
+ get pluginManager() {
+ return this.pm;
+ }
constructor(protected options: ApplicationOptions = {}) {
this.initRequireJs();
@@ -81,6 +89,8 @@ export class Application {
this.addReactRouterComponents();
this.addProviders(options.providers || []);
this.ws = new WebSocketClient(options.ws);
+ this.pluginSettingsManager = new PluginSettingsManager(this);
+ this.addRoutes();
}
private initRequireJs() {
@@ -102,6 +112,13 @@ export class Application {
});
}
+ private addRoutes() {
+ this.router.add('not-found', {
+ path: '*',
+ Component: this.components['AppNotFound'] || BlankComponent,
+ });
+ }
+
getComposeProviders() {
const Providers = compose(...this.providers)(BlankComponent);
Providers.displayName = 'Providers';
diff --git a/packages/core/client/src/application/PluginSettingsManager.ts b/packages/core/client/src/application/PluginSettingsManager.ts
new file mode 100644
index 000000000..ca8ecef88
--- /dev/null
+++ b/packages/core/client/src/application/PluginSettingsManager.ts
@@ -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 = {};
+ 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));
+ }
+}
diff --git a/packages/core/client/src/application/__tests__/Application.test.tsx b/packages/core/client/src/application/__tests__/Application.test.tsx
index ef34fec7b..4944ec9e1 100644
--- a/packages/core/client/src/application/__tests__/Application.test.tsx
+++ b/packages/core/client/src/application/__tests__/Application.test.tsx
@@ -16,7 +16,7 @@ describe('Application', () => {
});
const router: any = { type: 'memory', initialEntries: ['/'] };
- const initialComponentsLength = 6;
+ const initialComponentsLength = 7;
const initialProvidersLength = 2;
it('basic', () => {
const app = new Application({ router });
diff --git a/packages/core/client/src/application/__tests__/SettingsCenter.test.ts b/packages/core/client/src/application/__tests__/SettingsCenter.test.ts
new file mode 100644
index 000000000..bf6b5ede4
--- /dev/null
+++ b/packages/core/client/src/application/__tests__/SettingsCenter.test.ts
@@ -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": ,
+ "path": "*",
+ }
+ `);
+ });
+});
diff --git a/packages/core/client/src/application/components/defaultComponents.tsx b/packages/core/client/src/application/components/defaultComponents.tsx
index d6b8eaec0..38b1884f7 100644
--- a/packages/core/client/src/application/components/defaultComponents.tsx
+++ b/packages/core/client/src/application/components/defaultComponents.tsx
@@ -9,8 +9,11 @@ const AppError: FC<{ error: Error }> = ({ error }) => (
);
+const AppNotFound: FC = () => Not Found
;
+
export const defaultAppComponents = {
AppMain: MainComponent,
AppSpin: Loading,
AppError: AppError,
+ AppNotFound: AppNotFound,
};
diff --git a/packages/core/client/src/application/index.md b/packages/core/client/src/application/index.md
index b6677d9e3..af3e179be 100644
--- a/packages/core/client/src/application/index.md
+++ b/packages/core/client/src/application/index.md
@@ -27,6 +27,7 @@ Application 提供了强大的功能,包括:
- scopes 管理
- providers 管理
- 插件管理
+- 插件设置页面管理
### 组件管理
@@ -370,6 +371,76 @@ class MyPlugin extends Plugin {
}
```
+### 插件设置页面管理
+
+#### 基础用法
+
+```tsx | pure
+import { Plugin } from '@nocobase/client';
+import React from 'react';
+
+const HelloSettingPage = () => Hello Setting page
;
+
+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 = () => ;
+
+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: () => Demo1 Page Content
+ })
+
+ this.app.pluginSettingsManager.add('hello.demo2', {
+ title: 'Demo2 Page',
+ Component: () => Demo2 Page Content
+ })
+ }
+}
+```
+
+#### 获取路由路径
+
+
+如果想获取设置页面的跳转链接,可以通过 `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
diff --git a/packages/core/client/src/application/index.ts b/packages/core/client/src/application/index.ts
index b80152369..763f99006 100644
--- a/packages/core/client/src/application/index.ts
+++ b/packages/core/client/src/application/index.ts
@@ -3,3 +3,4 @@ export * from './hooks';
export * from './Plugin';
export * from './RouterManager';
export * from './utils';
+export * from './PluginSettingsManager';
diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts
index 868d8eee4..ec96d9778 100644
--- a/packages/core/client/src/locale/en_US.ts
+++ b/packages/core/client/src/locale/en_US.ts
@@ -38,6 +38,7 @@ export default {
"Unconnected": "Unconnected",
"System settings": "System settings",
"System title": "System title",
+ "Settings": "Settings",
"Logo": "Logo",
"Add menu item": "Add menu item",
"Page": "Page",
diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts
index f69229664..1ebdc3870 100644
--- a/packages/core/client/src/locale/zh_CN.ts
+++ b/packages/core/client/src/locale/zh_CN.ts
@@ -42,6 +42,7 @@ export default {
'System settings': '系统设置',
'System title': '系统名称',
Setting: '设置',
+ Settings: '设置',
Enable: '启用',
Disable: '禁用',
On: '启用',
diff --git a/packages/core/client/src/nocobase-buildin-plugin/index.tsx b/packages/core/client/src/nocobase-buildin-plugin/index.tsx
index d1b6cd682..99cfbc831 100644
--- a/packages/core/client/src/nocobase-buildin-plugin/index.tsx
+++ b/packages/core/client/src/nocobase-buildin-plugin/index.tsx
@@ -3,7 +3,7 @@ import { css } from '@emotion/css';
import { observer } from '@formily/reactive-react';
import { Button, Modal, Result, Spin } from 'antd';
import React, { FC } from 'react';
-import { Navigate } from 'react-router-dom';
+import { Navigate, useNavigate } from 'react-router-dom';
import { ACLPlugin } from '../acl';
import { Application } from '../application';
import { Plugin } from '../application/Plugin';
@@ -188,6 +188,22 @@ const AppMaintainingDialog: FC<{ app: Application; error: Error }> = observer(({
);
});
+const AppNotFound = () => {
+ const navigate = useNavigate();
+ return (
+ navigate('/', { replace: true })} type="primary">
+ Back Home
+
+ }
+ />
+ );
+};
+
export class NocoBaseBuildInPlugin extends Plugin {
async afterAdd() {
this.app.addComponents({
@@ -195,6 +211,7 @@ export class NocoBaseBuildInPlugin extends Plugin {
AppError,
AppMaintaining,
AppMaintainingDialog,
+ AppNotFound,
});
await this.addPlugins();
}
@@ -216,6 +233,11 @@ export class NocoBaseBuildInPlugin extends Plugin {
element: ,
});
+ this.router.add('not-found', {
+ path: '*',
+ Component: AppNotFound,
+ });
+
this.router.add('admin', {
path: '/admin',
Component: 'AdminLayout',
diff --git a/packages/core/client/src/pm/PluginCard.tsx b/packages/core/client/src/pm/PluginCard.tsx
index 34f33ec64..5a97f7533 100644
--- a/packages/core/client/src/pm/PluginCard.tsx
+++ b/packages/core/client/src/pm/PluginCard.tsx
@@ -7,6 +7,7 @@ import { useNavigate } from 'react-router-dom';
import { DeleteOutlined, ReadOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import { useAPIClient } from '../api-client';
+import { useApp } from '../application';
import { PluginDetail } from './PluginDetail';
import { PluginUpgradeModal } from './PluginForm/modal/PluginUpgradeModal';
import { useStyles } from './style';
@@ -18,6 +19,7 @@ interface IPluginInfo extends IPluginCard {
function PluginInfo(props: IPluginInfo) {
const { data, onClick } = props;
+ const app = useApp();
const { name, displayName, isCompatible, packageName, updatable, builtIn, enabled, description, type, error } = data;
const { styles, theme } = useStyles();
const navigate = useNavigate();
@@ -28,7 +30,6 @@ function PluginInfo(props: IPluginInfo) {
const [enabledVal, setEnabledVal] = useState(enabled);
const reload = () => window.location.reload();
const title = displayName || name || packageName;
-
return (
<>
{showUploadForm && (
@@ -92,14 +93,16 @@ function PluginInfo(props: IPluginInfo) {
)}
{enabled ? (
- {
- e.stopPropagation();
- navigate(`/admin/settings/${name}`);
- }}
- >
- {t('Setting')}
-
+ app.pluginSettingsManager.has(name) && (
+ {
+ e.stopPropagation();
+ navigate(app.pluginSettingsManager.getRoutePath(name));
+ }}
+ >
+ {t('Settings')}
+
+ )
) : (
{
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 = () => {
- const { snippets = [] } = useACLRoleContext();
const [visible, setVisible] = useState(false);
- const { t } = useTranslation();
const compile = useCompile();
- const navigate = useNavigate();
- const itemData = useContext(SettingsCenterContext);
+ const { t } = useTranslation();
const { token } = useToken();
- const pluginsTabs = getPluginsTabs(itemData, snippets);
- const bookmarkTabs = getBookmarkTabs(pluginsTabs);
- const menu = useMemo(() => {
- return {
- 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]);
+ const navigate = useNavigate();
+ const app = useApp();
+ const settings = app.pluginSettingsManager.getList();
+ const [open, setOpen] = useState(false);
return (
-
+ {
+ setOpen(open);
+ }}
+ arrow={false}
+ content={
+
+ }
+ >
}
// title={t('All plugin settings')}
/>
-
+
);
};
diff --git a/packages/core/client/src/pm/PluginSetting.tsx b/packages/core/client/src/pm/PluginSetting.tsx
index 29bb6e90c..6a306d64b 100644
--- a/packages/core/client/src/pm/PluginSetting.tsx
+++ b/packages/core/client/src/pm/PluginSetting.tsx
@@ -1,135 +1,97 @@
-export * from './PluginManagerLink';
import { PageHeader } from '@ant-design/pro-layout';
import { css } from '@emotion/css';
-import { Layout, Menu, Result, Tabs } from 'antd';
-import _, { sortBy } from 'lodash';
-import React, { createContext, useContext, useMemo } from 'react';
-import { Navigate, useNavigate, useParams } 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 { Layout, Menu, Result } from 'antd';
+import _, { get } from 'lodash';
+import React, { createContext, useCallback, useMemo } from 'react';
+import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { useStyles } from './style';
+import { ADMIN_SETTINGS_PATH, PluginSettingsPageType, useApp } from '../application';
+import { useCompile } from '../schema-component';
export const SettingsCenterContext = createContext({});
-export const settings = {
- acl: {
- title: '{{t("ACL")}}',
- icon: 'LockOutlined',
- tabs: {
- roles: {
- isBookmark: true,
- title: '{{t("Roles & Permissions")}}',
- component: () => ,
- },
- },
- },
- '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,
- );
+function getMenuItems(list: PluginSettingsPageType[]) {
+ return list.map((item) => {
return {
- ...items[plugin],
- key: plugin,
- tabs,
- isAllow: !tabs.every((v) => !v.isAllow),
+ key: item.name,
+ label: item.label,
+ title: item.title,
+ icon: item.icon,
+ children: item.children?.length ? getMenuItems(item.children) : undefined,
};
});
- return sortBy(pluginsTabs, (o) => !o.isAllow);
-});
+}
-export const SettingsCenter = () => {
- const { styles } = useStyles();
- const { snippets = [] } = useACLRoleContext();
- const params = useParams();
+export const SettingsCenterComponent = () => {
+ const { styles, theme } = useStyles();
+ const app = useApp();
const navigate = useNavigate();
- const items = useContext(SettingsCenterContext);
- const pluginsTabs = getPluginsTabs(items, snippets);
+ const location = useLocation();
const compile = useCompile();
- const firstUri = useMemo(() => {
- const pluginName = pluginsTabs[0].key;
- const tabName = pluginsTabs[0].tabs[0].key;
- return `/admin/settings/${pluginName}/${tabName}`;
- }, [pluginsTabs]);
- const { pluginName, tabName } = params;
- const activePlugin = pluginsTabs.find((v) => v.key === pluginName);
- const aclPluginTabCheck = activePlugin?.isAllow && activePlugin.tabs.find((v) => v.key === tabName)?.isAllow;
- if (!pluginName) {
- return ;
+ const settings = useMemo(() => {
+ const list = app.pluginSettingsManager.getList();
+ // compile title
+ function traverse(settings: PluginSettingsPageType[]) {
+ settings.forEach((item) => {
+ item.title = compile(item.title);
+ item.label = compile(item.title);
+ if (item.children?.length) {
+ traverse(item.children);
+ }
+ });
+ }
+ 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>(() => {
+ 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 ;
}
- if (!items[pluginName]) {
- return ;
+ if (location.pathname === currentPlugin.path && currentPlugin.children?.length > 0) {
+ return ;
}
- if (!tabName) {
- const firstTabName = Object.keys(items[pluginName]?.tabs).shift();
- return ;
- }
- 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 ? : null,
- };
- });
return (
{
theme={'light'}
>
{
- const item = items[e.key];
- const tabKey = Object.keys(item.tabs).shift();
- navigate(`/admin/settings/${e.key}/${tabKey}`);
+ onClick={({ key }) => {
+ const plugin = settings.find((item) => item.name === key);
+ if (plugin.children?.length) {
+ return navigate(getFirstDeepChildPath(plugin.children));
+ } else {
+ return navigate(plugin.path);
+ }
}}
- items={menuItems as any}
+ items={sidebarMenus}
/>
- {aclPluginTabCheck && (
+ {currentSetting && (
0 ? 0 : theme.paddingSM,
+ }}
ghost={false}
- title={compile(items[pluginName]?.title)}
+ title={currentPlugin.title}
footer={
- {
- navigate(`/admin/settings/${pluginName}/${activeKey}`);
- }}
- items={plugin.tabs?.map((tab) => {
- if (!tab.isAllow) {
- return null;
- }
- return {
- label: compile(tab?.title),
- key: tab.key,
- };
- })}
- />
+ currentPlugin.children?.length > 0 && (
+ {
+ navigate(app.pluginSettingsManager.getRoutePath(key));
+ }}
+ selectedKeys={[currentSetting?.name]}
+ mode="horizontal"
+ items={getMenuItems(currentPlugin.children)}
+ >
+ )
}
/>
)}
- {aclPluginTabCheck ? (
- component && React.createElement(component)
+ {currentSetting ? (
+
) : (
)}
@@ -191,15 +155,3 @@ export const SettingsCenter = () => {
);
};
-
-export const SettingsCenterProvider = (props) => {
- const { settings = {} } = props;
- const items = useContext(SettingsCenterContext);
- return (
- {props.children}
- );
-};
-
-export const PMProvider = (props) => {
- return {props.children} ;
-};
diff --git a/packages/core/client/src/pm/index.tsx b/packages/core/client/src/pm/index.tsx
index 58bc205e0..2991bf7fa 100644
--- a/packages/core/client/src/pm/index.tsx
+++ b/packages/core/client/src/pm/index.tsx
@@ -1,8 +1,14 @@
import React from 'react';
import { Plugin } from '../application/Plugin';
import { PluginManagerLink, SettingsCenterDropdown } from './PluginManagerLink';
-import { PMProvider, SettingsCenter } from './PluginSetting';
+import { SettingsCenterComponent } from './PluginSetting';
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 './PluginSetting';
@@ -12,7 +18,43 @@ export class PMPlugin extends Plugin {
async load() {
this.addComponents();
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: () => ,
+ isBookmark: true,
+ });
+
+ this.app.pluginSettingsManager.add('collection-manager.collections', {
+ title: '{{t("Collections & Fields")}}',
+ Component: CollectionManagerPane,
+ });
}
addComponents() {
@@ -36,17 +78,9 @@ export class PMPlugin extends Plugin {
element: ,
});
- this.app.router.add('admin.settings.list', {
- path: '/admin/settings',
- element: ,
- });
- this.app.router.add('admin.settings.pluginName', {
- path: '/admin/settings/:pluginName',
- element: ,
- });
- this.app.router.add('admin.settings.pluginName-tabName', {
- path: '/admin/settings/:pluginName/:tabName',
- element: ,
+ this.app.router.add('admin.settings', {
+ path: ADMIN_SETTINGS_PATH,
+ element: ,
});
}
}
diff --git a/packages/core/client/src/pm/style.ts b/packages/core/client/src/pm/style.ts
index 5be73ecf7..f55405c05 100644
--- a/packages/core/client/src/pm/style.ts
+++ b/packages/core/client/src/pm/style.ts
@@ -7,9 +7,9 @@ export const useStyles = createStyles(({ token }) => {
cursor: 'not-allowed',
},
pageHeader: {
- paddingBottom: 0,
backgroundColor: token.colorBgContainer,
paddingTop: token.paddingSM,
+ paddingBottom: 0,
paddingInline: token.paddingLG,
'.ant-page-header-footer': { marginBlockStart: '0' },
'& .ant-tabs-nav': {
diff --git a/packages/core/client/src/schema-component/antd/color-select/ColorSelect.tsx b/packages/core/client/src/schema-component/antd/color-select/ColorSelect.tsx
index 02cd4f390..97cdb13fc 100644
--- a/packages/core/client/src/schema-component/antd/color-select/ColorSelect.tsx
+++ b/packages/core/client/src/schema-component/antd/color-select/ColorSelect.tsx
@@ -25,7 +25,7 @@ export const ColorSelect = connect(
return (
{Object.keys(colors).map((color) => (
-
+
{compile(colors[color] || colors.default)}
))}
diff --git a/packages/core/client/src/schema-component/common/sortable-item/SortableItem.tsx b/packages/core/client/src/schema-component/common/sortable-item/SortableItem.tsx
index 0f1133d29..589fa7af0 100644
--- a/packages/core/client/src/schema-component/common/sortable-item/SortableItem.tsx
+++ b/packages/core/client/src/schema-component/common/sortable-item/SortableItem.tsx
@@ -50,18 +50,18 @@ export const Sortable = (props: any) => {
const useSortableItemProps = (props) => {
const id = useSortableItemId(props);
+ const schema = useFieldSchema();
if (props.schema) {
return { ...props, id };
}
- const schema = useFieldSchema();
return { ...props, id, schema };
};
const useSortableItemId = (props) => {
+ const field = useField();
if (props.id) {
return props.id;
}
- const field = useField();
return field.address.toString();
};
diff --git a/packages/plugins/@nocobase/plugin-api-doc/src/client/index.tsx b/packages/plugins/@nocobase/plugin-api-doc/src/client/index.tsx
index 78c3d1e0b..80876f5d2 100644
--- a/packages/plugins/@nocobase/plugin-api-doc/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-api-doc/src/client/index.tsx
@@ -1,9 +1,9 @@
import { RightOutlined } from '@ant-design/icons';
-import { Plugin, SettingsCenterProvider } from '@nocobase/client';
+import { Plugin } from '@nocobase/client';
import { Button, Tooltip } from 'antd';
import { createStyles } from 'antd-style';
import React, { lazy } from 'react';
-import { useTranslation } from '../locale';
+import { NAMESPACE } from '../locale';
const DOCUMENTATION_PATH = '/api-documentation';
const Documentation = lazy(() => import('./Document'));
@@ -37,32 +37,15 @@ const SCDocumentation = () => {
);
};
-const APIDocumentationProvider = React.memo((props) => {
- const { t } = useTranslation();
- return (
-
- {props.children}
-
- );
-});
-APIDocumentationProvider.displayName = 'APIDocumentationProvider';
-
export class APIDocumentationPlugin extends Plugin {
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', {
path: DOCUMENTATION_PATH,
Component: Documentation,
diff --git a/packages/plugins/@nocobase/plugin-api-doc/src/locale/zh-CN.ts b/packages/plugins/@nocobase/plugin-api-doc/src/locale/zh-CN.ts
index 4e815a3b1..86c8ac0a1 100644
--- a/packages/plugins/@nocobase/plugin-api-doc/src/locale/zh-CN.ts
+++ b/packages/plugins/@nocobase/plugin-api-doc/src/locale/zh-CN.ts
@@ -1,5 +1,5 @@
export default {
- 'API documentation': 'Api 文档',
+ 'API documentation': 'API 文档',
'Documentation': '文档',
'Select a definition': '选择端点',
};
diff --git a/packages/plugins/@nocobase/plugin-api-keys/src/client/index.tsx b/packages/plugins/@nocobase/plugin-api-keys/src/client/index.tsx
index 47af650d9..910dc0db0 100644
--- a/packages/plugins/@nocobase/plugin-api-keys/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-api-keys/src/client/index.tsx
@@ -1,34 +1,15 @@
-import { Plugin, SchemaComponentOptions, SettingsCenterProvider } from '@nocobase/client';
-import React from 'react';
+import { Plugin } from '@nocobase/client';
+import { NAMESPACE } from '../constants';
import { Configuration } from './Configuration';
-import { useTranslation } from './locale';
-
-const ApiKeysProvider = React.memo((props) => {
- const { t } = useTranslation();
- return (
-
- {props.children}
-
- );
-});
-ApiKeysProvider.displayName = 'ApiKeysProvider';
class APIKeysPlugin extends Plugin {
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',
+ });
}
}
diff --git a/packages/plugins/@nocobase/plugin-api-keys/src/collections/api-keys.ts b/packages/plugins/@nocobase/plugin-api-keys/src/collections/api-keys.ts
index 6001071e0..5e155b58d 100644
--- a/packages/plugins/@nocobase/plugin-api-keys/src/collections/api-keys.ts
+++ b/packages/plugins/@nocobase/plugin-api-keys/src/collections/api-keys.ts
@@ -5,7 +5,7 @@ export default {
namespace: 'api-keys',
duplicator: 'optional',
name: 'apiKeys',
- title: '{{t("API keys")}}',
+ title: '{{t("API keys", {"ns": "api-keys"})}}',
sortable: 'sort',
model: 'ApiKeyModel',
createdBy: true,
diff --git a/packages/plugins/@nocobase/plugin-api-keys/src/locale/zh-CN.ts b/packages/plugins/@nocobase/plugin-api-keys/src/locale/zh-CN.ts
index e91c21165..ee5083ec7 100644
--- a/packages/plugins/@nocobase/plugin-api-keys/src/locale/zh-CN.ts
+++ b/packages/plugins/@nocobase/plugin-api-keys/src/locale/zh-CN.ts
@@ -1,14 +1,15 @@
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.':
'请确保现在复制你的个人访问密钥,因为你将无法再次看到这个密钥。',
'Key name': '密钥名称',
Expiration: '过期时间',
- 'Delete API key': '删除 API key',
+ 'Delete API key': '删除 API 密钥',
Role: '角色',
'Keys manager': '密钥管理',
'Created at': '创建时间',
- 'Add API key': '添加 API key',
+ 'Add API key': '添加 API 密钥',
Never: '永不',
Custom: '自定义',
'Never expires': '永不过期',
diff --git a/packages/plugins/@nocobase/plugin-auth/src/client/AuthPluginProvider.tsx b/packages/plugins/@nocobase/plugin-auth/src/client/AuthPluginProvider.tsx
index dbde36a81..be4fb0873 100644
--- a/packages/plugins/@nocobase/plugin-auth/src/client/AuthPluginProvider.tsx
+++ b/packages/plugins/@nocobase/plugin-auth/src/client/AuthPluginProvider.tsx
@@ -1,41 +1,20 @@
-import {
- OptionsComponentProvider,
- SettingsCenterProvider,
- SigninPageProvider,
- SignupPageProvider,
-} from '@nocobase/client';
+import { OptionsComponentProvider, SigninPageProvider, SignupPageProvider } from '@nocobase/client';
import React, { FC } from 'react';
-import { Authenticator } from './settings/Authenticator';
-import SigninPage from './basic/SigninPage';
import { presetAuthType } from '../preset';
+import { Options } from './basic/Options';
+import SigninPage from './basic/SigninPage';
import SignupPage from './basic/SignupPage';
import { useAuthTranslation } from './locale';
-import { Options } from './basic/Options';
export const AuthPluginProvider: FC = (props) => {
const { t } = useAuthTranslation();
return (
- ,
- },
- },
- },
- }}
- >
-
-
-
- {props.children}
-
-
-
-
+
+
+
+ {props.children}
+
+
+
);
};
diff --git a/packages/plugins/@nocobase/plugin-auth/src/client/index.tsx b/packages/plugins/@nocobase/plugin-auth/src/client/index.tsx
index 75d63ca73..c4989454e 100644
--- a/packages/plugins/@nocobase/plugin-auth/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-auth/src/client/index.tsx
@@ -1,9 +1,17 @@
import { Plugin } from '@nocobase/client';
import { AuthPluginProvider } from './AuthPluginProvider';
import { AuthProvider } from './AuthProvider';
+import { NAMESPACE } from './locale';
+import { Authenticator } from './settings/Authenticator';
export class AuthPlugin extends Plugin {
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.use(AuthPluginProvider);
}
diff --git a/packages/plugins/@nocobase/plugin-charts/src/client/index.tsx b/packages/plugins/@nocobase/plugin-charts/src/client/index.tsx
index 44c2ea9aa..4570d0a50 100644
--- a/packages/plugins/@nocobase/plugin-charts/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-charts/src/client/index.tsx
@@ -4,7 +4,6 @@ import {
Plugin,
SchemaComponentOptions,
SchemaInitializerContext,
- SettingsCenterProvider,
useAPIClient,
} from '@nocobase/client';
import JSON5 from 'json5';
@@ -13,7 +12,7 @@ import { ChartBlockEngine } from './ChartBlockEngine';
import { ChartBlockInitializer } from './ChartBlockInitializer';
import { ChartQueryMetadataProvider } from './ChartQueryMetadataProvider';
import './Icons';
-import { lang } from './locale';
+import { lang, NAMESPACE } from './locale';
import { CustomSelect } from './select';
import { QueriesTable } from './settings/QueriesTable';
@@ -53,7 +52,7 @@ const ChartsProvider = React.memo((props) => {
key: 'chart',
type: 'item',
icon: 'PieChartOutlined',
- title: '{{t("Chart (Old)",{ns:"charts"})}}',
+ title: `{{t("Chart (Old)", { ns: "${NAMESPACE}" })}}`,
component: 'ChartBlockInitializer',
});
}
@@ -78,27 +77,12 @@ const ChartsProvider = React.memo((props) => {
};
return (
-
-
- {props.children}
-
-
+ {props.children}
+
);
});
@@ -110,6 +94,12 @@ export class ChartsPlugin extends Plugin {
}
async load() {
this.app.use(ChartsProvider);
+ this.app.pluginSettingsManager.add(NAMESPACE, {
+ title: `{{t("Charts", { ns: "${NAMESPACE}" })}}`,
+ icon: 'PieChartOutlined',
+ Component: QueriesTable,
+ aclSnippet: 'pm.charts.queries',
+ });
}
}
diff --git a/packages/plugins/@nocobase/plugin-duplicator/src/client/DuplicatorProvider.tsx b/packages/plugins/@nocobase/plugin-duplicator/src/client/DuplicatorProvider.tsx
deleted file mode 100644
index 7865537cd..000000000
--- a/packages/plugins/@nocobase/plugin-duplicator/src/client/DuplicatorProvider.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { SettingsCenterProvider } from '@nocobase/client';
-import { Card } from 'antd';
-import React, { FC } from 'react';
-
-const DuplicatorPanel = () => {
- return (
-
- hello world
-
- );
-};
-
-export const DuplicatorProvider: FC = function (props) {
- return (
-
- {props.children}
-
- );
-};
-
-DuplicatorProvider.displayName = 'DuplicatorProvider';
diff --git a/packages/plugins/@nocobase/plugin-duplicator/src/client/index.tsx b/packages/plugins/@nocobase/plugin-duplicator/src/client/index.tsx
index f24515df0..fe8d025af 100644
--- a/packages/plugins/@nocobase/plugin-duplicator/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-duplicator/src/client/index.tsx
@@ -1,10 +1,7 @@
import { Plugin } from '@nocobase/client';
-import { DuplicatorProvider } from './DuplicatorProvider';
export class DuplicatorPlugin extends Plugin {
- async load() {
- this.app.use(DuplicatorProvider);
- }
+ async load() {}
}
export default DuplicatorPlugin;
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/client/FileManagerProvider.tsx b/packages/plugins/@nocobase/plugin-file-manager/src/client/FileManagerProvider.tsx
index 1d4d2ab65..2e3800ac9 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/src/client/FileManagerProvider.tsx
+++ b/packages/plugins/@nocobase/plugin-file-manager/src/client/FileManagerProvider.tsx
@@ -6,16 +6,13 @@ import {
SchemaComponentOptions,
SchemaInitializerContext,
SchemaInitializerProvider,
- SettingsCenterProvider,
useCollection,
} from '@nocobase/client';
import { forEach } from '@nocobase/utils/client';
import React, { FC, useContext } from 'react';
-import { FileStoragePane } from './FileStorage';
import * as hooks from './hooks';
import * as initializers from './initializers';
import { attachment } from './interfaces/attachment';
-import { NAMESPACE } from './locale';
import * as templates from './templates';
// 注册之后就可以在 Crete collection 按钮中选择创建了
@@ -52,33 +49,18 @@ export const FileManagerProvider: FC = (props) => {
const ctx = useContext(PluginManagerContext);
return (
-
-
-
-
- {props.children}
-
-
-
-
+
+
+ {props.children}
+
+
+
);
};
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/client/__tests__/e2e/createLocalStorage.test.ts b/packages/plugins/@nocobase/plugin-file-manager/src/client/__tests__/e2e/createLocalStorage.test.ts
index 8716b3278..b94b2c90f 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/src/client/__tests__/e2e/createLocalStorage.test.ts
+++ b/packages/plugins/@nocobase/plugin-file-manager/src/client/__tests__/e2e/createLocalStorage.test.ts
@@ -12,7 +12,7 @@ test.describe('file manager', () => {
// 1、前置条件:已登录
// 2、测试步骤:进入“文件管理器”-“新建”按钮,填写表单,点击“确定”按钮
- await page.goto('/admin/settings/file-manager/storages');
+ await page.goto('/admin/settings/file-manager');
await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: 'plus Add new' }).hover();
await page.getByRole('menuitem', { name: 'Local storage' }).click();
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/client/__tests__/e2e/editLocalStorage.test.ts b/packages/plugins/@nocobase/plugin-file-manager/src/client/__tests__/e2e/editLocalStorage.test.ts
index ac180da61..3d725b0fd 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/src/client/__tests__/e2e/editLocalStorage.test.ts
+++ b/packages/plugins/@nocobase/plugin-file-manager/src/client/__tests__/e2e/editLocalStorage.test.ts
@@ -10,7 +10,7 @@ test.describe('File manager', () => {
let caseTitle = 'edit local storage title';
// 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.getByRole('button', { name: 'plus Add new' }).hover();
await page.getByRole('menuitem', { name: 'Local storage' }).click();
diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/client/index.tsx b/packages/plugins/@nocobase/plugin-file-manager/src/client/index.tsx
index 13f15fe0f..6cf1c247d 100644
--- a/packages/plugins/@nocobase/plugin-file-manager/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-file-manager/src/client/index.tsx
@@ -1,5 +1,7 @@
import { Plugin } from '@nocobase/client';
import { FileManagerProvider } from './FileManagerProvider';
+import { FileStoragePane } from './FileStorage';
+import { NAMESPACE } from './locale';
import { storageTypes } from './schemas/storageTypes';
export class FileManagerPlugin extends Plugin {
@@ -7,6 +9,12 @@ export class FileManagerPlugin extends Plugin {
async load() {
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) => {
this.registerStorageType(storageType.name, storageType);
});
diff --git a/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/GraphCollectionProvider.tsx b/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/GraphCollectionProvider.tsx
index 1085dad0b..3c5ff42c1 100644
--- a/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/GraphCollectionProvider.tsx
+++ b/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/GraphCollectionProvider.tsx
@@ -1,30 +1,19 @@
-import { PluginManagerContext, SettingsCenterContext, SettingsCenterProvider } from '@nocobase/client';
+import { PluginManagerContext } from '@nocobase/client';
import React, { useContext } from 'react';
-import { GraphCollectionPane } from './GraphCollectionShortcut';
-import { useGCMTranslation } from './utils';
export const GraphCollectionProvider = React.memo((props) => {
const ctx = useContext(PluginManagerContext);
- const { t } = useGCMTranslation();
- const items = useContext(SettingsCenterContext);
-
- items['collection-manager']['tabs']['graph'] = {
- title: t('Graphical interface'),
- component: GraphCollectionPane,
- };
return (
-
-
- {props.children}
-
-
+
+ {props.children}
+
);
});
GraphCollectionProvider.displayName = 'GraphCollectionProvider';
diff --git a/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/index.tsx b/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/index.tsx
index 5d58bfe26..dacdc5acf 100644
--- a/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/index.tsx
@@ -1,9 +1,17 @@
import { Plugin } from '@nocobase/client';
import { GraphCollectionProvider } from './GraphCollectionProvider';
+import { GraphCollectionPane } from './GraphCollectionShortcut';
+import { NAMESPACE } from './locale';
export class GraphCollectionPlugin extends Plugin {
async load() {
this.app.use(GraphCollectionProvider);
+
+ this.app.pluginSettingsManager.add('collection-manager.graph', {
+ title: `{{t("Graphical interface", { ns: "${NAMESPACE}" })}}`,
+ Component: GraphCollectionPane,
+ aclSnippet: 'pm.collection-manager.graph',
+ });
}
}
diff --git a/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/locale/index.ts b/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/locale/index.ts
index 086314849..c55f40d36 100644
--- a/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/locale/index.ts
+++ b/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/locale/index.ts
@@ -1,3 +1 @@
-// export { default as enUS } from './en-US';
-// export { default as zhCN } from './zh-CN';
-// export { default as jaJP } from './ja-JP';
+export const NAMESPACE = 'graph-collection-manager';
diff --git a/packages/plugins/@nocobase/plugin-localization-management/src/client/index.tsx b/packages/plugins/@nocobase/plugin-localization-management/src/client/index.tsx
index b61ed5a86..de058ee43 100644
--- a/packages/plugins/@nocobase/plugin-localization-management/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-localization-management/src/client/index.tsx
@@ -1,30 +1,14 @@
-import { Plugin, SettingsCenterProvider } from '@nocobase/client';
-import React from 'react';
+import { Plugin } from '@nocobase/client';
import { Localization } from './Localization';
-import { useLocalTranslation } from './locale';
+import { NAMESPACE } from './locale';
export class LocalizationManagementPlugin extends Plugin {
async load() {
- this.app.use((props) => {
- const { t } = useLocalTranslation();
- return (
- ,
- },
- },
- },
- }}
- >
- {props.children}
-
- );
+ this.app.pluginSettingsManager.add(NAMESPACE, {
+ title: `{{t("Localization management", { ns: "${NAMESPACE}" })}}`,
+ icon: 'GlobalOutlined',
+ Component: Localization,
+ aclSnippet: 'pm.localization-management.localization',
});
}
}
diff --git a/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Map.tsx b/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Map.tsx
index 14a7ce077..288bf45fb 100644
--- a/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Map.tsx
+++ b/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Map.tsx
@@ -1,8 +1,8 @@
import AMapLoader from '@amap/amap-jsapi-loader';
import '@amap/amap-jsapi-types';
import { SyncOutlined } from '@ant-design/icons';
-import { useField, useFieldSchema } from '@formily/react';
-import { css, useCollection } from '@nocobase/client';
+import { useFieldSchema } from '@formily/react';
+import { css, useApp, useCollection } from '@nocobase/client';
import { useMemoizedFn } from 'ahooks';
import { Alert, App, Button, Spin } from 'antd';
import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
@@ -373,11 +373,13 @@ export const AMapComponent = React.forwardRef navigate('/admin/settings/map/configuration')}>
+ navigate(app.pluginSettingsManager.getRoutePath('map'))}>
{t('Go to the configuration page')}
}
diff --git a/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Map.tsx b/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Map.tsx
index 5cf0f9231..76150d492 100644
--- a/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Map.tsx
+++ b/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Map.tsx
@@ -1,7 +1,7 @@
import { SyncOutlined } from '@ant-design/icons';
import { useFieldSchema } from '@formily/react';
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 { Alert, App, Button, Spin } from 'antd';
import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
@@ -369,12 +369,16 @@ export const GoogleMapsComponent = React.forwardRef navigate('/admin/settings/map/configuration?tab=google')}>
+ navigate(app.pluginSettingsManager.getRoutePath('map') + '?tab=google')}
+ >
{t('Go to the configuration page')}
}
diff --git a/packages/plugins/@nocobase/plugin-map/src/client/index.tsx b/packages/plugins/@nocobase/plugin-map/src/client/index.tsx
index 2190072b7..97f7981d7 100644
--- a/packages/plugins/@nocobase/plugin-map/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-map/src/client/index.tsx
@@ -1,45 +1,23 @@
-import {
- CollectionManagerContext,
- CurrentAppInfoProvider,
- Plugin,
- SchemaComponentOptions,
- SettingsCenterProvider,
-} from '@nocobase/client';
+import { CollectionManagerContext, CurrentAppInfoProvider, Plugin, SchemaComponentOptions } from '@nocobase/client';
import React, { useContext } from 'react';
import { MapBlockOptions } from './block';
import { Configuration, Map } from './components';
import { interfaces } from './fields';
import { MapInitializer } from './initialize';
-import { useMapTranslation } from './locale';
+import { NAMESPACE } from './locale';
const MapProvider = React.memo((props) => {
const ctx = useContext(CollectionManagerContext);
- const { t } = useMapTranslation();
return (
-
-
-
-
- {props.children}
-
-
-
-
+
+
+
+ {props.children}
+
+
+
);
@@ -49,6 +27,12 @@ MapProvider.displayName = 'MapProvider';
export class MapPlugin extends Plugin {
async load() {
this.app.use(MapProvider);
+ this.app.pluginSettingsManager.add(NAMESPACE, {
+ title: `{{t("Map Manager", { ns: "${NAMESPACE}" })}}`,
+ icon: 'EnvironmentOutlined',
+ Component: Configuration,
+ aclSnippet: 'pm.map.configuration',
+ });
}
}
diff --git a/packages/plugins/@nocobase/plugin-mobile-client/src/client/MobileClientProvider.tsx b/packages/plugins/@nocobase/plugin-mobile-client/src/client/MobileClientProvider.tsx
index 19e4b7811..cfa30aa8b 100644
--- a/packages/plugins/@nocobase/plugin-mobile-client/src/client/MobileClientProvider.tsx
+++ b/packages/plugins/@nocobase/plugin-mobile-client/src/client/MobileClientProvider.tsx
@@ -1,12 +1,8 @@
-import { SettingsCenterProvider } from '@nocobase/client';
import React, { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
-import { AppConfiguration, InterfaceConfiguration } from './configuration';
import { isJSBridge } from './core/bridge';
-import { useTranslation } from './locale';
export const MobileClientProvider = React.memo((props) => {
- const { t } = useTranslation();
const location = useLocation();
const navigation = useNavigate();
@@ -16,26 +12,5 @@ export const MobileClientProvider = React.memo((props) => {
}
}, [location.pathname, navigation]);
- return (
-
- {props.children}
-
- );
+ return <>{props.children}>;
});
diff --git a/packages/plugins/@nocobase/plugin-mobile-client/src/client/index.tsx b/packages/plugins/@nocobase/plugin-mobile-client/src/client/index.tsx
index b6481d595..b69c8c398 100644
--- a/packages/plugins/@nocobase/plugin-mobile-client/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-mobile-client/src/client/index.tsx
@@ -1,17 +1,38 @@
import { createRouterManager, Plugin, RouterManager, RouteSchemaComponent } from '@nocobase/client';
import React from 'react';
-import { Navigate } from 'react-router-dom';
+import { Navigate, Outlet } from 'react-router-dom';
import { MobileClientProvider } from './MobileClientProvider';
import MApplication from './router/Application';
+import { AppConfiguration, InterfaceConfiguration } from './configuration';
+import { NAMESPACE } from './locale';
export class MobileClientPlugin extends Plugin {
public mobileRouter: RouterManager;
async load() {
this.setMobileRouter();
this.addRoutes();
+ this.addSettings();
this.app.use(MobileClientProvider);
}
+ addSettings() {
+ this.app.pluginSettingsManager.add(NAMESPACE, {
+ title: `{{t("Mobile Client-side", { ns: "${NAMESPACE}" })}}`,
+ icon: 'MobileOutlined',
+ Component: () => ,
+ });
+ 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() {
const router = createRouterManager({ type: 'hash' });
router.add('root', {
diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/MultiAppManagerProvider.tsx b/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/MultiAppManagerProvider.tsx
index c26ead9f5..1cca06a95 100644
--- a/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/MultiAppManagerProvider.tsx
+++ b/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/MultiAppManagerProvider.tsx
@@ -1,14 +1,7 @@
-import {
- Icon,
- PinnedPluginListProvider,
- SchemaComponentOptions,
- SettingsCenterProvider,
- useRequest,
-} from '@nocobase/client';
+import { Icon, PinnedPluginListProvider, SchemaComponentOptions, useApp, useRequest } from '@nocobase/client';
import { Button, Dropdown } from 'antd';
import React from 'react';
import { Link } from 'react-router-dom';
-import { AppManager } from './AppManager';
import { AppNameInput } from './AppNameInput';
import { usePluginUtils } from './utils';
@@ -25,6 +18,7 @@ const MultiAppManager = () => {
},
);
const { t } = usePluginUtils();
+ const app = useApp();
const items = [
...(data?.data || []).map((app) => {
let link = `/apps/${app.name}/admin/`;
@@ -42,7 +36,7 @@ const MultiAppManager = () => {
}),
{
key: '.manager',
- label: {t('Manage applications')},
+ label: {t('Manage applications')},
},
];
return (
@@ -58,35 +52,13 @@ const MultiAppManager = () => {
};
export const MultiAppManagerProvider = (props) => {
- const { t } = usePluginUtils();
return (
-
- ,
- },
- // settings: {
- // title: 'Settings',
- // component: () => ,
- // },
- },
- },
- }}
- >
- {props.children}
-
-
+ {props.children}
);
};
diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/index.ts b/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/index.ts
index e33a63f62..bf4d7694c 100644
--- a/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/index.ts
+++ b/packages/plugins/@nocobase/plugin-multi-app-manager/src/client/index.ts
@@ -1,9 +1,18 @@
import { Plugin } from '@nocobase/client';
import { MultiAppManagerProvider } from './MultiAppManagerProvider';
+import { AppManager } from './AppManager';
+import { NAMESPACE } from '../locale';
export class MultiAppManagerPlugin extends Plugin {
async load() {
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',
+ });
}
}
diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/src/locale/index.ts b/packages/plugins/@nocobase/plugin-multi-app-manager/src/locale/index.ts
new file mode 100644
index 000000000..38903a58f
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-multi-app-manager/src/locale/index.ts
@@ -0,0 +1 @@
+export const NAMESPACE = 'multi-app-manager';
diff --git a/packages/plugins/@nocobase/plugin-oidc/package.json b/packages/plugins/@nocobase/plugin-oidc/package.json
index b6e8ea55a..647db4188 100644
--- a/packages/plugins/@nocobase/plugin-oidc/package.json
+++ b/packages/plugins/@nocobase/plugin-oidc/package.json
@@ -1,7 +1,7 @@
{
"name": "@nocobase/plugin-oidc",
- "displayName": "OIDC (OpenID Connect) auth - SSO login",
- "displayName.zh-CN": "OIDC (OpenID Connect) 认证 - SSO 登录",
+ "displayName": "OIDC auth - SSO login",
+ "displayName.zh-CN": "OIDC 认证 - SSO 登录",
"description": "OIDC (OpenID Connect) authentication for NocoBase",
"description.zh-CN": "OIDC (OpenID Connect) authentication for NocoBase",
"version": "0.14.0-alpha.8",
diff --git a/packages/plugins/@nocobase/plugin-sample-hello/src/client/index.tsx b/packages/plugins/@nocobase/plugin-sample-hello/src/client/index.tsx
index ed5783964..50dc16561 100644
--- a/packages/plugins/@nocobase/plugin-sample-hello/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-sample-hello/src/client/index.tsx
@@ -1,11 +1,5 @@
import { TableOutlined } from '@ant-design/icons';
-import {
- Plugin,
- SchemaComponentOptions,
- SchemaInitializer,
- SchemaInitializerContext,
- SettingsCenterProvider,
-} from '@nocobase/client';
+import { Plugin, SchemaComponentOptions, SchemaInitializer, SchemaInitializerContext, useApp } from '@nocobase/client';
import { Card } from 'antd';
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
@@ -56,31 +50,30 @@ const HelloProvider = React.memo((props) => {
}
return (
- Hello Settings ,
- },
- },
- },
- }}
- >
-
- {props.children}
-
-
+
+ {props.children}
+
);
});
HelloProvider.displayName = 'HelloProvider';
+const HelloPluginSettingPage = () => {
+ return (
+
+ Hello plugin setting page
+
+ );
+};
+
class HelloPlugin extends Plugin {
async load() {
this.app.addProvider(HelloProvider);
+ this.app.pluginSettingsManager.add('sample-hello', {
+ title: 'Hello',
+ icon: 'ApiOutlined',
+ Component: HelloPluginSettingPage,
+ sort: 100,
+ });
}
}
diff --git a/packages/plugins/@nocobase/plugin-sample-shop-i18n/src/client/index.tsx b/packages/plugins/@nocobase/plugin-sample-shop-i18n/src/client/index.tsx
index 09e333190..56a207ec3 100644
--- a/packages/plugins/@nocobase/plugin-sample-shop-i18n/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-sample-shop-i18n/src/client/index.tsx
@@ -1,11 +1,11 @@
-import { i18n, Plugin, PluginManagerContext, SettingsCenterProvider } from '@nocobase/client';
+import { i18n, Plugin, PluginManagerContext } from '@nocobase/client';
import { Select } from 'antd';
import React, { useContext } from 'react';
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: '店铺',
I18n: '国际化',
Pending: '已下单',
@@ -23,7 +23,7 @@ const ORDER_STATUS_LIST = [
];
function OrderStatusSelect() {
- const { t } = useTranslation(ns);
+ const { t } = useTranslation(NAMESPACE);
return (
@@ -40,36 +40,26 @@ const ShopI18nProvider = React.memo((props) => {
const ctx = useContext(PluginManagerContext);
return (
-
-
- {props.children}
-
-
+ {props.children}
+
);
});
class ShopI18nPlugin extends Plugin {
async load() {
this.app.addProvider(ShopI18nProvider);
+ this.app.pluginSettingsManager.add(NAMESPACE, {
+ title: `{{t("Shop", { ns: "${NAMESPACE}" })}}`,
+ icon: 'ShopOutlined',
+ Component: OrderStatusSelect,
+ });
}
}
diff --git a/packages/plugins/@nocobase/plugin-theme-editor/src/client/index.tsx b/packages/plugins/@nocobase/plugin-theme-editor/src/client/index.tsx
index fd8f45f5a..c7bb746fa 100644
--- a/packages/plugins/@nocobase/plugin-theme-editor/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-theme-editor/src/client/index.tsx
@@ -1,11 +1,4 @@
-import {
- Plugin,
- SettingsCenterProvider,
- createStyles,
- defaultTheme,
- useCurrentUserSettingsMenu,
- useGlobalTheme,
-} from '@nocobase/client';
+import { Plugin, createStyles, defaultTheme, useCurrentUserSettingsMenu, useGlobalTheme } from '@nocobase/client';
import { ConfigProvider } from 'antd';
import _ from 'lodash';
import React, { useEffect, useMemo } from 'react';
@@ -15,7 +8,7 @@ import ThemeList from './components/ThemeList';
import { ThemeListProvider } from './components/ThemeListProvider';
import CustomTheme from './components/theme-editor';
import { useThemeSettings } from './hooks/useThemeSettings';
-import { useTranslation } from './locale';
+import { NAMESPACE } from './locale';
const useStyles = createStyles(({ css, token }) => {
return {
@@ -38,7 +31,6 @@ const useStyles = createStyles(({ css, token }) => {
const CustomThemeProvider = React.memo((props) => {
const { addMenuItem } = useCurrentUserSettingsMenu();
const themeItem = useThemeSettings();
- const { t } = useTranslation();
const [open, setOpen] = React.useState(false);
const { theme, setTheme } = useGlobalTheme();
const { styles } = useStyles();
@@ -48,21 +40,6 @@ const CustomThemeProvider = React.memo((props) => {
addMenuItem(themeItem, { before: 'divider_3' });
}, [addMenuItem, themeItem]);
- const settings = useMemo(() => {
- return {
- 'theme-editor': {
- title: t('Theme editor'),
- icon: 'BgColorsOutlined',
- tabs: {
- themes: {
- title: t('Themes'),
- component: ThemeList,
- },
- },
- },
- };
- }, []);
-
const contentStyle = useMemo(() => {
return open
? { transform: 'rotate(0)', flexGrow: 1, width: 0, height: '100%' }
@@ -89,7 +66,7 @@ const CustomThemeProvider = React.memo((props) => {
- {editor}
+ {editor}
@@ -101,6 +78,12 @@ CustomThemeProvider.displayName = 'CustomThemeProvider';
export class ThemeEditorPlugin extends Plugin {
async load() {
this.app.use(CustomThemeProvider);
+ this.app.pluginSettingsManager.add(NAMESPACE, {
+ title: `{{t("Theme editor", {ns:"${NAMESPACE}"})}}`,
+ icon: 'BgColorsOutlined',
+ Component: ThemeList,
+ aclSnippet: 'pm.theme-editor.themes',
+ });
}
}
diff --git a/packages/plugins/@nocobase/plugin-verification/src/client/VerificationProvider.tsx b/packages/plugins/@nocobase/plugin-verification/src/client/VerificationProvider.tsx
index ef66d0528..084e22213 100644
--- a/packages/plugins/@nocobase/plugin-verification/src/client/VerificationProvider.tsx
+++ b/packages/plugins/@nocobase/plugin-verification/src/client/VerificationProvider.tsx
@@ -1,39 +1,19 @@
import React, { FC, useContext } from 'react';
-import { PluginManagerContext, SettingsCenterProvider } from '@nocobase/client';
-
-import { NAMESPACE } from './locale';
-
-import { VerificationProviders } from './VerificationProviders';
-
+import { PluginManagerContext } from '@nocobase/client';
export { default as verificationProviderTypes } from './providerTypes';
export const VerificationProvider: FC = (props) => {
const ctx = useContext(PluginManagerContext);
return (
-
-
- {props.children}
-
-
+ {props.children}
+
);
};
diff --git a/packages/plugins/@nocobase/plugin-verification/src/client/index.tsx b/packages/plugins/@nocobase/plugin-verification/src/client/index.tsx
index f5bf9a727..ec1b8c68b 100644
--- a/packages/plugins/@nocobase/plugin-verification/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-verification/src/client/index.tsx
@@ -1,9 +1,17 @@
import { Plugin } from '@nocobase/client';
import { VerificationProvider } from './VerificationProvider';
+import { VerificationProviders } from './VerificationProviders';
+import { NAMESPACE } from './locale';
export class VerificationPlugin extends Plugin {
async load() {
this.app.use(VerificationProvider);
+ this.app.pluginSettingsManager.add(NAMESPACE, {
+ icon: 'CheckCircleOutlined',
+ title: `{{t("Verification", { ns: "${NAMESPACE}" })}}`,
+ Component: VerificationProviders,
+ aclSnippet: 'pm.verification.providers',
+ });
}
}
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionCanvas.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionCanvas.tsx
index a9f39b39e..2f2d4d0ea 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionCanvas.tsx
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionCanvas.tsx
@@ -3,6 +3,7 @@ import {
cx,
SchemaComponent,
useAPIClient,
+ useApp,
useCompile,
useDocumentTitle,
useResourceActionContext,
@@ -20,6 +21,7 @@ import useStyles from './style';
import { linkNodes } from './utils';
import { DownOutlined } from '@ant-design/icons';
import { StatusButton } from './components/StatusButton';
+import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './constant';
function attachJobs(nodes, jobs: any[] = []): void {
const nodesMap = new Map();
@@ -165,7 +167,7 @@ function ExecutionsDropdown(props) {
const onClick = useCallback(
({ key }) => {
if (key != execution.id) {
- navigate(`/admin/settings/workflow/executions/${key}`);
+ navigate(getWorkflowExecutionsPath(key));
}
},
[execution],
@@ -208,6 +210,7 @@ export function ExecutionCanvas() {
const { data, loading } = useResourceActionContext();
const { setTitle } = useDocumentTitle();
const [viewJob, setViewJob] = useState(null);
+ const app = useApp();
useEffect(() => {
const { workflow } = data?.data ?? {};
setTitle?.(`${workflow?.title ? `${workflow.title} - ` : ''}${lang('Execution history')}`);
@@ -244,8 +247,8 @@ export function ExecutionCanvas() {
{lang('Workflow')} },
- { title: {workflow.title} },
+ { title: {lang('Workflow')} },
+ { title: {workflow.title} },
{ title: },
]}
/>
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionLink.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionLink.tsx
index 906e6ae23..5220a0f72 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionLink.tsx
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionLink.tsx
@@ -3,13 +3,14 @@ import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { useActionContext, useRecord } from '@nocobase/client';
+import { getWorkflowExecutionsPath } from './constant';
export const ExecutionLink = () => {
const { t } = useTranslation();
const { id } = useRecord();
const { setVisible } = useActionContext();
return (
- setVisible(false)}>
+ setVisible(false)}>
{t('View')}
);
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowCanvas.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowCanvas.tsx
index b9820798b..537d88495 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowCanvas.tsx
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowCanvas.tsx
@@ -4,6 +4,7 @@ import {
ResourceActionProvider,
SchemaComponent,
cx,
+ useApp,
useDocumentTitle,
useResourceActionContext,
useResourceContext,
@@ -21,6 +22,7 @@ import { lang } from './locale';
import { executionSchema } from './schemas/executions';
import useStyles from './style';
import { linkNodes } from './utils';
+import { getWorkflowDetailPath } from './constant';
function ExecutionResourceProvider({ request, filter = {}, ...others }) {
const { workflow } = useFlowContext();
@@ -44,6 +46,7 @@ function ExecutionResourceProvider({ request, filter = {}, ...others }) {
export function WorkflowCanvas() {
const navigate = useNavigate();
const { t } = useTranslation();
+ const app = useApp();
const { data, refresh, loading } = useResourceActionContext();
const { resource } = useResourceContext();
const { setTitle } = useDocumentTitle();
@@ -67,7 +70,7 @@ export function WorkflowCanvas() {
function onSwitchVersion({ key }) {
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'));
- navigate(`/admin/settings/workflow/workflows/${revision.id}`);
+ navigate(`/admin/workflow/workflows/${revision.id}`);
}
async function onDelete() {
@@ -110,8 +113,8 @@ export function WorkflowCanvas() {
navigate(
workflow.current
- ? '/admin/settings/workflow/workflows'
- : `/admin/settings/workflow/workflows/${revisions.find((item) => item.current)?.id}`,
+ ? app.pluginSettingsManager.getRoutePath('workflow')
+ : getWorkflowDetailPath(revisions.find((item) => item.current)?.id),
);
},
});
@@ -147,7 +150,7 @@ export function WorkflowCanvas() {
{lang('Workflow')} },
+ { title: {lang('Workflow')} },
{ title: {workflow.title} },
]}
/>
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowLink.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowLink.tsx
index 08e7963ef..27829791c 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowLink.tsx
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowLink.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
+import { getWorkflowDetailPath } from './constant';
import { useActionContext, useGetAriaLabelOfAction, useRecord } from '@nocobase/client';
export const WorkflowLink = () => {
@@ -11,7 +12,7 @@ export const WorkflowLink = () => {
const { getAriaLabel } = useGetAriaLabelOfAction('Configure');
return (
- setVisible(false)}>
+ setVisible(false)}>
{t('Configure')}
);
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowProvider.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowProvider.tsx
index 8de70eb31..7f0db0fac 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowProvider.tsx
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowProvider.tsx
@@ -3,7 +3,6 @@ import {
PluginManagerContext,
SchemaComponent,
SchemaComponentContext,
- SettingsCenterProvider,
} from '@nocobase/client';
import { Card } from 'antd';
import React, { useContext } from 'react';
@@ -12,7 +11,6 @@ import { ExecutionResourceProvider } from './ExecutionResourceProvider';
import { WorkflowLink } from './WorkflowLink';
import OpenDrawer from './components/OpenDrawer';
import expressionField from './interfaces/expression';
-import { lang } from './locale';
import { instructions } from './nodes';
import { workflowSchema } from './schemas/workflows';
import { getTriggersOptions, triggers } from './triggers';
@@ -26,7 +24,7 @@ export function useWorkflowContext() {
return useContext(WorkflowContext);
}
-function WorkflowPane() {
+export function WorkflowPane() {
const ctx = useContext(SchemaComponentContext);
return (
@@ -53,41 +51,24 @@ export const WorkflowProvider = (props) => {
const pmCtx = useContext(PluginManagerContext);
const cmCtx = useContext(CollectionManagerContext);
return (
-
-
-
- {props.children}
-
-
-
+ {props.children}
+
+
);
};
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/__tests__/e2e/createFormEvent.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/client/__tests__/e2e/createFormEvent.test.ts
index 6d6a25e38..b4e53e5ed 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/__tests__/e2e/createFormEvent.test.ts
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/__tests__/e2e/createFormEvent.test.ts
@@ -12,7 +12,7 @@ test.describe('workflow manage', () => {
// 1、前置条件:已登录
// 2、测试步骤:进入“工作流管理”-“新建”按钮,填写表单,点击“确定”按钮
- await page.goto('/admin/settings/workflow/workflows');
+ await page.goto('/admin/settings/workflow');
await page.waitForLoadState('networkidle');
await page.getByLabel('action-Action-Add new-workflows').click();
const createWorkFlow = new CreateWorkFlow(page);
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/__tests__/e2e/editFormEvent.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/client/__tests__/e2e/editFormEvent.test.ts
index 4ac94e86e..0e7075592 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/__tests__/e2e/editFormEvent.test.ts
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/__tests__/e2e/editFormEvent.test.ts
@@ -10,7 +10,7 @@ test.describe('workflow manage', () => {
const caseTitle = 'edit from event name';
// 1、前置条件:1.1、已登录,1.2、存在一个工作流
- await page.goto('/admin/settings/workflow/workflows');
+ await page.goto('/admin/settings/workflow');
await page.waitForLoadState('networkidle');
await page.getByLabel('action-Action-Add new-workflows').click();
const createWorkFlow = new CreateWorkFlow(page);
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/__tests__/e2e/triggerCollectionEvent.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/client/__tests__/e2e/triggerCollectionEvent.test.ts
index 92da34447..b1a82bcde 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/__tests__/e2e/triggerCollectionEvent.test.ts
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/__tests__/e2e/triggerCollectionEvent.test.ts
@@ -24,7 +24,7 @@ test.describe('trigger collection events', () => {
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.getByLabel('action-Action-Add new-workflows').click();
const createWorkFlow = new CreateWorkFlow(page);
@@ -85,7 +85,7 @@ test.describe('trigger collection events', () => {
// 3、预期结果:数据添加成功,工作流成功触发
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();
// 4、后置处理:删除工作流
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/constant.ts b/packages/plugins/@nocobase/plugin-workflow/src/client/constant.ts
new file mode 100644
index 000000000..60ad3252d
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/constant.ts
@@ -0,0 +1,2 @@
+export const getWorkflowDetailPath = (id: string | number) => `/admin/workflow/workflows/${id}`;
+export const getWorkflowExecutionsPath = (id: string | number) => `/admin/workflow/executions/${id}`;
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx
index 15a328b33..962c27ee5 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx
@@ -10,13 +10,15 @@ import { Plugin } from '@nocobase/client';
import React from 'react';
import { ExecutionPage } from './ExecutionPage';
import { WorkflowPage } from './WorkflowPage';
-import { WorkflowProvider } from './WorkflowProvider';
+import { WorkflowPane, WorkflowProvider } from './WorkflowProvider';
import { DynamicExpression } from './components/DynamicExpression';
import { triggers, useTrigger, getTriggersOptions } from './triggers';
import { instructions } from './nodes';
import { WorkflowTodo } from './nodes/manual/WorkflowTodo';
import { WorkflowTodoBlockInitializer } from './nodes/manual/WorkflowTodoBlockInitializer';
import { useTriggerWorkflowsActionProps } from './triggers/form';
+import { NAMESPACE } from './locale';
+import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './constant';
export class WorkflowPlugin extends Plugin {
triggers = triggers;
@@ -28,6 +30,12 @@ export class WorkflowPlugin extends Plugin {
this.addScopes();
this.addComponents();
this.app.addProvider(WorkflowProvider);
+ this.app.pluginSettingsManager.add(NAMESPACE, {
+ icon: 'PartitionOutlined',
+ title: `{{t("Workflow", { ns: "${NAMESPACE}" })}}`,
+ Component: WorkflowPane,
+ aclSnippet: 'pm.workflow.workflows',
+ });
}
addScopes() {
@@ -47,12 +55,12 @@ export class WorkflowPlugin extends Plugin {
}
addRoutes() {
- this.app.router.add('admin.settings.workflow.workflows.id', {
- path: '/admin/settings/workflow/workflows/:id',
+ this.app.router.add('admin.workflow.workflows.id', {
+ path: getWorkflowDetailPath(':id'),
element: ,
});
- this.app.router.add('admin.settings.workflow.executions.id', {
- path: '/admin/settings/workflow/executions/:id',
+ this.app.router.add('admin.workflow.executions.id', {
+ path: getWorkflowExecutionsPath(':id'),
element: ,
});
}
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/schemas/executions.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/schemas/executions.tsx
index 5ad34d0fe..06d8ae50d 100644
--- a/packages/plugins/@nocobase/plugin-workflow/src/client/schemas/executions.tsx
+++ b/packages/plugins/@nocobase/plugin-workflow/src/client/schemas/executions.tsx
@@ -6,6 +6,7 @@ import { ExecutionStatusOptions } from '../constants';
import { NAMESPACE } from '../locale';
import { useTranslation } from 'react-i18next';
import { message } from 'antd';
+import { getWorkflowDetailPath } from '../constant';
export const executionCollection = {
name: 'executions',
@@ -32,12 +33,7 @@ export const executionCollection = {
title: `{{t("Version", { ns: "${NAMESPACE}" })}}`,
['x-component']({ value }) {
const { setVisible } = useActionContext();
- return (
- setVisible(false)}
- >{`#${value}`}
- );
+ return setVisible(false)}>{`#${value}`};
},
} as ISchema,
},
diff --git a/tsconfig.json b/tsconfig.json
index 45d57e364..c5f460781 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -24,7 +24,15 @@
"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": [
"packages/**/node_modules",
"packages/**/dist",