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