feat(client): plugin manager toolbar

This commit is contained in:
chenos 2022-01-19 15:14:00 +08:00
parent e7918b7887
commit d1f9d3e25d
22 changed files with 540 additions and 41 deletions

View File

@ -0,0 +1,65 @@
import { Menu } from 'antd';
import React, { useState } from 'react';
import { LockOutlined } from '@ant-design/icons';
import { SchemaComponent, useActionVisible, VisibleContext } from '../schema-component';
import { ISchema, useForm } from '@formily/react';
const useCloseAction = () => {
const { setVisible } = useActionVisible();
const form = useForm();
return {
async run() {
setVisible(false);
form.submit((values) => {
console.log(values);
});
},
};
};
const schema: ISchema = {
type: 'object',
properties: {
drawer1: {
'x-component': 'Action.Drawer',
type: 'void',
title: 'Drawer Title',
properties: {
hello1: {
'x-content': 'Hello',
title: 'T1',
},
footer1: {
'x-component': 'Action.Drawer.Footer',
type: 'void',
properties: {
close1: {
title: 'Close',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCloseAction }}',
},
},
},
},
},
},
},
};
export const ACLAction = () => {
const [visible, setVisible] = useState(false);
return (
<VisibleContext.Provider value={[visible, setVisible]}>
<Menu.Item
eventKey={'ACLAction'}
onClick={() => {
setVisible(true);
}}
>
<LockOutlined />
</Menu.Item>
<SchemaComponent scope={{ useCloseAction }} schema={schema} />
</VisibleContext.Provider>
);
};

View File

@ -1,2 +1,3 @@
export * from './ACLProvider'; export * from './ACLProvider';
export * from './RolePermissionManager'; export * from './RolePermissionManager';
export * from './ACLAction';

View File

@ -12,6 +12,7 @@ import {
AntdConfigProvider, AntdConfigProvider,
SchemaComponentProvider, SchemaComponentProvider,
Menu, Menu,
Action,
} from '@nocobase/client'; } from '@nocobase/client';
import { I18nextProvider } from 'react-i18next'; import { I18nextProvider } from 'react-i18next';
import { Spin } from 'antd'; import { Spin } from 'antd';
@ -23,7 +24,7 @@ const providers = [
[APIClientProvider, { apiClient }], [APIClientProvider, { apiClient }],
[I18nextProvider, { i18n }], [I18nextProvider, { i18n }],
[AntdConfigProvider, { remoteLocale: true }], [AntdConfigProvider, { remoteLocale: true }],
[SchemaComponentProvider, { components: { Menu } }], [SchemaComponentProvider, { components: { Menu, Action } }],
[RouteSwitchProvider, { components: { AuthLayout, AdminLayout } }], [RouteSwitchProvider, { components: { AuthLayout, AdminLayout } }],
]; ];

View File

@ -0,0 +1,65 @@
import { Menu } from 'antd';
import React, { useState } from 'react';
import { DatabaseOutlined } from '@ant-design/icons';
import { SchemaComponent, useActionVisible, VisibleContext } from '../schema-component';
import { ISchema, useForm } from '@formily/react';
const useCloseAction = () => {
const { setVisible } = useActionVisible();
const form = useForm();
return {
async run() {
setVisible(false);
form.submit((values) => {
console.log(values);
});
},
};
};
const schema: ISchema = {
type: 'object',
properties: {
drawer1: {
'x-component': 'Action.Drawer',
type: 'void',
title: 'Drawer Title',
properties: {
hello1: {
'x-content': 'Hello',
title: 'T1',
},
footer1: {
'x-component': 'Action.Drawer.Footer',
type: 'void',
properties: {
close1: {
title: 'Close',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCloseAction }}',
},
},
},
},
},
},
},
};
export const CollectionManagerAction = () => {
const [visible, setVisible] = useState(false);
return (
<VisibleContext.Provider value={[visible, setVisible]}>
<Menu.Item
eventKey={'CollectionManagerAction'}
onClick={() => {
setVisible(true);
}}
>
<DatabaseOutlined />
</Menu.Item>
<SchemaComponent scope={{ useCloseAction }} schema={schema} />
</VisibleContext.Provider>
);
};

View File

