refactor: plugin settings manager (#2712)

* feat: add settingsCenter

* fix: style bug

* chore: optimized code

* refactor: settingCenter Auth

* feat: add aclSnippet option

* refactor: all plugin's setting center api

* feat: add plugin with name

* docs: add settings-center doc

* fix: settings center menu sort by name

* fix: change setting center layout

* fix: change hello sort

* test: add SettingsCenter.ts test case

* fix: bug

* fix: acl bug

* fix: bug

* fix: bug and 404 page

* fix: test bug

* fix: test bug

* fix: bug

* fix: locale

* fix: styling

* fix: rename settingsCenter to pluginSettingsManager

* fix: styling

* fix: e2e bug

* fix: e2e bug

* fix: locale

* feat: update docs

* fix: update

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
jack zhang 2023-11-13 11:01:18 +08:00 committed by GitHub
parent 454db91827
commit 35b06cbfa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 1178 additions and 887 deletions

View File

@ -8,11 +8,11 @@ NocoBase is in early stage of development and is subject to frequent changes, pl
## Recent major updates ## Recent major updates
- [v0.14: New plugin manager, supports adding plugins through UI - 2023/09/11](https://docs.nocobase.com/welcome/release/v14-changelog) - [v0.15: New plugin settings manager - 2023/11/13](https://blog.nocobase.com/posts/release-v015/)
- [v0.13: New application status flow - 2023/08/24](https://docs.nocobase.com/welcome/release/v13-changelog) - [v0.14: New plugin manager, supports adding plugins through UI - 2023/09/11](https://blog.nocobase.com/posts/release-v014/)
- [v0.12: New plugin build tool - 2023/08/01](https://docs.nocobase.com/welcome/release/v12-changelog) - [v0.13: New application status flow - 2023/08/24](https://blog.nocobase.com/posts/release-v013/)
- [v0.11: New client application, plugin and router - 2023/07/08](http://docs.nocobase.com/welcome/release/v11-changelog) - [v0.12: New plugin build tool - 2023/08/01](https://blog.nocobase.com/posts/release-v012/)
- [v0.10: Update instructions - 2023/06/23](http://docs.nocobase.com/welcome/release/v10-changelog) - [v0.11: New client application, plugin and router - 2023/07/08](https://blog.nocobase.com/posts/release-v011/)
## What is NocoBase ## What is NocoBase

View File

@ -8,11 +8,11 @@ NocoBase 正处在早期开发阶段,可能变动频繁,请谨慎用于生
## 最近重要更新 ## 最近重要更新
- [v0.14全新的插件管理器支持通过界面添加插件 - 2023/09/11](https://docs-cn.nocobase.com/welcome/release/v14-changelog) - [v0.15全新的插件设置中心 - 2023/11/13](https://blog-cn.nocobase.com/posts/release-v015/)
- [v0.13: 全新的应用状态流转 - 2023/08/24](https://docs-cn.nocobase.com/welcome/release/v13-changelog) - [v0.14全新的插件管理器支持通过界面添加插件 - 2023/09/11](https://blog-cn.nocobase.com/posts/release-v014/)
- [v0.12: 全新的插件构建工具 - 2023/08/01](https://docs-cn.nocobase.com/welcome/release/v12-changelog) - [v0.13: 全新的应用状态流转 - 2023/08/24](https://blog-cn.nocobase.com/posts/release-v013/)
- [v0.11: 全新的客户端 Application、Plugin 和 Router - 2023/07/08](https://docs-cn.nocobase.com/welcome/release/v11-changelog) - [v0.12: 全新的插件构建工具 - 2023/08/01](https://blog-cn.nocobase.com/posts/release-v012/)
- [v0.10: Update instructions - 2023/06/23](https://docs-cn.nocobase.com/welcome/release/v10-changelog) - [v0.11: 全新的客户端 Application、Plugin 和 Router - 2023/07/08](https://blog-cn.nocobase.com/posts/release-v011/)
## NocoBase 是什么 ## NocoBase 是什么

View File

@ -200,7 +200,7 @@ const sidebar = {
], ],
}, },
'/development/client/ui-router', '/development/client/ui-router',
'/development/client/settings-center', '/development/client/plugin-settings',
'/development/client/i18n', '/development/client/i18n',
'/development/client/test', '/development/client/test',
], ],

View File

@ -0,0 +1,75 @@
# Plugin Settings Manager
<img src="./plugin-settings/settings-tab.jpg" style="max-width: 100%;"/>
## Example
### Basic Usage
```tsx | pure
import { Plugin } from '@nocobase/client';
import React from 'react';
const HelloSettingPage = () => <div>Hello Setting page</div>;
export class HelloPlugin extends Plugin {
async load() {
this.app.pluginSettingsManager.add('hello', {
title: 'Hello', // menu title and page title
icon: 'ApiOutlined', // menu icon
Component: HelloSettingPage,
})
}
}
```
### Multiple Level Routes
```tsx | pure
import { Outlet } from 'react-router-dom'
const SettingPageLayout = () => <div> <div>This</div> public part, the following is the outlet of the sub-route: <div><Outlet /></div></div>;
class HelloPlugin extends Plugin {
async load() {
this.app.pluginSettingsManager.add('hello', {
title: 'HelloWorld',
icon: '',
Component: SettingPageLayout
})
this.app.pluginSettingsManager.add('hello.demo1', {
title: 'Demo1 Page',
Component: () => <div>Demo1 Page Content</div>
})
this.app.pluginSettingsManager.add('hello.demo2', {
title: 'Demo2 Page',
Component: () => <div>Demo2 Page Content</div>
})
}
}
```
### Get Route Path
If you want to get the jump link of the setting page, you can get it through the `getRoutePath` method.
```tsx | pure
import { useApp } from '@nocobase/client'
const app = useApp();
app.pluginSettingsManager.getRoutePath('hello'); // /admin/settings/hello
app.pluginSettingsManager.getRoutePath('hello.demo1'); // /admin/settings/hello/demo1
```
### Get Config
If you want to get the added configuration (already filtered by permissions), you can get it through the `get` method.
```tsx | pure
const app = useApp();
app.pluginSettingsManager.get('hello'); // { title: 'HelloWorld', icon: '', Component: HelloSettingPage, children: [{...}] }
```
See [samples/hello](https://github.com/nocobase/nocobase/blob/main/packages/plugins/%40nocobase/plugin-sample-hello/src/client/index.tsx) for full examples.

View File

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

View File

@ -1,33 +0,0 @@
# Settings Center
<img src="./settings-center/settings-tab.jpg" style="max-width: 100%;"/>
## Example
```tsx | pure
import { SettingsCenterProvider } from '@nocobase/client';
import React, { useContext } from 'react';
const HelloTab => () => <div>Hello Tab</div>;
export default React.memo((props) => {
return (
<SettingsCenterProvider
settings={{
'sample-hello': {
title: 'Hello',
icon: 'ApiOutlined',
tabs: {
tab1: {
title: 'Hello Tab',
component: HelloTab,
},
},
},
}}
>{props.children}</SettingsCenterProvider>
);
});
```
See [samples/hello](https://github.com/nocobase/nocobase/tree/develop/packages/samples/hello) for full examples.

View File

@ -47,7 +47,7 @@ Whether it is generic functionality or personalization, it is recommended to wri
Distribution of modules. Distribution of modules.
- Server - Server
- Collections & Fields: mainly used for system table configuration. Business tables are recommended to be configured in "Settings Center - Collection manager". - Collections & Fields: mainly used for system table configuration. Business tables are recommended to be configured in "Plugin Settings Manager - Collection manager".
- Resources & Actions: Mainly used to extend the Action API - Resources & Actions: Mainly used to extend the Action API
- Middleware: Middleware - Middleware: Middleware
- Events: Events - Events: Events
@ -57,5 +57,5 @@ Distribution of modules.
- Client - Client
- UI Schema Designer: Page Designer - UI Schema Designer: Page Designer
- UI Router: When there is a need for custom pages - UI Router: When there is a need for custom pages
- Settings Center: Provides configuration pages for plugins - Plugin Settings Manager: Provides configuration pages for plugins
- I18n: Client side internationalization - I18n: Client side internationalization

View File

@ -76,7 +76,7 @@ yarn pm remove hello
- Client - Client
- UI Schema Designer页面设计器 - UI Schema Designer页面设计器
- UI Router有自定义页面需求时 - UI Router有自定义页面需求时
- Settings Center为插件提供配置页面 - Plugin Settings Manager为插件提供配置页面
- I18n客户端国际化 - I18n客户端国际化
- Devtools - Devtools
- Commands自定义命令行 - Commands自定义命令行
@ -113,8 +113,6 @@ yarn pm remove hello
- UI Router - UI Router
- RouteSwitchProvider - RouteSwitchProvider
- RouteSwitch - RouteSwitch
- Settings Center
- SettingsCenterProvider
- I18n - I18n
- app.i18n - app.i18n
- useTranslation - useTranslation

View File

@ -6,7 +6,7 @@ Starting with v0.8, NocoBase begins to provide an available plugin manager and d
- UI Editor - UI Editor
- Plugin Manager - Plugin Manager
- Settings Center - Plugin Settings Manager
- Personal Center - Personal Center
<img src="./v08-changelog/topright.jpg" style="max-width: 500px;" /> <img src="./v08-changelog/topright.jpg" style="max-width: 500px;" />

View File

@ -0,0 +1,75 @@
# 配置中心
<img src="./plugin-settings/settings-tab.jpg" style="max-width: 100%;"/>
## 示例
### 基础用法
```tsx | pure
import { Plugin } from '@nocobase/client';
import React from 'react';
const HelloSettingPage = () => <div>Hello Setting page</div>;
export class HelloPlugin extends Plugin {
async load() {
this.app.pluginSettingsManager.add('hello', {
title: 'Hello', // 设置页面的标题和菜单名称
icon: 'ApiOutlined', // 设置页面菜单图标
Component: HelloSettingPage,
})
}
}
```
### 多层级路由
```tsx | pure
import { Outlet } from 'react-router-dom'
const SettingPageLayout = () => <div>公共部分,下面是子路由的出口: <div><Outlet /></div></div>;
class HelloPlugin extends Plugin {
async load() {
this.app.pluginSettingsManager.add('hello', {
title: 'HelloWorld', // 设置页面的标题和菜单名称
icon: '', // 菜单图标
Component: SettingPageLayout
})
this.app.pluginSettingsManager.add('hello.demo1', {
title: 'Demo1 Page',
Component: () => <div>Demo1 Page Content</div>
})
this.app.pluginSettingsManager.add('hello.demo2', {
title: 'Demo2 Page',
Component: () => <div>Demo2 Page Content</div>
})
}
}
```
### 获取路由路径
如果想获取设置页面的跳转链接,可以通过 `getRoutePath` 方法获取。
```tsx | pure
import { useApp } from '@nocobase/client'
const app = useApp();
app.pluginSettingsManager.getRoutePath('hello'); // /admin/settings/hello
app.pluginSettingsManager.getRoutePath('hello.demo1'); // /admin/settings/hello/demo1
```
### 获取配置
如果想获取添加的配置(已进行权限过滤),可以通过 `get` 方法获取。
```tsx | pure
const app = useApp();
app.pluginSettingsManager.get('hello'); // { title: 'HelloWorld', icon: '', Component: HelloSettingPage, children: [{...}] }
```
完整示例查看 [samples/hello](https://github.com/nocobase/nocobase/blob/main/packages/plugins/%40nocobase/plugin-sample-hello/src/client/index.tsx)。

View File

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

View File

@ -1,33 +0,0 @@
# 配置中心
<img src="./settings-center/settings-tab.jpg" style="max-width: 100%;"/>
## 示例
```tsx | pure
import { SettingsCenterProvider } from '@nocobase/client';
import React, { useContext } from 'react';
const HelloTab => () => <div>Hello Tab</div>;
export default React.memo((props) => {
return (
<SettingsCenterProvider
settings={{
'sample-hello': {
title: 'Hello',
icon: 'ApiOutlined',
tabs: {
tab1: {
title: 'Hello Tab',
component: HelloTab,
},
},
},
}}
>{props.children}</SettingsCenterProvider>
);
});
```
完整示例查看 [samples/hello](https://github.com/nocobase/nocobase/tree/develop/packages/samples/hello)。

View File

@ -57,6 +57,6 @@ yarn pm remove hello
- Client - Client
- UI Schema Designer页面设计器 - UI Schema Designer页面设计器
- UI Router有自定义页面需求时 - UI Router有自定义页面需求时
- Settings Center为插件提供配置页面 - Plugin Settings Manager为插件提供配置页面
- I18n客户端国际化 - I18n客户端国际化

View File

@ -76,7 +76,7 @@ yarn pm remove hello
- Client - Client
- UI Schema Designer页面设计器 - UI Schema Designer页面设计器
- UI Router有自定义页面需求时 - UI Router有自定义页面需求时
- Settings Center为插件提供配置页面 - Plugin Settings Manager为插件提供配置页面
- I18n客户端国际化 - I18n客户端国际化
- Devtools - Devtools
- Commands自定义命令行 - Commands自定义命令行
@ -113,8 +113,6 @@ yarn pm remove hello
- UI Router - UI Router
- RouteSwitchProvider - RouteSwitchProvider
- RouteSwitch - RouteSwitch
- Settings Center
- SettingsCenterProvider
- I18n - I18n
- app.i18n - app.i18n
- useTranslation - useTranslation

View File

@ -46,7 +46,7 @@ export default defineConfig({
link: '/apis/api-client', link: '/apis/api-client',
}, },
{ {
title: 'SettingsCenter', title: 'PluginSettingsManager',
link: '#', link: '#',
}, },
], ],

View File

@ -552,8 +552,8 @@ test.describe('blcok template', () => {
await page.locator('.ant-drawer-mask').click(); await page.locator('.ant-drawer-mask').click();
//删除模板 //删除模板
await page.getByTestId('settings-center-button').click(); await page.getByTestId('plugin-settings-button').click();
await page.getByRole('button', { name: 'All plugin settings' }).click(); await page.getByLabel('ui-schema-storage').click();
await page.getByRole('menuitem', { name: 'layout Block templates' }).click(); await page.getByRole('menuitem', { name: 'layout Block templates' }).click();
await page.getByLabel('action-Action.Link-Delete-destroy-uiSchemaTemplates-table-Users_Form').click(); await page.getByLabel('action-Action.Link-Delete-destroy-uiSchemaTemplates-table-Users_Form').click();
await page.getByRole('button', { name: 'OK' }).click(); await page.getByRole('button', { name: 'OK' }).click();

View File

@ -1,6 +1,7 @@
import { expect, test } from '@nocobase/test/client'; import { expect, test } from '@nocobase/test/client';
async function waitForModalToBeHidden(page) { async function waitForModalToBeHidden(page) {
test.slow();
await page.waitForFunction(() => { await page.waitForFunction(() => {
const modal = document.querySelector('.ant-modal'); const modal = document.querySelector('.ant-modal');
if (modal) { if (modal) {
@ -57,11 +58,12 @@ test.describe('remove plugin', () => {
await page.getByPlaceholder('Search plugin').fill('Hello'); await page.getByPlaceholder('Search plugin').fill('Hello');
await expect(page.getByLabel('Hello')).toBeVisible(); await expect(page.getByLabel('Hello')).toBeVisible();
const isActive = await page.getByLabel('Hello').getByLabel('enable').isChecked(); const isActive = await page.getByLabel('Hello').getByLabel('enable').isChecked();
await expect(isActive).toBe(false); expect(isActive).toBe(false);
//将hello插件remove //将hello插件remove
await page.getByLabel('Hello').getByText('Remove').click(); await page.getByLabel('Hello').getByText('Remove').click();
await page.getByRole('button', { name: 'Yes' }).click(); await page.getByRole('button', { name: 'Yes' }).click();
//等待页面刷新结束 //等待页面刷新结束
await waitForModalToBeHidden(page);
await page.waitForLoadState('load'); await page.waitForLoadState('load');
await page.getByPlaceholder('Search plugin').fill('hello'); await page.getByPlaceholder('Search plugin').fill('hello');
await expect(page.getByLabel('Hello')).not.toBeVisible(); await expect(page.getByLabel('Hello')).not.toBeVisible();
@ -102,7 +104,7 @@ test.describe('enable & disabled plugin', () => {
await expect(page.getByLabel('Hello')).toBeVisible(); await expect(page.getByLabel('Hello')).toBeVisible();
const isActive = await page.getByLabel('Hello').getByLabel('enable').isChecked(); const isActive = await page.getByLabel('Hello').getByLabel('enable').isChecked();
expect(isActive).toBe(false); expect(isActive).toBe(false);
//激活插件 // 激活插件
await page.getByLabel('Hello').getByLabel('enable').click(); await page.getByLabel('Hello').getByLabel('enable').click();
await page.waitForTimeout(1000); // 等待1秒钟 await page.waitForTimeout(1000); // 等待1秒钟
//等待弹窗消失和页面刷新结束 //等待弹窗消失和页面刷新结束

View File

@ -9,6 +9,7 @@ import { useCollection, useCollectionManager } from '../collection-manager';
import { useResourceActionContext } from '../collection-manager/ResourceActionProvider'; import { useResourceActionContext } from '../collection-manager/ResourceActionProvider';
import { useRecord } from '../record-provider'; import { useRecord } from '../record-provider';
import { SchemaComponentOptions, useDesignable } from '../schema-component'; import { SchemaComponentOptions, useDesignable } from '../schema-component';
import { useApp } from '../application';
export const ACLContext = createContext<any>({}); export const ACLContext = createContext<any>({});
@ -35,6 +36,7 @@ export const ACLRolesCheckProvider = (props) => {
const { setDesignable } = useDesignable(); const { setDesignable } = useDesignable();
const { render } = useAppSpin(); const { render } = useAppSpin();
const api = useAPIClient(); const api = useAPIClient();
const app = useApp();
const result = useRequest<{ const result = useRequest<{
data: { data: {
snippets: string[]; snippets: string[];
@ -57,6 +59,7 @@ export const ACLRolesCheckProvider = (props) => {
if (data?.data?.role !== api.auth.role) { if (data?.data?.role !== api.auth.role) {
api.auth.setRole(data?.data?.role); api.auth.setRole(data?.data?.role);
} }
app.pluginSettingsManager.setAclSnippets(data?.data?.snippets || []);
}, },
}, },
); );

View File

@ -1,11 +1,12 @@
import { Checkbox, message, Table } from 'antd'; import { Checkbox, message, Table } from 'antd';
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAPIClient, useRequest } from '../../api-client'; import { useAPIClient, useRequest } from '../../api-client';
import { SettingsCenterContext } from '../../pm'; import { SettingsCenterContext } from '../../pm';
import { useRecord } from '../../record-provider'; import { useRecord } from '../../record-provider';
import { useCompile } from '../../schema-component';
import { useStyles } from '../style'; import { useStyles } from '../style';
import { useApp } from '../../application';
import { useCompile } from '../../schema-component';
const getParentKeys = (tree, func, path = []) => { const getParentKeys = (tree, func, path = []) => {
if (!tree) return []; if (!tree) return [];
@ -22,7 +23,7 @@ const getParentKeys = (tree, func, path = []) => {
}; };
const getChildrenKeys = (data = [], arr = []) => { const getChildrenKeys = (data = [], arr = []) => {
for (const item of data) { for (const item of data) {
arr.push(item.key); arr.push(item.aclSnippet);
if (item.children && item.children.length) getChildrenKeys(item.children, arr); if (item.children && item.children.length) getChildrenKeys(item.children, arr);
} }
return arr; return arr;
@ -35,62 +36,40 @@ export const SettingCenterProvider = (props) => {
return <SettingMenuContext.Provider value={configureItems}>{props.children}</SettingMenuContext.Provider>; return <SettingMenuContext.Provider value={configureItems}>{props.children}</SettingMenuContext.Provider>;
}; };
const formatPluginTabs = (data) => {
const tabs = [];
for (const key in data) {
const plugin = data?.[key];
for (const tabKey in plugin?.tabs || {}) {
const tab = plugin?.tabs[tabKey];
tabs.push({
pluginTitle: plugin.title,
...tab,
key: `pm.${key}.${tabKey}`,
});
}
}
return tabs;
const arr: any[] = Object.entries(data);
const pluginsTabs = [];
console.log(tabs);
arr.forEach((v) => {
const children = Object.entries(v[1].tabs).map((k: any) => {
return {
key: 'pm.' + v[0] + '.' + k[0],
title: k[1].title,
};
});
pluginsTabs.push({
title: v[1].title,
key: 'pm.' + v[0],
children,
});
});
return pluginsTabs;
};
export const SettingsCenterConfigure = () => { export const SettingsCenterConfigure = () => {
const app = useApp();
const { styles } = useStyles(); const { styles } = useStyles();
const record = useRecord(); const record = useRecord();
const api = useAPIClient(); const api = useAPIClient();
const pluginTags = useContext(SettingMenuContext);
const items: any[] = (pluginTags && formatPluginTabs(pluginTags)) || [];
const { t } = useTranslation();
const compile = useCompile(); const compile = useCompile();
const { loading, refresh, data } = useRequest<{ const settings = app.pluginSettingsManager.getList(false);
data: any; const allAclSnippets = app.pluginSettingsManager.getAclSnippets();
}>({ const [snippets, setSnippets] = useState<string[]>([]);
const allChecked = useMemo(
() => snippets.includes('pm.*') && snippets.every((item) => !item.startsWith('!pm.')),
[snippets],
);
const { t } = useTranslation();
const { loading, refresh } = useRequest(
{
resource: 'roles.snippets', resource: 'roles.snippets',
resourceOf: record.name, resourceOf: record.name,
action: 'list', action: 'list',
params: { params: {
paginate: false, paginate: false,
}, },
}); },
{
onSuccess(data) {
setSnippets(data?.data || []);
},
},
);
const resource = api.resource('roles.snippets', record.name); const resource = api.resource('roles.snippets', record.name);
const handleChange = async (checked, record) => { const handleChange = async (checked, record) => {
const childrenKeys = getChildrenKeys(record?.children, []); const childrenKeys = getChildrenKeys(record?.children, []);
const totalKeys = childrenKeys.concat(record.key); const totalKeys = childrenKeys.concat(record.aclSnippet);
if (!checked) { if (!checked) {
await resource.remove({ await resource.remove({
values: totalKeys.map((v) => '!' + v), values: totalKeys.map((v) => '!' + v),
@ -104,24 +83,18 @@ export const SettingsCenterConfigure = () => {
} }
message.success(t('Saved successfully')); message.success(t('Saved successfully'));
}; };
return ( return (
items?.length && (
<Table <Table
className={styles} className={styles}
loading={loading} loading={loading}
rowKey={'key'} rowKey={'key'}
pagination={false} pagination={false}
expandable={{
defaultExpandAllRows: true,
}}
columns={[ columns={[
{ {
dataIndex: 'title', dataIndex: 'title',
title: t('Plugin tab name'),
render: (value) => {
return compile(value);
},
},
{
dataIndex: 'pluginTitle',
title: t('Plugin name'), title: t('Plugin name'),
render: (value) => { render: (value) => {
return compile(value); return compile(value);
@ -129,15 +102,35 @@ export const SettingsCenterConfigure = () => {
}, },
{ {
dataIndex: 'accessible', dataIndex: 'accessible',
title: t('Accessible'), title: (
<>
<Checkbox
checked={allChecked}
onChange={async () => {
const values = allAclSnippets.map((v) => '!' + v);
if (!allChecked) {
await resource.remove({
values,
});
} else {
await resource.add({
values,
});
}
refresh();
message.success(t('Saved successfully'));
}}
/>{' '}
{t('Accessible')}
</>
),
render: (_, record) => { render: (_, record) => {
const checked = !data?.data?.includes('!' + record.key); const checked = !snippets.includes('!' + record.aclSnippet);
return !record.children && <Checkbox checked={checked} onChange={() => handleChange(checked, record)} />; return <Checkbox checked={checked} onChange={() => handleChange(checked, record)} />;
}, },
}, },
]} ]}
dataSource={items} dataSource={settings}
/> />
)
); );
}; };

View File

@ -8,18 +8,22 @@ import React, { ComponentType, FC, ReactElement } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { I18nextProvider } from 'react-i18next'; import { I18nextProvider } from 'react-i18next';
import { Link, NavLink, Navigate } from 'react-router-dom'; import { Link, NavLink, Navigate } from 'react-router-dom';
import { APIClient, APIClientProvider } from '../api-client';
import { i18n } from '../i18n';
import type { Plugin } from './Plugin';
import { PluginManager, PluginType } from './PluginManager'; import { PluginManager, PluginType } from './PluginManager';
import { ComponentTypeAndString, RouterManager, RouterOptions } from './RouterManager'; import { ComponentTypeAndString, RouterManager, RouterOptions } from './RouterManager';
import { WebSocketClient, WebSocketClientOptions } from './WebSocketClient'; import { WebSocketClient, WebSocketClientOptions } from './WebSocketClient';
import { PluginSettingsManager } from './PluginSettingsManager';
import { APIClient, APIClientProvider } from '../api-client';
import { i18n } from '../i18n';
import { AppComponent, BlankComponent, defaultAppComponents } from './components'; import { AppComponent, BlankComponent, defaultAppComponents } from './components';
import { compose, normalizeContainer } from './utils'; import { compose, normalizeContainer } from './utils';
import { defineGlobalDeps } from './utils/globalDeps'; import { defineGlobalDeps } from './utils/globalDeps';
import type { RequireJS } from './utils/requirejs';
import { getRequireJs } from './utils/requirejs'; import { getRequireJs } from './utils/requirejs';
import type { RequireJS } from './utils/requirejs';
import type { Plugin } from './Plugin';
declare global { declare global {
interface Window { interface Window {
define: RequireJS['define']; define: RequireJS['define'];
@ -50,6 +54,7 @@ export class Application {
public apiClient: APIClient; public apiClient: APIClient;
public components: Record<string, ComponentType> = { ...defaultAppComponents }; public components: Record<string, ComponentType> = { ...defaultAppComponents };
public pm: PluginManager; public pm: PluginManager;
public pluginSettingsManager: PluginSettingsManager;
public devDynamicImport: DevDynamicImport; public devDynamicImport: DevDynamicImport;
public requirejs: RequireJS; public requirejs: RequireJS;
public notification; public notification;
@ -57,6 +62,9 @@ export class Application {
maintained = false; maintained = false;
maintaining = false; maintaining = false;
error = null; error = null;
get pluginManager() {
return this.pm;
}
constructor(protected options: ApplicationOptions = {}) { constructor(protected options: ApplicationOptions = {}) {
this.initRequireJs(); this.initRequireJs();
@ -81,6 +89,8 @@ export class Application {
this.addReactRouterComponents(); this.addReactRouterComponents();
this.addProviders(options.providers || []); this.addProviders(options.providers || []);
this.ws = new WebSocketClient(options.ws); this.ws = new WebSocketClient(options.ws);
this.pluginSettingsManager = new PluginSettingsManager(this);
this.addRoutes();
} }
private initRequireJs() { private initRequireJs() {
@ -102,6 +112,13 @@ export class Application {
}); });
} }
private addRoutes() {
this.router.add('not-found', {
path: '*',
Component: this.components['AppNotFound'] || BlankComponent,
});
}
getComposeProviders() { getComposeProviders() {
const Providers = compose(...this.providers)(BlankComponent); const Providers = compose(...this.providers)(BlankComponent);
Providers.displayName = 'Providers'; Providers.displayName = 'Providers';

View File

@ -0,0 +1,141 @@
import { set } from 'lodash';
import { createElement } from 'react';
import { Icon } from '../icon';
import type { Application } from './Application';
import type { RouteType } from './RouterManager';
export const ADMIN_SETTINGS_KEY = 'admin.settings.';
export const ADMIN_SETTINGS_PATH = '/admin/settings/';
export const SNIPPET_PREFIX = 'pm.';
export interface PluginSettingsManagerSettingOptionsType {
title: string;
Component: RouteType['Component'];
icon?: string;
/**
* sort, the smaller the number, the higher the priority
* @default 0
*/
sort?: number;
isBookmark?: boolean;
aclSnippet?: string;
[index: string]: any;
}
export interface PluginSettingsPageType {
label?: string;
title: string;
key: string;
icon: any;
path: string;
sort?: number;
name?: string;
pluginName?: string;
isBookmark?: boolean;
children?: PluginSettingsPageType[];
[index: string]: any;
}
export class PluginSettingsManager {
protected settings: Record<string, PluginSettingsManagerSettingOptionsType> = {};
protected aclSnippets: string[] = [];
constructor(protected app: Application) {
this.app = app;
}
setAclSnippets(aclSnippets: string[]) {
this.aclSnippets = aclSnippets;
}
getAclSnippet(name: string) {
const setting = this.settings[name];
return setting?.aclSnippet ? setting.aclSnippet : `${SNIPPET_PREFIX}${name}`;
}
getRouteName(name: string) {
return `${ADMIN_SETTINGS_KEY}${name}`;
}
getRoutePath(name: string) {
return `${ADMIN_SETTINGS_PATH}${name.replaceAll('.', '/')}`;
}
add(name: string, options: PluginSettingsManagerSettingOptionsType) {
const nameArr = name.split('.');
const pluginName = nameArr[0];
this.settings[name] = { ...options, name, pluginName };
// add children
if (nameArr.length > 1) {
set(this.settings, nameArr.join('.children.'), this.settings[name]);
}
// add route
this.app.router.add(this.getRouteName(name), {
path: this.getRoutePath(name),
Component: options.Component,
});
}
remove(name: string) {
// delete self and children
Object.keys(this.settings).forEach((key) => {
if (key.startsWith(name)) {
delete this.settings[key];
this.app.router.remove(`${ADMIN_SETTINGS_KEY}${key}`);
}
});
}
hasAuth(name: string) {
return this.aclSnippets.includes(`!${this.getAclSnippet(name)}`) === false;
}
getSetting(name: string) {
return this.settings[name];
}
has(name: string) {
const hasAuth = this.hasAuth(name);
if (!hasAuth) return false;
return !!this.getSetting(name);
}
get(name: string, filterAuth = true): PluginSettingsPageType {
const isAllow = this.hasAuth(name);
const pluginSetting = this.getSetting(name);
if ((filterAuth && !isAllow) || !pluginSetting) return null;
const children = Object.keys(pluginSetting.children || {})
.sort((a, b) => a.localeCompare(b)) // sort by name
.map((key) => this.get(pluginSetting.children[key].name, filterAuth))
.filter(Boolean)
.sort((a, b) => (a.sort || 0) - (b.sort || 0));
const { title, icon, aclSnippet, ...others } = pluginSetting;
return {
...others,
aclSnippet: this.getAclSnippet(name),
title,
isAllow,
label: title,
icon: typeof icon === 'string' ? createElement(Icon, { type: icon }) : icon,
path: this.getRoutePath(name),
key: name,
children: children.length ? children : undefined,
};
}
getList(filterAuth = true): PluginSettingsPageType[] {
return Object.keys(this.settings)
.filter((item) => !item.includes('.')) // top level
.sort((a, b) => a.localeCompare(b)) // sort by name
.map((name) => this.get(name, filterAuth))
.filter(Boolean)
.sort((a, b) => (a.sort || 0) - (b.sort || 0));
}
getAclSnippets() {
return Object.keys(this.settings).map((name) => this.getAclSnippet(name));
}
}

View File

@ -16,7 +16,7 @@ describe('Application', () => {
}); });
const router: any = { type: 'memory', initialEntries: ['/'] }; const router: any = { type: 'memory', initialEntries: ['/'] };
const initialComponentsLength = 6; const initialComponentsLength = 7;
const initialProvidersLength = 2; const initialProvidersLength = 2;
it('basic', () => { it('basic', () => {
const app = new Application({ router }); const app = new Application({ router });

View File

@ -0,0 +1,142 @@
import { Application } from '../Application';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
describe('PluginSettingsManager', () => {
let app: Application;
const test = {
title: 'test title',
Component: () => null,
};
const test1 = {
title: 'test1 title',
Component: () => null,
};
const test2 = {
title: 'test2 title',
Component: () => null,
};
beforeAll(() => {
const mock = new MockAdapter(axios);
mock.onGet('pm:listEnabled').reply(200, {
data: [],
});
});
beforeEach(() => {
app = new Application({});
});
it('basic use', () => {
const name = 'test';
app.pluginSettingsManager.add(name, test);
const settingRes = { ...test, name };
const getRes = {
...test,
name,
label: test.title,
path: '/admin/settings/test',
isAllow: true,
aclSnippet: 'pm.test',
key: name,
children: undefined,
};
expect(app.pluginSettingsManager.getSetting('test')).toContain(settingRes);
expect(app.pluginSettingsManager.get('test')).toContain(getRes);
expect(app.pluginSettingsManager.hasAuth('test')).toBeTruthy();
const list = app.pluginSettingsManager.getList();
expect(list.length).toBe(1);
expect(list[0]).toContain(getRes);
});
it('multi', () => {
app.pluginSettingsManager.add('test1', test1);
app.pluginSettingsManager.add('test2', test2);
expect(app.pluginSettingsManager.get('test1')).toContain(test1);
expect(app.pluginSettingsManager.get('test2')).toContain(test2);
const list = app.pluginSettingsManager.getList();
expect(list.length).toBe(2);
expect(list[0]).toContain(test1);
expect(list[1]).toContain(test2);
});
it('nested', () => {
app.pluginSettingsManager.add('test1', test1);
app.pluginSettingsManager.add('test1.test2', test2);
expect(app.pluginSettingsManager.get('test1')).toContain(test1);
expect(app.pluginSettingsManager.get('test1.test2')).toContain(test2);
expect(app.pluginSettingsManager.get('test1').children.length).toBe(1);
expect(app.pluginSettingsManager.get('test1').children[0]).toContain(test2);
});
it('remove', () => {
app.pluginSettingsManager.add('test1', test1);
app.pluginSettingsManager.add('test1.test2', test2);
app.pluginSettingsManager.remove('test1');
expect(app.pluginSettingsManager.get('test1')).toBeFalsy();
expect(app.pluginSettingsManager.get('test1.test2')).toBeFalsy();
expect(app.pluginSettingsManager.getList().length).toBe(0);
});
it('acl', () => {
app.pluginSettingsManager.setAclSnippets(['!pm.test']);
app.pluginSettingsManager.add('test', test);
expect(app.pluginSettingsManager.get('test')).toBeFalsy();
expect(app.pluginSettingsManager.hasAuth('test')).toBeFalsy();
expect(app.pluginSettingsManager.get('test', false)).toContain({ ...test, isAllow: false });
expect(app.pluginSettingsManager.getList().length).toBe(0);
expect(app.pluginSettingsManager.getList(false).length).toBe(1);
expect(app.pluginSettingsManager.getList(false)[0]).toContain({ ...test, isAllow: false });
});
it('has', () => {
app.pluginSettingsManager.add('test', test);
expect(app.pluginSettingsManager.has('test')).toBeTruthy();
expect(app.pluginSettingsManager.has('test1')).toBeFalsy();
});
it('getAclSnippet', () => {
app.pluginSettingsManager.add('test1', test1);
app.pluginSettingsManager.add('test2', {
...test2,
aclSnippet: 'any.string',
});
expect(app.pluginSettingsManager.getAclSnippet('test1')).toBe('pm.test1');
expect(app.pluginSettingsManager.getAclSnippet('test2')).toBe('any.string');
});
it('getRouteName', () => {
app.pluginSettingsManager.add('test1', test1);
app.pluginSettingsManager.add('test1.test2', test2);
expect(app.pluginSettingsManager.getRouteName('test1')).toBe('admin.settings.test1');
expect(app.pluginSettingsManager.getRouteName('test1.test2')).toBe('admin.settings.test1.test2');
});
it('getRoutePath', () => {
app.pluginSettingsManager.add('test1', test1);
app.pluginSettingsManager.add('test1.test2', test2);
expect(app.pluginSettingsManager.getRoutePath('test1')).toBe('/admin/settings/test1');
expect(app.pluginSettingsManager.getRoutePath('test1.test2')).toBe('/admin/settings/test1/test2');
});
it('router', () => {
app.pluginSettingsManager.add('test1', test1);
app.pluginSettingsManager.add('test1.test2', test2);
expect(app.router.getRoutes()[0]).toMatchInlineSnapshot(`
{
"children": undefined,
"element": <AppNotFound />,
"path": "*",
}
`);
});
});

View File

@ -9,8 +9,11 @@ const AppError: FC<{ error: Error }> = ({ error }) => (
</div> </div>
); );
const AppNotFound: FC = () => <div>Not Found</div>;
export const defaultAppComponents = { export const defaultAppComponents = {
AppMain: MainComponent, AppMain: MainComponent,
AppSpin: Loading, AppSpin: Loading,
AppError: AppError, AppError: AppError,
AppNotFound: AppNotFound,
}; };

View File

@ -27,6 +27,7 @@ Application 提供了强大的功能,包括:
- scopes 管理 - scopes 管理
- providers 管理 - providers 管理
- 插件管理 - 插件管理
- 插件设置页面管理
### 组件管理 ### 组件管理
@ -370,6 +371,76 @@ class MyPlugin extends Plugin {
} }
``` ```
### 插件设置页面管理
#### 基础用法
```tsx | pure
import { Plugin } from '@nocobase/client';
import React from 'react';
const HelloSettingPage = () => <div>Hello Setting page</div>;
export class HelloPlugin extends Plugin {
async load() {
this.app.pluginSettingsManager.add('hello', {
title: 'Hello', // 设置页面的标题和菜单名称
icon: 'ApiOutlined', // 设置页面菜单图标
Component: HelloSettingPage,
})
}
}
```
#### 多层级路由
```tsx | pure
import { Outlet } from 'react-router-dom'
const SettingPageLayout = () => <div>公共部分,下面是子路由的出口: <div><Outlet /></div></div>;
class HelloPlugin extends Plugin {
async load() {
this.app.pluginSettingsManager.add('hello', {
title: 'HelloWorld', // 设置页面的标题和菜单名称
icon: '', // 菜单图标
Component: SettingPageLayout
})
this.app.pluginSettingsManager.add('hello.demo1', {
title: 'Demo1 Page',
Component: () => <div>Demo1 Page Content</div>
})
this.app.pluginSettingsManager.add('hello.demo2', {
title: 'Demo2 Page',
Component: () => <div>Demo2 Page Content</div>
})
}
}
```
#### 获取路由路径
如果想获取设置页面的跳转链接,可以通过 `getRoutePath` 方法获取。
```tsx | pure
import { useApp } from '@nocobase/client'
const app = useApp();
app.pluginSettingsManager.getRoutePath('hello'); // /admin/settings/hello
app.pluginSettingsManager.getRoutePath('hello.demo1'); // /admin/settings/hello/demo1
```
#### 获取配置
如果想获取添加的配置(已进行权限过滤),可以通过 `get` 方法获取。
```tsx | pure
const app = useApp();
app.pluginSettingsManager.get('hello'); // { title: 'HelloWorld', icon: '', Component: HelloSettingPage, children: [{...}] }
```
### 渲染 ### 渲染
#### Root Component #### Root Component

View File

@ -3,3 +3,4 @@ export * from './hooks';
export * from './Plugin'; export * from './Plugin';
export * from './RouterManager'; export * from './RouterManager';
export * from './utils'; export * from './utils';
export * from './PluginSettingsManager';

View File

@ -38,6 +38,7 @@ export default {
"Unconnected": "Unconnected", "Unconnected": "Unconnected",
"System settings": "System settings", "System settings": "System settings",
"System title": "System title", "System title": "System title",
"Settings": "Settings",
"Logo": "Logo", "Logo": "Logo",
"Add menu item": "Add menu item", "Add menu item": "Add menu item",
"Page": "Page", "Page": "Page",

View File

@ -42,6 +42,7 @@ export default {
'System settings': '系统设置', 'System settings': '系统设置',
'System title': '系统名称', 'System title': '系统名称',
Setting: '设置', Setting: '设置',
Settings: '设置',
Enable: '启用', Enable: '启用',
Disable: '禁用', Disable: '禁用',
On: '启用', On: '启用',

View File

@ -3,7 +3,7 @@ import { css } from '@emotion/css';
import { observer } from '@formily/reactive-react'; import { observer } from '@formily/reactive-react';
import { Button, Modal, Result, Spin } from 'antd'; import { Button, Modal, Result, Spin } from 'antd';
import React, { FC } from 'react'; import React, { FC } from 'react';
import { Navigate } from 'react-router-dom'; import { Navigate, useNavigate } from 'react-router-dom';
import { ACLPlugin } from '../acl'; import { ACLPlugin } from '../acl';
import { Application } from '../application'; import { Application } from '../application';
import { Plugin } from '../application/Plugin'; import { Plugin } from '../application/Plugin';
@ -188,6 +188,22 @@ const AppMaintainingDialog: FC<{ app: Application; error: Error }> = observer(({
); );
}); });
const AppNotFound = () => {
const navigate = useNavigate();
return (
<Result
status="404"
title="404"
subTitle="Sorry, the page you visited does not exist."
extra={
<Button onClick={() => navigate('/', { replace: true })} type="primary">
Back Home
</Button>
}
/>
);
};
export class NocoBaseBuildInPlugin extends Plugin { export class NocoBaseBuildInPlugin extends Plugin {
async afterAdd() { async afterAdd() {
this.app.addComponents({ this.app.addComponents({
@ -195,6 +211,7 @@ export class NocoBaseBuildInPlugin extends Plugin {
AppError, AppError,
AppMaintaining, AppMaintaining,
AppMaintainingDialog, AppMaintainingDialog,
AppNotFound,
}); });
await this.addPlugins(); await this.addPlugins();
} }
@ -216,6 +233,11 @@ export class NocoBaseBuildInPlugin extends Plugin {
element: <Navigate replace to="/admin" />, element: <Navigate replace to="/admin" />,
}); });
this.router.add('not-found', {
path: '*',
Component: AppNotFound,
});
this.router.add('admin', { this.router.add('admin', {
path: '/admin', path: '/admin',
Component: 'AdminLayout', Component: 'AdminLayout',

View File

@ -7,6 +7,7 @@ import { useNavigate } from 'react-router-dom';
import { DeleteOutlined, ReadOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons'; import { DeleteOutlined, ReadOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { useAPIClient } from '../api-client'; import { useAPIClient } from '../api-client';
import { useApp } from '../application';
import { PluginDetail } from './PluginDetail'; import { PluginDetail } from './PluginDetail';
import { PluginUpgradeModal } from './PluginForm/modal/PluginUpgradeModal'; import { PluginUpgradeModal } from './PluginForm/modal/PluginUpgradeModal';
import { useStyles } from './style'; import { useStyles } from './style';
@ -18,6 +19,7 @@ interface IPluginInfo extends IPluginCard {
function PluginInfo(props: IPluginInfo) { function PluginInfo(props: IPluginInfo) {
const { data, onClick } = props; const { data, onClick } = props;
const app = useApp();
const { name, displayName, isCompatible, packageName, updatable, builtIn, enabled, description, type, error } = data; const { name, displayName, isCompatible, packageName, updatable, builtIn, enabled, description, type, error } = data;
const { styles, theme } = useStyles(); const { styles, theme } = useStyles();
const navigate = useNavigate(); const navigate = useNavigate();
@ -28,7 +30,6 @@ function PluginInfo(props: IPluginInfo) {
const [enabledVal, setEnabledVal] = useState(enabled); const [enabledVal, setEnabledVal] = useState(enabled);
const reload = () => window.location.reload(); const reload = () => window.location.reload();
const title = displayName || name || packageName; const title = displayName || name || packageName;
return ( return (
<> <>
{showUploadForm && ( {showUploadForm && (
@ -92,14 +93,16 @@ function PluginInfo(props: IPluginInfo) {
</a> </a>
)} )}
{enabled ? ( {enabled ? (
app.pluginSettingsManager.has(name) && (
<a <a
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate(`/admin/settings/${name}`); navigate(app.pluginSettingsManager.getRoutePath(name));
}} }}
> >
<SettingOutlined /> {t('Setting')} <SettingOutlined /> {t('Settings')}
</a> </a>
)
) : ( ) : (
<Popconfirm <Popconfirm
key={'delete'} key={'delete'}

View File

@ -1,13 +1,12 @@
import { ApiOutlined, SettingOutlined } from '@ant-design/icons'; import { ApiOutlined, SettingOutlined } from '@ant-design/icons';
import { Button, Dropdown, MenuProps, Tooltip } from 'antd'; import { css } from '@emotion/css';
import _ from 'lodash'; import { Button, Card, Popover, Tooltip } from 'antd';
import React, { useContext, useMemo, useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useACLRoleContext } from '../acl/ACLProvider'; import { useApp } from '../application';
import { ActionContextProvider, useCompile } from '../schema-component'; import { ActionContextProvider, useCompile } from '../schema-component';
import { useToken } from '../style'; import { useToken } from '../style';
import { SettingsCenterContext, getPluginsTabs } from './index';
export const PluginManagerLink = () => { export const PluginManagerLink = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -27,56 +26,85 @@ export const PluginManagerLink = () => {
); );
}; };
const getBookmarkTabs = _.memoize((data) => {
const bookmarkTabs = [];
data.forEach((plugin) => {
const tabs = plugin.tabs;
tabs.forEach((tab) => {
tab.isBookmark && tab.isAllow && bookmarkTabs.push({ ...tab, path: `${plugin.key}/${tab.key}` });
});
});
return bookmarkTabs;
});
export const SettingsCenterDropdown = () => { export const SettingsCenterDropdown = () => {
const { snippets = [] } = useACLRoleContext();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { t } = useTranslation();
const compile = useCompile(); const compile = useCompile();
const navigate = useNavigate(); const { t } = useTranslation();
const itemData = useContext(SettingsCenterContext);
const { token } = useToken(); const { token } = useToken();
const pluginsTabs = getPluginsTabs(itemData, snippets); const navigate = useNavigate();
const bookmarkTabs = getBookmarkTabs(pluginsTabs); const app = useApp();
const menu = useMemo<MenuProps>(() => { const settings = app.pluginSettingsManager.getList();
return { const [open, setOpen] = useState(false);
items: [
...bookmarkTabs.map((tab) => ({
role: 'button',
key: `/admin/settings/${tab.path}`,
label: compile(tab.title),
})),
{ type: 'divider' },
{
role: 'button',
key: '/admin/settings',
label: t('All plugin settings'),
},
],
onClick({ key }) {
navigate(key);
},
};
}, [bookmarkTabs]);
return ( return (
<ActionContextProvider value={{ visible, setVisible }}> <ActionContextProvider value={{ visible, setVisible }}>
<Dropdown placement="bottom" menu={menu}> <Popover
open={open}
onOpenChange={(open) => {
setOpen(open);
}}
arrow={false}
content={
<div style={{ maxWidth: '21rem' }}>
<Card
bordered={false}
className={css`
box-shadow: none;
`}
style={{ boxShadow: 'none' }}
>
{settings.map((setting) => (
<Card.Grid
className={css`
cursor: pointer;
padding: 0 !important;
box-shadow: none !important;
&:hover {
border-radius: ${token.borderRadius}px;
background: rgba(0, 0, 0, 0.045);
}
`}
key={setting.pluginName}
>
<a
role="button"
aria-label={setting.name}
onClick={(e) => {
e.preventDefault();
setOpen(false);
navigate(setting.path);
}}
title={compile(setting.title)}
style={{ display: 'block', color: 'inherit', padding: token.marginSM }}
href={setting.path}
>
<div style={{ fontSize: '1.2rem', textAlign: 'center', marginBottom: '0.3rem' }}>
{setting.icon || <SettingOutlined />}
</div>
<div
style={{
textAlign: 'center',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
fontSize: token.fontSizeSM,
}}
>
{compile(setting.title)}
</div>
</a>
</Card.Grid>
))}
</Card>
</div>
}
>
<Button <Button
data-testid="settings-center-button" data-testid="plugin-settings-button"
icon={<SettingOutlined style={{ color: token.colorTextHeaderMenu }} />} icon={<SettingOutlined style={{ color: token.colorTextHeaderMenu }} />}
// title={t('All plugin settings')} // title={t('All plugin settings')}
/> />
</Dropdown> </Popover>
</ActionContextProvider> </ActionContextProvider>
); );
}; };

View File

@ -1,135 +1,97 @@
export * from './PluginManagerLink';
import { PageHeader } from '@ant-design/pro-layout'; import { PageHeader } from '@ant-design/pro-layout';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Layout, Menu, Result, Tabs } from 'antd'; import { Layout, Menu, Result } from 'antd';
import _, { sortBy } from 'lodash'; import _, { get } from 'lodash';
import React, { createContext, useContext, useMemo } from 'react'; import React, { createContext, useCallback, useMemo } from 'react';
import { Navigate, useNavigate, useParams } from 'react-router-dom'; import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { useACLRoleContext } from '../acl/ACLProvider';
import { ACLPane } from '../acl/ACLShortcut';
import { CollectionManagerPane } from '../collection-manager';
import { Icon } from '../icon';
import { useCompile } from '../schema-component';
import { BlockTemplatesPane } from '../schema-templates';
import { SystemSettingsPane } from '../system-settings';
import { useStyles } from './style'; import { useStyles } from './style';
import { ADMIN_SETTINGS_PATH, PluginSettingsPageType, useApp } from '../application';
import { useCompile } from '../schema-component';
export const SettingsCenterContext = createContext<any>({}); export const SettingsCenterContext = createContext<any>({});
export const settings = { function getMenuItems(list: PluginSettingsPageType[]) {
acl: { return list.map((item) => {
title: '{{t("ACL")}}',
icon: 'LockOutlined',
tabs: {
roles: {
isBookmark: true,
title: '{{t("Roles & Permissions")}}',
component: () => <ACLPane />,
},
},
},
'ui-schema-storage': {
title: '{{t("Block templates")}}',
icon: 'LayoutOutlined',
tabs: {
'block-templates': {
title: '{{t("Block templates")}}',
component: BlockTemplatesPane,
},
},
},
'collection-manager': {
icon: 'DatabaseOutlined',
title: '{{t("Collection manager")}}',
tabs: {
collections: {
isBookmark: true,
title: '{{t("Collections & Fields")}}',
component: CollectionManagerPane,
},
},
},
'system-settings': {
icon: 'SettingOutlined',
title: '{{t("System settings")}}',
tabs: {
'system-settings': {
isBookmark: true,
title: '{{t("System settings")}}',
component: SystemSettingsPane,
},
},
},
};
export const getPluginsTabs = _.memoize((items, snippets) => {
const pluginsTabs = Object.keys(items).map((plugin) => {
const tabsObj = items[plugin].tabs;
const tabs = sortBy(
Object.keys(tabsObj).map((tab) => {
return { return {
key: tab, key: item.name,
...tabsObj[tab], label: item.label,
isAllow: snippets.includes('pm.*') && !snippets?.includes(`!pm.${plugin}.${tab}`), title: item.title,
}; icon: item.icon,
}), children: item.children?.length ? getMenuItems(item.children) : undefined,
(o) => !o.isAllow,
);
return {
...items[plugin],
key: plugin,
tabs,
isAllow: !tabs.every((v) => !v.isAllow),
}; };
}); });
return sortBy(pluginsTabs, (o) => !o.isAllow); }
});
export const SettingsCenter = () => { export const SettingsCenterComponent = () => {
const { styles } = useStyles(); const { styles, theme } = useStyles();
const { snippets = [] } = useACLRoleContext(); const app = useApp();
const params = useParams<any>();
const navigate = useNavigate(); const navigate = useNavigate();
const items = useContext(SettingsCenterContext); const location = useLocation();
const pluginsTabs = getPluginsTabs(items, snippets);
const compile = useCompile(); const compile = useCompile();
const firstUri = useMemo(() => { const settings = useMemo(() => {
const pluginName = pluginsTabs[0].key; const list = app.pluginSettingsManager.getList();
const tabName = pluginsTabs[0].tabs[0].key; // compile title
return `/admin/settings/${pluginName}/${tabName}`; function traverse(settings: PluginSettingsPageType[]) {
}, [pluginsTabs]); settings.forEach((item) => {
const { pluginName, tabName } = params; item.title = compile(item.title);
const activePlugin = pluginsTabs.find((v) => v.key === pluginName); item.label = compile(item.title);
const aclPluginTabCheck = activePlugin?.isAllow && activePlugin.tabs.find((v) => v.key === tabName)?.isAllow; if (item.children?.length) {
if (!pluginName) { traverse(item.children);
return <Navigate replace to={firstUri} />;
} }
if (!items[pluginName]) {
return <Navigate replace to={firstUri} />;
}
if (!tabName) {
const firstTabName = Object.keys(items[pluginName]?.tabs).shift();
return <Navigate replace to={`/admin/settings/${pluginName}/${firstTabName}`} />;
}
const component = items[pluginName]?.tabs?.[tabName]?.component;
const plugin: any = pluginsTabs.find((v) => v.key === pluginName);
const menuItems: any = pluginsTabs
.filter((plugin) => plugin.isAllow)
.map((plugin) => {
return {
label: compile(plugin.title),
key: plugin.key,
icon: plugin.icon ? <Icon type={plugin.icon} /> : null,
};
}); });
}
traverse(list);
return list;
}, [app.pluginSettingsManager, compile]);
const getFirstDeepChildPath = useCallback((settings: PluginSettingsPageType[]) => {
if (!settings || !settings.length) {
return '/admin';
}
const first = settings[0];
if (first.children?.length) {
return getFirstDeepChildPath(first.children);
}
return first.path;
}, []);
const settingsMapByPath = useMemo<Record<string, PluginSettingsPageType>>(() => {
const map = {};
const traverse = (settings: PluginSettingsPageType[]) => {
settings.forEach((item) => {
map[item.path] = item;
if (item.children?.length) {
traverse(item.children);
}
});
};
traverse(settings);
return map;
}, [settings]);
const currentSetting = useMemo(() => settingsMapByPath[location.pathname], [location.pathname, settingsMapByPath]);
const currentPlugin = useMemo(() => {
if (!currentSetting) {
return null;
}
return settings.find((item) => item.name === currentSetting.pluginName);
}, [currentSetting, settings]);
const sidebarMenus = useMemo(() => {
return getMenuItems(settings.map((item) => ({ ...item, children: null })));
}, [settings]);
if (!currentSetting || location.pathname === ADMIN_SETTINGS_PATH || location.pathname === ADMIN_SETTINGS_PATH + '/') {
return <Navigate replace to={getFirstDeepChildPath(settings)} />;
}
if (location.pathname === currentPlugin.path && currentPlugin.children?.length > 0) {
return <Navigate replace to={getFirstDeepChildPath(currentPlugin.children)} />;
}
return ( return (
<div> <div>
<Layout> <Layout>
<Layout.Sider <Layout.Sider
className={css` className={css`
height: 100%; height: 100%;
/* position: fixed;
padding-top: 46px; */
left: 0; left: 0;
top: 0; top: 0;
background: rgba(0, 0, 0, 0); background: rgba(0, 0, 0, 0);
@ -144,44 +106,46 @@ export const SettingsCenter = () => {
theme={'light'} theme={'light'}
> >
<Menu <Menu
selectedKeys={[pluginName]} selectedKeys={[currentSetting?.pluginName]}
style={{ height: 'calc(100vh - 46px)', overflowY: 'auto', overflowX: 'hidden' }} style={{ height: 'calc(100vh - 46px)', overflowY: 'auto', overflowX: 'hidden' }}
onClick={(e) => { onClick={({ key }) => {
const item = items[e.key]; const plugin = settings.find((item) => item.name === key);
const tabKey = Object.keys(item.tabs).shift(); if (plugin.children?.length) {
navigate(`/admin/settings/${e.key}/${tabKey}`); return navigate(getFirstDeepChildPath(plugin.children));
} else {
return navigate(plugin.path);
}
}} }}
items={menuItems as any} items={sidebarMenus}
/> />
</Layout.Sider> </Layout.Sider>
<Layout.Content> <Layout.Content>
{aclPluginTabCheck && ( {currentSetting && (
<PageHeader <PageHeader
className={styles.pageHeader} className={styles.pageHeader}
ghost={false} style={{
title={compile(items[pluginName]?.title)} paddingBottom: currentPlugin.children?.length > 0 ? 0 : theme.paddingSM,
footer={
<Tabs
activeKey={tabName}
onChange={(activeKey) => {
navigate(`/admin/settings/${pluginName}/${activeKey}`);
}} }}
items={plugin.tabs?.map((tab) => { ghost={false}
if (!tab.isAllow) { title={currentPlugin.title}
return null; footer={
} currentPlugin.children?.length > 0 && (
return { <Menu
label: compile(tab?.title), style={{ marginLeft: -theme.margin }}
key: tab.key, onClick={({ key }) => {
}; navigate(app.pluginSettingsManager.getRoutePath(key));
})} }}
/> selectedKeys={[currentSetting?.name]}
mode="horizontal"
items={getMenuItems(currentPlugin.children)}
></Menu>
)
} }
/> />
)} )}
<div className={styles.pageContent}> <div className={styles.pageContent}>
{aclPluginTabCheck ? ( {currentSetting ? (
component && React.createElement(component) <Outlet />
) : ( ) : (
<Result status="404" title="404" subTitle="Sorry, the page you visited does not exist." /> <Result status="404" title="404" subTitle="Sorry, the page you visited does not exist." />
)} )}
@ -191,15 +155,3 @@ export const SettingsCenter = () => {
</div> </div>
); );
}; };
export const SettingsCenterProvider = (props) => {
const { settings = {} } = props;
const items = useContext(SettingsCenterContext);
return (
<SettingsCenterContext.Provider value={{ ...items, ...settings }}>{props.children}</SettingsCenterContext.Provider>
);
};
export const PMProvider = (props) => {
return <SettingsCenterProvider settings={settings}>{props.children}</SettingsCenterProvider>;
};

View File

@ -1,8 +1,14 @@
import React from 'react'; import React from 'react';
import { Plugin } from '../application/Plugin'; import { Plugin } from '../application/Plugin';
import { PluginManagerLink, SettingsCenterDropdown } from './PluginManagerLink'; import { PluginManagerLink, SettingsCenterDropdown } from './PluginManagerLink';
import { PMProvider, SettingsCenter } from './PluginSetting'; import { SettingsCenterComponent } from './PluginSetting';
import { PluginManager } from './PluginManager'; import { PluginManager } from './PluginManager';
import { ACLPane } from '../acl/ACLShortcut';
import { CollectionManagerPane } from '../collection-manager';
import { BlockTemplatesPane } from '../schema-templates';
import { SystemSettingsPane } from '../system-settings';
import { ADMIN_SETTINGS_PATH } from '../application';
import { Outlet } from 'react-router-dom';
export * from './PluginManagerLink'; export * from './PluginManagerLink';
export * from './PluginSetting'; export * from './PluginSetting';
@ -12,7 +18,43 @@ export class PMPlugin extends Plugin {
async load() { async load() {
this.addComponents(); this.addComponents();
this.addRoutes(); this.addRoutes();
this.app.use(PMProvider); this.addSettings();
}
addSettings() {
this.app.pluginSettingsManager.add('acl', {
title: '{{t("ACL")}}',
icon: 'LockOutlined',
Component: ACLPane,
aclSnippet: 'pm.acl.roles',
isBookmark: true,
});
this.app.pluginSettingsManager.add('ui-schema-storage', {
title: '{{t("Block templates")}}',
icon: 'LayoutOutlined',
Component: BlockTemplatesPane,
isBookmark: true,
aclSnippet: 'pm.ui-schema-storage.block-templates',
});
this.app.pluginSettingsManager.add('system-settings', {
icon: 'SettingOutlined',
title: '{{t("System settings")}}',
Component: SystemSettingsPane,
isBookmark: true,
aclSnippet: 'pm.system-settings.system-settings',
});
this.app.pluginSettingsManager.add('collection-manager', {
icon: 'DatabaseOutlined',
title: '{{t("Collection manager")}}',
Component: () => <Outlet />,
isBookmark: true,
});
this.app.pluginSettingsManager.add('collection-manager.collections', {
title: '{{t("Collections & Fields")}}',
Component: CollectionManagerPane,
});
} }
addComponents() { addComponents() {
@ -36,17 +78,9 @@ export class PMPlugin extends Plugin {
element: <PluginManager />, element: <PluginManager />,
}); });
this.app.router.add('admin.settings.list', { this.app.router.add('admin.settings', {
path: '/admin/settings', path: ADMIN_SETTINGS_PATH,
element: <SettingsCenter />, element: <SettingsCenterComponent />,
});
this.app.router.add('admin.settings.pluginName', {
path: '/admin/settings/:pluginName',
element: <SettingsCenter />,
});
this.app.router.add('admin.settings.pluginName-tabName', {
path: '/admin/settings/:pluginName/:tabName',
element: <SettingsCenter />,
}); });
} }
} }

View File

@ -7,9 +7,9 @@ export const useStyles = createStyles(({ token }) => {
cursor: 'not-allowed', cursor: 'not-allowed',
}, },
pageHeader: { pageHeader: {
paddingBottom: 0,
backgroundColor: token.colorBgContainer, backgroundColor: token.colorBgContainer,
paddingTop: token.paddingSM, paddingTop: token.paddingSM,
paddingBottom: 0,
paddingInline: token.paddingLG, paddingInline: token.paddingLG,
'.ant-page-header-footer': { marginBlockStart: '0' }, '.ant-page-header-footer': { marginBlockStart: '0' },
'& .ant-tabs-nav': { '& .ant-tabs-nav': {

View File

@ -25,7 +25,7 @@ export const ColorSelect = connect(
return ( return (
<Select {...props}> <Select {...props}>
{Object.keys(colors).map((color) => ( {Object.keys(colors).map((color) => (
<Select.Option value={color}> <Select.Option key={color} value={color}>
<Tag color={color}>{compile(colors[color] || colors.default)}</Tag> <Tag color={color}>{compile(colors[color] || colors.default)}</Tag>
</Select.Option> </Select.Option>
))} ))}

View File

@ -50,18 +50,18 @@ export const Sortable = (props: any) => {
const useSortableItemProps = (props) => { const useSortableItemProps = (props) => {
const id = useSortableItemId(props); const id = useSortableItemId(props);
const schema = useFieldSchema();
if (props.schema) { if (props.schema) {
return { ...props, id }; return { ...props, id };
} }
const schema = useFieldSchema();
return { ...props, id, schema }; return { ...props, id, schema };
}; };
const useSortableItemId = (props) => { const useSortableItemId = (props) => {
const field = useField();
if (props.id) { if (props.id) {
return props.id; return props.id;
} }
const field = useField();
return field.address.toString(); return field.address.toString();
}; };

View File

@ -1,9 +1,9 @@
import { RightOutlined } from '@ant-design/icons'; import { RightOutlined } from '@ant-design/icons';
import { Plugin, SettingsCenterProvider } from '@nocobase/client'; import { Plugin } from '@nocobase/client';
import { Button, Tooltip } from 'antd'; import { Button, Tooltip } from 'antd';
import { createStyles } from 'antd-style'; import { createStyles } from 'antd-style';
import React, { lazy } from 'react'; import React, { lazy } from 'react';
import { useTranslation } from '../locale'; import { NAMESPACE } from '../locale';
const DOCUMENTATION_PATH = '/api-documentation'; const DOCUMENTATION_PATH = '/api-documentation';
const Documentation = lazy(() => import('./Document')); const Documentation = lazy(() => import('./Document'));
@ -37,32 +37,15 @@ const SCDocumentation = () => {
); );
}; };
const APIDocumentationProvider = React.memo((props) => {
const { t } = useTranslation();
return (
<SettingsCenterProvider
settings={{
['api-doc']: {
title: t('API documentation'),
icon: 'BookOutlined',
tabs: {
documentation: {
title: t('Documentation'),
component: SCDocumentation,
},
},
},
}}
>
{props.children}
</SettingsCenterProvider>
);
});
APIDocumentationProvider.displayName = 'APIDocumentationProvider';
export class APIDocumentationPlugin extends Plugin { export class APIDocumentationPlugin extends Plugin {
async load() { async load() {
this.app.use(APIDocumentationProvider); this.app.pluginSettingsManager.add(NAMESPACE, {
title: `{{t("API documentation", { ns: "${NAMESPACE}" })}}`,
icon: 'BookOutlined',
Component: SCDocumentation,
aclSnippet: 'pm.api-doc.documentation',
});
this.app.router.add('api-documentation', { this.app.router.add('api-documentation', {
path: DOCUMENTATION_PATH, path: DOCUMENTATION_PATH,
Component: Documentation, Component: Documentation,

View File

@ -1,5 +1,5 @@
export default { export default {
'API documentation': 'Api 文档', 'API documentation': 'API 文档',
'Documentation': '文档', 'Documentation': '文档',
'Select a definition': '选择端点', 'Select a definition': '选择端点',
}; };

View File

@ -1,34 +1,15 @@
import { Plugin, SchemaComponentOptions, SettingsCenterProvider } from '@nocobase/client'; import { Plugin } from '@nocobase/client';
import React from 'react'; import { NAMESPACE } from '../constants';
import { Configuration } from './Configuration'; import { Configuration } from './Configuration';
import { useTranslation } from './locale';
const ApiKeysProvider = React.memo((props) => {
const { t } = useTranslation();
return (
<SettingsCenterProvider
settings={{
['api-keys']: {
title: t('API keys'),
icon: 'EnvironmentOutlined',
tabs: {
configuration: {
title: t('Keys manager'),
component: Configuration,
},
},
},
}}
>
<SchemaComponentOptions components={{}}>{props.children}</SchemaComponentOptions>
</SettingsCenterProvider>
);
});
ApiKeysProvider.displayName = 'ApiKeysProvider';
class APIKeysPlugin extends Plugin { class APIKeysPlugin extends Plugin {
async load() { async load() {
this.app.addProvider(ApiKeysProvider); this.app.pluginSettingsManager.add(NAMESPACE, {
icon: 'KeyOutlined',
title: '{{t("API keys", {"ns": "api-keys"})}}',
Component: Configuration,
aclSnippet: 'pm.api-keys.configuration',
});
} }
} }

View File

@ -5,7 +5,7 @@ export default {
namespace: 'api-keys', namespace: 'api-keys',
duplicator: 'optional', duplicator: 'optional',
name: 'apiKeys', name: 'apiKeys',
title: '{{t("API keys")}}', title: '{{t("API keys", {"ns": "api-keys"})}}',
sortable: 'sort', sortable: 'sort',
model: 'ApiKeyModel', model: 'ApiKeyModel',
createdBy: true, createdBy: true,

View File

@ -1,14 +1,15 @@
const locale = { const locale = {
'API key created successfully': 'API key 创建成功', 'API keys': 'API 密钥',
'API key created successfully': 'API 密钥创建成功',
'Make sure to copy your personal access key now as you will not be able to see this again.': 'Make sure to copy your personal access key now as you will not be able to see this again.':
'请确保现在复制你的个人访问密钥,因为你将无法再次看到这个密钥。', '请确保现在复制你的个人访问密钥,因为你将无法再次看到这个密钥。',
'Key name': '密钥名称', 'Key name': '密钥名称',
Expiration: '过期时间', Expiration: '过期时间',
'Delete API key': '删除 API key', 'Delete API key': '删除 API 密钥',
Role: '角色', Role: '角色',
'Keys manager': '密钥管理', 'Keys manager': '密钥管理',
'Created at': '创建时间', 'Created at': '创建时间',
'Add API key': '添加 API key', 'Add API key': '添加 API 密钥',
Never: '永不', Never: '永不',
Custom: '自定义', Custom: '自定义',
'Never expires': '永不过期', 'Never expires': '永不过期',

View File

@ -1,34 +1,14 @@
import { import { OptionsComponentProvider, SigninPageProvider, SignupPageProvider } from '@nocobase/client';
OptionsComponentProvider,
SettingsCenterProvider,
SigninPageProvider,
SignupPageProvider,
} from '@nocobase/client';
import React, { FC } from 'react'; import React, { FC } from 'react';
import { Authenticator } from './settings/Authenticator';
import SigninPage from './basic/SigninPage';
import { presetAuthType } from '../preset'; import { presetAuthType } from '../preset';
import { Options } from './basic/Options';
import SigninPage from './basic/SigninPage';
import SignupPage from './basic/SignupPage'; import SignupPage from './basic/SignupPage';
import { useAuthTranslation } from './locale'; import { useAuthTranslation } from './locale';
import { Options } from './basic/Options';
export const AuthPluginProvider: FC = (props) => { export const AuthPluginProvider: FC = (props) => {
const { t } = useAuthTranslation(); const { t } = useAuthTranslation();
return ( return (
<SettingsCenterProvider
settings={{
auth: {
title: t('Authentication'),
icon: 'LoginOutlined',
tabs: {
authenticators: {
title: t('Authenticators'),
component: () => <Authenticator />,
},
},
},
}}
>
<OptionsComponentProvider authType={presetAuthType} component={Options}> <OptionsComponentProvider authType={presetAuthType} component={Options}>
<SigninPageProvider authType={presetAuthType} tabTitle={t('Sign in via password')} component={SigninPage}> <SigninPageProvider authType={presetAuthType} tabTitle={t('Sign in via password')} component={SigninPage}>
<SignupPageProvider authType={presetAuthType} component={SignupPage}> <SignupPageProvider authType={presetAuthType} component={SignupPage}>
@ -36,6 +16,5 @@ export const AuthPluginProvider: FC = (props) => {
</SignupPageProvider> </SignupPageProvider>
</SigninPageProvider> </SigninPageProvider>
</OptionsComponentProvider> </OptionsComponentProvider>
</SettingsCenterProvider>
); );
}; };

View File

@ -1,9 +1,17 @@
import { Plugin } from '@nocobase/client'; import { Plugin } from '@nocobase/client';
import { AuthPluginProvider } from './AuthPluginProvider'; import { AuthPluginProvider } from './AuthPluginProvider';
import { AuthProvider } from './AuthProvider'; import { AuthProvider } from './AuthProvider';
import { NAMESPACE } from './locale';
import { Authenticator } from './settings/Authenticator';
export class AuthPlugin extends Plugin { export class AuthPlugin extends Plugin {
async load() { async load() {
this.app.pluginSettingsManager.add(NAMESPACE, {
icon: 'LoginOutlined',
title: `{{t("Authentication", { ns: "${NAMESPACE}" })}}`,
Component: Authenticator,
aclSnippet: 'pm.auth.authenticators',
});
this.app.providers.unshift([AuthProvider, {}]); this.app.providers.unshift([AuthProvider, {}]);
this.app.use(AuthPluginProvider); this.app.use(AuthPluginProvider);
} }

View File

@ -4,7 +4,6 @@ import {
Plugin, Plugin,
SchemaComponentOptions, SchemaComponentOptions,
SchemaInitializerContext, SchemaInitializerContext,
SettingsCenterProvider,
useAPIClient, useAPIClient,
} from '@nocobase/client'; } from '@nocobase/client';
import JSON5 from 'json5'; import JSON5 from 'json5';
@ -13,7 +12,7 @@ import { ChartBlockEngine } from './ChartBlockEngine';
import { ChartBlockInitializer } from './ChartBlockInitializer'; import { ChartBlockInitializer } from './ChartBlockInitializer';
import { ChartQueryMetadataProvider } from './ChartQueryMetadataProvider'; import { ChartQueryMetadataProvider } from './ChartQueryMetadataProvider';
import './Icons'; import './Icons';
import { lang } from './locale'; import { lang, NAMESPACE } from './locale';
import { CustomSelect } from './select'; import { CustomSelect } from './select';
import { QueriesTable } from './settings/QueriesTable'; import { QueriesTable } from './settings/QueriesTable';
@ -53,7 +52,7 @@ const ChartsProvider = React.memo((props) => {
key: 'chart', key: 'chart',
type: 'item', type: 'item',
icon: 'PieChartOutlined', icon: 'PieChartOutlined',
title: '{{t("Chart (Old)",{ns:"charts"})}}', title: `{{t("Chart (Old)", { ns: "${NAMESPACE}" })}}`,
component: 'ChartBlockInitializer', component: 'ChartBlockInitializer',
}); });
} }
@ -78,27 +77,12 @@ const ChartsProvider = React.memo((props) => {
}; };
return ( return (
<ChartQueryMetadataProvider> <ChartQueryMetadataProvider>
<SettingsCenterProvider
settings={{
charts: {
title: '{{t("Charts", {ns:"charts"})}}',
icon: 'PieChartOutlined',
tabs: {
queries: {
title: '{{t("Queries", {ns:"charts"})}}',
component: QueriesTable,
},
},
},
}}
>
<SchemaComponentOptions <SchemaComponentOptions
scope={{ validateSQL }} scope={{ validateSQL }}
components={{ CustomSelect, ChartBlockInitializer, ChartBlockEngine }} components={{ CustomSelect, ChartBlockInitializer, ChartBlockEngine }}
> >
<SchemaInitializerContext.Provider value={items}>{props.children}</SchemaInitializerContext.Provider> <SchemaInitializerContext.Provider value={items}>{props.children}</SchemaInitializerContext.Provider>
</SchemaComponentOptions> </SchemaComponentOptions>
</SettingsCenterProvider>
</ChartQueryMetadataProvider> </ChartQueryMetadataProvider>
); );
}); });
@ -110,6 +94,12 @@ export class ChartsPlugin extends Plugin {
} }
async load() { async load() {
this.app.use(ChartsProvider); this.app.use(ChartsProvider);
this.app.pluginSettingsManager.add(NAMESPACE, {
title: `{{t("Charts", { ns: "${NAMESPACE}" })}}`,
icon: 'PieChartOutlined',
Component: QueriesTable,
aclSnippet: 'pm.charts.queries',
});
} }
} }

View File

@ -1,36 +0,0 @@
import { SettingsCenterProvider } from '@nocobase/client';
import { Card } from 'antd';
import React, { FC } from 'react';
const DuplicatorPanel = () => {
return (
<Card bordered={false}>
<div>hello world</div>
</Card>
);
};
export const DuplicatorProvider: FC = function (props) {
return (
<SettingsCenterProvider
settings={
{
// duplicator: {
// title: '应用导入导出',
// icon: 'CloudDownloadOutlined',
// tabs: {
// tab1: {
// title: '应用导入导出',
// component: DuplicatorPanel,
// },
// },
// },
}
}
>
{props.children}
</SettingsCenterProvider>
);
};
DuplicatorProvider.displayName = 'DuplicatorProvider';

View File

@ -1,10 +1,7 @@
import { Plugin } from '@nocobase/client'; import { Plugin } from '@nocobase/client';
import { DuplicatorProvider } from './DuplicatorProvider';
export class DuplicatorPlugin extends Plugin { export class DuplicatorPlugin extends Plugin {
async load() { async load() {}
this.app.use(DuplicatorProvider);
}
} }
export default DuplicatorPlugin; export default DuplicatorPlugin;

View File

@ -6,16 +6,13 @@ import {
SchemaComponentOptions, SchemaComponentOptions,
SchemaInitializerContext, SchemaInitializerContext,
SchemaInitializerProvider, SchemaInitializerProvider,
SettingsCenterProvider,
useCollection, useCollection,
} from '@nocobase/client'; } from '@nocobase/client';
import { forEach } from '@nocobase/utils/client'; import { forEach } from '@nocobase/utils/client';
import React, { FC, useContext } from 'react'; import React, { FC, useContext } from 'react';
import { FileStoragePane } from './FileStorage';
import * as hooks from './hooks'; import * as hooks from './hooks';
import * as initializers from './initializers'; import * as initializers from './initializers';
import { attachment } from './interfaces/attachment'; import { attachment } from './interfaces/attachment';
import { NAMESPACE } from './locale';
import * as templates from './templates'; import * as templates from './templates';
// 注册之后就可以在 Crete collection 按钮中选择创建了 // 注册之后就可以在 Crete collection 按钮中选择创建了
@ -52,20 +49,6 @@ export const FileManagerProvider: FC = (props) => {
const ctx = useContext(PluginManagerContext); const ctx = useContext(PluginManagerContext);
return ( return (
<SettingsCenterProvider
settings={{
'file-manager': {
title: `{{t("File manager", { ns: "${NAMESPACE}" })}}`,
icon: 'FileOutlined',
tabs: {
storages: {
title: `{{t("File storage", { ns: "${NAMESPACE}" })}}`,
component: FileStoragePane,
},
},
},
}}
>
<PluginManagerContext.Provider <PluginManagerContext.Provider
value={{ value={{
components: { components: {
@ -79,6 +62,5 @@ export const FileManagerProvider: FC = (props) => {
</SchemaComponentOptions> </SchemaComponentOptions>
</CollectionManagerProvider> </CollectionManagerProvider>
</PluginManagerContext.Provider> </PluginManagerContext.Provider>
</SettingsCenterProvider>
); );
}; };

View File

@ -12,7 +12,7 @@ test.describe('file manager', () => {
// 1、前置条件已登录 // 1、前置条件已登录
// 2、测试步骤进入“文件管理器”-“新建”按钮,填写表单,点击“确定”按钮 // 2、测试步骤进入“文件管理器”-“新建”按钮,填写表单,点击“确定”按钮
await page.goto('/admin/settings/file-manager/storages'); await page.goto('/admin/settings/file-manager');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: 'plus Add new' }).hover(); await page.getByRole('button', { name: 'plus Add new' }).hover();
await page.getByRole('menuitem', { name: 'Local storage' }).click(); await page.getByRole('menuitem', { name: 'Local storage' }).click();

View File

@ -10,7 +10,7 @@ test.describe('File manager', () => {
let caseTitle = 'edit local storage title'; let caseTitle = 'edit local storage title';
// 1、前置条件1.1已登录;1.2存在一个文件管理器 // 1、前置条件1.1已登录;1.2存在一个文件管理器
await page.goto('/admin/settings/file-manager/storages'); await page.goto('/admin/settings/file-manager');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
await page.getByRole('button', { name: 'plus Add new' }).hover(); await page.getByRole('button', { name: 'plus Add new' }).hover();
await page.getByRole('menuitem', { name: 'Local storage' }).click(); await page.getByRole('menuitem', { name: 'Local storage' }).click();

View File

@ -1,5 +1,7 @@
import { Plugin } from '@nocobase/client'; import { Plugin } from '@nocobase/client';
import { FileManagerProvider } from './FileManagerProvider'; import { FileManagerProvider } from './FileManagerProvider';
import { FileStoragePane } from './FileStorage';
import { NAMESPACE } from './locale';
import { storageTypes } from './schemas/storageTypes'; import { storageTypes } from './schemas/storageTypes';
export class FileManagerPlugin extends Plugin { export class FileManagerPlugin extends Plugin {
@ -7,6 +9,12 @@ export class FileManagerPlugin extends Plugin {
async load() { async load() {
this.app.use(FileManagerProvider); this.app.use(FileManagerProvider);
this.app.pluginSettingsManager.add(NAMESPACE, {
title: `{{t("File manager", { ns: "${NAMESPACE}" })}}`,
icon: 'FileOutlined',
Component: FileStoragePane,
aclSnippet: 'pm.file-manager.storages',
});
Object.values(storageTypes).forEach((storageType) => { Object.values(storageTypes).forEach((storageType) => {
this.registerStorageType(storageType.name, storageType); this.registerStorageType(storageType.name, storageType);
}); });

View File

@ -1,20 +1,10 @@
import { PluginManagerContext, SettingsCenterContext, SettingsCenterProvider } from '@nocobase/client'; import { PluginManagerContext } from '@nocobase/client';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { GraphCollectionPane } from './GraphCollectionShortcut';
import { useGCMTranslation } from './utils';
export const GraphCollectionProvider = React.memo((props) => { export const GraphCollectionProvider = React.memo((props) => {
const ctx = useContext(PluginManagerContext); const ctx = useContext(PluginManagerContext);
const { t } = useGCMTranslation();
const items = useContext(SettingsCenterContext);
items['collection-manager']['tabs']['graph'] = {
title: t('Graphical interface'),
component: GraphCollectionPane,
};
return ( return (
<SettingsCenterProvider settings={items}>
<PluginManagerContext.Provider <PluginManagerContext.Provider
value={{ value={{
components: { components: {
@ -24,7 +14,6 @@ export const GraphCollectionProvider = React.memo((props) => {
> >
{props.children} {props.children}
</PluginManagerContext.Provider> </PluginManagerContext.Provider>
</SettingsCenterProvider>
); );
}); });
GraphCollectionProvider.displayName = 'GraphCollectionProvider'; GraphCollectionProvider.displayName = 'GraphCollectionProvider';

View File

@ -1,9 +1,17 @@
import { Plugin } from '@nocobase/client'; import { Plugin } from '@nocobase/client';
import { GraphCollectionProvider } from './GraphCollectionProvider'; import { GraphCollectionProvider } from './GraphCollectionProvider';
import { GraphCollectionPane } from './GraphCollectionShortcut';
import { NAMESPACE } from './locale';
export class GraphCollectionPlugin extends Plugin { export class GraphCollectionPlugin extends Plugin {
async load() { async load() {
this.app.use(GraphCollectionProvider); this.app.use(GraphCollectionProvider);
this.app.pluginSettingsManager.add('collection-manager.graph', {
title: `{{t("Graphical interface", { ns: "${NAMESPACE}" })}}`,
Component: GraphCollectionPane,
aclSnippet: 'pm.collection-manager.graph',
});
} }
} }

View File

@ -1,3 +1 @@
// export { default as enUS } from './en-US'; export const NAMESPACE = 'graph-collection-manager';
// export { default as zhCN } from './zh-CN';
// export { default as jaJP } from './ja-JP';

View File

@ -1,30 +1,14 @@
import { Plugin, SettingsCenterProvider } from '@nocobase/client'; import { Plugin } from '@nocobase/client';
import React from 'react';
import { Localization } from './Localization'; import { Localization } from './Localization';
import { useLocalTranslation } from './locale'; import { NAMESPACE } from './locale';
export class LocalizationManagementPlugin extends Plugin { export class LocalizationManagementPlugin extends Plugin {
async load() { async load() {
this.app.use((props) => { this.app.pluginSettingsManager.add(NAMESPACE, {
const { t } = useLocalTranslation(); title: `{{t("Localization management", { ns: "${NAMESPACE}" })}}`,
return (
<SettingsCenterProvider
settings={{
['localization-management']: {
title: t('Localization management'),
icon: 'GlobalOutlined', icon: 'GlobalOutlined',
tabs: { Component: Localization,
localization: { aclSnippet: 'pm.localization-management.localization',
title: t('Translations'),
component: () => <Localization />,
},
},
},
}}
>
{props.children}
</SettingsCenterProvider>
);
}); });
} }
} }

View File

@ -1,8 +1,8 @@
import AMapLoader from '@amap/amap-jsapi-loader'; import AMapLoader from '@amap/amap-jsapi-loader';
import '@amap/amap-jsapi-types'; import '@amap/amap-jsapi-types';
import { SyncOutlined } from '@ant-design/icons'; import { SyncOutlined } from '@ant-design/icons';
import { useField, useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { css, useCollection } from '@nocobase/client'; import { css, useApp, useCollection } from '@nocobase/client';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Alert, App, Button, Spin } from 'antd'; import { Alert, App, Button, Spin } from 'antd';
import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
@ -373,11 +373,13 @@ export const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapCompone
errMessage, errMessage,
})); }));
const app = useApp();
if (!accessKey || errMessage) { if (!accessKey || errMessage) {
return ( return (
<Alert <Alert
action={ action={
<Button type="primary" onClick={() => navigate('/admin/settings/map/configuration')}> <Button type="primary" onClick={() => navigate(app.pluginSettingsManager.getRoutePath('map'))}>
{t('Go to the configuration page')} {t('Go to the configuration page')}
</Button> </Button>
} }

View File

@ -1,7 +1,7 @@
import { SyncOutlined } from '@ant-design/icons'; import { SyncOutlined } from '@ant-design/icons';
import { useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { Loader } from '@googlemaps/js-api-loader'; import { Loader } from '@googlemaps/js-api-loader';
import { css, useAPIClient, useCollection } from '@nocobase/client'; import { css, useAPIClient, useApp, useCollection } from '@nocobase/client';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Alert, App, Button, Spin } from 'antd'; import { Alert, App, Button, Spin } from 'antd';
import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
@ -369,12 +369,16 @@ export const GoogleMapsComponent = React.forwardRef<GoogleMapForwardedRefProps,
}, },
}); });
}); });
const app = useApp();
if (!accessKey || errMessage) { if (!accessKey || errMessage) {
return ( return (
<Alert <Alert
action={ action={
<Button type="primary" onClick={() => navigate('/admin/settings/map/configuration?tab=google')}> <Button
type="primary"
onClick={() => navigate(app.pluginSettingsManager.getRoutePath('map') + '?tab=google')}
>
{t('Go to the configuration page')} {t('Go to the configuration page')}
</Button> </Button>
} }

View File

@ -1,37 +1,16 @@
import { import { CollectionManagerContext, CurrentAppInfoProvider, Plugin, SchemaComponentOptions } from '@nocobase/client';
CollectionManagerContext,
CurrentAppInfoProvider,
Plugin,
SchemaComponentOptions,
SettingsCenterProvider,
} from '@nocobase/client';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { MapBlockOptions } from './block'; import { MapBlockOptions } from './block';
import { Configuration, Map } from './components'; import { Configuration, Map } from './components';
import { interfaces } from './fields'; import { interfaces } from './fields';
import { MapInitializer } from './initialize'; import { MapInitializer } from './initialize';
import { useMapTranslation } from './locale'; import { NAMESPACE } from './locale';
const MapProvider = React.memo((props) => { const MapProvider = React.memo((props) => {
const ctx = useContext(CollectionManagerContext); const ctx = useContext(CollectionManagerContext);
const { t } = useMapTranslation();
return ( return (
<CurrentAppInfoProvider> <CurrentAppInfoProvider>
<MapInitializer> <MapInitializer>
<SettingsCenterProvider
settings={{
map: {
title: t('Map Manager'),
icon: 'EnvironmentOutlined',
tabs: {
configuration: {
title: t('Configuration'),
component: Configuration,
},
},
},
}}
>
<SchemaComponentOptions components={{ Map }}> <SchemaComponentOptions components={{ Map }}>
<MapBlockOptions> <MapBlockOptions>
<CollectionManagerContext.Provider value={{ ...ctx, interfaces: { ...ctx.interfaces, ...interfaces } }}> <CollectionManagerContext.Provider value={{ ...ctx, interfaces: { ...ctx.interfaces, ...interfaces } }}>
@ -39,7 +18,6 @@ const MapProvider = React.memo((props) => {
</CollectionManagerContext.Provider> </CollectionManagerContext.Provider>
</MapBlockOptions> </MapBlockOptions>
</SchemaComponentOptions> </SchemaComponentOptions>
</SettingsCenterProvider>
</MapInitializer> </MapInitializer>
</CurrentAppInfoProvider> </CurrentAppInfoProvider>
); );
@ -49,6 +27,12 @@ MapProvider.displayName = 'MapProvider';
export class MapPlugin extends Plugin { export class MapPlugin extends Plugin {
async load() { async load() {
this.app.use(MapProvider); this.app.use(MapProvider);
this.app.pluginSettingsManager.add(NAMESPACE, {
title: `{{t("Map Manager", { ns: "${NAMESPACE}" })}}`,
icon: 'EnvironmentOutlined',
Component: Configuration,
aclSnippet: 'pm.map.configuration',
});
} }
} }

View File

@ -1,12 +1,8 @@
import { SettingsCenterProvider } from '@nocobase/client';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { AppConfiguration, InterfaceConfiguration } from './configuration';
import { isJSBridge } from './core/bridge'; import { isJSBridge } from './core/bridge';
import { useTranslation } from './locale';
export const MobileClientProvider = React.memo((props) => { export const MobileClientProvider = React.memo((props) => {
const { t } = useTranslation();
const location = useLocation(); const location = useLocation();
const navigation = useNavigate(); const navigation = useNavigate();
@ -16,26 +12,5 @@ export const MobileClientProvider = React.memo((props) => {
} }
}, [location.pathname, navigation]); }, [location.pathname, navigation]);
return ( return <>{props.children}</>;
<SettingsCenterProvider
settings={{
['mobile-client']: {
title: t('Mobile Client-side'),
icon: 'MobileOutlined',
tabs: {
interface: {
title: t('Interface Configuration'),
component: InterfaceConfiguration,
},
app: {
title: t('App Configuration'),
component: AppConfiguration,
},
},
},
}}
>
{props.children}
</SettingsCenterProvider>
);
}); });

View File

@ -1,17 +1,38 @@
import { createRouterManager, Plugin, RouterManager, RouteSchemaComponent } from '@nocobase/client'; import { createRouterManager, Plugin, RouterManager, RouteSchemaComponent } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { Navigate } from 'react-router-dom'; import { Navigate, Outlet } from 'react-router-dom';
import { MobileClientProvider } from './MobileClientProvider'; import { MobileClientProvider } from './MobileClientProvider';
import MApplication from './router/Application'; import MApplication from './router/Application';
import { AppConfiguration, InterfaceConfiguration } from './configuration';
import { NAMESPACE } from './locale';
export class MobileClientPlugin extends Plugin { export class MobileClientPlugin extends Plugin {
public mobileRouter: RouterManager; public mobileRouter: RouterManager;
async load() { async load() {
this.setMobileRouter(); this.setMobileRouter();
this.addRoutes(); this.addRoutes();
this.addSettings();
this.app.use(MobileClientProvider); this.app.use(MobileClientProvider);
} }
addSettings() {
this.app.pluginSettingsManager.add(NAMESPACE, {
title: `{{t("Mobile Client-side", { ns: "${NAMESPACE}" })}}`,
icon: 'MobileOutlined',
Component: () => <Outlet />,
});
this.app.pluginSettingsManager.add(`${NAMESPACE}.interface`, {
title: `{{t("Interface Configuration", { ns: "${NAMESPACE}" })}}`,
Component: InterfaceConfiguration,
sort: 1,
});
this.app.pluginSettingsManager.add(`${NAMESPACE}.app`, {
title: `{{t("App Configuration", { ns: "${NAMESPACE}" })}}`,
Component: AppConfiguration,
sort: 2,
});
}
setMobileRouter() { setMobileRouter() {
const router = createRouterManager({ type: 'hash' }); const router = createRouterManager({ type: 'hash' });
router.add('root', { router.add('root', {

View File

@ -1,14 +1,7 @@
import { import { Icon, PinnedPluginListProvider, SchemaComponentOptions, useApp, useRequest } from '@nocobase/client';
Icon,
PinnedPluginListProvider,
SchemaComponentOptions,
SettingsCenterProvider,
useRequest,
} from '@nocobase/client';
import { Button, Dropdown } from 'antd'; import { Button, Dropdown } from 'antd';
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { AppManager } from './AppManager';
import { AppNameInput } from './AppNameInput'; import { AppNameInput } from './AppNameInput';
import { usePluginUtils } from './utils'; import { usePluginUtils } from './utils';
@ -25,6 +18,7 @@ const MultiAppManager = () => {
}, },
); );
const { t } = usePluginUtils(); const { t } = usePluginUtils();
const app = useApp();
const items = [ const items = [
...(data?.data || []).map((app) => { ...(data?.data || []).map((app) => {
let link = `/apps/${app.name}/admin/`; let link = `/apps/${app.name}/admin/`;
@ -42,7 +36,7 @@ const MultiAppManager = () => {
}), }),
{ {
key: '.manager', key: '.manager',
label: <Link to="/admin/settings/multi-app-manager/applications">{t('Manage applications')}</Link>, label: <Link to={app.pluginSettingsManager.getRoutePath('multi-app-manager')}>{t('Manage applications')}</Link>,
}, },
]; ];
return ( return (
@ -58,35 +52,13 @@ const MultiAppManager = () => {
}; };
export const MultiAppManagerProvider = (props) => { export const MultiAppManagerProvider = (props) => {
const { t } = usePluginUtils();
return ( return (
<PinnedPluginListProvider <PinnedPluginListProvider
items={{ items={{
am: { order: 201, component: 'MultiAppManager', pin: true }, am: { order: 201, component: 'MultiAppManager', pin: true },
}} }}
> >
<SchemaComponentOptions components={{ MultiAppManager, AppNameInput }}> <SchemaComponentOptions components={{ MultiAppManager, AppNameInput }}>{props.children}</SchemaComponentOptions>
<SettingsCenterProvider
settings={{
'multi-app-manager': {
title: t('Multi-app manager'),
icon: 'AppstoreOutlined',
tabs: {
applications: {
title: t('Applications'),
component: () => <AppManager />,
},
// settings: {
// title: 'Settings',
// component: () => <Settings />,
// },
},
},
}}
>
{props.children}
</SettingsCenterProvider>
</SchemaComponentOptions>
</PinnedPluginListProvider> </PinnedPluginListProvider>
); );
}; };

View File

@ -1,9 +1,18 @@
import { Plugin } from '@nocobase/client'; import { Plugin } from '@nocobase/client';
import { MultiAppManagerProvider } from './MultiAppManagerProvider'; import { MultiAppManagerProvider } from './MultiAppManagerProvider';
import { AppManager } from './AppManager';
import { NAMESPACE } from '../locale';
export class MultiAppManagerPlugin extends Plugin { export class MultiAppManagerPlugin extends Plugin {
async load() { async load() {
this.app.use(MultiAppManagerProvider); this.app.use(MultiAppManagerProvider);
this.app.pluginSettingsManager.add(NAMESPACE, {
title: `{{t("Multi-app manager", { ns: "${NAMESPACE}" })}}`,
icon: 'AppstoreOutlined',
Component: AppManager,
aclSnippet: 'pm.multi-app-manager.applications',
});
} }
} }

View File

@ -0,0 +1 @@
export const NAMESPACE = 'multi-app-manager';

View File

@ -1,7 +1,7 @@
{ {
"name": "@nocobase/plugin-oidc", "name": "@nocobase/plugin-oidc",
"displayName": "OIDC (OpenID Connect) auth - SSO login", "displayName": "OIDC auth - SSO login",
"displayName.zh-CN": "OIDC (OpenID Connect) 认证 - SSO 登录", "displayName.zh-CN": "OIDC 认证 - SSO 登录",
"description": "OIDC (OpenID Connect) authentication for NocoBase", "description": "OIDC (OpenID Connect) authentication for NocoBase",
"description.zh-CN": "OIDC (OpenID Connect) authentication for NocoBase", "description.zh-CN": "OIDC (OpenID Connect) authentication for NocoBase",
"version": "0.14.0-alpha.8", "version": "0.14.0-alpha.8",

View File

@ -1,11 +1,5 @@
import { TableOutlined } from '@ant-design/icons'; import { TableOutlined } from '@ant-design/icons';
import { import { Plugin, SchemaComponentOptions, SchemaInitializer, SchemaInitializerContext, useApp } from '@nocobase/client';
Plugin,
SchemaComponentOptions,
SchemaInitializer,
SchemaInitializerContext,
SettingsCenterProvider,
} from '@nocobase/client';
import { Card } from 'antd'; import { Card } from 'antd';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -56,31 +50,30 @@ const HelloProvider = React.memo((props) => {
} }
return ( return (
<SettingsCenterProvider
settings={{
'sample-hello': {
title: 'Hello',
icon: 'ApiOutlined',
tabs: {
tab1: {
title: 'Hello tab',
component: () => <Card bordered={false}>Hello Settings</Card>,
},
},
},
}}
>
<SchemaComponentOptions components={{ HelloDesigner, HelloBlockInitializer }}> <SchemaComponentOptions components={{ HelloDesigner, HelloBlockInitializer }}>
<SchemaInitializerContext.Provider value={items}>{props.children}</SchemaInitializerContext.Provider> <SchemaInitializerContext.Provider value={items}>{props.children}</SchemaInitializerContext.Provider>
</SchemaComponentOptions> </SchemaComponentOptions>
</SettingsCenterProvider>
); );
}); });
HelloProvider.displayName = 'HelloProvider'; HelloProvider.displayName = 'HelloProvider';
const HelloPluginSettingPage = () => {
return (
<Card bordered={false}>
<div>Hello plugin setting page</div>
</Card>
);
};
class HelloPlugin extends Plugin { class HelloPlugin extends Plugin {
async load() { async load() {
this.app.addProvider(HelloProvider); this.app.addProvider(HelloProvider);
this.app.pluginSettingsManager.add('sample-hello', {
title: 'Hello',
icon: 'ApiOutlined',
Component: HelloPluginSettingPage,
sort: 100,
});
} }
} }

View File

@ -1,11 +1,11 @@
import { i18n, Plugin, PluginManagerContext, SettingsCenterProvider } from '@nocobase/client'; import { i18n, Plugin, PluginManagerContext } from '@nocobase/client';
import { Select } from 'antd'; import { Select } from 'antd';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const ns = '@nocobase/plugin-sample-shop-i18n'; const NAMESPACE = 'sample-shop-i18n';
i18n.addResources('zh-CN', ns, { i18n.addResources('zh-CN', NAMESPACE, {
Shop: '店铺', Shop: '店铺',
I18n: '国际化', I18n: '国际化',
Pending: '已下单', Pending: '已下单',
@ -23,7 +23,7 @@ const ORDER_STATUS_LIST = [
]; ];
function OrderStatusSelect() { function OrderStatusSelect() {
const { t } = useTranslation(ns); const { t } = useTranslation(NAMESPACE);
return ( return (
<Select style={{ minWidth: '8em' }}> <Select style={{ minWidth: '8em' }}>
@ -40,20 +40,6 @@ const ShopI18nProvider = React.memo((props) => {
const ctx = useContext(PluginManagerContext); const ctx = useContext(PluginManagerContext);
return ( return (
<SettingsCenterProvider
settings={{
workflow: {
icon: 'ShopOutlined',
title: `{{t("Shop", { ns: "${ns}" })}}`,
tabs: {
workflows: {
title: `{{t("I18n", { ns: "${ns}" })}}`,
component: OrderStatusSelect,
},
},
},
}}
>
<PluginManagerContext.Provider <PluginManagerContext.Provider
value={{ value={{
components: { components: {
@ -63,13 +49,17 @@ const ShopI18nProvider = React.memo((props) => {
> >
{props.children} {props.children}
</PluginManagerContext.Provider> </PluginManagerContext.Provider>
</SettingsCenterProvider>
); );
}); });
class ShopI18nPlugin extends Plugin { class ShopI18nPlugin extends Plugin {
async load() { async load() {
this.app.addProvider(ShopI18nProvider); this.app.addProvider(ShopI18nProvider);
this.app.pluginSettingsManager.add(NAMESPACE, {
title: `{{t("Shop", { ns: "${NAMESPACE}" })}}`,
icon: 'ShopOutlined',
Component: OrderStatusSelect,
});
} }
} }

View File

@ -1,11 +1,4 @@
import { import { Plugin, createStyles, defaultTheme, useCurrentUserSettingsMenu, useGlobalTheme } from '@nocobase/client';
Plugin,
SettingsCenterProvider,
createStyles,
defaultTheme,
useCurrentUserSettingsMenu,
useGlobalTheme,
} from '@nocobase/client';
import { ConfigProvider } from 'antd'; import { ConfigProvider } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
@ -15,7 +8,7 @@ import ThemeList from './components/ThemeList';
import { ThemeListProvider } from './components/ThemeListProvider'; import { ThemeListProvider } from './components/ThemeListProvider';
import CustomTheme from './components/theme-editor'; import CustomTheme from './components/theme-editor';
import { useThemeSettings } from './hooks/useThemeSettings'; import { useThemeSettings } from './hooks/useThemeSettings';
import { useTranslation } from './locale'; import { NAMESPACE } from './locale';
const useStyles = createStyles(({ css, token }) => { const useStyles = createStyles(({ css, token }) => {
return { return {
@ -38,7 +31,6 @@ const useStyles = createStyles(({ css, token }) => {
const CustomThemeProvider = React.memo((props) => { const CustomThemeProvider = React.memo((props) => {
const { addMenuItem } = useCurrentUserSettingsMenu(); const { addMenuItem } = useCurrentUserSettingsMenu();
const themeItem = useThemeSettings(); const themeItem = useThemeSettings();
const { t } = useTranslation();
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const { theme, setTheme } = useGlobalTheme(); const { theme, setTheme } = useGlobalTheme();
const { styles } = useStyles(); const { styles } = useStyles();
@ -48,21 +40,6 @@ const CustomThemeProvider = React.memo((props) => {
addMenuItem(themeItem, { before: 'divider_3' }); addMenuItem(themeItem, { before: 'divider_3' });
}, [addMenuItem, themeItem]); }, [addMenuItem, themeItem]);
const settings = useMemo(() => {
return {
'theme-editor': {
title: t('Theme editor'),
icon: 'BgColorsOutlined',
tabs: {
themes: {
title: t('Themes'),
component: ThemeList,
},
},
},
};
}, []);
const contentStyle = useMemo(() => { const contentStyle = useMemo(() => {
return open return open
? { transform: 'rotate(0)', flexGrow: 1, width: 0, height: '100%' } ? { transform: 'rotate(0)', flexGrow: 1, width: 0, height: '100%' }
@ -89,7 +66,7 @@ const CustomThemeProvider = React.memo((props) => {
<ThemeListProvider> <ThemeListProvider>
<InitializeTheme> <InitializeTheme>
<ThemeEditorProvider open={open} setOpen={setOpen}> <ThemeEditorProvider open={open} setOpen={setOpen}>
<SettingsCenterProvider settings={settings}>{editor}</SettingsCenterProvider> {editor}
</ThemeEditorProvider> </ThemeEditorProvider>
</InitializeTheme> </InitializeTheme>
</ThemeListProvider> </ThemeListProvider>
@ -101,6 +78,12 @@ CustomThemeProvider.displayName = 'CustomThemeProvider';
export class ThemeEditorPlugin extends Plugin { export class ThemeEditorPlugin extends Plugin {
async load() { async load() {
this.app.use(CustomThemeProvider); this.app.use(CustomThemeProvider);
this.app.pluginSettingsManager.add(NAMESPACE, {
title: `{{t("Theme editor", {ns:"${NAMESPACE}"})}}`,
icon: 'BgColorsOutlined',
Component: ThemeList,
aclSnippet: 'pm.theme-editor.themes',
});
} }
} }

View File

@ -1,30 +1,11 @@
import React, { FC, useContext } from 'react'; import React, { FC, useContext } from 'react';
import { PluginManagerContext, SettingsCenterProvider } from '@nocobase/client'; import { PluginManagerContext } from '@nocobase/client';
import { NAMESPACE } from './locale';
import { VerificationProviders } from './VerificationProviders';
export { default as verificationProviderTypes } from './providerTypes'; export { default as verificationProviderTypes } from './providerTypes';
export const VerificationProvider: FC = (props) => { export const VerificationProvider: FC = (props) => {
const ctx = useContext(PluginManagerContext); const ctx = useContext(PluginManagerContext);
return ( return (
<SettingsCenterProvider
settings={{
verification: {
icon: 'CheckCircleOutlined',
title: `{{t("Verification", { ns: "${NAMESPACE}" })}}`,
tabs: {
providers: {
title: `{{t("Verification providers", { ns: "${NAMESPACE}" })}}`,
component: VerificationProviders,
},
},
},
}}
>
<PluginManagerContext.Provider <PluginManagerContext.Provider
value={{ value={{
components: { components: {
@ -34,6 +15,5 @@ export const VerificationProvider: FC = (props) => {
> >
{props.children} {props.children}
</PluginManagerContext.Provider> </PluginManagerContext.Provider>
</SettingsCenterProvider>
); );
}; };

View File

@ -1,9 +1,17 @@
import { Plugin } from '@nocobase/client'; import { Plugin } from '@nocobase/client';
import { VerificationProvider } from './VerificationProvider'; import { VerificationProvider } from './VerificationProvider';
import { VerificationProviders } from './VerificationProviders';
import { NAMESPACE } from './locale';
export class VerificationPlugin extends Plugin { export class VerificationPlugin extends Plugin {
async load() { async load() {
this.app.use(VerificationProvider); this.app.use(VerificationProvider);
this.app.pluginSettingsManager.add(NAMESPACE, {
icon: 'CheckCircleOutlined',
title: `{{t("Verification", { ns: "${NAMESPACE}" })}}`,
Component: VerificationProviders,
aclSnippet: 'pm.verification.providers',
});
} }
} }

View File

@ -3,6 +3,7 @@ import {
cx, cx,
SchemaComponent, SchemaComponent,
useAPIClient, useAPIClient,
useApp,
useCompile, useCompile,
useDocumentTitle, useDocumentTitle,
useResourceActionContext, useResourceActionContext,
@ -20,6 +21,7 @@ import useStyles from './style';
import { linkNodes } from './utils'; import { linkNodes } from './utils';
import { DownOutlined } from '@ant-design/icons'; import { DownOutlined } from '@ant-design/icons';
import { StatusButton } from './components/StatusButton'; import { StatusButton } from './components/StatusButton';
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './constant';
function attachJobs(nodes, jobs: any[] = []): void { function attachJobs(nodes, jobs: any[] = []): void {
const nodesMap = new Map(); const nodesMap = new Map();
@ -165,7 +167,7 @@ function ExecutionsDropdown(props) {
const onClick = useCallback( const onClick = useCallback(
({ key }) => { ({ key }) => {
if (key != execution.id) { if (key != execution.id) {
navigate(`/admin/settings/workflow/executions/${key}`); navigate(getWorkflowExecutionsPath(key));
} }
}, },
[execution], [execution],
@ -208,6 +210,7 @@ export function ExecutionCanvas() {
const { data, loading } = useResourceActionContext(); const { data, loading } = useResourceActionContext();
const { setTitle } = useDocumentTitle(); const { setTitle } = useDocumentTitle();
const [viewJob, setViewJob] = useState(null); const [viewJob, setViewJob] = useState(null);
const app = useApp();
useEffect(() => { useEffect(() => {
const { workflow } = data?.data ?? {}; const { workflow } = data?.data ?? {};
setTitle?.(`${workflow?.title ? `${workflow.title} - ` : ''}${lang('Execution history')}`); setTitle?.(`${workflow?.title ? `${workflow.title} - ` : ''}${lang('Execution history')}`);
@ -244,8 +247,8 @@ export function ExecutionCanvas() {
<header> <header>
<Breadcrumb <Breadcrumb
items={[ items={[
{ title: <Link to={`/admin/settings/workflow/workflows`}>{lang('Workflow')}</Link> }, { title: <Link to={app.pluginSettingsManager.getRoutePath('workflow')}>{lang('Workflow')}</Link> },
{ title: <Link to={`/admin/settings/workflow/workflows/${workflow.id}`}>{workflow.title}</Link> }, { title: <Link to={getWorkflowDetailPath(workflow.id)}>{workflow.title}</Link> },
{ title: <ExecutionsDropdown /> }, { title: <ExecutionsDropdown /> },
]} ]}
/> />

View File

@ -3,13 +3,14 @@ import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useActionContext, useRecord } from '@nocobase/client'; import { useActionContext, useRecord } from '@nocobase/client';
import { getWorkflowExecutionsPath } from './constant';
export const ExecutionLink = () => { export const ExecutionLink = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { id } = useRecord(); const { id } = useRecord();
const { setVisible } = useActionContext(); const { setVisible } = useActionContext();
return ( return (
<Link to={`/admin/settings/workflow/executions/${id}`} onClick={() => setVisible(false)}> <Link to={getWorkflowExecutionsPath(id)} onClick={() => setVisible(false)}>
{t('View')} {t('View')}
</Link> </Link>
); );

View File

@ -4,6 +4,7 @@ import {
ResourceActionProvider, ResourceActionProvider,
SchemaComponent, SchemaComponent,
cx, cx,
useApp,
useDocumentTitle, useDocumentTitle,
useResourceActionContext, useResourceActionContext,
useResourceContext, useResourceContext,
@ -21,6 +22,7 @@ import { lang } from './locale';
import { executionSchema } from './schemas/executions'; import { executionSchema } from './schemas/executions';
import useStyles from './style'; import useStyles from './style';
import { linkNodes } from './utils'; import { linkNodes } from './utils';
import { getWorkflowDetailPath } from './constant';
function ExecutionResourceProvider({ request, filter = {}, ...others }) { function ExecutionResourceProvider({ request, filter = {}, ...others }) {
const { workflow } = useFlowContext(); const { workflow } = useFlowContext();
@ -44,6 +46,7 @@ function ExecutionResourceProvider({ request, filter = {}, ...others }) {
export function WorkflowCanvas() { export function WorkflowCanvas() {
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
const app = useApp();
const { data, refresh, loading } = useResourceActionContext(); const { data, refresh, loading } = useResourceActionContext();
const { resource } = useResourceContext(); const { resource } = useResourceContext();
const { setTitle } = useDocumentTitle(); const { setTitle } = useDocumentTitle();
@ -67,7 +70,7 @@ export function WorkflowCanvas() {
function onSwitchVersion({ key }) { function onSwitchVersion({ key }) {
if (key != workflow.id) { if (key != workflow.id) {
navigate(`/admin/settings/workflow/workflows/${key}`); navigate(getWorkflowDetailPath(key));
} }
} }
@ -92,7 +95,7 @@ export function WorkflowCanvas() {
}); });
message.success(t('Operation succeeded')); message.success(t('Operation succeeded'));
navigate(`/admin/settings/workflow/workflows/${revision.id}`); navigate(`/admin/workflow/workflows/${revision.id}`);
} }
async function onDelete() { async function onDelete() {
@ -110,8 +113,8 @@ export function WorkflowCanvas() {
navigate( navigate(
workflow.current workflow.current
? '/admin/settings/workflow/workflows' ? app.pluginSettingsManager.getRoutePath('workflow')
: `/admin/settings/workflow/workflows/${revisions.find((item) => item.current)?.id}`, : getWorkflowDetailPath(revisions.find((item) => item.current)?.id),
); );
}, },
}); });
@ -147,7 +150,7 @@ export function WorkflowCanvas() {
<header> <header>
<Breadcrumb <Breadcrumb
items={[ items={[
{ title: <Link to={`/admin/settings/workflow/workflows`}>{lang('Workflow')}</Link> }, { title: <Link to={app.pluginSettingsManager.getRoutePath('workflow')}>{lang('Workflow')}</Link> },
{ title: <strong>{workflow.title}</strong> }, { title: <strong>{workflow.title}</strong> },
]} ]}
/> />

View File

@ -2,6 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { getWorkflowDetailPath } from './constant';
import { useActionContext, useGetAriaLabelOfAction, useRecord } from '@nocobase/client'; import { useActionContext, useGetAriaLabelOfAction, useRecord } from '@nocobase/client';
export const WorkflowLink = () => { export const WorkflowLink = () => {
@ -11,7 +12,7 @@ export const WorkflowLink = () => {
const { getAriaLabel } = useGetAriaLabelOfAction('Configure'); const { getAriaLabel } = useGetAriaLabelOfAction('Configure');
return ( return (
<Link aria-label={getAriaLabel()} to={`/admin/settings/workflow/workflows/${id}`} onClick={() => setVisible(false)}> <Link aria-label={getAriaLabel()} to={getWorkflowDetailPath(id)} onClick={() => setVisible(false)}>
{t('Configure')} {t('Configure')}
</Link> </Link>
); );

View File

@ -3,7 +3,6 @@ import {
PluginManagerContext, PluginManagerContext,
SchemaComponent, SchemaComponent,
SchemaComponentContext, SchemaComponentContext,
SettingsCenterProvider,
} from '@nocobase/client'; } from '@nocobase/client';
import { Card } from 'antd'; import { Card } from 'antd';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
@ -12,7 +11,6 @@ import { ExecutionResourceProvider } from './ExecutionResourceProvider';
import { WorkflowLink } from './WorkflowLink'; import { WorkflowLink } from './WorkflowLink';
import OpenDrawer from './components/OpenDrawer'; import OpenDrawer from './components/OpenDrawer';
import expressionField from './interfaces/expression'; import expressionField from './interfaces/expression';
import { lang } from './locale';
import { instructions } from './nodes'; import { instructions } from './nodes';
import { workflowSchema } from './schemas/workflows'; import { workflowSchema } from './schemas/workflows';
import { getTriggersOptions, triggers } from './triggers'; import { getTriggersOptions, triggers } from './triggers';
@ -26,7 +24,7 @@ export function useWorkflowContext() {
return useContext(WorkflowContext); return useContext(WorkflowContext);
} }
function WorkflowPane() { export function WorkflowPane() {
const ctx = useContext(SchemaComponentContext); const ctx = useContext(SchemaComponentContext);
return ( return (
<Card bordered={false}> <Card bordered={false}>
@ -53,22 +51,6 @@ export const WorkflowProvider = (props) => {
const pmCtx = useContext(PluginManagerContext); const pmCtx = useContext(PluginManagerContext);
const cmCtx = useContext(CollectionManagerContext); const cmCtx = useContext(CollectionManagerContext);
return ( return (
<SettingsCenterProvider
settings={{
workflow: {
icon: 'PartitionOutlined',
// title: `{{t("Workflow", { ns: "${NAMESPACE}" })}}`,
title: lang('Workflow'),
tabs: {
workflows: {
isBookmark: true,
title: lang('Workflow'),
component: WorkflowPane,
},
},
},
}}
>
<PluginManagerContext.Provider <PluginManagerContext.Provider
value={{ value={{
components: { components: {
@ -88,6 +70,5 @@ export const WorkflowProvider = (props) => {
<WorkflowContext.Provider value={{ triggers, instructions }}>{props.children}</WorkflowContext.Provider> <WorkflowContext.Provider value={{ triggers, instructions }}>{props.children}</WorkflowContext.Provider>
</CollectionManagerContext.Provider> </CollectionManagerContext.Provider>
</PluginManagerContext.Provider> </PluginManagerContext.Provider>
</SettingsCenterProvider>
); );
}; };

View File

@ -12,7 +12,7 @@ test.describe('workflow manage', () => {
// 1、前置条件已登录 // 1、前置条件已登录
// 2、测试步骤进入“工作流管理”-“新建”按钮,填写表单,点击“确定”按钮 // 2、测试步骤进入“工作流管理”-“新建”按钮,填写表单,点击“确定”按钮
await page.goto('/admin/settings/workflow/workflows'); await page.goto('/admin/settings/workflow');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
await page.getByLabel('action-Action-Add new-workflows').click(); await page.getByLabel('action-Action-Add new-workflows').click();
const createWorkFlow = new CreateWorkFlow(page); const createWorkFlow = new CreateWorkFlow(page);

View File

@ -10,7 +10,7 @@ test.describe('workflow manage', () => {
const caseTitle = 'edit from event name'; const caseTitle = 'edit from event name';
// 1、前置条件1.1、已登录,1.2、存在一个工作流 // 1、前置条件1.1、已登录,1.2、存在一个工作流
await page.goto('/admin/settings/workflow/workflows'); await page.goto('/admin/settings/workflow');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
await page.getByLabel('action-Action-Add new-workflows').click(); await page.getByLabel('action-Action-Add new-workflows').click();
const createWorkFlow = new CreateWorkFlow(page); const createWorkFlow = new CreateWorkFlow(page);

View File

@ -24,7 +24,7 @@ test.describe('trigger collection events', () => {
const newPage = mockPage(appendJsonCollectionName(e2e_GeneralFormsTable, appendText)); const newPage = mockPage(appendJsonCollectionName(e2e_GeneralFormsTable, appendText));
//配置工作流 //配置工作流
await page.goto('/admin/settings/workflow/workflows'); await page.goto('/admin/settings/workflow');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
await page.getByLabel('action-Action-Add new-workflows').click(); await page.getByLabel('action-Action-Add new-workflows').click();
const createWorkFlow = new CreateWorkFlow(page); const createWorkFlow = new CreateWorkFlow(page);
@ -85,7 +85,7 @@ test.describe('trigger collection events', () => {
// 3、预期结果数据添加成功工作流成功触发 // 3、预期结果数据添加成功工作流成功触发
await expect(page.getByText(fieldData)).toBeVisible(); await expect(page.getByText(fieldData)).toBeVisible();
await page.goto('/admin/settings/workflow/workflows'); await page.goto('/admin/settings/workflow');
await expect(page.getByRole('table').locator('a').filter({ hasText: '1' })).toBeVisible(); await expect(page.getByRole('table').locator('a').filter({ hasText: '1' })).toBeVisible();
// 4、后置处理删除工作流 // 4、后置处理删除工作流

View File

@ -0,0 +1,2 @@
export const getWorkflowDetailPath = (id: string | number) => `/admin/workflow/workflows/${id}`;
export const getWorkflowExecutionsPath = (id: string | number) => `/admin/workflow/executions/${id}`;

View File

@ -10,13 +10,15 @@ import { Plugin } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { ExecutionPage } from './ExecutionPage'; import { ExecutionPage } from './ExecutionPage';
import { WorkflowPage } from './WorkflowPage'; import { WorkflowPage } from './WorkflowPage';
import { WorkflowProvider } from './WorkflowProvider'; import { WorkflowPane, WorkflowProvider } from './WorkflowProvider';
import { DynamicExpression } from './components/DynamicExpression'; import { DynamicExpression } from './components/DynamicExpression';
import { triggers, useTrigger, getTriggersOptions } from './triggers'; import { triggers, useTrigger, getTriggersOptions } from './triggers';
import { instructions } from './nodes'; import { instructions } from './nodes';
import { WorkflowTodo } from './nodes/manual/WorkflowTodo'; import { WorkflowTodo } from './nodes/manual/WorkflowTodo';
import { WorkflowTodoBlockInitializer } from './nodes/manual/WorkflowTodoBlockInitializer'; import { WorkflowTodoBlockInitializer } from './nodes/manual/WorkflowTodoBlockInitializer';
import { useTriggerWorkflowsActionProps } from './triggers/form'; import { useTriggerWorkflowsActionProps } from './triggers/form';
import { NAMESPACE } from './locale';
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './constant';
export class WorkflowPlugin extends Plugin { export class WorkflowPlugin extends Plugin {
triggers = triggers; triggers = triggers;
@ -28,6 +30,12 @@ export class WorkflowPlugin extends Plugin {
this.addScopes(); this.addScopes();
this.addComponents(); this.addComponents();
this.app.addProvider(WorkflowProvider); this.app.addProvider(WorkflowProvider);
this.app.pluginSettingsManager.add(NAMESPACE, {
icon: 'PartitionOutlined',
title: `{{t("Workflow", { ns: "${NAMESPACE}" })}}`,
Component: WorkflowPane,
aclSnippet: 'pm.workflow.workflows',
});
} }
addScopes() { addScopes() {
@ -47,12 +55,12 @@ export class WorkflowPlugin extends Plugin {
} }
addRoutes() { addRoutes() {
this.app.router.add('admin.settings.workflow.workflows.id', { this.app.router.add('admin.workflow.workflows.id', {
path: '/admin/settings/workflow/workflows/:id', path: getWorkflowDetailPath(':id'),
element: <WorkflowPage />, element: <WorkflowPage />,
}); });
this.app.router.add('admin.settings.workflow.executions.id', { this.app.router.add('admin.workflow.executions.id', {
path: '/admin/settings/workflow/executions/:id', path: getWorkflowExecutionsPath(':id'),
element: <ExecutionPage />, element: <ExecutionPage />,
}); });
} }

View File

@ -6,6 +6,7 @@ import { ExecutionStatusOptions } from '../constants';
import { NAMESPACE } from '../locale'; import { NAMESPACE } from '../locale';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { message } from 'antd'; import { message } from 'antd';
import { getWorkflowDetailPath } from '../constant';
export const executionCollection = { export const executionCollection = {
name: 'executions', name: 'executions',
@ -32,12 +33,7 @@ export const executionCollection = {
title: `{{t("Version", { ns: "${NAMESPACE}" })}}`, title: `{{t("Version", { ns: "${NAMESPACE}" })}}`,
['x-component']({ value }) { ['x-component']({ value }) {
const { setVisible } = useActionContext(); const { setVisible } = useActionContext();
return ( return <Link to={getWorkflowDetailPath(value)} onClick={() => setVisible(false)}>{`#${value}`}</Link>;
<Link
to={`/admin/settings/workflow/workflows/${value}`}
onClick={() => setVisible(false)}
>{`#${value}`}</Link>
);
}, },
} as ISchema, } as ISchema,
}, },

View File

@ -24,7 +24,15 @@
"module": "commonjs" "module": "commonjs"
} }
}, },
"include": ["packages/**/*", ".dumi/**/*", ".dumirc.ts", "scripts/*", "playwright.config.ts", "vitest.config.ts"], "include": [
"packages/**/*",
".dumi/**/*",
".dumirc.ts",
"scripts/*",
"playwright.config.ts",
"vitest.config.ts",
"jest.setupAfterEnv.ts"
],
"exclude": [ "exclude": [
"packages/**/node_modules", "packages/**/node_modules",
"packages/**/dist", "packages/**/dist",