feat(client): improve the collection manager module

This commit is contained in:
chenos 2022-02-23 18:22:37 +08:00
parent 57c9524f34
commit 37be46aacb
16 changed files with 231 additions and 40 deletions

View File

@ -5,6 +5,7 @@ import {
AntdSchemaComponentProvider, AntdSchemaComponentProvider,
APIClientProvider, APIClientProvider,
AuthLayout, AuthLayout,
ChinaRegionProvider,
CollectionManagerShortcut, CollectionManagerShortcut,
compose, compose,
DesignableSwitch, DesignableSwitch,
@ -53,6 +54,7 @@ const providers = [
RemoteCollectionManagerProvider, RemoteCollectionManagerProvider,
[SchemaInitializerProvider, { initializers: { MenuItemInitializers } }], [SchemaInitializerProvider, { initializers: { MenuItemInitializers } }],
AntdSchemaComponentProvider, AntdSchemaComponentProvider,
ChinaRegionProvider,
[DocumentTitleProvider, { addonAfter: 'NocoBase' }], [DocumentTitleProvider, { addonAfter: 'NocoBase' }],
[ [
RouteSwitchProvider, RouteSwitchProvider,

View File

@ -0,0 +1,76 @@
import { ArrayField } from '@formily/core';
import { useField } from '@formily/react';
import React from 'react';
import { SchemaComponentOptions } from '..';
import { useAPIClient, useRequest } from '../api-client';
const useChinaRegionDataSource = (options) => {
const field = useField<ArrayField>();
const maxLevel = field.componentProps.maxLevel;
return useRequest(
{
resource: 'chinaRegions',
action: 'list',
params: {
sort: 'code',
filter: {
level: 1,
},
},
},
{
...options,
onSuccess(data) {
options?.onSuccess({
data:
data?.data?.map((item) => {
if (maxLevel !== 1) {
item.isLeaf = false;
}
return item;
}) || [],
});
},
},
);
};
const useChinaRegionLoadData = () => {
const api = useAPIClient();
const field = useField<ArrayField>();
const maxLevel = field.componentProps.maxLevel;
return (selectedOptions) => {
const targetOption = selectedOptions[selectedOptions.length - 1];
if (targetOption?.children?.length > 0) {
return;
}
targetOption.loading = true;
api
.resource('chinaRegions')
.list({
sort: 'code',
filter: {
parentCode: targetOption.code,
},
})
.then(({ data }) => {
targetOption.loading = false;
targetOption.children =
data?.data?.map((item) => {
if (maxLevel > item.level) {
item.isLeaf = false;
}
return item;
}) || [];
field.dataSource = [...field.dataSource];
});
};
};
export const ChinaRegionProvider = (props) => {
return (
<SchemaComponentOptions scope={{ useChinaRegionDataSource, useChinaRegionLoadData }}>
{props.children}
</SchemaComponentOptions>
);
};

View File

@ -1,7 +1,7 @@
import { Field } from '@formily/core'; import { Field } from '@formily/core';
import { connect, useField, useFieldSchema } from '@formily/react'; import { connect, useField, useFieldSchema } from '@formily/react';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useComponent } from '..'; import { useCompile, useComponent } from '..';
import { CollectionFieldProvider } from './CollectionFieldProvider'; import { CollectionFieldProvider } from './CollectionFieldProvider';
import { useCollectionField } from './hooks'; import { useCollectionField } from './hooks';
@ -11,6 +11,7 @@ const InternalField: React.FC = (props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { uiSchema } = useCollectionField(); const { uiSchema } = useCollectionField();
const component = useComponent(uiSchema?.['x-component']); const component = useComponent(uiSchema?.['x-component']);
const compile = useCompile();
const setFieldProps = (key, value) => { const setFieldProps = (key, value) => {
field[key] = typeof field[key] === 'undefined' ? value : field[key]; field[key] = typeof field[key] === 'undefined' ? value : field[key];
}; };
@ -24,6 +25,7 @@ const InternalField: React.FC = (props) => {
if (!uiSchema) { if (!uiSchema) {
return; return;
} }
console.log('uiSchema', uiSchema);
setFieldProps('title', uiSchema.title); setFieldProps('title', uiSchema.title);
setFieldProps('description', uiSchema.description); setFieldProps('description', uiSchema.description);
setFieldProps('initialValue', uiSchema.default); setFieldProps('initialValue', uiSchema.default);

View File

@ -32,7 +32,7 @@ export const RemoteCollectionManagerProvider = (props: any) => {
resource: 'collections', resource: 'collections',
action: 'list', action: 'list',
params: { params: {
pageSize: 999, paginate: false,
appends: ['fields', 'fields.uiSchema'], appends: ['fields', 'fields.uiSchema'],
}, },
}; };

View File

@ -1,18 +1,29 @@
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { ArrayTable } from '@formily/antd'; import { ISchema, useForm } from '@formily/react';
import { ISchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { Button, Dropdown, Menu } from 'antd'; import { Button, Dropdown, Menu } from 'antd';
import { cloneDeep } from 'lodash';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useRequest } from '../../api-client';
import { ActionContext, SchemaComponent, useCompile } from '../../schema-component'; import { ActionContext, SchemaComponent, useCompile } from '../../schema-component';
import { useCreateAction } from '../action-hooks';
import { useCollectionManager } from '../hooks'; import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types'; import { IField } from '../interfaces/types';
import { ArrayTable } from './ArrayTable';
import { options } from './interfaces'; import { options } from './interfaces';
const getSchema = (schema: IField): ISchema => { const getSchema = (schema: IField): ISchema => {
if (!schema) { if (!schema) {
return; return;
} }
const properties = cloneDeep(schema.properties) as any;
const initialValue = {
...cloneDeep(schema.default),
interface: schema.name,
name: `f_${uid()}`,
};
initialValue.uiSchema.title = schema.title;
console.log('initialValue', initialValue);
return { return {
type: 'object', type: 'object',
properties: { properties: {
@ -21,16 +32,20 @@ const getSchema = (schema: IField): ISchema => {
'x-component': 'Action.Drawer', 'x-component': 'Action.Drawer',
'x-decorator': 'Form', 'x-decorator': 'Form',
'x-decorator-props': { 'x-decorator-props': {
initialValue: { useValues(options) {
interface: schema.name, return useRequest(
...schema.default, () =>
name: `f_${uid()}`, Promise.resolve({
data: initialValue,
}),
options,
);
}, },
}, },
title: '{{ t("Add field") }}', title: '{{ t("Add field") }}',
properties: { properties: {
// @ts-ignore // @ts-ignore
...schema.properties, ...properties,
footer: { footer: {
type: 'void', type: 'void',
'x-component': 'Action.Drawer.Footer', 'x-component': 'Action.Drawer.Footer',
@ -47,7 +62,7 @@ const getSchema = (schema: IField): ISchema => {
'x-component': 'Action', 'x-component': 'Action',
'x-component-props': { 'x-component-props': {
type: 'primary', type: 'primary',
useAction: '{{ cm.useCreateActionAndRefreshCM }}', useAction: '{{ useCreateCollectionField }}',
}, },
}, },
}, },
@ -58,6 +73,30 @@ const getSchema = (schema: IField): ISchema => {
}; };
}; };
const useCreateCollectionField = () => {
const form = useForm();
const { run } = useCreateAction();
const { refreshCM } = useCollectionManager();
return {
async run() {
await form.submit();
const options = form?.values?.uiSchema?.enum?.slice() || [];
form.setValuesIn(
'uiSchema.enum',
options.map((option) => {
return {
value: uid(),
...option,
};
}),
);
console.log('form.values', form.values);
await run();
await refreshCM();
},
};
};
export const AddFieldAction = () => { export const AddFieldAction = () => {
const { getInterface } = useCollectionManager(); const { getInterface } = useCollectionManager();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
@ -78,7 +117,6 @@ export const AddFieldAction = () => {
return ( return (
<Menu.SubMenu title={compile(option.label)}> <Menu.SubMenu title={compile(option.label)}>
{option.children.map((child) => { {option.children.map((child) => {
console.log(child);
return <Menu.Item key={child.name}>{compile(child.title)}</Menu.Item>; return <Menu.Item key={child.name}>{compile(child.title)}</Menu.Item>;
})} })}
</Menu.SubMenu> </Menu.SubMenu>
@ -91,7 +129,7 @@ export const AddFieldAction = () => {
</Button> </Button>
</Dropdown> </Dropdown>
<SchemaComponent schema={schema} components={{ ArrayTable }} /> <SchemaComponent schema={schema} components={{ ArrayTable }} scope={{ useCreateCollectionField }} />
</ActionContext.Provider> </ActionContext.Provider>
); );
}; };

View File

@ -0,0 +1,25 @@
import { ArrayBase, ArrayTable as FormilyArrayTable } from '@formily/antd';
import { connect } from '@formily/react';
import React, { Fragment } from 'react';
export const ArrayTable: any = connect((props) => {
const { onChange } = props;
return (
<FormilyArrayTable
{...props}
onChange={(value) => {
console.log('onChange', value);
onChange(value);
}}
/>
);
});
ArrayTable.displayName = 'ArrayTable'
ArrayTable.Column = () => {
return <Fragment />
}
ArrayBase.mixin(ArrayTable)

View File

@ -23,6 +23,8 @@ const useCollectionValues = (options) => {
Promise.resolve({ Promise.resolve({
data: { data: {
name: `t_${uid()}`, name: `t_${uid()}`,
createdBy: true,
updatedBy: true,
}, },
}), }),
{ {
@ -38,7 +40,7 @@ const useCollectionValues = (options) => {
}, [visible]); }, [visible]);
return result; return result;
} };
export const ConfigurationTable = () => { export const ConfigurationTable = () => {
const { collections = [] } = useCollectionManager(); const { collections = [] } = useCollectionManager();
@ -50,7 +52,7 @@ export const ConfigurationTable = () => {
}; };
return ( return (
<div> <div>
<SchemaComponent schema={collectionSchema} scope={{ useCollectionValues, useAsyncDataSource, loadCollections }}/> <SchemaComponent schema={collectionSchema} scope={{ useCollectionValues, useAsyncDataSource, loadCollections }} />
</div> </div>
); );
}; };

View File

@ -1,18 +1,22 @@
import { ISchema } from '@formily/react'; import { ISchema, useForm } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAPIClient } from '../../api-client'; import { useAPIClient, useRequest } from '../../api-client';
import { useRecord } from '../../record-provider'; import { useRecord } from '../../record-provider';
import { ActionContext, SchemaComponent } from '../../schema-component'; import { ActionContext, SchemaComponent } from '../../schema-component';
import { useUpdateAction } from '../action-hooks';
import { useCollectionManager } from '../hooks'; import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types'; import { IField } from '../interfaces/types';
import { ArrayTable } from './ArrayTable';
const getSchema = (schema: IField): ISchema => { const getSchema = (schema: IField): ISchema => {
if (!schema) { if (!schema) {
return; return;
} }
const properties = cloneDeep(schema.properties) as any;
properties.name['x-disabled'] = true;
return { return {
type: 'object', type: 'object',
properties: { properties: {
@ -21,12 +25,20 @@ const getSchema = (schema: IField): ISchema => {
'x-component': 'Action.Drawer', 'x-component': 'Action.Drawer',
'x-decorator': 'Form', 'x-decorator': 'Form',
'x-decorator-props': { 'x-decorator-props': {
initialValue: cloneDeep(schema.default), useValues(options) {
return useRequest(
() =>
Promise.resolve({
data: cloneDeep(schema.default),
}),
options,
);
},
}, },
title: '{{ t("Edit field") }}', title: '{{ t("Edit field") }}',
properties: { properties: {
// @ts-ignore // @ts-ignore
...schema.properties, ...properties,
footer: { footer: {
type: 'void', type: 'void',
'x-component': 'Action.Drawer.Footer', 'x-component': 'Action.Drawer.Footer',
@ -43,7 +55,7 @@ const getSchema = (schema: IField): ISchema => {
'x-component': 'Action', 'x-component': 'Action',
'x-component-props': { 'x-component-props': {
type: 'primary', type: 'primary',
useAction: '{{ cm.useUpdateActionAndRefreshCM }}', useAction: '{{ useUpdateCollectionField }}',
}, },
}, },
}, },
@ -54,6 +66,29 @@ const getSchema = (schema: IField): ISchema => {
}; };
}; };
const useUpdateCollectionField = () => {
const form = useForm();
const { run } = useUpdateAction();
const { refreshCM } = useCollectionManager();
return {
async run() {
await form.submit();
const options = form?.values?.uiSchema?.enum?.slice() || [];
form.setValuesIn(
'uiSchema.enum',
options.map((option) => {
return {
value: uid(),
...option,
};
}),
);
await run();
await refreshCM();
},
};
};
export const EditFieldAction = (props) => { export const EditFieldAction = (props) => {
const record = useRecord(); const record = useRecord();
const { getInterface } = useCollectionManager(); const { getInterface } = useCollectionManager();
@ -79,7 +114,7 @@ export const EditFieldAction = (props) => {
> >
{t('Edit')} {t('Edit')}
</a> </a>
<SchemaComponent schema={schema} /> <SchemaComponent schema={schema} components={{ ArrayTable }} scope={{ useUpdateCollectionField }} />
</ActionContext.Provider> </ActionContext.Provider>
); );
}; };

View File

@ -1,6 +1,7 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { useAPIClient } from '../../api-client'; import { useAPIClient } from '../../api-client';
import { useRecord } from '../../record-provider'; import { useRecord } from '../../record-provider';
import { useCompile } from '../../schema-component';
import { CollectionFieldContext } from '../context'; import { CollectionFieldContext } from '../context';
import { useCollection } from './useCollection'; import { useCollection } from './useCollection';
@ -8,6 +9,7 @@ export const useCollectionField = () => {
const collection = useCollection(); const collection = useCollection();
const record = useRecord(); const record = useRecord();
const api = useAPIClient(); const api = useAPIClient();
const compile = useCompile();
const ctx = useContext(CollectionFieldContext); const ctx = useContext(CollectionFieldContext);
if (!ctx) { if (!ctx) {
return {} as any; return {} as any;
@ -16,6 +18,7 @@ export const useCollectionField = () => {
const resource = api?.resource(resourceName, record[ctx.sourceKey]); const resource = api?.resource(resourceName, record[ctx.sourceKey]);
return { return {
...ctx, ...ctx,
uiSchema: compile(ctx.uiSchema),
resource, resource,
}; };
}; };

View File

@ -16,8 +16,9 @@ export const attachment: IField = {
type: 'array', type: 'array',
// title, // title,
'x-component': 'Upload.Attachment', 'x-component': 'Upload.Attachment',
'x-decorator': 'FormItem', 'x-component-props': {
'x-designable-bar': 'Upload.DesignableBar', action: 'attachments:upload',
},
}, },
}, },
initialize: (values: any) => { initialize: (values: any) => {

View File

@ -20,6 +20,8 @@ export const chinaRegion: IField = {
// title, // title,
'x-component': 'Cascader', 'x-component': 'Cascader',
'x-component-props': { 'x-component-props': {
useDataSource: '{{ useChinaRegionDataSource }}',
useLoadData: '{{ useChinaRegionLoadData }}',
changeOnSelectLast: false, changeOnSelectLast: false,
labelInValue: true, labelInValue: true,
maxLevel: 3, maxLevel: 3,

View File

@ -15,12 +15,12 @@ export const linkTo: IField = {
uiSchema: { uiSchema: {
type: 'array', type: 'array',
// title, // title,
'x-component': 'Select.Drawer', 'x-component': 'RecordPicker',
'x-component-props': {}, 'x-component-props': {},
}, },
}, },
initialize: (values: any) => { initialize: (values: any) => {
if (values.dataType === 'belongsToMany') { if (values.type === 'belongsToMany') {
if (!values.through) { if (!values.through) {
values.through = `t_${uid()}`; values.through = `t_${uid()}`;
} }
@ -34,11 +34,7 @@ export const linkTo: IField = {
values.sourceKey = 'id'; values.sourceKey = 'id';
} }
if (!values.targetKey) { if (!values.targetKey) {
if (values.target === 'roles') { values.targetKey = 'id';
values.targetKey = 'name';
} else {
values.targetKey = 'id';
}
} }
} }
}, },

View File

@ -15,7 +15,7 @@ export const updatedBy: IField = {
uiSchema: { uiSchema: {
type: 'object', type: 'object',
title: '{{t("Last updated by")}}', title: '{{t("Last updated by")}}',
'x-component': 'Select.Drawer', 'x-component': 'RecordPicker',
'x-component-props': { 'x-component-props': {
fieldNames: { fieldNames: {
value: 'id', value: 'id',

View File

@ -7,6 +7,7 @@ export * from './api-client';
export * from './application'; export * from './application';
export * from './async-data-provider'; export * from './async-data-provider';
export * from './board'; export * from './board';
export * from './china-region';
export * from './collection-manager'; export * from './collection-manager';
export * from './document-title'; export * from './document-title';
export * from './i18n'; export * from './i18n';

View File

@ -131,7 +131,7 @@
"Created at": "创建日期", "Created at": "创建日期",
"Last updated at": "最后修改日期", "Last updated at": "最后修改日期",
"Created by": "创建人", "Created by": "创建人",
"Last updated by": "最后更新日期", "Last updated by": "最后修改人",
"Add field": "添加字段", "Add field": "添加字段",
"Field display name": "字段名称", "Field display name": "字段名称",
"Field type": "字段类型", "Field type": "字段类型",

View File

@ -5,10 +5,18 @@ import { toArr } from '@formily/shared';
import { Cascader as AntdCascader } from 'antd'; import { Cascader as AntdCascader } from 'antd';
import { isBoolean, omit } from 'lodash'; import { isBoolean, omit } from 'lodash';
import React from 'react'; import React from 'react';
import { useRequest } from '../../../api-client';
import { defaultFieldNames } from './defaultFieldNames'; import { defaultFieldNames } from './defaultFieldNames';
import { ReadPretty } from './ReadPretty'; import { ReadPretty } from './ReadPretty';
const useDefLoadData = (props: any) => props.loadData; const useDefDataSource = (options) => {
const field = useField<ArrayField>();
return useRequest(() => Promise.resolve({ data: field.dataSource || [] }), options);
};
const useDefLoadData = (props: any) => {
return props?.loadData;
};
export const Cascader = connect( export const Cascader = connect(
(props: any) => { (props: any) => {
@ -18,6 +26,7 @@ export const Cascader = connect(
onChange, onChange,
labelInValue, labelInValue,
fieldNames = defaultFieldNames, fieldNames = defaultFieldNames,
useDataSource = useDefDataSource,
useLoadData = useDefLoadData, useLoadData = useDefLoadData,
changeOnSelectLast, changeOnSelectLast,
changeOnSelect, changeOnSelect,
@ -25,6 +34,11 @@ export const Cascader = connect(
...others ...others
} = props; } = props;
const loadData = useLoadData(props); const loadData = useLoadData(props);
const { loading } = useDataSource({
onSuccess(data) {
field.dataSource = data?.data || [];
},
});
// 兼容值为 object[] 的情况 // 兼容值为 object[] 的情况
const toValue = () => { const toValue = () => {
return toArr(value).map((item) => { return toArr(value).map((item) => {
@ -54,17 +68,11 @@ export const Cascader = connect(
return <span key={option[fieldNames.value]}>{option[fieldNames.label]} / </span>; return <span key={option[fieldNames.value]}>{option[fieldNames.label]} / </span>;
}); });
}; };
// if (loadData) {
// Object.assign(others, {
// loadData: (selectedOptions: any[]) => {
// // 将 field 传给 loadData
// loadData(selectedOptions, field);
// },
// });
// }
return ( return (
<AntdCascader <AntdCascader
loading={loading}
{...others} {...others}
options={field.dataSource}
loadData={loadData} loadData={loadData}
changeOnSelect={isBoolean(changeOnSelectLast) ? !changeOnSelectLast : changeOnSelect} changeOnSelect={isBoolean(changeOnSelectLast) ? !changeOnSelectLast : changeOnSelect}
value={toValue()} value={toValue()}