From 17ad645e45abecb838bba58e0a179e50f0ba50cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=AB=E9=9B=A8=E6=B0=B4=E8=BF=87=E6=BB=A4=E7=9A=84?= =?UTF-8?q?=E7=A9=BA=E6=B0=94-Rain?= <958414905@qq.com> Date: Tue, 25 Jul 2023 18:17:17 +0800 Subject: [PATCH] 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 --- .../collection-manager/hooks/useCollection.ts | 6 + .../hooks/useCollectionManager.ts | 29 ++++ .../src/filter-provider/FilterProvider.tsx | 22 ++- .../filter-provider/__tests__/utiles.test.ts | 133 ++++++++++++++++++ .../core/client/src/filter-provider/utils.ts | 34 ++++- .../src/schema-settings/SchemaSettings.tsx | 29 ++-- 6 files changed, 237 insertions(+), 16 deletions(-) create mode 100644 packages/core/client/src/filter-provider/__tests__/utiles.test.ts diff --git a/packages/core/client/src/collection-manager/hooks/useCollection.ts b/packages/core/client/src/collection-manager/hooks/useCollection.ts index ced71ea95..2920a4442 100644 --- a/packages/core/client/src/collection-manager/hooks/useCollection.ts +++ b/packages/core/client/src/collection-manager/hooks/useCollection.ts @@ -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, }; }; diff --git a/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts b/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts index 4b252f43d..1397f4319 100644 --- a/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts +++ b/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts @@ -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, }; }; diff --git a/packages/core/client/src/filter-provider/FilterProvider.tsx b/packages/core/client/src/filter-provider/FilterProvider.tsx index 03f252424..831a3172c 100644 --- a/packages/core/client/src/filter-provider/FilterProvider.tsx +++ b/packages/core/client/src/filter-provider/FilterProvider.tsx @@ -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; export interface DataBlock { @@ -14,7 +24,7 @@ export interface DataBlock { uid: string; /** 用户自行设置的区块名称 */ title?: string; - /** 与当前区块相关的数据表信息 */ + /** 与数据区块相关的数据表信息 */ collection: Collection; /** 根据提供的参数执行该方法即可刷新数据区块的数据 */ doFilter: (params: any, params2?: any) => Promise; @@ -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, diff --git a/packages/core/client/src/filter-provider/__tests__/utiles.test.ts b/packages/core/client/src/filter-provider/__tests__/utiles.test.ts new file mode 100644 index 000000000..3b4f688d7 --- /dev/null +++ b/packages/core/client/src/filter-provider/__tests__/utiles.test.ts @@ -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' }, + ]); + }); +}); diff --git a/packages/core/client/src/filter-provider/utils.ts b/packages/core/client/src/filter-provider/utils.ts index e1b5c2a62..9bf65e62a 100644 --- a/packages/core/client/src/filter-provider/utils.ts +++ b/packages/core/client/src/filter-provider/utils.ts @@ -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, + 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) ); }); } diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx index 7285930a8..441bc4bf4 100644 --- a/packages/core/client/src/schema-settings/SchemaSettings.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx @@ -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: '',