diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index b646acfc1..0fcddc7b7 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -1100,8 +1100,7 @@ export const useAssociationFilterBlockProps = () => { labelKey, }; }; - -function getAssociationPath(str) { +export function getAssociationPath(str) { const lastIndex = str.lastIndexOf('.'); if (lastIndex !== -1) { return str.substring(0, lastIndex); diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts index ad83395bb..b1e2818d2 100644 --- a/packages/core/client/src/locale/en_US.ts +++ b/packages/core/client/src/locale/en_US.ts @@ -710,5 +710,8 @@ export default { "Allow add new, update and delete actions":"Allow add new, update and delete actions", "Date display format":"Date display format", "Assign data scope for the template":"Assign data scope for the template", - "Table selected records":"Table selected records" + "Table selected records":"Table selected records", + "Sync successfully":"Sync successfully", + "Sync from form fields":"Sync from form fields", + "Select all":"Select all" }; diff --git a/packages/core/client/src/locale/ja_JP.ts b/packages/core/client/src/locale/ja_JP.ts index 175508b79..d23801238 100644 --- a/packages/core/client/src/locale/ja_JP.ts +++ b/packages/core/client/src/locale/ja_JP.ts @@ -621,4 +621,7 @@ export default { "Allow add new, update and delete actions":"削除変更操作の許可", "Date display format":"日付表示形式", "Assign data scope for the template":"テンプレートのデータ範囲の指定", + "Sync successfully":"同期成功", + "Sync from form fields":"フォームフィールドの同期", + "Select all":"すべて選択" } diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 73e2c4f5e..62a9a72a1 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -795,5 +795,8 @@ export default { "Allow add new, update and delete actions":"允许增删改操作", "Date display format":"日期显示格式", "Assign data scope for the template":"为模板指定数据范围", - "Table selected records":"表格中选中的记录" + "Table selected records":"表格中选中的记录", + "Sync successfully":"同步成功", + "Sync from form fields":"同步表单字段", + "Select all":"全选" } diff --git a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx index 25df35327..bdc60bbb1 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx @@ -1,8 +1,8 @@ -import { connect, ISchema, mapProps, useField, useFieldSchema } from '@formily/react'; +import { connect, ISchema, mapProps, useField, useFieldSchema, useForm } from '@formily/react'; import { isValid, uid } from '@formily/shared'; import { Tree as AntdTree } from 'antd'; import { cloneDeep } from 'lodash'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useDesignable } from '../..'; import { useCollection, useCollectionManager } from '../../../collection-manager'; @@ -12,15 +12,21 @@ import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks import { useLinkageAction } from './hooks'; import { requestSettingsSchema } from './utils'; import { useRecord } from '../../../record-provider'; +import { useSyncFromForm } from '../../../schema-settings/DataTemplates/utils'; const Tree = connect( AntdTree, mapProps((props, field: any) => { + const [checkedKeys, setCheckedKeys] = useState(props.defaultCheckedKeys || []); + const onCheck = (checkedKeys) => { + setCheckedKeys(checkedKeys); + field.value = checkedKeys; + }; + field.onCheck = onCheck; return { ...props, - onCheck: (checkedKeys) => { - field.value = checkedKeys; - }, + checkedKeys, + onCheck, }; }), ); @@ -218,6 +224,28 @@ function SaveMode() { ); } +const findFormBlock = (schema) => { + const formSchema = schema.reduceProperties((_, s) => { + if (s['x-decorator'] === 'FormBlockProvider') { + return s; + } else { + return findFormBlock(s); + } + }, null); + return formSchema; +}; + +const getAllkeys = (data, result) => { + for (let i = 0; i < data?.length; i++) { + const { children, ...rest } = data[i]; + result.push(rest.key); + if (children) { + getAllkeys(children, result); + } + } + return result; +}; + function DuplicationMode() { const { dn } = useDesignable(); const { t } = useTranslation(); @@ -227,7 +255,27 @@ function DuplicationMode() { const { collectionList, getEnableFieldTree, getOnLoadData, getOnCheck } = useCollectionState(name); const duplicateValues = cloneDeep(fieldSchema['x-component-props'].duplicateFields || []); const record = useRecord(); - + const syncCallBack = useCallback((treeData, selectFields, form) => { + form.query('duplicateFields').take((f) => { + f.componentProps.treeData = treeData; + f.componentProps.defaultCheckedKeys = selectFields; + f.setInitialValue(selectFields); + f?.onCheck(selectFields); + form.setValues({ ...form.values, treeData }); + }); + }, []); + const useSelectAllFields = (form) => { + return { + async run() { + form.query('duplicateFields').take((f) => { + const selectFields = getAllkeys(f.componentProps.treeData, []); + f.componentProps.defaultCheckedKeys = selectFields; + f.setInitialValue(selectFields); + f?.onCheck(selectFields); + }); + }, + }; + }; return ( { + const formSchema = useMemo(() => findFormBlock(fieldSchema), [fieldSchema]); + return useSyncFromForm( + formSchema, + fieldSchema['x-component-props']?.duplicateCollection || record?.__collection || name, + syncCallBack, + ); + }, + }, + 'x-reactions': [ + { + dependencies: ['.duplicateMode'], + fulfill: { + state: { + visible: `{{ $deps[0]!=="quickDulicate" }}`, + }, + }, + }, + ], + }, + selectAll: { + type: 'void', + title: '{{ t("Select all") }}', + 'x-component': 'Action.Link', + 'x-reactions': [ + { + dependencies: ['.duplicateMode'], + fulfill: { + state: { + visible: `{{ $deps[0]==="quickDulicate" }}`, + }, + }, + }, + ], + 'x-component-props': { + type: 'primary', + style: { float: 'right', position: 'relative', zIndex: 1200 }, + useAction: () => { + const from = useForm(); + return useSelectAllFields(from); + }, + }, + }, duplicateFields: { type: 'array', title: '{{ t("Data fields") }}', required: true, - default: duplicateValues, description: t('Only the selected fields will be used as the initialization data for the form'), 'x-decorator': 'FormItem', 'x-component': Tree, @@ -310,7 +408,7 @@ function DuplicationMode() { state: { disabled: '{{ !$deps[0] }}', componentProps: { - treeData: '{{ getEnableFieldTree($deps[0], $self) }}', + treeData: '{{ getEnableFieldTree($deps[0], $self,treeData) }}', }, }, }, @@ -320,7 +418,7 @@ function DuplicationMode() { }, } as ISchema } - onSubmit={({ duplicateMode, collection, duplicateFields }) => { + onSubmit={({ duplicateMode, collection, duplicateFields, treeData }) => { const fields = Array.isArray(duplicateFields) ? duplicateFields : duplicateFields.checked || []; field.componentProps.duplicateMode = duplicateMode; field.componentProps.duplicateFields = fields; @@ -328,6 +426,7 @@ function DuplicationMode() { fieldSchema['x-component-props'].duplicateMode = duplicateMode; fieldSchema['x-component-props'].duplicateFields = fields; fieldSchema['x-component-props'].duplicateCollection = collection; + fieldSchema['x-component-props'].treeData = treeData; dn.emit('patch', { schema: { ['x-uid']: fieldSchema['x-uid'], @@ -580,7 +679,7 @@ export const ActionDesigner = (props) => { const { name } = useCollection(); const { getChildrenCollections } = useCollectionManager(); const isAction = useLinkageAction(); - const isPopupAction = ['create', 'update', 'view', 'customize:popup', 'duplicate','customize:create'].includes( + const isPopupAction = ['create', 'update', 'view', 'customize:popup', 'duplicate', 'customize:create'].includes( fieldSchema['x-action'] || '', ); const isUpdateModePopupAction = ['customize:bulkUpdate', 'customize:bulkEdit'].includes(fieldSchema['x-action']); 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 index f4126253c..2324ca9a8 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx @@ -163,7 +163,7 @@ export const Templates = ({ style = {}, form }) => { {targetTemplate !== 'none' && ( ( () => @@ -61,7 +61,6 @@ export const FormDataTemplates = observer( ), [], ); - console.log(activeData); const getTargetField = (collectionName: string) => { const collection = getCollection(collectionName); return getCollectionField( @@ -170,6 +169,16 @@ export const FormDataTemplates = observer( required: true, 'x-reactions': '{{useTitleFieldDataSource}}', }, + syncFromForm: { + type: 'void', + title: '{{ t("Sync from form fields") }}', + 'x-component': 'Action.Link', + 'x-component-props': { + type: 'primary', + style: { float: 'right', position: 'relative', zIndex: 1200 }, + useAction: () => useSyncFromForm(formSchema), + }, + }, fields: { type: 'array', title: '{{ t("Data fields") }}', diff --git a/packages/core/client/src/schema-settings/DataTemplates/components/DataTemplateTitle.tsx b/packages/core/client/src/schema-settings/DataTemplates/components/DataTemplateTitle.tsx index 40719f6c6..680da1851 100644 --- a/packages/core/client/src/schema-settings/DataTemplates/components/DataTemplateTitle.tsx +++ b/packages/core/client/src/schema-settings/DataTemplates/components/DataTemplateTitle.tsx @@ -48,10 +48,9 @@ const DataTemplateTitle = observer<{ index: number; item: any }>((props) => { export interface IArrayCollapseProps extends CollapseProps { defaultOpenPanelCount?: number; } -type ComposedArrayCollapse = - | React.FC> & { - CollapsePanel?: React.FC>; - }; +type ComposedArrayCollapse = React.FC> & { + CollapsePanel?: React.FC>; +}; const isAdditionComponent = (schema: ISchema) => { return schema['x-component']?.indexOf?.('Addition') > -1; @@ -218,6 +217,9 @@ export const ArrayCollapse: ComposedArrayCollapse = observer( onAdd={(index) => { setActiveKeys(insertActiveKeys(activeKeys, index)); }} + onRemove={() => { + field.initialValue = field.value; + }} > {renderEmpty()} {renderItems()} diff --git a/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts b/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts index abd65c4c4..630567cee 100644 --- a/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts +++ b/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts @@ -1,32 +1,34 @@ import { ArrayField } from '@formily/core'; +import { useField } from '@formily/react'; import React, { useCallback, useState } from 'react'; import { useCollectionManager } from '../../../collection-manager'; -import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields'; import { useCompile } from '../../../schema-component'; import { TreeNode } from '../TreeLabel'; +// 过滤掉系统字段 +export const systemKeys = [ + // 'id', + 'sort', + 'createdById', + 'createdBy', + 'createdAt', + 'updatedById', + 'updatedBy', + 'updatedAt', + 'password', + 'sequence', +]; export const useCollectionState = (currentCollectionName: string) => { const { getCollectionFields, getAllCollectionsInheritChain, getCollection, getInterface } = useCollectionManager(); const [collectionList] = useState(getCollectionList); const compile = useCompile(); + const templateField: any = useField(); function getCollectionList() { const collections = getAllCollectionsInheritChain(currentCollectionName); return collections.map((name) => ({ label: getCollection(name)?.title, value: name })); } - // 过滤掉系统字段 - const systemKeys = [ - // 'id', - 'sort', - 'createdById', - 'createdBy', - 'createdAt', - 'updatedById', - 'updatedBy', - 'updatedAt', - ]; - /** * maxDepth: 从 0 开始,0 表示一层,1 表示两层,以此类推 */ @@ -115,12 +117,25 @@ export const useCollectionState = (currentCollectionName: string) => { }) .filter(Boolean); }; + const parseTreeData = (data) => { + return data.map((v) => { + return { + ...v, + title: React.createElement(TreeNode, { ...v, type: v.type }), + children: v.children ? parseTreeData(v.children) : null, + }; + }); + }; - const getEnableFieldTree = useCallback((collectionName: string) => { + const getEnableFieldTree = useCallback((collectionName: string, field, treeData?) => { + const index = field.index; + const targetTemplate = templateField.initialValue?.items?.[index]; if (!collectionName) { return []; } - + if (targetTemplate?.treeData || treeData) { + return parseTreeData(treeData || targetTemplate.treeData); + } try { return traverseFields(collectionName, { exclude: ['id', ...systemKeys], maxDepth: 1 }); } catch (error) { @@ -242,9 +257,8 @@ function findNode(treeData, item) { } function loadChildren({ node, traverseAssociations, traverseFields, systemKeys, fields }) { - const activeNode = findNode(fields.componentProps.treeData, node); + const activeNode = findNode(fields.dataSource || fields.componentProps.treeData, node); let children = []; - // 多对多和多对一只展示关系字段 if (['belongsTo', 'belongsToMany'].includes(node.field.type)) { children = traverseAssociations(node.field.target, { diff --git a/packages/core/client/src/schema-settings/DataTemplates/utils.tsx b/packages/core/client/src/schema-settings/DataTemplates/utils.tsx new file mode 100644 index 000000000..57cdedad9 --- /dev/null +++ b/packages/core/client/src/schema-settings/DataTemplates/utils.tsx @@ -0,0 +1,232 @@ +import { ArrayBase } from '@formily/antd-v5'; +import { useForm } from '@formily/react'; +import { message } from 'antd'; +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { getAssociationPath } from '../../block-provider/hooks'; +import { useCollectionManager } from '../../collection-manager'; +import { useCompile } from '../../schema-component'; +import { TreeNode } from './TreeLabel'; +import { systemKeys } from './hooks/useCollectionState'; +import LRUCache from 'lru-cache'; + +export const useSyncFromForm = (fieldSchema, collection?, callBack?) => { + const { getCollectionJoinField, getCollectionFields } = useCollectionManager(); + const array = ArrayBase.useArray(); + const index = ArrayBase.useIndex(); + const record = ArrayBase.useRecord(); + const compile = useCompile(); + const { t } = useTranslation(); + const from = useForm(); + + const traverseFields = ((cache) => { + return (collectionName, { exclude = [], depth = 0, maxDepth, prefix = '', disabled = false }, formData) => { + const cacheKey = `${collectionName}-${exclude.join(',')}-${depth}-${maxDepth}-${prefix}`; + const cachedResult = cache.get(cacheKey); + if (cachedResult) { + return cachedResult; + } + if (depth > maxDepth) { + return []; + } + const result = 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, + disabled, + }; + const tatgetFormField = formData.find((v) => v.name === field.name); + if ( + ['belongsTo', 'belongsToMany'].includes(field.type) && + (!tatgetFormField || ['Select', 'Picker'].includes(tatgetFormField?.fieldMode)) + ) { + 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) || + ['Nester', 'SubTable'].includes(tatgetFormField?.fieldMode) + ) { + let childrenDisabled = false; + if ( + ['hasOne', 'hasMany'].includes(field.type) && + ['Select', 'Picker'].includes(tatgetFormField?.fieldMode) + ) { + childrenDisabled = true; + } + option.disabled = true; + option.isLeaf = false; + option['children'] = traverseFields( + field.target, + { + depth: depth + 1, + maxDepth, + prefix: option.key, + exclude: ['id', ...systemKeys], + disabled: childrenDisabled, + }, + formData, + ); + } + return option; + }) + .filter(Boolean); + + cache.set(cacheKey, result); + return result; + }; + })( + new LRUCache({ max: 100 }), + ); + + const traverseAssociations = ((cache) => { + return (collectionName, { prefix, maxDepth, depth = 0, exclude = [] }) => { + const cacheKey = `${collectionName}-${exclude.join(',')}-${depth}-${maxDepth}-${prefix}`; + const cachedResult = cache.get(cacheKey); + if (cachedResult) { + return cachedResult; + } + + if (depth > maxDepth) { + return []; + } + + const result = 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 { + type: 'preloading', + tag: compile(field.uiSchema?.title) || field.name, + title: React.createElement(TreeNode, option), + key: value, + isLeaf: false, + field, + children: traverseAssociations(field.target, { + prefix: value, + depth: depth + 1, + maxDepth, + exclude, + }), + }; + }) + .filter(Boolean); + cache.set(cacheKey, result); + return result; + }; + })( + new LRUCache({ max: 100 }), + ); + const getEnableFieldTree = useCallback((collectionName: string, formData) => { + if (!collectionName) { + return []; + } + + try { + return traverseFields(collectionName, { exclude: ['id', ...systemKeys], maxDepth: 1, disabled: false }, formData); + } catch (error) { + console.error(error); + return []; + } + }, []); + + return { + async run() { + const formData = new Set([]); + const selectFields = new Set([]); + const getAssociationAppends = (schema, str) => { + schema.reduceProperties((pre, s) => { + const prefix = pre || str; + const collectionfield = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field']); + const isAssociationSubfield = s.name.includes('.'); + const isAssociationField = + collectionfield && ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(collectionfield.type); + const fieldPath = !isAssociationField && isAssociationSubfield ? getAssociationPath(s.name) : s.name; + const path = prefix === '' || !prefix ? fieldPath : prefix + '.' + fieldPath; + if ( + collectionfield && + !( + ['hasOne', 'hasMany'].includes(collectionfield.type) || + ['SubForm', 'Nester'].includes(s['x-component-props']?.mode) + ) + ) { + selectFields.add(path); + } + if (collectionfield && (isAssociationField || isAssociationSubfield) && s['x-component'] !== 'TableField') { + formData.add({ name: path, fieldMode: s['x-component-props']['mode'] || 'Select' }); + if (['Nester', 'SubTable'].includes(s['x-component-props']?.mode)) { + const bufPrefix = prefix && prefix !== '' ? prefix + '.' + s.name : s.name; + getAssociationAppends(s, bufPrefix); + } + } else if ( + ![ + 'ActionBar', + 'Action', + 'Action.Link', + 'Action.Modal', + 'Selector', + 'Viewer', + 'AddNewer', + 'AssociationField.Selector', + 'AssociationField.AddNewer', + 'TableField', + ].includes(s['x-component']) + ) { + getAssociationAppends(s, str); + } + }, str); + }; + getAssociationAppends(fieldSchema, ''); + const treeData = getEnableFieldTree(record?.collection || collection, [...formData]); + if (callBack) { + callBack(treeData, [...selectFields], from); + } else { + array?.field.form.query(`fieldReaction.items.${index}.layout.fields`).take((f: any) => { + f.componentProps.treeData = []; + setTimeout(() => (f.componentProps.treeData = treeData)); + }); + array?.field.value.splice(index, 1, { + ...array?.field?.value[index], + fields: [...selectFields], + treeData: treeData, + }); + } + message.success(t('Sync successfully')); + }, + }; +}; diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx index ab98a5bc1..f8a3b3bb6 100644 --- a/packages/core/client/src/schema-settings/SchemaSettings.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx @@ -1146,7 +1146,6 @@ SchemaSettings.DataTemplates = function DataTemplates(props) { const { t } = useTranslation(); const formSchema = findFormBlock(fieldSchema) || fieldSchema; const { templateData } = useDataTemplates(); - const schema = useMemo( () => ({ type: 'object', @@ -1171,7 +1170,6 @@ SchemaSettings.DataTemplates = function DataTemplates(props) { ); const onSubmit = useCallback((v) => { const data = { ...(formSchema['x-data-templates'] || {}), ...v.fieldReaction }; - // 当 Tree 组件开启 checkStrictly 属性时,会导致 checkedKeys 的值是一个对象,而不是数组,所以这里需要转换一下以支持旧版本 data.items.forEach((item) => { item.fields = Array.isArray(item.fields) ? item.fields : item.fields.checked;