From 1f12c208388eb097e209ef05a2a4ce673545c0c2 Mon Sep 17 00:00:00 2001 From: chenos Date: Wed, 20 Apr 2022 15:49:01 +0800 Subject: [PATCH] feat: details block (#302) --- .gitignore | 2 +- .../BlockSchemaComponentProvider.tsx | 4 + .../block-provider/DetailsBlockProvider.tsx | 62 +++++++ .../block-provider/KanbanBlockProvider.tsx | 5 +- .../client/src/block-provider/hooks/index.ts | 21 +++ .../src/collection-manager/action-hooks.ts | 20 ++- .../antd/form-v2/Form.Designer.tsx | 164 +++++++++++++++++- .../schema-component/antd/form-v2/index.ts | 4 +- .../client/src/schema-component/antd/index.ts | 2 + .../antd/pagination/index.tsx | 12 ++ .../buttons/BlockInitializers.tsx | 6 + .../buttons/DetailsActionInitializers.tsx | 37 ++++ .../buttons/RecordBlockInitializers.tsx | 2 +- .../src/schema-initializer/buttons/index.ts | 1 + .../src/schema-initializer/items/index.tsx | 18 ++ .../client/src/schema-initializer/utils.ts | 70 ++++++++ 16 files changed, 417 insertions(+), 13 deletions(-) create mode 100644 packages/core/client/src/block-provider/DetailsBlockProvider.tsx create mode 100644 packages/core/client/src/schema-component/antd/pagination/index.tsx create mode 100644 packages/core/client/src/schema-initializer/buttons/DetailsActionInitializers.tsx diff --git a/.gitignore b/.gitignore index 7c4ead619..a910d4795 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ yarn-error.log lerna-debug.log /.vscode /.idea -db.sqlite +*.sqlite coverage .umi /uploads diff --git a/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx b/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx index db175a408..942409534 100644 --- a/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx +++ b/packages/core/client/src/block-provider/BlockSchemaComponentProvider.tsx @@ -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 ( { TableBlockProvider, TableSelectorProvider, FormBlockProvider, + DetailsBlockProvider, KanbanBlockProvider, RecordLink, }} @@ -27,6 +30,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => { useParamsFromRecord, useCalendarBlockProps, useFormBlockProps, + useDetailsBlockProps, useTableFieldProps, useTableBlockProps, useTableSelectorProps, diff --git a/packages/core/client/src/block-provider/DetailsBlockProvider.tsx b/packages/core/client/src/block-provider/DetailsBlockProvider.tsx new file mode 100644 index 000000000..6367d70c6 --- /dev/null +++ b/packages/core/client/src/block-provider/DetailsBlockProvider.tsx @@ -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({}); + +const InternalDetailsBlockProvider = (props) => { + const { action, readPretty } = props; + const field = useField(); + const form = useMemo( + () => + createForm({ + readPretty, + }), + [], + ); + const { resource, service } = useBlockRequestContext(); + if (service.loading && !field.loaded) { + return ; + } + field.loaded = true; + return ( + + {props.children} + + ); +}; + +export const DetailsBlockProvider = (props) => { + return ( + + + + ); +}; + +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, + }; +}; diff --git a/packages/core/client/src/block-provider/KanbanBlockProvider.tsx b/packages/core/client/src/block-provider/KanbanBlockProvider.tsx index 7c33536be..2ac9c1376 100644 --- a/packages/core/client/src/block-provider/KanbanBlockProvider.tsx +++ b/packages/core/client/src/block-provider/KanbanBlockProvider.tsx @@ -20,15 +20,16 @@ const useGroupField = (props) => { }; const InternalKanbanBlockProvider = (props) => { - const field = useField(); + const field = useField(); const { resource, service } = useBlockRequestContext(); const groupField = useGroupField(props); if (!groupField) { return null; } - if (service.loading) { + if (service.loading && !field.loaded) { return ; } + field.loaded = true; return ( { @@ -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', + }, + }; +}; diff --git a/packages/core/client/src/collection-manager/action-hooks.ts b/packages/core/client/src/collection-manager/action-hooks.ts index b6452a2db..ff662320a 100644 --- a/packages/core/client/src/collection-manager/action-hooks.ts +++ b/packages/core/client/src/collection-manager/action-hooks.ts @@ -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); } diff --git a/packages/core/client/src/schema-component/antd/form-v2/Form.Designer.tsx b/packages/core/client/src/schema-component/antd/form-v2/Form.Designer.tsx index aaf69e203..65eb7c3f4 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Form.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/Form.Designer.tsx @@ -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 = () => { ); }; + +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 ( + + { + 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'], + }, + }); + }} + /> + { + 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 }); + }} + /> + + + + + ); +}; diff --git a/packages/core/client/src/schema-component/antd/form-v2/index.ts b/packages/core/client/src/schema-component/antd/form-v2/index.ts index 6803d189f..66195a9be 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/index.ts +++ b/packages/core/client/src/schema-component/antd/form-v2/index.ts @@ -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 }; diff --git a/packages/core/client/src/schema-component/antd/index.ts b/packages/core/client/src/schema-component/antd/index.ts index 0d42d53e4..5d37f3c78 100644 --- a/packages/core/client/src/schema-component/antd/index.ts +++ b/packages/core/client/src/schema-component/antd/index.ts @@ -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'; + diff --git a/packages/core/client/src/schema-component/antd/pagination/index.tsx b/packages/core/client/src/schema-component/antd/pagination/index.tsx new file mode 100644 index 000000000..674dedb26 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/pagination/index.tsx @@ -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 ; +}); diff --git a/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx index c47054039..095132f0d 100644 --- a/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx @@ -23,6 +23,12 @@ export const BlockInitializers = { title: 'Form', component: 'FormBlockInitializer', }, + { + key: 'details', + type: 'item', + title: 'Details', + component: 'DetailsBlockInitializer', + }, { key: 'calendar', type: 'item', diff --git a/packages/core/client/src/schema-initializer/buttons/DetailsActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/DetailsActionInitializers.tsx new file mode 100644 index 000000000..97de05340 --- /dev/null +++ b/packages/core/client/src/schema-initializer/buttons/DetailsActionInitializers.tsx @@ -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', + }, + }, + ], + }, + ], +}; diff --git a/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx index 5a7ecfcdc..525571acb 100644 --- a/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx @@ -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, diff --git a/packages/core/client/src/schema-initializer/buttons/index.ts b/packages/core/client/src/schema-initializer/buttons/index.ts index 22b1f37f1..2a359a2df 100644 --- a/packages/core/client/src/schema-initializer/buttons/index.ts +++ b/packages/core/client/src/schema-initializer/buttons/index.ts @@ -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'; diff --git a/packages/core/client/src/schema-initializer/items/index.tsx b/packages/core/client/src/schema-initializer/items/index.tsx index 09a380bd8..3dfaf6d5d 100644 --- a/packages/core/client/src/schema-initializer/items/index.tsx +++ b/packages/core/client/src/schema-initializer/items/index.tsx @@ -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 ( + } + 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(); diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index 62ea70f8c..2717acb94 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -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',