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 5df28eb2e9..295f62142d 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 1c9db8ef4e..a078dc8d9b 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 a20f31ee5a..2f2a96171f 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 a97e2e8ff3..7f4a245d13 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 62246c7365..7f1e8b26b4 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 1b85debc73..aaafc9e8dc 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 1e0a25190f..4f63f9252c 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 668742946d..cdf0623b70 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 71d85e9bde..673a5f770b 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 cabf6e83c1..c12457e112 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 3157083156..82af90d343 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 4f01b64ce5..cecbb407a1 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 24d814a2b8..9246175862 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 7ea686348b..e677002710 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',