[DASHBOARDS] Fix filter parsing (#16782)
## Fix filter parsing for charts Fixes https://github.com/twentyhq/twenty/issues/16606 Fixes https://github.com/twentyhq/private-issues/issues/396 When clicking on a chart slice/bar grouped by certain field types, the filter value was passed as a plain string instead of a JSON array, causing a JSON parse error on navigation. This happens because `arrayOfStringsOrVariablesSchema` in the filter parsing logic expects JSON array format for certain field types, but the values weren't wrapped correctly for all cases. Extracted the util `formatChartFilterValue` and renamed it to `serializeChartBucketValueForFilter` and modified it to handle JSON array wrapping for: - CURRENCY fields with currencyCode subfield (IS operand) - MULTI_SELECT fields (CONTAINS operand) - ADDRESS fields with addressCountry subfield (CONTAINS operand) Added unit tests for the new utility ### Before https://github.com/user-attachments/assets/9a52572b-e896-445a-9f5c-e21963f78441 ### After https://github.com/user-attachments/assets/12eed5e5-a49c-4557-a45c-ac7e00b3422a
This commit is contained in:
+189
@@ -0,0 +1,189 @@
|
||||
import { FieldMetadataType, ViewFilterOperand } from 'twenty-shared/types';
|
||||
|
||||
import { serializeChartBucketValueForFilter } from '../serializeChartBucketValueForFilter';
|
||||
|
||||
describe('serializeChartBucketValueForFilter', () => {
|
||||
describe('fields requiring JSON array with IS operand', () => {
|
||||
it('should wrap SELECT field value in JSON array', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.SELECT,
|
||||
bucketRawValue: 'option1',
|
||||
operand: ViewFilterOperand.IS,
|
||||
});
|
||||
|
||||
expect(result).toBe('["option1"]');
|
||||
});
|
||||
|
||||
it('should wrap UUID field value in JSON array', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.UUID,
|
||||
bucketRawValue: '123e4567-e89b-12d3-a456-426614174000',
|
||||
operand: ViewFilterOperand.IS,
|
||||
});
|
||||
|
||||
expect(result).toBe('["123e4567-e89b-12d3-a456-426614174000"]');
|
||||
});
|
||||
|
||||
it('should wrap RELATION field value in JSON array', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.RELATION,
|
||||
bucketRawValue: 'relation-id',
|
||||
operand: ViewFilterOperand.IS,
|
||||
});
|
||||
|
||||
expect(result).toBe('["relation-id"]');
|
||||
});
|
||||
|
||||
it('should wrap CURRENCY currencyCode subfield value in JSON array', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.CURRENCY,
|
||||
bucketRawValue: 'EUR',
|
||||
operand: ViewFilterOperand.IS,
|
||||
subFieldName: 'currencyCode',
|
||||
});
|
||||
|
||||
expect(result).toBe('["EUR"]');
|
||||
});
|
||||
|
||||
it('should not wrap CURRENCY amountMicros subfield value in JSON array', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.CURRENCY,
|
||||
bucketRawValue: '1000000',
|
||||
operand: ViewFilterOperand.IS,
|
||||
subFieldName: 'amountMicros',
|
||||
});
|
||||
|
||||
expect(result).toBe('1000000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fields requiring JSON array with CONTAINS operand', () => {
|
||||
it('should wrap MULTI_SELECT field value in JSON array', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.MULTI_SELECT,
|
||||
bucketRawValue: 'option1',
|
||||
operand: ViewFilterOperand.CONTAINS,
|
||||
});
|
||||
|
||||
expect(result).toBe('["option1"]');
|
||||
});
|
||||
|
||||
it('should wrap ADDRESS addressCountry subfield value in JSON array', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.ADDRESS,
|
||||
bucketRawValue: 'US',
|
||||
operand: ViewFilterOperand.CONTAINS,
|
||||
subFieldName: 'addressCountry',
|
||||
});
|
||||
|
||||
expect(result).toBe('["US"]');
|
||||
});
|
||||
|
||||
it('should not wrap ADDRESS addressStreet1 subfield value in JSON array', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.ADDRESS,
|
||||
bucketRawValue: '123 Main St',
|
||||
operand: ViewFilterOperand.CONTAINS,
|
||||
subFieldName: 'addressStreet1',
|
||||
});
|
||||
|
||||
expect(result).toBe('123 Main St');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fields not requiring JSON array', () => {
|
||||
it('should not wrap TEXT field value', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.TEXT,
|
||||
bucketRawValue: 'some text',
|
||||
operand: ViewFilterOperand.CONTAINS,
|
||||
});
|
||||
|
||||
expect(result).toBe('some text');
|
||||
});
|
||||
|
||||
it('should not wrap NUMBER field value', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.NUMBER,
|
||||
bucketRawValue: '42',
|
||||
operand: ViewFilterOperand.IS,
|
||||
});
|
||||
|
||||
expect(result).toBe('42');
|
||||
});
|
||||
|
||||
it('should not wrap BOOLEAN field value', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.BOOLEAN,
|
||||
bucketRawValue: 'true',
|
||||
operand: ViewFilterOperand.IS,
|
||||
});
|
||||
|
||||
expect(result).toBe('true');
|
||||
});
|
||||
|
||||
it('should not wrap SELECT field with non-IS operand', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.SELECT,
|
||||
bucketRawValue: 'option1',
|
||||
operand: ViewFilterOperand.IS_NOT,
|
||||
});
|
||||
|
||||
expect(result).toBe('option1');
|
||||
});
|
||||
|
||||
it('should not wrap MULTI_SELECT field with non-CONTAINS operand', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.MULTI_SELECT,
|
||||
bucketRawValue: 'option1',
|
||||
operand: ViewFilterOperand.DOES_NOT_CONTAIN,
|
||||
});
|
||||
|
||||
expect(result).toBe('option1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('value conversion', () => {
|
||||
it('should convert number to string', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.NUMBER,
|
||||
bucketRawValue: 123,
|
||||
operand: ViewFilterOperand.IS,
|
||||
});
|
||||
|
||||
expect(result).toBe('123');
|
||||
});
|
||||
|
||||
it('should convert boolean to string', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.BOOLEAN,
|
||||
bucketRawValue: true,
|
||||
operand: ViewFilterOperand.IS,
|
||||
});
|
||||
|
||||
expect(result).toBe('true');
|
||||
});
|
||||
|
||||
it('should handle null subFieldName', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.SELECT,
|
||||
bucketRawValue: 'option1',
|
||||
operand: ViewFilterOperand.IS,
|
||||
subFieldName: null,
|
||||
});
|
||||
|
||||
expect(result).toBe('["option1"]');
|
||||
});
|
||||
|
||||
it('should handle undefined subFieldName', () => {
|
||||
const result = serializeChartBucketValueForFilter({
|
||||
fieldType: FieldMetadataType.SELECT,
|
||||
bucketRawValue: 'option1',
|
||||
operand: ViewFilterOperand.IS,
|
||||
subFieldName: undefined,
|
||||
});
|
||||
|
||||
expect(result).toBe('["option1"]');
|
||||
});
|
||||
});
|
||||
});
|
||||
+5
-22
@@ -6,6 +6,7 @@ import { buildDateFilterForDayGranularity } from '@/page-layout/widgets/graph/ut
|
||||
import { buildDateRangeFiltersForGranularity } from '@/page-layout/widgets/graph/utils/buildDateRangeFiltersForGranularity';
|
||||
import { isCyclicalDateGranularity } from '@/page-layout/widgets/graph/utils/isCyclicalDateGranularity';
|
||||
import { isTimeRangeDateGranularity } from '@/page-layout/widgets/graph/utils/isTimeRangeDateGranularity';
|
||||
import { serializeChartBucketValueForFilter } from '@/page-layout/widgets/graph/utils/serializeChartBucketValueForFilter';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import {
|
||||
ObjectRecordGroupByDateGranularity,
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
isDefined,
|
||||
isFieldMetadataDateKind,
|
||||
} from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
type ChartFilter = {
|
||||
fieldName: string;
|
||||
@@ -32,24 +32,6 @@ type BuildFilterFromChartBucketParams = {
|
||||
timezone?: string;
|
||||
};
|
||||
|
||||
const formatChartFilterValue = (
|
||||
fieldType: FieldMetadataType,
|
||||
bucketRawValue: unknown,
|
||||
operand: ViewFilterOperand,
|
||||
): string => {
|
||||
const stringValue = String(bucketRawValue);
|
||||
|
||||
const needsJsonArray =
|
||||
operand === ViewFilterOperand.IS &&
|
||||
[
|
||||
FieldMetadataType.SELECT,
|
||||
FieldMetadataType.UUID,
|
||||
FieldMetadataType.RELATION,
|
||||
].includes(fieldType);
|
||||
|
||||
return needsJsonArray ? JSON.stringify([stringValue]) : stringValue;
|
||||
};
|
||||
|
||||
export const buildFilterFromChartBucket = ({
|
||||
fieldMetadataItem,
|
||||
bucketRawValue,
|
||||
@@ -126,11 +108,12 @@ export const buildFilterFromChartBucket = ({
|
||||
|
||||
const operand = availableOperands[0];
|
||||
|
||||
const value = formatChartFilterValue(
|
||||
fieldMetadataItem.type,
|
||||
const value = serializeChartBucketValueForFilter({
|
||||
fieldType: fieldMetadataItem.type,
|
||||
bucketRawValue,
|
||||
operand,
|
||||
);
|
||||
subFieldName,
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { FieldMetadataType, ViewFilterOperand } from 'twenty-shared/types';
|
||||
|
||||
type SerializeChartBucketValueForFilterParams = {
|
||||
fieldType: FieldMetadataType;
|
||||
bucketRawValue: unknown;
|
||||
operand: ViewFilterOperand;
|
||||
subFieldName?: string | null;
|
||||
};
|
||||
|
||||
export const serializeChartBucketValueForFilter = ({
|
||||
fieldType,
|
||||
bucketRawValue,
|
||||
operand,
|
||||
subFieldName,
|
||||
}: SerializeChartBucketValueForFilterParams): string => {
|
||||
const stringValue = String(bucketRawValue);
|
||||
|
||||
const isCurrencyCodeSubField =
|
||||
fieldType === FieldMetadataType.CURRENCY && subFieldName === 'currencyCode';
|
||||
|
||||
const isAddressCountrySubField =
|
||||
fieldType === FieldMetadataType.ADDRESS &&
|
||||
subFieldName === 'addressCountry';
|
||||
|
||||
const needsJsonArrayWithIsOperand =
|
||||
operand === ViewFilterOperand.IS &&
|
||||
([
|
||||
FieldMetadataType.SELECT,
|
||||
FieldMetadataType.UUID,
|
||||
FieldMetadataType.RELATION,
|
||||
].includes(fieldType) ||
|
||||
isCurrencyCodeSubField);
|
||||
|
||||
const needsJsonArrayWithContainsOperand =
|
||||
operand === ViewFilterOperand.CONTAINS &&
|
||||
(fieldType === FieldMetadataType.MULTI_SELECT || isAddressCountrySubField);
|
||||
|
||||
const needsJsonArray =
|
||||
needsJsonArrayWithIsOperand || needsJsonArrayWithContainsOperand;
|
||||
|
||||
return needsJsonArray ? JSON.stringify([stringValue]) : stringValue;
|
||||
};
|
||||
Reference in New Issue
Block a user