refactor: form data templates and depulicate action support sync from form fields (#2314)

* refactor: sync from form fields

* refactor: sync from form fields

* refactor: sync from form fields

* refactor: data fields

* refactor: traverseFields

* refactor: traverseFields

* refactor: locale improve

* fix: merge bug

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: depulicate action support sync form form fields

* refactor: code refactor

* refactor: direct duplicate support select all

* refactor: code improve

* refactor: code improve

* refactor: hasOne and hasMany avaliable for deplicate

* refactor:  code improve

* refactor: locale improve

* refactor: code improve

* refactor: code improve
This commit is contained in:
katherinehhh 2023-07-31 16:17:18 +08:00 committed by GitHub
parent c743d66b8e
commit 8f1d0d80af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 402 additions and 40 deletions

View File

@ -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);

View File

@ -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"
};

View File

@ -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":"すべて選択"
}

View File

@ -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":"全选"
}

View File

@ -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 (
<SchemaSettings.ModalItem
title={t('Duplicate mode')}
@ -238,6 +286,7 @@ function DuplicationMode() {
currentCollection: record?.__collection || name,
getOnLoadData,
getOnCheck,
treeData: fieldSchema['x-component-props']?.treeData,
}}
schema={
{
@ -278,11 +327,60 @@ function DuplicationMode() {
},
],
},
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: () => {
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']);

View File

@ -163,7 +163,7 @@ export const Templates = ({ style = {}, form }) => {
{targetTemplate !== 'none' && (
<RemoteSelect
style={{ width: 220 }}
fieldNames={{ label: template.titleField, value: 'id' }}
fieldNames={{ label: template?.titleField, value: 'id' }}
target={template?.collection}
value={targetTemplateData}
objectValue

View File

@ -13,6 +13,7 @@ import { AsDefaultTemplate } from './components/AsDefaultTemplate';
import { ArrayCollapse } from './components/DataTemplateTitle';
import { getSelectedIdFilter } from './components/Designer';
import { useCollectionState } from './hooks/useCollectionState';
import { useSyncFromForm } from './utils';
const Tree = connect(
AntdTree,
@ -48,7 +49,6 @@ export const FormDataTemplates = observer(
} = useCollectionState(collectionName);
const { getCollection, getCollectionField } = useCollectionManager();
const { t } = useTranslation();
// 不要在后面的数组中依赖 defaultValues否则会因为 defaultValues 的变化导致 activeData 响应性丢失
const activeData = useMemo<ITemplate>(
() =>
@ -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") }}',

View File

@ -48,10 +48,9 @@ const DataTemplateTitle = observer<{ index: number; item: any }>((props) => {
export interface IArrayCollapseProps extends CollapseProps {
defaultOpenPanelCount?: number;
}
type ComposedArrayCollapse =
| React.FC<React.PropsWithChildren<IArrayCollapseProps>> & {
type ComposedArrayCollapse = React.FC<React.PropsWithChildren<IArrayCollapseProps>> & {
CollapsePanel?: React.FC<React.PropsWithChildren<CollapsePanelProps>>;
};
};
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()}

View File

@ -1,22 +1,12 @@
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 useCollectionState = (currentCollectionName: string) => {
const { getCollectionFields, getAllCollectionsInheritChain, getCollection, getInterface } = useCollectionManager();
const [collectionList] = useState(getCollectionList);
const compile = useCompile();
function getCollectionList() {
const collections = getAllCollectionsInheritChain(currentCollectionName);
return collections.map((name) => ({ label: getCollection(name)?.title, value: name }));
}
// 过滤掉系统字段
const systemKeys = [
// 过滤掉系统字段
export const systemKeys = [
// 'id',
'sort',
'createdById',
@ -25,7 +15,19 @@ export const useCollectionState = (currentCollectionName: string) => {
'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 }));
}
/**
* 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, {

View File

@ -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<string, any>({ 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<string, any>({ 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'));
},
};
};

View File

@ -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;