fix(FilterFormBlock): fix association field can not to filter (#1699)

* feat: add getValuesByPath

* fix(FilterFormBlock): fix association field can not to filter

* test: hasMany filter test

* fix: remove null in filter

* fix: fix not responding for filter button

* fix: fix oho and o2m and obo

* fix: fix isInFilterFormBlock

* fix: fix errors

* fix: should filter out when params is empty

---------

Co-authored-by: chareice <chareice@live.com>
This commit is contained in:
被雨水过滤的空气-Rairn 2023-04-19 23:06:20 +08:00 committed by GitHub
parent d4165babf7
commit c02544c68b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 303 additions and 45 deletions

View File

@ -3,6 +3,7 @@ import { Schema, useField, useFieldSchema } from '@formily/react';
import uniq from 'lodash/uniq';
import React, { createContext, useContext, useEffect, useState } from 'react';
import { useCollectionManager } from '../collection-manager';
import { isInFilterFormBlock } from '../filter-provider';
import { RecordProvider, useRecord } from '../record-provider';
import { SchemaComponentOptions } from '../schema-component';
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
@ -164,7 +165,7 @@ export const TableSelectorProvider = (props: TableSelectorProviderProps) => {
}
let extraFilter;
if (collectionField) {
if (['oho', 'o2m'].includes(collectionField.interface)) {
if (['oho', 'o2m'].includes(collectionField.interface) && !isInFilterFormBlock(fieldSchema)) {
if (record?.[collectionField.sourceKey]) {
extraFilter = {
$or: [
@ -188,7 +189,7 @@ export const TableSelectorProvider = (props: TableSelectorProviderProps) => {
};
}
}
if (['obo'].includes(collectionField.interface)) {
if (['obo'].includes(collectionField.interface) && !isInFilterFormBlock(fieldSchema)) {
const fields = getCollectionFields(collectionField.target);
const targetField = fields.find((f) => f.foreignKey && f.foreignKey === collectionField.foreignKey);
if (targetField) {

View File

@ -10,7 +10,7 @@ import { useHistory } from 'react-router-dom';
import { useReactToPrint } from 'react-to-print';
import { AssociationFilter, useFormBlockContext, useTableBlockContext } from '../..';
import { useAPIClient, useRequest } from '../../api-client';
import { useCollection } from '../../collection-manager';
import { useCollection, useCollectionManager } from '../../collection-manager';
import { useFilterBlock } from '../../filter-provider/FilterProvider';
import { transformToFilter } from '../../filter-provider/utils';
import { useRecord } from '../../record-provider';
@ -241,6 +241,8 @@ export const useFilterBlockActionProps = () => {
const actionField = useField();
const fieldSchema = useFieldSchema();
const { getDataBlocks } = useFilterBlock();
const { name } = useCollection();
const { getCollectionJoinField } = useCollectionManager();
actionField.data = actionField.data || {};
@ -260,7 +262,9 @@ export const useFilterBlockActionProps = () => {
// 保留原有的 filter
const storedFilter = block.service.params?.[1]?.filters || {};
storedFilter[uid] = removeNullCondition(transformToFilter(form.values, fieldSchema));
storedFilter[uid] = removeNullCondition(
transformToFilter(form.values, fieldSchema, getCollectionJoinField, name),
);
const mergedFilter = mergeFilter([
...Object.values(storedFilter).map((filter) => removeNullCondition(filter)),
@ -279,6 +283,7 @@ export const useFilterBlockActionProps = () => {
);
actionField.data.loading = false;
} catch (error) {
console.error(error);
actionField.data.loading = false;
}
},
@ -785,8 +790,6 @@ export const useRefreshActionProps = () => {
};
};
export const useDetailsPaginationProps = () => {
const ctx = useDetailsBlockContext();
const count = ctx.service?.data?.meta?.count || 0;

View File

@ -0,0 +1,58 @@
import { transformToFilter } from '../utils';
// TODO: 前端测试报错解决之后,把该文件重命名为 transformToFilter.test.ts
describe('transformToFilter', () => {
it('should transform to filter', () => {
const values = {
name: 'name',
email: 'email',
user: {
name: 'name',
},
list: [{ name: 'name1' }, { name: 'name2' }, { name: 'name3' }],
};
const fieldSchema = {
'x-filter-operators': {
name: '$eq',
email: '$eq',
},
};
const getField = (name: string) => {
if (name === 'user' || name === 'list') {
return {
targetKey: 'name',
};
}
return {
targetKey: undefined,
};
};
expect(transformToFilter(values, fieldSchema as any, getField)).toEqual({
$and: [
{
name: {
$eq: 'name',
},
},
{
email: {
$eq: 'email',
},
},
{
'user.name': {
$eq: 'name',
},
},
{
'list.name': {
$eq: ['name1', 'name2', 'name3'],
},
},
],
});
});
});

View File

@ -1,10 +1,10 @@
import { Schema, useFieldSchema } from '@formily/react';
import { isEmpty, isPlainObject } from '@nocobase/utils/client';
import { flatten, getValuesByPath } from '@nocobase/utils/client';
import _ from 'lodash';
import { useCallback, useEffect, useState } from 'react';
import { mergeFilter } from '../block-provider';
import { FilterTarget, findFilterTargets } from '../block-provider/hooks';
import { Collection, FieldOptions, useCollection } from '../collection-manager';
import { Collection, CollectionFieldOptions, FieldOptions, useCollection } from '../collection-manager';
import { removeNullCondition } from '../schema-component';
import { findFilterOperators } from '../schema-component/antd/form-item/SchemaSettingOptions';
import { useFilterBlock } from './FilterProvider';
@ -45,38 +45,55 @@ export const useSupportedBlocks = (filterBlockType: FilterBlockType) => {
}
};
/**
* Recursively flatten an object and generate a query object.
* @param result - The resulting query object.
* @param obj - The object to be flattened and queried.
* @param key - The current key in the object tree.
* @param operators - The operators to use for each field.
* @returns The resulting query object.
*/
const flattenAndQueryObject = (result: Record<string, any>, obj: any, key: string, operators: Record<string, any>) => {
if (!obj) return result;
if (!isPlainObject(obj)) {
result[key] = {
[operators[key] || '$eq']: obj,
};
} else {
Object.keys(obj).forEach((k) => {
flattenAndQueryObject(result, obj[k], `${key}.${k}`, operators);
});
}
return result;
};
export const transformToFilter = (values: Record<string, any>, fieldSchema: Schema) => {
export const transformToFilter = (
values: Record<string, any>,
fieldSchema: Schema,
getCollectionJoinField: (name: string) => CollectionFieldOptions,
collectionName: string,
) => {
const { operators } = findFilterOperators(fieldSchema);
return {
values = flatten(values, {
breakOn({ value, path }) {
const collectionField = getCollectionJoinField(`${collectionName}.${path}`);
if (collectionField?.target) {
if (Array.isArray(value)) {
return true;
}
const targetKey = collectionField.targetKey || 'id';
if (value && value[targetKey] != null) {
return true;
}
}
return false;
},
});
const result = {
$and: Object.keys(values)
.map((key) => flattenAndQueryObject({}, values[key], key, operators))
.filter((item) => !isEmpty(item)),
.map((key) => {
let value = _.get(values, key);
const collectionField = getCollectionJoinField(`${collectionName}.${key}`);
if (collectionField?.target) {
value = getValuesByPath(value, collectionField.targetKey || 'id');
key = `${key}.${collectionField.targetKey || 'id'}`;
}
if (!value || value.length === 0) {
return null;
}
return {
[key]: {
[operators[key] || '$eq']: value,
},
};
})
.filter(Boolean),
};
return result;
};
export const useAssociatedFields = () => {
@ -86,7 +103,9 @@ export const useAssociatedFields = () => {
};
export const isAssocField = (field?: FieldOptions) => {
return ['o2o', 'oho', 'obo', 'm2o', 'createdBy', 'updatedBy', 'o2m', 'm2m', 'linkTo'].includes(field?.interface);
return ['o2o', 'oho', 'obo', 'm2o', 'createdBy', 'updatedBy', 'o2m', 'm2m', 'linkTo', 'chinaRegion'].includes(
field?.interface,
);
};
export const isSameCollection = (c1: Collection, c2: Collection) => {
@ -167,3 +186,13 @@ export const useFilterAPI = () => {
doFilter,
};
};
export const isInFilterFormBlock = (fieldSchema: Schema) => {
while (fieldSchema) {
if (fieldSchema['x-filter-targets']) {
return fieldSchema['x-decorator'] === 'FilterFormBlockProvider';
}
fieldSchema = fieldSchema.parent;
}
return false;
};

View File

@ -2,6 +2,7 @@ import { useFieldSchema } from '@formily/react';
import { useCallback, useMemo } from 'react';
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
import { useCollection, useCollectionManager } from '../../../collection-manager';
import { isInFilterFormBlock } from '../../../filter-provider';
import { useRecord } from '../../../record-provider';
export default function useServiceOptions(props) {
@ -43,7 +44,7 @@ export default function useServiceOptions(props) {
return mergeFilter(
[
mergeFilter([
isOToAny
isOToAny && !isInFilterFormBlock(fieldSchema)
? {
[collectionField.foreignKey]: {
$is: null,
@ -52,7 +53,7 @@ export default function useServiceOptions(props) {
: null,
params?.filter,
]),
isOToAny && sourceValue !== undefined && sourceValue !== null
isOToAny && sourceValue !== undefined && sourceValue !== null && !isInFilterFormBlock(fieldSchema)
? {
[collectionField.foreignKey]: {
$eq: sourceValue,

View File

@ -239,7 +239,7 @@ const Drawer: React.FunctionComponent<{
console.log('collectionField', options);
const getFilter = () => {
const targetKey = collectionField.targetKey || 'id';
const list = options.map((option) => option[targetKey]);
const list = options.map((option) => option[targetKey]).filter(Boolean);
const filter = list.length ? { $and: [{ [`${targetKey}.$ne`]: list }] } : {};
return filter;
};

View File

@ -294,10 +294,10 @@ export const useFilterFormItemInitializerFields = (options?: any) => {
if (isAssocField(field)) {
schema = {
type: 'string',
name: field.name,
name: `${field.name}`,
required: false,
'x-designer': 'AssociationSelect.FilterDesigner',
'x-component': 'AssociationSelect',
'x-designer': 'FormItem.FilterFormDesigner',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': `${name}.${field.name}`,
'x-component-props': field.uiSchema?.['x-component-props'],
@ -397,6 +397,9 @@ const getItem = (
title: field.uiSchema?.title,
children: subFields
.map((subField) =>
// 使用 | 分隔,是为了防止 form.values 中出现 { a: { b: 1 } } 的情况
// 使用 | 分隔后form.values 中会出现 { 'a|b': 1 } 的情况,这种情况下
// 就可以知道该字段是一个关系字段中的输入框,进而特殊处理
getItem(subField, `${schemaName}.${subField.name}`, collectionName, getCollectionFields, [
...processedCollections,
field.target,

View File

@ -14,6 +14,47 @@ describe('filter', () => {
await db.close();
});
it('should filter by hasMany association field', async () => {
const DeptCollection = db.collection({
name: 'depts',
fields: [
{ type: 'string', name: 'name' },
{ type: 'belongsTo', name: 'org', target: 'orgs' },
],
});
const OrgCollection = db.collection({
name: 'orgs',
fields: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'depts', target: 'depts' },
],
});
await db.sync();
await OrgCollection.repository.create({
values: [
{
name: 'org1',
depts: [{ name: 'dept1' }, { name: 'dept2' }],
},
{
name: 'org2',
depts: [{ name: 'dept3' }, { name: 'dept4' }],
},
],
});
const dept1 = await DeptCollection.repository.findOne({});
const orgs = await OrgCollection.repository.find({
filter: { $and: [{ depts: { id: { $eq: dept1.get('id') } } }] },
});
expect(orgs.length).toBe(1);
});
it('should filter by association field', async () => {
const UserCollection = db.collection({
name: 'users',

View File

@ -0,0 +1,89 @@
import { getValuesByPath } from '../getValuesByPath';
describe('getValuesByPath', () => {
it('should return correct value', () => {
const obj = {
a: {
b: 1,
},
};
const result = getValuesByPath(obj, 'a.b');
expect(result).toEqual(1);
});
it('should return an array of values', () => {
const obj = {
a: [{ b: 1 }, { b: 2 }],
};
const result = getValuesByPath(obj, 'a.b');
expect(result).toEqual([1, 2]);
});
it('nested array', () => {
const obj = {
a: [{ b: [{ c: 1 }, { c: 2 }] }, { b: [{ c: 3 }, { c: 4 }] }],
};
const result = getValuesByPath(obj, 'a.b.c');
expect(result).toEqual([1, 2, 3, 4]);
});
it('when path is empty', () => {
const obj = {
a: { b: 1 },
};
const result = getValuesByPath(obj, '');
expect(result).toEqual([]);
});
it('when path is not found', () => {
const obj = {
a: { b: 1 },
};
const result = getValuesByPath(obj, 'a.c');
expect(result).toEqual([]);
});
it('when path is not found in nested array', () => {
const obj = {
a: [{ b: 1 }, { b: 2 }],
};
const result = getValuesByPath(obj, 'a.c');
expect(result).toEqual([]);
});
it('when path is not found in nested array with empty string', () => {
const obj = {
a: [{ b: 1 }, { b: 2 }],
};
const result = getValuesByPath(obj, 'a.');
expect(result).toEqual([]);
});
it('when obj is null', () => {
const obj = null;
const result = getValuesByPath(obj, 'a.b');
expect(result).toBe(undefined);
});
it('default value', () => {
const obj = {
a: { b: 1 },
};
const result = getValuesByPath(obj, 'a.c', null);
expect(result).toEqual([]);
});
it('should return empty array when obj key value is undefined', () => {
const obj = {
a: undefined,
};
const result = getValuesByPath(obj, 'a.b');
expect(result).toEqual([]);
});
it('the initial value is an array', () => {
const arr = [{ b: 1 }, { b: 2 }];
const result = getValuesByPath(arr, 'b', []);
expect(result).toEqual([1, 2]);
});
});

View File

@ -2,6 +2,7 @@ export * from './collections-graph';
export * from './common';
export * from './date';
export * from './forEach';
export * from './getValuesByPath';
export * from './merge';
export * from './number';
export * from './parse-filter';

View File

@ -0,0 +1,32 @@
export const getValuesByPath = (obj: object, path: string, defaultValue?: any) => {
if (!obj) {
return defaultValue;
}
const keys = path.split('.');
let result: any[] = [];
let currentValue = obj;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (Array.isArray(currentValue)) {
for (let j = 0; j < currentValue.length; j++) {
const value = getValuesByPath(currentValue[j], keys.slice(i).join('.'), defaultValue);
result = result.concat(value);
}
break;
}
currentValue = currentValue[key] === undefined ? defaultValue : currentValue[key];
if (currentValue == null) {
break;
}
if (i === keys.length - 1) {
result.push(currentValue);
}
}
return result.length === 1 ? result[0] : result;
};

View File

@ -12,7 +12,7 @@ function keyIdentity(key) {
return key;
}
function flatten(target, opts?: any) {
export function flatten(target, opts?: any) {
opts = opts || {};
const delimiter = opts.delimiter || '.';
@ -32,7 +32,7 @@ function flatten(target, opts?: any) {
const newKey = prev ? prev + delimiter + transformKey(key) : transformKey(key);
if (opts.breakOn({ key })) {
if (opts.breakOn?.({ key, value, path: newKey })) {
output[newKey] = transformValue(value, newKey);
return;
}