lots of updates

This commit is contained in:
chenos 2021-07-19 18:33:03 +08:00
parent 63538cf240
commit 2606f28059
22 changed files with 570 additions and 105 deletions

View File

@ -7,16 +7,7 @@ export default () => {
name: 'collections', name: 'collections',
'x-component': 'DatabaseCollection', 'x-component': 'DatabaseCollection',
'x-component-props': {}, 'x-component-props': {},
default: [ default: [],
{
name: 'test1',
title: '数据表 1',
},
{
name: 'test2',
title: '数据表 2',
},
],
properties: { properties: {
title: { title: {
type: 'string', type: 'string',

View File

@ -99,7 +99,7 @@ export const SchemaField = createSchemaField({
}, },
}); });
export const DesignableContext = createContext<DesignableContextProps>(null); export const DesignableContext = createContext<DesignableContextProps>({});
export function useSchema(path?: any) { export function useSchema(path?: any) {
const { schema, refresh } = useContext(DesignableContext); const { schema, refresh } = useContext(DesignableContext);
@ -124,10 +124,14 @@ export function findPropertyByPath(schema: Schema, path?: any): Schema {
let property = schema; let property = schema;
while (arr.length) { while (arr.length) {
const name = arr.shift(); const name = arr.shift();
if (!property.properties) {
console.warn('property does not exist.');
return null;
}
property = property.properties[name]; property = property.properties[name];
if (!property) { if (!property) {
console.error('property does not exist.'); console.warn('property does not exist.');
break; return null;
} }
} }
return property; return property;
@ -168,7 +172,7 @@ function setKeys(schema: ISchema, parentKey = null) {
} }
export function useDesignable(path?: any) { export function useDesignable(path?: any) {
const { 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 = const currentSchema =

View File

@ -185,18 +185,20 @@ Action.Drawer = observer((props: any) => {
}); });
Action.Dropdown = observer((props: any) => { Action.Dropdown = observer((props: any) => {
const { buttonProps = {}, ...others } = props;
const { schema } = useDesignable(); const { schema } = useDesignable();
const componentProps = schema.parent['x-component-props'] || {}; const componentProps = schema.parent['x-component-props'] || {};
return ( return (
<Dropdown <Dropdown
trigger={['click']} trigger={['click']}
{...others}
overlay={ overlay={
<Menu> <Menu>
<RecursionField schema={schema} onlyRenderProperties /> <RecursionField schema={schema} onlyRenderProperties />
</Menu> </Menu>
} }
> >
<Button {...componentProps}>{schema.title || schema.parent.title}</Button> <Button {...buttonProps} {...componentProps}>{schema.title || schema.parent.title}</Button>
</Dropdown> </Dropdown>
); );
}); });

View File

@ -94,11 +94,11 @@ const Block = (props) => {
<div <div
onMouseEnter={(e) => { onMouseEnter={(e) => {
setActive(true); setActive(true);
console.log('e.onMouseEnter', new Date().toString()); // console.log('e.onMouseEnter', new Date().toString());
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
setActive(false); setActive(false);
console.log('e.onMouseLeave', new Date().toString()); // console.log('e.onMouseLeave', new Date().toString());
}} }}
className={cls('nb-grid-block', 'designable-form-item', { active })} className={cls('nb-grid-block', 'designable-form-item', { active })}
> >

View File

@ -8,6 +8,7 @@ import {
Schema, Schema,
useFieldSchema, useFieldSchema,
useForm, useForm,
FormConsumer,
} from '@formily/react'; } from '@formily/react';
import { ArrayCollapse, FormLayout } from '@formily/antd'; import { ArrayCollapse, FormLayout } from '@formily/antd';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
@ -21,6 +22,9 @@ import {
Select, Select,
Divider, Divider,
Input, Input,
Badge,
message,
Spin,
} from 'antd'; } from 'antd';
import { options, interfaces } from './interfaces'; import { options, interfaces } from './interfaces';
import { import {
@ -32,7 +36,7 @@ import {
import cls from 'classnames'; import cls from 'classnames';
import './style.less'; import './style.less';
import Modal from 'antd/lib/modal/Modal'; import Modal from 'antd/lib/modal/Modal';
import { get } from 'lodash'; import { clone, cloneDeep, get } from 'lodash';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRequest } from 'ahooks'; import { useRequest } from 'ahooks';
import { createOrUpdateCollection, deleteCollection } from '..'; import { createOrUpdateCollection, deleteCollection } from '..';
@ -46,19 +50,33 @@ export const DatabaseCollection = observer((props) => {
const form = useForm(); const form = useForm();
const [newValue, setNewValue] = useState(''); const [newValue, setNewValue] = useState('');
useRequest('collections:list?sort=-created_at', { const { run, loading } = useRequest('collections:findAll', {
formatResult: (result) => result?.data, formatResult: (result) => result?.data,
onSuccess(data) { onSuccess(data) {
field.setValue(data); field.setValue(data);
console.log('onSuccess', data); console.log('onSuccess', data);
}, },
manual: true,
}); });
return ( return (
<div> <div>
<Button <Button
onClick={() => { className={'nb-database-config'}
style={{
height: 46,
borderRadius: 0,
}}
onClick={async () => {
setVisible(true); setVisible(true);
await run();
if (field.value?.length === 0) {
field.push({
name: `t_${uid()}`,
unsaved: true,
fields: [],
});
}
}} }}
type={'primary'} type={'primary'}
> >
@ -68,6 +86,7 @@ export const DatabaseCollection = observer((props) => {
title={ title={
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<Select <Select
loading={loading}
value={activeIndex} value={activeIndex}
style={{ minWidth: 300, textAlign: 'center' }} style={{ minWidth: 300, textAlign: 'center' }}
onChange={(value) => { onChange={(value) => {
@ -97,8 +116,9 @@ export const DatabaseCollection = observer((props) => {
}} }}
onSearch={async (value) => { onSearch={async (value) => {
const data = { const data = {
name: uid(), name: `t_${uid()}`,
title: value, title: value,
fields: [],
}; };
field.push(data); field.push(data);
setActiveIndex(field.value.length - 1); setActiveIndex(field.value.length - 1);
@ -114,7 +134,12 @@ export const DatabaseCollection = observer((props) => {
> >
{field.value?.map((item, index) => { {field.value?.map((item, index) => {
return ( return (
<Select.Option value={index} label={item.title || '未命名'}> <Select.Option
value={index}
label={`${item.title || '未命名'}${
item.unsaved ? ' (未保存)' : ''
}`}
>
<div <div
style={{ style={{
display: 'flex', display: 'flex',
@ -122,17 +147,26 @@ export const DatabaseCollection = observer((props) => {
alignItems: 'center', alignItems: 'center',
}} }}
> >
{item.title || '未命名'} {item.title || '未命名'} {item.unsaved ? '(未保存)' : ''}
<DeleteOutlined <DeleteOutlined
onClick={async (e) => { onClick={async (e) => {
e.stopPropagation(); e.stopPropagation();
field.remove(index); field.remove(index);
if (field.value?.length === 0) {
field.push({
name: `t_${uid()}`,
unsaved: true,
fields: [],
});
}
if (activeIndex === index) { if (activeIndex === index) {
setActiveIndex(0); setActiveIndex(0);
} else if (activeIndex > index) { } else if (activeIndex > index) {
setActiveIndex(activeIndex - 1); setActiveIndex(activeIndex - 1);
} }
await deleteCollection(item.name); if (item.name) {
await deleteCollection(item.name);
}
}} }}
/> />
</div> </div>
@ -146,30 +180,38 @@ export const DatabaseCollection = observer((props) => {
onCancel={() => { onCancel={() => {
setVisible(false); setVisible(false);
}} }}
okText={'保存'}
cancelText={'关闭'}
onOk={async () => { onOk={async () => {
try { try {
form.clearErrors(); form.clearErrors();
await form.validate(`${field.address.entire}.${activeIndex}.*`); await form.validate(`${field.address.entire}.${activeIndex}.*`);
setVisible(false); delete field.value[activeIndex]['unsaved'];
await createOrUpdateCollection(field.value[activeIndex]); await createOrUpdateCollection(field.value[activeIndex]);
console.log( message.success('保存成功');
`${field.address.entire}.${activeIndex}.*`,
field.value[activeIndex],
);
} catch (error) {} } catch (error) {}
}} }}
> >
<FormLayout layout={'vertical'}> {loading ? (
<RecursionField <Spin />
name={activeIndex} ) : (
schema={ <FormLayout layout={'vertical'}>
new Schema({ <RecursionField
type: 'object', name={activeIndex}
properties: schema.properties, schema={
}) new Schema({
} type: 'object',
/> properties: schema.properties,
</FormLayout> })
}
/>
{/* <FormConsumer>
{form => (
<pre>{JSON.stringify(form.values, null, 2)}</pre>
)}
</FormConsumer> */}
</FormLayout>
)}
</Modal> </Modal>
</div> </div>
); );
@ -177,8 +219,13 @@ export const DatabaseCollection = observer((props) => {
export const DatabaseField: any = observer((props) => { export const DatabaseField: any = observer((props) => {
const field = useField<Formily.Core.Models.ArrayField>(); const field = useField<Formily.Core.Models.ArrayField>();
console.log('DatabaseField', field.value); useEffect(()=> {
if (!field.value) {
field.setValue([]);
}
}, [])
const [activeKey, setActiveKey] = useState(null); const [activeKey, setActiveKey] = useState(null);
console.log('DatabaseField', field);
return ( return (
<div> <div>
<Collapse <Collapse
@ -191,12 +238,16 @@ export const DatabaseField: any = observer((props) => {
> >
{field.value?.map((item, index) => { {field.value?.map((item, index) => {
const schema = interfaces.get(item.interface); const schema = interfaces.get(item.interface);
console.log({ schema }); const path = field.address.concat(index);
const errors = field.form.queryFeedbacks({
type: 'error',
address: `*(${path},${path}.*)`,
});
return ( return (
<Collapse.Panel <Collapse.Panel
header={ header={
<> <>
{(item.ui && item.ui.title) || ( {(item.uiSchema && item.uiSchema.title) || (
<i style={{ color: 'rgba(0, 0, 0, 0.25)' }}></i> <i style={{ color: 'rgba(0, 0, 0, 0.25)' }}></i>
)}{' '} )}{' '}
<Tag>{schema.title}</Tag> <Tag>{schema.title}</Tag>
@ -206,16 +257,18 @@ export const DatabaseField: any = observer((props) => {
</> </>
} }
extra={[ extra={[
<Badge count={errors.length} />,
<DeleteOutlined <DeleteOutlined
onClick={() => { onClick={(e) => {
e.stopPropagation();
field.remove(index); field.remove(index);
}} }}
/>, />,
]} ]}
key={item.id} key={item.key}
> >
<RecursionField <RecursionField
key={`${item.id}_${index}`} key={`${item.key}_${index}`}
name={index} name={index}
schema={ schema={
new Schema({ new Schema({
@ -252,14 +305,14 @@ export const DatabaseField: any = observer((props) => {
return; return;
} }
const data = { const data = {
...schema.default, ...cloneDeep(schema.default),
id: uid(), key: uid(),
name: uid(), name: `f_${uid()}`,
interface: info.key, interface: info.key,
}; };
field.push(data); field.push(data);
setActiveKey(data.id); setActiveKey(data.key);
console.log('info.key', info.key, schema); console.log('info.key', field.value);
}} }}
> >
{options.map((option) => ( {options.map((option) => (

View File

@ -8,7 +8,7 @@ export const select: ISchema = {
default: { default: {
dataType: 'string', dataType: 'string',
// name, // name,
ui: { uiSchema: {
type: 'string', type: 'string',
// title, // title,
'x-component': 'Select', 'x-component': 'Select',
@ -16,7 +16,7 @@ export const select: ISchema = {
} as ISchema, } as ISchema,
}, },
properties: { properties: {
'ui.title': { 'uiSchema.title': {
type: 'string', type: 'string',
required: true, required: true,
title: '字段名称', title: '字段名称',
@ -42,7 +42,7 @@ export const select: ISchema = {
{ label: 'Text', value: 'text' }, { label: 'Text', value: 'text' },
], ],
}, },
'ui.enum': { 'uiSchema.enum': {
type: 'array', type: 'array',
title: '可选项', title: '可选项',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
@ -132,7 +132,7 @@ export const select: ISchema = {
}, },
}, },
}, },
'ui.required': { 'uiSchema.required': {
type: 'string', type: 'string',
title: '必填', title: '必填',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',

View File

@ -8,14 +8,14 @@ export const string: ISchema = {
default: { default: {
dataType: 'string', dataType: 'string',
// name, // name,
ui: { uiSchema: {
type: 'string', type: 'string',
// title, // title,
'x-component': 'Input', 'x-component': 'Input',
} as ISchema, } as ISchema,
}, },
properties: { properties: {
'ui.title': { 'uiSchema.title': {
type: 'string', type: 'string',
title: '字段名称', title: '字段名称',
required: true, required: true,
@ -41,7 +41,7 @@ export const string: ISchema = {
{ label: 'Text', value: 'text' }, { label: 'Text', value: 'text' },
], ],
}, },
'ui.required': { 'uiSchema.required': {
type: 'string', type: 'string',
title: '必填', title: '必填',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',

View File

@ -7,7 +7,7 @@ export const subTable: ISchema = {
group: 'relation', group: 'relation',
default: { default: {
// name, // name,
ui: { uiSchema: {
type: 'string', type: 'string',
// title, // title,
'x-component': 'Select', 'x-component': 'Select',
@ -15,7 +15,7 @@ export const subTable: ISchema = {
} as ISchema, } as ISchema,
}, },
properties: { properties: {
'ui.title': { 'uiSchema.title': {
type: 'string', type: 'string',
required: true, required: true,
title: '字段名称', title: '字段名称',

View File

@ -8,14 +8,14 @@ export const textarea: ISchema = {
default: { default: {
dataType: 'text', dataType: 'text',
// name, // name,
ui: { uiSchema: {
type: 'string', type: 'string',
// title, // title,
'x-component': 'Input.TextArea', 'x-component': 'Input.TextArea',
} as ISchema, } as ISchema,
}, },
properties: { properties: {
'ui.title': { 'uiSchema.title': {
type: 'string', type: 'string',
required: true, required: true,
title: '字段名称', title: '字段名称',
@ -41,7 +41,7 @@ export const textarea: ISchema = {
{ label: 'Text', value: 'text' }, { label: 'Text', value: 'text' },
], ],
}, },
'ui.required': { 'uiSchema.required': {
type: 'string', type: 'string',
title: '必填', title: '必填',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',

View File

@ -1,3 +1,16 @@
.ant-collapse.empty { .ant-collapse.empty {
border: 0; border: 0;
} }
.nb-database-config {
height: 46px;
border-radius: 0px;
background: none;
border: 0;
&:hover {
background: #1890ff;
}
&:active {
background: #1890ff;
}
}

View File

@ -46,6 +46,16 @@ export async function createSchema(schema: ISchema) {
}); });
}; };
export async function updateSchema(schema: ISchema) {
if (!schema['key']) {
return;
}
return await request(`ui_schemas:update/${schema.key}`, {
method: 'post',
data: schema.toJSON(),
});
};
export async function removeSchema(schema: ISchema) { export async function removeSchema(schema: ISchema) {
if (!schema['key']) { if (!schema['key']) {
return; return;

View File

@ -32,17 +32,34 @@ import {
} from 'antd'; } from 'antd';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import cls from 'classnames'; import cls from 'classnames';
import { useDesignable } from '../../components/schema-renderer'; import {
import { MenuOutlined, PlusOutlined } from '@ant-design/icons'; SchemaField,
SchemaRenderer,
useDesignable,
} from '../../components/schema-renderer';
import {
MenuOutlined,
PlusOutlined,
GroupOutlined,
LinkOutlined,
} from '@ant-design/icons';
import { IconPicker } from '../../components/icon-picker'; import { IconPicker } from '../../components/icon-picker';
import { createSchema, removeSchema, useDefaultAction, VisibleContext } from '..'; import {
createSchema,
removeSchema,
updateSchema,
useDefaultAction,
VisibleContext,
} from '..';
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, useSchemaPath } from '@nocobase/client/lib';
import { request } from '../'; import { request } from '../';
import defaultSchemas from './defaultSchemas'; import defaultSchemas from './defaultSchemas';
import { get } from 'lodash'; import _, { cloneDeep, get, isNull } from 'lodash';
import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd';
import deepmerge from 'deepmerge';
export const MenuModeContext = createContext(null); export const MenuModeContext = createContext(null);
@ -62,8 +79,8 @@ const SideMenu = (props: any) => {
<AntdMenu mode={'inline'} onSelect={onSelect}> <AntdMenu mode={'inline'} onSelect={onSelect}>
<RecursionField schema={child} onlyRenderProperties /> <RecursionField schema={child} onlyRenderProperties />
<Menu.AddNew key={uid()} path={[...path, selectedKey]}> <Menu.AddNew key={uid()} path={[...path, selectedKey]}>
<Button block type={'dashed'}> <Button className={'nb-add-new-menu-item'} block type={'dashed'}>
<PlusOutlined className={'nb-add-new-icon'} /> <PlusOutlined />
</Button> </Button>
</Menu.AddNew> </Menu.AddNew>
</AntdMenu> </AntdMenu>
@ -109,13 +126,20 @@ export const Menu: any = observer((props: any) => {
setSelectedKey(info.key); setSelectedKey(info.key);
} }
const selectedSchema = schema.properties[info.key]; const selectedSchema = schema.properties[info.key];
console.log({ selectedSchema }) console.log({ selectedSchema });
onSelect && onSelect({ ...info, schema: selectedSchema }); onSelect && onSelect({ ...info, schema: selectedSchema });
}} }}
> >
<RecursionField schema={schema} onlyRenderProperties /> <RecursionField schema={schema} onlyRenderProperties />
<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
className={`nb-add-new-menu-item menu-mode-${mode === 'mix' ? 'horizontal' : mode}`}
block
type={mode == 'inline' ? 'dashed' : 'primary'}
>
<PlusOutlined />
</Button>
</Menu.AddNew> </Menu.AddNew>
</AntdMenu> </AntdMenu>
{mode === 'mix' && ( {mode === 'mix' && (
@ -126,7 +150,8 @@ export const Menu: any = observer((props: any) => {
const keyPath = [selectedKey, ...info.keyPath]; const keyPath = [selectedKey, ...info.keyPath];
const selectedSchema = findPropertyByPath(schema, keyPath); const selectedSchema = findPropertyByPath(schema, keyPath);
console.log('keyPath', keyPath, selectedSchema); console.log('keyPath', keyPath, selectedSchema);
onSelect && onSelect({ ...info, keyPath, schema: selectedSchema }); onSelect &&
onSelect({ ...info, keyPath, schema: selectedSchema });
}} }}
selectedKey={selectedKey} selectedKey={selectedKey}
sideMenuRef={sideMenuRef} sideMenuRef={sideMenuRef}
@ -240,39 +265,117 @@ Menu.SubMenu = observer((props: any) => {
Menu.AddNew = observer((props: any) => { Menu.AddNew = observer((props: any) => {
const { appendChild } = useDesignable(props.path); const { appendChild } = useDesignable(props.path);
const schemas = {
'Menu.Link': {
icon: <MenuOutlined />,
title: '新建菜单项',
schema: {
type: 'object',
properties: {
title: {
type: 'string',
title: '菜单项名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
'x-component-props.icon': {
type: 'string',
title: '图标',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
},
},
},
},
'Menu.SubMenu': {
icon: <GroupOutlined />,
title: '新建菜单分组',
schema: {
type: 'object',
properties: {
title: {
type: 'string',
title: '分组名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
'x-component-props.icon': {
type: 'string',
title: '图标',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
},
},
},
},
'Menu.URL': {
icon: <LinkOutlined />,
title: '添加自定义链接',
schema: {
type: 'object',
properties: {
title: {
type: 'string',
title: '链接名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
'x-component-props.icon': {
type: 'string',
title: '图标',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
},
'x-component-props.href': {
type: 'string',
title: '自定义链接',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
};
return ( return (
<AntdMenu.ItemGroup <AntdMenu.ItemGroup
className={'nb-menu-add-new'} className={'nb-menu-add-new'}
title={ title={
<Dropdown <Dropdown
overlay={ overlay={
<AntdMenu> <AntdMenu
<AntdMenu.Item onClick={async (info) => {
onClick={async () => { console.log({ info });
const data = appendChild({ const values = await FormDialog(schemas[info.key].title, () => {
...defaultSchemas['Menu.Link'], return (
title: uid(), <FormLayout layout={'vertical'}>
}); <SchemaField schema={schemas[info.key].schema} />
await createSchema(data); </FormLayout>
}} );
> }).open();
const defaults = cloneDeep(defaultSchemas[info.key]);
const data = appendChild(deepmerge(defaults, values));
await createSchema(data);
}}
>
<AntdMenu.Item key={'Menu.Link'} icon={<MenuOutlined />}>
</AntdMenu.Item> </AntdMenu.Item>
<AntdMenu.Item <AntdMenu.Item key={'Menu.SubMenu'} icon={<GroupOutlined />}>
onClick={async () => {
const data = appendChild({ </AntdMenu.Item>
...defaultSchemas['Menu.SubMenu'], <AntdMenu.Item key={'Menu.URL'} icon={<LinkOutlined />}>
title: uid(),
});
await createSchema(data);
}}
>
</AntdMenu.Item> </AntdMenu.Item>
</AntdMenu> </AntdMenu>
} }
> >
<a>{props.children}</a> {props.children}
</Dropdown> </Dropdown>
} }
/> />
@ -280,9 +383,131 @@ Menu.AddNew = observer((props: any) => {
}); });
Menu.DesignableBar = (props) => { Menu.DesignableBar = (props) => {
const schemas = {
'Menu.Action': {
icon: <MenuOutlined />,
title: '修改菜单项',
schema: {
type: 'object',
properties: {
title: {
type: 'string',
title: '菜单项名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
'x-component-props.icon': {
type: 'string',
title: '图标',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
},
},
},
},
'Menu.Item': {
icon: <MenuOutlined />,
title: '修改菜单项',
schema: {
type: 'object',
properties: {
title: {
type: 'string',
title: '菜单项名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
'x-component-props.icon': {
type: 'string',
title: '图标',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
},
},
},
},
'Menu.Link': {
icon: <MenuOutlined />,
title: '修改菜单项',
schema: {
type: 'object',
properties: {
title: {
type: 'string',
title: '菜单项名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
'x-component-props.icon': {
type: 'string',
title: '图标',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
},
},
},
},
'Menu.SubMenu': {
icon: <GroupOutlined />,
title: '修改菜单分组',
schema: {
type: 'object',
properties: {
title: {
type: 'string',
title: '分组名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
'x-component-props.icon': {
type: 'string',
title: '图标',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
},
},
},
},
'Menu.URL': {
icon: <LinkOutlined />,
title: '修改自定义链接',
schema: {
type: 'object',
properties: {
title: {
type: 'string',
title: '链接名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
'x-component-props.icon': {
type: 'string',
title: '图标',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
},
'x-component-props.href': {
type: 'string',
title: '自定义链接',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
};
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, appendChild } = useDesignable();
const formConfig = schemas[schema['x-component']];
console.log({ formConfig, schema })
return ( return (
<div className={cls('designable-bar', { active: visible })}> <div className={cls('designable-bar', { active: visible })}>
<div <div
@ -304,17 +529,39 @@ Menu.DesignableBar = (props) => {
overlay={ overlay={
<AntdMenu> <AntdMenu>
<AntdMenu.Item <AntdMenu.Item
onClick={() => { onClick={async () => {
const title = uid(); const initialValues = {};
field.componentProps['icon'] = 'DeleteOutlined'; Object.keys(formConfig.schema.properties).forEach((name) => {
_.set(initialValues, name, get(schema, name));
});
const values = await FormDialog(formConfig.title, () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField schema={formConfig.schema} />
</FormLayout>
);
}).open({
initialValues,
});
if (values.title) {
schema.title = values.title;
}
const icon = _.get(values, 'x-component-props.icon') || null;
schema['x-component-props'] = schema['x-component-props'] =
schema['x-component-props'] || {}; schema['x-component-props'] || {};
schema['x-component-props']['icon'] = 'DeleteOutlined'; schema['x-component-props']['icon'] = icon;
schema.title = title; field.componentProps['icon'] = icon;
refresh(); refresh();
await updateSchema(schema);
// const title = uid();
// field.componentProps['icon'] = 'DeleteOutlined';
// schema['x-component-props']['icon'] = 'DeleteOutlined';
// schema.title = title;
// refresh();
}} }}
> >
{formConfig.title}
</AntdMenu.Item> </AntdMenu.Item>
<AntdMenu.Item <AntdMenu.Item
onClick={async () => { onClick={async () => {

View File

@ -1,3 +1,8 @@
.ant-layout-header {
height: 46px;
line-height: 46px;
padding: 0;
}
.ant-menu-horizontal { .ant-menu-horizontal {
width: 100%; width: 100%;
@ -88,3 +93,21 @@
} }
} }
} }
.nb-add-new-menu-item.menu-mode-horizontal {
height: 46px;
border-radius: 0px;
background: none;
border: 0;
&:hover {
background: #1890ff;
}
&:active {
background: #1890ff;
}
}
.nb-add-new-menu-item.menu-mode-inline {
margin: 0 14px;
width: calc(100% - 28px);
}

View File

@ -3,6 +3,19 @@ import { actions, middlewares } from '@nocobase/actions';
import { sort } from '@nocobase/actions/src/actions/common'; import { sort } from '@nocobase/actions/src/actions/common';
import { cloneDeep, omit } from 'lodash'; import { cloneDeep, omit } from 'lodash';
export const findAll = async (ctx: actions.Context, next: actions.Next) => {
const Collection = ctx.db.getModel('collections');
const collections = await Collection.findAll(Collection.parseApiJson({
sort: '-created_at',
}));
const data = [];
for (const collection of collections) {
data.push(await collection.toProps());
}
ctx.body = data;
await next();
}
export const createOrUpdate = async (ctx: actions.Context, next: actions.Next) => { export const createOrUpdate = async (ctx: actions.Context, next: actions.Next) => {
const { values } = ctx.action.params; const { values } = ctx.action.params;
const Collection = ctx.db.getModel('collections'); const Collection = ctx.db.getModel('collections');
@ -16,6 +29,7 @@ export const createOrUpdate = async (ctx: actions.Context, next: actions.Next) =
} else { } else {
await collection.update(values); await collection.update(values);
} }
await collection.updateAssociations(values);
} catch (error) { } catch (error) {
console.log('error.errors', error.errors) console.log('error.errors', error.errors)
} }

View File

@ -5,6 +5,10 @@ export default {
title: '数据表配置', title: '数据表配置',
model: 'Collection', model: 'Collection',
fields: [ fields: [
{
type: 'sort',
name: 'sort',
},
{ {
type: 'uid', type: 'uid',
name: 'name', name: 'name',

View File

@ -6,6 +6,11 @@ export default {
title: '字段配置', title: '字段配置',
model: 'Field', model: 'Field',
fields: [ fields: [
{
type: 'sort',
name: 'sort',
scope: ['parentKey'],
},
{ {
type: 'uid', type: 'uid',
name: 'key', name: 'key',

View File

@ -2,5 +2,39 @@ import _ from 'lodash';
import { Model } from '@nocobase/database'; import { Model } from '@nocobase/database';
export class Collection extends Model { export class Collection extends Model {
static async create(value?: any, options?: any): Promise<any> {
// console.log({ value });
const attributes = this.toAttributes(value);
// @ts-ignore
const model: Model = await super.create(attributes, options);
return model;
}
static toAttributes(value = {}): any {
const data = _.cloneDeep(value);
const keys = [
...Object.keys(this.rawAttributes),
...Object.keys(this.associations),
];
const attrs = _.pick(data, keys);
const options = _.omit(data, keys);
return { ...attrs, options };
}
async toProps() {
const json = this.toJSON();
const data: any = _.omit(json, ['options', 'created_at', 'updated_at']);
const options = json['options'] || {};
const fields = await this.getNestedFields();
return { ...data, ...options, fields }
}
async getNestedFields() {
const fields = await this.getFields();
const items = [];
for (const field of fields) {
items.push(await field.toProps());
}
return items;
}
} }

View File

@ -2,5 +2,48 @@ import _ from 'lodash';
import { Model } from '@nocobase/database'; import { Model } from '@nocobase/database';
export class Field extends Model { export class Field extends Model {
static async create(value?: any, options?: any): Promise<any> {
// console.log({ value });
const attributes = this.toAttributes(value);
// @ts-ignore
const model: Model = await super.create(attributes, options);
return model;
}
static toAttributes(value = {}): any {
const data = _.cloneDeep(value);
const keys = [
...Object.keys(this.rawAttributes),
...Object.keys(this.associations),
];
const attrs = _.pick(data, keys);
const options = _.omit(data, keys);
return { ...attrs, options };
}
async toProps() {
const json = this.toJSON();
const data: any = _.omit(json, ['options', 'created_at', 'updated_at']);
const options = json['options'] || {};
const fields = await this.getNestedFields();
const props = { ...data, ...options };
if (fields.length) {
props['children'] = fields;
}
const uiSchema = await this.getUiSchema();
if (uiSchema) {
// props['uiSchema1'] = uiSchema;
props['uiSchema'] = await uiSchema.toJSONSchema();
}
return props;
}
async getNestedFields() {
const fields = await this.getChildren();
const items = [];
for (const field of fields) {
items.push(await field.toProps());
}
return items;
}
} }

View File

@ -2,21 +2,19 @@ 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 { createOrUpdate } from './actions'; import { createOrUpdate, findAll } from './actions';
export default async function (this: Application, options = {}) { export default async function (this: Application, options = {}) {
const database = this.database; const database = this.database;
registerModels(models); registerModels(models);
database.import({ database.import({
directory: path.resolve(__dirname, 'collections'), directory: path.resolve(__dirname, 'collections'),
}); });
database.getModel('fields').beforeCreate((model) => { database.getModel('fields').beforeCreate((model) => {
if (!model.get('name')) { if (!model.get('name')) {
model.set('name', model.get('key')); model.set('name', model.get('key'));
} }
}); });
this.resourcer.registerActionHandler('collections:findAll', findAll);
this.resourcer.registerActionHandler('collections:createOrUpdate', createOrUpdate); this.resourcer.registerActionHandler('collections:createOrUpdate', createOrUpdate);
} }

View File

@ -11,7 +11,9 @@ const flatToNested = new FlatToNested({
export default async (ctx: actions.Context, next: actions.Next) => { export default async (ctx: actions.Context, next: actions.Next) => {
const { resourceKey } = ctx.action.params; const { resourceKey } = ctx.action.params;
const Route = ctx.db.getModel('routes'); const Route = ctx.db.getModel('routes');
const routes = await Route.findAll(); const routes = await Route.findAll(Route.parseApiJson({
sort: 'sort',
}));
const data = flatToNested.convert(routes.map(route => route.toProps())); const data = flatToNested.convert(routes.map(route => route.toProps()));
ctx.body = data.routes; ctx.body = data.routes;
await next(); await next();

View File

@ -5,6 +5,11 @@ export default {
title: '路由表', title: '路由表',
model: 'Route', model: 'Route',
fields: [ fields: [
{
type: 'sort',
name: 'sort',
scope: ['parentKey'],
},
{ {
type: 'uid', type: 'uid',
name: 'key', name: 'key',

View File

@ -21,6 +21,14 @@ export class UISchema extends Model {
return model; return model;
} }
async update(key?: any, value?: any, options?: any): Promise<any> {
if (typeof key === 'object') {
const attributes = UISchema.toAttributes(key);
return super.update(attributes, value, options);
}
return super.update(key, value, options);
}
static toAttributes(value = {}): any { static toAttributes(value = {}): any {
const data = _.cloneDeep(value); const data = _.cloneDeep(value);
const keys = [ const keys = [
@ -53,6 +61,15 @@ export class UISchema extends Model {
return { ...data, ...options }; return { ...data, ...options };
} }
async toJSONSchema() {
const schema = this.toProperty();
const properties = await this.getProperties();
if (Object.keys(properties).length) {
schema['properties'] = properties;
}
return schema;
}
async getProperties() { async getProperties() {
const properties = {}; const properties = {};
const children: UISchema[] = await this.getChildren({ const children: UISchema[] = await this.getChildren({