From 0dcfca2ba3cb5ca0e4228a128ce5f35391694bba Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 4 Jul 2025 16:26:19 +0200 Subject: [PATCH] Fixes greater than or equal and less than and equal filters (#13033) This PR fixes a mismatch between the filter operation we have on NUMBER and RATING field types, and the labels we use for those filters in the application. What is actually used is : - Greater than or equal - Less than or equal But unfortunately, until now we display "less than" and "greater than" everywhere. This PR fixes that. We would still have to change the value that is saved in viewFilter table from `greaterThan` to `greaterThanOrEqual` and likewise for less than, but it would require a careful migration, and for now just changing the display labels is enough. See follow-up issue for migration of the DB values : https://github.com/twentyhq/core-team-issues/issues/1196 Fixes https://github.com/twentyhq/twenty/issues/13000 --- .../ObjectFilterDropdownFilterInput.tsx | 4 ++-- .../ObjectFilterDropdownRatingInput.tsx | 4 ++-- .../utils/__tests__/getOperandLabel.test.ts | 10 +++++----- .../getOperandsForFilterType.test.ts | 8 ++++---- .../isFilterOperandExpectingValue.test.ts | 4 ++-- .../utils/configurableViewFilterOperands.ts | 4 ++-- .../utils/getOperandLabel.ts | 16 +++++++-------- .../utils/isFilterOperandExpectingValue.ts | 4 ++-- ...omputeViewRecordGqlOperationFilter.test.ts | 10 +++++----- .../turnRecordFilterIntoGqlOperationFilter.ts | 20 +++++++++---------- .../utils/getRecordFilterOperands.ts | 16 +++++++-------- .../utils/buildRecordInputFromFilter.spec.ts | 8 ++++---- .../utils/buildRecordInputFromFilter.ts | 9 +++++---- .../modules/views/types/ViewFilterOperand.ts | 4 ++-- 14 files changed, 61 insertions(+), 60 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx index 5df28eb2e94..295f62142da 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx @@ -49,8 +49,8 @@ export const ObjectFilterDropdownFilterInput = ({ ViewFilterOperand.Is, ViewFilterOperand.IsNotNull, ViewFilterOperand.IsNot, - ViewFilterOperand.LessThan, - ViewFilterOperand.GreaterThan, + ViewFilterOperand.LessThanOrEqual, + ViewFilterOperand.GreaterThanOrEqual, ViewFilterOperand.IsBefore, ViewFilterOperand.IsAfter, ViewFilterOperand.Contains, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx index 1c9db8ef4e6..a078dc8d9b6 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx @@ -15,14 +15,14 @@ const convertFieldRatingValueToNumber = ( rating: Exclude, ): string => rating.split('_')[1]; -export const convertGreaterThanRatingToArrayOfRatingValues = ( +export const convertGreaterThanOrEqualRatingToArrayOfRatingValues = ( greaterThanValue: number, ) => RATING_VALUES.filter( (ratingValue) => +ratingValue.split('_')[1] >= greaterThanValue, ); -export const convertLessThanRatingToArrayOfRatingValues = ( +export const convertLessThanOrEqualRatingToArrayOfRatingValues = ( lessThanValue: number, ) => RATING_VALUES.filter( diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandLabel.test.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandLabel.test.ts index a20f31ee5a1..2f2a96171f9 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandLabel.test.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandLabel.test.ts @@ -1,14 +1,14 @@ import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; -import { getOperandLabel, getOperandLabelShort } from '../getOperandLabel'; import { capitalize } from 'twenty-shared/utils'; +import { getOperandLabel, getOperandLabelShort } from '../getOperandLabel'; describe('getOperandLabel', () => { const testCases = [ [ViewFilterOperand.Contains, 'Contains'], [ViewFilterOperand.DoesNotContain, "Doesn't contain"], - [ViewFilterOperand.GreaterThan, 'Greater than'], - [ViewFilterOperand.LessThan, 'Less than'], + [ViewFilterOperand.GreaterThanOrEqual, 'Greater than or equal'], + [ViewFilterOperand.LessThanOrEqual, 'Less than or equal'], [ViewFilterOperand.Is, 'Is'], [ViewFilterOperand.IsNot, 'Is not'], [ViewFilterOperand.IsNotNull, 'Is not null'], @@ -32,8 +32,8 @@ describe('getOperandLabelShort', () => { [ViewFilterOperand.IsNot, ': Not'], [ViewFilterOperand.DoesNotContain, ': Not'], [ViewFilterOperand.IsNotNull, ': NotNull'], - [ViewFilterOperand.GreaterThan, '\u00A0> '], - [ViewFilterOperand.LessThan, '\u00A0< '], + [ViewFilterOperand.GreaterThanOrEqual, '\u00A0≥ '], + [ViewFilterOperand.LessThanOrEqual, '\u00A0≤ '], [undefined, ': '], // undefined operand ]; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts index a97e2e8ff3b..7f4a245d13f 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts @@ -16,13 +16,13 @@ describe('getOperandsForFilterType', () => { ]; const numberOperands = [ - RecordFilterOperand.GreaterThan, - RecordFilterOperand.LessThan, + RecordFilterOperand.GreaterThanOrEqual, + RecordFilterOperand.LessThanOrEqual, ]; const currencyAmountMicrosOperands = [ - RecordFilterOperand.GreaterThan, - RecordFilterOperand.LessThan, + RecordFilterOperand.GreaterThanOrEqual, + RecordFilterOperand.LessThanOrEqual, RecordFilterOperand.Is, RecordFilterOperand.IsNot, ]; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/isFilterOperandExpectingValue.test.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/isFilterOperandExpectingValue.test.ts index 62246c7365e..7f1e8b26b43 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/isFilterOperandExpectingValue.test.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/isFilterOperandExpectingValue.test.ts @@ -6,8 +6,8 @@ describe('isFilterOperandExpectingValue', () => { const testCases = [ { operand: ViewFilterOperand.Contains, expectedResult: true }, { operand: ViewFilterOperand.DoesNotContain, expectedResult: true }, - { operand: ViewFilterOperand.GreaterThan, expectedResult: true }, - { operand: ViewFilterOperand.LessThan, expectedResult: true }, + { operand: ViewFilterOperand.GreaterThanOrEqual, expectedResult: true }, + { operand: ViewFilterOperand.LessThanOrEqual, expectedResult: true }, { operand: ViewFilterOperand.Is, expectedResult: true }, { operand: ViewFilterOperand.IsNot, expectedResult: true }, { operand: ViewFilterOperand.IsRelative, expectedResult: true }, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/configurableViewFilterOperands.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/configurableViewFilterOperands.ts index 1b85debc735..aaafc9e8dcb 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/configurableViewFilterOperands.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/configurableViewFilterOperands.ts @@ -4,8 +4,8 @@ export const configurableViewFilterOperands = new Set([ ViewFilterOperand.Is, ViewFilterOperand.IsNotNull, ViewFilterOperand.IsNot, - ViewFilterOperand.LessThan, - ViewFilterOperand.GreaterThan, + ViewFilterOperand.LessThanOrEqual, + ViewFilterOperand.GreaterThanOrEqual, ViewFilterOperand.IsBefore, ViewFilterOperand.IsAfter, ViewFilterOperand.Contains, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandLabel.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandLabel.ts index 1e0a25190f9..4f63f9252c8 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandLabel.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandLabel.ts @@ -9,10 +9,10 @@ export const getOperandLabel = ( return t`Contains`; case ViewFilterOperand.DoesNotContain: return t`Doesn't contain`; - case ViewFilterOperand.GreaterThan: - return t`Greater than`; - case ViewFilterOperand.LessThan: - return t`Less than`; + case ViewFilterOperand.GreaterThanOrEqual: + return t`Greater than or equal`; + case ViewFilterOperand.LessThanOrEqual: + return t`Less than or equal`; case ViewFilterOperand.IsBefore: return t`Is before`; case ViewFilterOperand.IsAfter: @@ -56,10 +56,10 @@ export const getOperandLabelShort = ( return t`: NotEmpty`; case ViewFilterOperand.IsEmpty: return t`: Empty`; - case ViewFilterOperand.GreaterThan: - return '\u00A0> '; - case ViewFilterOperand.LessThan: - return '\u00A0< '; + case ViewFilterOperand.GreaterThanOrEqual: + return '\u00A0≥ '; + case ViewFilterOperand.LessThanOrEqual: + return '\u00A0≤ '; case ViewFilterOperand.IsBefore: return '\u00A0< '; case ViewFilterOperand.IsAfter: diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isFilterOperandExpectingValue.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isFilterOperandExpectingValue.ts index 668742946d3..cdf0623b70d 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isFilterOperandExpectingValue.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isFilterOperandExpectingValue.ts @@ -12,8 +12,8 @@ export const isFilterOperandExpectingValue = (operand: ViewFilterOperand) => { case ViewFilterOperand.IsNot: case ViewFilterOperand.Contains: case ViewFilterOperand.DoesNotContain: - case ViewFilterOperand.GreaterThan: - case ViewFilterOperand.LessThan: + case ViewFilterOperand.GreaterThanOrEqual: + case ViewFilterOperand.LessThanOrEqual: case ViewFilterOperand.IsBefore: case ViewFilterOperand.IsAfter: case ViewFilterOperand.Is: diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts index 71d85e9bde1..673a5f770b1 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts @@ -85,7 +85,7 @@ describe('computeViewRecordGqlOperationFilter', () => { value: '1000', fieldMetadataId: companyMockEmployeesFieldMetadataId?.id, displayValue: '1000', - operand: ViewFilterOperand.GreaterThan, + operand: ViewFilterOperand.GreaterThanOrEqual, type: FieldMetadataType.NUMBER, label: 'Employees', }; @@ -1119,7 +1119,7 @@ describe('should work as expected for the different field types', () => { value: '1000', fieldMetadataId: companyMockEmployeesFieldMetadataId?.id, displayValue: '1000', - operand: ViewFilterOperand.GreaterThan, + operand: ViewFilterOperand.GreaterThanOrEqual, label: 'Employees', type: FieldMetadataType.NUMBER, }; @@ -1129,7 +1129,7 @@ describe('should work as expected for the different field types', () => { value: '1000', fieldMetadataId: companyMockEmployeesFieldMetadataId?.id, displayValue: '1000', - operand: ViewFilterOperand.LessThan, + operand: ViewFilterOperand.LessThanOrEqual, label: 'Employees', type: FieldMetadataType.NUMBER, }; @@ -1205,7 +1205,7 @@ describe('should work as expected for the different field types', () => { value: '1000', fieldMetadataId: companyMockARRFieldMetadataId?.id, displayValue: '1000', - operand: RecordFilterOperand.GreaterThan, + operand: RecordFilterOperand.GreaterThanOrEqual, subFieldName: 'amountMicros' satisfies Extract< keyof FieldCurrencyValue, 'amountMicros' @@ -1219,7 +1219,7 @@ describe('should work as expected for the different field types', () => { value: '1000', fieldMetadataId: companyMockARRFieldMetadataId?.id, displayValue: '1000', - operand: RecordFilterOperand.LessThan, + operand: RecordFilterOperand.LessThanOrEqual, subFieldName: 'amountMicros' satisfies Extract< keyof FieldCurrencyValue, 'amountMicros' diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter.ts index cabf6e83c1b..c12457e112a 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/compute-record-gql-operation-filter/turnRecordFilterIntoGqlOperationFilter.ts @@ -24,8 +24,8 @@ import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateIL import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { - convertGreaterThanRatingToArrayOfRatingValues, - convertLessThanRatingToArrayOfRatingValues, + convertGreaterThanOrEqualRatingToArrayOfRatingValues, + convertLessThanOrEqualRatingToArrayOfRatingValues, convertRatingToRatingValue, } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; @@ -268,18 +268,18 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ eq: convertRatingToRatingValue(parseFloat(recordFilter.value)), } as RatingFilter, }; - case RecordFilterOperand.GreaterThan: + case RecordFilterOperand.GreaterThanOrEqual: return { [correspondingFieldMetadataItem.name]: { - in: convertGreaterThanRatingToArrayOfRatingValues( + in: convertGreaterThanOrEqualRatingToArrayOfRatingValues( parseFloat(recordFilter.value), ), } as RatingFilter, }; - case RecordFilterOperand.LessThan: + case RecordFilterOperand.LessThanOrEqual: return { [correspondingFieldMetadataItem.name]: { - in: convertLessThanRatingToArrayOfRatingValues( + in: convertLessThanOrEqualRatingToArrayOfRatingValues( parseFloat(recordFilter.value), ), } as RatingFilter, @@ -291,13 +291,13 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ } case 'NUMBER': switch (recordFilter.operand) { - case RecordFilterOperand.GreaterThan: + case RecordFilterOperand.GreaterThanOrEqual: return { [correspondingFieldMetadataItem.name]: { gte: parseFloat(recordFilter.value), } as FloatFilter, }; - case RecordFilterOperand.LessThan: + case RecordFilterOperand.LessThanOrEqual: return { [correspondingFieldMetadataItem.name]: { lte: parseFloat(recordFilter.value), @@ -401,13 +401,13 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({ !isSubFieldFilter ) { switch (recordFilter.operand) { - case RecordFilterOperand.GreaterThan: + case RecordFilterOperand.GreaterThanOrEqual: return { [correspondingFieldMetadataItem.name]: { amountMicros: { gte: parseFloat(recordFilter.value) * 1000000 }, } as CurrencyFilter, }; - case RecordFilterOperand.LessThan: + case RecordFilterOperand.LessThanOrEqual: return { [correspondingFieldMetadataItem.name]: { amountMicros: { lte: parseFloat(recordFilter.value) * 1000000 }, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts index 3157083156a..82af90d3437 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/getRecordFilterOperands.ts @@ -69,13 +69,13 @@ export const FILTER_OPERANDS_MAP = { ...emptyOperands, ], CURRENCY: [ - RecordFilterOperand.GreaterThan, - RecordFilterOperand.LessThan, + RecordFilterOperand.GreaterThanOrEqual, + RecordFilterOperand.LessThanOrEqual, ...emptyOperands, ], NUMBER: [ - RecordFilterOperand.GreaterThan, - RecordFilterOperand.LessThan, + RecordFilterOperand.GreaterThanOrEqual, + RecordFilterOperand.LessThanOrEqual, ...emptyOperands, ], RAW_JSON: [ @@ -105,8 +105,8 @@ export const FILTER_OPERANDS_MAP = { ], RATING: [ RecordFilterOperand.Is, - RecordFilterOperand.GreaterThan, - RecordFilterOperand.LessThan, + RecordFilterOperand.GreaterThanOrEqual, + RecordFilterOperand.LessThanOrEqual, ...emptyOperands, ], RELATION: [...relationOperands, ...emptyOperands], @@ -139,8 +139,8 @@ export const COMPOSITE_FIELD_FILTER_OPERANDS_MAP = { ...emptyOperands, ], amountMicros: [ - RecordFilterOperand.GreaterThan, - RecordFilterOperand.LessThan, + RecordFilterOperand.GreaterThanOrEqual, + RecordFilterOperand.LessThanOrEqual, RecordFilterOperand.Is, RecordFilterOperand.IsNot, ...emptyOperands, diff --git a/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.spec.ts b/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.spec.ts index 4f01b64ce52..cecbb407a13 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.spec.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.spec.ts @@ -133,12 +133,12 @@ describe('buildValueFromFilter', () => { describe('NUMBER field type', () => { const testCases = [ { - operand: ViewFilterOperand.GreaterThan, + operand: ViewFilterOperand.GreaterThanOrEqual, value: '5', expected: 6, }, { - operand: ViewFilterOperand.LessThan, + operand: ViewFilterOperand.LessThanOrEqual, value: '5', expected: 4, }, @@ -359,12 +359,12 @@ describe('buildValueFromFilter', () => { expected: undefined, }, { - operand: ViewFilterOperand.GreaterThan, + operand: ViewFilterOperand.GreaterThanOrEqual, value: 'Rating 1', expected: 'RATING_2', }, { - operand: ViewFilterOperand.LessThan, + operand: ViewFilterOperand.LessThanOrEqual, value: 'Rating 2', expected: 'RATING_1', }, diff --git a/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.ts b/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.ts index 24d814a2b89..92461758620 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/utils/buildRecordInputFromFilter.ts @@ -150,9 +150,10 @@ const computeValueFromFilterNumber = ( value: string, ) => { switch (operand) { - case ViewFilterOperand.GreaterThan: + //TODO: we shouln't create values from those filters as it makes no sense for the user + case ViewFilterOperand.GreaterThanOrEqual: return Number(value) + 1; - case ViewFilterOperand.LessThan: + case ViewFilterOperand.LessThanOrEqual: return Number(value) - 1; case ViewFilterOperand.IsNotEmpty: return Number(value); @@ -205,13 +206,13 @@ const computeValueFromFilterRating = ( case ViewFilterOperand.Is: case ViewFilterOperand.IsNotEmpty: return option.value; - case ViewFilterOperand.GreaterThan: { + case ViewFilterOperand.GreaterThanOrEqual: { const plusOne = options?.find( (opt) => opt.position === option.position + 1, )?.value; return plusOne ? plusOne : option.value; } - case ViewFilterOperand.LessThan: { + case ViewFilterOperand.LessThanOrEqual: { const minusOne = options?.find( (opt) => opt.position === option.position - 1, )?.value; diff --git a/packages/twenty-front/src/modules/views/types/ViewFilterOperand.ts b/packages/twenty-front/src/modules/views/types/ViewFilterOperand.ts index 7ea686348ba..e6770027109 100644 --- a/packages/twenty-front/src/modules/views/types/ViewFilterOperand.ts +++ b/packages/twenty-front/src/modules/views/types/ViewFilterOperand.ts @@ -2,8 +2,8 @@ export enum ViewFilterOperand { Is = 'is', IsNotNull = 'isNotNull', IsNot = 'isNot', - LessThan = 'lessThan', - GreaterThan = 'greaterThan', + LessThanOrEqual = 'lessThan', // TODO: we could change this to 'lessThanOrEqual' for consistency but it would require a migration + GreaterThanOrEqual = 'greaterThan', // TODO: we could change this to 'greaterThanOrEqual' for consistency but it would require a migration IsBefore = 'isBefore', IsAfter = 'isAfter', Contains = 'contains',