updates...

This commit is contained in:
chenos 2021-07-18 17:16:57 +08:00
parent 057dc8406a
commit 63538cf240
15 changed files with 364 additions and 261 deletions

View File

@ -1,68 +0,0 @@
import { DatabaseOutlined } from '@ant-design/icons';
import { SchemaRenderer } from '../../../';
import Modal from 'antd/lib/modal/Modal';
import React from 'react';
import { useState } from 'react';
import schema from './schema';
import { Button, Dropdown, Menu } from 'antd';
import { useMemo } from 'react';
import { createForm } from '@formily/core';
export default () => {
const form = useMemo(
() =>
createForm({
initialValues: {
title: '数据表名称',
},
}),
[],
);
const [visible, setVisible] = useState(false);
return (
<>
<Button
onClick={() => {
setVisible(true);
}}
type={'primary'}
>
<DatabaseOutlined />
</Button>
<Modal
title={
<>
<Dropdown
overlay={
<Menu
onClick={(info) => {
form.setValues({
title: `数据表${info.key}`,
});
}}
>
<Menu.Item key={1}>1</Menu.Item>
<Menu.Item key={2}>2</Menu.Item>
<Menu.Divider></Menu.Divider>
<Menu.Item key={5}></Menu.Item>
</Menu>
}
>
<span style={{cursor: 'pointer'}}>3</span>
</Dropdown>
</>
}
visible={visible}
onOk={async () => {
await form.submit();
setVisible(false);
}}
onCancel={() => {
setVisible(false);
}}
>
<SchemaRenderer form={form} schema={schema} />
</Modal>
</>
);
};

View File

