lots of updates
This commit is contained in:
parent
63538cf240
commit
2606f28059
@ -7,16 +7,7 @@ export default () => {
|
||||
name: 'collections',
|
||||
'x-component': 'DatabaseCollection',
|
||||
'x-component-props': {},
|
||||
default: [
|
||||
{
|
||||
name: 'test1',
|
||||
title: '数据表 1',
|
||||
},
|
||||
{
|
||||
name: 'test2',
|
||||
title: '数据表 2',
|
||||
},
|
||||
],
|
||||
default: [],
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
|
@ -99,7 +99,7 @@ export const SchemaField = createSchemaField({
|
||||
},
|
||||
});
|
||||
|
||||
export const DesignableContext = createContext<DesignableContextProps>(null);
|
||||
export const DesignableContext = createContext<DesignableContextProps>({});
|
||||
|
||||
export function useSchema(path?: any) {
|
||||
const { schema, refresh } = useContext(DesignableContext);
|
||||
@ -124,10 +124,14 @@ export function findPropertyByPath(schema: Schema, path?: any): Schema {
|
||||
let property = schema;
|
||||
while (arr.length) {
|
||||
const name = arr.shift();
|
||||
if (!property.properties) {
|
||||
console.warn('property does not exist.');
|
||||
return null;
|
||||
}
|
||||
property = property.properties[name];
|
||||
if (!property) {
|
||||
console.error('property does not exist.');
|
||||
break;
|
||||
console.warn('property does not exist.');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return property;
|
||||
@ -168,7 +172,7 @@ function setKeys(schema: ISchema, parentKey = null) {
|
||||
}
|
||||
|
||||
export function useDesignable(path?: any) {
|
||||
const { schema, refresh } = useContext(DesignableContext);
|
||||
const { schema = new Schema({}), refresh } = useContext(DesignableContext);
|
||||
const schemaPath = path || useSchemaPath();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const currentSchema =
|
||||
|
@ -185,18 +185,20 @@ Action.Drawer = observer((props: any) => {
|
||||
});
|
||||
|
||||
Action.Dropdown = observer((props: any) => {
|
||||
const { buttonProps = {}, ...others } = props;
|
||||
const { schema } = useDesignable();
|
||||
const componentProps = schema.parent['x-component-props'] || {};
|
||||
return (
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
{...others}
|
||||
overlay={
|
||||
<Menu>
|
||||
<RecursionField schema={schema} onlyRenderProperties />
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button {...componentProps}>{schema.title || schema.parent.title}</Button>
|
||||
<Button {...buttonProps} {...componentProps}>{schema.title || schema.parent.title}</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
});
|
||||
|
@ -94,11 +94,11 @@ const Block = (props) => {
|
||||
<div
|
||||
onMouseEnter={(e) => {
|
||||
setActive(true);
|
||||
console.log('e.onMouseEnter', new Date().toString());
|
||||
// console.log('e.onMouseEnter', new Date().toString());
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
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 })}
|
||||
>
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
Schema,
|
||||
useFieldSchema,
|
||||
useForm,
|
||||
FormConsumer,
|
||||
} from '@formily/react';
|
||||
import { ArrayCollapse, FormLayout } from '@formily/antd';
|
||||
import { uid } from '@formily/shared';
|
||||
@ -21,6 +22,9 @@ import {
|
||||
Select,
|
||||
Divider,
|
||||
Input,
|
||||
Badge,
|
||||
message,
|
||||
Spin,
|
||||
} from 'antd';
|
||||
import { options, interfaces } from './interfaces';
|
||||
import {
|
||||
@ -32,7 +36,7 @@ import {
|
||||
import cls from 'classnames';
|
||||
import './style.less';
|
||||
import Modal from 'antd/lib/modal/Modal';
|
||||
import { get } from 'lodash';
|
||||
import { clone, cloneDeep, get } from 'lodash';
|
||||
import { useEffect } from 'react';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { createOrUpdateCollection, deleteCollection } from '..';
|
||||
@ -46,19 +50,33 @@ export const DatabaseCollection = observer((props) => {
|
||||
const form = useForm();
|
||||
const [newValue, setNewValue] = useState('');
|
||||
|
||||
useRequest('collections:list?sort=-created_at', {
|
||||
const { run, loading } = useRequest('collections:findAll', {
|
||||
formatResult: (result) => result?.data,
|
||||
onSuccess(data) {
|
||||
field.setValue(data);
|
||||
console.log('onSuccess', data);
|
||||
},
|
||||
manual: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
className={'nb-database-config'}
|
||||
style={{
|
||||
height: 46,
|
||||
borderRadius: 0,
|
||||
}}
|
||||
onClick={async () => {
|
||||
setVisible(true);
|
||||
await run();
|
||||
if (field.value?.length === 0) {
|
||||
field.push({
|
||||
name: `t_${uid()}`,
|
||||
unsaved: true,
|
||||
fields: [],
|
||||
});
|
||||
}
|
||||
}}
|
||||
type={'primary'}
|
||||
>
|
||||
@ -68,6 +86,7 @@ export const DatabaseCollection = observer((props) => {
|
||||
title={
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Select
|
||||
loading={loading}
|
||||
value={activeIndex}
|
||||
style={{ minWidth: 300, textAlign: 'center' }}
|
||||
onChange={(value) => {
|
||||
@ -97,8 +116,9 @@ export const DatabaseCollection = observer((props) => {
|
||||
}}
|
||||
onSearch={async (value) => {
|
||||
const data = {
|
||||
name: uid(),
|
||||
name: `t_${uid()}`,
|
||||
title: value,
|
||||
fields: [],
|
||||
};
|
||||
field.push(data);
|
||||
setActiveIndex(field.value.length - 1);
|
||||
@ -114,7 +134,12 @@ export const DatabaseCollection = observer((props) => {
|
||||
>
|
||||
{field.value?.map((item, index) => {
|
||||
return (
|
||||
<Select.Option value={index} label={item.title || '未命名'}>
|
||||
<Select.Option
|
||||
value={index}
|
||||
label={`${item.title || '未命名'}${
|
||||
item.unsaved ? ' (未保存)' : ''
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@ -122,17 +147,26 @@ export const DatabaseCollection = observer((props) => {
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{item.title || '未命名'}
|
||||
{item.title || '未命名'} {item.unsaved ? '(未保存)' : ''}
|
||||
<DeleteOutlined
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
field.remove(index);
|
||||
if (field.value?.length === 0) {
|
||||
field.push({
|
||||
name: `t_${uid()}`,
|
||||
unsaved: true,
|
||||
fields: [],
|
||||
});
|
||||
}
|
||||
if (activeIndex === index) {
|
||||
setActiveIndex(0);
|
||||
} else if (activeIndex > index) {
|
||||
setActiveIndex(activeIndex - 1);
|
||||
}
|
||||
await deleteCollection(item.name);
|
||||
if (item.name) {
|
||||
await deleteCollection(item.name);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -146,30 +180,38 @@ export const DatabaseCollection = observer((props) => {
|
||||
onCancel={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
okText={'保存'}
|
||||
cancelText={'关闭'}
|
||||
onOk={async () => {
|
||||
try {
|
||||
form.clearErrors();
|
||||
await form.validate(`${field.address.entire}.${activeIndex}.*`);
|
||||
setVisible(false);
|
||||
delete field.value[activeIndex]['unsaved'];
|
||||
await createOrUpdateCollection(field.value[activeIndex]);
|
||||
console.log(
|
||||
`${field.address.entire}.${activeIndex}.*`,
|
||||
field.value[activeIndex],
|
||||
);
|
||||
message.success('保存成功');
|
||||
} catch (error) {}
|
||||
}}
|
||||
>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<RecursionField
|
||||
name={activeIndex}
|
||||
schema={
|
||||
new Schema({
|
||||
type: 'object',
|
||||
properties: schema.properties,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</FormLayout>
|
||||
{loading ? (
|
||||
<Spin />
|
||||
) : (
|
||||
<FormLayout layout={'vertical'}>
|
||||
<RecursionField
|
||||
name={activeIndex}
|
||||
schema={
|
||||
new Schema({
|
||||
type: 'object',
|
||||
properties: schema.properties,
|
||||
})
|
||||
}
|
||||
/>
|
||||
{/* <FormConsumer>
|
||||
{form => (
|
||||
<pre>{JSON.stringify(form.values, null, 2)}</pre>
|
||||
)}
|
||||
</FormConsumer> */}
|
||||
</FormLayout>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
@ -177,8 +219,13 @@ export const DatabaseCollection = observer((props) => {
|
||||
|
||||
export const DatabaseField: any = observer((props) => {
|
||||
const field = useField<Formily.Core.Models.ArrayField>();
|
||||
console.log('DatabaseField', field.value);
|
||||
useEffect(()=> {
|
||||
if (!field.value) {
|
||||
field.setValue([]);
|
||||
}
|
||||
}, [])
|
||||
const [activeKey, setActiveKey] = useState(null);
|
||||
console.log('DatabaseField', field);
|
||||
return (
|
||||
<div>
|
||||
<Collapse
|
||||
@ -191,12 +238,16 @@ export const DatabaseField: any = observer((props) => {
|
||||
>
|
||||
{field.value?.map((item, index) => {
|
||||
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 (
|
||||
<Collapse.Panel
|
||||
header={
|
||||
<>
|
||||
{(item.ui && item.ui.title) || (
|
||||
{(item.uiSchema && item.uiSchema.title) || (
|
||||
<i style={{ color: 'rgba(0, 0, 0, 0.25)' }}>未命名</i>
|
||||
)}{' '}
|
||||
<Tag>{schema.title}</Tag>
|
||||
@ -206,16 +257,18 @@ export const DatabaseField: any = observer((props) => {
|
||||
</>
|
||||
}
|
||||
extra={[
|
||||
<Badge count={errors.length} />,
|
||||
<DeleteOutlined
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
field.remove(index);
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
key={item.id}
|
||||
key={item.key}
|
||||
>
|
||||
<RecursionField
|
||||
key={`${item.id}_${index}`}
|
||||
key={`${item.key}_${index}`}
|
||||
name={index}
|
||||
schema={
|
||||
new Schema({
|
||||
@ -252,14 +305,14 @@ export const DatabaseField: any = observer((props) => {
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
...schema.default,
|
||||
id: uid(),
|
||||
name: uid(),
|
||||
...cloneDeep(schema.default),
|
||||
key: uid(),
|
||||
name: `f_${uid()}`,
|
||||
interface: info.key,
|
||||
};
|
||||
field.push(data);
|
||||
setActiveKey(data.id);
|
||||
console.log('info.key', info.key, schema);
|
||||
setActiveKey(data.key);
|
||||
console.log('info.key', field.value);
|
||||
}}
|
||||
>
|
||||
{options.map((option) => (
|
||||
|
@ -8,7 +8,7 @@ export const select: ISchema = {
|
||||
default: {
|
||||
dataType: 'string',
|
||||
// name,
|
||||
ui: {
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
// title,
|
||||
'x-component': 'Select',
|
||||
@ -16,7 +16,7 @@ export const select: ISchema = {
|
||||
} as ISchema,
|
||||
},
|
||||
properties: {
|
||||
'ui.title': {
|
||||
'uiSchema.title': {
|
||||
type: 'string',
|
||||
required: true,
|
||||
title: '字段名称',
|
||||
@ -42,7 +42,7 @@ export const select: ISchema = {
|
||||
{ label: 'Text', value: 'text' },
|
||||
],
|
||||
},
|
||||
'ui.enum': {
|
||||
'uiSchema.enum': {
|
||||
type: 'array',
|
||||
title: '可选项',
|
||||
'x-decorator': 'FormItem',
|
||||
@ -132,7 +132,7 @@ export const select: ISchema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
'ui.required': {
|
||||
'uiSchema.required': {
|
||||
type: 'string',
|
||||
title: '必填',
|
||||
'x-decorator': 'FormItem',
|
||||
|
@ -8,14 +8,14 @@ export const string: ISchema = {
|
||||
default: {
|
||||
dataType: 'string',
|
||||
// name,
|
||||
ui: {
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
// title,
|
||||
'x-component': 'Input',
|
||||
} as ISchema,
|
||||
},
|
||||
properties: {
|
||||
'ui.title': {
|
||||
'uiSchema.title': {
|
||||
type: 'string',
|
||||
title: '字段名称',
|
||||
required: true,
|
||||
@ -41,7 +41,7 @@ export const string: ISchema = {
|
||||
{ label: 'Text', value: 'text' },
|
||||
],
|
||||
},
|
||||
'ui.required': {
|
||||
'uiSchema.required': {
|
||||
type: 'string',
|
||||
title: '必填',
|
||||
'x-decorator': 'FormItem',
|
||||
|
@ -7,7 +7,7 @@ export const subTable: ISchema = {
|
||||
group: 'relation',
|
||||
default: {
|
||||
// name,
|
||||
ui: {
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
// title,
|
||||
'x-component': 'Select',
|
||||
@ -15,7 +15,7 @@ export const subTable: ISchema = {
|
||||
} as ISchema,
|
||||
},
|
||||
properties: {
|
||||
'ui.title': {
|
||||
'uiSchema.title': {
|
||||
type: 'string',
|
||||
required: true,
|
||||
title: '字段名称',
|
||||
|
@ -8,14 +8,14 @@ export const textarea: ISchema = {
|
||||
default: {
|
||||
dataType: 'text',
|
||||
// name,
|
||||
ui: {
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
// title,
|
||||
'x-component': 'Input.TextArea',
|
||||
} as ISchema,
|
||||
},
|
||||
properties: {
|
||||
'ui.title': {
|
||||
'uiSchema.title': {
|
||||
type: 'string',
|
||||
required: true,
|
||||
title: '字段名称',
|
||||
@ -41,7 +41,7 @@ export const textarea: ISchema = {
|
||||
{ label: 'Text', value: 'text' },
|
||||
],
|
||||
},
|
||||
'ui.required': {
|
||||
'uiSchema.required': {
|
||||
type: 'string',
|
||||
title: '必填',
|
||||
'x-decorator': 'FormItem',
|
||||
|
@ -1,3 +1,16 @@
|
||||
.ant-collapse.empty {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.nb-database-config {
|
||||
height: 46px;
|
||||
border-radius: 0px;
|
||||
background: none;
|
||||
border: 0;
|
||||
&:hover {
|
||||
background: #1890ff;
|
||||
}
|
||||
&:active {
|
||||
background: #1890ff;
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
if (!schema['key']) {
|
||||
return;
|
||||
|
@ -32,17 +32,34 @@ import {
|
||||
} from 'antd';
|
||||
import { uid } from '@formily/shared';
|
||||
import cls from 'classnames';
|
||||
import { useDesignable } from '../../components/schema-renderer';
|
||||
import { MenuOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
SchemaField,
|
||||
SchemaRenderer,
|
||||
useDesignable,
|
||||
} from '../../components/schema-renderer';
|
||||
import {
|
||||
MenuOutlined,
|
||||
PlusOutlined,
|
||||
GroupOutlined,
|
||||
LinkOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { IconPicker } from '../../components/icon-picker';
|
||||
import { createSchema, removeSchema, useDefaultAction, VisibleContext } from '..';
|
||||
import {
|
||||
createSchema,
|
||||
removeSchema,
|
||||
updateSchema,
|
||||
useDefaultAction,
|
||||
VisibleContext,
|
||||
} from '..';
|
||||
import { useMount } from 'ahooks';
|
||||
import './style.less';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { findPropertyByPath, useSchemaPath } from '@nocobase/client/lib';
|
||||
import { request } from '../';
|
||||
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);
|
||||
|
||||
@ -62,8 +79,8 @@ const SideMenu = (props: any) => {
|
||||
<AntdMenu mode={'inline'} onSelect={onSelect}>
|
||||
<RecursionField schema={child} onlyRenderProperties />
|
||||
<Menu.AddNew key={uid()} path={[...path, selectedKey]}>
|
||||
<Button block type={'dashed'}>
|
||||
<PlusOutlined className={'nb-add-new-icon'} />
|
||||
<Button className={'nb-add-new-menu-item'} block type={'dashed'}>
|
||||
<PlusOutlined />
|
||||
</Button>
|
||||
</Menu.AddNew>
|
||||
</AntdMenu>
|
||||
@ -109,13 +126,20 @@ export const Menu: any = observer((props: any) => {
|
||||
setSelectedKey(info.key);
|
||||
}
|
||||
const selectedSchema = schema.properties[info.key];
|
||||
console.log({ selectedSchema })
|
||||
console.log({ selectedSchema });
|
||||
onSelect && onSelect({ ...info, schema: selectedSchema });
|
||||
}}
|
||||
>
|
||||
<RecursionField schema={schema} onlyRenderProperties />
|
||||
<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>
|
||||
</AntdMenu>
|
||||
{mode === 'mix' && (
|
||||
@ -126,7 +150,8 @@ export const Menu: any = observer((props: any) => {
|
||||
const keyPath = [selectedKey, ...info.keyPath];
|
||||
const selectedSchema = findPropertyByPath(schema, keyPath);
|
||||
console.log('keyPath', keyPath, selectedSchema);
|
||||
onSelect && onSelect({ ...info, keyPath, schema: selectedSchema });
|
||||
onSelect &&
|
||||
onSelect({ ...info, keyPath, schema: selectedSchema });
|
||||
}}
|
||||
selectedKey={selectedKey}
|
||||
sideMenuRef={sideMenuRef}
|
||||
@ -240,39 +265,117 @@ Menu.SubMenu = observer((props: any) => {
|
||||
|
||||
Menu.AddNew = observer((props: any) => {
|
||||
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 (
|
||||
<AntdMenu.ItemGroup
|
||||
className={'nb-menu-add-new'}
|
||||
title={
|
||||
<Dropdown
|
||||
overlay={
|
||||
<AntdMenu>
|
||||
<AntdMenu.Item
|
||||
onClick={async () => {
|
||||
const data = appendChild({
|
||||
...defaultSchemas['Menu.Link'],
|
||||
title: uid(),
|
||||
});
|
||||
await createSchema(data);
|
||||
}}
|
||||
>
|
||||
新建菜单
|
||||
<AntdMenu
|
||||
onClick={async (info) => {
|
||||
console.log({ info });
|
||||
const values = await FormDialog(schemas[info.key].title, () => {
|
||||
return (
|
||||
<FormLayout layout={'vertical'}>
|
||||
<SchemaField schema={schemas[info.key].schema} />
|
||||
</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
|
||||
onClick={async () => {
|
||||
const data = appendChild({
|
||||
...defaultSchemas['Menu.SubMenu'],
|
||||
title: uid(),
|
||||
});
|
||||
await createSchema(data);
|
||||
}}
|
||||
>
|
||||
新建菜单组
|
||||
<AntdMenu.Item key={'Menu.SubMenu'} icon={<GroupOutlined />}>
|
||||
新建菜单分组
|
||||
</AntdMenu.Item>
|
||||
<AntdMenu.Item key={'Menu.URL'} icon={<LinkOutlined />}>
|
||||
添加自定义链接
|
||||
</AntdMenu.Item>
|
||||
</AntdMenu>
|
||||
}
|
||||
>
|
||||
<a>{props.children}</a>
|
||||
{props.children}
|
||||
</Dropdown>
|
||||
}
|
||||
/>
|
||||
@ -280,9 +383,131 @@ Menu.AddNew = observer((props: any) => {
|
||||
});
|
||||
|
||||
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 [visible, setVisible] = useState(false);
|
||||
const { schema, remove, refresh, insertAfter, appendChild } = useDesignable();
|
||||
const formConfig = schemas[schema['x-component']];
|
||||
console.log({ formConfig, schema })
|
||||
return (
|
||||
<div className={cls('designable-bar', { active: visible })}>
|
||||
<div
|
||||
@ -304,17 +529,39 @@ Menu.DesignableBar = (props) => {
|
||||
overlay={
|
||||
<AntdMenu>
|
||||
<AntdMenu.Item
|
||||
onClick={() => {
|
||||
const title = uid();
|
||||
field.componentProps['icon'] = 'DeleteOutlined';
|
||||
onClick={async () => {
|
||||
const initialValues = {};
|
||||
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']['icon'] = 'DeleteOutlined';
|
||||
schema.title = title;
|
||||
schema['x-component-props']['icon'] = icon;
|
||||
field.componentProps['icon'] = icon;
|
||||
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
|
||||
onClick={async () => {
|
||||
|
@ -1,3 +1,8 @@
|
||||
.ant-layout-header {
|
||||
height: 46px;
|
||||
line-height: 46px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ant-menu-horizontal {
|
||||
width: 100%;
|
||||
@ -87,4 +92,22 @@
|
||||
width: 46px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
@ -3,6 +3,19 @@ import { actions, middlewares } from '@nocobase/actions';
|
||||
import { sort } from '@nocobase/actions/src/actions/common';
|
||||
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) => {
|
||||
const { values } = ctx.action.params;
|
||||
const Collection = ctx.db.getModel('collections');
|
||||
@ -16,6 +29,7 @@ export const createOrUpdate = async (ctx: actions.Context, next: actions.Next) =
|
||||
} else {
|
||||
await collection.update(values);
|
||||
}
|
||||
await collection.updateAssociations(values);
|
||||
} catch (error) {
|
||||
console.log('error.errors', error.errors)
|
||||
}
|
||||
|
@ -5,6 +5,10 @@ export default {
|
||||
title: '数据表配置',
|
||||
model: 'Collection',
|
||||
fields: [
|
||||
{
|
||||
type: 'sort',
|
||||
name: 'sort',
|
||||
},
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'name',
|
||||
|
@ -6,6 +6,11 @@ export default {
|
||||
title: '字段配置',
|
||||
model: 'Field',
|
||||
fields: [
|
||||
{
|
||||
type: 'sort',
|
||||
name: 'sort',
|
||||
scope: ['parentKey'],
|
||||
},
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'key',
|
||||
|
@ -2,5 +2,39 @@ import _ from 'lodash';
|
||||
import { Model } from '@nocobase/database';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -2,5 +2,48 @@ import _ from 'lodash';
|
||||
import { Model } from '@nocobase/database';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -2,21 +2,19 @@ import path from 'path';
|
||||
import { Application } from '@nocobase/server';
|
||||
import { registerModels, Table } from '@nocobase/database';
|
||||
import * as models from './models';
|
||||
import { createOrUpdate } from './actions';
|
||||
import { createOrUpdate, findAll } from './actions';
|
||||
|
||||
export default async function (this: Application, options = {}) {
|
||||
const database = this.database;
|
||||
registerModels(models);
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
database.getModel('fields').beforeCreate((model) => {
|
||||
if (!model.get('name')) {
|
||||
model.set('name', model.get('key'));
|
||||
}
|
||||
});
|
||||
|
||||
this.resourcer.registerActionHandler('collections:findAll', findAll);
|
||||
this.resourcer.registerActionHandler('collections:createOrUpdate', createOrUpdate);
|
||||
}
|
||||
|
@ -11,7 +11,9 @@ const flatToNested = new FlatToNested({
|
||||
export default async (ctx: actions.Context, next: actions.Next) => {
|
||||
const { resourceKey } = ctx.action.params;
|
||||
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()));
|
||||
ctx.body = data.routes;
|
||||
await next();
|
||||
|
@ -5,6 +5,11 @@ export default {
|
||||
title: '路由表',
|
||||
model: 'Route',
|
||||
fields: [
|
||||
{
|
||||
type: 'sort',
|
||||
name: 'sort',
|
||||
scope: ['parentKey'],
|
||||
},
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'key',
|
||||
|
@ -21,6 +21,14 @@ export class UISchema extends 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 {
|
||||
const data = _.cloneDeep(value);
|
||||
const keys = [
|
||||
@ -53,6 +61,15 @@ export class UISchema extends Model {
|
||||
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() {
|
||||
const properties = {};
|
||||
const children: UISchema[] = await this.getChildren({
|
||||
|
Loading…
Reference in New Issue
Block a user