feat: improve sub table

This commit is contained in:
chenos 2022-02-25 00:08:32 +08:00
parent f5b2600640
commit 6d0978f2c2
15 changed files with 702 additions and 45 deletions

View File

@ -1,8 +1,8 @@
import React, { createContext } from 'react';
import { I18nextProvider, useTranslation } from 'react-i18next';
import { ConfigProvider, Spin } from 'antd';
import enUS from 'antd/lib/locale/en_US';
import zhCN from 'antd/lib/locale/zh_CN';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useRequest } from '../api-client';
export function AntdConfigProvider(props) {
@ -19,7 +19,7 @@ export function AntdConfigProvider(props) {
i18n.changeLanguage(data?.data?.lang);
}
},
manual: !remoteLocale,
manual: true, // !remoteLocale,
},
);
if (loading) {

View File

@ -2,7 +2,7 @@ import { Result } from 'ahooks/lib/useRequest/src/types';
import React, { createContext, useContext } from 'react';
import { useRequest } from '../api-client';
export const AsyncDataContext = createContext<Result<any, any>>(null);
export const AsyncDataContext = createContext<Result<any, any> & { state?: any; setState?: any }>(null);
export interface AsyncDataProviderProps {
value?: any;

View File

@ -0,0 +1,138 @@
import { PlusOutlined } from '@ant-design/icons';
import { ArrayTable } from '@formily/antd';
import { ISchema } from '@formily/react';
import { uid } from '@formily/shared';
import { Button, Dropdown, Menu } from 'antd';
import { cloneDeep } from 'lodash';
import React, { useState } from 'react';
import { useRequest } from '../../api-client';
import { ActionContext, SchemaComponent, useActionContext, useCompile, useFormBlockContext } from '../../schema-component';
import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types';
import { options } from './interfaces';
const getSchema = (schema: IField): ISchema => {
if (!schema) {
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 {
type: 'object',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Action.Drawer',
'x-decorator': 'Form',
'x-decorator-props': {
useValues(options) {
return useRequest(
() =>
Promise.resolve({
data: initialValue,
}),
options,
);
},
},
title: '{{ t("Add field") }}',
properties: {
// @ts-ignore
...properties,
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
action1: {
title: '{{ t("Cancel") }}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ cm.useCancelAction }}',
},
},
action2: {
title: '{{ t("Submit") }}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ useCreateSubField }}',
},
},
},
},
},
},
},
};
};
const useCreateSubField = () => {
// const form = useForm();
const { form, parent } = useFormBlockContext();
const ctx = useActionContext();
return {
async run() {
await form.submit();
const children = parent.form.values?.children?.slice?.();
children.push(cloneDeep(form.values));
console.log('form.values', form.values, children);
parent.form.setValuesIn('children', children);
ctx.setVisible(false);
// const options = form?.values?.uiSchema?.enum?.slice() || [];
// form.setValuesIn(
// 'uiSchema.enum',
// options.map((option) => {
// return {
// value: uid(),
// ...option,
// };
// }),
// );
// await run();
// await refreshCM();
},
};
};
export const AddSubFieldAction = () => {
const { getInterface } = useCollectionManager();
const [visible, setVisible] = useState(false);
const [schema, setSchema] = useState({});
const compile = useCompile();
return (
<ActionContext.Provider value={{ visible, setVisible }}>
<Dropdown
overlay={
<Menu
onClick={(info) => {
const schema = getSchema(getInterface(info.key));
setSchema(schema);
setVisible(true);
}}
>
{options.map((option) => {
return (
<Menu.SubMenu title={compile(option.label)}>
{option.children.map((child) => {
return <Menu.Item key={child.name}>{compile(child.title)}</Menu.Item>;
})}
</Menu.SubMenu>
);
})}
</Menu>
}
>
<Button icon={<PlusOutlined />} type={'primary'}>
</Button>
</Dropdown>
<SchemaComponent schema={schema} components={{ ArrayTable }} scope={{ useCreateSubField }} />
</ActionContext.Provider>
);
};

