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",
|
||||
"Try again": "Try again",
|
||||
"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",
|
||||
"Enable link": "Activar enlace",
|
||||
"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': 'データテンプレートセレクターを表示',
|
||||
'Form data templates': 'フォームデータテンプレート',
|
||||
"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',
|
||||
'Form data templates': 'Modelos de dados do formulário',
|
||||
"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': "Отображать селектор шаблона данных",
|
||||
'Form data templates': "Шаблоны данных формы",
|
||||
"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',
|
||||
'Form data templates': 'Form veri şablonları',
|
||||
"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": "允许清除缓存,重启应用",
|
||||
'The will interrupt service, it may take a few seconds to restart. Are you sure to continue?': '重启将会中断当前服务,这个过程可能需要一点时间,确定要继续吗?',
|
||||
'Reboot': '重启',
|
||||
"Template fields have been removed and need to be reconfigured": "模板字段已被删除,需要重新配置",
|
||||
'Clear cache': '清除缓存',
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 _ from 'lodash';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
@ -71,7 +71,7 @@ export const Templates = ({ style = {}, form }) => {
|
||||
if (defaultTemplate) {
|
||||
fetchTemplateData(api, defaultTemplate, t)
|
||||
.then((data) => {
|
||||
if (form) {
|
||||
if (form && data) {
|
||||
forEach(data, (value, key) => {
|
||||
if (value) {
|
||||
form.values[key] = value;
|
||||
@ -91,7 +91,7 @@ export const Templates = ({ style = {}, form }) => {
|
||||
if (option.key !== 'none') {
|
||||
fetchTemplateData(api, option, t)
|
||||
.then((data) => {
|
||||
if (form) {
|
||||
if (form && data) {
|
||||
forEach(data, (value, key) => {
|
||||
if (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) {
|
||||
if (template.fields.length === 0) {
|
||||
return showToast(t('Template fields have been removed and need to be reconfigured'));
|
||||
return;
|
||||
}
|
||||
return api
|
||||
.resource(template.collection)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { connect, mapProps, observer } from '@formily/react';
|
||||
import { Tree as AntdTree } from 'antd';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCollectionManager } from '../../collection-manager';
|
||||
import { AssociationSelect, SchemaComponent } from '../../schema-component';
|
||||
@ -13,24 +13,22 @@ const Tree = connect(
|
||||
mapProps({
|
||||
value: 'checkedKeys',
|
||||
dataSource: 'treeData',
|
||||
onInput: 'onCheck',
|
||||
// onInput: 'onCheck',
|
||||
}),
|
||||
);
|
||||
|
||||
export const FormDataTemplates = observer((props: any) => {
|
||||
const { useProps } = props;
|
||||
const { defaultValues, collectionName } = useProps();
|
||||
const { collectionList, getEnableFieldTree } = useCollectionState(collectionName);
|
||||
const { collectionList, getEnableFieldTree, onLoadData, onCheck } = useCollectionState(collectionName);
|
||||
const { getCollection, getCollectionField } = useCollectionManager();
|
||||
const collection = getCollection(collectionName);
|
||||
const { t } = useTranslation();
|
||||
const field = getCollectionField(`${collectionName}.${collection?.titleField || 'id'}`);
|
||||
|
||||
return (
|
||||
<SchemaComponent
|
||||
components={{ ArrayCollapse }}
|
||||
scope={{ getEnableFieldTree }}
|
||||
schema={{
|
||||
const components = useMemo(() => ({ ArrayCollapse }), []);
|
||||
const scope = useMemo(() => ({ getEnableFieldTree }), []);
|
||||
const schema = useMemo(
|
||||
() => ({
|
||||
type: 'object',
|
||||
properties: {
|
||||
items: {
|
||||
@ -45,7 +43,7 @@ export const FormDataTemplates = observer((props: any) => {
|
||||
type: 'object',
|
||||
'x-component': 'ArrayCollapse.CollapsePanel',
|
||||
'x-component-props': {
|
||||
extra: [<AsDefaultTemplate />],
|
||||
extra: [<AsDefaultTemplate key="0" />],
|
||||
},
|
||||
properties: {
|
||||
layout: {
|
||||
@ -54,7 +52,6 @@ export const FormDataTemplates = observer((props: any) => {
|
||||
'x-component-props': {
|
||||
layout: 'vertical',
|
||||
},
|
||||
// TODO: 翻译
|
||||
properties: {
|
||||
collection: {
|
||||
type: 'string',
|
||||
@ -131,7 +128,10 @@ export const FormDataTemplates = observer((props: any) => {
|
||||
'x-component-props': {
|
||||
treeData: [],
|
||||
checkable: true,
|
||||
checkStrictly: true,
|
||||
selectable: false,
|
||||
loadData: onLoadData,
|
||||
onCheck,
|
||||
rootStyle: {
|
||||
padding: '8px 0',
|
||||
border: '1px solid #d9d9d9',
|
||||
@ -148,7 +148,7 @@ export const FormDataTemplates = observer((props: any) => {
|
||||
state: {
|
||||
disabled: '{{ !$deps[0] }}',
|
||||
componentProps: {
|
||||
treeData: '{{ getEnableFieldTree($deps[0]) }}',
|
||||
treeData: '{{ getEnableFieldTree($deps[0], $self) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -187,9 +187,11 @@ export const FormDataTemplates = observer((props: any) => {
|
||||
'x-component': 'Checkbox',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return <SchemaComponent components={components} scope={scope} schema={schema} />;
|
||||
});
|
||||
|
||||
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 { useCompile } from '../../../schema-component';
|
||||
import { TreeNode } from '../TreeLabel';
|
||||
@ -9,15 +12,13 @@ export const useCollectionState = (currentCollectionName: string) => {
|
||||
const [collectionList] = useState(getCollectionList);
|
||||
const compile = useCompile();
|
||||
|
||||
let dataFields: ArrayField = {} as any;
|
||||
|
||||
function getCollectionList() {
|
||||
const collections = getAllCollectionsInheritChain(currentCollectionName);
|
||||
return collections.map((name) => ({ label: getCollection(name)?.title, value: name }));
|
||||
}
|
||||
|
||||
const getEnableFieldTree = (collectionName: string) => {
|
||||
if (!collectionName) {
|
||||
return [];
|
||||
}
|
||||
// 过滤掉系统字段
|
||||
const systemKeys = [
|
||||
// 'id',
|
||||
@ -29,40 +30,15 @@ export const useCollectionState = (currentCollectionName: string) => {
|
||||
'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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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)) {
|
||||
@ -82,12 +58,15 @@ export const useCollectionState = (currentCollectionName: string) => {
|
||||
...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,
|
||||
@ -95,6 +74,7 @@ export const useCollectionState = (currentCollectionName: string) => {
|
||||
exclude: systemKeys,
|
||||
});
|
||||
} else if (['hasOne', 'hasMany'].includes(field.type)) {
|
||||
option.isLeaf = false;
|
||||
option['children'] = traverseFields(field.target, {
|
||||
depth: depth + 1,
|
||||
maxDepth,
|
||||
@ -106,16 +86,139 @@ export const useCollectionState = (currentCollectionName: string) => {
|
||||
})
|
||||
.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) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return traverseFields(collectionName, { exclude: ['id', ...systemKeys], maxDepth: 3 });
|
||||
return traverseFields(collectionName, { exclude: ['id', ...systemKeys], maxDepth: 1 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
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 {
|
||||
collectionList,
|
||||
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 { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
|
||||
import { FormDataTemplates } from './DataTemplates';
|
||||
import { EnableChildCollections } from './EnableChildCollections';
|
||||
import { FormLinkageRules } from './LinkageRules';
|
||||
import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks';
|
||||
import { EnableChildCollections } from './EnableChildCollections';
|
||||
|
||||
interface SchemaSettingsProps {
|
||||
title?: any;
|
||||
@ -815,6 +815,10 @@ SchemaSettings.ModalItem = function ModalItem(props) {
|
||||
})
|
||||
.then((values) => {
|
||||
onSubmit(values);
|
||||
return values;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}}
|
||||
>
|
||||
@ -897,7 +901,7 @@ SchemaSettings.DefaultSortingRules = function DefaultSortingRules(props) {
|
||||
field: {
|
||||
type: 'string',
|
||||
enum: sortFields,
|
||||
required:true,
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
@ -1023,13 +1027,8 @@ SchemaSettings.DataTemplates = function DataTemplates(props) {
|
||||
const formSchema = findFormBlock(fieldSchema) || fieldSchema;
|
||||
const { templateData } = useDataTemplates();
|
||||
|
||||
return (
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Form data templates')}
|
||||
components={{ ArrayCollapse, FormLayout }}
|
||||
width={770}
|
||||
schema={
|
||||
{
|
||||
const schema = useMemo(
|
||||
() => ({
|
||||
type: 'object',
|
||||
title: t('Form data templates'),
|
||||
properties: {
|
||||
@ -1045,10 +1044,17 @@ SchemaSettings.DataTemplates = function DataTemplates(props) {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={(v) => {
|
||||
}),
|
||||
[templateData],
|
||||
);
|
||||
const onSubmit = useCallback((v) => {
|
||||
const data = v.fieldReaction || {};
|
||||
|
||||
// 当 Tree 组件开启 checkStrictly 属性时,会导致 checkedKeys 的值是一个对象,而不是数组,所以这里需要转换一下以支持旧版本
|
||||
data.items.forEach((item) => {
|
||||
item.fields = Array.isArray(item.fields) ? item.fields : item.fields.checked;
|
||||
});
|
||||
|
||||
const schema = {
|
||||
['x-uid']: formSchema['x-uid'],
|
||||
['x-data-templates']: data,
|
||||
@ -1058,8 +1064,12 @@ SchemaSettings.DataTemplates = function DataTemplates(props) {
|
||||
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