@ -5,3 +5,4 @@ export * from './context';
export * from './hooks'; export * from './hooks';
export * from './types'; export * from './types';
export * from './CollectionField'; export * from './CollectionField';
export * from './CollectionManagerAction';

View File

@ -0,0 +1,65 @@
import { Menu } from 'antd';
import React, { useState } from 'react';
import { DatabaseOutlined } from '@ant-design/icons';
import { SchemaComponent, useActionVisible, VisibleContext } from '../schema-component';
import { ISchema, useForm } from '@formily/react';
const useCloseAction = () => {
const { setVisible } = useActionVisible();
const form = useForm();
return {
async run() {
setVisible(false);
form.submit((values) => {
console.log(values);
});
},
};
};
const schema: ISchema = {
type: 'object',
properties: {
drawer1: {
'x-component': 'Action.Drawer',
type: 'void',
title: 'Drawer Title',
properties: {
hello1: {
'x-content': 'Hello',
title: 'T1',
},
footer1: {
'x-component': 'Action.Drawer.Footer',
type: 'void',
properties: {
close1: {
title: 'Close',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCloseAction }}',
},
},
},
},
},
},
},
};
export const ProfileAction = () => {
const [visible, setVisible] = useState(false);
return (
<VisibleContext.Provider value={[visible, setVisible]}>
<Menu.Item
eventKey={'ProfileAction'}
onClick={() => {
setVisible(true);
}}
>
</Menu.Item>
<SchemaComponent scope={{ useCloseAction }} schema={schema} />
</VisibleContext.Provider>
);
};

View File

@ -1,3 +1,19 @@
export function CurrentUser() { import { Menu } from 'antd';
import React from 'react';
import { MoreOutlined, DesktopOutlined } from '@ant-design/icons';
import { ProfileAction } from './ProfileAction';
} export const CurrentUser = () => null;
CurrentUser.Dropdown = () => {
return (
// @ts-ignore
<Menu.SubMenu eventKey={'CurrentUser.Dropdown'} title={'超级管理员'}>
<ProfileAction />
<Menu.Item></Menu.Item>
<Menu.Item></Menu.Item>
<Menu.Divider />
<Menu.Item></Menu.Item>
</Menu.SubMenu>
);
};

View File

@ -14,3 +14,4 @@ export * from './system-settings';
export * from './application'; export * from './application';
export * from './record-provider'; export * from './record-provider';
export * from './async-data-provider'; export * from './async-data-provider';
export * from './plugin-manager';

View File

@ -0,0 +1,53 @@
import React, { createContext } from 'react';
import { Menu, Space } from 'antd';
import { SettingOutlined, MoreOutlined, DesktopOutlined } from '@ant-design/icons';
import { CurrentUser, DesignableSwitch, CollectionManagerAction, ACLAction, SystemSettings } from '../';
import { get } from 'lodash';
import { PluginManagerProvider } from '.';
export const PluginManager = () => null;
PluginManager.Provider = PluginManagerProvider;
PluginManager.Toolbar = (props: any) => {
const components = { CurrentUser, DesignableSwitch, CollectionManagerAction, ACLAction, SystemSettings };
const items = [
{
action: 'DesignableSwitch',
pin: true,
},
{
action: 'CollectionManagerAction',
pin: true,
},
{
action: 'ACLAction',
pin: true,
},
{
action: 'SystemSettings.Action',
},
];
const CurrentUserDropdown = get(components, 'CurrentUser.Dropdown');
return (
<Menu selectable={false} mode={'horizontal'} theme={'dark'}>
{items
.filter((item) => item.pin)
.map((item) => {
const Action = get(components, item.action);
return Action && <Action />;
})}
<Menu.SubMenu key={'more'} title={<MoreOutlined />}>
{items
.filter((item) => !item.pin)
.map((item) => {
const Action = get(components, item.action);
return Action && <Action />;
})}
<Menu.Divider></Menu.Divider>
<Menu.Item icon={<SettingOutlined />}></Menu.Item>
</Menu.SubMenu>
<CurrentUserDropdown />
</Menu>
);
};

View File

@ -0,0 +1,7 @@
import React from 'react';
import { PluginManagerContext } from './context';
export const PluginManagerProvider: React.FC<any> = (props) => {
const { components, children } = props;
return <PluginManagerContext.Provider value={{ components }}>{children}</PluginManagerContext.Provider>;
};

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const PluginManagerContext = createContext<any>({});