View File

@ -1,9 +1,13 @@
import { useForm } from '@formily/react';
import { action } from '@formily/reactive';
import { uid } from '@formily/shared';
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useRequest } from '../../api-client';
import { useRecord } from '../../record-provider';
import { SchemaComponent, useActionContext } from '../../schema-component';
import { useCollectionManager } from '../hooks/useCollectionManager';
import { AddSubFieldAction } from './AddSubFieldAction';
import { EditSubFieldAction } from './EditSubFieldAction';
import { collectionSchema } from './schemas/collections';
const useAsyncDataSource = (service: any) => (field: any) => {
@ -42,6 +46,33 @@ const useCollectionValues = (options) => {
return result;
};
const useSelectedRowKeys = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
return [selectedRowKeys, setSelectedRowKeys];
};
const useDestroySubField = () => {
const record = useRecord();
const form = useForm();
return {
async run() {
const children = form.values?.children?.slice?.();
form.setValuesIn(
'children',
children.filter((child) => {
return child.name !== record.name;
}),
);
},
};
};
const useBulkDestroySubField = () => {
return {
async run() {},
};
};
export const ConfigurationTable = () => {
const { collections = [] } = useCollectionManager();
const loadCollections = async (field: any) => {
@ -52,7 +83,21 @@ export const ConfigurationTable = () => {
};
return (
<div>
<SchemaComponent schema={collectionSchema} scope={{ useCollectionValues, useAsyncDataSource, loadCollections }} />
<SchemaComponent
schema={collectionSchema}
components={{
AddSubFieldAction,
EditSubFieldAction,
}}
scope={{
useDestroySubField,
useBulkDestroySubField,
useSelectedRowKeys,
useCollectionValues,
useAsyncDataSource,
loadCollections,
}}
/>
</div>
);
};

View File

@ -102,7 +102,7 @@ export const EditFieldAction = (props) => {
onClick={async () => {
const { data } = await api.resource('collections.fields', record.collectionName).get({
filterByTk: record.name,
appends: ['uiSchema'],
appends: record.interface === 'subTable' ? ['uiSchema', 'children'] : ['uiSchema'],
});
const schema = getSchema({
...getInterface(record.interface),

View File

@ -0,0 +1,119 @@
import { ArrayTable } from '@formily/antd';
import { ISchema } from '@formily/react';
import { uid } from '@formily/shared';
import cloneDeep from 'lodash/cloneDeep';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAPIClient, useRequest } from '../../api-client';
import { useRecord } from '../../record-provider';
import { ActionContext, SchemaComponent, useActionContext, useFormBlockContext } from '../../schema-component';
import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types';
const getSchema = (schema: IField): ISchema => {
if (!schema) {
return;
}
const properties = cloneDeep(schema.properties) as any;
properties.name['x-disabled'] = true;
return {
type: 'object',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Action.Drawer',
'x-decorator': 'Form',
'x-decorator-props': {
useValues(options) {
return useRequest(
() =>
Promise.resolve({
data: cloneDeep(schema.default),
}),
options,
);
},
},
title: '{{ t("Edit field") }}',
properties: {
// @ts-ignore
...properties,
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
action1: {
title: '{{ t("Cancel") }}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ cm.useCancelAction }}',
},
},
action2: {
title: '{{ t("Submit") }}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ useUpdateSubField }}',
},
},
},
},
},
},
},
};
};
const useUpdateSubField = () => {
const { form, parent } = useFormBlockContext();
const ctx = useActionContext();
return {
async run() {
await form.submit();
const children = parent.form.values?.children?.slice?.();
parent.form.setValuesIn(
'children',
children.map((child) => {
if (child.name === form.values.name) {
return cloneDeep(form.values);
}
return child;
}),
);
ctx.setVisible(false);
},
};
};
export const EditSubFieldAction = (props) => {
const record = useRecord();
const { getInterface } = useCollectionManager();
const [visible, setVisible] = useState(false);
const [schema, setSchema] = useState({});
const api = useAPIClient();
const { t } = useTranslation();
return (
<ActionContext.Provider value={{ visible, setVisible }}>
<a
onClick={async () => {
// const { data } = await api.resource('fields.children', record.key).get({
// filterByTk: record.name,
// appends: ['uiSchema'],
// });
const schema = getSchema({
...getInterface(record.interface),
default: {
...record,
},
});
setSchema(schema);
setVisible(true);
}}
>
{t('Edit')}
</a>
<SchemaComponent schema={schema} components={{ ArrayTable }} scope={{ useUpdateSubField }} />
</ActionContext.Provider>
);
};

