This commit is contained in:
chenos 2021-07-23 18:08:44 +08:00
parent e21a9ab57d
commit 3223d95d01
16 changed files with 548 additions and 196 deletions

View File

@ -1,5 +1,6 @@
import { SchemaRenderer } from '../../'; import { SchemaRenderer } from '../../';
import React from 'react'; import React from 'react';
import { FormItem } from '@formily/antd';
export default () => { export default () => {
const schema = { const schema = {
@ -32,5 +33,5 @@ export default () => {
}, },
}, },
}; };
return <SchemaRenderer schema={schema} />; return <SchemaRenderer components={{ FormItem }} schema={schema} />;
}; };

View File

@ -69,10 +69,11 @@ export function createRouteSwitch(options: RouteSwitchOptions) {
value={{ ...options.components, ...props.components }} value={{ ...options.components, ...props.components }}
> >
<Switch> <Switch>
{routes.map((route) => { {routes.map((route, index) => {
if (route.type == 'redirect') { if (route.type == 'redirect') {
return ( return (
<Redirect <Redirect
key={index}
to={route.to} to={route.to}
push={route.push} push={route.push}
from={route.from} from={route.from}
@ -87,6 +88,7 @@ export function createRouteSwitch(options: RouteSwitchOptions) {
} }
return ( return (
<Route <Route
key={index}
path={route.path} path={route.path}
exact={route.exact} exact={route.exact}
strict={route.strict} strict={route.strict}

View File

@ -1,4 +1,4 @@
import request from 'umi-request'; import { request } from './schemas';
export interface ResourceOptions { export interface ResourceOptions {
resourceName: string; resourceName: string;
@ -33,7 +33,7 @@ export class Resource {
list(options: ListOptions = {}) { list(options: ListOptions = {}) {
const { resourceName } = this.options; const { resourceName } = this.options;
return request(`/api/${resourceName}:list`); return request(`${resourceName}:list`);
} }
get(options: GetOptions = {}) { get(options: GetOptions = {}) {
@ -42,19 +42,30 @@ export class Resource {
if (!resourceKey) { if (!resourceKey) {
return Promise.resolve({ data: {} }); return Promise.resolve({ data: {} });
} }
return request(`/api/${resourceName}:get/${resourceKey}`); return request(`${resourceName}:get/${resourceKey}`);
} }
save(values: any, options: SaveOptions = {}) { save(values: any, options: SaveOptions = {}) {
const resourceKey = options.resourceKey || this.options.resourceKey; const resourceKey = options.resourceKey || this.options.resourceKey;
const { resourceName } = this.options; const { resourceName } = this.options;
const url = `/api/${resourceName}:${resourceKey ? `update/${resourceKey}` : 'create'}`; const url = `${resourceName}:${resourceKey ? `update/${resourceKey}` : 'create'}`;
return request(url, { return request(url, {
method: 'post', method: 'post',
data: values, data: values,
}); });
} }
destroy(filter: any) {
const { resourceName } = this.options;
const url = `${resourceName}:destroy`;
return request(url, {
method: 'get',
params: {
filter
},
});
}
static make(options: string | Resource | ResourceOptions) { static make(options: string | Resource | ResourceOptions) {
if (typeof options === 'string') { if (typeof options === 'string') {
return new Resource({ resourceName: options }); return new Resource({ resourceName: options });

View File

@ -13,7 +13,7 @@ import {
} from '@formily/react'; } from '@formily/react';
import { Button, Dropdown, Menu, Popover, Space, Drawer, Modal } from 'antd'; import { Button, Dropdown, Menu, Popover, Space, Drawer, Modal } from 'antd';
import { Link, useHistory, LinkProps } from 'react-router-dom'; import { Link, useHistory, LinkProps } from 'react-router-dom';
import { useDesignable, VisibleContext, useDefaultAction } from '..'; import { useDesignable, VisibleContext, useDefaultAction, useVisible } from '..';
import './style.less'; import './style.less';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import cls from 'classnames'; import cls from 'classnames';
@ -25,8 +25,9 @@ import { useSchemaComponent } from '../../components/schema-renderer';
export const Action: any = observer((props: any) => { export const Action: any = observer((props: any) => {
const { useAction = useDefaultAction, icon, ...others } = props; const { useAction = useDefaultAction, icon, ...others } = props;
const { run } = useAction(); const { run } = useAction();
const field = useField();
const { schema, DesignableBar } = useDesignable(); const { schema, DesignableBar } = useDesignable();
const [visible, setVisible] = useState(false); const { visible, setVisible } = useVisible(field.address.entire);
const child = Object.values(schema.properties || {}).shift(); const child = Object.values(schema.properties || {}).shift();
const isDropdownOrPopover = const isDropdownOrPopover =
child && child &&
@ -74,6 +75,7 @@ Action.Modal = observer((props: any) => {
} = props; } = props;
const { schema } = useDesignable(); const { schema } = useDesignable();
const [visible, setVisible] = useContext(VisibleContext); const [visible, setVisible] = useContext(VisibleContext);
console.log('VisibleContext', visible);
const form = useForm(); const form = useForm();
const { run: runOk } = useOkAction(); const { run: runOk } = useOkAction();
const { run: runCancel } = useCancelAction(); const { run: runCancel } = useCancelAction();
@ -81,7 +83,7 @@ Action.Modal = observer((props: any) => {
return ( return (
<Modal <Modal
title={schema.title} title={schema.title}
destroyOnClose // destroyOnClose
maskClosable maskClosable
footer={ footer={
isFormDecorator isFormDecorator

View File

@ -64,6 +64,8 @@ import { cloneDeep } from 'lodash';
import { options } from '../database-field/interfaces'; import { options } from '../database-field/interfaces';
import { useDisplayFieldsContext } from '../form'; import { useDisplayFieldsContext } from '../form';
import './style.less';
const generateGridBlock = (schema: ISchema) => { const generateGridBlock = (schema: ISchema) => {
const name = schema.name || uid(); const name = schema.name || uid();
return { return {
@ -117,17 +119,17 @@ function generateCardItemSchema(component) {
'x-decorator': 'CardItem', 'x-decorator': 'CardItem',
'x-component': 'Table', 'x-component': 'Table',
default: [ default: [
{ key: uid(), field1: uid(), field2: uid() }, // { key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() }, // { key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() }, // { key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() }, // { key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() }, // { key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() }, // { key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() }, // { key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() }, // { key: uid(), field1: uid(), field2: uid() },
], ],
'x-component-props': { 'x-component-props': {
rowKey: 'key', rowKey: 'id',
dragSort: true, dragSort: true,
showIndex: true, showIndex: true,
}, },
@ -232,45 +234,34 @@ function generateCardItemSchema(component) {
'x-component': 'Menu.Action', 'x-component': 'Menu.Action',
'x-designable-bar': 'Table.Action.DesignableBar', 'x-designable-bar': 'Table.Action.DesignableBar',
properties: { properties: {
drawer1: { [uid()]: {
type: 'void', type: 'void',
title: '查看', title: '查看',
'x-component': 'Action.Modal', 'x-component': 'Action.Modal',
'x-component-props': {}, 'x-component-props': {
bodyStyle: {
background: '#f0f2f5',
paddingTop: 0,
},
},
properties: { properties: {
[uid()]: { [uid()]: {
type: 'void', type: 'void',
'x-component': 'Tabs', 'x-component': 'Tabs',
properties: { properties: {
tab1: { [uid()]: {
type: 'void', type: 'void',
'x-component': 'Tabs.TabPane', 'x-component': 'Tabs.TabPane',
'x-component-props': { 'x-component-props': {
tab: 'Tab1', tab: 'Tab1',
}, },
properties: { properties: {
aaa: { [uid()]: {
type: 'string', type: 'void',
title: 'AAA', 'x-component': 'Grid',
'x-decorator': 'FormItem', 'x-component-props': {
required: true, addNewComponent: 'AddNew.PaneItem',
'x-component': 'Input', },
},
},
},
tab2: {
type: 'void',
'x-component': 'Tabs.TabPane',
'x-component-props': {
tab: 'Tab2',
},
properties: {
bbb: {
type: 'string',
title: 'BBB',
'x-decorator': 'FormItem',
required: true,
'x-component': 'Input',
}, },
}, },
}, },
@ -282,14 +273,13 @@ function generateCardItemSchema(component) {
}, },
[uid()]: { [uid()]: {
type: 'void', type: 'void',
name: 'action1', title: '编辑',
title: '修改',
'x-component': 'Menu.Action', 'x-component': 'Menu.Action',
'x-designable-bar': 'Table.Action.DesignableBar', 'x-designable-bar': 'Table.Action.DesignableBar',
properties: { properties: {
drawer1: { [uid()]: {
type: 'void', type: 'void',
title: '编辑表单', title: '编辑数据',
'x-decorator': 'Form', 'x-decorator': 'Form',
'x-decorator-props': { 'x-decorator-props': {
useValues: '{{ Table.useTableRow }}', useValues: '{{ Table.useTableRow }}',
@ -299,17 +289,12 @@ function generateCardItemSchema(component) {
useOkAction: '{{ Table.useTableUpdateAction }}', useOkAction: '{{ Table.useTableUpdateAction }}',
}, },
properties: { properties: {
field1: { [uid()]: {
type: 'string', type: 'void',
title: '字段1', 'x-component': 'Grid',
'x-decorator': 'FormItem', 'x-component-props': {
'x-component': 'Input', addNewComponent: 'AddNew.FormItem',
}, },
field2: {
type: 'string',
title: '字段2',
'x-decorator': 'FormItem',
'x-component': 'Input',
}, },
}, },
}, },
@ -327,48 +312,6 @@ function generateCardItemSchema(component) {
}, },
}, },
}, },
[uid()]: {
type: 'void',
title: '字段1',
'x-component': 'Table.Column',
'x-component-props': {
// title: 'z1',
},
'x-designable-bar': 'Table.Column.DesignableBar',
properties: {
field1: {
type: 'string',
required: true,
'x-read-pretty': true,
'x-decorator-props': {
feedbackLayout: 'popover',
},
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
[uid()]: {
type: 'void',
title: '字段2',
'x-component': 'Table.Column',
'x-component-props': {
// title: 'z1',
},
'x-designable-bar': 'Table.Column.DesignableBar',
properties: {
field2: {
type: 'string',
required: true,
'x-read-pretty': true,
'x-decorator-props': {
feedbackLayout: 'popover',
},
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
}, },
}, },
Form: { Form: {
@ -679,7 +622,7 @@ AddNew.FormItem = observer((props: any) => {
placement={'bottomCenter'} placement={'bottomCenter'}
overlay={ overlay={
<Menu> <Menu>
<Menu.ItemGroup title={`要展示的字段`}> <Menu.ItemGroup className={'display-fields'} title={`要展示的字段`}>
{resource?.fields?.map((field) => ( {resource?.fields?.map((field) => (
<Menu.Item key={field.key}> <Menu.Item key={field.key}>
<FieldSwitch <FieldSwitch
@ -723,7 +666,11 @@ AddNew.FormItem = observer((props: any) => {
))} ))}
</Menu.ItemGroup> </Menu.ItemGroup>
<Menu.Divider /> <Menu.Divider />
<Menu.SubMenu title={'新建字段'}> <Menu.SubMenu
popupClassName={'add-new-fields-popup'}
className={'sub-menu-add-new-fields'}
title={'新建字段'}
>
{options.map( {options.map(
(option) => (option) =>
option.children.length > 0 && ( option.children.length > 0 && (
@ -827,4 +774,163 @@ AddNew.FormItem = observer((props: any) => {
); );
}); });
AddNew.PaneItem = observer((props: any) => {
const { ghost, defaultAction } = props;
const { schema, insertBefore, insertAfter, appendChild } = useDesignable();
const path = useSchemaPath();
const { resource = {}, refresh } = useResourceContext();
const [visible, setVisible] = useState(false);
const { displayFields = [] } = useDisplayFieldsContext();
console.log({ displayFields });
return (
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={setVisible}
overlayStyle={{
minWidth: 200,
}}
placement={'bottomCenter'}
overlay={
<Menu>
<Menu.SubMenu title={'新建卡片'}>
<Menu.Item
onClick={async () => {
let data: ISchema = {
type: 'void',
name: uid(),
'x-decorator': 'CardItem',
'x-component': 'Form',
'x-read-pretty': true,
'x-component-props': {},
'x-designable-bar': 'Form.DesignableBar',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid',
'x-component-props': {
addNewComponent: 'AddNew.FormItem',
},
},
},
};
if (isGridBlock(schema)) {
path.pop();
path.pop();
data = generateGridBlock(data);
} else if (isGrid(schema)) {
data = generateGridBlock(data);
}
if (data) {
let s;
if (isGrid(schema)) {
s = appendChild(data, [...path]);
} else if (defaultAction === 'insertAfter') {
s = insertAfter(data, [...path]);
} else {
s = insertBefore(data, [...path]);
}
await createSchema(s);
}
setVisible(false);
}}
style={{ minWidth: 150 }}
>
</Menu.Item>
<Menu.Item
onClick={async () => {
let data: ISchema = {
type: 'void',
name: uid(),
'x-decorator': 'CardItem',
'x-component': 'Form',
'x-component-props': {
showDefaultButtons: true,
},
'x-designable-bar': 'Form.DesignableBar',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid',
'x-component-props': {
addNewComponent: 'AddNew.FormItem',
},
},
},
};
if (isGridBlock(schema)) {
path.pop();
path.pop();
data = generateGridBlock(data);
} else if (isGrid(schema)) {
data = generateGridBlock(data);
}
if (data) {
let s;
if (isGrid(schema)) {
s = appendChild(data, [...path]);
} else if (defaultAction === 'insertAfter') {
s = insertAfter(data, [...path]);
} else {
s = insertBefore(data, [...path]);
}
await createSchema(s);
}
setVisible(false);
}}
>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu title={'要展示的相关数据'}>
<Menu.Item style={{ minWidth: 150 }}></Menu.Item>
<Menu.Item></Menu.Item>
</Menu.SubMenu>
<Menu.Item
onClick={async () => {
let data: ISchema = {
key: uid(),
type: 'void',
default: '这是一段演示文字,**支持使用 Markdown 语法**',
'x-designable-bar': 'Markdown.Void.DesignableBar',
'x-decorator': 'CardItem',
'x-read-pretty': true,
'x-component': 'Markdown.Void',
};
if (isGridBlock(schema)) {
path.pop();
path.pop();
data = generateGridBlock(data);
} else if (isGrid(schema)) {
data = generateGridBlock(data);
}
if (data) {
let s;
if (isGrid(schema)) {
s = appendChild(data, [...path]);
} else if (defaultAction === 'insertAfter') {
s = insertAfter(data, [...path]);
} else {
s = insertBefore(data, [...path]);
}
await createSchema(s);
}
setVisible(false);
}}
>
</Menu.Item>
</Menu>
}
>
{ghost ? (
<PlusOutlined />
) : (
<Button block type={'dashed'} icon={<PlusOutlined />}></Button>
)}
</Dropdown>
);
});
export default AddNew; export default AddNew;

View File

@ -0,0 +1,10 @@
.ant-dropdown-menu-item-group.display-fields {
.ant-dropdown-menu-item-group-list {
overflow: auto;
max-height: 300px;
}
}
.add-new-fields-popup .ant-dropdown-menu {
overflow: auto;
max-height: 300px;
}

View File

@ -145,6 +145,7 @@ export const DatabaseCollection = observer((props) => {
{field.value?.map((item, index) => { {field.value?.map((item, index) => {
return ( return (
<Select.Option <Select.Option
key={index}
value={index} value={index}
label={`${item.title || '未命名'}${ label={`${item.title || '未命名'}${
item.unsaved ? ' (未保存)' : '' item.unsaved ? ' (未保存)' : ''
@ -254,6 +255,7 @@ export const DatabaseField: any = observer((props) => {
type: 'error', type: 'error',
address: `*(${path},${path}.*)`, address: `*(${path},${path}.*)`,
}); });
console.log('item.key', item.key);
return ( return (
<Collapse.Panel <Collapse.Panel
header={ header={
@ -268,8 +270,9 @@ export const DatabaseField: any = observer((props) => {
</> </>
} }
extra={[ extra={[
<Badge count={errors.length} />, <Badge key={'1'} count={errors.length} />,
<DeleteOutlined <DeleteOutlined
key={'2'}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
field.remove(index); field.remove(index);
@ -305,6 +308,7 @@ export const DatabaseField: any = observer((props) => {
</Collapse> </Collapse>
<Dropdown <Dropdown
placement={'bottomCenter'} placement={'bottomCenter'}
overlayClassName={'all-fields'}
overlayStyle={{ overlayStyle={{
minWidth: 200, minWidth: 200,
}} }}
@ -327,9 +331,9 @@ export const DatabaseField: any = observer((props) => {
}} }}
> >
{options.map( {options.map(
(option) => (option, groupIndex) =>
option.children.length > 0 && ( option.children.length > 0 && (
<Menu.ItemGroup title={option.label}> <Menu.ItemGroup key={groupIndex} 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>
))} ))}

View File

@ -14,3 +14,10 @@
background: #1890ff; background: #1890ff;
} }
} }
.ant-dropdown.all-fields {
.ant-dropdown-menu {
max-height: 300px;
overflow: auto;
}
}

View File

@ -22,7 +22,7 @@ import {
ResourceContextProvider, ResourceContextProvider,
} from '../'; } from '../';
import get from 'lodash/get'; import get from 'lodash/get';
import { Button, Dropdown, Menu, Space } from 'antd'; import { Button, Dropdown, Menu, message, Space } from 'antd';
import { MenuOutlined, DragOutlined } from '@ant-design/icons'; import { MenuOutlined, DragOutlined } from '@ant-design/icons';
import cls from 'classnames'; import cls from 'classnames';
import { FormLayout } from '@formily/antd'; import { FormLayout } from '@formily/antd';
@ -38,6 +38,7 @@ import { DesignableBar } from './DesignableBar';
import { FieldDesignableBar } from './Field.DesignableBar'; import { FieldDesignableBar } from './Field.DesignableBar';
import { createContext } from 'react'; import { createContext } from 'react';
import { BlockItem } from '../block-item'; import { BlockItem } from '../block-item';
import { Resource } from '../../resource';
const [DisplayFieldsContextProvider, useDisplayFieldsContext] = constate(() => { const [DisplayFieldsContextProvider, useDisplayFieldsContext] = constate(() => {
const [displayFields, setDisplayFields] = useState([]); const [displayFields, setDisplayFields] = useState([]);
@ -46,12 +47,12 @@ const [DisplayFieldsContextProvider, useDisplayFieldsContext] = constate(() => {
displayFields, displayFields,
setDisplayFields, setDisplayFields,
addDisplayField(fieldName) { addDisplayField(fieldName) {
setDisplayFields(fields => fields.concat(fieldName)); setDisplayFields((fields) => fields.concat(fieldName));
}, },
} };
}); });
export { DisplayFieldsContextProvider, useDisplayFieldsContext } export { DisplayFieldsContextProvider, useDisplayFieldsContext };
export const Form: any = observer((props: any) => { export const Form: any = observer((props: any) => {
const { const {
@ -60,12 +61,12 @@ export const Form: any = observer((props: any) => {
resourceName, resourceName,
...others ...others
} = props; } = props;
const { schema } = useDesignable();
const initialValues = useValues(); const initialValues = useValues();
const form = useMemo(() => { const form = useMemo(() => {
console.log('Form.useMemo', initialValues); console.log('Form.useMemo', initialValues);
return createForm({ initialValues }); return createForm({ initialValues, readPretty: schema['x-read-pretty'] });
}, []); }, []);
const { schema } = useDesignable();
const path = useSchemaPath(); const path = useSchemaPath();
const scope = useContext(SchemaExpressionScopeContext); const scope = useContext(SchemaExpressionScopeContext);
const content = ( const content = (
@ -102,6 +103,9 @@ export const Form: any = observer((props: any) => {
onClick={async () => { onClick={async () => {
const values = await form.submit(); const values = await form.submit();
console.log({ values }); console.log({ values });
await Resource.make(resourceName).save(values);
message.success('保存成功');
await form.reset();
}} }}
type={'primary'} type={'primary'}
> >
@ -121,9 +125,7 @@ export const Form: any = observer((props: any) => {
return resourceName ? ( return resourceName ? (
// @ts-ignore // @ts-ignore
<ResourceContextProvider resourceName={resourceName}> <ResourceContextProvider resourceName={resourceName}>
<DisplayFieldsContextProvider> <DisplayFieldsContextProvider>{content}</DisplayFieldsContextProvider>
{content}
</DisplayFieldsContextProvider>
</ResourceContextProvider> </ResourceContextProvider>
) : ( ) : (
content content
@ -141,7 +143,7 @@ Form.Field = observer((props: any) => {
const { resource = {} } = useResourceContext(); const { resource = {} } = useResourceContext();
useEffect(() => { useEffect(() => {
if (fieldName) { if (fieldName) {
addDisplayField(fieldName); addDisplayField && addDisplayField(fieldName);
} }
}, [fieldName]); }, [fieldName]);
if (!fieldName) { if (!fieldName) {

View File

@ -32,6 +32,21 @@ export function mapReadPretty(component: any, readPrettyProps?: any) {
} }
export const VisibleContext = createContext(null); export const VisibleContext = createContext(null);
export function useVisible(name, defaultValue = false) {
const [visible, setVisible] = useState(defaultValue);
return { visible, setVisible };
// const [visible, setVisible] = useCookieState(`${name}-visible`, {
// defaultValue: defaultValue ? 'true' : null,
// });
// return {
// visible: visible === 'true' ? true : false,
// setVisible: (value) => {
// setVisible(value ? 'true' : null);
// },
// }
}
export const DesignableBarContext = createContext(null); export const DesignableBarContext = createContext(null);
const [PageTitleContextProvider, usePageTitleContext] = constate(() => { const [PageTitleContextProvider, usePageTitleContext] = constate(() => {

View File

@ -24,7 +24,6 @@ type ComposedInput = React.FC<InputProps> & {
export const Input: ComposedInput = connect( export const Input: ComposedInput = connect(
AntdInput, AntdInput,
mapProps((props, field) => { mapProps((props, field) => {
console.log('fieldfieldfieldfieldfield', field.address.entire);
return { return {
...props, ...props,
suffix: ( suffix: (

View File

@ -55,6 +55,7 @@ import {
removeSchema, removeSchema,
updateSchema, updateSchema,
useDefaultAction, useDefaultAction,
useVisible,
VisibleContext, VisibleContext,
} from '..'; } from '..';
import { useMount } from 'ahooks'; import { useMount } from 'ahooks';
@ -205,7 +206,8 @@ Menu.Link = observer((props: any) => {
eventKey={schema.name} eventKey={schema.name}
key={schema.name} key={schema.name}
> >
<Link to={props.to}>{schema.title}</Link> {schema.title}
{/* <Link to={props.to || schema.name}>{schema.title}</Link> */}
<DesignableBar /> <DesignableBar />
</AntdMenu.Item> </AntdMenu.Item>
); );
@ -232,7 +234,8 @@ Menu.URL = observer((props: any) => {
Menu.Action = observer((props: any) => { Menu.Action = observer((props: any) => {
const { icon, useAction = useDefaultAction, ...others } = props; const { icon, useAction = useDefaultAction, ...others } = props;
const { schema, DesignableBar } = useDesignable(); const { schema, DesignableBar } = useDesignable();
const [visible, setVisible] = useState(false); const field = useField();
const { visible, setVisible } = useVisible(field.address.entire);
const { run } = useAction(); const { run } = useAction();
return ( return (
<VisibleContext.Provider value={[visible, setVisible]}> <VisibleContext.Provider value={[visible, setVisible]}>

View File

@ -1,4 +1,10 @@
import React, { createContext, useContext, useRef, useState } from 'react'; import React, {
createContext,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { import {
useFieldSchema, useFieldSchema,
Schema, Schema,
@ -22,7 +28,7 @@ import {
Select, Select,
Switch, Switch,
} from 'antd'; } from 'antd';
import { findIndex, get, set } from 'lodash'; import { cloneDeep, findIndex, get, set } from 'lodash';
import constate from 'constate'; import constate from 'constate';
import useRequest from '@ahooksjs/use-request'; import useRequest from '@ahooksjs/use-request';
import { uid, clone } from '@formily/shared'; import { uid, clone } from '@formily/shared';
@ -39,9 +45,11 @@ import {
} from 'react-sortable-hoc'; } from 'react-sortable-hoc';
import cls from 'classnames'; import cls from 'classnames';
import { import {
createSchema,
getSchemaPath, getSchemaPath,
removeSchema, removeSchema,
ResourceContextProvider, ResourceContextProvider,
updateSchema,
useCollectionContext, useCollectionContext,
useDesignable, useDesignable,
useResourceContext, useResourceContext,
@ -53,6 +61,8 @@ import { DraggableBlockContext } from '../../components/drag-and-drop';
import AddNew from '../add-new'; import AddNew from '../add-new';
import { isGridRowOrCol } from '../grid'; import { isGridRowOrCol } from '../grid';
import { options } from '../database-field/interfaces'; import { options } from '../database-field/interfaces';
import { Resource } from '../../resource';
import { useDisplayFieldsContext } from '../form';
interface TableRowProps { interface TableRowProps {
index: number; index: number;
@ -131,10 +141,13 @@ function useTableActionBars() {
return findActionBars(schema); return findActionBars(schema);
} }
const ResourceFieldContext = createContext(null);
function useTableColumns(props?: any) { function useTableColumns(props?: any) {
const { schema, designable } = useDesignable(); const { schema, designable } = useDesignable();
// const schema = useFieldSchema(); // const schema = useFieldSchema();
const { dataSource } = props || {}; const { dataSource, rowKey = 'id' } = props || {};
const { resource = {} } = useResourceContext();
function findColumns(schema: Schema): Schema[] { function findColumns(schema: Schema): Schema[] {
return schema.reduceProperties((columns, current) => { return schema.reduceProperties((columns, current) => {
@ -148,21 +161,35 @@ function useTableColumns(props?: any) {
const columns = findColumns(schema).map((item) => { const columns = findColumns(schema).map((item) => {
const columnProps = item['x-component-props'] || {}; const columnProps = item['x-component-props'] || {};
console.log(item); console.log(item);
const resourceField = columnProps.fieldName
? resource?.fields?.find((item) => item.name === columnProps.fieldName)
: null;
return { return {
title: <RecursionField name={item.name} schema={item} onlyRenderSelf />, title: (
<ResourceFieldContext.Provider value={resourceField}>
<RecursionField name={item.name} schema={item} onlyRenderSelf />
</ResourceFieldContext.Provider>
),
dataIndex: item.name, dataIndex: item.name,
...columnProps, ...columnProps,
render(value, record, recordIndex) { render(value, record, recordIndex) {
const index = dataSource.indexOf(record); const index = findIndex(
dataSource,
(item) => item[rowKey] === record[rowKey],
);
console.log({ dataSource, record, index });
return ( return (
<TableRowContext.Provider <ResourceFieldContext.Provider value={resourceField}>
value={{ <TableRowContext.Provider
index: index, value={{
data: record, index: index,
}} data: record,
> }}
<RecursionField schema={item} name={index} onlyRenderProperties /> >
</TableRowContext.Provider> <Table.Cell schema={item} />
</TableRowContext.Provider>
</ResourceFieldContext.Provider>
); );
}, },
}; };
@ -198,6 +225,7 @@ function useTableColumns(props?: any) {
function AddColumn() { function AddColumn() {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { appendChild } = useDesignable();
const { resource = {}, refresh } = useResourceContext(); const { resource = {}, refresh } = useResourceContext();
return ( return (
<Dropdown <Dropdown
@ -206,7 +234,7 @@ function AddColumn() {
onVisibleChange={setVisible} onVisibleChange={setVisible}
overlay={ overlay={
<Menu> <Menu>
<Menu.ItemGroup title={'要展示的字段'}> <Menu.ItemGroup className={'display-fields'} title={'要展示的字段'}>
{resource?.fields?.map((field) => ( {resource?.fields?.map((field) => (
<Menu.Item style={{ minWidth: 150 }}> <Menu.Item style={{ minWidth: 150 }}>
<div <div
@ -215,15 +243,29 @@ function AddColumn() {
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
}} }}
onClick={async () => {
const data = appendChild({
type: 'void',
'x-component': 'Table.Column',
'x-component-props': {
fieldName: field.name,
},
'x-designable-bar': 'Table.Column.DesignableBar',
});
await createSchema(data);
}}
> >
<span>{field.uiSchema.title}</span> <span>{field.uiSchema.title}</span>
<Switch size={'small'} defaultChecked /> <Switch size={'small'} />
</div> </div>
</Menu.Item> </Menu.Item>
))} ))}
</Menu.ItemGroup> </Menu.ItemGroup>
<Menu.Divider /> <Menu.Divider />
<Menu.SubMenu title={'新增字段'}> <Menu.SubMenu
popupClassName={'add-new-fields-popup'}
title={'新增字段'}
>
{options.map((option) => ( {options.map((option) => (
<Menu.ItemGroup title={option.label}> <Menu.ItemGroup title={option.label}>
{option.children.map((item) => ( {option.children.map((item) => (
@ -266,26 +308,31 @@ export function useTableIndex() {
export function useTableDestroyAction() { export function useTableDestroyAction() {
const ctx = useContext(TableRowContext); const ctx = useContext(TableRowContext);
const { field, selectedRowKeys, setSelectedRowKeys, refresh } = const { resource, field, selectedRowKeys, setSelectedRowKeys, refresh } =
useTableContext(); useTableContext();
const rowKey = field.componentProps.rowKey || 'id'; const rowKey = field.componentProps.rowKey || 'id';
return { return {
async run() { async run() {
if (ctx && typeof ctx.index !== 'undefined') { if (ctx && typeof ctx.index !== 'undefined') {
field.remove(ctx.index); // field.remove(ctx.index);
refresh(); await resource.destroy({
[rowKey]: ctx.data[rowKey],
});
await refresh();
return; return;
} }
if (!selectedRowKeys.length) { if (!selectedRowKeys.length) {
return; return;
} }
while (selectedRowKeys.length) { await resource.destroy({
const key = selectedRowKeys.shift(); [`${rowKey}.in`]: selectedRowKeys,
const index = findIndex(field.value, (item) => item[rowKey] === key); });
field.remove(index); // while (selectedRowKeys.length) {
} // const key = selectedRowKeys.shift();
// const index = findIndex(field.value, (item) => item[rowKey] === key);
// field.remove(index);
// }
refresh(); refresh();
setSelectedRowKeys([]); setSelectedRowKeys([]);
}, },
@ -305,16 +352,18 @@ export function useTableFilterAction() {
} }
export function useTableCreateAction() { export function useTableCreateAction() {
const { field, run: exec, params } = useTableContext(); const { resource, field, run: exec, params, refresh } = useTableContext();
const [, setVisible] = useContext(VisibleContext); const [, setVisible] = useContext(VisibleContext);
const form = useForm(); const form = useForm();
return { return {
async run() { async run() {
setVisible && setVisible(false); setVisible && setVisible(false);
field.push({ await resource.save(form.values);
key: uid(), // await refresh();
...clone(form.values), // field.push({
}); // key: uid(),
// ...clone(form.values),
// });
form.reset(); form.reset();
exec({ ...params, page: 1 }); exec({ ...params, page: 1 });
}, },
@ -322,14 +371,16 @@ export function useTableCreateAction() {
} }
export function useTableUpdateAction() { export function useTableUpdateAction() {
const { field, refresh, params } = useTableContext(); const { resource, field, refresh, params } = useTableContext();
const [, setVisible] = useContext(VisibleContext); const [, setVisible] = useContext(VisibleContext);
const ctx = useContext(TableRowContext); const ctx = useContext(TableRowContext);
const form = useForm(); const form = useForm();
return { return {
async run() { async run() {
field.value[ctx.index] = form.values;
setVisible && setVisible(false); setVisible && setVisible(false);
await resource.save(form.values, {
resourceKey: form.values[field.componentProps.rowKey || 'id'],
});
refresh(); refresh();
}, },
}; };
@ -342,6 +393,7 @@ const [TableContextProvider, useTableContext] = constate(() => {
const [selectedRowKeys, setSelectedRowKeys] = useState( const [selectedRowKeys, setSelectedRowKeys] = useState(
defaultSelectedRowKeys || [], defaultSelectedRowKeys || [],
); );
const resource = Resource.make(field.componentProps.resourceName);
console.log({ defaultSelectedRowKeys }); console.log({ defaultSelectedRowKeys });
const pagination = usePaginationProps(); const pagination = usePaginationProps();
const response = useRequest<{ const response = useRequest<{
@ -349,6 +401,21 @@ const [TableContextProvider, useTableContext] = constate(() => {
total: number; total: number;
}>((params = {}) => { }>((params = {}) => {
return new Promise((resolve) => { return new Promise((resolve) => {
resource.list({}).then((res) => {
field.setValue(res?.data || []);
resolve({
list: res?.data || [],
total: res?.meta?.count || res?.data?.length,
});
console.log(
'field.componentProps.resourceName',
field.componentProps.resourceName,
res,
);
});
return;
const dataSource = Array.isArray(field.value) ? field.value.slice() : []; const dataSource = Array.isArray(field.value) ? field.value.slice() : [];
if (pagination) { if (pagination) {
const { const {
@ -375,6 +442,7 @@ const [TableContextProvider, useTableContext] = constate(() => {
...(response.params[0] || {}), ...(response.params[0] || {}),
}; };
return { return {
resource,
...response, ...response,
params, params,
field, field,
@ -440,7 +508,10 @@ const TableContainer = observer((props) => {
type: 'checkbox', type: 'checkbox',
selectedRowKeys, selectedRowKeys,
renderCell: (checked, record, _, originNode) => { renderCell: (checked, record, _, originNode) => {
const index = dataSource.indexOf(record); const index = findIndex(
dataSource,
(item) => item[rowKey] === record[rowKey],
);
return ( return (
<TableRowContext.Provider <TableRowContext.Provider
value={{ value={{
@ -571,13 +642,79 @@ function useDesignableBar() {
}; };
} }
Table.Column = observer((props) => { Table.Cell = observer((props: any) => {
const schema = useFieldSchema(); const ctx = useContext(TableRowContext);
const field = useField(); const schema = props.schema;
const { DesignableBar } = useDesignableBar(); const resourceField = useContext(ResourceFieldContext);
return (
<RecursionField
schema={
!resourceField
? schema
: new Schema({
type: 'void',
properties: {
[resourceField.name]: {
...resourceField.uiSchema,
title: undefined,
'x-read-pretty': true,
'x-decorator-props': {
feedbackLayout: 'popover',
},
'x-decorator': 'FormilyFormItem',
},
},
})
}
name={ctx.index}
onlyRenderProperties
/>
);
// const { fieldName } = props;
// const { schema } = useDesignable();
// const path = getSchemaPath(schema);
// const { addDisplayField } = useDisplayFieldsContext();
// const { resource = {} } = useResourceContext();
// useEffect(() => {
// if (fieldName) {
// addDisplayField && addDisplayField(fieldName);
// }
// }, [fieldName]);
// if (!fieldName) {
// return null;
// }
// const field = resource?.fields?.find((item) => item.name === fieldName);
// if (!field) {
// return null;
// }
// const name = useContext(FormFieldUIDContext);
// const title = schema['title'] || field.uiSchema['title'];
// return (
// <RecursionField
// name={name}
// schema={
// new Schema({
// 'x-path': path,
// type: 'void',
// properties: {
// [field.name]: {
// ...field.uiSchema,
// title,
// 'x-decorator': 'FormilyFormItem',
// },
// },
// })
// }
// />
// );
});
Table.Column = observer((props: any) => {
const resourceField = useContext(ResourceFieldContext) || {};
const { schema, DesignableBar } = useDesignable();
return ( return (
<div className={'nb-table-column'}> <div className={'nb-table-column'}>
{field.title} {schema.title || resourceField?.uiSchema?.title}
<DesignableBar /> <DesignableBar />
</div> </div>
); );
@ -605,52 +742,28 @@ Table.Column.DesignableBar = () => {
overlay={ overlay={
<Menu> <Menu>
<Menu.Item <Menu.Item
onClick={(e) => { onClick={async (e) => {
const title = uid(); const title = uid();
field.title = title; field.title = title;
schema.title = title; schema.title = title;
refresh();
await updateSchema({
key: schema['key'],
title: title,
});
setVisible(false); setVisible(false);
}} }}
> >
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
onClick={() => { onClick={async () => {
remove(); const s = remove();
console.log('Table.Column.DesignableBar', { schema }); await removeSchema(s);
}} }}
> >
</Menu.Item> </Menu.Item>
<Menu.Item
onClick={() => {
const name = uid();
insertAfter({
name: `column_${name}`,
type: 'void',
title: `字段 ${name}`,
'x-component': 'Table.Column',
'x-component-props': {
// title: 'z1',
},
'x-designable-bar': 'Table.Column.DesignableBar',
properties: {
[name]: {
type: 'string',
required: true,
// 'x-read-pretty': true,
'x-decorator-props': {
feedbackLayout: 'popover',
},
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
});
}}
>
</Menu.Item>
</Menu> </Menu>
} }
> >
@ -927,9 +1040,12 @@ Table.Action.DesignableBar = () => {
{inActionBar ? ( {inActionBar ? (
<Menu.Item></Menu.Item> <Menu.Item></Menu.Item>
) : ( ) : (
<Menu.Item> &nbsp;&nbsp;<Switch size={'small'} defaultChecked/></Menu.Item> <Menu.Item>
&nbsp;&nbsp;
<Switch size={'small'} defaultChecked />
</Menu.Item>
)} )}
<Menu.Divider /> <Menu.Divider />
<Menu.Item></Menu.Item> <Menu.Item></Menu.Item>
</Menu> </Menu>

View File

@ -30,9 +30,10 @@ export const createOrUpdate = async (ctx: actions.Context, next: actions.Next) =
await collection.update(values); await collection.update(values);
} }
await collection.updateAssociations(values); await collection.updateAssociations(values);
await collection.migrate();
} catch (error) { } catch (error) {
console.log('error.errors', error.errors) console.log('error.errors', error.errors)
throw error;
} }
ctx.body = collection; ctx.body = collection;
} }

View File

@ -1,5 +1,16 @@
import _ from 'lodash'; import _ from 'lodash';
import { Model } from '@nocobase/database'; import { Model, TableOptions } from '@nocobase/database';
export interface LoadOptions {
reset?: boolean;
where?: any;
skipExisting?: boolean;
[key: string]: any;
}
export interface MigrateOptions {
[key: string]: any;
}
export class Collection extends Model { export class Collection extends Model {
static async create(value?: any, options?: any): Promise<any> { static async create(value?: any, options?: any): Promise<any> {
@ -37,4 +48,66 @@ export class Collection extends Model {
} }
return items; return items;
} }
/**
* DOTO
* - database.table
* -
*
* @param opts
*/
async loadTableOptions(opts: any = {}) {
const options = await this.toProps();
console.log(JSON.stringify(options, null, 2));
const table = this.database.table(options);
return table;
}
/**
*
*/
async migrate(options: MigrateOptions = {}) {
const { isNewRecord } = options;
const table = await this.loadTableOptions(options);
// 如果不是新增数据force 必须为 false
if (!isNewRecord) {
return await table.sync({
force: false,
alter: {
drop: false,
}
});
}
// TODO: 暂时加了个 collectionSync 解决 collection.create 的数据不清空问题
// @ts-ignore
const sync = this.sequelize.options.collectionSync;
return await table.sync(sync || {
force: false,
alter: {
drop: false,
}
});
}
/**
* TODO
*
* @param options
*/
static async load(options: LoadOptions = {}) {
const { skipExisting = false, reset = false, where = {}, transaction } = options;
const collections = await this.findAll({
transaction,
where,
});
for (const collection of collections) {
if (skipExisting && this.database.isDefined(collection.get('name'))) {
continue;
}
await collection.loadTableOptions({
transaction,
reset,
});
}
}
} }

View File

@ -26,7 +26,7 @@ export class Field extends Model {
const data: any = _.omit(json, ['options', 'created_at', 'updated_at']); const data: any = _.omit(json, ['options', 'created_at', 'updated_at']);
const options = json['options'] || {}; const options = json['options'] || {};
const fields = await this.getNestedFields(); const fields = await this.getNestedFields();
const props = { ...data, ...options }; const props = { type: json['dataType'], ...data, ...options };
if (fields.length) { if (fields.length) {
props['children'] = fields; props['children'] = fields;
} }