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