View File

@ -31,6 +31,132 @@ export const subTable: IField = {
},
properties: {
...defaultProps,
subtable: {
type: 'void',
'x-component': 'div',
properties: {
actions: {
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 16,
},
},
properties: {
delete: {
type: 'void',
title: '{{ t("Delete") }}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useBulkDestroySubField }}',
confirm: {
title: "{{t('Delete record')}}",
content: "{{t('Are you sure you want to delete it?')}}",
},
},
},
create: {
type: 'void',
title: '{{ t("Add new") }}',
'x-component': 'AddSubFieldAction',
'x-component-props': {
type: 'primary',
},
},
},
},
children: {
type: 'array',
title: '{{t("Fields")}}',
'x-decorator': 'FormItem',
'x-component': 'Table.Array',
'x-component-props': {
pagination: false,
rowKey: 'name',
rowSelection: {
type: 'checkbox',
},
useSelectedRowKeys: '{{ useSelectedRowKeys }}',
// scroll: { x: '100%' },
},
properties: {
column1: {
type: 'void',
title: '{{ t("Field display name") }}',
'x-component': 'Table.Column',
properties: {
'uiSchema.title': {
type: 'number',
'x-component': 'Input',
'x-read-pretty': true,
},
},
},
column2: {
type: 'void',
title: '{{ t("Field name") }}',
'x-decorator': 'Table.Column.Decorator',
'x-component': 'Table.Column',
properties: {
name: {
'x-component': 'Input',
'x-read-pretty': true,
},
},
},
column3: {
type: 'void',
title: '{{ t("Field interface") }}',
'x-decorator': 'Table.Column.Decorator',
'x-component': 'Table.Column',
properties: {
interface: {
'x-component': 'Input',
'x-read-pretty': true,
},
},
},
column4: {
type: 'void',
title: '{{ t("Actions") }}',
'x-component': 'Table.Column',
properties: {
actions: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
properties: {
update: {
type: 'void',
title: '{{ t("Edit") }}',
'x-component': 'EditSubFieldAction',
'x-component-props': {
type: 'primary',
},
},
delete: {
type: 'void',
title: '{{ t("Delete") }}',
'x-component': 'Action.Link',
'x-component-props': {
useAction: '{{ useDestroySubField }}',
confirm: {
title: "{{t('Delete record')}}",
content: "{{t('Are you sure you want to delete it?')}}",
},
},
},
},
},
},
},
},
},
},
},
// children: {
// type: 'array',
// title: '{{t("Sub-table fields")}}',

View File

@ -104,6 +104,7 @@
"Edit": "编辑",
"Edit collection": "编辑数据表",
"Configure fields": "配置字段",
"Configure columns": "配置字段",
"Edit field": "编辑字段",
"Configure fields of {{title}}": "「{{title}}」的字段配置",
"Basic": "基本类型",

View File

