feat: details block (#302)

This commit is contained in:
chenos 2022-04-20 15:49:01 +08:00 committed by GitHub
parent 111b9e67b0
commit 1f12c20838
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 417 additions and 13 deletions

2
.gitignore vendored
View File

@ -8,7 +8,7 @@ yarn-error.log
lerna-debug.log
/.vscode
/.idea
db.sqlite
*.sqlite
coverage
.umi
/uploads

View File

@ -2,12 +2,14 @@ import React from 'react';
import { SchemaComponentOptions } from '../schema-component/core/SchemaComponentOptions';
import { RecordLink, useParamsFromRecord, useSourceIdFromParentRecord, useSourceIdFromRecord } from './BlockProvider';
import { CalendarBlockProvider, useCalendarBlockProps } from './CalendarBlockProvider';
import { DetailsBlockProvider, useDetailsBlockProps } from './DetailsBlockProvider';
import { FormBlockProvider, useFormBlockProps } from './FormBlockProvider';
import * as bp from './hooks';
import { KanbanBlockProvider, useKanbanBlockProps } from './KanbanBlockProvider';
import { TableBlockProvider, useTableBlockProps } from './TableBlockProvider';
import { TableFieldProvider, useTableFieldProps } from './TableFieldProvider';
import { TableSelectorProvider, useTableSelectorProps } from './TableSelectorProvider';
export const BlockSchemaComponentProvider: React.FC = (props) => {
return (
<SchemaComponentOptions
@ -17,6 +19,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
TableBlockProvider,
TableSelectorProvider,
FormBlockProvider,
DetailsBlockProvider,
KanbanBlockProvider,
RecordLink,
}}
@ -27,6 +30,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
useParamsFromRecord,
useCalendarBlockProps,
useFormBlockProps,
useDetailsBlockProps,
useTableFieldProps,
useTableBlockProps,
useTableSelectorProps,

View File

@ -0,0 +1,62 @@
import { createForm } from '@formily/core';
import { useField } from '@formily/react';
import { Spin } from 'antd';
import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { RecordProvider } from '../record-provider';
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
export const DetailsBlockContext = createContext<any>({});
const InternalDetailsBlockProvider = (props) => {
const { action, readPretty } = props;
const field = useField<any>();
const form = useMemo(
() =>
createForm({
readPretty,
}),
[],
);
const { resource, service } = useBlockRequestContext();
if (service.loading && !field.loaded) {
return <Spin />;
}
field.loaded = true;
return (
<DetailsBlockContext.Provider
value={{
action,
form,
field,
service,
resource,
}}
>
<RecordProvider record={service?.data?.data?.[0] || {}}>{props.children}</RecordProvider>
</DetailsBlockContext.Provider>
);
};
export const DetailsBlockProvider = (props) => {
return (
<BlockProvider {...props}>
<InternalDetailsBlockProvider {...props} />
</BlockProvider>
);
};
export const useDetailsBlockContext = () => {
return useContext(DetailsBlockContext);
};
export const useDetailsBlockProps = () => {
const ctx = useDetailsBlockContext();
useEffect(() => {
if (!ctx.service.loading) {
ctx.form.setValues(ctx.service?.data?.data?.[0] || {});
}
}, [ctx.service.loading]);
return {
form: ctx.form,
};
};

View File

@ -20,15 +20,16 @@ const useGroupField = (props) => {
};
const InternalKanbanBlockProvider = (props) => {
const field = useField();
const field = useField<any>();
const { resource, service } = useBlockRequestContext();
const groupField = useGroupField(props);
if (!groupField) {
return null;
}
if (service.loading) {
if (service.loading && !field.loaded) {
return <Spin />;
}
field.loaded = true;
return (
<KanbanBlockContext.Provider
value={{

View File

@ -5,6 +5,7 @@ import { useHistory } from 'react-router-dom';
import { useCollection } from '../../collection-manager';
import { useActionContext } from '../../schema-component';
import { useBlockRequestContext, useFilterByTk } from '../BlockProvider';
import { useDetailsBlockContext } from '../DetailsBlockProvider';
import { TableFieldResource } from '../TableFieldProvider';
export const usePickActionProps = () => {
@ -207,3 +208,23 @@ export const useBulkDestroyActionProps = () => {
},
};
};
export const useDetailsPaginationProps = () => {
const ctx = useDetailsBlockContext();
const count = ctx.service?.data?.meta?.count || 0;
return {
simple: true,
hidden: count <= 1,
current: ctx.service?.data?.meta?.page || 1,
total: count,
pageSize: 1,
async onChange(page) {
const params = ctx.service?.params?.[0];
ctx.service.run({ ...params, page });
},
style: {
marginTop: 24,
textAlign: 'center',
},
};
};

View File

@ -81,7 +81,8 @@ export const useSortFields = (collectionName: string) => {
export const useCollectionFilterOptions = (collectionName: string) => {
const { getCollectionFields, getInterface } = useCollectionManager();
const fields = getCollectionFields(collectionName);
const field2option = (field, nochildren) => {
let depth = 0;
const field2option = (field) => {
if (!field.interface) {
return;
}
@ -94,9 +95,15 @@ export const useCollectionFilterOptions = (collectionName: string) => {
name: field.name,
title: field?.uiSchema?.title || field.name,
schema: field?.uiSchema,
operators: operators || [],
operators:
operators?.filter?.((operator) => {
return !operator?.visible || operator.visible(field);
}) || [],
};
if (nochildren) {
if (field.target && depth > 2) {
return;
}
if (depth > 2) {
return option;
}
if (children?.length) {
@ -104,16 +111,17 @@ export const useCollectionFilterOptions = (collectionName: string) => {
}
if (nested) {
const targetFields = getCollectionFields(field.target);
const options = getOptions(targetFields, true);
const options = getOptions(targetFields).filter(Boolean);
option['children'] = option['children'] || [];
option['children'].push(...options);
}
return option;
};
const getOptions = (fields, nochildren = false) => {
const getOptions = (fields) => {
++depth;
const options = [];
fields.forEach((field) => {
const option = field2option(field, nochildren);
const option = field2option(field);
if (option) {
options.push(option);
}

View File

@ -1,8 +1,11 @@
import { useField, useFieldSchema } from '@formily/react';
import { ArrayItems } from '@formily/antd';
import { ISchema, useField, useFieldSchema } from '@formily/react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useFormBlockContext } from '../../../block-provider';
import { useDetailsBlockContext } from '../../../block-provider/DetailsBlockProvider';
import { useCollection } from '../../../collection-manager';
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates';
import { useDesignable } from '../../hooks';
@ -53,3 +56,162 @@ export const ReadPrettyFormDesigner = () => {
</GeneralSchemaDesigner>
);
};
export const DetailsDesigner = () => {
const { name, title } = useCollection();
const template = useSchemaTemplate();
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const field = useField();
const dataSource = useCollectionFilterOptions(name);
const { service } = useDetailsBlockContext();
const { dn } = useDesignable();
const sortFields = useSortFields(name);
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const sort = defaultSort?.map((item: string) => {
return item.startsWith('-')
? {
field: item.substring(1),
direction: 'desc',
}
: {
field: item,
direction: 'asc',
};
});
return (
<GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: defaultFilter,
// title: '数据范围',
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ filter }) => {
const params = field.decoratorProps.params || {};
params.filter = filter;
field.decoratorProps.params = params;
fieldSchema['x-decorator-props']['params'] = params;
service.run({ ...service.params?.[0], filter });
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
}}
/>
<SchemaSettings.ModalItem
title={t('Set default sorting rules')}
components={{ ArrayItems }}
schema={
{
type: 'object',
title: t('Set default sorting rules'),
properties: {
sort: {
type: 'array',
default: sort,
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
properties: {
sort: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.SortHandle',
},
field: {
type: 'string',
enum: sortFields,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
style: {
width: 260,
},
},
},
direction: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {
optionType: 'button',
},
enum: [
{
label: t('ASC'),
value: 'asc',
},
{
label: t('DESC'),
value: 'desc',
},
],
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: t('Add sort field'),
'x-component': 'ArrayItems.Addition',
},
},
},
},
} as ISchema
}
onSubmit={({ sort }) => {
const sortArr = sort.map((item) => {
return item.direction === 'desc' ? `-${item.field}` : item.field;
});
const params = field.decoratorProps.params || {};
params.sort = sortArr;
field.decoratorProps.params = params;
fieldSchema['x-decorator-props']['params'] = params;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
service.run({ ...service.params?.[0], sort: sortArr });
}}
/>
<SchemaSettings.Template componentName={'Details'} collectionName={name} />
<SchemaSettings.Divider />
<SchemaSettings.Remove
removeParentsIfNoChildren
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</GeneralSchemaDesigner>
);
};

View File

@ -1,7 +1,7 @@
import { Form as FormV2 } from './Form';
import { FormDesigner, ReadPrettyFormDesigner } from './Form.Designer';
import { DetailsDesigner, FormDesigner, ReadPrettyFormDesigner } from './Form.Designer';
FormV2.Designer = FormDesigner;
FormV2.ReadPrettyDesigner = ReadPrettyFormDesigner;
export { FormV2 };
export { FormV2, DetailsDesigner };

View File

@ -22,6 +22,7 @@ export * from './kanban-v2';
export * from './markdown';
export * from './menu';
export * from './page';
export * from './pagination';
export * from './password';
export * from './radio';
export * from './record-picker';
@ -36,3 +37,4 @@ export * from './tree-select';
export * from './upload';
import './index.less';

View File

@ -0,0 +1,12 @@
import { observer } from '@formily/react';
import { Pagination as AntdPagination } from 'antd';
import React from 'react';
import { useProps } from '../../hooks/useProps';
export const Pagination = observer((props: any) => {
const { hidden, ...others } = useProps(props);
if (hidden) {
return null;
}
return <AntdPagination {...others} />;
});

View File

@ -23,6 +23,12 @@ export const BlockInitializers = {
title: 'Form',
component: 'FormBlockInitializer',
},
{
key: 'details',
type: 'item',
title: 'Details',
component: 'DetailsBlockInitializer',
},
{
key: 'calendar',
type: 'item',

View File

@ -0,0 +1,37 @@
// 表单的操作配置
export const DetailsActionInitializers = {
title: '{{t("Configure actions")}}',
icon: 'SettingOutlined',
style: {
marginLeft: 8,
},
items: [
{
type: 'itemGroup',
title: '{{t("Enable actions")}}',
children: [
{
type: 'item',
title: '{{t("Edit")}}',
component: 'UpdateActionInitializer',
schema: {
'x-component': 'Action',
'x-decorator': 'ACLActionProvider',
'x-component-props': {
type: 'primary',
},
},
},
{
type: 'item',
title: '{{t("Delete")}}',
component: 'DestroyActionInitializer',
schema: {
'x-component': 'Action',
'x-decorator': 'ACLActionProvider',
},
},
],
},
],
};

View File

@ -6,7 +6,7 @@ import { gridRowColWrap } from '../utils';
const useRelationFields = () => {
const { fields } = useCollection();
return fields
.filter((field) => field.interface === 'linkTo')
.filter((field) => ['linkTo', 'subTable'].includes(field.interface))
.map((field) => {
return {
key: field.name,

View File

@ -1,6 +1,7 @@
export * from './BlockInitializers';
export * from './CalendarActionInitializers';
export * from './CreateFormBlockInitializers';
export * from './DetailsActionInitializers';
export * from './FormActionInitializers';
export * from './FormItemInitializers';
export * from './ReadPrettyFormActionInitializers';

View File

@ -12,6 +12,7 @@ import { useSchemaTemplateManager } from '../../schema-templates';
import { SchemaInitializer } from '../SchemaInitializer';
import {
createCalendarBlockSchema,
createDetailsBlockSchema,
createFormBlockSchema,
createKanbanBlockSchema,
createReadPrettyFormBlockSchema,
@ -125,6 +126,23 @@ export const FormBlockInitializer = (props) => {
);
};
export const DetailsBlockInitializer = (props) => {
const { insert } = props;
const { getCollection } = useCollectionManager();
return (
<DataBlockInitializer
{...props}
icon={<TableOutlined />}
componentType={'Details'}
onCreateBlockSchema={async ({ item }) => {
const collection = getCollection(item.name);
const schema = createDetailsBlockSchema({ collection: item.name, rowKey: collection.filterTargetKey || 'id' });
insert(schema);
}}
/>
);
};
export const CalendarBlockInitializer = (props) => {
const { insert } = props;
const { getCollection } = useCollectionManager();

View File

@ -283,6 +283,76 @@ export const useCollectionDataSourceItems = (componentName) => {
];
};
export const createDetailsBlockSchema = (options) => {
const {
formItemInitializers = 'ReadPrettyFormItemInitializers',
actionInitializers = 'DetailsActionInitializers',
collection,
association,
resource,
template,
...others
} = options;
const resourceName = resource || association || collection;
const schema: ISchema = {
type: 'void',
'x-acl-action': `${resourceName}:get`,
'x-decorator': 'DetailsBlockProvider',
'x-decorator-props': {
resource: resourceName,
collection,
association,
readPretty: true,
action: 'list',
params: {
pageSize: 1,
},
// useParams: '{{ useParamsFromRecord }}',
...others,
},
'x-designer': 'DetailsDesigner',
'x-component': 'CardItem',
properties: {
[uid()]: {
type: 'void',
'x-component': 'FormV2',
'x-read-pretty': true,
'x-component-props': {
useProps: '{{ useDetailsBlockProps }}',
},
properties: {
actions: {
type: 'void',
'x-initializer': actionInitializers,
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 24,
},
},
properties: {},
},
grid: template || {
type: 'void',
'x-component': 'Grid',
'x-initializer': formItemInitializers,
properties: {},
},
pagination: {
type: 'void',
'x-component': 'Pagination',
'x-component-props': {
useProps: '{{ useDetailsPaginationProps }}',
},
},
},
},
},
};
console.log(JSON.stringify(schema, null, 2));
return schema;
};
export const createFormBlockSchema = (options) => {
const {
formItemInitializers = 'FormItemInitializers',