From 3067b3a4b1bdbe5d6c6376ecc73cec0723b02a33 Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Fri, 18 Apr 2025 09:24:08 +0800 Subject: [PATCH] fix(tree-block): the issue where the tree block filter condition was empty (#6634) * fix: enhance useFilterAPI to support sourceKey in filter functions * fix: update useFilterAPI to use key variable for dynamic field assignment * feat: add treeFind function for depth-first traversal of tree structures * feat: enhance getSupportFieldsByForeignKey to include target collection matching * feat: add getInheritChain method to InheritanceCollectionMixin and corresponding tests --- .../mixins/InheritanceCollectionMixin.ts | 40 +++- .../InheritanceCollectionMixin.test.ts | 189 ++++++++++++++++++ .../data-source/DataSourceManagerProvider.tsx | 4 +- .../filter-provider/__tests__/utiles.test.ts | 102 ++++++++++ .../core/client/src/filter-provider/utils.ts | 27 ++- packages/core/utils/src/common.ts | 44 ++++ 6 files changed, 395 insertions(+), 11 deletions(-) create mode 100644 packages/core/client/src/collection-manager/mixins/__tests__/InheritanceCollectionMixin.test.ts diff --git a/packages/core/client/src/collection-manager/mixins/InheritanceCollectionMixin.ts b/packages/core/client/src/collection-manager/mixins/InheritanceCollectionMixin.ts index 5399efe4c1..5648500ddc 100644 --- a/packages/core/client/src/collection-manager/mixins/InheritanceCollectionMixin.ts +++ b/packages/core/client/src/collection-manager/mixins/InheritanceCollectionMixin.ts @@ -7,9 +7,9 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { filter, unionBy, uniq } from 'lodash'; import type { CollectionFieldOptions, GetCollectionFieldPredicate } from '../../data-source'; import { Collection } from '../../data-source/collection/Collection'; -import _, { filter, unionBy, uniq } from 'lodash'; export class InheritanceCollectionMixin extends Collection { protected parentCollectionsName: string[]; @@ -22,6 +22,7 @@ export class InheritanceCollectionMixin extends Collection { protected parentCollectionFields: Record = {}; protected allCollectionsInheritChain: string[]; protected inheritCollectionsChain: string[]; + protected inheritChain: string[]; protected foreignKeyFields: CollectionFieldOptions[]; getParentCollectionsName() { @@ -233,6 +234,43 @@ export class InheritanceCollectionMixin extends Collection { return this.inheritCollectionsChain; } + /** + * 获取所有祖先数据表和后代数据表,不包括兄弟表。用于下面这些地方: + * - 筛选区块链接数据区块时使用 + */ + getInheritChain() { + if (this.inheritChain) { + return this.inheritChain.slice(); + } + + const ancestorChain = this.getInheritCollectionsChain(); + const descendantNames = this.getChildrenCollectionsName(); + + // 构建最终的链,首先包含祖先链(包括自身) + const inheritChain = [...ancestorChain]; + + // 再添加直接后代及其后代,但不包括兄弟表 + const addDescendants = (names: string[]) => { + for (const name of names) { + if (!inheritChain.includes(name)) { + inheritChain.push(name); + const childCollection = this.collectionManager.getCollection(name); + if (childCollection) { + // 递归添加每个后代的后代 + const childrenNames = childCollection.getChildrenCollectionsName(); + addDescendants(childrenNames); + } + } + } + }; + + // 从当前集合的直接后代开始添加 + addDescendants(descendantNames); + + this.inheritChain = inheritChain; + return this.inheritChain; + } + getAllFields(predicate?: GetCollectionFieldPredicate) { if (this.allFields) { return this.allFields.slice(); diff --git a/packages/core/client/src/collection-manager/mixins/__tests__/InheritanceCollectionMixin.test.ts b/packages/core/client/src/collection-manager/mixins/__tests__/InheritanceCollectionMixin.test.ts new file mode 100644 index 0000000000..b3bd5c4613 --- /dev/null +++ b/packages/core/client/src/collection-manager/mixins/__tests__/InheritanceCollectionMixin.test.ts @@ -0,0 +1,189 @@ +import { Application } from '@nocobase/client'; +import { CollectionManager } from '../../../data-source/collection/CollectionManager'; +import { InheritanceCollectionMixin } from '../InheritanceCollectionMixin'; + +describe('InheritanceCollectionMixin', () => { + let app: Application; + let collectionManager: CollectionManager; + + beforeEach(() => { + app = new Application({ + dataSourceManager: { + collectionMixins: [InheritanceCollectionMixin], + }, + }); + collectionManager = app.getCollectionManager(); + }); + + describe('getInheritChain', () => { + it('should return itself when there are no ancestors or descendants', () => { + const options = { + name: 'test', + fields: [{ name: 'field1', interface: 'input' }], + }; + + collectionManager.addCollections([options]); + const collection = collectionManager.getCollection('test'); + + const inheritChain = collection.getInheritChain(); + expect(inheritChain).toEqual(['test']); + }); + + it('should return a chain including all ancestor tables', () => { + // 创建三代数据表结构:grandparent -> parent -> child + const grandparentOptions = { + name: 'grandparent', + fields: [{ name: 'field1', interface: 'input' }], + }; + const parentOptions = { + name: 'parent', + inherits: ['grandparent'], + fields: [{ name: 'field2', interface: 'input' }], + }; + const childOptions = { + name: 'child', + inherits: ['parent'], + fields: [{ name: 'field3', interface: 'input' }], + }; + + // 先将所有集合添加到 collectionManager + collectionManager.addCollections([grandparentOptions, parentOptions, childOptions]); + + // 获取最终的集合实例以调用方法 + const child = collectionManager.getCollection('child'); + + // 测试 child 的继承链包含所有祖先表 + const inheritChain = child.getInheritChain(); + expect(inheritChain).toContain('child'); + expect(inheritChain).toContain('parent'); + expect(inheritChain).toContain('grandparent'); + expect(inheritChain.length).toBe(3); + }); + + it('should include all descendant tables, but not sibling tables', () => { + // 创建具有兄弟和后代关系的数据表结构 + // parent (祖先表) + // |-- child1 (子表) + // | |-- grandChild1 (孙表1) + // | |-- grandChild2 (孙表2) + // |-- child2 (兄弟表) + // |-- grandChild3 (兄弟的子表,不应该包括在测试集合的继承链中) + + const collections = [ + { + name: 'parent', + fields: [{ name: 'parentField', interface: 'input' }], + }, + { + name: 'child1', + inherits: ['parent'], + fields: [{ name: 'child1Field', interface: 'input' }], + }, + { + name: 'child2', + inherits: ['parent'], + fields: [{ name: 'child2Field', interface: 'input' }], + }, + { + name: 'grandChild1', + inherits: ['child1'], + fields: [{ name: 'grandChild1Field', interface: 'input' }], + }, + { + name: 'grandChild2', + inherits: ['child1'], + fields: [{ name: 'grandChild2Field', interface: 'input' }], + }, + { + name: 'grandChild3', + inherits: ['child2'], + fields: [{ name: 'grandChild3Field', interface: 'input' }], + }, + ]; + + // 一次性添加所有集合 + collectionManager.addCollections(collections); + + // 获取要测试的集合实例 + const child1 = collectionManager.getCollection('child1'); + + // 测试 child1 的继承链 + const child1InheritChain = child1.getInheritChain(); + + // 应该包含自身、父表和子表 + expect(child1InheritChain).toContain('child1'); + expect(child1InheritChain).toContain('parent'); + expect(child1InheritChain).toContain('grandChild1'); + expect(child1InheritChain).toContain('grandChild2'); + + // 不应该包含兄弟表及其子表 + expect(child1InheritChain).not.toContain('child2'); + expect(child1InheritChain).not.toContain('grandChild3'); + + // 检查总数量是否正确 (parent, child1, grandChild1, grandChild2) + expect(child1InheritChain.length).toBe(4); + }); + + it('should properly handle multiple inheritance', () => { + // 创建多重继承的数据表结构 + // parent1 parent2 + // \ / + // \ / + // child + // | + // grandChild + + const collections = [ + { + name: 'parent1', + fields: [{ name: 'parent1Field', interface: 'input' }], + }, + { + name: 'parent2', + fields: [{ name: 'parent2Field', interface: 'input' }], + }, + { + name: 'child', + inherits: ['parent1', 'parent2'], + fields: [{ name: 'childField', interface: 'input' }], + }, + { + name: 'grandChild', + inherits: ['child'], + fields: [{ name: 'grandChildField', interface: 'input' }], + }, + ]; + + // 一次性添加所有集合 + collectionManager.addCollections(collections); + + // 获取要测试的集合实例 + const child = collectionManager.getCollection('child'); + const grandChild = collectionManager.getCollection('grandChild'); + + // 测试 child 的继承链 + const childInheritChain = child.getInheritChain(); + + // 应该包含自身、两个父表和子表 + expect(childInheritChain).toContain('child'); + expect(childInheritChain).toContain('parent1'); + expect(childInheritChain).toContain('parent2'); + expect(childInheritChain).toContain('grandChild'); + + // 检查总数量是否正确 (child, parent1, parent2, grandChild) + expect(childInheritChain.length).toBe(4); + + // 测试 grandChild 的继承链 + const grandChildInheritChain = grandChild.getInheritChain(); + + // 应该包含自身及所有祖先表 + expect(grandChildInheritChain).toContain('grandChild'); + expect(grandChildInheritChain).toContain('child'); + expect(grandChildInheritChain).toContain('parent1'); + expect(grandChildInheritChain).toContain('parent2'); + + // 检查总数量是否正确 (grandChild, child, parent1, parent2) + expect(grandChildInheritChain.length).toBe(4); + }); + }); +}); diff --git a/packages/core/client/src/data-source/data-source/DataSourceManagerProvider.tsx b/packages/core/client/src/data-source/data-source/DataSourceManagerProvider.tsx index 547c038e0e..578b447868 100644 --- a/packages/core/client/src/data-source/data-source/DataSourceManagerProvider.tsx +++ b/packages/core/client/src/data-source/data-source/DataSourceManagerProvider.tsx @@ -29,7 +29,7 @@ export function useDataSourceManager() { } /** - * 获取当前 collection 继承链路上的所有 collection + * 获取当前 collection 继承链路上的所有 collection(不包括兄弟表) * @returns */ export function useAllCollectionsInheritChainGetter() { @@ -39,7 +39,7 @@ export function useAllCollectionsInheritChainGetter() { return dm ?.getDataSource(customDataSource) ?.collectionManager?.getCollection(collectionName) - ?.getAllCollectionsInheritChain(); + ?.getInheritChain(); }, [dm], ); diff --git a/packages/core/client/src/filter-provider/__tests__/utiles.test.ts b/packages/core/client/src/filter-provider/__tests__/utiles.test.ts index be25dce192..f8340bd2a5 100644 --- a/packages/core/client/src/filter-provider/__tests__/utiles.test.ts +++ b/packages/core/client/src/filter-provider/__tests__/utiles.test.ts @@ -67,6 +67,108 @@ describe('getSupportFieldsByAssociation', () => { }); describe('getSupportFieldsByForeignKey', () => { + it('should return foreign key fields matching both name and target collection', () => { + const filterBlockCollection = { + fields: [ + { id: 1, name: 'field1', type: 'hasMany', foreignKey: 'fk1', target: 'collection1' }, + { id: 2, name: 'field2', type: 'hasMany', foreignKey: 'fk2', target: 'collection2' }, + { id: 3, name: 'field3', type: 'hasMany', foreignKey: 'fk3', target: 'collection3' }, + ], + }; + + const block = { + foreignKeyFields: [ + { id: 1, name: 'fk1', collectionName: 'collection1' }, + { id: 2, name: 'fk2', collectionName: 'collection2' }, + { id: 3, name: 'fk3', collectionName: 'collection3' }, + ], + }; + + const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any); + + expect(result).toEqual([ + { id: 1, name: 'fk1', collectionName: 'collection1' }, + { id: 2, name: 'fk2', collectionName: 'collection2' }, + { id: 3, name: 'fk3', collectionName: 'collection3' }, + ]); + }); + + it("should not return foreign key fields when target collection doesn't match", () => { + const filterBlockCollection = { + fields: [ + { id: 1, name: 'field1', type: 'hasMany', foreignKey: 'fk1', target: 'collection1' }, + { id: 2, name: 'field2', type: 'hasMany', foreignKey: 'fk2', target: 'collectionX' }, // target不匹配 + { id: 3, name: 'field3', type: 'hasMany', foreignKey: 'fk3', target: 'collection3' }, + ], + }; + + const block = { + foreignKeyFields: [ + { id: 1, name: 'fk1', collectionName: 'collection1' }, + { id: 2, name: 'fk2', collectionName: 'collection2' }, // 与field2的target不匹配 + { id: 3, name: 'fk3', collectionName: 'collection3' }, + ], + }; + + const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any); + + expect(result).toEqual([ + { id: 1, name: 'fk1', collectionName: 'collection1' }, + { id: 3, name: 'fk3', collectionName: 'collection3' }, + ]); + }); + + it('should filter out belongsTo type fields', () => { + const filterBlockCollection = { + fields: [ + { id: 1, name: 'field1', type: 'hasMany', foreignKey: 'fk1', target: 'collection1' }, + { id: 2, name: 'field2', type: 'belongsTo', foreignKey: 'fk2', target: 'collection2' }, // belongsTo类型 + { id: 3, name: 'field3', type: 'hasMany', foreignKey: 'fk3', target: 'collection3' }, + ], + }; + + const block = { + foreignKeyFields: [ + { id: 1, name: 'fk1', collectionName: 'collection1' }, + { id: 2, name: 'fk2', collectionName: 'collection2' }, + { id: 3, name: 'fk3', collectionName: 'collection3' }, + ], + }; + + const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any); + + expect(result).toEqual([ + { id: 1, name: 'fk1', collectionName: 'collection1' }, + { id: 3, name: 'fk3', collectionName: 'collection3' }, + ]); + }); + + it('should handle when both name and target collection match', () => { + const filterBlockCollection = { + fields: [ + { id: 1, name: 'field1', type: 'hasMany', foreignKey: 'fk1', target: 'collection1' }, + { id: 2, name: 'field2', type: 'hasOne', foreignKey: 'fk2', target: 'collection2' }, + { id: 3, name: 'field3', type: 'hasMany', foreignKey: 'fk3', target: 'wrongCollection' }, // 目标表不匹配 + ], + }; + + const block = { + foreignKeyFields: [ + { id: 1, name: 'fk1', collectionName: 'collection1' }, + { id: 2, name: 'fk2', collectionName: 'collection2' }, + { id: 3, name: 'fk3', collectionName: 'collection3' }, // 与field3的target不匹配 + ], + }; + + const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any); + + expect(result).toEqual([ + { id: 1, name: 'fk1', collectionName: 'collection1' }, + { id: 2, name: 'fk2', collectionName: 'collection2' }, + ]); + }); + + // 保留原有的通用测试用例 it("should return all foreign key fields matching the filter block collection's foreign key properties", () => { const filterBlockCollection = { fields: [ diff --git a/packages/core/client/src/filter-provider/utils.ts b/packages/core/client/src/filter-provider/utils.ts index 1b3c6920ba..c2759df6ed 100644 --- a/packages/core/client/src/filter-provider/utils.ts +++ b/packages/core/client/src/filter-provider/utils.ts @@ -49,9 +49,13 @@ export const getSupportFieldsByAssociation = (inheritCollectionsChain: string[], export const getSupportFieldsByForeignKey = (filterBlockCollection: Collection, block: DataBlock) => { return block.foreignKeyFields?.filter((foreignKeyField) => { - return filterBlockCollection.fields.some( - (field) => field.type !== 'belongsTo' && field.foreignKey === foreignKeyField.name, - ); + return filterBlockCollection.fields.some((field) => { + return ( + field.type !== 'belongsTo' && + field.foreignKey === foreignKeyField.name && // 1. 外键字段的 name 要一致 + field.target === foreignKeyField.collectionName // 2. 关系字段的目标表要和外键的数据表一致 + ); + }); }); }; @@ -193,19 +197,21 @@ export const useFilterAPI = () => { const doFilter = useCallback( ( - value: any | ((target: FilterTarget['targets'][0], block: DataBlock) => any), + value: any | ((target: FilterTarget['targets'][0], block: DataBlock, sourceKey?: string) => any), field: string | ((target: FilterTarget['targets'][0], block: DataBlock) => string) = 'id', operator: string | ((target: FilterTarget['targets'][0]) => string) = '$eq', ) => { + const currentBlock = dataBlocks.find((block) => block.uid === fieldSchema.parent['x-uid']); dataBlocks.forEach((block) => { + let key = field as string; const target = targets.find((target) => target.uid === block.uid); if (!target) return; if (_.isFunction(value)) { - value = value(target, block); + value = value(target, block, getSourceKey(currentBlock, target.field)); } if (_.isFunction(field)) { - field = field(target, block); + key = field(target, block); } if (_.isFunction(operator)) { operator = operator(target); @@ -219,7 +225,7 @@ export const useFilterAPI = () => { storedFilter[uid] = { $and: [ { - [field]: { + [key]: { [operator]: value, }, }, @@ -248,7 +254,7 @@ export const useFilterAPI = () => { ); }); }, - [dataBlocks, targets, uid], + [dataBlocks, targets, uid, fieldSchema], ); return { @@ -268,3 +274,8 @@ export const isInFilterFormBlock = (fieldSchema: Schema) => { } return false; }; + +function getSourceKey(currentBlock: DataBlock, field: string) { + const associationField = currentBlock?.associatedFields?.find((item) => item.foreignKey === field); + return associationField?.sourceKey || field?.split?.('.')?.[1]; +} diff --git a/packages/core/utils/src/common.ts b/packages/core/utils/src/common.ts index c4894af961..f6e9aef0b7 100644 --- a/packages/core/utils/src/common.ts +++ b/packages/core/utils/src/common.ts @@ -54,3 +54,47 @@ export const hasEmptyValue = (objOrArr: object | any[]) => { export const nextTick = (fn: () => void) => { setTimeout(fn); }; + +/** + * 通用树节点深度优先遍历函数 + * @param {Object|Array} tree - 要遍历的树结构 + * @param {Function} callback - 遍历每个节点时执行的回调函数,返回真值时停止遍历并返回当前节点 + * @param {Object} options - 配置选项 + * @param {string|Function} options.childrenKey - 子节点的属性名,默认为'children',也可以是一个函数 + * @returns {any|undefined} - 找到的节点或undefined + */ +export function treeFind( + tree: T | T[], + callback: (node: T) => boolean, + options: { + childrenKey?: string | ((node: T) => T[] | undefined); + } = {}, +): T | undefined { + if (!tree) return undefined; + + const { childrenKey = 'children' } = options; + + // 处理根节点是数组的情况 + const nodes = Array.isArray(tree) ? [...tree] : [tree]; + + // 深度优先搜索 + for (const node of nodes) { + // 对当前节点调用回调函数 + if (callback(node)) { + return node; + } + + // 获取子节点 + const children = typeof childrenKey === 'function' ? childrenKey(node) : (node as any)[childrenKey]; + + // 递归处理子节点 + if (Array.isArray(children) && children.length > 0) { + const found = treeFind(children, callback, options); + if (found !== undefined) { + return found; + } + } + } + + return undefined; +}