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:
被雨水过滤的空气-Rain 2023-07-25 18:17:17 +08:00 committed by GitHub
parent e5612f87f8
commit 17ad645e45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 237 additions and 16 deletions

View File

@ -26,6 +26,11 @@ export const useCollection = () => {
const totalFields = unionBy(currentFields?.concat(inheritedFields), 'name').filter((v) => { const totalFields = unionBy(currentFields?.concat(inheritedFields), 'name').filter((v) => {
return !v.isForeignKey; return !v.isForeignKey;
}); });
const foreignKeyFields = unionBy(currentFields?.concat(inheritedFields), 'name').filter((v) => {
return v.isForeignKey;
});
return { return {
...collection, ...collection,
resource, resource,
@ -47,5 +52,6 @@ export const useCollection = () => {
}, },
currentFields, currentFields,
inheritedFields, inheritedFields,
foreignKeyFields,
}; };
}; };

View File

@ -236,6 +236,34 @@ export const useCollectionManager = () => {
return getInheritChain(collectionName); 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 { return {
service, service,
interfaces, interfaces,
@ -299,5 +327,6 @@ export const useCollectionManager = () => {
}); });
}, },
getAllCollectionsInheritChain, getAllCollectionsInheritChain,
getInheritCollectionsChain,
}; };
}; };

View File

