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:
parent
aabc681245
commit
e27cff15c9
1
__mocks__/styleMock.js
Normal file
1
__mocks__/styleMock.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = {};
|
@ -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: {
|
||||||
|
...pathsToModuleNameMapper(compilerOptions.paths, {
|
||||||
prefix: '<rootDir>/',
|
prefix: '<rootDir>/',
|
||||||
}),
|
}),
|
||||||
|
'\\.(css|less)$': '<rootDir>/__mocks__/styleMock.js',
|
||||||
|
},
|
||||||
globals: {
|
globals: {
|
||||||
'ts-jest': {
|
'ts-jest': {
|
||||||
babelConfig: false,
|
babelConfig: false,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
@ -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,
|
||||||
|
@ -10,3 +10,4 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
} as Record<string, any>;
|
} as Record<string, any>;
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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(
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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('.')];
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user