@ -1,49 +0,0 @@
import { ISchema } from "@formily/react";
export default {
type: 'object',
properties: {
form: {
type: 'void',
"x-component": 'FormLayout',
"x-component-props": {
layout: 'vertical',
},
properties: {
title: {
type: 'string',
title: '数据表名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
fields: {
type: 'array',
title: '字段',
'x-decorator': 'FormItem',
'x-component': 'DatabaseField',
default: [
{
id: 1,
interface: 'string',
dataType: 'string',
name: 'title',
ui: {
title: '标题',
},
},
{
id: 2,
dataType: 'text',
interface: 'textarea',
name: 'content',
ui: {
title: '内容',
},
},
],
}
}
},
},
} as ISchema;

View File

@ -1,56 +0,0 @@
export const schema = {
type: 'void',
name: 'action1',
// title: 'ModalForm',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
icon: 'DatabaseOutlined',
},
properties: {
modal1: {
type: 'void',
title: '对话框标题',
'x-decorator': 'Form',
'x-component': 'Action.Modal',
properties: {
title: {
type: 'string',
title: '数据表名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
fields: {
type: 'array',
title: '字段',
'x-decorator': 'FormItem',
'x-component': 'DatabaseField',
default: [
{
id: 1,
interface: 'string',
dataType: 'string',
name: 'title',
ui: {
title: '标题',
},
},
{
id: 2,
dataType: 'text',
interface: 'textarea',
name: 'content',
ui: {
title: '内容',
},
},
],
},
},
},
},
};
export default schema;

View File

@ -0,0 +1,45 @@
import { SchemaRenderer } from '@nocobase/client/lib';
import React from 'react';
export default () => {
const schema = {
type: 'array',
name: 'collections',
'x-component': 'DatabaseCollection',
'x-component-props': {},
default: [
{
name: 'test1',
title: '数据表 1',
},
{
name: 'test2',
title: '数据表 2',
},
],
properties: {
title: {
type: 'string',
title: '数据表名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
name: {
type: 'string',
title: '数据表标识',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-read-pretty': true,
},
fields: {
type: 'array',
title: '数据表字段',
'x-decorator': 'FormItem',
'x-component': 'DatabaseField',
default: [],
},
},
};
return <SchemaRenderer schema={schema} />;
};

View File

@ -23,7 +23,7 @@ import './style.less';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import DataTableConfig from './DataTableConfig'; import Database from './datatable';
function LayoutWithMenu({ schema }) { function LayoutWithMenu({ schema }) {
const match = useRouteMatch<any>(); const match = useRouteMatch<any>();
@ -53,7 +53,7 @@ function LayoutWithMenu({ schema }) {
selectedKeys: [activeKey].filter(Boolean), selectedKeys: [activeKey].filter(Boolean),
}} }}
/> />
<DataTableConfig /> <Database />
</Layout.Header> </Layout.Header>
<Layout> <Layout>
<Layout.Sider <Layout.Sider

View File

@ -18,20 +18,20 @@
// } // }
// } // }
.ant-collapse { // .ant-collapse {
border: 1px solid #d9d9d9 !important; // border: 1px solid #d9d9d9 !important;
border-bottom: 0 !important; // border-bottom: 0 !important;
> .ant-collapse-item { // > .ant-collapse-item {
border-bottom: 1px solid #d9d9d9 !important; // border-bottom: 1px solid #d9d9d9 !important;
} // }
.ant-collapse-content { // .ant-collapse-content {
border-top: 1px solid #d9d9d9 !important; // border-top: 1px solid #d9d9d9 !important;
} // }
.ant-collapse-content > .ant-collapse-content-box { // .ant-collapse-content > .ant-collapse-content-box {
padding: 24px !important; // padding: 24px !important;
padding-bottom: 0 !important; // padding-bottom: 0 !important;
} // }
} // }
.database-sider { .database-sider {
background: #fafafa; background: #fafafa;

View File

@ -27,7 +27,7 @@ import { AddNew } from '../../schemas/add-new';
import { Cascader } from '../../schemas/cascader'; import { Cascader } from '../../schemas/cascader';
import { Checkbox } from '../../schemas/checkbox'; import { Checkbox } from '../../schemas/checkbox';
import { ColorSelect } from '../../schemas/color-select'; import { ColorSelect } from '../../schemas/color-select';
import { DatabaseField } from '../../schemas/database-field'; import { DatabaseField, DatabaseCollection } from '../../schemas/database-field';
import { DatePicker } from '../../schemas/date-picker'; import { DatePicker } from '../../schemas/date-picker';
import { Filter } from '../../schemas/filter'; import { Filter } from '../../schemas/filter';
import { Form } from '../../schemas/form'; import { Form } from '../../schemas/form';
@ -79,6 +79,7 @@ export const SchemaField = createSchemaField({
Checkbox, Checkbox,
ColorSelect, ColorSelect,
DatabaseField, DatabaseField,
DatabaseCollection,
DatePicker, DatePicker,
Filter, Filter,
Form, Form,

View File

@ -1,55 +1,46 @@
import React from 'react'; import React from 'react';
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { SchemaRenderer } from '../../'; import { SchemaRenderer } from '../../';
import { observer, connect, useField } from '@formily/react'; import { observer, connect, useField, ISchema } from '@formily/react';
const schema = { const schema: ISchema = {
type: 'void', type: 'array',
name: 'action1', name: 'collections',
title: 'ModalForm', 'x-component': 'DatabaseCollection',
'x-component': 'Action',
'x-component-props': {}, 'x-component-props': {},
properties: { default: [
modal1: { {
type: 'void', name: 'test1',
title: '对话框标题', title: '数据表 1',
'x-decorator': 'Form',
'x-component': 'Action.Modal',
properties: {
title: {
type: 'string',
title: '数据表名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
fields: {
type: 'array',
'x-component': 'DatabaseField',
default: [
{
id: 1,
interface: 'string',
dataType: 'string',
name: 'title',
ui: {
title: '标题',
},
},
{
id: 2,
dataType: 'text',
interface: 'textarea',
name: 'content',
ui: {
title: '内容',
},
},
],
},
},
}, },
}, {
name: 'test2',
title: '数据表 2',
}
],
properties: {
title: {
type: 'string',
title: '数据表名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
name: {
type: 'string',
title: '数据表标识',
'x-decorator': 'FormItem',
'x-component': 'Input',
"x-read-pretty": true,
},
fields: {
type: 'array',
title: '数据表字段',
'x-decorator': 'FormItem',
'x-component': 'DatabaseField',
default: [],
}
}
}; };
const form = createForm(); const form = createForm();

View File

@ -7,17 +7,177 @@ import {
ISchema, ISchema,
Schema, Schema,
useFieldSchema, useFieldSchema,
useForm,
} from '@formily/react'; } from '@formily/react';
import { ArrayCollapse } from '@formily/antd'; import { ArrayCollapse, FormLayout } from '@formily/antd';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import '@formily/antd/lib/form-tab/style'; import '@formily/antd/lib/form-tab/style';
import { Collapse, Button, Dropdown, Menu, Tag } from 'antd'; import {
Collapse,
Button,
Dropdown,
Menu,
Tag,
Select,
Divider,
Input,
} from 'antd';
import { options, interfaces } from './interfaces'; import { options, interfaces } from './interfaces';
import { DeleteOutlined } from '@ant-design/icons'; import {
DeleteOutlined,
DatabaseOutlined,
PlusOutlined,
CloseOutlined,
} from '@ant-design/icons';
import cls from 'classnames';
import './style.less';
import Modal from 'antd/lib/modal/Modal';
import { get } from 'lodash';
import { useEffect } from 'react';
import { useRequest } from 'ahooks';
import { createOrUpdateCollection, deleteCollection } from '..';
export const DatabaseCollection = observer((props) => {
const field = useField<Formily.Core.Models.ArrayField>();
const [visible, setVisible] = useState(false);
const [open, setOpen] = useState(false);
const schema = useFieldSchema();
const [activeIndex, setActiveIndex] = useState(0);
const form = useForm();
const [newValue, setNewValue] = useState('');
useRequest('collections:list?sort=-created_at', {
formatResult: (result) => result?.data,
onSuccess(data) {
field.setValue(data);
console.log('onSuccess', data);
},
});
return (
<div>
<Button
onClick={() => {
setVisible(true);
}}
type={'primary'}
>
<DatabaseOutlined />
</Button>
<Modal
title={
<div style={{ textAlign: 'center' }}>
<Select
value={activeIndex}
style={{ minWidth: 300, textAlign: 'center' }}
onChange={(value) => {
setActiveIndex(value as any);
}}
open={open}
onDropdownVisibleChange={setOpen}
optionLabelProp={'label'}
dropdownRender={(menu) => {
return (
<div>
{menu}
<Divider style={{ margin: '5px 0 4px' }} />
<div
style={{
cursor: 'pointer',
padding: '5px 12px',
}}
>
<Input.Search
size={'middle'}
placeholder={'新增数据表'}
enterButton={<PlusOutlined />}
value={newValue}
onChange={(e) => {
setNewValue(e.target.value);
}}
onSearch={async (value) => {
const data = {
name: uid(),
title: value,
};
field.push(data);
setActiveIndex(field.value.length - 1);
setOpen(false);
setNewValue('');
await createOrUpdateCollection(data);
}}
/>
</div>
</div>
);
}}
>
{field.value?.map((item, index) => {
return (
<Select.Option value={index} label={item.title || '未命名'}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
{item.title || '未命名'}
<DeleteOutlined
onClick={async (e) => {
e.stopPropagation();
field.remove(index);
if (activeIndex === index) {
setActiveIndex(0);
} else if (activeIndex > index) {
setActiveIndex(activeIndex - 1);
}
await deleteCollection(item.name);
}}
/>
</div>
</Select.Option>
);
})}
</Select>
</div>
}
visible={visible}
onCancel={() => {
setVisible(false);
}}
onOk={async () => {
try {
form.clearErrors();
await form.validate(`${field.address.entire}.${activeIndex}.*`);
setVisible(false);
await createOrUpdateCollection(field.value[activeIndex]);
console.log(
`${field.address.entire}.${activeIndex}.*`,
field.value[activeIndex],
);
} catch (error) {}
}}
>
<FormLayout layout={'vertical'}>
<RecursionField
name={activeIndex}
schema={
new Schema({
type: 'object',
properties: schema.properties,
})
}
/>
</FormLayout>
</Modal>
</div>
);
});
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.componentProps); console.log('DatabaseField', field.value);
const [activeKey, setActiveKey] = useState(null); const [activeKey, setActiveKey] = useState(null);
return ( return (
<div> <div>
@ -26,6 +186,7 @@ export const DatabaseField: any = observer((props) => {
onChange={(key) => { onChange={(key) => {
setActiveKey(key); setActiveKey(key);
}} }}
className={cls({ empty: !field.value?.length })}
accordion accordion
> >
{field.value?.map((item, index) => { {field.value?.map((item, index) => {
@ -35,9 +196,13 @@ export const DatabaseField: any = observer((props) => {
<Collapse.Panel <Collapse.Panel
header={ header={
<> <>
{(item.ui && item.ui.title) || <i style={{color: 'rgba(0, 0, 0, 0.25)'}}></i>}{' '} {(item.ui && item.ui.title) || (
<i style={{ color: 'rgba(0, 0, 0, 0.25)' }}></i>
)}{' '}
<Tag>{schema.title}</Tag> <Tag>{schema.title}</Tag>
<span style={{ color: 'rgba(0, 0, 0, 0.25)', fontSize: 14 }}>{item.name}</span> <span style={{ color: 'rgba(0, 0, 0, 0.25)', fontSize: 14 }}>
{item.name}
</span>
</> </>
} }
extra={[ extra={[
@ -97,13 +262,11 @@ export const DatabaseField: any = observer((props) => {
console.log('info.key', info.key, schema); console.log('info.key', info.key, schema);
}} }}
> >
{options.map(option => ( {options.map((option) => (
<Menu.ItemGroup title={option.label}> <Menu.ItemGroup title={option.label}>
{ {option.children.map((item) => (
option.children.map(item => ( <Menu.Item key={item.name}>{item.title}</Menu.Item>
<Menu.Item key={item.name}>{item.title}</Menu.Item> ))}
))
}
</Menu.ItemGroup> </Menu.ItemGroup>
))} ))}
</Menu> </Menu>

View File

@ -0,0 +1,3 @@
.ant-collapse.empty {
border: 0;
}

View File

@ -21,7 +21,7 @@ import {
import './style.less'; import './style.less';
import cls from 'classnames'; import cls from 'classnames';
import { createSchema, useDesignable, useSchemaPath } from '../'; import { createSchema, removeSchema, useDesignable, useSchemaPath } from '../';
import { import {
useDrag, useDrag,
useDrop, useDrop,
@ -37,6 +37,10 @@ export const GridBlockContext = createContext({
dragRef: null, dragRef: null,
}); });
function isGridRowOrCol(schema: ISchema) {
return ['Grid.Row', 'Grid.Col'].includes(schema['x-component']);
}
const RowDivider = ({ name, onDrop }) => { const RowDivider = ({ name, onDrop }) => {
const uid = useDragDropUID(); const uid = useDragDropUID();
const { isOver, dropRef } = useDrop({ const { isOver, dropRef } = useDrop({
@ -111,7 +115,11 @@ export const Grid: any = observer((props) => {
}); });
await createSchema(data); await createSchema(data);
console.log('prepend', data); console.log('prepend', data);
deepRemove(path); const removed = deepRemove(path);
const last = removed.pop();
if (isGridRowOrCol(last)) {
await removeSchema(last);
}
}} }}
/> />
{schema.mapProperties((property, key, index) => { {schema.mapProperties((property, key, index) => {
@ -143,8 +151,12 @@ export const Grid: any = observer((props) => {
[...gridPath, key], [...gridPath, key],
); );
await createSchema(data); await createSchema(data);
console.log('insertAfter', data); const removed = deepRemove(path);
deepRemove(path); const last = removed.pop();
if (isGridRowOrCol(last)) {
await removeSchema(last);
}
console.log('insertAfter', data, removed);
}} }}
/> />
</> </>
@ -185,7 +197,11 @@ Grid.Row = observer((props) => {
}); });
await createSchema(data); await createSchema(data);
const path = [...e.dragItem.path]; const path = [...e.dragItem.path];
deepRemove(path); const removed = deepRemove(path);
const last = removed.pop();
if (isGridRowOrCol(last)) {
await removeSchema(last);
}
}} }}
/> />
{schema.mapProperties((property, key, index) => { {schema.mapProperties((property, key, index) => {
@ -207,10 +223,14 @@ Grid.Row = observer((props) => {
}, },
[...rowPath, key], [...rowPath, key],
); );
console.log('ColDivider', data) console.log('ColDivider', data);
await createSchema(data); await createSchema(data);
const path = [...e.dragItem.path]; const path = [...e.dragItem.path];
deepRemove(path); const removed = deepRemove(path);
const last = removed.pop();
if (isGridRowOrCol(last)) {
await removeSchema(last);
}
}} }}
onDragEnd={(e) => { onDragEnd={(e) => {
designableSchema.mapProperties((s, key, index) => { designableSchema.mapProperties((s, key, index) => {

View File

@ -18,6 +18,23 @@ export const request = extend({
timeout: 1000, timeout: 1000,
}); });
export async function createOrUpdateCollection(data: any) {
return await request('collections:createOrUpdate', {
method: 'post',
data,
});
}
export async function deleteCollection(name) {
await request('collections:destroy', {
method: 'post',
params: {
filter: {
name,
}
},
});
}
export async function createSchema(schema: ISchema) { export async function createSchema(schema: ISchema) {
if (!schema['key']) { if (!schema['key']) {

View File

@ -0,0 +1,24 @@
import { Model, ModelCtor } from '@nocobase/database';
import { actions, middlewares } from '@nocobase/actions';
import { sort } from '@nocobase/actions/src/actions/common';
import { cloneDeep, omit } from 'lodash';
export const createOrUpdate = async (ctx: actions.Context, next: actions.Next) => {
const { values } = ctx.action.params;
const Collection = ctx.db.getModel('collections');
let collection;
if (values.name) {
collection = await Collection.findByPk(values.name);
}
try {
if (!collection) {
collection = await Collection.create(values);
} else {
await collection.update(values);
}
} catch (error) {
console.log('error.errors', error.errors)
}
ctx.body = collection;
}

View File

@ -2,6 +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 { createOrUpdate } from './actions';
export default async function (this: Application, options = {}) { export default async function (this: Application, options = {}) {
const database = this.database; const database = this.database;
@ -16,4 +17,6 @@ export default async function (this: Application, options = {}) {
model.set('name', model.get('key')); model.set('name', model.get('key'));
} }
}); });
this.resourcer.registerActionHandler('collections:createOrUpdate', createOrUpdate);
} }

View File

@ -18,14 +18,19 @@ export const create = async (ctx: actions.Context, next: actions.Next) => {
payload: 'replace', payload: 'replace',
}, },
); );
await actions.common.create(ctx, async () => {}); await actions.common.create(ctx, async () => { });
const sticky = values['__prepend__'];
const targetKey = values['__insertAfter__'] || values['__insertBefore__']; const targetKey = values['__insertAfter__'] || values['__insertBefore__'];
if (targetKey) { if (sticky || targetKey) {
console.log({ console.log({
associatedKey: values.parentKey, associatedKey: values.parentKey,
resourceKey: ctx.body.key, resourceKey: ctx.body.key,
values: { values: sticky ? {
field: 'sort', field: 'sort',
sticky: true,
} : {
field: 'sort',
insertAfter: !!values['__insertAfter__'],
target: { target: {
key: targetKey, key: targetKey,
}, },
@ -35,7 +40,11 @@ export const create = async (ctx: actions.Context, next: actions.Next) => {
{ {
associatedKey: values.parentKey, associatedKey: values.parentKey,
resourceKey: ctx.body.key, resourceKey: ctx.body.key,
values: { values: sticky ? {
field: 'sort',
sticky: true,
target: {},
} : {
field: 'sort', field: 'sort',
insertAfter: !!values['__insertAfter__'], insertAfter: !!values['__insertAfter__'],
target: { target: {
@ -47,8 +56,8 @@ export const create = async (ctx: actions.Context, next: actions.Next) => {
payload: 'replace', payload: 'replace',
}, },
); );
await middlewares.associated(ctx, async () => {}); await middlewares.associated(ctx, async () => { });
await sort(ctx, async () => {}); await sort(ctx, async () => { });
} }
await next(); await next();
}; };