Support define is tool logic function (#17926)
- supports isTool and timeout settings in defineLogicFunction in apps and in setting tabs definition - compute for all toolInputSchema for logic funciton, in settings and in code steps <img width="991" height="802" alt="image" src="https://github.com/user-attachments/assets/05dc1221-cac9-45a3-87b0-3b13161446fd" />
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { fromArrayToValuesByKeyRecord } from '@/utils/fromArrayToValuesByKeyRecord.util';
|
||||
|
||||
describe('fromArrayToValuesByKeyRecord', () => {
|
||||
it('should group items by the specified key', () => {
|
||||
const array = [
|
||||
{ name: 'Alice', role: 'admin' },
|
||||
{ name: 'Bob', role: 'user' },
|
||||
{ name: 'Charlie', role: 'admin' },
|
||||
];
|
||||
|
||||
const result = fromArrayToValuesByKeyRecord({ array, key: 'role' });
|
||||
|
||||
expect(result).toEqual({
|
||||
admin: [
|
||||
{ name: 'Alice', role: 'admin' },
|
||||
{ name: 'Charlie', role: 'admin' },
|
||||
],
|
||||
user: [{ name: 'Bob', role: 'user' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an empty object for an empty array', () => {
|
||||
const result = fromArrayToValuesByKeyRecord({
|
||||
array: [] as { name: string }[],
|
||||
key: 'name',
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle unique keys', () => {
|
||||
const array = [
|
||||
{ name: 'Alice', role: 'admin' },
|
||||
{ name: 'Bob', role: 'user' },
|
||||
];
|
||||
|
||||
const result = fromArrayToValuesByKeyRecord({ array, key: 'name' });
|
||||
|
||||
expect(result).toEqual({
|
||||
Alice: [{ name: 'Alice', role: 'admin' }],
|
||||
Bob: [{ name: 'Bob', role: 'user' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { getURLSafely } from '@/utils/getURLSafely';
|
||||
|
||||
describe('getURLSafely', () => {
|
||||
it('should return a URL object for a valid URL', () => {
|
||||
const result = getURLSafely('https://example.com');
|
||||
|
||||
expect(result).toBeInstanceOf(URL);
|
||||
expect(result?.hostname).toBe('example.com');
|
||||
});
|
||||
|
||||
it('should return null for an invalid URL', () => {
|
||||
const result = getURLSafely('not-a-url');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { removePropertiesFromRecord } from '@/utils/removePropertiesFromRecord';
|
||||
|
||||
describe('removePropertiesFromRecord', () => {
|
||||
it('should remove specified keys from the record', () => {
|
||||
const record = { a: 1, b: 2, c: 3 };
|
||||
|
||||
const result = removePropertiesFromRecord(record, ['b']);
|
||||
|
||||
expect(result).toEqual({ a: 1, c: 3 });
|
||||
expect('b' in result).toBe(false);
|
||||
});
|
||||
|
||||
it('should remove multiple keys', () => {
|
||||
const record = { a: 1, b: 2, c: 3 };
|
||||
|
||||
const result = removePropertiesFromRecord(record, ['a', 'c']);
|
||||
|
||||
expect(result).toEqual({ b: 2 });
|
||||
});
|
||||
|
||||
it('should not mutate the original record', () => {
|
||||
const record = { a: 1, b: 2 };
|
||||
|
||||
removePropertiesFromRecord(record, ['a']);
|
||||
|
||||
expect(record).toEqual({ a: 1, b: 2 });
|
||||
});
|
||||
|
||||
it('should return the same shape when no keys are removed', () => {
|
||||
const record = { a: 1, b: 2 };
|
||||
|
||||
const result = removePropertiesFromRecord(record, []);
|
||||
|
||||
expect(result).toEqual({ a: 1, b: 2 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { uuidToBase36 } from '@/utils/uuidToBase36';
|
||||
|
||||
describe('uuidToBase36', () => {
|
||||
it('should convert a UUID to base36', () => {
|
||||
const uuid = '550e8400-e29b-41d4-a716-446655440000';
|
||||
const result = uuidToBase36(uuid);
|
||||
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
// base36 only contains alphanumeric characters
|
||||
expect(result).toMatch(/^[0-9a-z]+$/);
|
||||
});
|
||||
|
||||
it('should produce consistent results', () => {
|
||||
const uuid = '00000000-0000-0000-0000-000000000001';
|
||||
|
||||
expect(uuidToBase36(uuid)).toBe(uuidToBase36(uuid));
|
||||
});
|
||||
|
||||
it('should convert the zero UUID', () => {
|
||||
const uuid = '00000000-0000-0000-0000-000000000000';
|
||||
|
||||
expect(uuidToBase36(uuid)).toBe('0');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { filterOutByProperty } from '@/utils/array/filterOutByProperty';
|
||||
|
||||
describe('filterOutByProperty', () => {
|
||||
it('should filter out items matching the excluded value', () => {
|
||||
const items = [
|
||||
{ id: '1', status: 'active' },
|
||||
{ id: '2', status: 'inactive' },
|
||||
{ id: '3', status: 'active' },
|
||||
];
|
||||
|
||||
const result = items.filter(filterOutByProperty('status', 'inactive'));
|
||||
|
||||
expect(result).toEqual([
|
||||
{ id: '1', status: 'active' },
|
||||
{ id: '3', status: 'active' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should keep all items when no match exists', () => {
|
||||
const items = [
|
||||
{ id: '1', name: 'Alice' },
|
||||
{ id: '2', name: 'Bob' },
|
||||
];
|
||||
|
||||
const result = items.filter(filterOutByProperty('name', 'Charlie'));
|
||||
|
||||
expect(result).toEqual(items);
|
||||
});
|
||||
|
||||
it('should handle null exclusion value', () => {
|
||||
const items = [
|
||||
{ id: '1', name: null as string | null },
|
||||
{ id: '2', name: 'Bob' },
|
||||
];
|
||||
|
||||
const result = items.filter(filterOutByProperty('name', null));
|
||||
|
||||
expect(result).toEqual([{ id: '2', name: 'Bob' }]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { findByProperty } from '@/utils/array/findByProperty';
|
||||
|
||||
describe('findByProperty', () => {
|
||||
it('should find item matching the value', () => {
|
||||
const items = [
|
||||
{ id: '1', status: 'active' },
|
||||
{ id: '2', status: 'inactive' },
|
||||
];
|
||||
|
||||
const result = items.find(findByProperty('status', 'inactive'));
|
||||
|
||||
expect(result).toEqual({ id: '2', status: 'inactive' });
|
||||
});
|
||||
|
||||
it('should return undefined when no match exists', () => {
|
||||
const items = [{ id: '1', name: 'Alice' }];
|
||||
|
||||
const result = items.find(findByProperty('name', 'Bob'));
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle null match value', () => {
|
||||
const items = [
|
||||
{ id: '1', name: null as string | null },
|
||||
{ id: '2', name: 'Bob' },
|
||||
];
|
||||
|
||||
const result = items.find(findByProperty('name', null));
|
||||
|
||||
expect(result).toEqual({ id: '1', name: null });
|
||||
});
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { getContiguousIncrementalValues } from '@/utils/array/getContiguousIncrementalValues';
|
||||
|
||||
describe('getContiguousIncrementalValues', () => {
|
||||
it('should return incremental values starting from 0 by default', () => {
|
||||
expect(getContiguousIncrementalValues(5)).toEqual([0, 1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('should return incremental values starting from a custom value', () => {
|
||||
expect(getContiguousIncrementalValues(3, 10)).toEqual([10, 11, 12]);
|
||||
});
|
||||
|
||||
it('should return an empty array for 0 values', () => {
|
||||
expect(getContiguousIncrementalValues(0)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle negative starting values', () => {
|
||||
expect(getContiguousIncrementalValues(3, -2)).toEqual([-2, -1, 0]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { mapById } from '@/utils/array/mapById';
|
||||
|
||||
describe('mapById', () => {
|
||||
it('should extract the id from an item', () => {
|
||||
expect(mapById({ id: 'abc-123' })).toBe('abc-123');
|
||||
});
|
||||
|
||||
it('should work with Array.map', () => {
|
||||
const items = [{ id: '1' }, { id: '2' }, { id: '3' }];
|
||||
|
||||
expect(items.map(mapById)).toEqual(['1', '2', '3']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { mapByProperty } from '@/utils/array/mapByProperty';
|
||||
|
||||
describe('mapByProperty', () => {
|
||||
it('should extract the specified property from an item', () => {
|
||||
const items = [
|
||||
{ id: '1', name: 'Alice' },
|
||||
{ id: '2', name: 'Bob' },
|
||||
];
|
||||
|
||||
expect(items.map(mapByProperty('name'))).toEqual(['Alice', 'Bob']);
|
||||
});
|
||||
|
||||
it('should extract the id property', () => {
|
||||
const items = [{ id: 'a' }, { id: 'b' }];
|
||||
|
||||
expect(items.map(mapByProperty('id'))).toEqual(['a', 'b']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { sumByProperty } from '@/utils/array/sumByProperty';
|
||||
|
||||
describe('sumByProperty', () => {
|
||||
it('should sum numeric property values', () => {
|
||||
const items = [
|
||||
{ id: '1', amount: 10 },
|
||||
{ id: '2', amount: 20 },
|
||||
{ id: '3', amount: 30 },
|
||||
];
|
||||
|
||||
expect(items.reduce(sumByProperty('amount'), 0)).toBe(60);
|
||||
});
|
||||
|
||||
it('should skip non-numeric values', () => {
|
||||
const items = [
|
||||
{ id: '1', amount: 10 },
|
||||
{ id: '2', amount: 'not-a-number' as unknown as number },
|
||||
{ id: '3', amount: 30 },
|
||||
];
|
||||
|
||||
expect(items.reduce(sumByProperty('amount'), 0)).toBe(40);
|
||||
});
|
||||
|
||||
it('should handle an empty array', () => {
|
||||
const items: { id: string; amount: number }[] = [];
|
||||
|
||||
expect(items.reduce(sumByProperty('amount'), 0)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Temporal } from 'temporal-polyfill';
|
||||
|
||||
import { parseToPlainDateOrThrow } from '@/utils/date/parseToPlainDateOrThrow';
|
||||
import { turnJSDateToPlainDate } from '@/utils/date/turnJSDateToPlainDate';
|
||||
import { turnPlainDateIntoUserTimeZoneInstantString } from '@/utils/date/turnPlainDateIntoUserTimeZoneInstantString';
|
||||
import { turnPlainDateToShiftedDateInSystemTimeZone } from '@/utils/date/turnPlainDateToShiftedDateInSystemTimeZone';
|
||||
|
||||
describe('parseToPlainDateOrThrow', () => {
|
||||
it('should parse an ISO instant string', () => {
|
||||
const result = parseToPlainDateOrThrow('2024-03-15T10:00:00Z');
|
||||
|
||||
expect(result.year).toBe(2024);
|
||||
expect(result.month).toBe(3);
|
||||
expect(result.day).toBe(15);
|
||||
});
|
||||
|
||||
it('should parse a plain date string', () => {
|
||||
const result = parseToPlainDateOrThrow('2024-03-15');
|
||||
|
||||
expect(result.year).toBe(2024);
|
||||
expect(result.month).toBe(3);
|
||||
expect(result.day).toBe(15);
|
||||
});
|
||||
|
||||
it('should throw for an invalid date string', () => {
|
||||
expect(() => parseToPlainDateOrThrow('not-a-date')).toThrow(
|
||||
'Cannot parse date string as PlainDate',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('turnJSDateToPlainDate', () => {
|
||||
it('should convert a JS Date to a PlainDate', () => {
|
||||
const jsDate = new Date(2024, 2, 15); // March 15, 2024
|
||||
|
||||
const result = turnJSDateToPlainDate(jsDate);
|
||||
|
||||
expect(result.year).toBe(2024);
|
||||
expect(result.month).toBe(3);
|
||||
expect(result.day).toBe(15);
|
||||
});
|
||||
});
|
||||
|
||||
describe('turnPlainDateIntoUserTimeZoneInstantString', () => {
|
||||
it('should convert a PlainDate to an instant string in the given timezone', () => {
|
||||
const plainDate = Temporal.PlainDate.from('2024-03-15');
|
||||
const result = turnPlainDateIntoUserTimeZoneInstantString(
|
||||
plainDate,
|
||||
'UTC',
|
||||
);
|
||||
|
||||
expect(result).toBe('2024-03-15T00:00:00Z');
|
||||
});
|
||||
});
|
||||
|
||||
describe('turnPlainDateToShiftedDateInSystemTimeZone', () => {
|
||||
it('should return a JS Date for the given PlainDate', () => {
|
||||
const plainDate = Temporal.PlainDate.from('2024-03-15');
|
||||
|
||||
const result = turnPlainDateToShiftedDateInSystemTimeZone(plainDate);
|
||||
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
expect(result.getFullYear()).toBe(2024);
|
||||
expect(result.getMonth()).toBe(2); // 0-indexed
|
||||
expect(result.getDate()).toBe(15);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Temporal } from 'temporal-polyfill';
|
||||
|
||||
import { isPlainDateAfter } from '@/utils/date/isPlainDateAfter';
|
||||
import { isPlainDateBefore } from '@/utils/date/isPlainDateBefore';
|
||||
import { isPlainDateBeforeOrEqual } from '@/utils/date/isPlainDateBeforeOrEqual';
|
||||
import { isPlainDateInSameMonth } from '@/utils/date/isPlainDateInSameMonth';
|
||||
import { isPlainDateInWeekend } from '@/utils/date/isPlainDateInWeekend';
|
||||
import { isSamePlainDate } from '@/utils/date/isSamePlainDate';
|
||||
|
||||
describe('isPlainDateAfter', () => {
|
||||
it('should return true when first date is after second', () => {
|
||||
const a = Temporal.PlainDate.from('2024-03-15');
|
||||
const b = Temporal.PlainDate.from('2024-03-10');
|
||||
|
||||
expect(isPlainDateAfter(a, b)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when first date is before second', () => {
|
||||
const a = Temporal.PlainDate.from('2024-03-10');
|
||||
const b = Temporal.PlainDate.from('2024-03-15');
|
||||
|
||||
expect(isPlainDateAfter(a, b)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when dates are equal', () => {
|
||||
const a = Temporal.PlainDate.from('2024-03-15');
|
||||
const b = Temporal.PlainDate.from('2024-03-15');
|
||||
|
||||
expect(isPlainDateAfter(a, b)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPlainDateBefore', () => {
|
||||
it('should return true when first date is before second', () => {
|
||||
const a = Temporal.PlainDate.from('2024-03-10');
|
||||
const b = Temporal.PlainDate.from('2024-03-15');
|
||||
|
||||
expect(isPlainDateBefore(a, b)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when first date is after second', () => {
|
||||
const a = Temporal.PlainDate.from('2024-03-15');
|
||||
const b = Temporal.PlainDate.from('2024-03-10');
|
||||
|
||||
expect(isPlainDateBefore(a, b)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPlainDateBeforeOrEqual', () => {
|
||||
it('should return true when first date is before second', () => {
|
||||
const a = Temporal.PlainDate.from('2024-03-10');
|
||||
const b = Temporal.PlainDate.from('2024-03-15');
|
||||
|
||||
expect(isPlainDateBeforeOrEqual(a, b)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when dates are equal', () => {
|
||||
const a = Temporal.PlainDate.from('2024-03-15');
|
||||
const b = Temporal.PlainDate.from('2024-03-15');
|
||||
|
||||
expect(isPlainDateBeforeOrEqual(a, b)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when first date is after second', () => {
|
||||
const a = Temporal.PlainDate.from('2024-03-20');
|
||||
const b = Temporal.PlainDate.from('2024-03-15');
|
||||
|
||||
expect(isPlainDateBeforeOrEqual(a, b)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPlainDateInSameMonth', () => {
|
||||
it('should return true for dates in same month and year', () => {
|
||||
const a = Temporal.PlainDate.from('2024-03-01');
|
||||
const b = Temporal.PlainDate.from('2024-03-31');
|
||||
|
||||
expect(isPlainDateInSameMonth(a, b)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for dates in different months', () => {
|
||||
const a = Temporal.PlainDate.from('2024-03-15');
|
||||
const b = Temporal.PlainDate.from('2024-04-15');
|
||||
|
||||
expect(isPlainDateInSameMonth(a, b)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for same month but different year', () => {
|
||||
const a = Temporal.PlainDate.from('2024-03-15');
|
||||
const b = Temporal.PlainDate.from('2025-03-15');
|
||||
|
||||
expect(isPlainDateInSameMonth(a, b)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPlainDateInWeekend', () => {
|
||||
it('should return true for Saturday', () => {
|
||||
// 2024-03-16 is a Saturday
|
||||
const saturday = Temporal.PlainDate.from('2024-03-16');
|
||||
|
||||
expect(isPlainDateInWeekend(saturday)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for Sunday', () => {
|
||||
// 2024-03-17 is a Sunday
|
||||
const sunday = Temporal.PlainDate.from('2024-03-17');
|
||||
|
||||
expect(isPlainDateInWeekend(sunday)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for a weekday', () => {
|
||||
// 2024-03-18 is a Monday
|
||||
const monday = Temporal.PlainDate.from('2024-03-18');
|
||||
|
||||
expect(isPlainDateInWeekend(monday)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSamePlainDate', () => {
|
||||
it('should return true for equal dates', () => {
|
||||
const a = Temporal.PlainDate.from('2024-03-15');
|
||||
const b = Temporal.PlainDate.from('2024-03-15');
|
||||
|
||||
expect(isSamePlainDate(a, b)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for different dates', () => {
|
||||
const a = Temporal.PlainDate.from('2024-03-15');
|
||||
const b = Temporal.PlainDate.from('2024-03-16');
|
||||
|
||||
expect(isSamePlainDate(a, b)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { CustomError } from '@/utils/errors/CustomError';
|
||||
|
||||
describe('CustomError', () => {
|
||||
it('should create an error with a message', () => {
|
||||
const error = new CustomError('Something went wrong');
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error.message).toBe('Something went wrong');
|
||||
expect(error.code).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create an error with a message and code', () => {
|
||||
const error = new CustomError('Not found', 'NOT_FOUND');
|
||||
|
||||
expect(error.message).toBe('Not found');
|
||||
expect(error.code).toBe('NOT_FOUND');
|
||||
});
|
||||
});
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { computeMorphRelationFieldName } from '@/utils/fieldMetadata/compute-morph-relation-field-name';
|
||||
|
||||
describe('computeMorphRelationFieldName', () => {
|
||||
it('should return singular-based name for MANY_TO_ONE', () => {
|
||||
const result = computeMorphRelationFieldName({
|
||||
fieldName: 'assigned',
|
||||
relationType: 'MANY_TO_ONE' as any,
|
||||
targetObjectMetadataNameSingular: 'person',
|
||||
targetObjectMetadataNamePlural: 'people',
|
||||
});
|
||||
|
||||
expect(result).toBe('assignedPerson');
|
||||
});
|
||||
|
||||
it('should return plural-based name for ONE_TO_MANY', () => {
|
||||
const result = computeMorphRelationFieldName({
|
||||
fieldName: 'assigned',
|
||||
relationType: 'ONE_TO_MANY' as any,
|
||||
targetObjectMetadataNameSingular: 'person',
|
||||
targetObjectMetadataNamePlural: 'people',
|
||||
});
|
||||
|
||||
expect(result).toBe('assignedPeople');
|
||||
});
|
||||
|
||||
it('should throw for invalid relation type', () => {
|
||||
expect(() =>
|
||||
computeMorphRelationFieldName({
|
||||
fieldName: 'assigned',
|
||||
relationType: 'INVALID' as any,
|
||||
targetObjectMetadataNameSingular: 'person',
|
||||
targetObjectMetadataNamePlural: 'people',
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { FieldMetadataType } from '@/types';
|
||||
import { isFieldMetadataDateKind } from '@/utils/fieldMetadata/isFieldMetadataDateKind';
|
||||
|
||||
describe('isFieldMetadataDateKind', () => {
|
||||
it('should return true for DATE type', () => {
|
||||
expect(isFieldMetadataDateKind(FieldMetadataType.DATE)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for DATE_TIME type', () => {
|
||||
expect(isFieldMetadataDateKind(FieldMetadataType.DATE_TIME)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for TEXT type', () => {
|
||||
expect(isFieldMetadataDateKind(FieldMetadataType.TEXT)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for undefined', () => {
|
||||
expect(isFieldMetadataDateKind(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { FieldMetadataType, ViewFilterOperand } from '@/types';
|
||||
import { computeEmptyGqlOperationFilterForEmails } from '@/utils/filter/computeEmptyGqlOperationFilterForEmails';
|
||||
|
||||
describe('computeEmptyGqlOperationFilterForEmails', () => {
|
||||
const baseFilter = {
|
||||
id: '1',
|
||||
fieldMetadataId: 'f1',
|
||||
value: '',
|
||||
type: 'EMAILS' as const,
|
||||
operand: ViewFilterOperand.IS_EMPTY,
|
||||
};
|
||||
|
||||
const field = { name: 'email', type: FieldMetadataType.EMAILS };
|
||||
|
||||
it('should compute filter for primaryEmail subfield', () => {
|
||||
const result = computeEmptyGqlOperationFilterForEmails({
|
||||
recordFilter: { ...baseFilter, subFieldName: 'primaryEmail' },
|
||||
correspondingFieldMetadataItem: field,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
expect((result as any).or).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should compute filter for additionalEmails subfield', () => {
|
||||
const result = computeEmptyGqlOperationFilterForEmails({
|
||||
recordFilter: { ...baseFilter, subFieldName: 'additionalEmails' },
|
||||
correspondingFieldMetadataItem: field,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should throw for unknown subfield', () => {
|
||||
expect(() =>
|
||||
computeEmptyGqlOperationFilterForEmails({
|
||||
recordFilter: { ...baseFilter, subFieldName: 'unknown' as any },
|
||||
correspondingFieldMetadataItem: field,
|
||||
}),
|
||||
).toThrow('Unknown subfield name');
|
||||
});
|
||||
|
||||
it('should compute combined filter when no subfield is specified', () => {
|
||||
const result = computeEmptyGqlOperationFilterForEmails({
|
||||
recordFilter: { ...baseFilter, subFieldName: undefined },
|
||||
correspondingFieldMetadataItem: field,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
expect((result as any).and).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { ViewFilterOperand } from '@/types';
|
||||
import { computeEmptyGqlOperationFilterForLinks } from '@/utils/filter/computeEmptyGqlOperationFilterForLinks';
|
||||
|
||||
describe('computeEmptyGqlOperationFilterForLinks', () => {
|
||||
const baseFilter = {
|
||||
id: '1',
|
||||
fieldMetadataId: 'f1',
|
||||
value: '',
|
||||
type: 'LINKS' as const,
|
||||
operand: ViewFilterOperand.IS_EMPTY,
|
||||
};
|
||||
|
||||
const field = { name: 'links' };
|
||||
|
||||
it('should compute filter for primaryLinkLabel subfield', () => {
|
||||
const result = computeEmptyGqlOperationFilterForLinks({
|
||||
recordFilter: { ...baseFilter, subFieldName: 'primaryLinkLabel' },
|
||||
correspondingFieldMetadataItem: field,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should compute filter for primaryLinkUrl subfield', () => {
|
||||
const result = computeEmptyGqlOperationFilterForLinks({
|
||||
recordFilter: { ...baseFilter, subFieldName: 'primaryLinkUrl' },
|
||||
correspondingFieldMetadataItem: field,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should compute filter for secondaryLinks subfield', () => {
|
||||
const result = computeEmptyGqlOperationFilterForLinks({
|
||||
recordFilter: { ...baseFilter, subFieldName: 'secondaryLinks' },
|
||||
correspondingFieldMetadataItem: field,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should throw for unknown subfield', () => {
|
||||
expect(() =>
|
||||
computeEmptyGqlOperationFilterForLinks({
|
||||
recordFilter: { ...baseFilter, subFieldName: 'unknown' as any },
|
||||
correspondingFieldMetadataItem: field,
|
||||
}),
|
||||
).toThrow('Unknown subfield name');
|
||||
});
|
||||
|
||||
it('should compute combined filter when no subfield is specified', () => {
|
||||
const result = computeEmptyGqlOperationFilterForLinks({
|
||||
recordFilter: { ...baseFilter, subFieldName: undefined },
|
||||
correspondingFieldMetadataItem: field,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
expect((result as any).and).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RecordFilterGroupLogicalOperator,
|
||||
ViewFilterOperand,
|
||||
} from '@/types';
|
||||
import { turnRecordFilterGroupsIntoGqlOperationFilter } from '@/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter';
|
||||
|
||||
describe('turnRecordFilterGroupsIntoGqlOperationFilter', () => {
|
||||
const fields = [
|
||||
{
|
||||
id: 'f1',
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Name',
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
name: 'age',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Age',
|
||||
},
|
||||
];
|
||||
|
||||
it('should return undefined when group is not found', () => {
|
||||
const result = turnRecordFilterGroupsIntoGqlOperationFilter({
|
||||
filterValueDependencies: {},
|
||||
filters: [],
|
||||
fields,
|
||||
recordFilterGroups: [],
|
||||
currentRecordFilterGroupId: 'nonexistent',
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return AND filter for AND logical operator', () => {
|
||||
const result = turnRecordFilterGroupsIntoGqlOperationFilter({
|
||||
filterValueDependencies: {},
|
||||
filters: [
|
||||
{
|
||||
id: 'filter1',
|
||||
fieldMetadataId: 'f1',
|
||||
value: 'test',
|
||||
type: 'TEXT',
|
||||
operand: ViewFilterOperand.CONTAINS,
|
||||
recordFilterGroupId: 'group1',
|
||||
},
|
||||
],
|
||||
fields,
|
||||
recordFilterGroups: [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: RecordFilterGroupLogicalOperator.AND,
|
||||
},
|
||||
],
|
||||
currentRecordFilterGroupId: 'group1',
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
});
|
||||
|
||||
it('should return OR filter for OR logical operator', () => {
|
||||
const result = turnRecordFilterGroupsIntoGqlOperationFilter({
|
||||
filterValueDependencies: {},
|
||||
filters: [
|
||||
{
|
||||
id: 'filter1',
|
||||
fieldMetadataId: 'f1',
|
||||
value: 'test',
|
||||
type: 'TEXT',
|
||||
operand: ViewFilterOperand.CONTAINS,
|
||||
recordFilterGroupId: 'group1',
|
||||
},
|
||||
],
|
||||
fields,
|
||||
recordFilterGroups: [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: RecordFilterGroupLogicalOperator.OR,
|
||||
},
|
||||
],
|
||||
currentRecordFilterGroupId: 'group1',
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should handle nested groups', () => {
|
||||
const result = turnRecordFilterGroupsIntoGqlOperationFilter({
|
||||
filterValueDependencies: {},
|
||||
filters: [
|
||||
{
|
||||
id: 'filter1',
|
||||
fieldMetadataId: 'f1',
|
||||
value: 'test',
|
||||
type: 'TEXT',
|
||||
operand: ViewFilterOperand.CONTAINS,
|
||||
recordFilterGroupId: 'subgroup1',
|
||||
},
|
||||
],
|
||||
fields,
|
||||
recordFilterGroups: [
|
||||
{
|
||||
id: 'group1',
|
||||
logicalOperator: RecordFilterGroupLogicalOperator.AND,
|
||||
},
|
||||
{
|
||||
id: 'subgroup1',
|
||||
parentRecordFilterGroupId: 'group1',
|
||||
logicalOperator: RecordFilterGroupLogicalOperator.OR,
|
||||
},
|
||||
],
|
||||
currentRecordFilterGroupId: 'group1',
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
const andFilters = (result as any).and;
|
||||
|
||||
expect(andFilters.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
+826
@@ -0,0 +1,826 @@
|
||||
import {
|
||||
FieldMetadataType,
|
||||
ViewFilterOperand as RecordFilterOperand,
|
||||
} from '@/types';
|
||||
import { turnRecordFilterIntoRecordGqlOperationFilter } from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter';
|
||||
import { type RecordFilter } from '@/utils';
|
||||
|
||||
const fields = [
|
||||
{ id: 'f-text', name: 'name', type: FieldMetadataType.TEXT, label: 'Name' },
|
||||
{
|
||||
id: 'f-number',
|
||||
name: 'amount',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Amount',
|
||||
},
|
||||
{
|
||||
id: 'f-date',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE,
|
||||
label: 'Created At',
|
||||
},
|
||||
{
|
||||
id: 'f-datetime',
|
||||
name: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
label: 'Updated At',
|
||||
},
|
||||
{
|
||||
id: 'f-select',
|
||||
name: 'status',
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Status',
|
||||
},
|
||||
{
|
||||
id: 'f-multiselect',
|
||||
name: 'tags',
|
||||
type: FieldMetadataType.MULTI_SELECT,
|
||||
label: 'Tags',
|
||||
},
|
||||
{
|
||||
id: 'f-rating',
|
||||
name: 'rating',
|
||||
type: FieldMetadataType.RATING,
|
||||
label: 'Rating',
|
||||
},
|
||||
{
|
||||
id: 'f-relation',
|
||||
name: 'company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
label: 'Company',
|
||||
},
|
||||
{
|
||||
id: 'f-bool',
|
||||
name: 'isActive',
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: 'Is Active',
|
||||
},
|
||||
{
|
||||
id: 'f-rawjson',
|
||||
name: 'metadata',
|
||||
type: FieldMetadataType.RAW_JSON,
|
||||
label: 'Metadata',
|
||||
},
|
||||
{
|
||||
id: 'f-files',
|
||||
name: 'attachments',
|
||||
type: FieldMetadataType.FILES,
|
||||
label: 'Attachments',
|
||||
},
|
||||
{
|
||||
id: 'f-tsvector',
|
||||
name: 'search',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
label: 'Search',
|
||||
},
|
||||
{
|
||||
id: 'f-currency',
|
||||
name: 'revenue',
|
||||
type: FieldMetadataType.CURRENCY,
|
||||
label: 'Revenue',
|
||||
},
|
||||
{
|
||||
id: 'f-fullname',
|
||||
name: 'fullName',
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
label: 'Full Name',
|
||||
},
|
||||
{
|
||||
id: 'f-address',
|
||||
name: 'location',
|
||||
type: FieldMetadataType.ADDRESS,
|
||||
label: 'Location',
|
||||
},
|
||||
{
|
||||
id: 'f-actor',
|
||||
name: 'actor',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
label: 'Actor',
|
||||
},
|
||||
{
|
||||
id: 'f-phones',
|
||||
name: 'phone',
|
||||
type: FieldMetadataType.PHONES,
|
||||
label: 'Phone',
|
||||
},
|
||||
{
|
||||
id: 'f-emails',
|
||||
name: 'email',
|
||||
type: FieldMetadataType.EMAILS,
|
||||
label: 'Email',
|
||||
},
|
||||
{
|
||||
id: 'f-links',
|
||||
name: 'website',
|
||||
type: FieldMetadataType.LINKS,
|
||||
label: 'Website',
|
||||
},
|
||||
{
|
||||
id: 'f-array',
|
||||
name: 'items',
|
||||
type: FieldMetadataType.ARRAY,
|
||||
label: 'Items',
|
||||
},
|
||||
{
|
||||
id: 'f-uuid',
|
||||
name: 'recordId',
|
||||
type: FieldMetadataType.UUID,
|
||||
label: 'Record ID',
|
||||
},
|
||||
];
|
||||
|
||||
const filterValueDependencies = { timeZone: 'UTC' };
|
||||
|
||||
const makeFilter = (
|
||||
fieldMetadataId: string,
|
||||
operand: RecordFilterOperand,
|
||||
value: string,
|
||||
type: string = 'TEXT',
|
||||
subFieldName?: string,
|
||||
) =>
|
||||
({
|
||||
id: 'test-filter',
|
||||
fieldMetadataId,
|
||||
value,
|
||||
type: type as any,
|
||||
operand,
|
||||
subFieldName,
|
||||
}) as RecordFilter;
|
||||
|
||||
describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
|
||||
it('should return undefined when field metadata is not found', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'nonexistent',
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'x',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when value is empty for non-emptiness operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter('f-text', RecordFilterOperand.CONTAINS, ''),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('TEXT filter', () => {
|
||||
it('should handle CONTAINS operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-text',
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'test',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ name: { ilike: '%test%' } });
|
||||
});
|
||||
|
||||
it('should handle DOES_NOT_CONTAIN operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-text',
|
||||
RecordFilterOperand.DOES_NOT_CONTAIN,
|
||||
'test',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ not: { name: { ilike: '%test%' } } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('NUMBER filter', () => {
|
||||
it('should handle IS operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter('f-number', RecordFilterOperand.IS, '42'),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ amount: { eq: 42 } });
|
||||
});
|
||||
|
||||
it('should handle IS_NOT operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter('f-number', RecordFilterOperand.IS_NOT, '42'),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ not: { amount: { eq: 42 } } });
|
||||
});
|
||||
|
||||
it('should handle GREATER_THAN_OR_EQUAL operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-number',
|
||||
RecordFilterOperand.GREATER_THAN_OR_EQUAL,
|
||||
'10',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ amount: { gte: 10 } });
|
||||
});
|
||||
|
||||
it('should handle LESS_THAN_OR_EQUAL operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-number',
|
||||
RecordFilterOperand.LESS_THAN_OR_EQUAL,
|
||||
'100',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ amount: { lte: 100 } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('DATE filter', () => {
|
||||
it('should handle IS_AFTER operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-date',
|
||||
RecordFilterOperand.IS_AFTER,
|
||||
'2024-03-15',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ createdAt: { gte: '2024-03-15' } });
|
||||
});
|
||||
|
||||
it('should handle IS_BEFORE operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-date',
|
||||
RecordFilterOperand.IS_BEFORE,
|
||||
'2024-03-15',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ createdAt: { lt: '2024-03-15' } });
|
||||
});
|
||||
|
||||
it('should handle IS operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-date',
|
||||
RecordFilterOperand.IS,
|
||||
'2024-03-15',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ createdAt: { eq: '2024-03-15' } });
|
||||
});
|
||||
|
||||
it('should handle IS_IN_PAST operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter('f-date', RecordFilterOperand.IS_IN_PAST, ''),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('createdAt.lt');
|
||||
});
|
||||
|
||||
it('should handle IS_IN_FUTURE operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-date',
|
||||
RecordFilterOperand.IS_IN_FUTURE,
|
||||
'',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('createdAt.gte');
|
||||
});
|
||||
|
||||
it('should handle IS_TODAY operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter('f-date', RecordFilterOperand.IS_TODAY, ''),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('createdAt.eq');
|
||||
});
|
||||
|
||||
it('should handle IS_RELATIVE operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-date',
|
||||
RecordFilterOperand.IS_RELATIVE,
|
||||
'PAST_7_DAY',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DATE_TIME filter', () => {
|
||||
it('should handle IS_AFTER operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-datetime',
|
||||
RecordFilterOperand.IS_AFTER,
|
||||
'2024-03-15T10:00:00Z',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('updatedAt.gte');
|
||||
});
|
||||
|
||||
it('should handle IS_BEFORE operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-datetime',
|
||||
RecordFilterOperand.IS_BEFORE,
|
||||
'2024-03-15T10:00:00Z',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('updatedAt.lt');
|
||||
});
|
||||
|
||||
it('should handle IS operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-datetime',
|
||||
RecordFilterOperand.IS,
|
||||
'2024-03-15T10:00:00Z',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
});
|
||||
|
||||
it('should handle IS_IN_PAST operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-datetime',
|
||||
RecordFilterOperand.IS_IN_PAST,
|
||||
'',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('updatedAt.lt');
|
||||
});
|
||||
|
||||
it('should handle IS_IN_FUTURE operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-datetime',
|
||||
RecordFilterOperand.IS_IN_FUTURE,
|
||||
'',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('updatedAt.gt');
|
||||
});
|
||||
|
||||
it('should handle IS_TODAY operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-datetime',
|
||||
RecordFilterOperand.IS_TODAY,
|
||||
'',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
});
|
||||
|
||||
it('should handle IS_RELATIVE operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-datetime',
|
||||
RecordFilterOperand.IS_RELATIVE,
|
||||
`PAST_7_DAY;;UTC;;`,
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RATING filter', () => {
|
||||
it('should handle IS operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter('f-rating', RecordFilterOperand.IS, '3'),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('rating.eq');
|
||||
});
|
||||
|
||||
it('should handle GREATER_THAN_OR_EQUAL operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-rating',
|
||||
RecordFilterOperand.GREATER_THAN_OR_EQUAL,
|
||||
'3',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('rating.in');
|
||||
});
|
||||
|
||||
it('should handle LESS_THAN_OR_EQUAL operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-rating',
|
||||
RecordFilterOperand.LESS_THAN_OR_EQUAL,
|
||||
'3',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('rating.in');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BOOLEAN filter', () => {
|
||||
it('should handle IS operand with true', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter('f-bool', RecordFilterOperand.IS, 'true'),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ isActive: { eq: true } });
|
||||
});
|
||||
|
||||
it('should handle IS operand with false', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter('f-bool', RecordFilterOperand.IS, 'false'),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ isActive: { eq: false } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('SELECT filter', () => {
|
||||
it('should handle IS operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-select',
|
||||
RecordFilterOperand.IS,
|
||||
'["ACTIVE","PENDING"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('status.in');
|
||||
});
|
||||
|
||||
it('should handle IS_NOT operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-select',
|
||||
RecordFilterOperand.IS_NOT,
|
||||
'["ACTIVE"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('not');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MULTI_SELECT filter', () => {
|
||||
it('should handle CONTAINS operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-multiselect',
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'["TAG1","TAG2"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('tags.containsAny');
|
||||
});
|
||||
|
||||
it('should handle DOES_NOT_CONTAIN operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-multiselect',
|
||||
RecordFilterOperand.DOES_NOT_CONTAIN,
|
||||
'["TAG1"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RELATION filter', () => {
|
||||
it('should handle IS operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-relation',
|
||||
RecordFilterOperand.IS,
|
||||
'["550e8400-e29b-41d4-a716-446655440000"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('companyId.in');
|
||||
});
|
||||
|
||||
it('should handle IS_NOT operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-relation',
|
||||
RecordFilterOperand.IS_NOT,
|
||||
'["550e8400-e29b-41d4-a716-446655440000"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RAW_JSON filter', () => {
|
||||
it('should handle CONTAINS operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-rawjson',
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'test',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ metadata: { like: '%test%' } });
|
||||
});
|
||||
|
||||
it('should handle DOES_NOT_CONTAIN operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-rawjson',
|
||||
RecordFilterOperand.DOES_NOT_CONTAIN,
|
||||
'test',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ not: { metadata: { like: '%test%' } } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('FILES filter', () => {
|
||||
it('should handle CONTAINS operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-files',
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'doc',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ attachments: { like: '%doc%' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('TS_VECTOR filter', () => {
|
||||
it('should handle VECTOR_SEARCH operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-tsvector',
|
||||
RecordFilterOperand.VECTOR_SEARCH,
|
||||
'hello world',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ search: { search: 'hello world' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('CURRENCY filter', () => {
|
||||
it('should handle GREATER_THAN_OR_EQUAL on amountMicros', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-currency',
|
||||
RecordFilterOperand.GREATER_THAN_OR_EQUAL,
|
||||
'1000',
|
||||
'CURRENCY',
|
||||
'amountMicros',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('revenue');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FULL_NAME filter', () => {
|
||||
it('should handle CONTAINS operand without subfield', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-fullname',
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'John',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ADDRESS filter', () => {
|
||||
it('should handle CONTAINS operand without subfield', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-address',
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'Paris',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ACTOR filter', () => {
|
||||
it('should handle CONTAINS operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-actor',
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'Admin',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PHONES filter', () => {
|
||||
it('should handle CONTAINS on primaryPhoneNumber', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-phones',
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'555',
|
||||
'PHONES',
|
||||
'primaryPhoneNumber',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('EMAILS filter', () => {
|
||||
it('should handle CONTAINS on primaryEmail', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-emails',
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'test@example.com',
|
||||
'EMAILS',
|
||||
'primaryEmail',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('LINKS filter', () => {
|
||||
it('should handle CONTAINS on primaryLinkUrl', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-links',
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'example.com',
|
||||
'LINKS',
|
||||
'primaryLinkUrl',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ARRAY filter', () => {
|
||||
it('should handle CONTAINS operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-array',
|
||||
RecordFilterOperand.CONTAINS,
|
||||
'["item1"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle DOES_NOT_CONTAIN operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-array',
|
||||
RecordFilterOperand.DOES_NOT_CONTAIN,
|
||||
'["item1"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('not');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UUID filter', () => {
|
||||
it('should handle IS operand', () => {
|
||||
const result = turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: makeFilter(
|
||||
'f-uuid',
|
||||
RecordFilterOperand.IS,
|
||||
'["550e8400-e29b-41d4-a716-446655440000"]',
|
||||
),
|
||||
fieldMetadataItems: fields,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('recordId.in');
|
||||
});
|
||||
});
|
||||
});
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { addUnitToDateTime } from '@/utils/filter/dates/utils/addUnitToDateTime';
|
||||
|
||||
describe('addUnitToDateTime', () => {
|
||||
const baseDate = new Date('2024-03-15T12:00:00Z');
|
||||
|
||||
it('should add seconds', () => {
|
||||
const result = addUnitToDateTime(baseDate, 30, 'SECOND');
|
||||
|
||||
expect(result.getTime() - baseDate.getTime()).toBe(30_000);
|
||||
});
|
||||
|
||||
it('should add minutes', () => {
|
||||
const result = addUnitToDateTime(baseDate, 5, 'MINUTE');
|
||||
|
||||
expect(result.getTime() - baseDate.getTime()).toBe(5 * 60_000);
|
||||
});
|
||||
|
||||
it('should add hours', () => {
|
||||
const result = addUnitToDateTime(baseDate, 2, 'HOUR');
|
||||
|
||||
expect(result.getTime() - baseDate.getTime()).toBe(2 * 3_600_000);
|
||||
});
|
||||
|
||||
it('should add days', () => {
|
||||
const result = addUnitToDateTime(baseDate, 3, 'DAY');
|
||||
|
||||
expect(result.getDate()).toBe(18);
|
||||
});
|
||||
|
||||
it('should add weeks', () => {
|
||||
const result = addUnitToDateTime(baseDate, 1, 'WEEK');
|
||||
|
||||
expect(result.getDate()).toBe(22);
|
||||
});
|
||||
|
||||
it('should add months', () => {
|
||||
const result = addUnitToDateTime(baseDate, 2, 'MONTH');
|
||||
|
||||
expect(result.getMonth()).toBe(4); // May (0-indexed)
|
||||
});
|
||||
|
||||
it('should add years', () => {
|
||||
const result = addUnitToDateTime(baseDate, 1, 'YEAR');
|
||||
|
||||
expect(result.getFullYear()).toBe(2025);
|
||||
});
|
||||
});
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { Temporal } from 'temporal-polyfill';
|
||||
|
||||
import { addUnitToZonedDateTime } from '@/utils/filter/dates/utils/addUnitToZonedDateTime';
|
||||
|
||||
describe('addUnitToZonedDateTime', () => {
|
||||
const baseZdt = Temporal.ZonedDateTime.from(
|
||||
'2024-03-15T12:00:00[UTC]',
|
||||
);
|
||||
|
||||
it('should add days', () => {
|
||||
const result = addUnitToZonedDateTime(baseZdt, 'DAY', 3);
|
||||
|
||||
expect(result.day).toBe(18);
|
||||
});
|
||||
|
||||
it('should add weeks', () => {
|
||||
const result = addUnitToZonedDateTime(baseZdt, 'WEEK', 1);
|
||||
|
||||
expect(result.day).toBe(22);
|
||||
});
|
||||
|
||||
it('should add months', () => {
|
||||
const result = addUnitToZonedDateTime(baseZdt, 'MONTH', 2);
|
||||
|
||||
expect(result.month).toBe(5);
|
||||
});
|
||||
|
||||
it('should add quarters', () => {
|
||||
const result = addUnitToZonedDateTime(baseZdt, 'QUARTER', 1);
|
||||
|
||||
expect(result.month).toBe(6);
|
||||
});
|
||||
|
||||
it('should add years', () => {
|
||||
const result = addUnitToZonedDateTime(baseZdt, 'YEAR', 1);
|
||||
|
||||
expect(result.year).toBe(2025);
|
||||
});
|
||||
|
||||
it('should add seconds', () => {
|
||||
const result = addUnitToZonedDateTime(baseZdt, 'SECOND', 30);
|
||||
|
||||
expect(result.second).toBe(30);
|
||||
});
|
||||
|
||||
it('should add minutes', () => {
|
||||
const result = addUnitToZonedDateTime(baseZdt, 'MINUTE', 15);
|
||||
|
||||
expect(result.minute).toBe(15);
|
||||
});
|
||||
|
||||
it('should add hours', () => {
|
||||
const result = addUnitToZonedDateTime(baseZdt, 'HOUR', 3);
|
||||
|
||||
expect(result.hour).toBe(15);
|
||||
});
|
||||
});
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { CalendarStartDay } from '@/constants';
|
||||
import { FirstDayOfTheWeek } from '@/types';
|
||||
import { convertCalendarStartDayNonIsoNumberToFirstDayOfTheWeek } from '@/utils/filter/dates/utils/convertCalendarStartDayNonIsoNumberToFirstDayOfTheWeek';
|
||||
import { convertFirstDayOfTheWeekToCalendarStartDayNumber } from '@/utils/filter/dates/utils/convertFirstDayOfTheWeekToCalendarStartDayNumber';
|
||||
import { getFirstDayOfTheWeekAsANumberForDateFNS } from '@/utils/filter/dates/utils/getFirstDayOfTheWeekAsANumberForDateFNS';
|
||||
|
||||
describe('convertCalendarStartDayNonIsoNumberToFirstDayOfTheWeek', () => {
|
||||
it('should convert MONDAY', () => {
|
||||
expect(
|
||||
convertCalendarStartDayNonIsoNumberToFirstDayOfTheWeek(
|
||||
CalendarStartDay.MONDAY,
|
||||
FirstDayOfTheWeek.SUNDAY,
|
||||
),
|
||||
).toBe(FirstDayOfTheWeek.MONDAY);
|
||||
});
|
||||
|
||||
it('should convert SATURDAY', () => {
|
||||
expect(
|
||||
convertCalendarStartDayNonIsoNumberToFirstDayOfTheWeek(
|
||||
CalendarStartDay.SATURDAY,
|
||||
FirstDayOfTheWeek.SUNDAY,
|
||||
),
|
||||
).toBe(FirstDayOfTheWeek.SATURDAY);
|
||||
});
|
||||
|
||||
it('should convert SUNDAY', () => {
|
||||
expect(
|
||||
convertCalendarStartDayNonIsoNumberToFirstDayOfTheWeek(
|
||||
CalendarStartDay.SUNDAY,
|
||||
FirstDayOfTheWeek.MONDAY,
|
||||
),
|
||||
).toBe(FirstDayOfTheWeek.SUNDAY);
|
||||
});
|
||||
|
||||
it('should use system default for SYSTEM', () => {
|
||||
expect(
|
||||
convertCalendarStartDayNonIsoNumberToFirstDayOfTheWeek(
|
||||
CalendarStartDay.SYSTEM,
|
||||
FirstDayOfTheWeek.SATURDAY,
|
||||
),
|
||||
).toBe(FirstDayOfTheWeek.SATURDAY);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertFirstDayOfTheWeekToCalendarStartDayNumber', () => {
|
||||
it('should convert MONDAY', () => {
|
||||
expect(
|
||||
convertFirstDayOfTheWeekToCalendarStartDayNumber(
|
||||
FirstDayOfTheWeek.MONDAY,
|
||||
),
|
||||
).toBe(CalendarStartDay.MONDAY);
|
||||
});
|
||||
|
||||
it('should convert SATURDAY', () => {
|
||||
expect(
|
||||
convertFirstDayOfTheWeekToCalendarStartDayNumber(
|
||||
FirstDayOfTheWeek.SATURDAY,
|
||||
),
|
||||
).toBe(CalendarStartDay.SATURDAY);
|
||||
});
|
||||
|
||||
it('should convert SUNDAY', () => {
|
||||
expect(
|
||||
convertFirstDayOfTheWeekToCalendarStartDayNumber(
|
||||
FirstDayOfTheWeek.SUNDAY,
|
||||
),
|
||||
).toBe(CalendarStartDay.SUNDAY);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFirstDayOfTheWeekAsANumberForDateFNS', () => {
|
||||
it('should return 1 for MONDAY', () => {
|
||||
expect(
|
||||
getFirstDayOfTheWeekAsANumberForDateFNS(FirstDayOfTheWeek.MONDAY),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should return 6 for SATURDAY', () => {
|
||||
expect(
|
||||
getFirstDayOfTheWeekAsANumberForDateFNS(FirstDayOfTheWeek.SATURDAY),
|
||||
).toBe(6);
|
||||
});
|
||||
|
||||
it('should return 0 for SUNDAY', () => {
|
||||
expect(
|
||||
getFirstDayOfTheWeekAsANumberForDateFNS(FirstDayOfTheWeek.SUNDAY),
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { relativeDateFilterStringifiedSchema } from '@/utils/filter/dates/utils/relativeDateFilterStringifiedSchema';
|
||||
|
||||
describe('relativeDateFilterStringifiedSchema', () => {
|
||||
it('should parse a valid PAST filter string', () => {
|
||||
const result = relativeDateFilterStringifiedSchema.safeParse('PAST_7_DAY');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.direction).toBe('PAST');
|
||||
expect(result.data.amount).toBe(7);
|
||||
expect(result.data.unit).toBe('DAY');
|
||||
}
|
||||
});
|
||||
|
||||
it('should parse a valid NEXT filter string', () => {
|
||||
const result = relativeDateFilterStringifiedSchema.safeParse('NEXT_30_MINUTE');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.direction).toBe('NEXT');
|
||||
expect(result.data.amount).toBe(30);
|
||||
expect(result.data.unit).toBe('MINUTE');
|
||||
}
|
||||
});
|
||||
|
||||
it('should parse a THIS filter string', () => {
|
||||
const result = relativeDateFilterStringifiedSchema.safeParse('THIS_1_MONTH');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.direction).toBe('THIS');
|
||||
expect(result.data.unit).toBe('MONTH');
|
||||
}
|
||||
});
|
||||
|
||||
it('should parse a filter string with timezone', () => {
|
||||
const result = relativeDateFilterStringifiedSchema.safeParse(
|
||||
'PAST_7_DAY;;America/New_York;;',
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.timezone).toBe('America/New_York');
|
||||
}
|
||||
});
|
||||
|
||||
it('should parse a filter string with firstDayOfTheWeek', () => {
|
||||
const result = relativeDateFilterStringifiedSchema.safeParse(
|
||||
'THIS_1_WEEK;;UTC;;MONDAY;;',
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.firstDayOfTheWeek).toBe('MONDAY');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail for invalid strings', () => {
|
||||
const result =
|
||||
relativeDateFilterStringifiedSchema.safeParse('INVALID_STRING');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { ViewFilterOperand } from '@/types';
|
||||
import { resolveDateFilter } from '@/utils/filter/dates/utils/resolveDateFilter';
|
||||
import { resolveDateTimeFilter } from '@/utils/filter/dates/utils/resolveDateTimeFilter';
|
||||
|
||||
describe('resolveDateFilter', () => {
|
||||
it('should return null for empty value', () => {
|
||||
expect(
|
||||
resolveDateFilter({ value: '', operand: ViewFilterOperand.IS_AFTER }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the value directly for non-relative operands', () => {
|
||||
expect(
|
||||
resolveDateFilter({
|
||||
value: '2024-03-15',
|
||||
operand: ViewFilterOperand.IS_AFTER,
|
||||
}),
|
||||
).toBe('2024-03-15');
|
||||
});
|
||||
|
||||
it('should resolve relative date filter for IS_RELATIVE operand', () => {
|
||||
const result = resolveDateFilter({
|
||||
value: 'PAST_7_DAY',
|
||||
operand: ViewFilterOperand.IS_RELATIVE,
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toHaveProperty('start');
|
||||
expect(result).toHaveProperty('end');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveDateTimeFilter', () => {
|
||||
it('should return null for empty value', () => {
|
||||
expect(
|
||||
resolveDateTimeFilter({
|
||||
value: '',
|
||||
operand: ViewFilterOperand.IS_AFTER,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the value directly for non-relative operands', () => {
|
||||
expect(
|
||||
resolveDateTimeFilter({
|
||||
value: '2024-03-15T10:00:00Z',
|
||||
operand: ViewFilterOperand.IS_AFTER,
|
||||
}),
|
||||
).toBe('2024-03-15T10:00:00Z');
|
||||
});
|
||||
|
||||
it('should resolve relative date-time filter for IS_RELATIVE operand', () => {
|
||||
const result = resolveDateTimeFilter({
|
||||
value: 'PAST_7_DAY',
|
||||
operand: ViewFilterOperand.IS_RELATIVE,
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toHaveProperty('start');
|
||||
expect(result).toHaveProperty('end');
|
||||
});
|
||||
});
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { Temporal } from 'temporal-polyfill';
|
||||
|
||||
import { resolveRelativeDateFilter } from '@/utils/filter/dates/utils/resolveRelativeDateFilter';
|
||||
|
||||
describe('resolveRelativeDateFilter', () => {
|
||||
const referenceZdt = Temporal.ZonedDateTime.from(
|
||||
'2024-03-15T12:00:00[UTC]',
|
||||
);
|
||||
|
||||
describe('NEXT direction', () => {
|
||||
it('should compute start and end for NEXT 7 DAY', () => {
|
||||
const result = resolveRelativeDateFilter(
|
||||
{ direction: 'NEXT', amount: 7, unit: 'DAY' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
expect(result.start).toBe('2024-03-16');
|
||||
expect(result.end).toBe('2024-03-23');
|
||||
});
|
||||
|
||||
it('should throw if amount is undefined', () => {
|
||||
expect(() =>
|
||||
resolveRelativeDateFilter(
|
||||
{ direction: 'NEXT', amount: undefined as any, unit: 'DAY' },
|
||||
referenceZdt,
|
||||
),
|
||||
).toThrow('Amount is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PAST direction', () => {
|
||||
it('should compute start and end for PAST 7 DAY', () => {
|
||||
const result = resolveRelativeDateFilter(
|
||||
{ direction: 'PAST', amount: 7, unit: 'DAY' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
expect(result.start).toBe('2024-03-08');
|
||||
expect(result.end).toBe('2024-03-15');
|
||||
});
|
||||
|
||||
it('should throw if amount is undefined', () => {
|
||||
expect(() =>
|
||||
resolveRelativeDateFilter(
|
||||
{ direction: 'PAST', amount: undefined as any, unit: 'DAY' },
|
||||
referenceZdt,
|
||||
),
|
||||
).toThrow('Amount is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('THIS direction', () => {
|
||||
it('should compute start and end for THIS MONTH', () => {
|
||||
const result = resolveRelativeDateFilter(
|
||||
{ direction: 'THIS', amount: 1, unit: 'MONTH' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
expect(result.start).toBe('2024-03-01');
|
||||
expect(result.end).toBe('2024-04-01');
|
||||
});
|
||||
|
||||
it('should compute start and end for THIS YEAR', () => {
|
||||
const result = resolveRelativeDateFilter(
|
||||
{ direction: 'THIS', amount: 1, unit: 'YEAR' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
expect(result.start).toBe('2024-01-01');
|
||||
expect(result.end).toBe('2025-01-01');
|
||||
});
|
||||
});
|
||||
});
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { resolveRelativeDateFilterStringified } from '@/utils/filter/dates/utils/resolveRelativeDateFilterStringified';
|
||||
import { resolveRelativeDateTimeFilterStringified } from '@/utils/filter/dates/utils/resolveRelativeDateTimeFilterStringified';
|
||||
|
||||
describe('resolveRelativeDateFilterStringified', () => {
|
||||
it('should return null for empty string', () => {
|
||||
expect(resolveRelativeDateFilterStringified('')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for null', () => {
|
||||
expect(resolveRelativeDateFilterStringified(null)).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for undefined', () => {
|
||||
expect(resolveRelativeDateFilterStringified(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for invalid filter string', () => {
|
||||
expect(resolveRelativeDateFilterStringified('INVALID')).toBeNull();
|
||||
});
|
||||
|
||||
it('should resolve a valid PAST filter', () => {
|
||||
const result = resolveRelativeDateFilterStringified('PAST_7_DAY');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.direction).toBe('PAST');
|
||||
expect(result?.start).toBeDefined();
|
||||
expect(result?.end).toBeDefined();
|
||||
});
|
||||
|
||||
it('should resolve a valid NEXT filter', () => {
|
||||
const result = resolveRelativeDateFilterStringified('NEXT_3_MONTH');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.direction).toBe('NEXT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveRelativeDateTimeFilterStringified', () => {
|
||||
it('should return null for empty string', () => {
|
||||
expect(resolveRelativeDateTimeFilterStringified('')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for null', () => {
|
||||
expect(resolveRelativeDateTimeFilterStringified(null)).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for invalid filter string', () => {
|
||||
expect(resolveRelativeDateTimeFilterStringified('INVALID')).toBeNull();
|
||||
});
|
||||
|
||||
it('should resolve a valid PAST filter', () => {
|
||||
const result = resolveRelativeDateTimeFilterStringified('PAST_24_HOUR');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.direction).toBe('PAST');
|
||||
expect(result?.start).toBeDefined();
|
||||
expect(result?.end).toBeDefined();
|
||||
});
|
||||
});
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import { Temporal } from 'temporal-polyfill';
|
||||
|
||||
import { resolveRelativeDateTimeFilter } from '@/utils/filter/dates/utils/resolveRelativeDateTimeFilter';
|
||||
|
||||
describe('resolveRelativeDateTimeFilter', () => {
|
||||
const referenceZdt = Temporal.ZonedDateTime.from(
|
||||
'2024-03-15T12:00:00[UTC]',
|
||||
);
|
||||
|
||||
describe('NEXT direction', () => {
|
||||
it('should compute for sub-day unit (HOUR)', () => {
|
||||
const result = resolveRelativeDateTimeFilter(
|
||||
{ direction: 'NEXT', amount: 3, unit: 'HOUR' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
expect(result.start).toEqual(referenceZdt);
|
||||
expect(result.end?.hour).toBe(15);
|
||||
});
|
||||
|
||||
it('should compute for DAY unit', () => {
|
||||
const result = resolveRelativeDateTimeFilter(
|
||||
{ direction: 'NEXT', amount: 7, unit: 'DAY' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
expect(result.start?.day).toBe(16);
|
||||
expect(result.end?.day).toBe(23);
|
||||
});
|
||||
|
||||
it('should throw if amount is undefined', () => {
|
||||
expect(() =>
|
||||
resolveRelativeDateTimeFilter(
|
||||
{ direction: 'NEXT', amount: undefined as any, unit: 'DAY' },
|
||||
referenceZdt,
|
||||
),
|
||||
).toThrow('Amount is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PAST direction', () => {
|
||||
it('should compute for sub-day unit (MINUTE)', () => {
|
||||
const result = resolveRelativeDateTimeFilter(
|
||||
{ direction: 'PAST', amount: 30, unit: 'MINUTE' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
expect(result.end).toEqual(referenceZdt);
|
||||
expect(result.start?.minute).toBe(30);
|
||||
expect(result.start?.hour).toBe(11);
|
||||
});
|
||||
|
||||
it('should compute for DAY unit', () => {
|
||||
const result = resolveRelativeDateTimeFilter(
|
||||
{ direction: 'PAST', amount: 3, unit: 'DAY' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
expect(result.end?.hour).toBe(0);
|
||||
expect(result.start?.day).toBe(12);
|
||||
});
|
||||
|
||||
it('should throw if amount is undefined', () => {
|
||||
expect(() =>
|
||||
resolveRelativeDateTimeFilter(
|
||||
{ direction: 'PAST', amount: undefined as any, unit: 'MONTH' },
|
||||
referenceZdt,
|
||||
),
|
||||
).toThrow('Amount is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('THIS direction', () => {
|
||||
it('should compute for THIS MONTH', () => {
|
||||
const result = resolveRelativeDateTimeFilter(
|
||||
{ direction: 'THIS', amount: 1, unit: 'MONTH' },
|
||||
referenceZdt,
|
||||
);
|
||||
|
||||
expect(result.start?.day).toBe(1);
|
||||
expect(result.end?.month).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { subUnitFromDateTime } from '@/utils/filter/dates/utils/subUnitFromDateTime';
|
||||
|
||||
describe('subUnitFromDateTime', () => {
|
||||
const baseDate = new Date('2024-03-15T12:00:00Z');
|
||||
|
||||
it('should subtract seconds', () => {
|
||||
const result = subUnitFromDateTime(baseDate, 30, 'SECOND');
|
||||
|
||||
expect(baseDate.getTime() - result.getTime()).toBe(30_000);
|
||||
});
|
||||
|
||||
it('should subtract minutes', () => {
|
||||
const result = subUnitFromDateTime(baseDate, 5, 'MINUTE');
|
||||
|
||||
expect(baseDate.getTime() - result.getTime()).toBe(5 * 60_000);
|
||||
});
|
||||
|
||||
it('should subtract hours', () => {
|
||||
const result = subUnitFromDateTime(baseDate, 2, 'HOUR');
|
||||
|
||||
expect(baseDate.getTime() - result.getTime()).toBe(2 * 3_600_000);
|
||||
});
|
||||
|
||||
it('should subtract days', () => {
|
||||
const result = subUnitFromDateTime(baseDate, 3, 'DAY');
|
||||
|
||||
expect(result.getDate()).toBe(12);
|
||||
});
|
||||
|
||||
it('should subtract weeks', () => {
|
||||
const result = subUnitFromDateTime(baseDate, 1, 'WEEK');
|
||||
|
||||
expect(result.getDate()).toBe(8);
|
||||
});
|
||||
|
||||
it('should subtract months', () => {
|
||||
const result = subUnitFromDateTime(baseDate, 2, 'MONTH');
|
||||
|
||||
expect(result.getMonth()).toBe(0); // January
|
||||
});
|
||||
|
||||
it('should subtract years', () => {
|
||||
const result = subUnitFromDateTime(baseDate, 1, 'YEAR');
|
||||
|
||||
expect(result.getFullYear()).toBe(2023);
|
||||
});
|
||||
});
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { Temporal } from 'temporal-polyfill';
|
||||
|
||||
import { subUnitFromZonedDateTime } from '@/utils/filter/dates/utils/subUnitFromZonedDateTime';
|
||||
|
||||
describe('subUnitFromZonedDateTime', () => {
|
||||
const baseZdt = Temporal.ZonedDateTime.from(
|
||||
'2024-03-15T12:00:00[UTC]',
|
||||
);
|
||||
|
||||
it('should subtract days', () => {
|
||||
const result = subUnitFromZonedDateTime(baseZdt, 'DAY', 3);
|
||||
|
||||
expect(result.day).toBe(12);
|
||||
});
|
||||
|
||||
it('should subtract weeks', () => {
|
||||
const result = subUnitFromZonedDateTime(baseZdt, 'WEEK', 1);
|
||||
|
||||
expect(result.day).toBe(8);
|
||||
});
|
||||
|
||||
it('should subtract months', () => {
|
||||
const result = subUnitFromZonedDateTime(baseZdt, 'MONTH', 2);
|
||||
|
||||
expect(result.month).toBe(1);
|
||||
});
|
||||
|
||||
it('should subtract quarters', () => {
|
||||
const result = subUnitFromZonedDateTime(baseZdt, 'QUARTER', 1);
|
||||
|
||||
expect(result.month).toBe(12);
|
||||
expect(result.year).toBe(2023);
|
||||
});
|
||||
|
||||
it('should subtract years', () => {
|
||||
const result = subUnitFromZonedDateTime(baseZdt, 'YEAR', 1);
|
||||
|
||||
expect(result.year).toBe(2023);
|
||||
});
|
||||
|
||||
it('should subtract seconds', () => {
|
||||
const result = subUnitFromZonedDateTime(baseZdt, 'SECOND', 30);
|
||||
|
||||
expect(result.second).toBe(30);
|
||||
expect(result.minute).toBe(59);
|
||||
expect(result.hour).toBe(11);
|
||||
});
|
||||
|
||||
it('should subtract minutes', () => {
|
||||
const result = subUnitFromZonedDateTime(baseZdt, 'MINUTE', 15);
|
||||
|
||||
expect(result.minute).toBe(45);
|
||||
});
|
||||
|
||||
it('should subtract hours', () => {
|
||||
const result = subUnitFromZonedDateTime(baseZdt, 'HOUR', 3);
|
||||
|
||||
expect(result.hour).toBe(9);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { combineFilters } from '@/utils/filter/utils/combineFilters';
|
||||
|
||||
describe('combineFilters', () => {
|
||||
it('should return empty object when all filters are empty', () => {
|
||||
expect(combineFilters([{}, {}, {}])).toEqual({});
|
||||
});
|
||||
|
||||
it('should return single filter when only one non-empty filter exists', () => {
|
||||
const filter = { name: { eq: 'test' } };
|
||||
|
||||
expect(combineFilters([{}, filter, {}])).toEqual(filter);
|
||||
});
|
||||
|
||||
it('should combine multiple non-empty filters with and', () => {
|
||||
const filter1 = { name: { eq: 'test' } };
|
||||
const filter2 = { age: { gt: 18 } };
|
||||
|
||||
expect(combineFilters([filter1, filter2])).toEqual({
|
||||
and: [filter1, filter2],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty object for empty array', () => {
|
||||
expect(combineFilters([])).toEqual({});
|
||||
});
|
||||
});
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { convertViewFilterValueToString } from '@/utils/filter/utils/convertViewFilterValueToString';
|
||||
|
||||
describe('convertViewFilterValueToString', () => {
|
||||
it('should return string values as-is', () => {
|
||||
expect(convertViewFilterValueToString('hello')).toBe('hello');
|
||||
});
|
||||
|
||||
it('should stringify non-string values', () => {
|
||||
expect(convertViewFilterValueToString(42)).toBe('42');
|
||||
});
|
||||
|
||||
it('should stringify objects', () => {
|
||||
expect(convertViewFilterValueToString({ key: 'value' })).toBe(
|
||||
'{"key":"value"}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should stringify null as empty string', () => {
|
||||
expect(convertViewFilterValueToString(null)).toBe('""');
|
||||
});
|
||||
|
||||
it('should stringify undefined as empty string', () => {
|
||||
expect(convertViewFilterValueToString(undefined)).toBe('""');
|
||||
});
|
||||
});
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
import { FieldMetadataType, ViewFilterOperand } from '@/types';
|
||||
import { getEmptyRecordGqlOperationFilter } from '@/utils/filter/utils/getEmptyRecordGqlOperationFilter';
|
||||
import { type RecordFilter } from '@/utils';
|
||||
|
||||
const makeParams = (
|
||||
fieldType: FieldMetadataType,
|
||||
operand: ViewFilterOperand = ViewFilterOperand.IS_EMPTY,
|
||||
subFieldName?: string,
|
||||
) => ({
|
||||
operand,
|
||||
correspondingField: { id: 'f1', name: 'testField', type: fieldType },
|
||||
recordFilter: {
|
||||
id: '1',
|
||||
fieldMetadataId: 'f1',
|
||||
value: '',
|
||||
type: 'TEXT' as any,
|
||||
operand,
|
||||
subFieldName,
|
||||
} as RecordFilter,
|
||||
});
|
||||
|
||||
describe('getEmptyRecordGqlOperationFilter', () => {
|
||||
describe('IS_EMPTY operand', () => {
|
||||
it('should handle TEXT type', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.TEXT),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should handle NUMBER type', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.NUMBER),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
testField: { is: 'NULL' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle DATE type', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.DATE),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
testField: { is: 'NULL' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle DATE_TIME type', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.DATE_TIME),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
testField: { is: 'NULL' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle SELECT type', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.SELECT),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
testField: { is: 'NULL' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle MULTI_SELECT type', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.MULTI_SELECT),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should handle RATING type', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.RATING),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
testField: { is: 'NULL' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle RELATION type', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.RELATION),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
testFieldId: { is: 'NULL' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle CURRENCY type', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.CURRENCY),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should handle ACTOR type', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.ACTOR),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should handle ARRAY type', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.ARRAY),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should handle RAW_JSON type', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.RAW_JSON),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should handle FILES type', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.FILES),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should handle FULL_NAME type without subfield', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.FULL_NAME),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
});
|
||||
|
||||
it('should handle FULL_NAME type with subfield', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(
|
||||
FieldMetadataType.FULL_NAME,
|
||||
ViewFilterOperand.IS_EMPTY,
|
||||
'firstName',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should handle PHONES type without subfield', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.PHONES),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
});
|
||||
|
||||
it('should handle PHONES type with primaryPhoneNumber subfield', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(
|
||||
FieldMetadataType.PHONES,
|
||||
ViewFilterOperand.IS_EMPTY,
|
||||
'primaryPhoneNumber',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should handle PHONES type with additionalPhones subfield', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(
|
||||
FieldMetadataType.PHONES,
|
||||
ViewFilterOperand.IS_EMPTY,
|
||||
'additionalPhones',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should handle ADDRESS type without subfield', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.ADDRESS),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
expect((result as any).and).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('should handle ADDRESS type with subfield', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(
|
||||
FieldMetadataType.ADDRESS,
|
||||
ViewFilterOperand.IS_EMPTY,
|
||||
'addressCity',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('or');
|
||||
});
|
||||
|
||||
it('should handle LINKS type', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.LINKS),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
});
|
||||
|
||||
it('should handle EMAILS type', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.EMAILS),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('and');
|
||||
});
|
||||
});
|
||||
|
||||
describe('IS_NOT_EMPTY operand', () => {
|
||||
it('should wrap filter in not for IS_NOT_EMPTY', () => {
|
||||
const result = getEmptyRecordGqlOperationFilter(
|
||||
makeParams(FieldMetadataType.TEXT, ViewFilterOperand.IS_NOT_EMPTY),
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('not');
|
||||
});
|
||||
});
|
||||
});
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { isMatchingMultiSelectFilter } from '@/utils/filter/utils/isMatchingMultiSelectFilter';
|
||||
|
||||
describe('isMatchingMultiSelectFilter', () => {
|
||||
describe('containsAny', () => {
|
||||
it('should return true when value contains all filter items', () => {
|
||||
expect(
|
||||
isMatchingMultiSelectFilter({
|
||||
multiSelectFilter: { containsAny: ['A', 'B'] },
|
||||
value: ['A', 'B', 'C'],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when value does not contain all filter items', () => {
|
||||
expect(
|
||||
isMatchingMultiSelectFilter({
|
||||
multiSelectFilter: { containsAny: ['A', 'D'] },
|
||||
value: ['A', 'B', 'C'],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for null value', () => {
|
||||
expect(
|
||||
isMatchingMultiSelectFilter({
|
||||
multiSelectFilter: { containsAny: ['A'] },
|
||||
value: null,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEmptyArray', () => {
|
||||
it('should return true for empty array', () => {
|
||||
expect(
|
||||
isMatchingMultiSelectFilter({
|
||||
multiSelectFilter: { isEmptyArray: true },
|
||||
value: [],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-empty array', () => {
|
||||
expect(
|
||||
isMatchingMultiSelectFilter({
|
||||
multiSelectFilter: { isEmptyArray: true },
|
||||
value: ['A'],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is', () => {
|
||||
it('should match NULL check', () => {
|
||||
expect(
|
||||
isMatchingMultiSelectFilter({
|
||||
multiSelectFilter: { is: 'NULL' },
|
||||
value: null,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should match NOT_NULL check', () => {
|
||||
expect(
|
||||
isMatchingMultiSelectFilter({
|
||||
multiSelectFilter: { is: 'NOT_NULL' },
|
||||
value: ['A'],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
it('should throw for unexpected filter', () => {
|
||||
expect(() =>
|
||||
isMatchingMultiSelectFilter({
|
||||
multiSelectFilter: {} as any,
|
||||
value: ['A'],
|
||||
}),
|
||||
).toThrow('Unexpected value for multi-select filter');
|
||||
});
|
||||
});
|
||||
});
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { isMatchingRatingFilter } from '@/utils/filter/utils/isMatchingRatingFilter';
|
||||
|
||||
describe('isMatchingRatingFilter', () => {
|
||||
describe('eq', () => {
|
||||
it('should return true when value equals', () => {
|
||||
expect(
|
||||
isMatchingRatingFilter({
|
||||
ratingFilter: { eq: 'RATING_3' },
|
||||
value: 'RATING_3',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when value does not equal', () => {
|
||||
expect(
|
||||
isMatchingRatingFilter({
|
||||
ratingFilter: { eq: 'RATING_3' },
|
||||
value: 'RATING_1',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('in', () => {
|
||||
it('should return true when value is in list', () => {
|
||||
expect(
|
||||
isMatchingRatingFilter({
|
||||
ratingFilter: { in: ['RATING_3', 'RATING_5'] },
|
||||
value: 'RATING_3',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for null value', () => {
|
||||
expect(
|
||||
isMatchingRatingFilter({
|
||||
ratingFilter: { in: ['RATING_3'] },
|
||||
value: null,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is', () => {
|
||||
it('should match NULL check', () => {
|
||||
expect(
|
||||
isMatchingRatingFilter({
|
||||
ratingFilter: { is: 'NULL' },
|
||||
value: null,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should match NOT_NULL check', () => {
|
||||
expect(
|
||||
isMatchingRatingFilter({
|
||||
ratingFilter: { is: 'NOT_NULL' },
|
||||
value: 'RATING_3',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
it('should throw for unexpected filter', () => {
|
||||
expect(() =>
|
||||
isMatchingRatingFilter({
|
||||
ratingFilter: {} as any,
|
||||
value: 'RATING_3',
|
||||
}),
|
||||
).toThrow('Unexpected value for rating filter');
|
||||
});
|
||||
});
|
||||
});
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { isMatchingRawJsonFilter } from '@/utils/filter/utils/isMatchingRawJsonFilter';
|
||||
|
||||
describe('isMatchingRawJsonFilter', () => {
|
||||
describe('like', () => {
|
||||
it('should match using wildcard pattern', () => {
|
||||
expect(
|
||||
isMatchingRawJsonFilter({
|
||||
rawJsonFilter: { like: '%test%' },
|
||||
value: 'some test value',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match when pattern does not match', () => {
|
||||
expect(
|
||||
isMatchingRawJsonFilter({
|
||||
rawJsonFilter: { like: '%xyz%' },
|
||||
value: 'some test value',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is', () => {
|
||||
it('should match NULL check', () => {
|
||||
expect(
|
||||
isMatchingRawJsonFilter({
|
||||
rawJsonFilter: { is: 'NULL' },
|
||||
value: null as any,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should match NOT_NULL check', () => {
|
||||
expect(
|
||||
isMatchingRawJsonFilter({
|
||||
rawJsonFilter: { is: 'NOT_NULL' },
|
||||
value: '{"key": "val"}',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
it('should throw for unexpected filter', () => {
|
||||
expect(() =>
|
||||
isMatchingRawJsonFilter({
|
||||
rawJsonFilter: {} as any,
|
||||
value: 'test',
|
||||
}),
|
||||
).toThrow('Unexpected value for string filter');
|
||||
});
|
||||
});
|
||||
});
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { isMatchingRichTextV2Filter } from '@/utils/filter/utils/isMatchingRichTextV2Filter';
|
||||
|
||||
describe('isMatchingRichTextV2Filter', () => {
|
||||
describe('markdown ilike', () => {
|
||||
it('should match with wildcard pattern', () => {
|
||||
expect(
|
||||
isMatchingRichTextV2Filter({
|
||||
richTextV2Filter: { markdown: { ilike: '%hello%' } },
|
||||
value: 'say hello world',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match when pattern does not match', () => {
|
||||
expect(
|
||||
isMatchingRichTextV2Filter({
|
||||
richTextV2Filter: { markdown: { ilike: '%goodbye%' } },
|
||||
value: 'say hello world',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should be case insensitive', () => {
|
||||
expect(
|
||||
isMatchingRichTextV2Filter({
|
||||
richTextV2Filter: { markdown: { ilike: '%HELLO%' } },
|
||||
value: 'say hello world',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
it('should throw for unexpected filter', () => {
|
||||
expect(() =>
|
||||
isMatchingRichTextV2Filter({
|
||||
richTextV2Filter: {} as any,
|
||||
value: 'test',
|
||||
}),
|
||||
).toThrow('Unexpected value for RICH_TEXT_V2 filter');
|
||||
});
|
||||
});
|
||||
});
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { isMatchingSelectFilter } from '@/utils/filter/utils/isMatchingSelectFilter';
|
||||
|
||||
describe('isMatchingSelectFilter', () => {
|
||||
describe('in', () => {
|
||||
it('should return true when value is in the list', () => {
|
||||
expect(
|
||||
isMatchingSelectFilter({
|
||||
selectFilter: { in: ['ACTIVE', 'PENDING'] },
|
||||
value: 'ACTIVE',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when value is not in the list', () => {
|
||||
expect(
|
||||
isMatchingSelectFilter({
|
||||
selectFilter: { in: ['ACTIVE', 'PENDING'] },
|
||||
value: 'CLOSED',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is', () => {
|
||||
it('should match NULL check', () => {
|
||||
expect(
|
||||
isMatchingSelectFilter({
|
||||
selectFilter: { is: 'NULL' },
|
||||
value: null as any,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should match NOT_NULL check', () => {
|
||||
expect(
|
||||
isMatchingSelectFilter({
|
||||
selectFilter: { is: 'NOT_NULL' },
|
||||
value: 'ACTIVE',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('eq', () => {
|
||||
it('should return true when value equals', () => {
|
||||
expect(
|
||||
isMatchingSelectFilter({
|
||||
selectFilter: { eq: 'ACTIVE' },
|
||||
value: 'ACTIVE',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when value does not equal', () => {
|
||||
expect(
|
||||
isMatchingSelectFilter({
|
||||
selectFilter: { eq: 'ACTIVE' },
|
||||
value: 'CLOSED',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('neq', () => {
|
||||
it('should return true when value does not equal', () => {
|
||||
expect(
|
||||
isMatchingSelectFilter({
|
||||
selectFilter: { neq: 'ACTIVE' },
|
||||
value: 'CLOSED',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
it('should throw for unexpected filter', () => {
|
||||
expect(() =>
|
||||
isMatchingSelectFilter({
|
||||
selectFilter: {} as any,
|
||||
value: 'ACTIVE',
|
||||
}),
|
||||
).toThrow('Unexpected value for select filter');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { getGenericOperationName } from '@/utils/sentry/getGenericOperationName';
|
||||
|
||||
describe('getGenericOperationName', () => {
|
||||
it('should extract the first word from a PascalCase name', () => {
|
||||
expect(getGenericOperationName('FindOnePerson')).toBe('Find');
|
||||
});
|
||||
|
||||
it('should extract the first word from another PascalCase name', () => {
|
||||
expect(getGenericOperationName('AggregateCompanies')).toBe('Aggregate');
|
||||
});
|
||||
|
||||
it('should return undefined for undefined input', () => {
|
||||
expect(getGenericOperationName(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle single word', () => {
|
||||
expect(getGenericOperationName('Create')).toBe('Create');
|
||||
});
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { getHumanReadableNameFromCode } from '@/utils/sentry/getHumanReadableNameFromCode';
|
||||
|
||||
describe('getHumanReadableNameFromCode', () => {
|
||||
it('should convert snake_case to Title Case', () => {
|
||||
expect(getHumanReadableNameFromCode('hello_world')).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should handle single word', () => {
|
||||
expect(getHumanReadableNameFromCode('hello')).toBe('Hello');
|
||||
});
|
||||
|
||||
it('should handle uppercase code', () => {
|
||||
expect(getHumanReadableNameFromCode('NOT_FOUND')).toBe('Not Found');
|
||||
});
|
||||
|
||||
it('should handle multiple underscores', () => {
|
||||
expect(getHumanReadableNameFromCode('a_b_c_d')).toBe('A B C D');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { isPlainObject } from '@/utils/typeguard/isPlainObject';
|
||||
|
||||
describe('isPlainObject', () => {
|
||||
it('should return true for plain objects', () => {
|
||||
expect(isPlainObject({})).toBe(true);
|
||||
expect(isPlainObject({ key: 'value' })).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for arrays', () => {
|
||||
expect(isPlainObject([])).toBe(false);
|
||||
expect(isPlainObject([1, 2, 3])).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for null', () => {
|
||||
expect(isPlainObject(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for primitives', () => {
|
||||
expect(isPlainObject(42)).toBe(false);
|
||||
expect(isPlainObject('string')).toBe(false);
|
||||
expect(isPlainObject(undefined)).toBe(false);
|
||||
expect(isPlainObject(true)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { throwIfNotDefined } from '@/utils/typeguard/throwIfNotDefined';
|
||||
|
||||
describe('throwIfNotDefined', () => {
|
||||
it('should not throw for defined values', () => {
|
||||
expect(() => throwIfNotDefined('hello', 'myVar')).not.toThrow();
|
||||
expect(() => throwIfNotDefined(0, 'myVar')).not.toThrow();
|
||||
expect(() => throwIfNotDefined(false, 'myVar')).not.toThrow();
|
||||
expect(() => throwIfNotDefined('', 'myVar')).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw for null', () => {
|
||||
expect(() => throwIfNotDefined(null, 'myVar')).toThrow(
|
||||
'Value must be defined for variable myVar',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for undefined', () => {
|
||||
expect(() => throwIfNotDefined(undefined, 'myVar')).toThrow(
|
||||
'Value must be defined for variable myVar',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { getAbsoluteUrl } from '@/utils/url/getAbsoluteUrl';
|
||||
|
||||
describe('getAbsoluteUrl', () => {
|
||||
it('should return https URL as-is', () => {
|
||||
expect(getAbsoluteUrl('https://example.com')).toBe('https://example.com');
|
||||
});
|
||||
|
||||
it('should return http URL as-is', () => {
|
||||
expect(getAbsoluteUrl('http://example.com')).toBe('http://example.com');
|
||||
});
|
||||
|
||||
it('should return HTTPS URL as-is', () => {
|
||||
expect(getAbsoluteUrl('HTTPS://example.com')).toBe('HTTPS://example.com');
|
||||
});
|
||||
|
||||
it('should return HTTP URL as-is', () => {
|
||||
expect(getAbsoluteUrl('HTTP://example.com')).toBe('HTTP://example.com');
|
||||
});
|
||||
|
||||
it('should prepend https:// to bare domains', () => {
|
||||
expect(getAbsoluteUrl('example.com')).toBe('https://example.com');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { safeDecodeURIComponent } from '@/utils/url/safeDecodeURIComponent';
|
||||
|
||||
describe('safeDecodeURIComponent', () => {
|
||||
it('should decode valid encoded components', () => {
|
||||
expect(safeDecodeURIComponent('hello%20world')).toBe('hello world');
|
||||
});
|
||||
|
||||
it('should return malformed input as-is', () => {
|
||||
expect(safeDecodeURIComponent('%E0%A4%A')).toBe('%E0%A4%A');
|
||||
});
|
||||
|
||||
it('should return plain text as-is', () => {
|
||||
expect(safeDecodeURIComponent('hello')).toBe('hello');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { isValidVariable } from '@/utils/validation/isValidVariable';
|
||||
|
||||
describe('isValidVariable', () => {
|
||||
it('should return true for valid variable syntax', () => {
|
||||
expect(isValidVariable('{{step.output}}')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for nested variable', () => {
|
||||
expect(isValidVariable('{{trigger.output.name}}')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for string without brackets', () => {
|
||||
expect(isValidVariable('no brackets')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for partial brackets', () => {
|
||||
expect(isValidVariable('{{incomplete')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty brackets', () => {
|
||||
expect(isValidVariable('{{}}')).toBe(false);
|
||||
});
|
||||
});
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { getCountryCodesForCallingCode } from '@/utils/validation/phones-value/getCountryCodesForCallingCode';
|
||||
|
||||
describe('getCountryCodesForCallingCode', () => {
|
||||
it('should return country codes for calling code 1 (US/CA)', () => {
|
||||
const result = getCountryCodesForCallingCode('1');
|
||||
|
||||
expect(result).toContain('US');
|
||||
expect(result).toContain('CA');
|
||||
});
|
||||
|
||||
it('should handle + prefix', () => {
|
||||
const result = getCountryCodesForCallingCode('+33');
|
||||
|
||||
expect(result).toContain('FR');
|
||||
});
|
||||
|
||||
it('should return empty array for invalid calling code', () => {
|
||||
expect(getCountryCodesForCallingCode('99999')).toEqual([]);
|
||||
});
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { isValidCountryCode } from '@/utils/validation/phones-value/isValidCountryCode';
|
||||
|
||||
describe('isValidCountryCode', () => {
|
||||
it('should return true for valid country code US', () => {
|
||||
expect(isValidCountryCode('US')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid country code FR', () => {
|
||||
expect(isValidCountryCode('FR')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid country code', () => {
|
||||
expect(isValidCountryCode('XX')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for lowercase', () => {
|
||||
expect(isValidCountryCode('us')).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user