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)'],
|
||||
setupFiles: ['./jest.setup.ts'],
|
||||
setupFilesAfterEnv: [require.resolve('jest-dom/extend-expect'), './jest.setupAfterEnv.ts'],
|
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
|
||||
moduleNameMapper: {
|
||||
...pathsToModuleNameMapper(compilerOptions.paths, {
|
||||
prefix: '<rootDir>/',
|
||||
}),
|
||||
'\\.(css|less)$': '<rootDir>/__mocks__/styleMock.js',
|
||||
},
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
babelConfig: false,
|
||||
|
@ -80,7 +80,7 @@ export const transformToFilter = (
|
||||
key = `${key}.${collectionField.targetKey || 'id'}`;
|
||||
}
|
||||
|
||||
if (!value || value.length === 0) {
|
||||
if (!value) {
|
||||
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) => {
|
||||
if ((option.type !== 'belongsTo' && option.type !== 'hasOne') || !option.target) {
|
||||
if (!option.target) {
|
||||
return {
|
||||
key: option.name,
|
||||
value: option.name,
|
||||
|
@ -10,3 +10,4 @@ export default {
|
||||
};
|
||||
},
|
||||
} as Record<string, any>;
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { isPg } from './utils';
|
||||
import { Op } from 'sequelize';
|
||||
import { isPg } from './utils';
|
||||
|
||||
export default {
|
||||
$includes(value, ctx) {
|
||||
|
@ -32,7 +32,7 @@ describe('getValuesByPath', () => {
|
||||
a: { b: 1 },
|
||||
};
|
||||
const result = getValuesByPath(obj, '');
|
||||
expect(result).toEqual([]);
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('when path is not found', () => {
|
||||
@ -40,7 +40,7 @@ describe('getValuesByPath', () => {
|
||||
a: { b: 1 },
|
||||
};
|
||||
const result = getValuesByPath(obj, 'a.c');
|
||||
expect(result).toEqual([]);
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('when path is not found in nested array', () => {
|
||||
@ -48,7 +48,7 @@ describe('getValuesByPath', () => {
|
||||
a: [{ b: 1 }, { b: 2 }],
|
||||
};
|
||||
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', () => {
|
||||
@ -56,7 +56,7 @@ describe('getValuesByPath', () => {
|
||||
a: [{ b: 1 }, { b: 2 }],
|
||||
};
|
||||
const result = getValuesByPath(obj, 'a.');
|
||||
expect(result).toEqual([]);
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('when obj is null', () => {
|
||||
@ -70,7 +70,7 @@ describe('getValuesByPath', () => {
|
||||
a: { b: 1 },
|
||||
};
|
||||
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', () => {
|
||||
@ -78,7 +78,7 @@ describe('getValuesByPath', () => {
|
||||
a: undefined,
|
||||
};
|
||||
const result = getValuesByPath(obj, 'a.b');
|
||||
expect(result).toEqual([]);
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('the initial value is an array', () => {
|
||||
|
@ -289,6 +289,24 @@ describe('parseFilter', () => {
|
||||
).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 () => {
|
||||
const date = new Date();
|
||||
await expectParseFilter(
|
||||
|
@ -10,23 +10,28 @@ export const getValuesByPath = (obj: object, path: string, defaultValue?: any) =
|
||||
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);
|
||||
for (const element of currentValue) {
|
||||
const value = getValuesByPath(element, keys.slice(i).join('.'), defaultValue);
|
||||
result = result.concat(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
currentValue = currentValue[key] === undefined ? defaultValue : currentValue[key];
|
||||
|
||||
if (currentValue == null) {
|
||||
if (currentValue?.[key] === undefined) {
|
||||
break;
|
||||
}
|
||||
currentValue = currentValue[key];
|
||||
|
||||
if (i === keys.length - 1) {
|
||||
result.push(currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
result = result.filter(Boolean);
|
||||
|
||||
if (result.length === 0) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return result.length === 1 ? result[0] : result;
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import get from 'lodash/get';
|
||||
import set from 'lodash/set';
|
||||
import moment from 'moment';
|
||||
import { getValuesByPath } from './getValuesByPath';
|
||||
|
||||
const re = /^\s*\{\{([\s\S]*)\}\}\s*$/;
|
||||
|
||||
@ -59,9 +59,9 @@ export function flatten(target, opts?: any) {
|
||||
function unflatten(obj, opts: any = {}) {
|
||||
const parsed = {};
|
||||
const transformValue = opts.transformValue || keyIdentity;
|
||||
Object.keys(obj).forEach((key) => {
|
||||
for (const key of Object.keys(obj)) {
|
||||
set(parsed, key, transformValue(obj[key], key));
|
||||
});
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@ -161,7 +161,7 @@ export const parseFilter = async (filter: any, opts: ParseFilterOptions = {}) =>
|
||||
const match = re.exec(value);
|
||||
if (match) {
|
||||
const key = match[1].trim();
|
||||
const val = get(vars, key, null);
|
||||
const val = getValuesByPath(vars, key, null);
|
||||
const field = getField?.(path);
|
||||
value = typeof val === 'function' ? val?.({ field, operator, timezone, now }) : val;
|
||||
}
|
||||
@ -293,3 +293,8 @@ export function getDateVars() {
|
||||
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