feat(data-template): support for unlimited levels of fields selection (#1910)
* refactor: use useMemo * refactor: use useCallback * feat(Data-template): change maxDepth from 3 to 10 * feat(Data-template): support for unlimited levels of fields selection * fix: fix invalid selection * fix: should be updated when data changes * feat: parent treeNode and children treeNodes are not associated * fix: avoid error * fix: fix error * chore: remove notification * fix: parant node should be selected when child is selected * fix: auto select all ancester nodes
This commit is contained in:
		
							parent
							
								
									948d39bab4
								
							
						
					
					
						commit
						c722ca41d9
					
				| @ -709,5 +709,4 @@ export default { | |||||||
|   "Feedback": "Feedback", |   "Feedback": "Feedback", | ||||||
|   "Try again": "Try again", |   "Try again": "Try again", | ||||||
|   "Data template": "Data template", |   "Data template": "Data template", | ||||||
|   "Template fields have been removed and need to be reconfigured": "Template fields have been removed and need to be reconfigured", |  | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -688,5 +688,4 @@ export default { | |||||||
|     "Sortable": "Clasificable", |     "Sortable": "Clasificable", | ||||||
|     "Enable link": "Activar enlace", |     "Enable link": "Activar enlace", | ||||||
|     "Data template": "Plantilla de datos", |     "Data template": "Plantilla de datos", | ||||||
|     "Template fields have been removed and need to be reconfigured": "Los campos de la plantilla se han eliminado y deben reconfigurarse", |  | ||||||
|  }; |  }; | ||||||
|  | |||||||
| @ -618,5 +618,4 @@ export default { | |||||||
|   'Display data template selector': 'データテンプレートセレクターを表示', |   'Display data template selector': 'データテンプレートセレクターを表示', | ||||||
|   'Form data templates': 'フォームデータテンプレート', |   'Form data templates': 'フォームデータテンプレート', | ||||||
|   "Data template": "データテンプレート", |   "Data template": "データテンプレート", | ||||||
|   "Template fields have been removed and need to be reconfigured": "テンプレートフィールドが削除されました。再設定する必要があります", |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -668,5 +668,4 @@ export default { | |||||||
|   'Display data template selector': 'Exibir seletor de modelo de dados', |   'Display data template selector': 'Exibir seletor de modelo de dados', | ||||||
|   'Form data templates': 'Modelos de dados do formulário', |   'Form data templates': 'Modelos de dados do formulário', | ||||||
|   "Data template": "Modelo de dados", |   "Data template": "Modelo de dados", | ||||||
|   "Template fields have been removed and need to be reconfigured": "Os campos do modelo foram removidos e precisam ser reconfigurados", |  | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -522,5 +522,4 @@ export default { | |||||||
|   'Display data template selector': "Отображать селектор шаблона данных", |   'Display data template selector': "Отображать селектор шаблона данных", | ||||||
|   'Form data templates': "Шаблоны данных формы", |   'Form data templates': "Шаблоны данных формы", | ||||||
|   "Data template": "Шаблон данных", |   "Data template": "Шаблон данных", | ||||||
|   "Template fields have been removed and need to be reconfigured": "Поля шаблона были удалены и требуют повторной настройки", |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -521,5 +521,4 @@ export default { | |||||||
|   'Display data template selector': 'Veri şablonu seçicisini görüntüle', |   'Display data template selector': 'Veri şablonu seçicisini görüntüle', | ||||||
|   'Form data templates': 'Form veri şablonları', |   'Form data templates': 'Form veri şablonları', | ||||||
|   "Data template": "Veri şablonu", |   "Data template": "Veri şablonu", | ||||||
|   "Template fields have been removed and need to be reconfigured": "Şablon alanları kaldırıldı ve yeniden yapılandırılması gerekiyor", |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -786,6 +786,5 @@ export default { | |||||||
|   "Allows to clear cache, reboot application": "允许清除缓存,重启应用", |   "Allows to clear cache, reboot application": "允许清除缓存,重启应用", | ||||||
|   'The will interrupt service, it may take a few seconds to restart. Are you sure to continue?': '重启将会中断当前服务,这个过程可能需要一点时间,确定要继续吗?', |   'The will interrupt service, it may take a few seconds to restart. Are you sure to continue?': '重启将会中断当前服务,这个过程可能需要一点时间,确定要继续吗?', | ||||||
|   'Reboot': '重启', |   'Reboot': '重启', | ||||||
|   "Template fields have been removed and need to be reconfigured": "模板字段已被删除,需要重新配置", |  | ||||||
|   'Clear cache': '清除缓存', |   'Clear cache': '清除缓存', | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import { useFieldSchema } from '@formily/react'; | import { useFieldSchema } from '@formily/react'; | ||||||
| import { error, forEach, showToast } from '@nocobase/utils/client'; | import { error, forEach } from '@nocobase/utils/client'; | ||||||
| import { Select } from 'antd'; | import { Select } from 'antd'; | ||||||
| import _ from 'lodash'; | import _ from 'lodash'; | ||||||
| import React, { useCallback, useEffect } from 'react'; | import React, { useCallback, useEffect } from 'react'; | ||||||
| @ -71,7 +71,7 @@ export const Templates = ({ style = {}, form }) => { | |||||||
|     if (defaultTemplate) { |     if (defaultTemplate) { | ||||||
|       fetchTemplateData(api, defaultTemplate, t) |       fetchTemplateData(api, defaultTemplate, t) | ||||||
|         .then((data) => { |         .then((data) => { | ||||||
|           if (form) { |           if (form && data) { | ||||||
|             forEach(data, (value, key) => { |             forEach(data, (value, key) => { | ||||||
|               if (value) { |               if (value) { | ||||||
|                 form.values[key] = value; |                 form.values[key] = value; | ||||||
| @ -91,7 +91,7 @@ export const Templates = ({ style = {}, form }) => { | |||||||
|     if (option.key !== 'none') { |     if (option.key !== 'none') { | ||||||
|       fetchTemplateData(api, option, t) |       fetchTemplateData(api, option, t) | ||||||
|         .then((data) => { |         .then((data) => { | ||||||
|           if (form) { |           if (form && data) { | ||||||
|             forEach(data, (value, key) => { |             forEach(data, (value, key) => { | ||||||
|               if (value) { |               if (value) { | ||||||
|                 form.values[key] = value; |                 form.values[key] = value; | ||||||
| @ -138,7 +138,7 @@ function findDataTemplates(fieldSchema): ITemplate { | |||||||
| 
 | 
 | ||||||
| async function fetchTemplateData(api, template: { collection: string; dataId: number; fields: string[] }, t) { | async function fetchTemplateData(api, template: { collection: string; dataId: number; fields: string[] }, t) { | ||||||
|   if (template.fields.length === 0) { |   if (template.fields.length === 0) { | ||||||
|     return showToast(t('Template fields have been removed and need to be reconfigured')); |     return; | ||||||
|   } |   } | ||||||
|   return api |   return api | ||||||
|     .resource(template.collection) |     .resource(template.collection) | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { connect, mapProps, observer } from '@formily/react'; | import { connect, mapProps, observer } from '@formily/react'; | ||||||
| import { Tree as AntdTree } from 'antd'; | import { Tree as AntdTree } from 'antd'; | ||||||
| import React from 'react'; | import React, { useMemo } from 'react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { useCollectionManager } from '../../collection-manager'; | import { useCollectionManager } from '../../collection-manager'; | ||||||
| import { AssociationSelect, SchemaComponent } from '../../schema-component'; | import { AssociationSelect, SchemaComponent } from '../../schema-component'; | ||||||
| @ -13,183 +13,185 @@ const Tree = connect( | |||||||
|   mapProps({ |   mapProps({ | ||||||
|     value: 'checkedKeys', |     value: 'checkedKeys', | ||||||
|     dataSource: 'treeData', |     dataSource: 'treeData', | ||||||
|     onInput: 'onCheck', |     // onInput: 'onCheck',
 | ||||||
|   }), |   }), | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| export const FormDataTemplates = observer((props: any) => { | export const FormDataTemplates = observer((props: any) => { | ||||||
|   const { useProps } = props; |   const { useProps } = props; | ||||||
|   const { defaultValues, collectionName } = useProps(); |   const { defaultValues, collectionName } = useProps(); | ||||||
|   const { collectionList, getEnableFieldTree } = useCollectionState(collectionName); |   const { collectionList, getEnableFieldTree, onLoadData, onCheck } = useCollectionState(collectionName); | ||||||
|   const { getCollection, getCollectionField } = useCollectionManager(); |   const { getCollection, getCollectionField } = useCollectionManager(); | ||||||
|   const collection = getCollection(collectionName); |   const collection = getCollection(collectionName); | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const field = getCollectionField(`${collectionName}.${collection?.titleField || 'id'}`); |   const field = getCollectionField(`${collectionName}.${collection?.titleField || 'id'}`); | ||||||
| 
 |   const components = useMemo(() => ({ ArrayCollapse }), []); | ||||||
|   return ( |   const scope = useMemo(() => ({ getEnableFieldTree }), []); | ||||||
|     <SchemaComponent |   const schema = useMemo( | ||||||
|       components={{ ArrayCollapse }} |     () => ({ | ||||||
|       scope={{ getEnableFieldTree }} |       type: 'object', | ||||||
|       schema={{ |       properties: { | ||||||
|         type: 'object', |         items: { | ||||||
|         properties: { |           type: 'array', | ||||||
|  |           default: defaultValues?.items, | ||||||
|  |           'x-component': 'ArrayCollapse', | ||||||
|  |           'x-decorator': 'FormItem', | ||||||
|  |           'x-component-props': { | ||||||
|  |             accordion: true, | ||||||
|  |           }, | ||||||
|           items: { |           items: { | ||||||
|             type: 'array', |             type: 'object', | ||||||
|             default: defaultValues?.items, |             'x-component': 'ArrayCollapse.CollapsePanel', | ||||||
|             'x-component': 'ArrayCollapse', |  | ||||||
|             'x-decorator': 'FormItem', |  | ||||||
|             'x-component-props': { |             'x-component-props': { | ||||||
|               accordion: true, |               extra: [<AsDefaultTemplate key="0" />], | ||||||
|             }, |  | ||||||
|             items: { |  | ||||||
|               type: 'object', |  | ||||||
|               'x-component': 'ArrayCollapse.CollapsePanel', |  | ||||||
|               'x-component-props': { |  | ||||||
|                 extra: [<AsDefaultTemplate />], |  | ||||||
|               }, |  | ||||||
|               properties: { |  | ||||||
|                 layout: { |  | ||||||
|                   type: 'void', |  | ||||||
|                   'x-component': 'FormLayout', |  | ||||||
|                   'x-component-props': { |  | ||||||
|                     layout: 'vertical', |  | ||||||
|                   }, |  | ||||||
|                   // TODO: 翻译
 |  | ||||||
|                   properties: { |  | ||||||
|                     collection: { |  | ||||||
|                       type: 'string', |  | ||||||
|                       title: '{{ t("Collection") }}', |  | ||||||
|                       required: true, |  | ||||||
|                       description: t('If collection inherits, choose inherited collections as templates'), |  | ||||||
|                       default: collectionName, |  | ||||||
|                       'x-display': collectionList.length > 1 ? 'visible' : 'hidden', |  | ||||||
|                       'x-decorator': 'FormItem', |  | ||||||
|                       'x-component': 'Select', |  | ||||||
|                       'x-component-props': { |  | ||||||
|                         options: collectionList, |  | ||||||
|                       }, |  | ||||||
|                     }, |  | ||||||
|                     dataId: { |  | ||||||
|                       type: 'number', |  | ||||||
|                       title: '{{ t("Template Data") }}', |  | ||||||
|                       required: true, |  | ||||||
|                       description: t('Select an existing piece of data as the initialization data for the form'), |  | ||||||
|                       'x-decorator': 'FormItem', |  | ||||||
|                       'x-component': AssociationSelect, |  | ||||||
|                       'x-component-props': { |  | ||||||
|                         service: { |  | ||||||
|                           resource: collectionName, |  | ||||||
|                         }, |  | ||||||
|                         action: 'list', |  | ||||||
|                         multiple: false, |  | ||||||
|                         objectValue: false, |  | ||||||
|                         manual: false, |  | ||||||
|                         targetField: field, |  | ||||||
|                         mapOptions(option) { |  | ||||||
|                           try { |  | ||||||
|                             const label = getLabel(collection); |  | ||||||
|                             option[label] = ( |  | ||||||
|                               <> |  | ||||||
|                                 #{option.id} {option[label]} |  | ||||||
|                               </> |  | ||||||
|                             ); |  | ||||||
| 
 |  | ||||||
|                             return option; |  | ||||||
|                           } catch (error) { |  | ||||||
|                             console.error(error); |  | ||||||
|                             return option; |  | ||||||
|                           } |  | ||||||
|                         }, |  | ||||||
|                         fieldNames: { |  | ||||||
|                           label: getLabel(collection), |  | ||||||
|                           value: 'id', |  | ||||||
|                         }, |  | ||||||
|                       }, |  | ||||||
|                       'x-reactions': [ |  | ||||||
|                         { |  | ||||||
|                           dependencies: ['.collection'], |  | ||||||
|                           fulfill: { |  | ||||||
|                             state: { |  | ||||||
|                               disabled: '{{ !$deps[0] }}', |  | ||||||
|                               componentProps: { |  | ||||||
|                                 service: { |  | ||||||
|                                   resource: '{{ $deps[0] }}', |  | ||||||
|                                 }, |  | ||||||
|                               }, |  | ||||||
|                             }, |  | ||||||
|                           }, |  | ||||||
|                         }, |  | ||||||
|                       ], |  | ||||||
|                     }, |  | ||||||
|                     fields: { |  | ||||||
|                       type: 'array', |  | ||||||
|                       title: '{{ t("Data fields") }}', |  | ||||||
|                       required: true, |  | ||||||
|                       description: t('Only the selected fields will be used as the initialization data for the form'), |  | ||||||
|                       'x-decorator': 'FormItem', |  | ||||||
|                       'x-component': Tree, |  | ||||||
|                       'x-component-props': { |  | ||||||
|                         treeData: [], |  | ||||||
|                         checkable: true, |  | ||||||
|                         selectable: false, |  | ||||||
|                         rootStyle: { |  | ||||||
|                           padding: '8px 0', |  | ||||||
|                           border: '1px solid #d9d9d9', |  | ||||||
|                           borderRadius: '2px', |  | ||||||
|                           maxHeight: '30vh', |  | ||||||
|                           overflow: 'auto', |  | ||||||
|                           margin: '2px 0', |  | ||||||
|                         }, |  | ||||||
|                       }, |  | ||||||
|                       'x-reactions': [ |  | ||||||
|                         { |  | ||||||
|                           dependencies: ['.collection'], |  | ||||||
|                           fulfill: { |  | ||||||
|                             state: { |  | ||||||
|                               disabled: '{{ !$deps[0] }}', |  | ||||||
|                               componentProps: { |  | ||||||
|                                 treeData: '{{ getEnableFieldTree($deps[0]) }}', |  | ||||||
|                               }, |  | ||||||
|                             }, |  | ||||||
|                           }, |  | ||||||
|                         }, |  | ||||||
|                       ], |  | ||||||
|                     }, |  | ||||||
|                   }, |  | ||||||
|                 }, |  | ||||||
|                 remove: { |  | ||||||
|                   type: 'void', |  | ||||||
|                   'x-component': 'ArrayCollapse.Remove', |  | ||||||
|                 }, |  | ||||||
|                 moveUp: { |  | ||||||
|                   type: 'void', |  | ||||||
|                   'x-component': 'ArrayCollapse.MoveUp', |  | ||||||
|                 }, |  | ||||||
|                 moveDown: { |  | ||||||
|                   type: 'void', |  | ||||||
|                   'x-component': 'ArrayCollapse.MoveDown', |  | ||||||
|                 }, |  | ||||||
|               }, |  | ||||||
|             }, |             }, | ||||||
|             properties: { |             properties: { | ||||||
|               add: { |               layout: { | ||||||
|                 type: 'void', |                 type: 'void', | ||||||
|                 title: '{{ t("Add template") }}', |                 'x-component': 'FormLayout', | ||||||
|                 'x-component': 'ArrayCollapse.Addition', |                 'x-component-props': { | ||||||
|  |                   layout: 'vertical', | ||||||
|  |                 }, | ||||||
|  |                 properties: { | ||||||
|  |                   collection: { | ||||||
|  |                     type: 'string', | ||||||
|  |                     title: '{{ t("Collection") }}', | ||||||
|  |                     required: true, | ||||||
|  |                     description: t('If collection inherits, choose inherited collections as templates'), | ||||||
|  |                     default: collectionName, | ||||||
|  |                     'x-display': collectionList.length > 1 ? 'visible' : 'hidden', | ||||||
|  |                     'x-decorator': 'FormItem', | ||||||
|  |                     'x-component': 'Select', | ||||||
|  |                     'x-component-props': { | ||||||
|  |                       options: collectionList, | ||||||
|  |                     }, | ||||||
|  |                   }, | ||||||
|  |                   dataId: { | ||||||
|  |                     type: 'number', | ||||||
|  |                     title: '{{ t("Template Data") }}', | ||||||
|  |                     required: true, | ||||||
|  |                     description: t('Select an existing piece of data as the initialization data for the form'), | ||||||
|  |                     'x-decorator': 'FormItem', | ||||||
|  |                     'x-component': AssociationSelect, | ||||||
|  |                     'x-component-props': { | ||||||
|  |                       service: { | ||||||
|  |                         resource: collectionName, | ||||||
|  |                       }, | ||||||
|  |                       action: 'list', | ||||||
|  |                       multiple: false, | ||||||
|  |                       objectValue: false, | ||||||
|  |                       manual: false, | ||||||
|  |                       targetField: field, | ||||||
|  |                       mapOptions(option) { | ||||||
|  |                         try { | ||||||
|  |                           const label = getLabel(collection); | ||||||
|  |                           option[label] = ( | ||||||
|  |                             <> | ||||||
|  |                               #{option.id} {option[label]} | ||||||
|  |                             </> | ||||||
|  |                           ); | ||||||
|  | 
 | ||||||
|  |                           return option; | ||||||
|  |                         } catch (error) { | ||||||
|  |                           console.error(error); | ||||||
|  |                           return option; | ||||||
|  |                         } | ||||||
|  |                       }, | ||||||
|  |                       fieldNames: { | ||||||
|  |                         label: getLabel(collection), | ||||||
|  |                         value: 'id', | ||||||
|  |                       }, | ||||||
|  |                     }, | ||||||
|  |                     'x-reactions': [ | ||||||
|  |                       { | ||||||
|  |                         dependencies: ['.collection'], | ||||||
|  |                         fulfill: { | ||||||
|  |                           state: { | ||||||
|  |                             disabled: '{{ !$deps[0] }}', | ||||||
|  |                             componentProps: { | ||||||
|  |                               service: { | ||||||
|  |                                 resource: '{{ $deps[0] }}', | ||||||
|  |                               }, | ||||||
|  |                             }, | ||||||
|  |                           }, | ||||||
|  |                         }, | ||||||
|  |                       }, | ||||||
|  |                     ], | ||||||
|  |                   }, | ||||||
|  |                   fields: { | ||||||
|  |                     type: 'array', | ||||||
|  |                     title: '{{ t("Data fields") }}', | ||||||
|  |                     required: true, | ||||||
|  |                     description: t('Only the selected fields will be used as the initialization data for the form'), | ||||||
|  |                     'x-decorator': 'FormItem', | ||||||
|  |                     'x-component': Tree, | ||||||
|  |                     'x-component-props': { | ||||||
|  |                       treeData: [], | ||||||
|  |                       checkable: true, | ||||||
|  |                       checkStrictly: true, | ||||||
|  |                       selectable: false, | ||||||
|  |                       loadData: onLoadData, | ||||||
|  |                       onCheck, | ||||||
|  |                       rootStyle: { | ||||||
|  |                         padding: '8px 0', | ||||||
|  |                         border: '1px solid #d9d9d9', | ||||||
|  |                         borderRadius: '2px', | ||||||
|  |                         maxHeight: '30vh', | ||||||
|  |                         overflow: 'auto', | ||||||
|  |                         margin: '2px 0', | ||||||
|  |                       }, | ||||||
|  |                     }, | ||||||
|  |                     'x-reactions': [ | ||||||
|  |                       { | ||||||
|  |                         dependencies: ['.collection'], | ||||||
|  |                         fulfill: { | ||||||
|  |                           state: { | ||||||
|  |                             disabled: '{{ !$deps[0] }}', | ||||||
|  |                             componentProps: { | ||||||
|  |                               treeData: '{{ getEnableFieldTree($deps[0], $self) }}', | ||||||
|  |                             }, | ||||||
|  |                           }, | ||||||
|  |                         }, | ||||||
|  |                       }, | ||||||
|  |                     ], | ||||||
|  |                   }, | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|  |               remove: { | ||||||
|  |                 type: 'void', | ||||||
|  |                 'x-component': 'ArrayCollapse.Remove', | ||||||
|  |               }, | ||||||
|  |               moveUp: { | ||||||
|  |                 type: 'void', | ||||||
|  |                 'x-component': 'ArrayCollapse.MoveUp', | ||||||
|  |               }, | ||||||
|  |               moveDown: { | ||||||
|  |                 type: 'void', | ||||||
|  |                 'x-component': 'ArrayCollapse.MoveDown', | ||||||
|               }, |               }, | ||||||
|             }, |             }, | ||||||
|           }, |           }, | ||||||
|           display: { |           properties: { | ||||||
|             type: 'boolean', |             add: { | ||||||
|             'x-content': '{{ t("Display data template selector") }}', |               type: 'void', | ||||||
|             default: defaultValues?.display !== false, |               title: '{{ t("Add template") }}', | ||||||
|             'x-decorator': 'FormItem', |               'x-component': 'ArrayCollapse.Addition', | ||||||
|             'x-component': 'Checkbox', |             }, | ||||||
|           }, |           }, | ||||||
|         }, |         }, | ||||||
|       }} |         display: { | ||||||
|     /> |           type: 'boolean', | ||||||
|  |           'x-content': '{{ t("Display data template selector") }}', | ||||||
|  |           default: defaultValues?.display !== false, | ||||||
|  |           'x-decorator': 'FormItem', | ||||||
|  |           'x-component': 'Checkbox', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }), | ||||||
|  |     [], | ||||||
|   ); |   ); | ||||||
|  | 
 | ||||||
|  |   return <SchemaComponent components={components} scope={scope} schema={schema} />; | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| function getLabel(collection: any) { | function getLabel(collection: any) { | ||||||
|  | |||||||
| @ -1,4 +1,7 @@ | |||||||
| import React, { useState } from 'react'; | import { ArrayField } from '@formily/core'; | ||||||
|  | import { error } from '@nocobase/utils/client'; | ||||||
|  | import _ from 'lodash'; | ||||||
|  | import React, { useCallback, useState } from 'react'; | ||||||
| import { useCollectionManager } from '../../../collection-manager'; | import { useCollectionManager } from '../../../collection-manager'; | ||||||
| import { useCompile } from '../../../schema-component'; | import { useCompile } from '../../../schema-component'; | ||||||
| import { TreeNode } from '../TreeLabel'; | import { TreeNode } from '../TreeLabel'; | ||||||
| @ -9,113 +12,213 @@ export const useCollectionState = (currentCollectionName: string) => { | |||||||
|   const [collectionList] = useState(getCollectionList); |   const [collectionList] = useState(getCollectionList); | ||||||
|   const compile = useCompile(); |   const compile = useCompile(); | ||||||
| 
 | 
 | ||||||
|  |   let dataFields: ArrayField = {} as any; | ||||||
|  | 
 | ||||||
|   function getCollectionList() { |   function getCollectionList() { | ||||||
|     const collections = getAllCollectionsInheritChain(currentCollectionName); |     const collections = getAllCollectionsInheritChain(currentCollectionName); | ||||||
|     return collections.map((name) => ({ label: getCollection(name)?.title, value: name })); |     return collections.map((name) => ({ label: getCollection(name)?.title, value: name })); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const getEnableFieldTree = (collectionName: string) => { |   // 过滤掉系统字段
 | ||||||
|  |   const systemKeys = [ | ||||||
|  |     // 'id',
 | ||||||
|  |     'sort', | ||||||
|  |     'createdById', | ||||||
|  |     'createdBy', | ||||||
|  |     'createdAt', | ||||||
|  |     'updatedById', | ||||||
|  |     'updatedBy', | ||||||
|  |     'updatedAt', | ||||||
|  |   ]; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * maxDepth: 从 0 开始,0 表示一层,1 表示两层,以此类推 | ||||||
|  |    */ | ||||||
|  |   const traverseFields = (collectionName, { exclude = [], depth = 0, maxDepth, prefix = '' }) => { | ||||||
|  |     if (depth > maxDepth) { | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return getCollectionFields(collectionName) | ||||||
|  |       .map((field) => { | ||||||
|  |         if (exclude.includes(field.name)) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         if (!field.interface) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         if (['sort', 'password', 'sequence'].includes(field.type)) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         const node = { | ||||||
|  |           type: 'duplicate', | ||||||
|  |           tag: compile(field.uiSchema?.title) || field.name, | ||||||
|  |         }; | ||||||
|  |         const option = { | ||||||
|  |           ...node, | ||||||
|  |           title: React.createElement(TreeNode, node), | ||||||
|  |           key: prefix ? `${prefix}.${field.name}` : field.name, | ||||||
|  |           isLeaf: true, | ||||||
|  |           field, | ||||||
|  |         }; | ||||||
|  |         // 多对多和多对一只展示关系字段
 | ||||||
|  |         if (['belongsTo', 'belongsToMany'].includes(field.type)) { | ||||||
|  |           node['type'] = 'reference'; | ||||||
|  |           option['type'] = 'reference'; | ||||||
|  |           option['title'] = React.createElement(TreeNode, { ...node, type: 'reference' }); | ||||||
|  |           option.isLeaf = false; | ||||||
|  |           option['children'] = traverseAssociations(field.target, { | ||||||
|  |             depth: depth + 1, | ||||||
|  |             maxDepth, | ||||||
|  |             prefix: option.key, | ||||||
|  |             exclude: systemKeys, | ||||||
|  |           }); | ||||||
|  |         } else if (['hasOne', 'hasMany'].includes(field.type)) { | ||||||
|  |           option.isLeaf = false; | ||||||
|  |           option['children'] = traverseFields(field.target, { | ||||||
|  |             depth: depth + 1, | ||||||
|  |             maxDepth, | ||||||
|  |             prefix: option.key, | ||||||
|  |             exclude: ['id', ...systemKeys], | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |         return option; | ||||||
|  |       }) | ||||||
|  |       .filter(Boolean); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const traverseAssociations = (collectionName, { prefix, maxDepth, depth = 0, exclude = [] }) => { | ||||||
|  |     if (depth > maxDepth) { | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  |     return getCollectionFields(collectionName) | ||||||
|  |       .map((field) => { | ||||||
|  |         if (!field.target || !field.interface) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         if (exclude.includes(field.name)) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         const option = { | ||||||
|  |           type: 'preloading', | ||||||
|  |           tag: compile(field.uiSchema?.title) || field.name, | ||||||
|  |         }; | ||||||
|  |         const value = prefix ? `${prefix}.${field.name}` : field.name; | ||||||
|  |         return { | ||||||
|  |           title: React.createElement(TreeNode, option), | ||||||
|  |           key: value, | ||||||
|  |           isLeaf: false, | ||||||
|  |           field, | ||||||
|  |           children: traverseAssociations(field.target, { | ||||||
|  |             prefix: value, | ||||||
|  |             depth: depth + 1, | ||||||
|  |             maxDepth, | ||||||
|  |             exclude, | ||||||
|  |           }), | ||||||
|  |         }; | ||||||
|  |       }) | ||||||
|  |       .filter(Boolean); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * fields: 通过在 x-reactions 字段中传递 $self 得到的,相当于 useField 的返回值,目的是修改其中的状态页面会更新 | ||||||
|  |    */ | ||||||
|  |   const getEnableFieldTree = useCallback((collectionName: string, fields: ArrayField) => { | ||||||
|  |     dataFields = fields; | ||||||
|  | 
 | ||||||
|     if (!collectionName) { |     if (!collectionName) { | ||||||
|       return []; |       return []; | ||||||
|     } |     } | ||||||
|     // 过滤掉系统字段
 | 
 | ||||||
|     const systemKeys = [ |  | ||||||
|       // 'id',
 |  | ||||||
|       'sort', |  | ||||||
|       'createdById', |  | ||||||
|       'createdBy', |  | ||||||
|       'createdAt', |  | ||||||
|       'updatedById', |  | ||||||
|       'updatedBy', |  | ||||||
|       'updatedAt', |  | ||||||
|     ]; |  | ||||||
|     const traverseAssociations = (collectionName, { prefix, maxDepth, depth, exclude = [] }) => { |  | ||||||
|       if (depth > maxDepth) { |  | ||||||
|         return []; |  | ||||||
|       } |  | ||||||
|       return getCollectionFields(collectionName) |  | ||||||
|         .map((field) => { |  | ||||||
|           if (!field.target || !field.interface) { |  | ||||||
|             return; |  | ||||||
|           } |  | ||||||
|           if (exclude.includes(field.name)) { |  | ||||||
|             return; |  | ||||||
|           } |  | ||||||
|           const option = { |  | ||||||
|             type: 'preloading', |  | ||||||
|             tag: compile(field.uiSchema?.title) || field.name, |  | ||||||
|           }; |  | ||||||
|           const value = prefix ? `${prefix}.${field.name}` : field.name; |  | ||||||
|           return { |  | ||||||
|             title: React.createElement(TreeNode, option), |  | ||||||
|             key: value, |  | ||||||
|             children: traverseAssociations(getCollectionFields(field.target), { |  | ||||||
|               prefix: value, |  | ||||||
|               depth: depth + 1, |  | ||||||
|               maxDepth, |  | ||||||
|               exclude, |  | ||||||
|             }), |  | ||||||
|           }; |  | ||||||
|         }) |  | ||||||
|         .filter(Boolean); |  | ||||||
|     }; |  | ||||||
|     const traverseFields = (collectionName, { exclude = [], depth = 0, maxDepth, prefix = '' }) => { |  | ||||||
|       if (depth > maxDepth) { |  | ||||||
|         return []; |  | ||||||
|       } |  | ||||||
|       return getCollectionFields(collectionName) |  | ||||||
|         .map((field) => { |  | ||||||
|           if (exclude.includes(field.name)) { |  | ||||||
|             return; |  | ||||||
|           } |  | ||||||
|           if (!field.interface) { |  | ||||||
|             return; |  | ||||||
|           } |  | ||||||
|           if (['sort', 'password', 'sequence'].includes(field.type)) { |  | ||||||
|             return; |  | ||||||
|           } |  | ||||||
|           const node = { |  | ||||||
|             type: 'duplicate', |  | ||||||
|             tag: compile(field.uiSchema?.title) || field.name, |  | ||||||
|           }; |  | ||||||
|           const option = { |  | ||||||
|             ...node, |  | ||||||
|             title: React.createElement(TreeNode, node), |  | ||||||
|             key: prefix ? `${prefix}.${field.name}` : field.name, |  | ||||||
|           }; |  | ||||||
|           // 多对多的只展示关系字段
 |  | ||||||
|           if (['belongsTo', 'belongsToMany'].includes(field.type)) { |  | ||||||
|             node['type'] = 'reference'; |  | ||||||
|             option['type'] = 'reference'; |  | ||||||
|             option['title'] = React.createElement(TreeNode, { ...node, type: 'reference' }); |  | ||||||
|             option['children'] = traverseAssociations(field.target, { |  | ||||||
|               depth: depth + 1, |  | ||||||
|               maxDepth, |  | ||||||
|               prefix: option.key, |  | ||||||
|               exclude: systemKeys, |  | ||||||
|             }); |  | ||||||
|           } else if (['hasOne', 'hasMany'].includes(field.type)) { |  | ||||||
|             option['children'] = traverseFields(field.target, { |  | ||||||
|               depth: depth + 1, |  | ||||||
|               maxDepth, |  | ||||||
|               prefix: option.key, |  | ||||||
|               exclude: ['id', ...systemKeys], |  | ||||||
|             }); |  | ||||||
|           } |  | ||||||
|           return option; |  | ||||||
|         }) |  | ||||||
|         .filter(Boolean); |  | ||||||
|     }; |  | ||||||
|     try { |     try { | ||||||
|       return traverseFields(collectionName, { exclude: ['id', ...systemKeys], maxDepth: 3 }); |       return traverseFields(collectionName, { exclude: ['id', ...systemKeys], maxDepth: 1 }); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.error(error); |       console.error(error); | ||||||
|       return []; |       return []; | ||||||
|     } |     } | ||||||
|   }; |   }, []); | ||||||
|  | 
 | ||||||
|  |   const onLoadData = useCallback((node) => { | ||||||
|  |     return new Promise((resolve) => { | ||||||
|  |       if (node.children.length) { | ||||||
|  |         node.children.forEach((child) => { | ||||||
|  |           loadChildren({ node: child, traverseAssociations, traverseFields, systemKeys, dataFields }); | ||||||
|  |         }); | ||||||
|  |         return resolve(void 0); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       loadChildren({ node, traverseAssociations, traverseFields, systemKeys, dataFields }); | ||||||
|  |       resolve(void 0); | ||||||
|  |     }); | ||||||
|  |   }, []); | ||||||
|  | 
 | ||||||
|  |   const onCheck = useCallback((checkedKeys, { node, checked }) => { | ||||||
|  |     if (checked) { | ||||||
|  |       let parentKey = node.key.split('.').slice(0, -1).join('.'); | ||||||
|  | 
 | ||||||
|  |       try { | ||||||
|  |         // 当子节点被选中时,也选中所有祖先节点,提高用户辨识度
 | ||||||
|  |         while (parentKey) { | ||||||
|  |           if (parentKey) { | ||||||
|  |             checkedKeys.checked = _.uniq([...checkedKeys.checked, parentKey]); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           parentKey = parentKey.split('.').slice(0, -1).join('.'); | ||||||
|  |         } | ||||||
|  |       } catch (err) { | ||||||
|  |         error(err); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     dataFields.value = checkedKeys; | ||||||
|  |   }, []); | ||||||
| 
 | 
 | ||||||
|   return { |   return { | ||||||
|     collectionList, |     collectionList, | ||||||
|     getEnableFieldTree, |     getEnableFieldTree, | ||||||
|  |     onLoadData, | ||||||
|  |     onCheck, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | // 广度优先遍历查找一个 key 相等的节点并返回
 | ||||||
|  | function findNode(treeData, item) { | ||||||
|  |   const queue = [...treeData]; | ||||||
|  |   while (queue.length) { | ||||||
|  |     const node = queue.shift(); | ||||||
|  |     if (node.key === item.key) { | ||||||
|  |       return node; | ||||||
|  |     } | ||||||
|  |     if (node.children) { | ||||||
|  |       queue.push(...node.children); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function loadChildren({ node, traverseAssociations, traverseFields, systemKeys, dataFields }) { | ||||||
|  |   const activeNode = findNode(dataFields.componentProps.treeData, node); | ||||||
|  |   let children = []; | ||||||
|  | 
 | ||||||
|  |   // 多对多和多对一只展示关系字段
 | ||||||
|  |   if (['belongsTo', 'belongsToMany'].includes(node.field.type)) { | ||||||
|  |     children = traverseAssociations(node.field.target, { | ||||||
|  |       exclude: systemKeys, | ||||||
|  |       prefix: node.key, | ||||||
|  |       maxDepth: 1, | ||||||
|  |     }); | ||||||
|  |   } else if (['hasOne', 'hasMany'].includes(node.field.type)) { | ||||||
|  |     children = traverseFields(node.field.target, { | ||||||
|  |       exclude: ['id', ...systemKeys], | ||||||
|  |       prefix: node.key, | ||||||
|  |       maxDepth: 1, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (children.length) { | ||||||
|  |     activeNode.children = children; | ||||||
|  |   } else { | ||||||
|  |     activeNode.isLeaf = true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return children; | ||||||
|  | } | ||||||
|  | |||||||
| @ -47,9 +47,9 @@ import { getTargetKey } from '../schema-component/antd/association-filter/utilts | |||||||
| import { useSchemaTemplateManager } from '../schema-templates'; | import { useSchemaTemplateManager } from '../schema-templates'; | ||||||
| import { useBlockTemplateContext } from '../schema-templates/BlockTemplate'; | import { useBlockTemplateContext } from '../schema-templates/BlockTemplate'; | ||||||
| import { FormDataTemplates } from './DataTemplates'; | import { FormDataTemplates } from './DataTemplates'; | ||||||
|  | import { EnableChildCollections } from './EnableChildCollections'; | ||||||
| import { FormLinkageRules } from './LinkageRules'; | import { FormLinkageRules } from './LinkageRules'; | ||||||
| import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks'; | import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks'; | ||||||
| import { EnableChildCollections } from './EnableChildCollections'; |  | ||||||
| 
 | 
 | ||||||
| interface SchemaSettingsProps { | interface SchemaSettingsProps { | ||||||
|   title?: any; |   title?: any; | ||||||
| @ -815,6 +815,10 @@ SchemaSettings.ModalItem = function ModalItem(props) { | |||||||
|           }) |           }) | ||||||
|           .then((values) => { |           .then((values) => { | ||||||
|             onSubmit(values); |             onSubmit(values); | ||||||
|  |             return values; | ||||||
|  |           }) | ||||||
|  |           .catch((err) => { | ||||||
|  |             console.error(err); | ||||||
|           }); |           }); | ||||||
|       }} |       }} | ||||||
|     > |     > | ||||||
| @ -897,7 +901,7 @@ SchemaSettings.DefaultSortingRules = function DefaultSortingRules(props) { | |||||||
|                       field: { |                       field: { | ||||||
|                         type: 'string', |                         type: 'string', | ||||||
|                         enum: sortFields, |                         enum: sortFields, | ||||||
|                         required:true, |                         required: true, | ||||||
|                         'x-decorator': 'FormItem', |                         'x-decorator': 'FormItem', | ||||||
|                         'x-component': 'Select', |                         'x-component': 'Select', | ||||||
|                         'x-component-props': { |                         'x-component-props': { | ||||||
| @ -1023,43 +1027,49 @@ SchemaSettings.DataTemplates = function DataTemplates(props) { | |||||||
|   const formSchema = findFormBlock(fieldSchema) || fieldSchema; |   const formSchema = findFormBlock(fieldSchema) || fieldSchema; | ||||||
|   const { templateData } = useDataTemplates(); |   const { templateData } = useDataTemplates(); | ||||||
| 
 | 
 | ||||||
|   return ( |   const schema = useMemo( | ||||||
|     <SchemaSettings.ModalItem |     () => ({ | ||||||
|       title={t('Form data templates')} |       type: 'object', | ||||||
|       components={{ ArrayCollapse, FormLayout }} |       title: t('Form data templates'), | ||||||
|       width={770} |       properties: { | ||||||
|       schema={ |         fieldReaction: { | ||||||
|         { |           'x-component': FormDataTemplates, | ||||||
|           type: 'object', |           'x-component-props': { | ||||||
|           title: t('Form data templates'), |             useProps: () => { | ||||||
|           properties: { |               return { | ||||||
|             fieldReaction: { |                 defaultValues: templateData, | ||||||
|               'x-component': FormDataTemplates, |                 collectionName, | ||||||
|               'x-component-props': { |               }; | ||||||
|                 useProps: () => { |  | ||||||
|                   return { |  | ||||||
|                     defaultValues: templateData, |  | ||||||
|                     collectionName, |  | ||||||
|                   }; |  | ||||||
|                 }, |  | ||||||
|               }, |  | ||||||
|             }, |             }, | ||||||
|           }, |           }, | ||||||
|         } as ISchema |         }, | ||||||
|       } |       }, | ||||||
|       onSubmit={(v) => { |     }), | ||||||
|         const data = v.fieldReaction || {}; |     [templateData], | ||||||
|         const schema = { |   ); | ||||||
|           ['x-uid']: formSchema['x-uid'], |   const onSubmit = useCallback((v) => { | ||||||
|           ['x-data-templates']: data, |     const data = v.fieldReaction || {}; | ||||||
|         }; | 
 | ||||||
|         formSchema['x-data-templates'] = data; |     // 当 Tree 组件开启 checkStrictly 属性时,会导致 checkedKeys 的值是一个对象,而不是数组,所以这里需要转换一下以支持旧版本
 | ||||||
|         dn.emit('patch', { |     data.items.forEach((item) => { | ||||||
|           schema, |       item.fields = Array.isArray(item.fields) ? item.fields : item.fields.checked; | ||||||
|         }); |     }); | ||||||
|         dn.refresh(); | 
 | ||||||
|       }} |     const schema = { | ||||||
|     /> |       ['x-uid']: formSchema['x-uid'], | ||||||
|  |       ['x-data-templates']: data, | ||||||
|  |     }; | ||||||
|  |     formSchema['x-data-templates'] = data; | ||||||
|  |     dn.emit('patch', { | ||||||
|  |       schema, | ||||||
|  |     }); | ||||||
|  |     dn.refresh(); | ||||||
|  |   }, []); | ||||||
|  |   const title = useMemo(() => t('Form data templates'), []); | ||||||
|  |   const components = useMemo(() => ({ ArrayCollapse, FormLayout }), []); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <SchemaSettings.ModalItem title={title} components={components} width={770} schema={schema} onSubmit={onSubmit} /> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user