Merge branch 'main' into next

This commit is contained in:
nocobase[bot] 2025-04-18 01:24:33 +00:00
commit e1e2f7a83c
6 changed files with 395 additions and 11 deletions

View File

@ -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<string, CollectionFieldOptions[]> = {};
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<InheritanceCollectionMixin>(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();

View File

@ -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<InheritanceCollectionMixin>('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<InheritanceCollectionMixin>('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<InheritanceCollectionMixin>('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<InheritanceCollectionMixin>('child');
const grandChild = collectionManager.getCollection<InheritanceCollectionMixin>('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);
});
});
});

View File

@ -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<InheritanceCollectionMixin>(collectionName)
?.getAllCollectionsInheritChain();
?.getInheritChain();
},
[dm],
);

View File

@ -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: [

View File

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

View File

@ -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<T = any>(
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;
}