@ -3,10 +3,20 @@ import { uniqBy } from 'lodash';
import React, { createContext, useEffect, useRef } from 'react'; import React, { createContext, useEffect, useRef } from 'react';
import { useBlockRequestContext } from '../block-provider/BlockProvider'; import { useBlockRequestContext } from '../block-provider/BlockProvider';
import { SharedFilter, mergeFilter } from '../block-provider/SharedFilterProvider'; 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 { removeNullCondition } from '../schema-component';
import { useAssociatedFields } from './utils'; 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>; type Collection = ReturnType<typeof useCollection>;
export interface DataBlock { export interface DataBlock {
@ -14,7 +24,7 @@ export interface DataBlock {
uid: string; uid: string;
/** 用户自行设置的区块名称 */ /** 用户自行设置的区块名称 */
title?: string; title?: string;
/** 与当前区块相关的数据表信息 */ /** 与数据区块相关的数据表信息 */
collection: Collection; collection: Collection;
/** 根据提供的参数执行该方法即可刷新数据区块的数据 */ /** 根据提供的参数执行该方法即可刷新数据区块的数据 */
doFilter: (params: any, params2?: any) => Promise<void>; doFilter: (params: any, params2?: any) => Promise<void>;
@ -22,10 +32,13 @@ export interface DataBlock {
clearFilter: (uid: string) => void; clearFilter: (uid: string) => void;
/** 数据区块表中所有的关系字段 */ /** 数据区块表中所有的关系字段 */
associatedFields?: CollectionFieldOptions[]; associatedFields?: CollectionFieldOptions[];
/** 通过右上角菜单设置的过滤条件 */ /** 数据区块表中所有的外键字段 */
foreignKeyFields?: ForeignKeyField[];
/** 数据区块已经存在的过滤条件(通过 `设置数据范围` 或者其它能设置筛选条件的功能) */
defaultFilter?: SharedFilter; defaultFilter?: SharedFilter;
/** 数据区块用于请求数据的接口 */
service?: any; service?: any;
/** 区块所对应的 DOM 容器 */ /** 数据区块所的 DOM 容器 */
dom: HTMLElement; dom: HTMLElement;
} }
@ -73,6 +86,7 @@ export const FilterBlockRecord = ({
doFilter: service.runAsync, doFilter: service.runAsync,
collection, collection,
associatedFields, associatedFields,
foreignKeyFields: collection.foreignKeyFields as ForeignKeyField[],
defaultFilter: params?.filter || {}, defaultFilter: params?.filter || {},
service, service,
dom: container.current, dom: container.current,

View File

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

View File

@ -4,10 +4,16 @@ import _ from 'lodash';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { mergeFilter } from '../block-provider'; import { mergeFilter } from '../block-provider';
import { FilterTarget, findFilterTargets } from '../block-provider/hooks'; 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 { removeNullCondition } from '../schema-component';
import { findFilterOperators } from '../schema-component/antd/form-item/SchemaSettingOptions'; import { findFilterOperators } from '../schema-component/antd/form-item/SchemaSettingOptions';
import { useFilterBlock } from './FilterProvider'; import { DataBlock, useFilterBlock } from './FilterProvider';
export enum FilterBlockType { export enum FilterBlockType {
FORM, FORM,
@ -16,6 +22,23 @@ export enum FilterBlockType {
COLLAPSE, 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 * @param filterBlockType
@ -25,6 +48,7 @@ export const useSupportedBlocks = (filterBlockType: FilterBlockType) => {
const { getDataBlocks } = useFilterBlock(); const { getDataBlocks } = useFilterBlock();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const collection = useCollection(); const collection = useCollection();
const { getAllCollectionsInheritChain } = useCollectionManager();
// Form 和 Collapse 仅支持同表的数据区块 // Form 和 Collapse 仅支持同表的数据区块
if (filterBlockType === FilterBlockType.FORM || filterBlockType === FilterBlockType.COLLAPSE) { if (filterBlockType === FilterBlockType.FORM || filterBlockType === FilterBlockType.COLLAPSE) {
@ -36,10 +60,14 @@ export const useSupportedBlocks = (filterBlockType: FilterBlockType) => {
// Table 和 Tree 支持同表或者关系表的数据区块 // Table 和 Tree 支持同表或者关系表的数据区块
if (filterBlockType === FilterBlockType.TABLE || filterBlockType === FilterBlockType.TREE) { if (filterBlockType === FilterBlockType.TABLE || filterBlockType === FilterBlockType.TREE) {
return getDataBlocks().filter((block) => { return getDataBlocks().filter((block) => {
// 1. 同表
// 2. 关系字段
// 3. 外键字段
return ( return (
fieldSchema['x-uid'] !== block.uid && fieldSchema['x-uid'] !== block.uid &&
(isSameCollection(block.collection, collection) || (isSameCollection(block.collection, collection) ||
block.associatedFields.some((field) => field.target === collection.name)) getSupportFieldsByAssociation(getAllCollectionsInheritChain(collection.name), block)?.length ||
getSupportFieldsByForeignKey(collection, block)?.length)
); );
}); });
} }

View File

@ -59,7 +59,13 @@ import {
} from '..'; } from '..';
import { useTableBlockContext } from '../block-provider'; import { useTableBlockContext } from '../block-provider';
import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks'; 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 { useCollectMenuItem, useCollectMenuItems, useMenuItem } from '../hooks/useMenuItem';
import { getTargetKey } from '../schema-component/antd/association-filter/utilts'; import { getTargetKey } from '../schema-component/antd/association-filter/utilts';
import { getFieldDefaultValue } from '../schema-component/antd/form-item'; import { getFieldDefaultValue } from '../schema-component/antd/form-item';
@ -550,6 +556,7 @@ SchemaSettings.ConnectDataBlocks = function ConnectDataBlocks(props: {
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let { targets = [], uid } = findFilterTargets(fieldSchema); let { targets = [], uid } = findFilterTargets(fieldSchema);
const compile = useCompile(); const compile = useCompile();
const { getAllCollectionsInheritChain } = useCollectionManager();
if (!inProvider) { if (!inProvider) {
return null; return null;
@ -614,14 +621,18 @@ SchemaSettings.ConnectDataBlocks = function ConnectDataBlocks(props: {
title={title} title={title}
value={target?.field || ''} value={target?.field || ''}
options={[ options={[
...block.associatedFields ...getSupportFieldsByAssociation(getAllCollectionsInheritChain(collection.name), block).map((field) => {
.filter((field) => field.target === collection.name) return {
.map((field) => { label: compile(field.uiSchema.title) || field.name,
return { value: `${field.name}.${getTargetKey(field)}`,
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'), label: t('Unconnected'),
value: '', value: '',