feat: support to-multi field variables (#1680)

* feat: support to parse association variable

* chore: change comment

* feat(operators): support array as value in string operator

* refactor: add a special marker

* feat: date support to-multi field variables

* feat: support for notIn

* refactor: rename

* test: fix test error

* Revert "test: fix test error"

This reverts commit 3d139698f6295678a39b77b08c88427f9fafe247.

* Revert "refactor: rename"

This reverts commit 2e16225c038e18ee25f1136d510cb4746bd9932f.

* Revert "feat: support for notIn"

This reverts commit 2087e5c4da1429b260890d5136c714ad541955a9.

* Revert "feat: date support to-multi field variables"

This reverts commit 0d7b2db0512ba7b632a2ab61f37cf83c0d06b9a1.

* Revert "refactor: add a special marker"

This reverts commit 7ba2e4bc00d79f60a2e90340df65b2938985750a.

* Revert "feat(operators): support array as value in string operator"

This reverts commit 0897cd19e9863248ef9e2af6c91ee14e805778a4.

* refactor: improve code

* test: fix can not import style file

* feat: only eq and ne oprators support mutil relation fields

* test: add example.test.ts

* refactor: remove jsonata

* Revert "test: add example.test.ts"

This reverts commit 0ad2ea458cd8a964891490d2021a5b000f391395.

* test: fix error

* fix: fix error

* refactor: remove async

* chore: rebase

* test: fix error

* test: fix errors
This commit is contained in:
被雨水过滤的空气-Rairn 2023-05-21 17:18:35 +08:00 committed by GitHub
parent aabc681245
commit e27cff15c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 128 additions and 21 deletions

1
__mocks__/styleMock.js Normal file
View File

@ -0,0 +1 @@
module.exports = {};

View File

@ -11,9 +11,12 @@ module.exports = {
testMatch: ['**/__tests__/**/*.test.[jt]s?(x)'], testMatch: ['**/__tests__/**/*.test.[jt]s?(x)'],
setupFiles: ['./jest.setup.ts'], setupFiles: ['./jest.setup.ts'],
setupFilesAfterEnv: [require.resolve('jest-dom/extend-expect'), './jest.setupAfterEnv.ts'], setupFilesAfterEnv: [require.resolve('jest-dom/extend-expect'), './jest.setupAfterEnv.ts'],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { moduleNameMapper: {
prefix: '<rootDir>/', ...pathsToModuleNameMapper(compilerOptions.paths, {
}), prefix: '<rootDir>/',
}),
'\\.(css|less)$': '<rootDir>/__mocks__/styleMock.js',
},
globals: { globals: {
'ts-jest': { 'ts-jest': {
babelConfig: false, babelConfig: false,

View File

@ -80,7 +80,7 @@ export const transformToFilter = (
key = `${key}.${collectionField.targetKey || 'id'}`; key = `${key}.${collectionField.targetKey || 'id'}`;
} }
if (!value || value.length === 0) { if (!value) {
return null; return null;
} }

View File

@ -0,0 +1,74 @@
import { CollectionFieldOptions } from '../../../../collection-manager';
import { useFilterOptions } from '../../filter';
interface Operator {
label: string;
value: string;
}
interface GetOptionsParams {
schema: any;
operator: Operator;
maxDepth: number;
count?: number;
}
const isOperatorSupportMultiRelation = (operator: Operator) => {
if (!operator) return false;
return ['$eq', '$ne'].includes(operator.value);
};
const isSingleRelationField = (field: CollectionFieldOptions) => {
if (!field) return false;
return field.type === 'belongsTo' || field.type === 'hasOne';
};
export const useOptions = (collectionName: string, { schema, operator, maxDepth, count = 1 }: GetOptionsParams) => {
if (count > maxDepth) {
return [];
}
const result = useFilterOptions(collectionName).map((option) => {
if (!option.target) {
return {
key: option.name,
value: option.name,
label: option.title,
// TODO: 现在是通过组件的名称来过滤能够被选择的选项,这样的坏处是不够精确,后续可以优化
disabled: schema?.['x-component'] !== option.schema?.['x-component'],
};
}
const children =
useOptions(option.target, {
schema,
operator,
maxDepth,
count: count + 1,
}) || [];
return {
key: option.name,
value: option.name,
label: option.title,
children,
disabled:
(!isSingleRelationField(option) && !isOperatorSupportMultiRelation(operator)) ||
children.every((child) => child.disabled),
};
});
return result;
};
export const useUserVariable = ({ schema, operator }) => {
const options = useOptions('users', { schema, operator, maxDepth: 3 }) || [];
return {
label: `{{t("Current user")}}`,
value: '$user',
key: '$user',
disabled: options.every((option) => option.disabled),
children: options,
};
};

View File

@ -15,7 +15,7 @@ const getChildren = (options: any[], { schema, operator, maxDepth, count = 1, ge
} }
const result = options.map((option) => { const result = options.map((option) => {
if ((option.type !== 'belongsTo' && option.type !== 'hasOne') || !option.target) { if (!option.target) {
return { return {
key: option.name, key: option.name,
value: option.name, value: option.name,

View File

@ -10,3 +10,4 @@ export default {
}; };
}, },
} as Record<string, any>; } as Record<string, any>;

View File

@ -1,5 +1,5 @@
import { isPg } from './utils';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import { isPg } from './utils';
export default { export default {
$includes(value, ctx) { $includes(value, ctx) {

View File

@ -32,7 +32,7 @@ describe('getValuesByPath', () => {
a: { b: 1 }, a: { b: 1 },
}; };
const result = getValuesByPath(obj, ''); const result = getValuesByPath(obj, '');
expect(result).toEqual([]); expect(result).toEqual(undefined);
}); });
it('when path is not found', () => { it('when path is not found', () => {
@ -40,7 +40,7 @@ describe('getValuesByPath', () => {
a: { b: 1 }, a: { b: 1 },
}; };
const result = getValuesByPath(obj, 'a.c'); const result = getValuesByPath(obj, 'a.c');
expect(result).toEqual([]); expect(result).toEqual(undefined);
}); });
it('when path is not found in nested array', () => { it('when path is not found in nested array', () => {
@ -48,7 +48,7 @@ describe('getValuesByPath', () => {
a: [{ b: 1 }, { b: 2 }], a: [{ b: 1 }, { b: 2 }],
}; };
const result = getValuesByPath(obj, 'a.c'); const result = getValuesByPath(obj, 'a.c');
expect(result).toEqual([]); expect(result).toEqual(undefined);
}); });
it('when path is not found in nested array with empty string', () => { it('when path is not found in nested array with empty string', () => {
@ -56,7 +56,7 @@ describe('getValuesByPath', () => {
a: [{ b: 1 }, { b: 2 }], a: [{ b: 1 }, { b: 2 }],
}; };
const result = getValuesByPath(obj, 'a.'); const result = getValuesByPath(obj, 'a.');
expect(result).toEqual([]); expect(result).toEqual(undefined);
}); });
it('when obj is null', () => { it('when obj is null', () => {
@ -70,7 +70,7 @@ describe('getValuesByPath', () => {
a: { b: 1 }, a: { b: 1 },
}; };
const result = getValuesByPath(obj, 'a.c', null); const result = getValuesByPath(obj, 'a.c', null);
expect(result).toEqual([]); expect(result).toEqual(null);
}); });
it('should return empty array when obj key value is undefined', () => { it('should return empty array when obj key value is undefined', () => {
@ -78,7 +78,7 @@ describe('getValuesByPath', () => {
a: undefined, a: undefined,
}; };
const result = getValuesByPath(obj, 'a.b'); const result = getValuesByPath(obj, 'a.b');
expect(result).toEqual([]); expect(result).toEqual(undefined);
}); });
it('the initial value is an array', () => { it('the initial value is an array', () => {

View File

@ -289,6 +289,24 @@ describe('parseFilter', () => {
).toEqual({ createdAt: { $eq: date } }); ).toEqual({ createdAt: { $eq: date } });
}); });
test('$user & array', async () => {
const date = new Date();
await expectParseFilter(
{
'roles.name.$eq': '{{$user.roles.name}}',
},
{
vars: {
$user: async (fields) => {
return {
roles: [{ name: 'admin' }, { name: 'user' }],
};
},
},
},
).toEqual({ roles: { name: { $eq: ['admin', 'user'] } } });
});
test('$dateOn', async () => { test('$dateOn', async () => {
const date = new Date(); const date = new Date();
await expectParseFilter( await expectParseFilter(

View File

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

View File

@ -1,6 +1,6 @@
import get from 'lodash/get';
import set from 'lodash/set'; import set from 'lodash/set';
import moment from 'moment'; import moment from 'moment';
import { getValuesByPath } from './getValuesByPath';
const re = /^\s*\{\{([\s\S]*)\}\}\s*$/; const re = /^\s*\{\{([\s\S]*)\}\}\s*$/;
@ -59,9 +59,9 @@ export function flatten(target, opts?: any) {
function unflatten(obj, opts: any = {}) { function unflatten(obj, opts: any = {}) {
const parsed = {}; const parsed = {};
const transformValue = opts.transformValue || keyIdentity; const transformValue = opts.transformValue || keyIdentity;
Object.keys(obj).forEach((key) => { for (const key of Object.keys(obj)) {
set(parsed, key, transformValue(obj[key], key)); set(parsed, key, transformValue(obj[key], key));
}); }
return parsed; return parsed;
} }
@ -161,7 +161,7 @@ export const parseFilter = async (filter: any, opts: ParseFilterOptions = {}) =>
const match = re.exec(value); const match = re.exec(value);
if (match) { if (match) {
const key = match[1].trim(); const key = match[1].trim();
const val = get(vars, key, null); const val = getValuesByPath(vars, key, null);
const field = getField?.(path); const field = getField?.(path);
value = typeof val === 'function' ? val?.({ field, operator, timezone, now }) : val; value = typeof val === 'function' ? val?.({ field, operator, timezone, now }) : val;
} }
@ -293,3 +293,8 @@ export function getDateVars() {
next90Days: toDays(90), next90Days: toDays(90),
}; };
} }
export function splitPathToTwoParts(path: string) {
const parts = path.split('.');
return [parts.shift(), parts.join('.')];
}