This commit is contained in:
chenos 2021-07-19 23:57:54 +08:00
parent 2606f28059
commit 97ca17bc10
6 changed files with 355 additions and 85 deletions

View File

@ -47,6 +47,7 @@ import { Upload } from '../../schemas/upload';
import { FormItem } from '../../schemas/form-item'; import { FormItem } from '../../schemas/form-item';
import { BlockItem } from '../../schemas/block-item'; import { BlockItem } from '../../schemas/block-item';
import { DragAndDrop } from '../../schemas/drag-and-drop'; import { DragAndDrop } from '../../schemas/drag-and-drop';
import { TreeSelect } from '../../schemas/tree-select';
export const BlockContext = createContext({ dragRef: null }); export const BlockContext = createContext({ dragRef: null });
@ -67,6 +68,7 @@ export const SchemaField = createSchemaField({
ArrayCollapse, ArrayCollapse,
ArrayTable, ArrayTable,
FormLayout, FormLayout,
TreeSelect,
DragAndDrop, DragAndDrop,
@ -175,15 +177,21 @@ export function useDesignable(path?: any) {
const { schema = new Schema({}), refresh } = useContext(DesignableContext); const { schema = new Schema({}), refresh } = useContext(DesignableContext);
const schemaPath = path || useSchemaPath(); const schemaPath = path || useSchemaPath();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const currentSchema = let currentSchema =
findPropertyByPath(schema, schemaPath) || ({} as Schema); findPropertyByPath(schema, schemaPath) || ({} as Schema);
if (!currentSchema) {
currentSchema = fieldSchema;
}
if (Object.keys(currentSchema).length === 0) {
currentSchema = fieldSchema;
}
// console.log('useDesignable', { schema, schemaPath, currentSchema }); // console.log('useDesignable', { schema, schemaPath, currentSchema });
const options = useContext(SchemaOptionsContext); const options = useContext(SchemaOptionsContext);
const DesignableBar = const DesignableBar =
get(options.components, currentSchema['x-designable-bar']) || (() => null); get(options.components, currentSchema['x-designable-bar']) || (() => null);
return { return {
DesignableBar, DesignableBar,
schema: currentSchema || fieldSchema, schema: currentSchema,
refresh, refresh,
prepend: (property: ISchema, targetPath?: any): Schema => { prepend: (property: ISchema, targetPath?: any): Schema => {
let target = currentSchema; let target = currentSchema;

View File

@ -42,6 +42,12 @@ import {
PlusOutlined, PlusOutlined,
GroupOutlined, GroupOutlined,
LinkOutlined, LinkOutlined,
DeleteOutlined,
EditOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
ArrowRightOutlined,
DragOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { IconPicker } from '../../components/icon-picker'; import { IconPicker } from '../../components/icon-picker';
import { import {
@ -54,12 +60,13 @@ import {
import { useMount } from 'ahooks'; import { useMount } from 'ahooks';
import './style.less'; import './style.less';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { findPropertyByPath, useSchemaPath } from '@nocobase/client/lib'; import { findPropertyByPath, getSchemaPath, useSchemaPath } from '../../';
import { request } from '../'; import { request } from '../';
import defaultSchemas from './defaultSchemas'; import defaultSchemas from './defaultSchemas';
import _, { cloneDeep, get, isNull } from 'lodash'; import _, { cloneDeep, get, isNull } from 'lodash';
import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd'; import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd';
import deepmerge from 'deepmerge'; import deepmerge from 'deepmerge';
import { onFieldChange } from '@formily/core';
export const MenuModeContext = createContext(null); export const MenuModeContext = createContext(null);
@ -91,6 +98,8 @@ const SideMenu = (props: any) => {
export const Menu: any = observer((props: any) => { export const Menu: any = observer((props: any) => {
const { mode, onSelect, sideMenuRef, ...others } = props; const { mode, onSelect, sideMenuRef, ...others } = props;
const { schema } = useDesignable(); const { schema } = useDesignable();
const fieldSchema = useFieldSchema();
console.log('Menu.schema', schema, fieldSchema);
const [selectedKey, setSelectedKey] = useState(null); const [selectedKey, setSelectedKey] = useState(null);
const ref = useRef(); const ref = useRef();
const path = useSchemaPath(); const path = useSchemaPath();
@ -134,7 +143,9 @@ export const Menu: any = observer((props: any) => {
<Menu.AddNew key={uid()} path={path}> <Menu.AddNew key={uid()} path={path}>
{/* <PlusOutlined className={'nb-add-new-icon'} /> */} {/* <PlusOutlined className={'nb-add-new-icon'} /> */}
<Button <Button
className={`nb-add-new-menu-item menu-mode-${mode === 'mix' ? 'horizontal' : mode}`} className={`nb-add-new-menu-item menu-mode-${
mode === 'mix' ? 'horizontal' : mode
}`}
block block
type={mode == 'inline' ? 'dashed' : 'primary'} type={mode == 'inline' ? 'dashed' : 'primary'}
> >
@ -386,7 +397,7 @@ Menu.DesignableBar = (props) => {
const schemas = { const schemas = {
'Menu.Action': { 'Menu.Action': {
icon: <MenuOutlined />, icon: <MenuOutlined />,
title: '修改菜单项', title: '菜单项',
schema: { schema: {
type: 'object', type: 'object',
properties: { properties: {
@ -408,7 +419,7 @@ Menu.DesignableBar = (props) => {
}, },
'Menu.Item': { 'Menu.Item': {
icon: <MenuOutlined />, icon: <MenuOutlined />,
title: '修改菜单项', title: '菜单项',
schema: { schema: {
type: 'object', type: 'object',
properties: { properties: {
@ -430,7 +441,7 @@ Menu.DesignableBar = (props) => {
}, },
'Menu.Link': { 'Menu.Link': {
icon: <MenuOutlined />, icon: <MenuOutlined />,
title: '修改菜单项', title: '菜单项',
schema: { schema: {
type: 'object', type: 'object',
properties: { properties: {
@ -452,7 +463,7 @@ Menu.DesignableBar = (props) => {
}, },
'Menu.SubMenu': { 'Menu.SubMenu': {
icon: <GroupOutlined />, icon: <GroupOutlined />,
title: '修改菜单分组', title: '菜单分组',
schema: { schema: {
type: 'object', type: 'object',
properties: { properties: {
@ -474,7 +485,7 @@ Menu.DesignableBar = (props) => {
}, },
'Menu.URL': { 'Menu.URL': {
icon: <LinkOutlined />, icon: <LinkOutlined />,
title: '修改自定义链接', title: '自定义链接',
schema: { schema: {
type: 'object', type: 'object',
properties: { properties: {
@ -505,9 +516,11 @@ Menu.DesignableBar = (props) => {
const field = useField(); const field = useField();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { schema, remove, refresh, insertAfter, appendChild } = useDesignable(); const { schema, remove, refresh, insertAfter, insertBefore, appendChild } =
useDesignable();
const formConfig = schemas[schema['x-component']]; const formConfig = schemas[schema['x-component']];
console.log({ formConfig, schema }) const isSubMenu = schema['x-component'] === 'Menu.SubMenu';
return ( return (
<div className={cls('designable-bar', { active: visible })}> <div className={cls('designable-bar', { active: visible })}>
<div <div
@ -521,26 +534,62 @@ Menu.DesignableBar = (props) => {
overlayStyle={{ overlayStyle={{
minWidth: 150, minWidth: 150,
}} }}
// visible={visible}
// onVisibleChange={(visible) => {
// setVisible(visible);
// }}
trigger={['click']} trigger={['click']}
overlay={ overlay={
<AntdMenu> <AntdMenu
onClick={async (info) => {
if (['update', 'move', 'delete'].includes(info.key)) {
return;
}
const methodLabels = {
insertBefore: '之前',
insertAfter: '之后',
appendChild: '里',
};
const keys = info.key.split('.');
const method = keys.shift();
const type = keys.join('.');
const config = schemas[type];
if (!config) {
return;
}
const values = await FormDialog(
`在「${schema.title}${methodLabels[method]}新建${config.title}`,
() => {
return (
<FormLayout layout={'vertical'}>
<SchemaField schema={config.schema} />
</FormLayout>
);
},
).open();
const methods = {
insertAfter,
insertBefore,
appendChild,
};
const defaults = cloneDeep(defaultSchemas[type]);
const data = methods[method](deepmerge(defaults, values));
await createSchema(data);
}}
>
<AntdMenu.Item <AntdMenu.Item
key={'update'}
onClick={async () => { onClick={async () => {
const initialValues = {}; const initialValues = {};
Object.keys(formConfig.schema.properties).forEach((name) => { Object.keys(formConfig.schema.properties).forEach((name) => {
_.set(initialValues, name, get(schema, name)); _.set(initialValues, name, get(schema, name));
}); });
const values = await FormDialog(formConfig.title, () => { const values = await FormDialog(
return ( `修改${formConfig.title}`,
<FormLayout layout={'vertical'}> () => {
<SchemaField schema={formConfig.schema} /> return (
</FormLayout> <FormLayout layout={'vertical'}>
); <SchemaField schema={formConfig.schema} />
}).open({ </FormLayout>
);
},
).open({
initialValues, initialValues,
}); });
if (values.title) { if (values.title) {
@ -553,50 +602,203 @@ Menu.DesignableBar = (props) => {
field.componentProps['icon'] = icon; field.componentProps['icon'] = icon;
refresh(); refresh();
await updateSchema(schema); await updateSchema(schema);
// const title = uid();
// field.componentProps['icon'] = 'DeleteOutlined';
// schema['x-component-props']['icon'] = 'DeleteOutlined';
// schema.title = title;
// refresh();
}} }}
> >
{formConfig.title} <EditOutlined /> {formConfig.title}
</AntdMenu.Item> </AntdMenu.Item>
<AntdMenu.Item <AntdMenu.Item
key={'move'}
onClick={async () => { onClick={async () => {
const s = insertAfter({ let menuSchema: Schema;
...defaultSchemas['Menu.Link'], let parent = schema.parent;
title: uid(), while (parent) {
if (parent['x-component'] === 'Menu') {
menuSchema = parent;
break;
}
parent = parent.parent;
}
console.log({ menuSchema })
const toTreeData = (s: Schema) => {
const items = [];
Object.keys(s.properties || {}).forEach((name) => {
const current = s.properties[name];
if (
!(current['x-component'] as string).startsWith('Menu.')
) {
return;
}
// if (current.name === schema.name) {
// return;
// }
items.push({
key: current['key'] || current.name,
label: current.title,
title: current.title,
value: getSchemaPath(current).join('.'),
children: toTreeData(current),
});
});
return items;
};
const dataSource = toTreeData(menuSchema);
const values = await FormDialog(
`将「${schema.title}」移动到`,
() => {
return (
<FormLayout layout={'vertical'}>
<SchemaField
schema={{
type: 'object',
properties: {
path: {
type: 'string',
title: '目标位置',
enum: dataSource,
'x-decorator': 'FormItem',
'x-component': 'TreeSelect',
},
method: {
type: 'string',
default: 'insertAfter',
enum: [
{ label: '之后', value: 'insertAfter' },
{ label: '之前', value: 'insertBefore' },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
},
},
}}
/>
</FormLayout>
);
},
).open({
effects(form) {
onFieldChange('path', (field) => {
const target = findPropertyByPath(
schema.root,
// @ts-ignore
field.value,
);
console.log({ field });
field.query('method').take((f) => {
// @ts-ignore
// f.value = 'insertAfter';
// @ts-ignore
f.dataSource =
target['x-component'] === 'Menu.SubMenu'
? [
{ label: '之后', value: 'insertAfter' },
{ label: '之前', value: 'insertBefore' },
{ label: '组里', value: 'appendChild' },
]
: [
{ label: '之后', value: 'insertAfter' },
{ label: '之前', value: 'insertBefore' },
];
});
});
},
}); });
await createSchema(s); const methods = {
insertAfter,
insertBefore,
appendChild,
};
const source = methods[values.method](
schema.toJSON(),
values.path,
);
remove();
await updateSchema(source);
}} }}
> >
<DragOutlined />
</AntdMenu.Item> </AntdMenu.Item>
<AntdMenu.Item <AntdMenu.Divider />
onClick={async () => { <AntdMenu.SubMenu
const s = insertAfter({ key={'insertBefore'}
...defaultSchemas['Menu.SubMenu'], icon={<ArrowUpOutlined />}
title: uid(), title={'当前菜单项之前'}
});
await createSchema(s);
}}
> >
<AntdMenu.Item
</AntdMenu.Item> key={'insertBefore.Menu.Link'}
<AntdMenu.Item icon={<MenuOutlined />}
onClick={async () => { >
const s = appendChild({
...defaultSchemas['Menu.Link'], </AntdMenu.Item>
title: uid(), <AntdMenu.Item
}); key={'insertBefore.Menu.SubMenu'}
await createSchema(s); icon={<GroupOutlined />}
}} >
</AntdMenu.Item>
<AntdMenu.Item
key={'insertBefore.Menu.URL'}
icon={<LinkOutlined />}
>
</AntdMenu.Item>
</AntdMenu.SubMenu>
<AntdMenu.SubMenu
key={'insertAfter'}
icon={<ArrowDownOutlined />}
title={'当前菜单项之后'}
> >
<AntdMenu.Item
</AntdMenu.Item> key={'insertAfter.Menu.Link'}
icon={<MenuOutlined />}
>
</AntdMenu.Item>
<AntdMenu.Item
key={'insertAfter.Menu.SubMenu'}
icon={<GroupOutlined />}
>
</AntdMenu.Item>
<AntdMenu.Item
key={'insertAfter.Menu.URL'}
icon={<LinkOutlined />}
>
</AntdMenu.Item>
</AntdMenu.SubMenu>
{isSubMenu && (
<AntdMenu.SubMenu
key={'appendChild'}
icon={<ArrowRightOutlined />}
title={'当前菜单分组里'}
>
<AntdMenu.Item
key={'appendChild.Menu.Link'}
icon={<MenuOutlined />}
>
</AntdMenu.Item>
<AntdMenu.Item
key={'appendChild.Menu.SubMenu'}
icon={<GroupOutlined />}
>
</AntdMenu.Item>
<AntdMenu.Item
key={'appendChild.Menu.URL'}
icon={<LinkOutlined />}
>
</AntdMenu.Item>
</AntdMenu.SubMenu>
)}
<AntdMenu.Divider />
<AntdMenu.Item <AntdMenu.Item
key={'delete'}
onClick={() => { onClick={() => {
Modal.confirm({ Modal.confirm({
title: '删除菜单', title: '删除菜单',
@ -608,7 +810,7 @@ Menu.DesignableBar = (props) => {
}); });
}} }}
> >
<DeleteOutlined />
</AntdMenu.Item> </AntdMenu.Item>
</AntdMenu> </AntdMenu>
} }

View File

@ -0,0 +1,27 @@
import React from 'react'
import { connect, mapReadPretty, mapProps } from '@formily/react'
import { TreeSelect as AntdTreeSelect } from 'antd'
import { Display } from '../display'
import { LoadingOutlined } from '@ant-design/icons'
export const TreeSelect = connect(
AntdTreeSelect,
mapProps(
{
dataSource: 'treeData',
},
(props, field) => {
return {
...props,
suffixIcon:
field?.['loading'] || field?.['validating'] ? (
<LoadingOutlined />
) : (
props.suffixIcon
),
}
}
),
mapReadPretty(Display.TreeSelect)
)
export default TreeSelect

View File

@ -7,12 +7,12 @@ export const create = async (ctx: actions.Context, next: actions.Next) => {
const values = cloneDeep(ctx.action.params.values); const values = cloneDeep(ctx.action.params.values);
ctx.action.mergeParams( ctx.action.mergeParams(
{ {
values: omit(values, [ values: cloneDeep(omit(values, [
'__insertAfter__', '__insertAfter__',
'__insertBefore__', '__insertBefore__',
'__prepend__', '__prepend__',
'_isJSONSchemaObject', '_isJSONSchemaObject',
]), ])),
}, },
{ {
payload: 'replace', payload: 'replace',
@ -22,24 +22,11 @@ export const create = async (ctx: actions.Context, next: actions.Next) => {
const sticky = values['__prepend__']; const sticky = values['__prepend__'];
const targetKey = values['__insertAfter__'] || values['__insertBefore__']; const targetKey = values['__insertAfter__'] || values['__insertBefore__'];
if (sticky || targetKey) { if (sticky || targetKey) {
console.log({ const body = ctx.body;
associatedKey: values.parentKey,
resourceKey: ctx.body.key,
values: sticky ? {
field: 'sort',
sticky: true,
} : {
field: 'sort',
insertAfter: !!values['__insertAfter__'],
target: {
key: targetKey,
},
},
});
ctx.action.mergeParams( ctx.action.mergeParams(
{ {
associatedKey: values.parentKey, associatedKey: values.parentKey,
resourceKey: ctx.body.key, resourceKey: body.key,
values: sticky ? { values: sticky ? {
field: 'sort', field: 'sort',
sticky: true, sticky: true,
@ -56,8 +43,58 @@ export const create = async (ctx: actions.Context, next: actions.Next) => {
payload: 'replace', payload: 'replace',
}, },
); );
console.log(ctx.action.params.values);
await middlewares.associated(ctx, async () => { }); await middlewares.associated(ctx, async () => { });
await sort(ctx, async () => { }); await sort(ctx, async () => { });
console.log(ctx.body.toJSON());
}
await next();
};
export const update = async (ctx: actions.Context, next: actions.Next) => {
const values = cloneDeep(ctx.action.params.values);
ctx.action.mergeParams(
{
values: cloneDeep(omit(values, [
'__insertAfter__',
'__insertBefore__',
'__prepend__',
'_isJSONSchemaObject',
])),
},
{
payload: 'replace',
},
);
await actions.common.update(ctx, async () => { });
const sticky = values['__prepend__'];
const targetKey = values['__insertAfter__'] || values['__insertBefore__'];
if (sticky || targetKey) {
const body = ctx.body;
ctx.action.mergeParams(
{
associatedKey: values.parentKey,
resourceKey: body.key,
values: sticky ? {
field: 'sort',
sticky: true,
target: {},
} : {
field: 'sort',
insertAfter: !!values['__insertAfter__'],
target: {
key: targetKey,
},
},
},
{
payload: 'replace',
},
);
console.log(ctx.action.params.values);
await middlewares.associated(ctx, async () => { });
await sort(ctx, async () => { });
console.log(ctx.body.toJSON());
} }
await next(); await next();
}; };

View File

@ -4,16 +4,10 @@ import { Model } from '@nocobase/database';
export class UISchema extends Model { export class UISchema extends Model {
static async create(value?: any, options?: any): Promise<any> { static async create(value?: any, options?: any): Promise<any> {
// console.log({ value }); // console.log({ value });
const attributes = this.toAttributes(value); const attributes = this.toAttributes(_.cloneDeep(value));
console.log({ attributes })
// @ts-ignore // @ts-ignore
const model: Model = await super.create(attributes, options); const model: Model = await super.create(attributes, options);
if (value['__prepend__']) {
console.log('__prepend__', model.parentKey);
} else if (value['__insertAfter__']) {
console.log('__insertAfter__', model.parentKey);
} else if (value['__insertBefore__']) {
console.log('__insertBefore__', model.parentKey);
}
if (!attributes.children) { if (!attributes.children) {
attributes.children = this.properties2children(attributes.properties); attributes.children = this.properties2children(attributes.properties);
await model.updateAssociation('children', attributes.children, options); await model.updateAssociation('children', attributes.children, options);
@ -22,14 +16,15 @@ export class UISchema extends Model {
} }
async update(key?: any, value?: any, options?: any): Promise<any> { async update(key?: any, value?: any, options?: any): Promise<any> {
const opts = this.get('options') || {};
if (typeof key === 'object') { if (typeof key === 'object') {
const attributes = UISchema.toAttributes(key); const attributes = UISchema.toAttributes(key, opts);
return super.update(attributes, value, options); return super.update(attributes, value, options);
} }
return super.update(key, value, options); return super.update(key, value, options);
} }
static toAttributes(value = {}): any { static toAttributes(value = {}, opts = {}): any {
const data = _.cloneDeep(value); const data = _.cloneDeep(value);
const keys = [ const keys = [
'properties', 'properties',
@ -38,7 +33,7 @@ export class UISchema extends Model {
]; ];
const attrs = _.pick(data, keys); const attrs = _.pick(data, keys);
const options = _.omit(data, keys); const options = _.omit(data, keys);
return { ...attrs, options }; return { ...attrs, options: {...options, ...opts} };
} }
static properties2children(properties = []) { static properties2children(properties = []) {

View File

@ -2,7 +2,7 @@ import path from 'path';
import { Application } from '@nocobase/server'; import { Application } from '@nocobase/server';
import { registerModels, Table } from '@nocobase/database'; import { registerModels, Table } from '@nocobase/database';
import * as models from './models'; import * as models from './models';
import { create, getTree } from './actions'; import { create, update, getTree } from './actions';
export default async function (this: Application, options = {}) { export default async function (this: Application, options = {}) {
const database = this.database; const database = this.database;
@ -13,5 +13,6 @@ export default async function (this: Application, options = {}) {
}); });
this.resourcer.registerActionHandler('ui_schemas:create', create); this.resourcer.registerActionHandler('ui_schemas:create', create);
this.resourcer.registerActionHandler('ui_schemas:update', update);
this.resourcer.registerActionHandler('ui_schemas:getTree', getTree); this.resourcer.registerActionHandler('ui_schemas:getTree', getTree);
} }