From 69bc18e1661eb19e3d94a56a91494b7ca55d6b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=AB=E9=9B=A8=E6=B0=B4=E8=BF=87=E6=BB=A4=E7=9A=84?= =?UTF-8?q?=E7=A9=BA=E6=B0=94-Rairn?= <958414905@qq.com> Date: Sun, 16 Apr 2023 14:22:46 +0800 Subject: [PATCH] feat(form-block): data templates (#1704) * feat(Form): support to select existing data as template * refactor: extract useDataTemplates * feat(Form): support to use template * fix: template switch * fix: fix association field * fix: filter fields * fix: fix unselected default value * fix: avoid errors * refactor: remove useless code * refactor: move templateSelect to FormBlockProvider * feat: add a checkbox to toggle template selector * feat: change the options order * feat: hide Collection option when no inherit * fix: optimize the label text * fix: should empty form * fix: should hide configuration when is not added * chore: change text * fix: template selector not displayed * feat: optimize template * feat: data template middleware * fix: template select * fix: default * fix: fields * feat: field delete button changed from hidden to disabled * fix: improve code * fix: prefix error * fix: items * feat: use Tree * fix: maxDepth --------- Co-authored-by: chenos --- .../src/block-provider/FormBlockProvider.tsx | 35 ++- .../Configuration/schemas/collectionFields.ts | 2 +- .../src/collection-manager/action-hooks.ts | 6 +- .../hooks/useCollectionManager.ts | 91 +++++- packages/core/client/src/locale/en_US.ts | 1 + packages/core/client/src/locale/ja_JP.ts | 1 + packages/core/client/src/locale/pt_BR.ts | 1 + packages/core/client/src/locale/ru_RU.ts | 1 + packages/core/client/src/locale/tr_TR.ts | 1 + packages/core/client/src/locale/zh_CN.ts | 1 + .../association-select/useServiceOptions.ts | 1 - .../antd/form-item/FormItem.tsx | 10 +- .../antd/form-v2/Form.Designer.tsx | 7 +- .../schema-component/antd/form-v2/Form.tsx | 48 ++-- .../antd/form-v2/Templates.tsx | 107 +++++++ .../schema-component/antd/form-v2/index.ts | 5 +- .../antd/remote-select/RemoteSelect.tsx | 18 +- .../buttons/FormItemInitializers.tsx | 2 +- .../items/CollectionFieldInitializer.tsx | 12 +- .../items/InitializerWithSwitch.tsx | 2 +- .../DataTemplates/FormDataTemplates.tsx | 178 ++++++++++++ .../DataTemplates/TreeLabel.tsx | 23 ++ .../components/AsDefaultTemplate.tsx | 38 +++ .../components/DataTemplateTitle.tsx | 271 ++++++++++++++++++ .../DataTemplates/hooks/useCollectionState.ts | 116 ++++++++ .../schema-settings/DataTemplates/index.tsx | 1 + .../src/schema-settings/SchemaSettings.tsx | 87 +++++- packages/core/server/src/helper.ts | 3 + .../server/src/middlewares/data-template.ts | 76 +++++ packages/plugins/client/src/locale/en-US.json | 1 + packages/plugins/client/src/locale/pt-BR.json | 3 +- packages/plugins/client/src/locale/zh-CN.json | 1 + 32 files changed, 1074 insertions(+), 76 deletions(-) create mode 100644 packages/core/client/src/schema-component/antd/form-v2/Templates.tsx create mode 100644 packages/core/client/src/schema-settings/DataTemplates/FormDataTemplates.tsx create mode 100644 packages/core/client/src/schema-settings/DataTemplates/TreeLabel.tsx create mode 100644 packages/core/client/src/schema-settings/DataTemplates/components/AsDefaultTemplate.tsx create mode 100644 packages/core/client/src/schema-settings/DataTemplates/components/DataTemplateTitle.tsx create mode 100644 packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts create mode 100644 packages/core/client/src/schema-settings/DataTemplates/index.tsx create mode 100644 packages/core/server/src/middlewares/data-template.ts diff --git a/packages/core/client/src/block-provider/FormBlockProvider.tsx b/packages/core/client/src/block-provider/FormBlockProvider.tsx index e23745775..abaa1aa81 100644 --- a/packages/core/client/src/block-provider/FormBlockProvider.tsx +++ b/packages/core/client/src/block-provider/FormBlockProvider.tsx @@ -1,11 +1,12 @@ import { createForm } from '@formily/core'; -import { useField } from '@formily/react'; +import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; import { Spin } from 'antd'; import isEmpty from 'lodash/isEmpty'; import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react'; import { useCollection } from '../collection-manager'; import { RecordProvider, useRecord } from '../record-provider'; import { useActionContext, useDesignable } from '../schema-component'; +import { Templates as DataTemplateSelect } from '../schema-component/antd/form-v2/Templates'; import { BlockProvider, useBlockRequestContext } from './BlockProvider'; export const FormBlockContext = createContext({}); @@ -40,10 +41,14 @@ const InternalFormBlockProvider = (props) => { > {readPretty ? ( -
{props.children}
+
+ +
) : ( -
{props.children}
+
+ +
)} ); @@ -108,3 +113,27 @@ export const useFormBlockProps = () => { form: ctx.form, }; }; + +const RenderChildrenWithDataTemplates = ({ form }) => { + const FieldSchema = useFieldSchema(); + const { findComponent } = useDesignable(); + const field = useField(); + const Component = findComponent(field.component?.[0]) || React.Fragment; + + return ( + + + + + ); +}; + +export const findFormBlock = (schema: Schema) => { + while (schema) { + if (schema['x-decorator'] === 'FormBlockProvider') { + return schema; + } + schema = schema.parent; + } + return null; +}; diff --git a/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts b/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts index 4d7480ef8..b2296be02 100644 --- a/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts +++ b/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts @@ -194,7 +194,7 @@ export const collectionFieldSchema: ISchema = { delete: { type: 'void', title: '{{ t("Delete") }}', - 'x-visible': '{{cm.useDeleteButtonVisible()}}', + 'x-disabled': '{{cm.useDeleteButtonDisabled()}}', 'x-component': 'Action.Link', 'x-component-props': { confirm: { diff --git a/packages/core/client/src/collection-manager/action-hooks.ts b/packages/core/client/src/collection-manager/action-hooks.ts index 9a182e36d..0bac4bcfa 100644 --- a/packages/core/client/src/collection-manager/action-hooks.ts +++ b/packages/core/client/src/collection-manager/action-hooks.ts @@ -3,9 +3,9 @@ import { message } from 'antd'; import omit from 'lodash/omit'; import { useEffect } from 'react'; import { useCollection, useCollectionManager } from '.'; +import { useCompile } from '..'; import { useRequest } from '../api-client'; import { useRecord } from '../record-provider'; -import { useCompile } from '..'; import { useActionContext } from '../schema-component'; import { useFilterFieldOptions, useFilterFieldProps } from '../schema-component/antd/filter/useFilterActionProps'; import { useResourceActionContext, useResourceContext } from './ResourceActionProvider'; @@ -377,10 +377,10 @@ export const useDestroyActionAndRefreshCM = () => { }; }; -export const useDeleteButtonVisible = () => { +export const useDeleteButtonDisabled = () => { const { interface: i, deletable = true } = useRecord(); - return deletable && i !== 'id'; + return !deletable || i === 'id'; }; export const useBulkDestroyActionAndRefreshCM = () => { diff --git a/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts b/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts index d67f25d3b..74e93421b 100644 --- a/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts +++ b/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts @@ -1,7 +1,6 @@ import { clone } from '@formily/shared'; import { CascaderProps } from 'antd'; -import _ from 'lodash'; -import { reduce, unionBy, uniq, uniqBy } from 'lodash'; +import _, { reduce, unionBy, uniq, uniqBy } from 'lodash'; import { useContext } from 'react'; import { useCompile } from '../../schema-component'; import { CollectionManagerContext } from '../context'; @@ -108,9 +107,31 @@ export const useCollectionManager = () => { * Max depth of recursion */ maxDepth?: number; + allowAllTypes?: boolean; + /** + * 排除这些接口的字段 + */ + exceptInterfaces?: string[]; + /** + * field value 的前缀,用 . 连接,比如 a.b.c + */ + prefixFieldValue?: string; + /** + * 是否使用 prefixFieldValue 作为 field value + */ + usePrefix?: boolean; }, ) => { - const { association = false, cached = {}, collectionNames = [collectionName], maxDepth = 1 } = opts || {}; + const { + association = false, + cached = {}, + collectionNames = [collectionName], + maxDepth = 1, + allowAllTypes = false, + exceptInterfaces = [], + prefixFieldValue = '', + usePrefix = false, + } = opts || {}; if (collectionNames.length - 1 > maxDepth) { return; @@ -129,14 +150,16 @@ export const useCollectionManager = () => { ?.filter( (field) => field.interface && - (type.includes(field.type) || + !exceptInterfaces.includes(field.interface) && + (allowAllTypes || + type.includes(field.type) || (association && field.target && field.target !== collectionName && Array.isArray(association) ? association.includes(field.interface) : false)), ) ?.map((field) => { const result: CascaderProps['options'][0] = { - value: field.name, + value: usePrefix && prefixFieldValue ? `${prefixFieldValue}.${field.name}` : field.name, label: compile(field?.uiSchema?.title) || field.name, ...field, }; @@ -147,6 +170,12 @@ export const useCollectionManager = () => { ...opts, cached, collectionNames: [...collectionNames, field.target], + prefixFieldValue: usePrefix + ? prefixFieldValue + ? `${prefixFieldValue}.${field.name}` + : field.name + : '', + usePrefix, }); if (!result.children?.length) { return null; @@ -161,6 +190,50 @@ export const useCollectionManager = () => { return options; }; + const getCollection = (name: any) => { + if (typeof name !== 'string') { + return name; + } + return collections?.find((collection) => collection.name === name); + }; + + // 获取当前 collection 继承链路上的所有 collection + const getAllCollectionsInheritChain = (collectionName: string) => { + const collectionsInheritChain = [collectionName]; + const getInheritChain = (name: string) => { + const collection = getCollection(name); + if (collection) { + const { inherits } = collection; + const children = getChildrenCollections(name); + // 搜寻祖先表 + if (inherits) { + for (let index = 0; index < inherits.length; index++) { + const collectionKey = inherits[index]; + if (collectionsInheritChain.includes(collectionKey)) { + continue; + } + collectionsInheritChain.push(collectionKey); + getInheritChain(collectionKey); + } + } + // 搜寻后代表 + if (children) { + for (let index = 0; index < children.length; index++) { + const collectionKey = children[index].name; + if (collectionsInheritChain.includes(collectionKey)) { + continue; + } + collectionsInheritChain.push(collectionKey); + getInheritChain(collectionKey); + } + } + } + return collectionsInheritChain; + }; + + return getInheritChain(collectionName); + }; + return { service, interfaces, @@ -176,12 +249,7 @@ export const useCollectionManager = () => { getCollectionFields, getCollectionFieldsOptions, getCurrentCollectionFields, - getCollection(name: any) { - if (typeof name !== 'string') { - return name; - } - return collections?.find((collection) => collection.name === name); - }, + getCollection, getCollectionJoinField(name: string) { if (!name) { return; @@ -227,5 +295,6 @@ export const useCollectionManager = () => { }); }); }, + getAllCollectionsInheritChain, }; }; diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts index 9b8b2fca7..e5c8983fb 100644 --- a/packages/core/client/src/locale/en_US.ts +++ b/packages/core/client/src/locale/en_US.ts @@ -454,6 +454,7 @@ export default { "Turn pages": "Turn pages", "Others": "Others", "Save as template": "Save as template", + "Save as block template": "Save as block template", "Block templates": "Block templates", "Convert reference to duplicate": "Convert reference to duplicate", "Template name": "Template name", diff --git a/packages/core/client/src/locale/ja_JP.ts b/packages/core/client/src/locale/ja_JP.ts index 986b371d7..e1061a3fe 100644 --- a/packages/core/client/src/locale/ja_JP.ts +++ b/packages/core/client/src/locale/ja_JP.ts @@ -383,6 +383,7 @@ export default { "Turn pages": "ページをめくる", "Others": "Others", "Save as template": "テンプレートとして保存", + "Save as block template": "ブロックテンプレートとして保存", "Block templates": "ブロックテンプレート", "Convert reference to duplicate": "参照を複製に変換", "Template name": "テンプレート名", diff --git a/packages/core/client/src/locale/pt_BR.ts b/packages/core/client/src/locale/pt_BR.ts index 9593e0eb2..65143d859 100644 --- a/packages/core/client/src/locale/pt_BR.ts +++ b/packages/core/client/src/locale/pt_BR.ts @@ -416,6 +416,7 @@ export default { "Turn pages": "Virar páginas", "Others": "Outros", "Save as template": "Salvar como modelo", + "Save as block template": "Salvar como modelo de bloco", "Block templates": "Modelos de bloco", "Convert reference to duplicate": "Converter referência em duplicado", "Template name": "Nome do modelo", diff --git a/packages/core/client/src/locale/ru_RU.ts b/packages/core/client/src/locale/ru_RU.ts index 08c254e03..54ba05fa8 100644 --- a/packages/core/client/src/locale/ru_RU.ts +++ b/packages/core/client/src/locale/ru_RU.ts @@ -327,6 +327,7 @@ export default { "Turn pages": "Перелистывать страницы", "Others": "Другие", "Save as template": "Сохранить как шаблон", + "Save as block template": "Сохранить как шаблон Блока", "Block templates": "Шаблоны Блока", "Convert reference to duplicate": "Преобразовать ссылку в дубликат", "Template name": "Имя Шаблона", diff --git a/packages/core/client/src/locale/tr_TR.ts b/packages/core/client/src/locale/tr_TR.ts index 1a53fb933..be0855bcb 100644 --- a/packages/core/client/src/locale/tr_TR.ts +++ b/packages/core/client/src/locale/tr_TR.ts @@ -326,6 +326,7 @@ export default { "Turn pages": "Sayfaları çevir", "Others": "Diğerleri", "Save as template": "Şablon olarak kaydet", + "Save as block template": "Blok şablonu olarak kaydet", "Block templates": "Blok şablonları", "Convert reference to duplicate": "Referansı kopyaya dönüştür", "Template name": "Şablon adı", diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 78788ba5f..74cac1cfa 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -490,6 +490,7 @@ export default { "Turn pages": "翻页", "Others": "其他", "Save as template": "保存为模板", + "Save as block template": "保存为区块模板", "Block templates": "区块模板", "Convert reference to duplicate": "模板引用转为复制", "Template name": "模板名称", diff --git a/packages/core/client/src/schema-component/antd/association-select/useServiceOptions.ts b/packages/core/client/src/schema-component/antd/association-select/useServiceOptions.ts index 1bf787da8..6629d3f11 100644 --- a/packages/core/client/src/schema-component/antd/association-select/useServiceOptions.ts +++ b/packages/core/client/src/schema-component/antd/association-select/useServiceOptions.ts @@ -1,6 +1,5 @@ import { useFieldSchema } from '@formily/react'; import { useCallback, useMemo } from 'react'; -import { useFormBlockContext } from '../../../block-provider'; import { mergeFilter } from '../../../block-provider/SharedFilterProvider'; import { useCollection, useCollectionManager } from '../../../collection-manager'; import { useRecord } from '../../../record-provider'; diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx index 70166d40f..4a2d954df 100644 --- a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx @@ -81,12 +81,12 @@ FormItem.Designer = () => { const { dn, refresh, insertAdjacent } = useDesignable(); const compile = useCompile(); const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); - const targetCollection = getCollection(collectionField.target); + const targetCollection = getCollection(collectionField?.target); const interfaceConfig = getInterface(collectionField?.interface); const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema); const originalTitle = collectionField?.uiSchema?.title; const targetFields = collectionField?.target - ? getCollectionFields(collectionField.target) + ? getCollectionFields(collectionField?.target) : getCollectionFields(collectionField?.targetCollection) ?? []; const fieldComponentOptions = useFieldComponentOptions(); const isSubFormAssociationField = field.address.segments.includes('__form_grid'); @@ -402,11 +402,11 @@ FormItem.Designer = () => { title: t('Set default value'), properties: { default: { - ...collectionField.uiSchema, + ...collectionField?.uiSchema, name: 'default', title: t('Default value'), 'x-decorator': 'FormItem', - default: fieldSchema.default || collectionField.defaultValue, + default: fieldSchema.default || collectionField?.defaultValue, }, }, } as ISchema @@ -434,7 +434,7 @@ FormItem.Designer = () => { value={fieldSchema['x-component']} onChange={(type) => { const schema: ISchema = { - name: collectionField.name, + name: collectionField?.name, type: 'void', required: fieldSchema['required'], description: fieldSchema['description'], 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 56af18047..6fd3fc7ca 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,10 +1,12 @@ import { ArrayItems } from '@formily/antd'; import { ISchema, useField, useFieldSchema } from '@formily/react'; +import _ from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useDetailsBlockContext } from '../../../block-provider/DetailsBlockProvider'; import { useCollection } from '../../../collection-manager'; import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks'; +import { useRecord } from '../../../record-provider'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { useSchemaTemplate } from '../../../schema-templates'; import { useDesignable } from '../../hooks'; @@ -16,17 +18,20 @@ export const FormDesigner = () => { const template = useSchemaTemplate(); const fieldSchema = useFieldSchema(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + const record = useRecord(); return ( {/* */} + + {_.isEmpty(record) ? : null} + - { ); }; -export const Form: React.FC & { Designer?: any; FilterDesigner?: any; ReadPrettyDesigner?: any } = observer( - (props) => { - const field = useField(); - const { form, disabled, ...others } = useProps(props); - const formDisabled = disabled || field.disabled; - return ( - -
- - {form ? ( - - ) : ( - - )} - -
-
- ); - }, -); +export const Form: React.FC & { + Designer?: any; + FilterDesigner?: any; + ReadPrettyDesigner?: any; + Templates?: any; +} = observer((props) => { + const field = useField(); + const { form, disabled, ...others } = useProps(props); + const formDisabled = disabled || field.disabled; + return ( + +
+ + {form ? ( + + ) : ( + + )} + +
+
+ ); +}); diff --git a/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx b/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx new file mode 100644 index 000000000..cfefc95de --- /dev/null +++ b/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx @@ -0,0 +1,107 @@ +import { useFieldSchema } from '@formily/react'; +import { Select } from 'antd'; +import _ from 'lodash'; +import React, { useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useAPIClient } from '../../../api-client'; +import { findFormBlock } from '../../../block-provider'; + +interface ITemplate { + items: { + key: string; + title: string; + collection: string; + dataId: number; + fields: string[]; + default?: boolean; + }[]; + /** 是否在 Form 区块显示模板选择器 */ + display: boolean; +} + +const useDataTemplates = () => { + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { items = [], display = true } = findDataTemplates(fieldSchema); + const templates: any = [ + { + key: 'none', + title: t('None'), + }, + ].concat(items.map((t, i) => ({ key: i, ...t }))); + const defaultTemplate = items.find((item) => item.default); + return { + templates, + display, + defaultTemplate, + enabled: items.length > 0, + }; +}; + +export const Templates = ({ style = {}, form }) => { + const { templates, display, enabled, defaultTemplate } = useDataTemplates(); + const [value, setValue] = React.useState(defaultTemplate?.key || 'none'); + const api = useAPIClient(); + const { t } = useTranslation(); + + useEffect(() => { + if (defaultTemplate) { + fetchTemplateData(api, defaultTemplate).then((data) => { + if (form) { + form.values = data; + } + }); + } + }, []); + + const handleChange = useCallback(async (value, option) => { + setValue(value); + if (option.key !== 'none') { + if (form) { + form.values = await fetchTemplateData(api, option); + } + } else { + form?.reset(); + } + }, []); + + if (!enabled || !display) { + return null; + } + + return ( +
+ +