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 './RolePermissionManager';
export * from './RolePermissionManager';
export * from './ACLAction';

View File

@ -12,6 +12,7 @@ import {
AntdConfigProvider,
SchemaComponentProvider,
Menu,
Action,
} from '@nocobase/client';
import { I18nextProvider } from 'react-i18next';
import { Spin } from 'antd';
@ -23,7 +24,7 @@ const providers = [
[APIClientProvider, { apiClient }],
[I18nextProvider, { i18n }],
[AntdConfigProvider, { remoteLocale: true }],
[SchemaComponentProvider, { components: { Menu } }],
[SchemaComponentProvider, { components: { Menu, Action } }],
[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 './types';
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 './record-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
- SiteTitle
- MenuSchemaComponent
- PluginActionBar
- Designable.Action(引用)
- CollectionManager.Action(引用)
- ACL.Action(引用)
- SystemSettings.Action(引用)
- CurrentUser.Dropdown(引用)
- PluginManager.Toolbar
- DesignableAction
- CollectionManager.Action
- ACL.Action
- SystemSettings.Action
- CurrentUser.Dropdown
- Layout.Sider
- SideMenu随 Menu 联动的)
- Layout.Content

View File

@ -1,8 +1,8 @@
import React, { useRef, useState } from 'react';
import { Layout, Spin } from 'antd';
import { useRoute } from '../..';
import { findMenuItem, RemoteSchemaComponent } from '../../../schema-component';
import { Redirect, useHistory, useRouteMatch } from 'react-router-dom';
import { Layout } from 'antd';
import { useRoute } from '../../hooks';
import { useHistory, useRouteMatch } from 'react-router-dom';
import { findMenuItem, RemoteSchemaComponent, PluginManager } from '../../../';
export function AdminLayout(props: any) {
const route = useRoute();
@ -19,28 +19,33 @@ export function AdminLayout(props: any) {
const [hidden, setHidden] = useState(false);
return (
<Layout>
<Layout.Header>
<RemoteSchemaComponent
hidden={hidden}
uid={route.uiSchemaUid}
scope={{ onSelect, sideMenuRef, defaultSelectedUid }}
schemaTransform={(data) => {
data['x-component-props']['defaultSelectedUid'] = defaultSelectedUid;
return data;
}}
onSuccess={(data) => {
if (defaultSelectedUid) {
return;
}
setHidden(true);
setTimeout(() => setHidden(false), 11);
const s = findMenuItem(data?.data);
if (s) {
setSchema(s);
history.push(`/admin/${s['x-uid']}`);
}
}}
/>
<Layout.Header style={{ position: 'relative' }}>
<div>
<RemoteSchemaComponent
hidden={hidden}
uid={route.uiSchemaUid}
scope={{ onSelect, sideMenuRef, defaultSelectedUid }}
schemaTransform={(data) => {
data['x-component-props']['defaultSelectedUid'] = defaultSelectedUid;
return data;
}}
onSuccess={(data) => {
if (defaultSelectedUid) {
return;
}
setHidden(true);
setTimeout(() => setHidden(false), 11);
const s = findMenuItem(data?.data);
if (s) {
setSchema(s);
history.push(`/admin/${s['x-uid']}`);
}
}}
/>
</div>
<div style={{ position: 'absolute', top: 0, right: 0 }}>
<PluginManager.Toolbar />
</div>
</Layout.Header>
<Layout>
<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
### Action + Action.Drawer
<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 './SchemaOptionsExpressionScopeProvider';
export * from './RemoteSchemaComponent';
export * from './DesignableSwitch';

View File

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