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:
parent
d4165babf7
commit
c02544c68b
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
89
packages/core/utils/src/__tests__/getValuesByPath.test.ts
Normal file
89
packages/core/utils/src/__tests__/getValuesByPath.test.ts
Normal 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]);
|
||||
});
|
||||
});
|
@ -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';
|
||||
|
32
packages/core/utils/src/getValuesByPath.ts
Normal file
32
packages/core/utils/src/getValuesByPath.ts
Normal 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;
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user