feat: support mobile field related (#941)
Reviewed-on: daoyoucloud/tachybase#941 Reviewed-by: sealday <zhanglin@daoyoucloud.com> Co-authored-by: bai.zixv <bai.zixv@foxmail.com> Co-committed-by: bai.zixv <bai.zixv@foxmail.com>
This commit is contained in:
		
							parent
							
								
									9e63e1c392
								
							
						
					
					
						commit
						4f26e6e8c9
					
				| @ -1,9 +1,10 @@ | ||||
| import React from 'react'; | ||||
| import { RecursionField } from '@tachybase/schema'; | ||||
| import { useTabSearchAction } from './TabSearchAction'; | ||||
| import { RecursionField, useFieldSchema } from '@tachybase/schema'; | ||||
| import { useDesigner, useSchemaInitializerRender } from '@tachybase/client'; | ||||
| import { isTabSearchCollapsibleInputItem } from '../utils'; | ||||
| 
 | ||||
| export const TabSearch = () => { | ||||
|   const { fieldSchema, Designer, render, filterProperties } = useTabSearchAction(); | ||||
|   const { fieldSchema, Designer, render, filterProperties } = useAction(); | ||||
|   return ( | ||||
|     <> | ||||
|       <Designer /> | ||||
| @ -12,3 +13,25 @@ export const TabSearch = () => { | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| const useAction = () => { | ||||
|   const Designer = useDesigner(); | ||||
|   const fieldSchema = useFieldSchema(); | ||||
|   const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']); | ||||
| 
 | ||||
|   const filterProperties = (s) => { | ||||
|     if (!isTabSearchCollapsibleInputItem(s['x-component'])) { | ||||
|       return true; | ||||
|     } else { | ||||
|       // 保留第一个,如果进入这个判断一定有值,所以取0不会出错
 | ||||
|       return ( | ||||
|         s === Object.values(s.parent.properties).filter((s) => isTabSearchCollapsibleInputItem(s['x-component']))[0] | ||||
|       ); | ||||
|     } | ||||
|   }; | ||||
|   return { | ||||
|     fieldSchema, | ||||
|     Designer, | ||||
|     render, | ||||
|     filterProperties, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| @ -1,26 +0,0 @@ | ||||
| import { useDesigner, useSchemaInitializerRender } from '@tachybase/client'; | ||||
| import { useFieldSchema } from '@tachybase/schema'; | ||||
| import { isTabSearchCollapsibleInputItem } from '../utils'; | ||||
| 
 | ||||
| export const useTabSearchAction = () => { | ||||
|   const Designer = useDesigner(); | ||||
|   const fieldSchema = useFieldSchema(); | ||||
|   const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']); | ||||
| 
 | ||||
|   const filterProperties = (s) => { | ||||
|     if (!isTabSearchCollapsibleInputItem(s['x-component'])) { | ||||
|       return true; | ||||
|     } else { | ||||
|       // 保留第一个,如果进入这个判断一定有值,所以取0不会出错
 | ||||
|       return ( | ||||
|         s === Object.values(s.parent.properties).filter((s) => isTabSearchCollapsibleInputItem(s['x-component']))[0] | ||||
|       ); | ||||
|     } | ||||
|   }; | ||||
|   return { | ||||
|     fieldSchema, | ||||
|     Designer, | ||||
|     render, | ||||
|     filterProperties, | ||||
|   }; | ||||
| }; | ||||
| @ -23,7 +23,7 @@ export const ISelect = (props) => { | ||||
|       }} | ||||
|     > | ||||
|       <span style={{ textAlign: 'center', width: '100%' }}> | ||||
|         {options.find((option) => option.value === customLabelKey).label} | ||||
|         {options.find((option) => option.value === customLabelKey)?.label} | ||||
|       </span> | ||||
|       <DownOutline /> | ||||
|       {options.length <= 5 ? ( | ||||
|  | ||||
| @ -1,11 +1,11 @@ | ||||
| import { SortableItem, withDynamicSchemaProps } from '@tachybase/client'; | ||||
| import { ConfigProvider, Menu } from 'antd'; | ||||
| import React from 'react'; | ||||
| import { useTabSearchFieldItemAction } from './TabSearchFieldItemAction'; | ||||
| import { useAction } from './TabSearchFieldMItem'; | ||||
| 
 | ||||
| export const TabSearchFieldItem = withDynamicSchemaProps( | ||||
|   (props) => { | ||||
|     const { collectionField, Designer, items, onSelect } = useTabSearchFieldItemAction(props); | ||||
|     const { collectionField, Designer, items, onSelect } = useAction(props); | ||||
| 
 | ||||
|     if (!collectionField) { | ||||
|       return null; | ||||
|  | ||||
| @ -1,48 +0,0 @@ | ||||
| import React from 'react'; | ||||
| import { MenuProps } from 'antd'; | ||||
| import { useCollection, useDesigner } from '@tachybase/client'; | ||||
| import { useFieldSchema } from '@tachybase/schema'; | ||||
| import { useTranslation } from '../../../../../../locale'; | ||||
| 
 | ||||
| export const useTabSearchFieldItemAction = (props) => { | ||||
|   const { list, onSelected, valueKey: _valueKey, labelKey: _labelKey, filterKey } = props; | ||||
|   const { t } = useTranslation(); | ||||
|   const fieldSchema = useFieldSchema(); | ||||
|   const c = useCollection(); | ||||
|   const collectionField = React.useMemo( | ||||
|     () => c?.getField(fieldSchema['fieldName'] as any), | ||||
|     [c, fieldSchema['fieldName']], | ||||
|   ); | ||||
|   const Designer = useDesigner(); | ||||
|   const valueKey = _valueKey || collectionField?.targetKey || 'id'; | ||||
|   const labelKey = _labelKey || fieldSchema['x-component-props']?.fieldNames?.label || valueKey; | ||||
|   const onSelect = (itemKey) => { | ||||
|     const key = itemKey.keyPath?.[0] || itemKey; | ||||
|     onSelected([key], filterKey); | ||||
|   }; | ||||
|   const fieldNames = { | ||||
|     title: labelKey || valueKey, | ||||
|     key: valueKey, | ||||
|   }; | ||||
|   const items: MenuProps['items'] = []; | ||||
| 
 | ||||
|   if (list?.length) { | ||||
|     list.forEach((value) => { | ||||
|       items.push({ | ||||
|         label: value[fieldNames.title], | ||||
|         key: value[fieldNames.key], | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|   items?.unshift({ | ||||
|     label: t('all'), | ||||
|     key: 'all', | ||||
|   }); | ||||
| 
 | ||||
|   return { | ||||
|     collectionField, | ||||
|     Designer, | ||||
|     items, | ||||
|     onSelect, | ||||
|   }; | ||||
| }; | ||||
| @ -8,20 +8,25 @@ import { canBeOptionalField } from '../../utils'; | ||||
| export const useTabSearchFieldItemProps = () => { | ||||
|   const fieldSchema = useFieldSchema(); | ||||
|   const collection = useCollection(); | ||||
|   const optionalFieldList = useOptionalFieldList(); | ||||
|   const currentCollectionName = fieldSchema['x-component-props']?.currentCollection || collection?.name; | ||||
|   const optionalFieldList = useOptionalFieldList(currentCollectionName); | ||||
| 
 | ||||
|   const cm = useCollectionManager(); | ||||
|   const collectionField = useMemo( | ||||
|     () => collection?.getField(fieldSchema['fieldName'] as any), | ||||
|     [collection, fieldSchema['fieldName']], | ||||
|   ); | ||||
| 
 | ||||
|   const { onSelected } = useTabSearchCollapsibleInputItem(); | ||||
|   const result = { list: null, valueKey: '', labelKey: '', filterKey: '' }; | ||||
|   if (!collection) return; | ||||
|   result.valueKey = collectionField?.target ? cm.getCollection(collectionField.target)?.getPrimaryKey() : 'id'; | ||||
|   result.labelKey = fieldSchema['x-component-props']?.fieldNames?.label || result.valueKey; | ||||
|   const fieldInterface = fieldSchema['x-component-props'].interface; | ||||
|   const fieldInterface = fieldSchema['x-component-props']?.interface; | ||||
|   if (canBeOptionalField(fieldInterface)) { | ||||
|     const field = optionalFieldList.find((field) => field.name === fieldSchema['fieldName']); | ||||
|     const field = optionalFieldList.find( | ||||
|       (field) => field.name === fieldSchema['fieldName'].split('.').findLast(Boolean), | ||||
|     ); | ||||
|     const operatorMap = { | ||||
|       select: '$in', | ||||
|       multipleSelect: '$anyOf', | ||||
| @ -29,12 +34,15 @@ export const useTabSearchFieldItemProps = () => { | ||||
|       radioGroup: '$in', | ||||
|     }; | ||||
| 
 | ||||
|     fieldSchema['fieldName']; | ||||
| 
 | ||||
|     const _list = field?.uiSchema?.enum || []; | ||||
|     result.valueKey = 'value'; | ||||
|     result.labelKey = 'label'; | ||||
|     result.list = _list; | ||||
|     result.filterKey = `${field.name}.${operatorMap[field.interface]}`; | ||||
|     result.filterKey = `${fieldSchema['fieldName']}.${operatorMap[field?.interface]}`; | ||||
|   } | ||||
| 
 | ||||
|   return { | ||||
|     list: result.list, | ||||
|     valueKey: result.valueKey, | ||||
| @ -44,8 +52,9 @@ export const useTabSearchFieldItemProps = () => { | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const useOptionalFieldList = () => { | ||||
|   const collection = useCollection(); | ||||
| const useOptionalFieldList = (collectionName) => { | ||||
|   const cm = useCollectionManager(); | ||||
|   const collection = cm.getCollection(collectionName); | ||||
|   const currentFields = collection?.fields ?? []; | ||||
|   return currentFields.filter((field) => canBeOptionalField(field.interface) && field.uiSchema.enum); | ||||
| }; | ||||
|  | ||||
| @ -1,11 +1,13 @@ | ||||
| import { SortableItem, withDynamicSchemaProps } from '@tachybase/client'; | ||||
| import React from 'react'; | ||||
| import { useTabSearchFieldItemAction } from './TabSearchFieldItemAction'; | ||||
| import { SortableItem, useCollection, useDesigner, withDynamicSchemaProps } from '@tachybase/client'; | ||||
| import { useFieldSchema } from '@tachybase/schema'; | ||||
| import { MenuProps } from 'antd'; | ||||
| import { Tabs } from 'antd-mobile'; | ||||
| import React from 'react'; | ||||
| import { useTranslation } from '../../../../../../locale'; | ||||
| 
 | ||||
| export const TabSearchFieldMItem = withDynamicSchemaProps( | ||||
|   (props) => { | ||||
|     const { collectionField, Designer, items, onSelect } = useTabSearchFieldItemAction(props); | ||||
|     const { collectionField, Designer, items, onSelect } = useAction(props); | ||||
| 
 | ||||
|     if (!collectionField) { | ||||
|       return null; | ||||
| @ -15,7 +17,7 @@ export const TabSearchFieldMItem = withDynamicSchemaProps( | ||||
|       <SortableItem> | ||||
|         <Designer /> | ||||
|         <Tabs style={{ '--title-font-size': '12px', color: '#a5a5a5' }} onChange={onSelect}> | ||||
|           {items.map((value) => ( | ||||
|           {items.map((value: any) => ( | ||||
|             <Tabs.Tab title={value.label} key={value.key} /> | ||||
|           ))} | ||||
|         </Tabs> | ||||
| @ -24,3 +26,46 @@ export const TabSearchFieldMItem = withDynamicSchemaProps( | ||||
|   }, | ||||
|   { displayName: 'TabSearchFieldItem' }, | ||||
| ); | ||||
| 
 | ||||
| export const useAction = (props) => { | ||||
|   const { list, onSelected, valueKey: _valueKey, labelKey: _labelKey, filterKey } = props; | ||||
|   const { t } = useTranslation(); | ||||
|   const fieldSchema = useFieldSchema(); | ||||
|   const c = useCollection(); | ||||
|   const collectionField = React.useMemo( | ||||
|     () => c?.getField(fieldSchema['fieldName'] as any), | ||||
|     [c, fieldSchema['fieldName']], | ||||
|   ); | ||||
|   const Designer = useDesigner(); | ||||
|   const valueKey = _valueKey || collectionField?.targetKey || 'id'; | ||||
|   const labelKey = _labelKey || fieldSchema['x-component-props']?.fieldNames?.label || valueKey; | ||||
|   const onSelect = (itemKey) => { | ||||
|     const key = itemKey.keyPath?.[0] || itemKey; | ||||
|     onSelected([key], filterKey); | ||||
|   }; | ||||
|   const fieldNames = { | ||||
|     title: labelKey || valueKey, | ||||
|     key: valueKey, | ||||
|   }; | ||||
|   const items: MenuProps['items'] = []; | ||||
| 
 | ||||
|   if (list?.length) { | ||||
|     list.forEach((value) => { | ||||
|       items.push({ | ||||
|         label: value[fieldNames.title], | ||||
|         key: value[fieldNames.key], | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|   items?.unshift({ | ||||
|     label: t('all'), | ||||
|     key: 'all', | ||||
|   }); | ||||
| 
 | ||||
|   return { | ||||
|     collectionField, | ||||
|     Designer, | ||||
|     items, | ||||
|     onSelect, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| @ -1,12 +1,6 @@ | ||||
| // 关系字段类型
 | ||||
| import { useEffect, useMemo } from 'react'; | ||||
| import { | ||||
|   useCollection, | ||||
|   useCollectionManager, | ||||
|   useDataSourceHeaders, | ||||
|   useFilterBlock, | ||||
|   useRequest, | ||||
| } from '@tachybase/client'; | ||||
| import { useCollection, useCollectionManager, useDataSourceHeaders, useRequest } from '@tachybase/client'; | ||||
| import { useField, useFieldSchema } from '@tachybase/schema'; | ||||
| import _ from 'lodash'; | ||||
| import { useTabSearchCollapsibleInputItem } from './hooks'; | ||||
| @ -24,7 +18,10 @@ export const useTabSearchFieldItemRelatedProps = () => { | ||||
|     () => collection?.getField(fieldSchema['fieldName'] as any), | ||||
|     [collection, fieldSchema['fieldName']], | ||||
|   ); | ||||
|   const collectionFieldName = collectionField?.name; | ||||
|   const currentCollectionName = fieldSchema['x-component-props'].currentCollection; | ||||
|   const collectionFieldName = | ||||
|     currentCollectionName === collection?.name ? collectionField?.name : fieldSchema['fieldName']; | ||||
| 
 | ||||
|   const fieldInterface = fieldSchema['x-component-props'].interface; | ||||
|   const result = { list: null, valueKey: '', labelKey: '', filterKey: '' }; | ||||
|   const { onSelected } = useTabSearchCollapsibleInputItem(); | ||||
|  | ||||
| @ -1,3 +1,2 @@ | ||||
| export * from './TabSearch'; | ||||
| export * from './TabSearchAction'; | ||||
| export * from './field-item'; | ||||
|  | ||||
| @ -0,0 +1,371 @@ | ||||
| import { | ||||
|   CollectionFieldOptions_deprecated, | ||||
|   CollectionManager, | ||||
|   FieldOptions, | ||||
|   InheritanceCollectionMixin, | ||||
|   SchemaInitializerChildren, | ||||
|   SchemaInitializerItemType, | ||||
|   isAssocField, | ||||
|   useCollection, | ||||
|   useCollectionManager, | ||||
|   useDataSource, | ||||
|   useDataSourceManager, | ||||
| } from '@tachybase/client'; | ||||
| import React, { useCallback } from 'react'; | ||||
| import { tval } from '../../../../../locale'; | ||||
| import { canBeDataField, canBeOptionalField, canBeRelatedField, canBeSearchField } from '../utils'; | ||||
| import { useIsMobile } from '../components/field-item/hooks'; | ||||
| 
 | ||||
| interface ItemInterface { | ||||
|   field: FieldOptions; | ||||
|   label: string; | ||||
|   schemaName: string; | ||||
|   collectionName: string; | ||||
|   getCollectionFields; | ||||
|   processedCollections: string[]; | ||||
|   collectionManager: CollectionManager; | ||||
|   currentCollection?: string; | ||||
| } | ||||
| 
 | ||||
| export const TabSearchAssociatedFields = () => { | ||||
|   // NOTE: 这里的 useAssociationFields 借鉴的是 useFilterAssociatedFormItemInitializerFields
 | ||||
|   const associationFieldsText = useAssociationFields('textField'); | ||||
|   const associationFieldsChoice = useAssociationFields('choiceField'); | ||||
|   // 这里的 children, 后续实际不是用来渲染的,  是个普通的 props 字段
 | ||||
|   // XXX: refactor, 不好的设计, 用 children 来做普通 props 的数据传递
 | ||||
|   const children: any[] = [ | ||||
|     { | ||||
|       type: 'itemGroup', | ||||
|       title: tval('Display association Textfields', false), | ||||
|       children: associationFieldsText, | ||||
|     }, | ||||
|     { | ||||
|       type: 'itemGroup', | ||||
|       title: tval('Display association Choicefields', false), | ||||
|       children: associationFieldsChoice, | ||||
|     }, | ||||
|   ]; | ||||
| 
 | ||||
|   return <SchemaInitializerChildren>{children}</SchemaInitializerChildren>; | ||||
| }; | ||||
| 
 | ||||
| function useAssociationFields(type: 'textField' | 'choiceField') { | ||||
|   const collection = useCollection<{ allFields: FieldOptions[] }>(); | ||||
| 
 | ||||
|   const { name, allFields } = collection || {}; | ||||
| 
 | ||||
|   const { getCollectionFields } = useCollectionFields_custom(); | ||||
|   const isMobield = useIsMobile(); | ||||
| 
 | ||||
|   const cm = useCollectionManager(); | ||||
| 
 | ||||
|   const filteredAssociationFields = allFields?.filter(({ interface: _interface }) => { | ||||
|     return canBeRelatedField(_interface); | ||||
|   }); | ||||
| 
 | ||||
|   const paramsItem = { | ||||
|     collectionName: name, | ||||
|     getCollectionFields, | ||||
|     processedCollections: [], | ||||
|     collectionManager: cm, | ||||
|   }; | ||||
| 
 | ||||
|   let mappedAssociationFields = []; | ||||
|   if (type === 'textField') { | ||||
|     mappedAssociationFields = filteredAssociationFields.map((field) => { | ||||
|       // 将关联表字段转换为 schema 树
 | ||||
|       const label = canBeRelatedField(field.interface) | ||||
|         ? cm.getCollection(`${name}.${field.name}`)?.titleField | ||||
|         : field.name; | ||||
| 
 | ||||
|       const result = getItemInput( | ||||
|         { | ||||
|           ...paramsItem, | ||||
|           field, | ||||
|           label, | ||||
|           schemaName: field.name, | ||||
|         }, | ||||
|         isMobield, | ||||
|       ); | ||||
|       return result; | ||||
|     }); | ||||
|   } else if (type === 'choiceField') { | ||||
|     mappedAssociationFields = filteredAssociationFields.map((field) => { | ||||
|       // 将关联表字段转换为 schema 树
 | ||||
|       const label = cm.getCollection(field.target)?.getPrimaryKey() || 'id'; | ||||
|       const result = getItemChoice( | ||||
|         { | ||||
|           ...paramsItem, | ||||
|           field, | ||||
|           label, | ||||
|           schemaName: field.name, | ||||
|         }, | ||||
|         isMobield, | ||||
|       ); | ||||
|       return result; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   const associationFields = mappedAssociationFields.filter(Boolean); | ||||
|   return associationFields; | ||||
| } | ||||
| 
 | ||||
| // NOTE: 递归生成 schema 树
 | ||||
| function getItemInput(params: ItemInterface, isMobield?: boolean) { | ||||
|   const { | ||||
|     field, | ||||
|     label, | ||||
|     schemaName, | ||||
|     collectionName, | ||||
|     getCollectionFields, | ||||
|     processedCollections, | ||||
|     collectionManager: cm, | ||||
|   } = params; | ||||
|   const { interface: _interface } = field; | ||||
|   const isCanBeOptional = canBeOptionalField(_interface); | ||||
|   const isCanBeRelated = canBeRelatedField(_interface); | ||||
| 
 | ||||
|   // NOTE: 目前只支持 m2o 和 obo 关联
 | ||||
|   if (['m2o', 'obo'].includes(_interface)) { | ||||
|     if (processedCollections.includes(field.target)) return null; | ||||
|     const subFields = getCollectionFields(field.target); | ||||
|     return { | ||||
|       type: 'subMenu', | ||||
|       name: field.uiSchema?.title, | ||||
|       title: field.uiSchema?.title, | ||||
|       // 将所有的关联字段排在最后
 | ||||
|       children: subFields | ||||
|         .toSorted((a, b) => +Object.hasOwn(a, 'target') - +Object.hasOwn(b, 'target')) | ||||
|         .map((subField) => { | ||||
|           const label = canBeRelatedField(field.interface) | ||||
|             ? cm.getCollection(`${name}.${field.name}`)?.titleField | ||||
|             : field.name; | ||||
|           return getItemInput({ | ||||
|             field: subField, | ||||
|             label, | ||||
|             schemaName: `${schemaName}.${subField.name}`, | ||||
|             collectionName: collectionName, | ||||
|             getCollectionFields: getCollectionFields, | ||||
|             processedCollections: [...processedCollections, field.target], | ||||
|             collectionManager: cm, | ||||
|           }); | ||||
|         }) | ||||
|         .filter(Boolean), | ||||
|     } as SchemaInitializerItemType; | ||||
|   } | ||||
|   // 然后排除其他类型的关联字段
 | ||||
|   if (isAssocField(field)) return null; | ||||
| 
 | ||||
|   if (!(canBeSearchField(_interface) && !canBeRelatedField(_interface) && !canBeDataField(_interface))) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   // 制造 schema
 | ||||
|   // const schema = ;
 | ||||
| 
 | ||||
|   // {
 | ||||
|   //   type: 'itemGroup',
 | ||||
|   //   name: 'textFields',
 | ||||
|   //   title: tval('Text fields'),
 | ||||
|   //   useChildren: useChildrenTextField,
 | ||||
|   // },
 | ||||
|   // {
 | ||||
|   //   type: 'itemGroup',
 | ||||
|   //   name: 'choicesFields',
 | ||||
|   //   title: tval('Choices fields'),
 | ||||
|   //   useChildren: useChildrenChoiceField,
 | ||||
|   // },
 | ||||
| 
 | ||||
|   return { | ||||
|     // type: 'item',
 | ||||
|     name: field.uiSchema?.title || field.name, | ||||
|     title: field.uiSchema?.title || field.name, | ||||
|     Component: 'TabSearchFieldSchemaInitializerGadget', | ||||
|     // remove: removeGridFormItem,
 | ||||
|     schema: { | ||||
|       // type: 'string',
 | ||||
|       type: 'void', | ||||
|       name: schemaName, | ||||
|       title: `${field.uiSchema?.title}`, | ||||
|       fieldName: `${schemaName}`, | ||||
|       // 'x-designer': 'FormItem.FilterFormDesigner',
 | ||||
|       // 'x-designer-props': {
 | ||||
|       //   // 在 useOperatorList 中使用,用于获取对应的操作符列表
 | ||||
|       //   interface: field.interface,
 | ||||
|       // },
 | ||||
|       'x-toolbar': 'CollapseItemSchemaToolbar', | ||||
|       'x-settings': 'fieldSettings:TabSearchItem', | ||||
|       'x-collection-field': `${collectionName}.${schemaName}`, | ||||
|       // 'x-decorator': 'FormItem',
 | ||||
|       // 'x-component': 'CollectionField',
 | ||||
|       'x-component': matchTruthValue({ | ||||
|         ['TabSearchCollapsibleInputMItem']: isMobield, | ||||
|         ['TabSearchCollapsibleInputItem']: !isMobield, | ||||
|       }), | ||||
|       'x-component-props': { | ||||
|         fieldNames: { | ||||
|           label, | ||||
|         }, | ||||
|         interface: field.interface, | ||||
|         collectionName: collectionName, | ||||
|         correlation: field.name, | ||||
|       }, | ||||
|       'x-use-component-props': matchTruthValue({ | ||||
|         ['useTabSearchFieldItemProps']: isCanBeOptional, | ||||
|         ['useTabSearchFieldItemRelatedProps']: isCanBeRelated, | ||||
|       }), | ||||
| 
 | ||||
|       // 'x-read-pretty': false,
 | ||||
|     }, | ||||
|   } as SchemaInitializerItemType; | ||||
| } | ||||
| // NOTE: 递归生成 schema 树
 | ||||
| function getItemChoice(params: ItemInterface, isMobield?: boolean) { | ||||
|   const { | ||||
|     field, | ||||
|     label, | ||||
|     schemaName, | ||||
|     collectionName, | ||||
|     getCollectionFields, | ||||
|     processedCollections, | ||||
|     collectionManager: cm, | ||||
|     currentCollection, | ||||
|   } = params; | ||||
| 
 | ||||
|   const { interface: _interface } = field; | ||||
|   const isCanBeOptional = canBeOptionalField(_interface); | ||||
|   const isCanBeRelated = canBeRelatedField(_interface); | ||||
| 
 | ||||
|   // NOTE: 目前只支持 m2o 和 obo 关联
 | ||||
|   if (['m2o', 'obo'].includes(_interface) && !field.choiceTag) { | ||||
|     if (processedCollections.includes(field.target)) return null; | ||||
|     const subFields = getCollectionFields(field.target); | ||||
|     return { | ||||
|       type: 'subMenu', | ||||
|       name: field.uiSchema?.title, | ||||
|       title: field.uiSchema?.title, | ||||
|       // 将所有的关联字段排在最后
 | ||||
|       children: subFields | ||||
|         .reduce((acc, subField) => { | ||||
|           if (Object.hasOwn(subField, 'target') && ['m2o', 'obo'].includes(subField.interface)) { | ||||
|             // NOTE: 将当前的关联字段复制一份, 作为当前层级的选择展示,并打上选择标记 choiceTag
 | ||||
|             acc.push({ | ||||
|               ...subField, | ||||
|               choiceTag: true, | ||||
|             }); | ||||
|           } | ||||
|           return [...acc, subField]; | ||||
|         }, []) | ||||
|         // NOTE: 排序规则: 1. 关联字段放最后 2. 选择复制字段次之 3. 其他字段置前
 | ||||
|         .toSorted( | ||||
|           (a, b) => | ||||
|             +Object.hasOwn(a, 'target') * 2 - | ||||
|             +Object.hasOwn(a, 'choiceTag') - | ||||
|             +Object.hasOwn(b, 'target') * 2 - | ||||
|             +Object.hasOwn(b, 'choiceTag'), | ||||
|         ) | ||||
|         .map((subField) => { | ||||
|           const label = cm.getCollection(field.target)?.getPrimaryKey() || 'id'; | ||||
|           return getItemChoice({ | ||||
|             field: subField, | ||||
|             label, | ||||
|             schemaName: `${schemaName}.${subField.name}`, | ||||
|             collectionName: collectionName, | ||||
|             getCollectionFields: getCollectionFields, | ||||
|             processedCollections: [...processedCollections, field.target], | ||||
|             collectionManager: cm, | ||||
|             currentCollection: field.target, | ||||
|           }); | ||||
|         }) | ||||
|         .filter(Boolean), | ||||
|     } as SchemaInitializerItemType; | ||||
|   } | ||||
| 
 | ||||
|   // 然后排除其他类型的关联字段, 并且不要删除打上选择标记 choiceTag 的关联字段
 | ||||
|   if (isAssocField(field) && !field.choiceTag) return null; | ||||
| 
 | ||||
|   if (!canBeOptionalField(_interface) && !canBeRelatedField(_interface)) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   return { | ||||
|     type: 'item', | ||||
|     name: field.uiSchema?.title || field.name, | ||||
|     title: field.uiSchema?.title || field.name, | ||||
|     Component: 'TabSearchFieldSchemaInitializerGadget', | ||||
|     // remove: removeGridFormItem,
 | ||||
|     schema: { | ||||
|       // type: 'string',
 | ||||
|       type: 'void', | ||||
|       name: schemaName, | ||||
|       title: `${field.uiSchema?.title}`, | ||||
|       fieldName: `${schemaName}`, | ||||
|       // 'x-designer': 'FormItem.FilterFormDesigner',
 | ||||
|       // 'x-designer-props': {
 | ||||
|       //   // 在 useOperatorList 中使用,用于获取对应的操作符列表
 | ||||
|       //   interface: field.interface,
 | ||||
|       // },
 | ||||
|       'x-toolbar': 'CollapseItemSchemaToolbar', | ||||
|       'x-settings': 'fieldSettings:TabSearchItem', | ||||
|       // 'x-read-pretty': false,
 | ||||
|       'x-collection-field': `${collectionName}.${schemaName}`, | ||||
|       // 'x-decorator': 'FormItem',
 | ||||
|       // 'x-component': 'CollectionField',
 | ||||
|       'x-component': matchTruthValue({ | ||||
|         ['TabSearchFieldMItem']: isMobield, | ||||
|         ['TabSearchFieldItem']: !isMobield, | ||||
|       }), | ||||
|       'x-component-props': { | ||||
|         fieldNames: { | ||||
|           label, | ||||
|         }, | ||||
|         interface: field.interface, | ||||
|         collectionName: collectionName, | ||||
|         correlation: field.name, | ||||
|         currentCollection, | ||||
|       }, | ||||
|       'x-use-component-props': matchTruthValue({ | ||||
|         ['useTabSearchFieldItemProps']: isCanBeOptional, | ||||
|         ['useTabSearchFieldItemRelatedProps']: isCanBeRelated, | ||||
|       }), | ||||
|     }, | ||||
|   } as SchemaInitializerItemType; | ||||
| } | ||||
| 
 | ||||
| // XXX: 为了不使用标记废弃的 api, 模拟复制原方法实现; 参考 useCollectionManager_deprecated
 | ||||
| function useCollectionFields_custom(dataSourceName?: string) { | ||||
|   const dm = useDataSourceManager(); | ||||
|   const cm = useCollectionManager(); | ||||
|   const dataSource = useDataSource(); | ||||
|   const dataSourceNameValue = dataSourceName || dataSource?.key || undefined; | ||||
| 
 | ||||
|   const getCm = useCallback( | ||||
|     (dataSource?: string) => { | ||||
|       if (cm && !dataSource) { | ||||
|         return cm; | ||||
|       } else { | ||||
|         return dm?.getDataSource(dataSource || dataSourceNameValue)?.collectionManager; | ||||
|       } | ||||
|     }, | ||||
|     [cm, dm, dataSourceNameValue], | ||||
|   ); | ||||
| 
 | ||||
|   const getCollectionFields = useCallback( | ||||
|     (name: any, customDataSource?: string): CollectionFieldOptions_deprecated[] => { | ||||
|       if (!name) return []; | ||||
|       const collection = getCm(customDataSource)?.getCollection<InheritanceCollectionMixin>(name); | ||||
|       return collection?.getAllFields?.() || collection?.getFields() || []; | ||||
|     }, | ||||
|     [dm, getCm], | ||||
|   ); | ||||
| 
 | ||||
|   return { | ||||
|     getCollectionFields, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function matchTruthValue(obj: Record<string, boolean>): string { | ||||
|   const result = Object.entries(obj).find(([, value]) => value); | ||||
|   const [key] = result || []; | ||||
|   return key; | ||||
| } | ||||
| @ -1,6 +1,5 @@ | ||||
| import { DataBlockInitializer, useSchemaInitializer, useSchemaInitializerItem } from '@tachybase/client'; | ||||
| import React from 'react'; | ||||
| import _ from 'lodash'; | ||||
| import { DataBlockInitializer, Icon, useSchemaInitializer, useSchemaInitializerItem } from '@tachybase/client'; | ||||
| import { createTabSearchBlockSchema } from '../create/createTabSearchBlockSchema'; | ||||
| 
 | ||||
| export const TabSearchBlockInitializer = (props) => { | ||||
|  | ||||
| @ -1,78 +1,100 @@ | ||||
| import { SchemaInitializer, SchemaInitializerItemType, useCollection, useCollectionManager } from '@tachybase/client'; | ||||
| import _ from 'lodash'; | ||||
| import { canBeDataField, canBeOptionalField, canBeRelatedField, canBeSearchField } from '../utils'; | ||||
| import { createTabSearchItemSchema } from '../create/createTabSearchItemSchema'; | ||||
| import { useIsMobile } from '../components/field-item/hooks'; | ||||
| import { SchemaInitializer, useCollection, useCollectionManager } from '@tachybase/client'; | ||||
| import { tval } from '../../../../../locale'; | ||||
| 
 | ||||
| const textFieldsItem: SchemaInitializerItemType<any> = { | ||||
|   name: 'textFields', | ||||
|   type: 'itemGroup', | ||||
|   title: tval('Text fields'), | ||||
|   useChildren() { | ||||
|     const collection = useCollection(); | ||||
|     const associatedFields = collection.fields; | ||||
|     const cm = useCollectionManager(); | ||||
|     const isMobield = useIsMobile(); | ||||
|     const itemComponent = isMobield ? 'TabSearchCollapsibleInputMItem' : 'TabSearchCollapsibleInputItem'; | ||||
|     const children = associatedFields | ||||
|       .map((field) => { | ||||
|         if ( | ||||
|           !field['isForeignKey'] && | ||||
|           (canBeSearchField(field.interface) || canBeRelatedField(field.interface) || canBeDataField(field.interface)) | ||||
|         ) { | ||||
|           const label = canBeRelatedField(field.interface) | ||||
|             ? cm.getCollection(`${collection.name}.${field.name}`).titleField | ||||
|             : field.name; | ||||
|           return createTabSearchItemSchema({ field, itemComponent, label, collection, type: 'text' }); | ||||
|         } | ||||
|       }) | ||||
|       .filter(Boolean); | ||||
|     return children; | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| const choicesFieldsItem: SchemaInitializerItemType<any> = { | ||||
|   name: 'choicesFields', | ||||
|   type: 'itemGroup', | ||||
|   title: tval('Choices fields'), | ||||
|   useChildren() { | ||||
|     const collection = useCollection(); | ||||
|     const optionalList = collection.fields; | ||||
|     const cm = useCollectionManager(); | ||||
|     const isMobield = useIsMobile(); | ||||
|     const itemComponent = isMobield ? 'TabSearchFieldMItem' : 'TabSearchFieldItem'; | ||||
|     const optionalChildren = optionalList | ||||
|       .map((field) => { | ||||
|         const label = cm.getCollection(field.target)?.getPrimaryKey() || 'id'; | ||||
|         if (canBeOptionalField(field.interface)) { | ||||
|           return createTabSearchItemSchema({ | ||||
|             field, | ||||
|             itemComponent, | ||||
|             label, | ||||
|             itemUseProps: 'useTabSearchFieldItemProps', | ||||
|             type: 'choices', | ||||
|           }); | ||||
|         } else if (canBeRelatedField(field.interface)) { | ||||
|           return createTabSearchItemSchema({ | ||||
|             field, | ||||
|             itemComponent, | ||||
|             label, | ||||
|             itemUseProps: 'useTabSearchFieldItemRelatedProps', | ||||
|             type: 'choices', | ||||
|           }); | ||||
|         } | ||||
|       }) | ||||
|       .filter(Boolean); | ||||
| 
 | ||||
|     return optionalChildren; | ||||
|   }, | ||||
| }; | ||||
| import { useIsMobile } from '../components/field-item/hooks'; | ||||
| import { createTabSearchItemSchema } from '../create/createTabSearchItemSchema'; | ||||
| import { canBeDataField, canBeOptionalField, canBeRelatedField, canBeSearchField } from '../utils'; | ||||
| import { TabSearchAssociatedFields } from './TabSearchAssociated.fields'; | ||||
| 
 | ||||
| export const TabSearchFieldSchemaInitializer = new SchemaInitializer({ | ||||
|   name: 'tabSearch:configureFields', | ||||
|   style: { marginTop: 16 }, | ||||
|   icon: 'SettingOutlined', | ||||
|   title: tval('Configure fields'), | ||||
|   items: [textFieldsItem, choicesFieldsItem], | ||||
|   items: [ | ||||
|     { | ||||
|       name: 'textFields', | ||||
|       type: 'itemGroup', | ||||
|       title: tval('Text fields'), | ||||
|       useChildren: useChildrenTextField, | ||||
|     }, | ||||
|     { | ||||
|       name: 'choicesFields', | ||||
|       type: 'itemGroup', | ||||
|       title: tval('Choices fields'), | ||||
|       useChildren: useChildrenChoiceField, | ||||
|     }, | ||||
|     { | ||||
|       name: 'divider', | ||||
|       type: 'divider', | ||||
|     }, | ||||
|     { | ||||
|       name: 'associationFields', | ||||
|       Component: TabSearchAssociatedFields, | ||||
|     }, | ||||
|     { | ||||
|       name: 'divider', | ||||
|       type: 'divider', | ||||
|     }, | ||||
|     { | ||||
|       title: tval('Add text', false), | ||||
|       Component: 'MarkdownFormItemInitializer', | ||||
|       name: 'addText', | ||||
|     }, | ||||
|   ], | ||||
| }); | ||||
| 
 | ||||
| // 文本字段
 | ||||
| function useChildrenTextField() { | ||||
|   const collection = useCollection(); | ||||
|   const associatedFields = collection.fields; | ||||
|   const cm = useCollectionManager(); | ||||
|   const isMobield = useIsMobile(); | ||||
|   const itemComponent = isMobield ? 'TabSearchCollapsibleInputMItem' : 'TabSearchCollapsibleInputItem'; | ||||
|   const children = associatedFields | ||||
|     .map((field) => { | ||||
|       if ( | ||||
|         !field['isForeignKey'] && | ||||
|         (canBeSearchField(field.interface) || canBeRelatedField(field.interface) || canBeDataField(field.interface)) | ||||
|       ) { | ||||
|         const label = canBeRelatedField(field.interface) | ||||
|           ? cm.getCollection(`${collection.name}.${field.name}`).titleField | ||||
|           : field.name; | ||||
|         return createTabSearchItemSchema({ field, itemComponent, label, collection, type: 'text' }); | ||||
|       } | ||||
|     }) | ||||
|     .filter(Boolean); | ||||
|   return children; | ||||
| } | ||||
| 
 | ||||
| // 选择字段
 | ||||
| function useChildrenChoiceField() { | ||||
|   const collection = useCollection(); | ||||
|   const optionalList = collection.fields; | ||||
|   const cm = useCollectionManager(); | ||||
|   const isMobield = useIsMobile(); | ||||
|   const itemComponent = isMobield ? 'TabSearchFieldMItem' : 'TabSearchFieldItem'; | ||||
|   const optionalChildren = optionalList | ||||
|     .map((field) => { | ||||
|       const label = cm.getCollection(field.target)?.getPrimaryKey() || 'id'; | ||||
|       if (canBeOptionalField(field.interface)) { | ||||
|         return createTabSearchItemSchema({ | ||||
|           field, | ||||
|           itemComponent, | ||||
|           label, | ||||
|           itemUseProps: 'useTabSearchFieldItemProps', | ||||
|           type: 'choices', | ||||
|         }); | ||||
|       } else if (canBeRelatedField(field.interface)) { | ||||
|         return createTabSearchItemSchema({ | ||||
|           field, | ||||
|           itemComponent, | ||||
|           label, | ||||
|           itemUseProps: 'useTabSearchFieldItemRelatedProps', | ||||
|           type: 'choices', | ||||
|         }); | ||||
|       } | ||||
|     }) | ||||
|     .filter(Boolean); | ||||
| 
 | ||||
|   return optionalChildren; | ||||
| } | ||||
|  | ||||
| @ -71,11 +71,14 @@ export const TabSearchItemFieldSettings = new SchemaSettings({ | ||||
|               const fieldSchema = useFieldSchema(); | ||||
|               const { t } = useTranslation(); | ||||
|               const cm = useCollectionManager(); | ||||
|               const c = useCollection(); | ||||
|               const fieldCollection = fieldSchema['x-component-props']?.['collectionName']; | ||||
|               const correlation = fieldSchema['x-component-props']?.['correlation']; | ||||
|               const collection = useCollection(); | ||||
| 
 | ||||
|               const propsInSchema = fieldSchema['x-component-props'] || {}; | ||||
|               const { collectionName, currentCollection, correlation } = propsInSchema; | ||||
|               const fieldCollection = currentCollection || collectionName || collection.name; | ||||
| 
 | ||||
|               const collectionField = | ||||
|                 c.getField(fieldSchema['fieldName']) || | ||||
|                 collection.getField(fieldSchema['fieldName']) || | ||||
|                 cm.getCollectionField(fieldSchema['x-collection-field']) || | ||||
|                 cm.getCollection(fieldCollection + '.' + correlation); | ||||
|               const compile = useCompile(); | ||||
|  | ||||
| @ -11,7 +11,14 @@ export function generateNTemplate(key: string) { | ||||
|   return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`; | ||||
| } | ||||
| 
 | ||||
| export const tval = (key: string) => nTval(key, { ns: NAMESPACE }); | ||||
| export const tval = (key: string, haveNamespace: boolean = true) => { | ||||
|   if (haveNamespace) { | ||||
|     return nTval(key, { ns: NAMESPACE }) | ||||
|   } else { | ||||
|     return nTval(key) | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| export function useTranslation() { | ||||
|   return useT([NAMESPACE, '@tachybase/plugin-mobile-client', 'client'], { | ||||
|  | ||||
| @ -29,8 +29,10 @@ | ||||
|   "Mobile client access address": "Mobile client access address", | ||||
|   "The full address is": "The full address is", | ||||
|   "Preview": "Preview", | ||||
|    "Choices fields":"Choices fields", | ||||
|   "Choices fields":"Choices fields", | ||||
|   "Text fields":"Text fields", | ||||
|   "Display association Textfields": "Display association Textfields", | ||||
|   "Display association Choicefields": "Display association Choicefields", | ||||
|   "Generic properties":"Generic properties", | ||||
|   "Search":"Search", | ||||
|   "Please enter search content":"Please enter search content", | ||||
|  | ||||
| @ -29,8 +29,10 @@ | ||||
|   "Mobile client access address": "移动端访问地址", | ||||
|   "The full address is": "完整的地址是", | ||||
|   "Preview": "预览", | ||||
|    "Choices fields":"选择字段", | ||||
|   "Choices fields":"选择字段", | ||||
|   "Text fields":"文本字段", | ||||
|   "Display association Textfields": "文本关联字段", | ||||
|   "Display association Choicefields": "选择关联字段", | ||||
|   "Generic properties":"通用属性", | ||||
|   "Search":"搜索", | ||||
|   "Please enter search content":"请输入查询内容", | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user