mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 08:36:44 +00:00
feat(filter-block): support foreign key and inheritance (#2302)
* feat: support foreign key * feat: inherit * fix: exclude belongsTo * fix: should get all collection names on inherit chain
This commit is contained in:
parent
e5612f87f8
commit
17ad645e45
@ -26,6 +26,11 @@ export const useCollection = () => {
|
||||
const totalFields = unionBy(currentFields?.concat(inheritedFields), 'name').filter((v) => {
|
||||
return !v.isForeignKey;
|
||||
});
|
||||
|
||||
const foreignKeyFields = unionBy(currentFields?.concat(inheritedFields), 'name').filter((v) => {
|
||||
return v.isForeignKey;
|
||||
});
|
||||
|
||||
return {
|
||||
...collection,
|
||||
resource,
|
||||
@ -47,5 +52,6 @@ export const useCollection = () => {
|
||||
},
|
||||
currentFields,
|
||||
inheritedFields,
|
||||
foreignKeyFields,
|
||||
};
|
||||
};
|
||||
|
@ -236,6 +236,34 @@ export const useCollectionManager = () => {
|
||||
return getInheritChain(collectionName);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取继承的所有 collectionName,排列顺序为当前表往祖先表排列
|
||||
* @param collectionName
|
||||
* @returns
|
||||
*/
|
||||
const getInheritCollectionsChain = (collectionName: string) => {
|
||||
const collectionsInheritChain = [collectionName];
|
||||
const getInheritChain = (name: string) => {
|
||||
const collection = getCollection(name);
|
||||
if (collection) {
|
||||
const { inherits } = collection;
|
||||
if (inherits) {
|
||||
for (let index = 0; index < inherits.length; index++) {
|
||||
const collectionKey = inherits[index];
|
||||
if (collectionsInheritChain.includes(collectionKey)) {
|
||||
continue;
|
||||
}
|
||||
collectionsInheritChain.push(collectionKey);
|
||||
getInheritChain(collectionKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
return collectionsInheritChain;
|
||||
};
|
||||
|
||||
return getInheritChain(collectionName);
|
||||
};
|
||||
|
||||
return {
|
||||
service,
|
||||
interfaces,
|
||||
@ -299,5 +327,6 @@ export const useCollectionManager = () => {
|
||||
});
|
||||
},
|
||||
getAllCollectionsInheritChain,
|
||||
getInheritCollectionsChain,
|
||||
};
|
||||
};
|
||||
|
@ -3,10 +3,20 @@ import { uniqBy } from 'lodash';
|
||||
import React, { createContext, useEffect, useRef } from 'react';
|
||||
import { useBlockRequestContext } from '../block-provider/BlockProvider';
|
||||
import { SharedFilter, mergeFilter } from '../block-provider/SharedFilterProvider';
|
||||
import { CollectionFieldOptions, useCollection } from '../collection-manager';
|
||||
import { CollectionFieldOptions, FieldOptions, useCollection } from '../collection-manager';
|
||||
import { removeNullCondition } from '../schema-component';
|
||||
import { useAssociatedFields } from './utils';
|
||||
|
||||
export interface ForeignKeyField extends FieldOptions {
|
||||
/** 外键字段所在的数据表的名称 */
|
||||
collectionName: string;
|
||||
isForeignKey: boolean;
|
||||
key: string;
|
||||
name: string;
|
||||
parentKey: null | string;
|
||||
reverseKey: null | string;
|
||||
}
|
||||
|
||||
type Collection = ReturnType<typeof useCollection>;
|
||||
|
||||
export interface DataBlock {
|
||||
@ -14,7 +24,7 @@ export interface DataBlock {
|
||||
uid: string;
|
||||
/** 用户自行设置的区块名称 */
|
||||
title?: string;
|
||||
/** 与当前区块相关的数据表信息 */
|
||||
/** 与数据区块相关的数据表信息 */
|
||||
collection: Collection;
|
||||
/** 根据提供的参数执行该方法即可刷新数据区块的数据 */
|
||||
doFilter: (params: any, params2?: any) => Promise<void>;
|
||||
@ -22,10 +32,13 @@ export interface DataBlock {
|
||||
clearFilter: (uid: string) => void;
|
||||
/** 数据区块表中所有的关系字段 */
|
||||
associatedFields?: CollectionFieldOptions[];
|
||||
/** 通过右上角菜单设置的过滤条件 */
|
||||
/** 数据区块表中所有的外键字段 */
|
||||
foreignKeyFields?: ForeignKeyField[];
|
||||
/** 数据区块已经存在的过滤条件(通过 `设置数据范围` 或者其它能设置筛选条件的功能) */
|
||||
defaultFilter?: SharedFilter;
|
||||
/** 数据区块用于请求数据的接口 */
|
||||
service?: any;
|
||||
/** 区块所对应的 DOM 容器 */
|
||||
/** 数据区块所的 DOM 容器 */
|
||||
dom: HTMLElement;
|
||||
}
|
||||
|
||||
@ -73,6 +86,7 @@ export const FilterBlockRecord = ({
|
||||
doFilter: service.runAsync,
|
||||
collection,
|
||||
associatedFields,
|
||||
foreignKeyFields: collection.foreignKeyFields as ForeignKeyField[],
|
||||
defaultFilter: params?.filter || {},
|
||||
service,
|
||||
dom: container.current,
|
||||
|
@ -0,0 +1,133 @@
|
||||
import { getSupportFieldsByAssociation, getSupportFieldsByForeignKey } from '../utils';
|
||||
|
||||
describe('getSupportFieldsByAssociation', () => {
|
||||
it('should return all associated fields matching the inherited collections chain', () => {
|
||||
const block = {
|
||||
associatedFields: [
|
||||
{ id: 1, target: 'collection1', name: 'field1' },
|
||||
{ id: 2, target: 'collection2', name: 'field2' },
|
||||
{ id: 3, target: 'collection1', name: 'field3' },
|
||||
],
|
||||
};
|
||||
|
||||
const inheritCollectionsChain = ['collection1', 'collection2'];
|
||||
|
||||
const result = getSupportFieldsByAssociation(inheritCollectionsChain, block as any);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ id: 1, target: 'collection1', name: 'field1' },
|
||||
{ id: 2, target: 'collection2', name: 'field2' },
|
||||
{ id: 3, target: 'collection1', name: 'field3' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an empty array when there are no matching associated fields', () => {
|
||||
const block = {
|
||||
associatedFields: [
|
||||
{ id: 1, target: 'collection1', name: 'field1' },
|
||||
{ id: 2, target: 'collection2', name: 'field2' },
|
||||
{ id: 3, target: 'collection1', name: 'field3' },
|
||||
],
|
||||
};
|
||||
|
||||
const inheritCollectionsChain = ['collection3', 'collection4'];
|
||||
|
||||
const result = getSupportFieldsByAssociation(inheritCollectionsChain, block as any);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return associated fields matching the inherited collections chain', () => {
|
||||
const block = {
|
||||
associatedFields: [
|
||||
{ id: 1, target: 'collection1', name: 'field1' },
|
||||
{ id: 2, target: 'collection2', name: 'field2' },
|
||||
{ id: 3, target: 'collection1', name: 'field3' },
|
||||
],
|
||||
};
|
||||
|
||||
const inheritCollectionsChain = ['collection1'];
|
||||
|
||||
const result = getSupportFieldsByAssociation(inheritCollectionsChain, block as any);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ id: 1, target: 'collection1', name: 'field1' },
|
||||
{ id: 3, target: 'collection1', name: 'field3' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSupportFieldsByForeignKey', () => {
|
||||
it("should return all foreign key fields matching the filter block collection's foreign key properties", () => {
|
||||
const filterBlockCollection = {
|
||||
fields: [
|
||||
{ id: 1, name: 'field1', foreignKey: 'fk1' },
|
||||
{ id: 2, name: 'field2', foreignKey: 'fk2' },
|
||||
{ id: 3, name: 'field3', foreignKey: 'fk3' },
|
||||
],
|
||||
};
|
||||
|
||||
const block = {
|
||||
foreignKeyFields: [
|
||||
{ id: 1, name: 'fk1', target: 'collection1' },
|
||||
{ id: 2, name: 'fk2', target: 'collection2' },
|
||||
{ id: 3, name: 'fk4', target: 'collection1' },
|
||||
],
|
||||
};
|
||||
|
||||
const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ id: 1, name: 'fk1', target: 'collection1' },
|
||||
{ id: 2, name: 'fk2', target: 'collection2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an empty array when there are no matching foreign key fields', () => {
|
||||
const filterBlockCollection = {
|
||||
fields: [
|
||||
{ id: 1, name: 'field1', foreignKey: 'fk1' },
|
||||
{ id: 2, name: 'field2', foreignKey: 'fk2' },
|
||||
{ id: 3, name: 'field3', foreignKey: 'fk3' },
|
||||
],
|
||||
};
|
||||
|
||||
const block = {
|
||||
foreignKeyFields: [
|
||||
{ id: 1, name: 'fk4', target: 'collection1' },
|
||||
{ id: 2, name: 'fk5', target: 'collection2' },
|
||||
{ id: 3, name: 'fk6', target: 'collection1' },
|
||||
],
|
||||
};
|
||||
|
||||
const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return foreign key fields matching the filter block collection's foreign key properties", () => {
|
||||
const filterBlockCollection = {
|
||||
fields: [
|
||||
{ id: 1, name: 'field1', foreignKey: 'fk1' },
|
||||
{ id: 2, name: 'field2', foreignKey: 'fk2' },
|
||||
{ id: 3, name: 'field3', foreignKey: 'fk3' },
|
||||
],
|
||||
};
|
||||
|
||||
const block = {
|
||||
foreignKeyFields: [
|
||||
{ id: 1, name: 'fk1', target: 'collection1' },
|
||||
{ id: 2, name: 'fk2', target: 'collection2' },
|
||||
{ id: 3, name: 'fk3', target: 'collection1' },
|
||||
],
|
||||
};
|
||||
|
||||
const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ id: 1, name: 'fk1', target: 'collection1' },
|
||||
{ id: 2, name: 'fk2', target: 'collection2' },
|
||||
{ id: 3, name: 'fk3', target: 'collection1' },
|
||||
]);
|
||||
});
|
||||
});
|
@ -4,10 +4,16 @@ import _ from 'lodash';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { mergeFilter } from '../block-provider';
|
||||
import { FilterTarget, findFilterTargets } from '../block-provider/hooks';
|
||||
import { Collection, CollectionFieldOptions, FieldOptions, useCollection } from '../collection-manager';
|
||||
import {
|
||||
Collection,
|
||||
CollectionFieldOptions,
|
||||
FieldOptions,
|
||||
useCollection,
|
||||
useCollectionManager,
|
||||
} from '../collection-manager';
|
||||
import { removeNullCondition } from '../schema-component';
|
||||
import { findFilterOperators } from '../schema-component/antd/form-item/SchemaSettingOptions';
|
||||
import { useFilterBlock } from './FilterProvider';
|
||||
import { DataBlock, useFilterBlock } from './FilterProvider';
|
||||
|
||||
export enum FilterBlockType {
|
||||
FORM,
|
||||
@ -16,6 +22,23 @@ export enum FilterBlockType {
|
||||
COLLAPSE,
|
||||
}
|
||||
|
||||
export const getSupportFieldsByAssociation = (inheritCollectionsChain: string[], block: DataBlock) => {
|
||||
return block.associatedFields?.filter((field) =>
|
||||
inheritCollectionsChain.some((collectionName) => collectionName === field.target),
|
||||
);
|
||||
};
|
||||
|
||||
export const getSupportFieldsByForeignKey = (
|
||||
filterBlockCollection: ReturnType<typeof useCollection>,
|
||||
block: DataBlock,
|
||||
) => {
|
||||
return block.foreignKeyFields?.filter((foreignKeyField) => {
|
||||
return filterBlockCollection.fields.some(
|
||||
(field) => field.type !== 'belongsTo' && field.foreignKey === foreignKeyField.name,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据筛选区块类型筛选出支持的数据区块(同表或具有关系字段的表)
|
||||
* @param filterBlockType
|
||||
@ -25,6 +48,7 @@ export const useSupportedBlocks = (filterBlockType: FilterBlockType) => {
|
||||
const { getDataBlocks } = useFilterBlock();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const collection = useCollection();
|
||||
const { getAllCollectionsInheritChain } = useCollectionManager();
|
||||
|
||||
// Form 和 Collapse 仅支持同表的数据区块
|
||||
if (filterBlockType === FilterBlockType.FORM || filterBlockType === FilterBlockType.COLLAPSE) {
|
||||
@ -36,10 +60,14 @@ export const useSupportedBlocks = (filterBlockType: FilterBlockType) => {
|
||||
// Table 和 Tree 支持同表或者关系表的数据区块
|
||||
if (filterBlockType === FilterBlockType.TABLE || filterBlockType === FilterBlockType.TREE) {
|
||||
return getDataBlocks().filter((block) => {
|
||||
// 1. 同表
|
||||
// 2. 关系字段
|
||||
// 3. 外键字段
|
||||
return (
|
||||
fieldSchema['x-uid'] !== block.uid &&
|
||||
(isSameCollection(block.collection, collection) ||
|
||||
block.associatedFields.some((field) => field.target === collection.name))
|
||||
getSupportFieldsByAssociation(getAllCollectionsInheritChain(collection.name), block)?.length ||
|
||||
getSupportFieldsByForeignKey(collection, block)?.length)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -59,7 +59,13 @@ import {
|
||||
} from '..';
|
||||
import { useTableBlockContext } from '../block-provider';
|
||||
import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks';
|
||||
import { FilterBlockType, isSameCollection, useSupportedBlocks } from '../filter-provider/utils';
|
||||
import {
|
||||
FilterBlockType,
|
||||
getSupportFieldsByAssociation,
|
||||
getSupportFieldsByForeignKey,
|
||||
isSameCollection,
|
||||
useSupportedBlocks,
|
||||
} from '../filter-provider/utils';
|
||||
import { useCollectMenuItem, useCollectMenuItems, useMenuItem } from '../hooks/useMenuItem';
|
||||
import { getTargetKey } from '../schema-component/antd/association-filter/utilts';
|
||||
import { getFieldDefaultValue } from '../schema-component/antd/form-item';
|
||||
@ -550,6 +556,7 @@ SchemaSettings.ConnectDataBlocks = function ConnectDataBlocks(props: {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { targets = [], uid } = findFilterTargets(fieldSchema);
|
||||
const compile = useCompile();
|
||||
const { getAllCollectionsInheritChain } = useCollectionManager();
|
||||
|
||||
if (!inProvider) {
|
||||
return null;
|
||||
@ -614,14 +621,18 @@ SchemaSettings.ConnectDataBlocks = function ConnectDataBlocks(props: {
|
||||
title={title}
|
||||
value={target?.field || ''}
|
||||
options={[
|
||||
...block.associatedFields
|
||||
.filter((field) => field.target === collection.name)
|
||||
.map((field) => {
|
||||
return {
|
||||
label: compile(field.uiSchema.title) || field.name,
|
||||
value: `${field.name}.${getTargetKey(field)}`,
|
||||
};
|
||||
}),
|
||||
...getSupportFieldsByAssociation(getAllCollectionsInheritChain(collection.name), block).map((field) => {
|
||||
return {
|
||||
label: compile(field.uiSchema.title) || field.name,
|
||||
value: `${field.name}.${getTargetKey(field)}`,
|
||||
};
|
||||
}),
|
||||
...getSupportFieldsByForeignKey(collection, block).map((field) => {
|
||||
return {
|
||||
label: `${compile(field.uiSchema.title) || field.name} [${t('Foreign key')}]`,
|
||||
value: field.name,
|
||||
};
|
||||
}),
|
||||
{
|
||||
label: t('Unconnected'),
|
||||
value: '',
|
||||
|
Loading…
Reference in New Issue
Block a user