refactor: action/menu/schema-renderer...

This commit is contained in:
chenos 2021-07-15 18:47:21 +08:00
parent 61796cbc27
commit c9aa381045
23 changed files with 1110 additions and 1916 deletions

View File

@ -0,0 +1,54 @@
import React, { useState } from 'react';
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { Input, Popover, Button } from 'antd';
import { LoadingOutlined, CloseOutlined } from '@ant-design/icons';
import { createFromIconfontCN } from '@ant-design/icons';
import * as antIcons from '@ant-design/icons';
export const IconFont = createFromIconfontCN({
scriptUrl: ['//at.alicdn.com/t/font_2261954_u9jzwc44ug.js'],
});
export const icons = new Map<string, any>();
export function registerIcon(type: string, icon: any = IconFont) {
icons.set(type.toLowerCase(), icon);
}
export function hasIcon(type: string) {
if (!type) {
return false;
}
return icons.has(type.toLowerCase());
}
export function registerIcons(components) {
Object.keys(components).forEach((type) => {
registerIcon(type, components[type]);
});
}
Object.keys(antIcons).forEach((name) => {
if (name.endsWith('Outlined')) {
registerIcon(name, antIcons[name]);
}
});
interface IconProps {
type: string;
[key: string]: any;
}
export function IconPicker(props: IconProps) {
const { type = '', ...restProps } = props;
if (type && icons.has(type.toLowerCase())) {
const IconComponent = icons.get(type.toLowerCase());
if (IconComponent === IconFont) {
return <IconFont type={type} />;
}
return <IconComponent {...restProps} />;
}
return null;
}
export default IconPicker;

View File