View File

@ -0,0 +1,55 @@
---
nav:
path: /client
group:
path: /client
---
# PluginManager
```tsx | pure
<PluginManager.Provider components={{
CurrentUser,
DesignableSwitch,
CollectionManagerAction,
ACLAction,
SystemSettings,
}}>
<PluginManager.Toolbar items={[
{
action: 'DesignableSwitch',
pin: true,
},
{
action: 'CollectionManagerAction',
pin: true,
},
{
action: 'ACLAction',
pin: true,
},
{
action: 'SystemSettings.Action',
},
]}/>
</PluginManager.Provider>
```
## 扩展
如何扩展插件的 `PluginManager.Toolbar.Item`
```tsx | pure
import React from 'react';
import { Button, Menu } from 'antd';
import { HighlightOutlined } from '@ant-design/icons';
export const DesignableSwitch = () => {
const { designable, setDesignable } = useDesignable();
return (
<Menu.Item key={'DesignableSwitch'} eventKey={'DesignableSwitch'}>
<HighlightOutlined />
</Menu.Item>
);
};
```

View File

@ -0,0 +1,3 @@
export * from './context';
export * from './PluginManager';
export * from './PluginManagerProvider';

View File

@ -14,12 +14,12 @@ group:
- Layout.Header - Layout.Header
- SiteTitle - SiteTitle
- MenuSchemaComponent - MenuSchemaComponent
- PluginActionBar - PluginManager.Toolbar
- Designable.Action(引用) - DesignableAction
- CollectionManager.Action(引用) - CollectionManager.Action
- ACL.Action(引用) - ACL.Action
- SystemSettings.Action(引用) - SystemSettings.Action
- CurrentUser.Dropdown(引用) - CurrentUser.Dropdown
- Layout.Sider - Layout.Sider
- SideMenu随 Menu 联动的) - SideMenu随 Menu 联动的)
- Layout.Content - Layout.Content

View File

@ -1,8 +1,8 @@
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { Layout, Spin } from 'antd'; import { Layout } from 'antd';
import { useRoute } from '../..'; import { useRoute } from '../../hooks';
import { findMenuItem, RemoteSchemaComponent } from '../../../schema-component'; import { useHistory, useRouteMatch } from 'react-router-dom';
import { Redirect, useHistory, useRouteMatch } from 'react-router-dom'; import { findMenuItem, RemoteSchemaComponent, PluginManager } from '../../../';
export function AdminLayout(props: any) { export function AdminLayout(props: any) {
const route = useRoute(); const route = useRoute();
@ -19,7 +19,8 @@ export function AdminLayout(props: any) {
const [hidden, setHidden] = useState(false); const [hidden, setHidden] = useState(false);
return ( return (
<Layout> <Layout>
<Layout.Header> <Layout.Header style={{ position: 'relative' }}>
<div>
<RemoteSchemaComponent <RemoteSchemaComponent
hidden={hidden} hidden={hidden}
uid={route.uiSchemaUid} uid={route.uiSchemaUid}
@ -41,6 +42,10 @@ export function AdminLayout(props: any) {
} }
}} }}
/> />
</div>
<div style={{ position: 'absolute', top: 0, right: 0 }}>
<PluginManager.Toolbar />
</div>
</Layout.Header> </Layout.Header>
<Layout> <Layout>
<Layout.Sider style={{ display: 'none' }} theme={'light'} ref={sideMenuRef}></Layout.Sider> <Layout.Sider style={{ display: 'none' }} theme={'light'} ref={sideMenuRef}></Layout.Sider>

View File