@ -3,7 +3,7 @@ import { createForm } from '@formily/core';
import { FieldContext, FormContext, observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { Options, Result } from 'ahooks/lib/useRequest/src/types';
import { Spin } from 'antd';
import React, { useMemo } from 'react';
import React, { createContext, useContext, useMemo } from 'react';
import { useAttach, useComponent } from '../..';
import { useRequest } from '../../../api-client';
import { useCollection } from '../../../collection-manager';
@ -75,9 +75,17 @@ const useDef = (opts: any = {}, props: FormProps = {}) => {
return useRequest(useRequestProps(props), opts);
};
const FormBlockContext = createContext<any>(null);
export const useFormBlockContext = () => {
const ctx = useContext(FormBlockContext);
return ctx;
};
export const Form: React.FC<FormProps> & { Designer?: any } = observer((props) => {
const { request, effects, initialValue, useValues = useDef, ...others } = props;
const fieldSchema = useFieldSchema();
const field = useField();
const form = useMemo(() => createForm({ effects }), []);
const result = useValues(
{
@ -89,14 +97,17 @@ export const Form: React.FC<FormProps> & { Designer?: any } = observer((props) =
},
props,
);
const parent = useContext(FormBlockContext);
return (
<Spin spinning={result?.loading || false}>
{fieldSchema['x-decorator'] === 'Form' ? (
<FormDecorator form={form} {...others} />
) : (
<FormComponent form={form} {...others} />
)}
</Spin>
<FormBlockContext.Provider value={{ parent, form, result, field, fieldSchema }}>
<Spin spinning={result?.loading || false}>
{fieldSchema['x-decorator'] === 'Form' ? (
<FormDecorator form={form} {...others} />
) : (
<FormComponent form={form} {...others} />
)}
</Spin>
</FormBlockContext.Provider>
);
});

View File

@ -1,11 +1,11 @@
import { css } from '@emotion/css';
import { ArrayField } from '@formily/core';
import { ArrayField, Field } from '@formily/core';
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
import { Table, TableColumnProps } from 'antd';
import cls from 'classnames';
import React from 'react';
import React, { useState } from 'react';
import { DndContext } from '../..';
import { RecordProvider, useSchemaInitializer } from '../../../';
import { RecordProvider, useRequest, useSchemaInitializer } from '../../../';
const isColumnComponent = (schema: Schema) => {
return schema['x-component']?.endsWith('.Column') > -1;
@ -83,13 +83,45 @@ export const components = {
},
};
const useDef = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
return [selectedRowKeys, setSelectedRowKeys];
};
const useDefDataSource = (options, props) => {
const field = useField<Field>();
return useRequest(() => {
return Promise.resolve({
data: field.value,
});
}, options);
};
export const TableArray: React.FC<any> = observer((props) => {
const field = useField<ArrayField>();
const columns = useTableColumns();
const { onChange, ...others } = props;
const { useSelectedRowKeys = useDef, useDataSource = useDefDataSource, onChange, ...others } = props;
const [selectedRowKeys, setSelectedRowKeys] = useSelectedRowKeys();
useDataSource({
onSuccess(data) {
field.value = data?.data || [];
},
});
const restProps = {
rowSelection: props.rowSelection
? {
type: 'checkbox',
selectedRowKeys,
onChange(selectedRowKeys: any[]) {
setSelectedRowKeys(selectedRowKeys);
},
...props.rowSelection,
}
: undefined,
};
return (
<div>
<Table {...others} components={components} columns={columns} dataSource={field.value?.slice()} />
<Table {...others} {...restProps} components={components} columns={columns} dataSource={field.value?.slice()} />
</div>
);
});

View File

@ -1,9 +1,11 @@
import { useField, useFieldSchema } from '@formily/react';
import React, { useLayoutEffect } from 'react';
import { designerCss, SortableItem, useCollection, useDesignable, useDesigner } from '../../../';
import { SortableItem, useCollection, useCompile, useDesignable, useDesigner } from '../../../';
import { designerCss } from './Table.Column.ActionBar';
export const useColumnSchema = () => {
const { getField } = useCollection();
const compile = useCompile();
const columnSchema = useFieldSchema();
const fieldSchema = columnSchema.reduceProperties((buf, s) => {
if (s['x-component'] === 'CollectionField') {
@ -15,14 +17,15 @@ export const useColumnSchema = () => {
return {};
}
const collectionField = getField(fieldSchema.name);
return { columnSchema, fieldSchema, collectionField };
return { columnSchema, fieldSchema, uiSchema: compile(collectionField?.uiSchema) };
};
export const TableColumnDecorator = (props) => {
const Designer = useDesigner();
const field = useField();
const { fieldSchema, collectionField } = useColumnSchema();
const { fieldSchema, uiSchema } = useColumnSchema();
const { refresh } = useDesignable();
const compile = useCompile();
useLayoutEffect(() => {
if (field.title) {
return;
@ -30,16 +33,16 @@ export const TableColumnDecorator = (props) => {
if (!fieldSchema) {
return;
}
if (collectionField?.uiSchema?.title) {
field.title = collectionField?.uiSchema?.title;
if (uiSchema?.title) {
field.title = uiSchema?.title;
}
}, [collectionField?.uiSchema?.title]);
console.log('field.title', collectionField?.uiSchema?.title, field.title);
}, [uiSchema?.title]);
console.log('field.title', uiSchema?.title, field.title);
return (
<SortableItem className={designerCss}>
<Designer />
{/* <RecursionField name={columnSchema.name} schema={columnSchema}/> */}
{field.title || collectionField?.uiSchema?.title}
{field.title || compile(uiSchema?.title)}
{/* <div
onClick={() => {
field.title = uid();

View File

@ -4,12 +4,13 @@ import { Options, Result } from 'ahooks/lib/useRequest/src/types';
import { TablePaginationConfig, TableProps } from 'antd';
import { cloneDeep } from 'lodash';
import React, { useMemo } from 'react';
import { AsyncDataProvider, useRequest } from '../../../';
import { AsyncDataProvider, useAsyncData, useRequest } from '../../../';
import { useAttach } from '../../hooks';
import { TableArray } from './Table.Array';
type TableVoidProps = TableProps<any> & {
request?: any;
useSelectedRowKeys?: any;
useDataSource?: (
options?: Options<any, any> & { uid?: string },
props?: any,
@ -69,8 +70,13 @@ const useDef = (options, props) => {
return useRequest(useRequestProps(props), options);
};
const useDefSelectedRowKeys = () => {
const result = useAsyncData();
return [result?.state?.selectedRowKeys, (selectedRowKeys) => result?.setState?.({ selectedRowKeys })];
};
export const TableVoid: React.FC<TableVoidProps> = observer((props) => {
const { useDataSource = useDef } = props;
const { useDataSource = useDef, useSelectedRowKeys = useDefSelectedRowKeys } = props;
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const form = useMemo(() => createForm(), []);
@ -96,23 +102,16 @@ export const TableVoid: React.FC<TableVoidProps> = observer((props) => {
},
props,
);
const others = {
rowSelection: props.rowSelection
? {
type: 'checkbox',
...props.rowSelection,
selectedRowKeys: result?.state?.selectedRowKeys || [],
onChange(selectedRowKeys: any[]) {
result?.setState?.({ selectedRowKeys });
},
}
: undefined,
};
return (
<AsyncDataProvider value={result}>
<FormContext.Provider value={form}>
<FieldContext.Provider value={f}>
<TableArray {...props} {...others} loading={result?.loading} pagination={usePaginationProps(props, result)} />
<TableArray
{...props}
useSelectedRowKeys={useSelectedRowKeys}
loading={result?.loading}
pagination={usePaginationProps(props, result)}
/>
</FieldContext.Provider>
</FormContext.Provider>
</AsyncDataProvider>

View File

@ -0,0 +1,181 @@
import { ISchema, observer } from '@formily/react';
import { uid } from '@formily/shared';
import {
Action,
Input,
SchemaComponent,
SchemaComponentProvider,
Table,
useRecord,
useRequest
} from '@nocobase/client';
import React, { createContext, useContext, useState } from 'react';
const DataSourceContext = createContext(null);
const schema: ISchema = {
type: 'object',
properties: {
context: {
type: 'void',
'x-component': 'Context',
properties: {
action1: {
title: '提交',
'x-component': 'Action',
'x-component-props': {
useAction() {
const ctx = useContext(DataSourceContext);
return {
async run() {
console.log(ctx.dataSource);
const dataSource = ctx.dataSource || [];
dataSource.push({
id: uid(),
name: uid(),
});
ctx.setDataSource([...dataSource]);
},
};
},
},
},
action2: {
title: '删除',
'x-component': 'Action',
'x-component-props': {
useAction() {
const ctx = useContext(DataSourceContext);
const { selectedRowKeys, setSelectedRowKeys } = ctx;
return {
async run() {
const dataSource = ctx.dataSource || [];
ctx.setDataSource(
dataSource.filter((item) => {
return !selectedRowKeys.includes(item.id);
}),
);
setSelectedRowKeys([]);
},
};
},
},
},
input: {
type: 'array',
title: `编辑模式`,
'x-component': 'Table.Array',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
useSelectedRowKeys() {
const ctx = useContext(DataSourceContext);
return [ctx.selectedRowKeys, ctx.setSelectedRowKeys];
},
useDataSource(options) {
const ctx = useContext(DataSourceContext);
return useRequest(
() => {
console.log('ctx.dataSource', ctx.dataSource);
return Promise.resolve({
data: ctx.dataSource,
});
},
{
...options,
refreshDeps: [JSON.stringify(ctx.dataSource)],
},
);
},
},
properties: {
column1: {
type: 'void',
title: 'Name',
'x-component': 'Table.Column',
properties: {
name: {
type: 'string',
'x-component': 'Input',
'x-read-pretty': true,
},
},
},
column2: {
type: 'void',
title: 'Actions',
'x-component': 'Table.Column',
properties: {
action: {
title: '删除',
'x-component': 'Action',
'x-component-props': {
useAction() {
const record = useRecord();
const ctx = useContext(DataSourceContext);
return {
async run() {
const dataSource = ctx.dataSource || [];
ctx.setDataSource(
dataSource.filter((item) => {
return record.id !== item.id;
}),
);
},
};
},
},
},
},
},
},
},
},
},
},
};
const Context = observer((props) => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [dataSource, setDataSource] = useState([]);
const service = useRequest(
() => {
return Promise.resolve({
data: [
{ id: 1, name: 'Name1' },
{ id: 2, name: 'Name2' },
{ id: 3, name: 'Name3' },
],
});
},
{
onSuccess(data) {
setDataSource(data.data);
},
},
);
console.log('dataSource1', dataSource);
return (
<DataSourceContext.Provider
value={{
service,
dataSource,
setDataSource,
selectedRowKeys,
setSelectedRowKeys,
}}
>
{props.children}
</DataSourceContext.Provider>
);
});
export default () => {
return (
<SchemaComponentProvider components={{ Action, Context, Table, Input }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
};

View File

@ -25,3 +25,5 @@ group:
## Table.RowSelection
<code src="./demos/demo3.tsx" />
<code src="./demos/demo4.tsx" />

View File

@ -12,8 +12,8 @@ export const TableColumnInitializers = (props: any) => {
wrap={(s) => {
return {
type: 'void',
'x-decorator': 'TableColumnDecorator',
'x-designer': 'TableColumnDeigner',
'x-decorator': 'Table.Column.Decorator',
'x-designer': 'Table.Column.Deigner',
'x-component': 'Table.Column',
properties: {
[s.name]: {