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) => {
|
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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -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 { 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)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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: '',
|
||||||
|
Loading…
Reference in New Issue
Block a user