@ -3,6 +3,30 @@ import { Spin } from 'antd';
import { Helmet } from 'react-helmet';
import { useRequest } from 'ahooks';
import { SchemaRenderer } from '../../schemas';
import { useForm } from '@formily/react';
import { useHistory } from 'react-router-dom';
export function useLogin() {
const form = useForm();
const history = useHistory();
return {
async run() {
await form.submit();
history.push('/admin');
console.log(form.values);
},
};
}
export function useRegister() {
const form = useForm();
return {
async run() {
await form.submit();
console.log(form.values);
},
};
}
export function RouteSchemaRenderer({ route }) {
const { data = {}, loading } = useRequest(
@ -17,7 +41,7 @@ export function RouteSchemaRenderer({ route }) {
}
return (
<div>
<SchemaRenderer schema={data} />
<SchemaRenderer scope={{ useLogin, useRegister }} schema={data} />
</div>
);
}

View File

@ -22,13 +22,7 @@ import Editor from '@monaco-editor/react';
import { ArrayCollapse, ArrayTable, FormLayout } from '@formily/antd';
import { Space, Card, Modal, Spin } from 'antd';
import {
Action,
useLogin,
useRegister,
useSubmit,
useDesignableValues,
} from '../../schemas/action';
import { Action } from '../../schemas/action';
import { AddNew } from '../../schemas/add-new';
import { Cascader } from '../../schemas/cascader';
import { Checkbox } from '../../schemas/checkbox';
@ -46,14 +40,7 @@ import { Menu } from '../../schemas/menu';
import { Password } from '../../schemas/password';
import { Radio } from '../../schemas/radio';
import { Select } from '../../schemas/select';
import {
Table,
useTableRow,
useTableCreateAction,
useTableUpdateAction,
useTableDestroyAction,
useTableFilterAction,
} from '../../schemas/table';
import { Table } from '../../schemas/table';
import { Tabs } from '../../schemas/tabs';
import { TimePicker } from '../../schemas/time-picker';
import { Upload } from '../../schemas/upload';
@ -71,17 +58,7 @@ interface DesignableContextProps {
}
export const SchemaField = createSchemaField({
scope: {
useLogin,
useRegister,
useSubmit,
useTableCreateAction,
useTableDestroyAction,
useTableFilterAction,
useTableRow,
useTableUpdateAction,
useDesignableValues,
},
scope: {},
components: {
Card,
Div,
@ -180,9 +157,11 @@ export function addPropertyAfter(target: Schema, data: ISchema) {
export function useDesignable(path?: any) {
const { schema, refresh } = useContext(DesignableContext);
const schemaPath = path || useSchemaPath();
console.log({ schemaPath });
const fieldSchema = useFieldSchema();
const currentSchema =
findPropertyByPath(schema, schemaPath) || ({} as Schema);
console.log({ currentSchema })
// console.log('useDesignable', { schema, schemaPath, currentSchema });
const options = useContext(SchemaOptionsContext);
const DesignableBar =
@ -317,13 +296,17 @@ export function useDesignable(path?: any) {
}
export function getSchemaPath(schema: Schema) {
const path = [schema.name];
const path = schema['x-path'] || [schema.name];
let parent = schema.parent;
while (parent) {
if (!parent.name) {
break;
}
if (parent['x-path']) {
path.unshift(...parent['x-path']);
} else {
path.unshift(parent.name);
}
parent = parent.parent;
}
// console.log('getSchemaPath', path, schema);

View File

@ -1,30 +0,0 @@
/**
* title: 按钮操作
* desc: 可以通过配置 `useAction`
*/
import React from 'react';
// @ts-ignore
import { SchemaRenderer } from '@nocobase/client';
function useAction() {
return {
run() {
alert('这是自定义的操作逻辑');
},
};
}
const schema = {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': 'Action',
'x-designable-bar': 'Action.DesignableBar',
'x-component-props': {
useAction: '{{ useAction }}',
},
};
export default () => {
return <SchemaRenderer debug={true} scope={{ useAction }} schema={schema} />;
};

View File

@ -1,16 +0,0 @@
import React from 'react';
import { SchemaRenderer } from '@nocobase/client';
const schema = {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': 'Action.Link',
'x-component-props': {
to: 'abc',
},
};
export default () => {
return <SchemaRenderer schema={schema} />;
};

View File

@ -1,16 +0,0 @@
import React from 'react';
import { SchemaRenderer } from '@nocobase/client';
const schema = {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': 'Action.URL',
'x-component-props': {
url: 'https://www.nocobase.com/',
},
};
export default () => {
return <SchemaRenderer schema={schema} />;
};

View File

@ -1,92 +0,0 @@
import React from 'react';
import { SchemaRenderer } from '@nocobase/client';
function useAction() {
return {
run() {
alert('这是自定义的操作逻辑');
},
};
}
const schema = {
type: 'void',
name: 'action1',
title: '下拉菜单',
'x-component': 'Action.Dropdown',
properties: {
action1: {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': 'Action',
'x-component-props': {
useAction,
},
},
action2: {
type: 'void',
title: 'Drawer 按钮',
'x-component': 'Action',
properties: {
drawer1: {
type: 'void',
title: '抽屉标题',
'x-component': 'Action.Drawer',
'x-component-props': {},
properties: {
input: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
action2: {
type: 'void',
title: '打开二级抽屉',
// 'x-decorator': 'FormItem',
'x-component': 'Action',
properties: {
drawer1: {
type: 'void',
title: '二级抽屉标题',
'x-component': 'Action.Drawer',
'x-component-props': {},
properties: {
input: {
type: 'string',
'x-component': 'Input',
},
},
},
},
},
},
},
},
},
action3: {
type: 'void',
name: 'action1',
title: 'Popover 按钮',
'x-component': 'Action',
properties: {
popover1: {
type: 'void',
title: '弹窗标题',
'x-component': 'Action.Popover',
'x-component-props': {},
properties: {
input: {
type: 'string',
'x-component': 'Input',
},
},
},
},
},
},
};
export default () => {
return <SchemaRenderer schema={schema} />;
};

View File

@ -1,61 +0,0 @@
import { ISchema } from '@formily/react';
import {
useDesignableValues,
useDesignableUpdate,
useDesignableSchemaRemove,
} from '.';
export const DesignableBar: { [key: string]: ISchema } = {};
DesignableBar.Action = {
name: 'bar',
type: 'void',
// title: '操作栏2',
'x-component': 'Action.Dropdown',
'x-component-props': {
icon: 'MenuOutlined',
},
properties: {
edit: {
type: 'void',
title: '编辑按钮',
'x-component': 'Action',
'x-component-props': {
// icon,
},
properties: {
modal: {
type: 'void',
title: '修改按钮配置',
'x-decorator': 'Form',
'x-decorator-props': {
useValues: useDesignableValues,
},
'x-component': 'Action.Modal',
'x-component-props': {
width: '500px',
useAction: useDesignableUpdate,
},
properties: {
title: {
type: 'string',
title: '按钮标题',
required: true,
'x-component': 'Input',
'x-decorator': 'FormItem',
},
},
},
},
},
remove: {
type: 'void',
title: '删除按钮',
'x-component': 'Action',
'x-component-props': {
useAction: useDesignableSchemaRemove,
// icon,
},
},
},
};

View File

@ -75,208 +75,75 @@ group:
## Examples
### Action - 常规操作
<code src="./demos/demo1.tsx">
### Action.Link - 内链跳转
<code src="./demos/demo2.tsx">
### Action.URL - 外链跳转
<code src="./demos/demo3.tsx">
### Action.Dropdown - 下拉操作
<code src="./demos/demo4.tsx">
### Action.Popover - 打开气泡
### Action
```tsx
/**
* title: 按钮操作
* desc: 可以通过配置 `useAction` 来处理操作逻辑
*/
import React from 'react';
import { SchemaRenderer } from '../';
// @ts-ignore
import { SchemaRenderer } from '@nocobase/client';
function useAction() {
return {
run() {
alert('这是自定义的操作逻辑');
},
};
}
const schema = {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': 'Action',
properties: {
popover1: {
type: 'void',
title: '弹窗标题',
'x-component': 'Action.Popover',
'x-component-props': {},
properties: {
input: {
type: 'string',
'x-component': 'Input',
}
},
},
'x-designable-bar': 'Action.DesignableBar',
'x-component-props': {
useAction: '{{ useAction }}',
},
};
export default () => {
return <SchemaRenderer schema={schema} />
}
return (
<SchemaRenderer
debug={true}
scope={{ useAction }}
schema={schema}
/>
);
};
```
### Action.Drawer - 打开抽屉
### Action.Modal
```tsx
import React from 'react';
import { SchemaRenderer } from '@nocobase/client';
import { Space } from 'antd';
import { SchemaRenderer } from '../';
const schema1 = {
const schema = {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': 'Action',
properties: {
drawer1: {
type: 'void',
title: '抽屉标题',
'x-component': 'Action.Drawer',
'x-component-props': {},
properties: {
input: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
action2: {
type: 'void',
title: '打开二级抽屉',
// 'x-decorator': 'FormItem',
'x-component': 'Action',
properties: {
drawer1: {
type: 'void',
title: '二级抽屉标题',
'x-component': 'Action.Drawer',
'x-component-props': {},
properties: {
input: {
type: 'string',
'x-component': 'Input',
},
},
},
},
}
},
},
},
};
const schema2 = {
type: 'void',
name: 'action1',
title: 'x-decorator=Form',
'x-component': 'Action',
properties: {
drawer1: {
type: 'void',
title: '弹窗标题',
'x-component': 'Action.Drawer',
'x-designable-bar': 'Action.DesignableBar',
'x-component-props': {
// footer: null,
},
'x-decorator': 'Form',
properties: {
modal1: {
type: 'void',
title: '对话框标题',
'x-component': 'Action.Modal',
properties: {
input: {
type: 'string',
title: '输入框',
'x-decorator': 'FormItem',
'x-component': 'Input',
required: true,
},
},
},
},
};
export default () => {
return (
<Space>
<SchemaRenderer schema={schema1} />
<SchemaRenderer schema={schema2} />
</Space>
)
}
```
### Action.Modal - 打开对话框
`x-decorator='Form'`Model 为 Form并自带按钮
```tsx
import React from 'react';
import { Space } from 'antd';
import { SchemaRenderer } from '../';
import { useVisibleContext } from './';
function useAction() {
const { visible, setVisible } = useVisibleContext();
return {
async run() {
console.log({ visible })
setVisible(false);
}
}
}
const schema = {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': 'Action',
properties: {
drawer1: {
type: 'void',
title: '弹窗标题',
'x-component': 'Action.Modal',
'x-component-props': {
// footer: null,
},
properties: {
input: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
close: {
type: 'void',
name: 'action1',
title: '关闭',
'x-component': 'Action',
'x-component-props': {
useAction,
},
},
action2: {
type: 'void',
title: '打开二级抽屉',
// 'x-decorator': 'FormItem',
'x-component': 'Action',
properties: {
drawer1: {
type: 'void',
title: '二级抽屉标题',
'x-component': 'Action.Modal',
'x-component-props': {},
properties: {
input: {
type: 'string',
'x-component': 'Input',
},
},
},
},
}
},
},
},
@ -285,20 +152,22 @@ const schema = {
const schema2 = {
type: 'void',
name: 'action1',
title: 'x-decorator=Form',
title: 'ModalForm',
'x-component': 'Action',
properties: {
drawer1: {
type: 'void',
title: '弹窗标题',
'x-component': 'Action.Modal',
'x-designable-bar': 'Action.DesignableBar',
'x-component-props': {
// footer: null,
},
properties: {
modal1: {
type: 'void',
title: '对话框标题',
'x-decorator': 'Form',
'x-component': 'Action.Modal',
properties: {
input: {
type: 'string',
title: '输入框',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
@ -310,51 +179,226 @@ const schema2 = {
export default () => {
return (
<Space>
<SchemaRenderer schema={schema} />
<SchemaRenderer schema={schema2} />
<SchemaRenderer
schema={schema}
/>
<SchemaRenderer
schema={schema2}
/>
</Space>
)
}
);
};
```
### Action.Container - 指定容器内打开
### Action.Drawer
```tsx
import React, { useRef } from 'react';
import { SchemaRenderer } from '../';
import { ActionContext } from './';
import React from 'react';
import { SchemaRenderer } from '@nocobase/client';
import { Space } from 'antd';
export default () => {
const containerRef = useRef();
const schema = {
const schema = {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': 'Action',
'x-designable-bar': 'Action.DesignableBar',
'x-component-props': {
containerRef
},
properties: {
container1: {
modal1: {
type: 'void',
title: '页面标题',
'x-component': 'Action.Container',
title: '抽屉标题',
'x-component': 'Action.Drawer',
properties: {
input: {
type: 'string',
title: '字段',
'x-designable-bar': 'FormItem.DesignableBar',
title: '输入框',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
}
},
},
},
};
},
};
const schema2 = {
type: 'void',
name: 'action1',
title: 'DrawerForm',
'x-component': 'Action',
'x-designable-bar': 'Action.DesignableBar',
'x-component-props': {
},
properties: {
modal1: {
type: 'void',
title: '抽屉标题',
'x-decorator': 'Form',
'x-component': 'Action.Drawer',
properties: {
input: {
type: 'string',
title: '输入框',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
};
export default () => {
return (
<div>
<SchemaRenderer schema={schema} />
</div>
)
}
<Space>
<SchemaRenderer
schema={schema}
/>
<SchemaRenderer
schema={schema2}
/>
</Space>
);
};
```
### Action.Dropdown
```tsx
import React from 'react';
import { SchemaRenderer } from '@nocobase/client';
const schema = {
type: 'void',
name: 'dropdown1',
title: '下拉菜单',
'x-component': 'Action.Dropdown',
'x-designable-bar': 'Action.DesignableBar',
'x-component-props': {},
properties: {
item1: {
type: 'void',
title: `操作1`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Action',
properties: {
modal1: {
type: 'void',
title: '对话框标题',
'x-component': 'Action.Modal',
properties: {
input: {
type: 'string',
'x-component': 'Input',
},
},
},
},
},
item2: {
type: 'void',
title: `操作2`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Action',
properties: {
modal2: {
type: 'void',
title: '对话框标题',
'x-component': 'Action.Modal',
properties: {
input: {
type: 'string',
'x-component': 'Input',
},
},
},
},
},
},
};
export default () => {
return (
<SchemaRenderer
debug={true}
schema={schema}
/>
);
};
```
### Action.Popover
```tsx
import React from 'react';
import { SchemaRenderer } from '@nocobase/client';
import { Space } from 'antd';
const schema = {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': 'Action',
'x-designable-bar': 'Action.DesignableBar',
'x-component-props': {
},
properties: {
modal1: {
type: 'void',
title: '抽屉标题',
'x-component': 'Action.Popover',
properties: {
input: {
type: 'string',
title: '输入框',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
};
const schema2 = {
type: 'void',
name: 'action1',
title: 'PopoverForm',
'x-component': 'Action',
'x-designable-bar': 'Action.DesignableBar',
'x-component-props': {
},
properties: {
modal1: {
type: 'void',
title: '抽屉标题',
'x-decorator': 'Form',
'x-component': 'Action.Popover',
properties: {
input: {
type: 'string',
title: '输入框',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
};
export default () => {
return (
<Space>
<SchemaRenderer
schema={schema}
/>
<SchemaRenderer
schema={schema2}
/>
</Space>
);
};
```

View File

@ -13,266 +13,132 @@ import {
} from '@formily/react';
import { Button, Dropdown, Menu, Popover, Space, Drawer, Modal } from 'antd';
import { Link, useHistory, LinkProps } from 'react-router-dom';
import { SchemaRenderer, useDesignable } from '..';
import ReactDOM from 'react-dom';
import get from 'lodash/get';
import { MenuOutlined, DownOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import { useMount } from 'ahooks';
import { uid } from '@formily/shared';
import { useDesignable, VisibleContext, useDefaultAction } from '..';
import './style.less';
import constate from 'constate';
import { Icon } from '../icon-picker';
import { useMemo } from 'react';
import { createForm } from '@formily/core';
import { uid } from '@formily/shared';
import cls from 'classnames';
import { MenuOutlined } from '@ant-design/icons';
export function useDefaultAction() {
return {
run() {},
};
}
export function useSubmit() {
const form = useForm();
const run = async () => {
await form.submit();
console.log(form.values);
};
return {
run,
};
}
export function useLogin() {
const form = useForm();
const history = useHistory();
const run = async () => {
await form.submit();
history.push('/admin');
console.log(form.values);
};
return {
run,
};
}
export function useRegister() {
const form = useForm();
const run = async () => {
await form.submit();
console.log(form.values);
};
return {
run,
};
}
export interface ActionProps {
useAction?: any;
[key: string]: any;
}
export type ActionType = React.FC<ActionProps> & {
Link?: React.FC<LinkProps>;
URL?: React.FC<any>;
Page?: React.FC<any>;
Container?: React.FC<any>;
Popover?: React.FC<any>;
Drawer?: React.FC<any>;
Modal?: React.FC<any>;
Dropdown?: React.FC<any>;
Group?: React.FC<any>;
DesignableBar?: React.FC<any>;
};
function useDesignableBar() {
const schema = useFieldSchema();
const options = useContext(SchemaOptionsContext);
const DesignableBar = get(options.components, schema['x-designable-bar']);
return {
DesignableBar: DesignableBar || (() => null),
};
}
const [VisibleProvider, useVisibleContext] = constate((props: any = {}) => {
const { initialVisible = false, containerRef = null } = props;
const [visible, setVisible] = useState(initialVisible);
return { containerRef, visible, setVisible };
});
export { VisibleProvider, useVisibleContext };
const BaseAction = observer((props: any) => {
const {
ButtonComponent = Button,
className,
icon,
useAction = useDefaultAction,
...others
} = props;
const field = useField();
export const Action: any = observer((props: any) => {
const { useAction = useDefaultAction, ...others } = props;
const { run } = useAction();
const { DesignableBar } = useDesignableBar();
const { setVisible } = useVisibleContext();
const fieldSchema = useFieldSchema();
const { schema } = useDesignable();
useEffect(() => {
field.componentProps.setVisible = setVisible;
}, []);
const renderButton = () => (
<ButtonComponent
const { schema, DesignableBar } = useDesignable();
const [visible, setVisible] = useState(false);
const child = Object.values(schema.properties || {}).shift();
const isDropdownOrPopover =
child &&
['Action.Dropdown', 'Action.Popover'].includes(child['x-component']);
console.log({ DesignableBar, schema })
return (
<VisibleContext.Provider value={[visible, setVisible]}>
{!isDropdownOrPopover && (
<Button
{...others}
icon={icon ? <Icon type={icon} /> : null}
className={classNames(className, `name-${schema.name}`)}
onClick={async (e) => {
e.stopPropagation && e.stopPropagation();
setVisible && setVisible(true);
onClick={async () => {
setVisible(true);
await run();
}}
>
{schema.title}
<DesignableBar />
</ButtonComponent>
);
const popover = schema.reduceProperties((items, current) => {
if (current['x-component'] === 'Action.Popover') {
return current;
}
return null;
}, null);
if (popover) {
return (
<RecursionField
schema={
new Schema({
type: 'object',
properties: {
[popover.name]: {
...popover.toJSON(),
'x-button': renderButton(),
},
},
})
}
/>
);
}
return (
<>
{renderButton()}
{props.children}
</>
</Button>
)}
<RecursionField schema={schema} onlyRenderProperties />
</VisibleContext.Provider>
);
});
export const Action: ActionType = observer((props: any) => {
const schema = useFieldSchema();
if (schema.properties) {
return (
<VisibleProvider
initialVisible={props.initialVisible}
containerRef={props.containerRef}
>
<BaseAction {...props} />
</VisibleProvider>
);
}
return <BaseAction {...props} />;
});
Action.Link = observer((props) => {
const schema = useFieldSchema();
return <Link {...props}>{schema.title}</Link>;
});
Action.URL = observer((props) => {
const { url, ...others } = props;
const schema = useFieldSchema();
return (
<a target={'_blank'} href={url} {...others}>
{schema.title}
</a>
);
});
Action.Container = observer((props) => {
const { visible, setVisible } = useVisibleContext();
return visible && <div>{props.children}</div>;
});
Action.Popover = observer((props) => {
const { visible, setVisible } = useVisibleContext();
const schema = useFieldSchema();
return (
<Popover
placement={'bottom'}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
{...props}
content={props.children}
>
{schema['x-button']}
</Popover>
);
});
Action.Drawer = observer((props) => {
const field = useField();
// const { visible, setVisible } = useVisibleContext();
const { useAction = () => ({ async run() {} }), ...others } = props;
Action.Modal = observer((props: any) => {
const { schema } = useDesignable();
const { visible, setVisible } = useVisibleContext();
const { run } = useAction();
const [visible, setVisible] = useContext(VisibleContext);
const form = useForm();
console.log(`schema['x-decorator']`, schema['x-decorator']);
if (schema['x-decorator'] === 'Form.Decorator') {
Object.assign(others, {
footer: (
<Space>
const isFormDecorator = schema['x-decorator'] === 'Form';
return (
<Modal
title={schema.title}
destroyOnClose
maskClosable
footer={
isFormDecorator
? [
<Button
onClick={async () => {
if (isFormDecorator) {
form.clearErrors();
}
setVisible(false);
}}
>
Cancel
</Button>,
<Button
type={'primary'}
onClick={async () => {
if (isFormDecorator) {
await form.submit();
await run();
}
setVisible(false);
}}
>
OK
</Button>,
]
: null
}
{...props}
onCancel={() => {
if (isFormDecorator) {
form.clearErrors();
}
setVisible(false);
}}
visible={visible}
>
{props.children}
</Modal>
);
});
Action.Drawer = observer((props: any) => {
const { schema } = useDesignable();
const [visible, setVisible] = useContext(VisibleContext);
const form = useForm();
const isFormDecorator = schema['x-decorator'] === 'Form';
return (
<Drawer
width={'50%'}
title={schema.title}
maskClosable
destroyOnClose
footer={
isFormDecorator && (
<Space style={{ float: 'right' }}>
<Button
onClick={async (e) => {
form.clearErrors();
props.onClose && (await props.onClose(e));
setVisible(false);
}}
>
Cancel
</Button>
<Button
onClick={async (e) => {
await form.submit();
props.onOk && (await props.onOk(e));
setVisible(false);
}}
type={'primary'}
>
OK
</Button>
<Button
onClick={async () => {
form.clearErrors();
setVisible(false);
}}
>
Cancel
</Button>
</Space>
),
});
)
}
return (
<Drawer
onClick={(e) => {
e.stopPropagation();
}}
title={field.title}
width={'50%'}
{...others}
{...props}
visible={visible}
destroyOnClose
onClose={(e) => {
e.stopPropagation();
form.clearErrors();
onClose={async (e) => {
props.onClose && (await props.onClose(e));
setVisible(false);
}}
>
@ -281,145 +147,121 @@ Action.Drawer = observer((props) => {
);
});
const [DesignableContextProvider, useDesignableContext] = constate(() => {
return useDesignable();
});
import { DesignableBar } from './designableBar';
export function useDesignableValues() {
const { schema } = useDesignableContext();
return schema
? {
title: schema.title,
}
: {};
}
export function useDesignableUpdate() {
const { schema, refresh } = useDesignableContext();
const form = useForm();
return {
async run() {
schema.title = form.values.title;
refresh();
console.log('useDesignableUpdate', schema, form.values);
},
};
}
export function useDesignableSchemaRemove() {
const { schema, refresh, remove } = useDesignableContext();
return {
async run() {
remove();
},
};
}
Action.Modal = observer((props) => {
const { useAction = () => ({ async run() {} }), ...others } = props;
Action.Dropdown = observer((props: any) => {
const { schema } = useDesignable();
const { setVisible: setDropdownVisible } = useDropdownVisibleContext();
const { visible, setVisible } = useVisibleContext();
const { run } = useAction();
const form = useForm();
// console.log('Action.Modal', schema['x-component-props'], Object.keys(others).includes('footer'));
if (
!Object.keys(others).includes('footer') &&
schema['x-decorator'] !== 'Form.Decorator'
) {
Object.assign(others, { footer: null });
}
const componentProps = schema.parent['x-component-props'] || {};
return (
<Modal
title={schema.title}
width={'50%'}
{...others}
visible={visible}
closable
maskClosable
destroyOnClose
onCancel={async (e) => {
e.stopPropagation();
setVisible(false);
// await form.reset();
setDropdownVisible && setDropdownVisible(false);
}}
onOk={async (e) => {
console.log('onOk', form.values);
// await form.submit();
await run();
setVisible(false);
setDropdownVisible && setDropdownVisible(false);
}}
<Dropdown
trigger={['click']}
overlay={
<Menu>
<RecursionField schema={schema} onlyRenderProperties />
</Menu>
}
>
<div
//onClick={(e) => e.stopPropagation()}
>
{props.children}
</div>
</Modal>
<Button {...componentProps}>{schema.title || schema.parent.title}</Button>
</Dropdown>
);
});
const [DropdownVisibleProvider, useDropdownVisibleContext] = constate(
(props: any = {}) => {
const { initialVisible = false } = props;
const [visible, setVisible] = useState(initialVisible);
return { visible, setVisible };
},
);
const ActionDropdown = observer((props: any) => {
const { icon, ...others } = props;
const schema = useFieldSchema();
const { visible, setVisible } = useDropdownVisibleContext();
Action.Popover = observer((props) => {
const fieldSchema = useFieldSchema();
const { schema } = useDesignable();
const form = useForm();
const isFormDecorator = schema['x-decorator'] === 'Form';
const [visible, setVisible] = useContext(VisibleContext);
console.log('Action.Popover', schema, fieldSchema);
const componentProps =
(schema.parent && schema.parent['x-component-props']) || {};
return (
<Popover
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
trigger={['click']}
onVisibleChange={setVisible}
{...props}
title={schema.title}
content={
<div>
{props.children}
{isFormDecorator && (
<div>
<Space>
<Button
onClick={() => {
form.clearErrors();
setVisible(false);
}}
// {...props}
overlayClassName={'nb-action-group'}
// trigger={'click'}
content={props.children}
placement={'bottomLeft'}
>
<Button icon={icon ? <Icon type={icon} /> : null} {...others}>
{schema.title}
Cancel
</Button>
<Button
type={'primary'}
onClick={async () => {
await form.submit();
setVisible(false);
}}
>
Ok
</Button>
</Space>
</div>
)}
</div>
}
>
<Button {...componentProps}>{schema.parent.title || schema.title}</Button>
</Popover>
);
});
Action.Dropdown = observer((props) => {
return (
<DropdownVisibleProvider>
<ActionDropdown {...props} />
<div style={{ display: 'none' }}>{props.children}</div>
</DropdownVisibleProvider>
);
});
Action.DesignableBar = () => {
const field = useField();
// const schema = useFieldSchema();
const { schema, insertAfter, refresh } = useDesignable();
const [visible, setVisible] = useState(false);
return (
<DesignableContextProvider>
<div className={classNames('designable-bar', { active: visible })}>
<div className={cls('designable-bar', { active: visible })}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={classNames('designable-bar-actions', { active: visible })}
className={cls('designable-bar-actions', { active: visible })}
>
<SchemaRenderer schema={DesignableBar.Action} />
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.Item
onClick={(e) => {
// console.log({ field, schema });
schema.title = '按钮文案被修改了';
// field.setTitle('按钮文案被修改了');
// setVisible(false);
refresh();
}}
>
</Menu.Item>
<Menu.Item
onClick={() => {
insertAfter({
name: uid(),
'x-component': 'Input',
});
}}
>
insertAfter
</Menu.Item>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</span>
</div>
</DesignableContextProvider>
);
};

View File

@ -56,13 +56,13 @@ FormItem.DesignableBar = () => {
<Menu.Item
onClick={(e) => {
const title = uid();
schema.title = title;
// schema.title = title;
field.title = title;
setVisible(false);
refresh();
// refresh();
}}
>
</Menu.Item>
</Menu>
}

View File

@ -20,6 +20,7 @@ group:
// 添加其他节点
</Form>
// Form 用作 decorator 时只用于创建独立的作用域
<Table x-decorator={'Form'}/>
</pre>
@ -36,15 +37,19 @@ import React from 'react';
import { SchemaRenderer } from '../';
const schema = {
name: 'form',
type: 'object',
name: 'form',
'x-component': 'Form',
'x-component-props': {
layout: 'horizontal',
},
properties: {
username: {
type: 'string',
title: '用户名',
required: true,
'x-decorator': 'FormItem',
'x-designable-bar': 'FormItem.DesignableBar',
'x-component': 'Input',
},
password: {
@ -59,7 +64,7 @@ const schema = {
export default () => {
return (
<SchemaRenderer schema={schema} />
<SchemaRenderer debug={true} schema={schema} />
);
};
```
@ -124,25 +129,14 @@ const schema = {
type: 'void',
'x-component': 'Action',
'x-component-props': {
// block: true,
type: 'primary',
useAction: '{{ useLogin }}',
style: {
// width: '100%',
},
},
title: '提交',
},
reset: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
// block: true,
useAction: '{{ useLogin }}',
style: {
// width: '100%',
},
},
'x-component-props': {},
title: '重置',
},
},
@ -220,12 +214,7 @@ const schema = {
type: 'void',
'x-component': 'Action',
'x-component-props': {
// block: true,
type: 'primary',
useAction: '{{ useLogin }}',
style: {
// width: '100%',
},
},
title: '提交',
},
@ -233,11 +222,6 @@ const schema = {
type: 'void',
'x-component': 'Action',
'x-component-props': {
// block: true,
useAction: '{{ useLogin }}',
style: {
// width: '100%',
},
},
title: '重置',
},

View File

@ -9,77 +9,53 @@ import {
FormProvider,
ISchema,
} from '@formily/react';
import { SchemaRenderer, useDesignable } from '../';
import { useSchemaPath, SchemaField, useDesignable } from '../';
import get from 'lodash/get';
import { Dropdown, Menu } from 'antd';
import {
MenuOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import { useMouseEvents } from 'beautiful-react-hooks';
import { MenuOutlined } from '@ant-design/icons';
import cls from 'classnames';
import { FormLayout } from '@formily/antd';
import './style.less';
import { clone } from '@formily/shared';
function Blank() {
return null;
}
function useDesignableBar() {
const schema = useFieldSchema();
const options = useContext(SchemaOptionsContext);
const DesignableBar = get(options.components, schema['x-designable-bar']);
return {
DesignableBar: DesignableBar || Blank,
};
}
function useDefaultValues() {
return {};
}
export const Form: any = observer((props: any) => {
const scope = useContext(SchemaExpressionScopeContext);
const { useValues = useDefaultValues } = props;
const values = useValues();
const { useValues = () => ({}), ...others } = props;
const initialValues = useValues();
const form = useMemo(() => {
console.log('Form.useMemo', values);
return createForm({
values,
// values: clone(values),
});
}, [values]);
const schema = useFieldSchema();
const { schema: designableSchema, refresh } = useDesignable();
const formSchema: ISchema = schema['x-decorator'] === 'Form' ? {
console.log('Form.useMemo', initialValues);
return createForm({ initialValues });
}, []);
const { schema } = useDesignable();
const path = useSchemaPath();
return (
<FormProvider form={form}>
{schema['x-decorator'] === 'Form' ? (
<SchemaField
schema={{
type: 'object',
properties: {
[schema.name]: {
...schema.toJSON(),
"x-decorator": 'Form.Decorator',
}
}
} : schema.toJSON();
return (
<SchemaRenderer
scope={scope}
// components={options.components}
onRefresh={(subSchema: Schema) => {
designableSchema.properties = subSchema.properties;
refresh();
'x-path': path,
'x-decorator': 'Form.__Decorator',
},
},
}}
form={form}
schema={formSchema}
onlyRenderProperties
/>
) : (
<FormLayout layout={'vertical'} {...others}>
<SchemaField
schema={{
type: 'object',
properties: schema.properties,
}}
/>
</FormLayout>
)}
</FormProvider>
);
});
Form.Decorator = ({children}) => children;
Form.__Decorator = ({ children }) => children;
Form.DesignableBar = (props) => {
const { active } = props;

View File

@ -3,77 +3,10 @@ import { connect, mapProps, mapReadPretty } from '@formily/react';
import { Input, Popover, Button } from 'antd';
import { LoadingOutlined, CloseOutlined } from '@ant-design/icons';
import {
createFromIconfontCN,
UserOutlined,
TeamOutlined,
DatabaseOutlined,
DashboardOutlined,
SettingOutlined,
TableOutlined,
MenuOutlined,
HistoryOutlined,
NotificationOutlined,
} from '@ant-design/icons';
import * as antIcons from '@ant-design/icons';
export const IconFont = createFromIconfontCN({
scriptUrl: ['//at.alicdn.com/t/font_2261954_u9jzwc44ug.js'],
});
export const icons = new Map<string, any>();
export function registerIcon(type: string, icon: any = IconFont) {
icons.set(type.toLowerCase(), icon);
}
export function hasIcon(type: string) {
if (!type) {
return false;
}
return icons.has(type.toLowerCase());
}
export function registerIcons(components) {
Object.keys(components).forEach((type) => {
registerIcon(type, components[type]);
});
}
Object.keys(antIcons).forEach((name) => {
if (name.endsWith('Outlined')) {
registerIcon(name, antIcons[name]);
}
});
registerIcons({
// HistoryOutlined,
// MenuOutlined,
// TableOutlined,
// SettingOutlined,
// TeamOutlined,
// UserOutlined,
// DatabaseOutlined,
// DashboardOutlined,
// NotificationOutlined,
});
interface IconProps {
type: string;
[key: string]: any;
}
export function Icon(props: IconProps) {
const { type = '', ...restProps } = props;
if (type && icons.has(type.toLowerCase())) {
const IconComponent = icons.get(type.toLowerCase());
if (IconComponent === IconFont) {
return <IconFont type={type} />;
}
return <IconComponent {...restProps} />;
}
return <IconFont type={type} />;
}
icons,
hasIcon,
IconPicker as Icon,
} from '../../components/icon-picker';
function IconField(props: any) {
const { value, onChange } = props;

View File

@ -1,2 +1,11 @@
// export * from './DesignableSchemaField';
import { createContext } from 'react';
export * from '../components/schema-renderer';
export const VisibleContext = createContext(null);
export const DesignableBarContext = createContext(null);
export function useDefaultAction() {
return {
async run () {}
}
}

View File

@ -0,0 +1,86 @@
/**
* title: 横向菜单
*/
import React from 'react';
import { SchemaRenderer } from '@nocobase/client';
const schema = {
type: 'object',
properties: {
menu1: {
type: 'void',
'x-component': 'Menu',
'x-designable-bar': 'Menu.DesignableBar',
'x-component-props': {
mode: 'horizontal',
theme: 'dark',
},
properties: {
item1: {
type: 'void',
title: `菜单1`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
item2: {
type: 'void',
title: `菜单2`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
item3: {
type: 'void',
title: '菜单组3',
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.SubMenu',
properties: {
item5: {
type: 'void',
title: `子菜单5`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.SubMenu',
properties: {
item8: {
type: 'void',
title: `子菜单8`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
item9: {
type: 'void',
title: `子菜单9`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
},
},
},
},
item4: {
type: 'void',
title: '菜单组4',
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.SubMenu',
properties: {
item6: {
type: 'void',
title: `子菜单6`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
item7: {
type: 'void',
title: `子菜单7`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
},
},
},
},
},
};
export default () => {
return <SchemaRenderer schema={schema} />;
};

View File

@ -0,0 +1,90 @@
/**
* title: 横向菜单
*/
import React from 'react';
import { SchemaRenderer } from '@nocobase/client';
const schema = {
type: 'object',
properties: {
menu1: {
type: 'void',
'x-component': 'Menu',
'x-designable-bar': 'Menu.DesignableBar',
'x-component-props': {
mode: 'inline',
// theme: 'dark',
},
properties: {
item1: {
type: 'void',
title: `菜单1`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
item2: {
type: 'void',
title: `菜单2`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
item3: {
type: 'void',
title: '菜单组3',
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.SubMenu',
properties: {
item5: {
type: 'void',
title: `子菜单5`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.SubMenu',
properties: {
item8: {
type: 'void',
title: `子菜单8`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
item9: {
type: 'void',
title: `子菜单9`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
},
},
},
},
item4: {
type: 'void',
title: '菜单组4',
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.SubMenu',
properties: {
item6: {
type: 'void',
title: `子菜单6`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
item7: {
type: 'void',
title: `子菜单7`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
},
},
},
},
},
};
export default () => {
return (
<div style={{ width: 200 }}>
<SchemaRenderer schema={schema} />
</div>
);
};

View File

@ -0,0 +1,68 @@
/**
* title: 横向菜单
*/
import React from 'react';
import { SchemaRenderer } from '@nocobase/client';
import { ISchema } from '@formily/react';
const schema: ISchema = {
type: 'object',
properties: {
menu1: {
type: 'void',
'x-component': 'Menu',
'x-component-props': {
mode: 'horizontal',
// theme: 'dark',
},
properties: {
item1: {
type: 'void',
title: `菜单1`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Action',
properties: {
modal1: {
type: 'void',
title: '对话框标题',
'x-component': 'Action.Modal',
properties: {
input: {
type: 'string',
'x-component': 'Input',
},
},
},
},
},
item2: {
type: 'void',
title: `菜单2`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Action',
properties: {
modal2: {
type: 'void',
title: '对话框标题',
'x-component': 'Action.Modal',
properties: {
input: {
type: 'string',
'x-component': 'Input',
},
},
},
},
},
},
},
},
};
export default () => {
return (
<div style={{ width: 200 }}>
<SchemaRenderer schema={schema} />
</div>
);
};

View File

@ -0,0 +1,98 @@
import React, { useRef } from 'react';
import { SchemaRenderer } from '@nocobase/client';
import { Layout } from 'antd';
const schema = {
type: 'object',
properties: {
menu1: {
type: 'void',
'x-component': 'Menu',
'x-designable-bar': 'Menu.DesignableBar',
'x-component-props': {
sideMenuRef: '{{ sideMenuRef }}',
mode: 'mix',
theme: 'dark',
},
properties: {
item1: {
type: 'void',
title: `菜单1`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
item2: {
type: 'void',
title: `菜单2`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
item3: {
type: 'void',
title: '菜单组3',
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.SubMenu',
properties: {
item5: {
type: 'void',
title: `子菜单5`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.SubMenu',
properties: {
item8: {
type: 'void',
title: `子菜单8`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
item9: {
type: 'void',
title: `子菜单9`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
},
},
},
},
item4: {
type: 'void',
title: '菜单组4',
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.SubMenu',
properties: {
item6: {
type: 'void',
title: `子菜单6`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
item7: {
type: 'void',
title: `子菜单7`,
'x-designable-bar': 'Menu.DesignableBar',
'x-component': 'Menu.Item',
},
},
},
},
},
},
};
export default () => {
const sideMenuRef = useRef();
return (
<Layout>
<Layout.Header>
<SchemaRenderer
scope={{ sideMenuRef }}
schema={schema}
/>
</Layout.Header>
<Layout>
<Layout.Sider theme={'light'} ref={sideMenuRef}></Layout.Sider>
</Layout>
</Layout>
);
};

View File

@ -51,539 +51,16 @@ group:
### 横向菜单
```tsx
/**
* title: 横向菜单
*/
import React from 'react';
import { SchemaRenderer } from '../';
const schema = {
type: 'object',
properties: {
menu1: {
type: 'void',
'x-component': 'Menu',
'x-designable-bar': 'Menu.DesignableBar',
'x-component-props': {
mode: 'horizontal',
theme: 'dark',
},
properties: {
item1: {
type: 'void',
title: `菜单1`,
'x-component': 'Menu.Item',
},
item2: {
type: 'void',
title: `菜单2`,
'x-component': 'Menu.Item',
},
item3: {
type: 'void',
title: '菜单组3',
'x-component': 'Menu.SubMenu',
properties: {
item5: {
type: 'void',
title: `子菜单5`,
'x-component': 'Menu.SubMenu',
properties: {
item8: {
type: 'void',
title: `子菜单8`,
'x-component': 'Menu.Item',
},
item9: {
type: 'void',
title: `子菜单9`,
'x-component': 'Menu.Item',
},
},
},
}
},
item4: {
type: 'void',
title: '菜单组4',
'x-component': 'Menu.SubMenu',
properties: {
item6: {
type: 'void',
title: `子菜单6`,
'x-component': 'Menu.Item',
},
item7: {
type: 'void',
title: `子菜单7`,
'x-component': 'Menu.Item',
},
}
},
},
},
},
}
export default () => {
return (
<SchemaRenderer schema={schema} />
);
};
```
<code src="./demos/demo1.tsx"/>
### 竖向菜单
```tsx
/**
* title: 竖向菜单
*/
import React from 'react';
import { SchemaRenderer } from '../';
const schema = {
type: 'object',
properties: {
menu1: {
type: 'void',
name: 'name1',
'x-component': 'Menu',
// 'x-decorator': 'Menu.Designable',
'x-component-props': {
mode: 'inline',
},
properties: {
item1: {
type: 'void',
title: `菜单1`,
'x-component': 'Menu.Item',
},
item2: {
type: 'void',
title: `菜单2`,
'x-component': 'Menu.Item',
},
item3: {
type: 'void',
title: '菜单组3',
'x-component': 'Menu.SubMenu',
properties: {
item4: {
type: 'void',
title: `子菜单4`,
'x-component': 'Menu.Item',
},
item5: {
type: 'void',
title: `子菜单5`,
'x-component': 'Menu.Item',
},
}
},
item4: {
type: 'void',
title: '菜单组4',
'x-component': 'Menu.SubMenu',
properties: {
item6: {
type: 'void',
title: `子菜单6`,
'x-component': 'Menu.Item',
},
item7: {
type: 'void',
title: `子菜单7`,
'x-component': 'Menu.Item',
},
}
},
},
},
},
}
export default () => {
return (
<div style={{width: 200}}><SchemaRenderer schema={schema} /></div>
);
};
```
<code src="./demos/demo2.tsx"/>
### 混合菜单
```tsx
import React, { useRef, useState } from 'react';
import { SchemaRenderer } from '../';
import { MenuContainerContext } from './';
import { Layout } from 'antd';
export default () => {
const sideMenuRef = useRef();
const [activeKey, setActiveKey] = useState('item3');
const onSelect = (info) => {
setActiveKey(info.key);
console.log({ info })
}
const schema = {
type: 'object',
properties: {
menu1: {
type: 'void',
'x-component': 'Menu',
'x-component-props': {
sideMenuRef: '{{ sideMenuRef }}',
defaultSelectedKeys: [activeKey],
mode: 'mix',
theme: 'dark',
onSelect: '{{ onSelect }}',
},
properties: {
item1: {
type: 'void',
title: `菜单1`,
'x-component': 'Menu.Item',
},
item2: {
type: 'void',
title: `菜单2`,
'x-component': 'Menu.Item',
},
item3: {
type: 'void',
title: '菜单组3',
'x-component': 'Menu.SubMenu',
properties: {
item4: {
type: 'void',
title: `子菜单4`,
'x-component': 'Menu.Item',
},
item5: {
type: 'void',
title: `子菜单5`,
'x-component': 'Menu.SubMenu',
properties: {
item8: {
type: 'void',
title: `子菜单8`,
'x-component': 'Menu.Item',
},
item9: {
type: 'void',
title: `子菜单9`,
'x-component': 'Menu.Item',
},
},
},
}
},
item4: {
type: 'void',
title: '菜单组4',
'x-component': 'Menu.SubMenu',
properties: {
item6: {
type: 'void',
title: `子菜单6`,
'x-component': 'Menu.Item',
},
item7: {
type: 'void',
title: `子菜单7`,
'x-component': 'Menu.Item',
},
}
},
},
},
},
}
return (
<div>
<Layout>
<Layout.Header>
<SchemaRenderer
schema={schema}
scope={{ onSelect, sideMenuRef }}
/>
</Layout.Header>
<Layout>
<Layout.Sider ref={sideMenuRef} theme={'light'} width={200}>
</Layout.Sider>
<Layout.Content>
{activeKey}
</Layout.Content>
</Layout>
</Layout>
</div>
)
}
```
### 设计器模式
```tsx
import React, { useRef, useState } from 'react';
import { SchemaRenderer } from '../';
import { MenuContainerContext } from './';
import { Layout } from 'antd';
export default () => {
const sideMenuRef = useRef();
const [activeKey, setActiveKey] = useState('item3');
const onSelect = (info) => {
setActiveKey(info.key);
console.log({ info })
}
const schema = {
type: 'object',
properties: {
menu1: {
type: 'void',
'x-component': 'Menu',
'x-designable-bar': 'Menu.DesignableBar',
'x-component-props': {
defaultSelectedKeys: [activeKey],
mode: 'mix',
theme: 'dark',
sideMenuRef: '{{ sideMenuRef }}',
onSelect: '{{ onSelect }}',
},
properties: {
item1: {
type: 'void',
title: `菜单1`,
'x-component': 'Menu.Item',
},
item2: {
type: 'void',
title: `菜单2`,
'x-component': 'Menu.Item',
},
item3: {
type: 'void',
title: '菜单组3',
'x-component': 'Menu.SubMenu',
properties: {
item34: {
type: 'void',
title: `子菜单4`,
'x-component': 'Menu.Item',
},
item5: {
type: 'void',
title: `子菜单5`,
'x-component': 'Menu.SubMenu',
properties: {
item8: {
type: 'void',
title: `子菜单8`,
'x-component': 'Menu.Item',
},
item9: {
type: 'void',
title: `子菜单9`,
'x-component': 'Menu.Item',
},
},
},
}
},
item4: {
type: 'void',
title: '菜单组4',
'x-component': 'Menu.SubMenu',
properties: {
item6: {
type: 'void',
title: `子菜单6`,
'x-component': 'Menu.Item',
},
item7: {
type: 'void',
title: `子菜单7`,
'x-component': 'Menu.Item',
},
}
},
},
},
},
}
return (
<div>
<Layout>
<Layout.Header>
<SchemaRenderer
debug={true}
schema={schema}
scope={{ onSelect, sideMenuRef }}
/>
</Layout.Header>
<Layout>
<Layout.Sider
ref={sideMenuRef}
theme={'light'}
width={200}
>
</Layout.Sider>
<Layout.Content>
{activeKey}
</Layout.Content>
</Layout>
</Layout>
</div>
)
}
```
### 设计器模式 - 菜单项为空时
```tsx
import React, { useRef, useState } from 'react';
import { SchemaRenderer } from '../';
import { MenuContainerContext } from './';
import { Layout } from 'antd';
export default () => {
const sideMenuRef = useRef();
const [activeKey, setActiveKey] = useState('item3');
const onSelect = (info) => {
setActiveKey(info.key);
console.log({ info })
}
const schema = {
type: 'object',
properties: {
menu1: {
type: 'void',
'x-component': 'Menu',
'x-designable-bar': 'Menu.DesignableBar',
'x-component-props': {
defaultSelectedKeys: [activeKey],
mode: 'mix',
theme: 'dark',
sideMenuRef: '{{ sideMenuRef }}',
onSelect: '{{ onSelect }}',
},
},
},
}
return (
<div>
<Layout>
<Layout.Header>
<SchemaRenderer
schema={schema}
scope={{ onSelect, sideMenuRef }}
/>
</Layout.Header>
<Layout>
<Layout.Sider
ref={sideMenuRef}
theme={'light'}
width={200}
>
</Layout.Sider>
<Layout.Content>
{activeKey}
</Layout.Content>
</Layout>
</Layout>
</div>
)
}
```
<code src="./demos/demo4.tsx"/>
### Menu.Action
```tsx
/**
* title: 横向菜单
*/
import React from 'react';
import { SchemaRenderer } from '../';
const schema = {
type: 'object',
properties: {
menu1: {
type: 'void',
'x-component': 'Menu',
'x-designable-bar': 'Menu.DesignableBar',
'x-component-props': {
mode: 'horizontal',
theme: 'dark',
},
properties: {
item1: {
type: 'void',
title: `菜单1`,
'x-component': 'Menu.Item',
},
item2: {
type: 'void',
title: `菜单1`,
'x-component': 'Menu.Action',
properties: {
drawer1: {
type: 'void',
title: '抽屉标题',
'x-component': 'Action.Drawer',
'x-component-props': {},
properties: {
input: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
action2: {
type: 'void',
title: '打开二级抽屉',
// 'x-decorator': 'FormItem',
'x-component': 'Action',
properties: {
drawer1: {
type: 'void',
title: '二级抽屉标题',
'x-component': 'Action.Drawer',
'x-component-props': {},
properties: {
input: {
type: 'string',
'x-component': 'Input',
},
},
},
},
}
},
},
},
},
},
},
},
}
export default () => {
return (
<SchemaRenderer schema={schema} />
);
};
```
<code src="./demos/demo3.tsx"/>

View File

@ -5,6 +5,7 @@ import React, {
useEffect,
useRef,
useState,
forwardRef,
} from 'react';
import {
connect,
@ -29,311 +30,94 @@ import {
Modal,
Button,
} from 'antd';
import get from 'lodash/get';
import { uid } from '@formily/shared';
import {
MenuOutlined,
GroupOutlined,
PlusOutlined,
LinkOutlined,
AppstoreAddOutlined,
EditOutlined,
DeleteOutlined,
ArrowRightOutlined,
SettingOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
} from '@ant-design/icons';
import './style.less';
import { Icon } from '../icon-picker';
import { useHistory } from 'react-router-dom';
import cls from 'classnames';
import ReactDOM from 'react-dom';
import { useDesignable } from '../../components/schema-renderer';
import { MenuOutlined } from '@ant-design/icons';
import { IconPicker } from '../../components/icon-picker';
import { VisibleContext } from '..';
import { useMount } from 'ahooks';
import { useDesignable, SchemaRenderer } from '..';
import { Router } from 'react-router';
import { Action, useDefaultAction } from '../action';
import './style.less';
export type MenuType = React.FC<MenuProps & { hideSubMenu?: boolean }> & {
Item?: React.FC<MenuItemProps>;
SubMenu?: React.FC<SubMenuProps>;
Divider?: React.FC<DividerProps>;
DesignableBar?: React.FC<any>;
AddNew?: React.FC<any>;
Link?: React.FC<MenuItemProps>;
Action?: React.FC<MenuItemProps>;
Url?: React.FC<MenuItemProps & { url: string }>;
export const MenuModeContext = createContext(null);
const SideMenu = (props: any) => {
const { visible, selectedKey, onSelect } = props;
const { schema } = useDesignable();
if (!selectedKey || !visible) {
return null;
}
const child = schema.properties && schema.properties[selectedKey];
if (!child || child['x-component'] !== 'Menu.SubMenu') {
return null;
}
return (
<AntdMenu mode={'inline'} onSelect={onSelect}>
<RecursionField schema={child} onlyRenderProperties />
<AntdMenu.Item key={uid()}>{selectedKey}</AntdMenu.Item>
</AntdMenu>
);
};
export const MenuContext = createContext({
mode: null,
designableBar: null,
});
export const Menu: any = observer((props: any) => {
const { mode, onSelect, sideMenuRef, ...others } = props;
const { schema } = useDesignable();
const [selectedKey, setSelectedKey] = useState(null);
const ref = useRef();
function useDesignableBar() {
const { designableBar } = useContext(MenuContext);
const options = useContext(SchemaOptionsContext);
const DesignableBar = designableBar
? get(options.components, designableBar)
: null;
return DesignableBar || (() => null);
}
export const Menu: MenuType = observer((props: any) => {
const {
children,
sideMenuRef,
onSelect,
mode,
defaultSelectedKeys,
...others
} = props;
let defaultSelectedKey = defaultSelectedKeys ? defaultSelectedKeys[0] : null;
const { schema, schema: designableSchema, refresh } = useDesignable();
const designableBar = schema['x-designable-bar'];
const history = useHistory();
const renderSideMenu = (selectedKey) => {
if ((mode as any) !== 'mix') {
return;
}
if (!sideMenuRef || !sideMenuRef.current) {
return;
}
if (!selectedKey || !schema.properties) {
sideMenuRef.current.style.display = 'none';
return;
}
const subSchema = schema.properties[selectedKey];
if (!subSchema) {
sideMenuRef.current.style.display = 'none';
ReactDOM.render(null, sideMenuRef.current);
return;
}
if (subSchema['x-component'] !== 'Menu.SubMenu') {
sideMenuRef.current.style.display = 'none';
return;
}
const properties = subSchema.properties || {};
sideMenuRef.current.style.display = 'block';
const newProps = {};
Object.keys(properties || {}).forEach((name) => {
newProps[name] = properties[name].toJSON();
});
ReactDOM.render(
<Router history={history}>
<SchemaRenderer
key={`${Math.random()}`}
onRefresh={(subSchema: Schema) => {
const selected = designableSchema.properties[selectedKey];
const diff = subSchema.properties[`${schema.name}.${selectedKey}`];
Object.keys(selected.properties || {}).forEach((name) => {
selected.properties[name].parent.removeProperty(name);
});
Object.keys(diff.properties).forEach((name) => {
if (name.endsWith('-add-new')) {
return;
}
console.log('diff', name);
const current = diff.properties[name];
selected.addProperty(current.name, current.toJSON());
});
console.log({ selected });
refresh();
}}
schema={{
type: 'void',
name: `${schema.name}.${selectedKey}`,
'x-component': 'Menu',
'x-designable-bar': designableBar,
'x-component-props': {
mode: 'inline',
onSelect,
},
properties: {
...newProps,
[`${uid()}-add-new`]: {
type: 'void',
parentKey: subSchema['key'],
'x-component': 'Menu.AddNew',
},
},
}}
/>
</Router>,
sideMenuRef.current,
);
};
useMount(() => {
console.log({ defaultSelectedKey }, schema.properties);
renderSideMenu(defaultSelectedKey);
if (mode !== 'mix') {
return;
}
const sideMenuElement = sideMenuRef && (sideMenuRef.current as HTMLElement);
if (sideMenuElement && ref.current) {
sideMenuElement.querySelector(':scope > div').appendChild(ref.current);
}
});
const isEmpty = !Object.keys(designableSchema.properties || {}).length;
console.log({ designableSchema });
return (
<MenuContext.Provider
value={{
mode,
designableBar,
}}
>
<MenuModeContext.Provider value={mode}>
<AntdMenu
defaultSelectedKeys={defaultSelectedKeys}
{...others}
mode={mode === 'mix' ? 'horizontal' : mode}
onSelect={(info) => {
console.log('info.key', info.key);
renderSideMenu(info.key);
if (mode === 'mix') {
setSelectedKey(info.key);
}
onSelect && onSelect(info);
}}
mode={(mode as any) === 'mix' ? 'horizontal' : mode}
>
{!isEmpty ? (
<RecursionField schema={schema} onlyRenderProperties />
) : (
<SchemaRenderer
onRefresh={(subSchema: Schema) => {
Object.keys(subSchema.properties).forEach((name) => {
if (name === 'add-new') {
return;
}
designableSchema.addProperty(
name,
subSchema.properties[name].toJSON(),
);
});
refresh();
}}
schema={{
type: 'object',
properties: {
'add-new': {
parentKey: schema['key'],
type: 'void',
'x-component': 'Menu.AddNew',
},
},
}}
<AntdMenu.ItemGroup title={
<div></div>
}></AntdMenu.ItemGroup>
</AntdMenu>
{mode === 'mix' && (
<div ref={ref}>
<SideMenu
onSelect={onSelect}
visible={mode === 'mix'}
selectedKey={selectedKey}
sideMenuRef={sideMenuRef}
/>
</div>
)}
</AntdMenu>
</MenuContext.Provider>
</MenuModeContext.Provider>
);
});
const AddNewAction = () => {
const { schema, insertBefore } = useDesignable();
return (
<Dropdown
overlayStyle={{
minWidth: 150,
}}
trigger={['click']}
overlay={
<AntdMenu>
<AntdMenu.Item
onClick={() => {
const s = insertBefore({
type: 'void',
title: uid(),
key: uid(),
'x-component': 'Menu.Item',
});
console.log('s.s.s.s.s.s', s);
}}
style={{ minWidth: 150 }}
>
<MenuOutlined />
</AntdMenu.Item>
<AntdMenu.Item
onClick={() => {
const s = insertBefore({
type: 'void',
key: uid(),
title: uid(),
'x-component': 'Menu.SubMenu',
});
}}
>
<GroupOutlined />
</AntdMenu.Item>
<AntdMenu.Item>
<LinkOutlined />
</AntdMenu.Item>
</AntdMenu>
}
>
<Button block type={'dashed'} icon={<PlusOutlined />}></Button>
</Dropdown>
);
};
Menu.AddNew = observer((props) => {
const { designableBar } = useContext(MenuContext);
return designableBar ? (
<AntdMenu.ItemGroup
key={'add-menu-item'}
className={'menu-add'}
title={<AddNewAction />}
></AntdMenu.ItemGroup>
) : null;
});
Menu.Url = observer((props) => {
const DesignableBar = useDesignableBar();
const { schema } = useDesignable();
return (
<AntdMenu.Item
{...props}
// @ts-ignore
eventKey={schema.name}
key={schema.name}
onClick={(e) => {
window.open(props.url);
}}
icon={props.icon ? <Icon type={props.icon as string} /> : undefined}
>
{schema.title}
<DesignableBar />
</AntdMenu.Item>
);
});
Menu.Link = observer((props) => {
const history = useHistory();
const DesignableBar = useDesignableBar();
const { schema } = useDesignable();
return (
<AntdMenu.Item
{...props}
// @ts-ignore
eventKey={schema.name}
key={schema.name}
onClick={(e) => {
history.push(schema.name as string);
}}
icon={props.icon ? <Icon type={props.icon as string} /> : undefined}
>
{schema.title}
<DesignableBar />
</AntdMenu.Item>
);
});
Menu.Divider = observer(AntdMenu.Divider);
Menu.Item = observer((props: any) => {
const { useAction = useDefaultAction, ...others } = props;
const { run } = useAction();
const DesignableBar = useDesignableBar();
const { schema } = useDesignable();
const { icon } = props;
const { schema, DesignableBar } = useDesignable();
return (
<AntdMenu.Item
{...others}
onClick={async () => {
await run();
}}
schema={schema}
// @ts-ignore
{...props}
icon={<IconPicker type={icon} />}
eventKey={schema.name}
key={schema.name}
icon={props.icon ? <Icon type={props.icon as string} /> : undefined}
>
{schema.title}
<DesignableBar />
@ -342,32 +126,39 @@ Menu.Item = observer((props: any) => {
});
Menu.Action = observer((props: any) => {
const { icon, ...others } = props;
// const DesignableBar = useDesignableBar();
const { schema } = useDesignable();
const { icon } = props;
const { schema, DesignableBar } = useDesignable();
const [visible, setVisible] = useState(false);
return (
<Action
// @ts-ignore
eventKey={schema.name}
<VisibleContext.Provider value={[visible, setVisible]}>
<AntdMenu.Item
{...props}
key={schema.name}
icon={icon ? <Icon type={icon as string} /> : undefined}
ButtonComponent={AntdMenu.Item}
{...others}
/>
eventKey={schema.name}
icon={<IconPicker type={icon} />}
onClick={() => {
setVisible(true);
}}
>
{schema.title}
<DesignableBar />
</AntdMenu.Item>
{props.children}
{/* <RecursionField schema={schema} onlyRenderProperties /> */}
</VisibleContext.Provider>
);
});
Menu.SubMenu = observer((props) => {
const DesignableBar = useDesignableBar();
const { schema } = useDesignable();
const { mode } = useContext(MenuContext);
Menu.SubMenu = observer((props: any) => {
const { icon } = props;
const { schema, DesignableBar } = useDesignable();
const mode = useContext(MenuModeContext);
return mode === 'mix' ? (
<Menu.Item {...props} />
) : (
<AntdMenu.SubMenu
{...props}
// @ts-ignore
schema={schema}
icon={<IconPicker type={icon} />}
title={
<>
{schema.title} <DesignableBar />
@ -375,28 +166,12 @@ Menu.SubMenu = observer((props) => {
}
eventKey={schema.name}
key={schema.name}
icon={
props.icon ? (
<RecursionField
name={schema.name}
schema={
new Schema({
type: 'string',
'x-read-pretty': true,
default: props.icon,
'x-component': 'IconPicker',
})
}
onlyRenderProperties
/>
) : undefined
}
/>
>
<RecursionField schema={schema} onlyRenderProperties />
</AntdMenu.SubMenu>
);
});
Menu.Divider = observer(AntdMenu.Divider);
Menu.DesignableBar = (props) => {
const field = useField();
const [visible, setVisible] = useState(false);
@ -481,10 +256,7 @@ Menu.DesignableBar = (props) => {
});
}}
>
<DeleteOutlined />
</AntdMenu.Item>
<AntdMenu.Item>
<ModalButton />
</AntdMenu.Item>
</AntdMenu>
}
@ -496,27 +268,4 @@ Menu.DesignableBar = (props) => {
);
};
function ModalButton() {
const [visible, setVisible] = useState(false);
return (
<>
<div
onClick={() => {
setVisible(true);
}}
>
</div>
<Modal
visible={visible}
onCancel={() => {
setVisible(false);
}}
>
aaa
</Modal>
</>
);
}
export default Menu;

View File

@ -1,9 +1,9 @@
import React from 'react';
import { SchemaRenderer } from '../..';
import { Table } from '../';
import { uid } from '@formily/shared';
import Editor from '@monaco-editor/react';
import { createForm } from '@formily/core';
import { observer } from '@formily/react';
import { ISchema, observer } from '@formily/react';
function useAction() {
return {
@ -13,7 +13,7 @@ function useAction() {
}
}
const schema = {
const schema: ISchema = {
name: `table_${uid()}`,
type: 'array',
'x-component': 'Table',
@ -49,13 +49,9 @@ const schema = {
popover1: {
type: 'void',
title: '弹窗标题',
'x-decorator': 'Form',
'x-component': 'Action.Popover',
'x-component-props': {},
properties: {
[uid()]: {
name: 'form',
type: 'object',
'x-component': 'Form',
properties: {
filter: {
name: 'filter',
@ -98,17 +94,7 @@ const schema = {
},
},
}
},
action: {
type: 'void',
title: '提交',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useTableFilterAction }}',
},
},
},
},
}
},
},
},
@ -148,7 +134,7 @@ const schema = {
title: '提交',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useTableCreateAction }}',
useAction: '{{ Table.useTableCreateAction }}',
},
},
},
@ -163,7 +149,7 @@ const schema = {
title: '删除',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useTableDestroyAction }}',
useAction: '{{ Table.useTableDestroyAction }}',
},
},
},
@ -340,7 +326,7 @@ const schema = {
type: 'void',
'x-component': 'Form',
'x-component-props': {
useValues: '{{ useTableRow }}',
useValues: '{{ Table.useTableRow }}',
},
properties: {
field1: {
@ -360,7 +346,7 @@ const schema = {
title: '提交',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useTableUpdateAction }}',
useAction: '{{ Table.useTableUpdateAction }}',
},
},
},
@ -374,7 +360,7 @@ const schema = {
title: '删除',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useTableDestroyAction }}',
useAction: '{{ Table.useTableDestroyAction }}',
},
},
[uid()]: {
@ -474,7 +460,7 @@ const schema = {
type: 'void',
'x-component': 'Form',
'x-component-props': {
useValues: '{{ useTableRow }}',
useValues: '{{ Table.useTableRow }}',
},
properties: {
field1: {
@ -494,7 +480,7 @@ const schema = {
title: '提交',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useTableUpdateAction }}',
useAction: '{{ Table.useTableUpdateAction }}',
},
},
},
@ -508,7 +494,7 @@ const schema = {
title: '删除',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useTableDestroyAction }}',
useAction: '{{ Table.useTableDestroyAction }}',
},
},
},
@ -525,7 +511,7 @@ const form = createForm({
export default observer(() => {
return (
<div>
<SchemaRenderer form={form} schema={schema} />
<SchemaRenderer scope={{ Table }} form={form} schema={schema} />
</div>
);
});

View File

@ -26,14 +26,13 @@ import useRequest from '@ahooksjs/use-request';
import { BaseResult } from '@ahooksjs/use-request/lib/types';
import { uid, clone } from '@formily/shared';
import { MenuOutlined } from '@ant-design/icons';
import { useVisibleContext } from '../action';
import {
SortableHandle,
SortableContainer,
SortableElement,
} from 'react-sortable-hoc';
import cls from 'classnames';
import { getSchemaPath, useDesignable, useSchemaPath } from '../';
import { getSchemaPath, useDesignable, useSchemaPath, VisibleContext } from '../';
import './style.less';
interface TableRowProps {
@ -198,7 +197,7 @@ export function useTableDestroyAction() {
export function useTableFilterAction() {
const { field, refresh, params } = useTableContext();
const { setVisible } = useVisibleContext();
const [,setVisible] = useContext(VisibleContext);
const form = useForm();
return {
async run() {
@ -210,7 +209,7 @@ export function useTableFilterAction() {
export function useTableCreateAction() {
const { field, run: exec, params } = useTableContext();
const { setVisible } = useVisibleContext();
const [,setVisible] = useContext(VisibleContext);
const form = useForm();
return {
async run() {
@ -227,7 +226,7 @@ export function useTableCreateAction() {
export function useTableUpdateAction() {
const { field, refresh, params } = useTableContext();
const { setVisible } = useVisibleContext();
const [,setVisible] = useContext(VisibleContext);
const form = useForm();
return {
async run() {
@ -317,7 +316,7 @@ const TableContainer = observer((props) => {
{actionBars.top.map((actionBarSchema) => {
return (
<RecursionField
onlyRenderProperties
// onlyRenderProperties
schema={
new Schema({
type: 'object',
@ -419,6 +418,13 @@ export const Table: any = observer((props) => {
);
});
Table.useTableRow = useTableRow;
Table.useTableIndex = useTableIndex;
Table.useTableDestroyAction = useTableDestroyAction;
Table.useTableFilterAction = useTableFilterAction;
Table.useTableCreateAction = useTableCreateAction;
Table.useTableUpdateAction = useTableUpdateAction;
function Blank() {
return null;
}