@ -0,0 +1,67 @@
import React, { useState } from 'react';
import { observer, ISchema, useForm } from '@formily/react';
import {
SchemaComponent,
SchemaComponentProvider,
Form,
Action,
useActionVisible,
VisibleContext,
} from '@nocobase/client';
import { FormItem, Input } from '@formily/antd';
import { Button } from 'antd';
const useCloseAction = () => {
const { setVisible } = useActionVisible();
const form = useForm();
return {
async run() {
setVisible(false);
form.submit((values) => {
console.log(values);
});
},
};
};
const schema: ISchema = {
type: 'object',
properties: {
drawer1: {
'x-component': 'Action.Drawer',
type: 'void',
title: 'Drawer Title',
properties: {
hello1: {
'x-content': 'Hello',
title: 'T1',
},
footer1: {
'x-component': 'Action.Drawer.Footer',
type: 'void',
properties: {
close1: {
title: 'Close',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCloseAction }}',
},
},
},
},
},
},
},
};
export default observer(() => {
const [visible, setVisible] = useState(false);
return (
<SchemaComponentProvider components={{ Form, Action, Input, FormItem }}>
<VisibleContext.Provider value={[visible, setVisible]}>
<a onClick={() => setVisible(true)}>Open</a>
<SchemaComponent scope={{ useCloseAction }} schema={schema} />
</VisibleContext.Provider>
</SchemaComponentProvider>
);
});

View File

@ -30,4 +30,12 @@ group:
## Examples ## Examples
### Action + Action.Drawer
<code src="./demos/demo1.tsx"/> <code src="./demos/demo1.tsx"/>
### VisibleContext + Action.Drawer
只配置 Action.Drawer而不需要 Action通过 VisibleContext 自定义按钮。
<code src="./demos/demo2.tsx"/>

View File

@ -0,0 +1,14 @@
import React from 'react';
import { Button, Menu } from 'antd';
import { HighlightOutlined } from '@ant-design/icons';
import cls from 'classnames';
import { useDesignable } from '..';
export const DesignableSwitch = () => {
const { designable, setDesignable } = useDesignable();
return (
<Menu.Item key={'DesignableSwitch'} eventKey={'DesignableSwitch'}>
<HighlightOutlined />
</Menu.Item>
);
};

View File

@ -3,3 +3,4 @@ export * from './SchemaComponent';
export * from './SchemaComponentProvider'; export * from './SchemaComponentProvider';
export * from './SchemaOptionsExpressionScopeProvider'; export * from './SchemaOptionsExpressionScopeProvider';
export * from './RemoteSchemaComponent'; export * from './RemoteSchemaComponent';
export * from './DesignableSwitch';

View File

@ -216,7 +216,7 @@ export class Designable {
// TODO // TODO
export function useDesignable() { export function useDesignable() {
const { designable, refresh, reset } = useContext(SchemaComponentContext); const { designable, setDesignable, refresh, reset } = useContext(SchemaComponentContext);
const DesignableBar = () => { const DesignableBar = () => {
return <></>; return <></>;
}; };
@ -229,6 +229,7 @@ export function useDesignable() {
designable, designable,
reset, reset,
refresh, refresh,
setDesignable,
DesignableBar, DesignableBar,
on: dn.on.bind(dn), on: dn.on.bind(dn),
// TODO // TODO

View File

@ -0,0 +1,65 @@
import { Menu } from 'antd';
import React, { useState } from 'react';
import { LockOutlined } from '@ant-design/icons';
import { SchemaComponent, useActionVisible, VisibleContext } from '../schema-component';
import { ISchema, useForm } from '@formily/react';
const useCloseAction = () => {
const { setVisible } = useActionVisible();
const form = useForm();
return {
async run() {
setVisible(false);
form.submit((values) => {
console.log(values);
});
},
};
};
const schema: ISchema = {
type: 'object',
properties: {
drawer1: {
'x-component': 'Action.Drawer',
type: 'void',
title: 'Drawer Title',
properties: {
hello1: {
'x-content': 'Hello',
title: 'T1',
},
footer1: {
'x-component': 'Action.Drawer.Footer',
type: 'void',
properties: {
close1: {
title: 'Close',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCloseAction }}',
},
},
},
},
},
},
},
};
export const SystemSettingsAction = () => {
const [visible, setVisible] = useState(false);
return (
<VisibleContext.Provider value={[visible, setVisible]}>
<Menu.Item
eventKey={'ACLAction'}
onClick={() => {
setVisible(true);
}}
>
<LockOutlined />
</Menu.Item>
<SchemaComponent scope={{ useCloseAction }} schema={schema} />
</VisibleContext.Provider>
);
};

View File

@ -1,3 +1,5 @@
export function SystemSettings() { import { SystemSettingsAction } from './SystemSettingsAction';
} export const SystemSettings = () => null;
SystemSettings.Action = SystemSettingsAction;