mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 05:29:26 +08:00
Merge branch 'main' into next
This commit is contained in:
commit
e1e2f7a83c
@ -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();
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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],
|
||||
);
|
||||
|
@ -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: [
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user