diff --git a/packages/twenty-front/src/modules/object-record/record-group/utils/__tests__/computeRecordGroupOptionsFilter.test.ts b/packages/twenty-front/src/modules/object-record/record-group/utils/__tests__/computeRecordGroupOptionsFilter.test.ts new file mode 100644 index 0000000000..1bd1193f95 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/utils/__tests__/computeRecordGroupOptionsFilter.test.ts @@ -0,0 +1,54 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { computeRecordGroupOptionsFilter } from '@/object-record/record-group/utils/computeRecordGroupOptionsFilter'; + +const mockFieldMetadata = { + id: 'field-1', + name: 'status', +} as FieldMetadataItem; + +describe('computeRecordGroupOptionsFilter', () => { + it('should return empty object when recordGroupFieldMetadata is undefined', () => { + const result = computeRecordGroupOptionsFilter({ + recordGroupFieldMetadata: undefined, + recordGroupValues: ['value1', 'value2'], + }); + + expect(result).toEqual({}); + }); + + it('should return empty object when recordGroupFieldMetadata is null', () => { + const result = computeRecordGroupOptionsFilter({ + recordGroupFieldMetadata: null, + recordGroupValues: ['value1', 'value2'], + }); + + expect(result).toEqual({}); + }); + + it('should return simple IN filter when no null values present', () => { + const result = computeRecordGroupOptionsFilter({ + recordGroupFieldMetadata: mockFieldMetadata, + recordGroupValues: ['value1', 'value2', 'value3'], + }); + + expect(result).toEqual({ + status: { + in: ['value1', 'value2', 'value3'], + }, + }); + }); + + it('should return OR filter with IS NULL when null value is present', () => { + const result = computeRecordGroupOptionsFilter({ + recordGroupFieldMetadata: mockFieldMetadata, + recordGroupValues: ['value1', null, 'value2'], + }); + + expect(result).toEqual({ + or: [ + { status: { is: 'NULL' } }, + { status: { in: ['value1', 'value2'] } }, + ], + }); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/record-group/utils/computeRecordGroupOptionsFilter.ts b/packages/twenty-front/src/modules/object-record/record-group/utils/computeRecordGroupOptionsFilter.ts new file mode 100644 index 0000000000..0fda029544 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/utils/computeRecordGroupOptionsFilter.ts @@ -0,0 +1,39 @@ +import { isNull } from '@sniptt/guards'; +import { type RecordGqlOperationFilter } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; + +export const computeRecordGroupOptionsFilter = ({ + recordGroupFieldMetadata, + recordGroupValues, +}: { + recordGroupFieldMetadata: FieldMetadataItem | null | undefined; + recordGroupValues: RecordGroupDefinition['value'][]; +}): RecordGqlOperationFilter => { + if (!isDefined(recordGroupFieldMetadata) || recordGroupValues.length === 0) { + return {}; + } + + const fieldName = recordGroupFieldMetadata.name; + const hasNullValue = recordGroupValues.some(isNull); + const nonNullValues = recordGroupValues.filter( + (value): value is NonNullable => !isNull(value), + ); + + return hasNullValue + ? { + or: [ + { [fieldName]: { is: 'NULL' } }, + ...(nonNullValues.length > 0 + ? [{ [fieldName]: { in: nonNullValues } }] + : []), + ], + } + : nonNullValues.length > 0 + ? { + [fieldName]: { in: recordGroupValues }, + } + : {}; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables.ts index 3c1c509632..2ab4eca048 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables.ts @@ -5,6 +5,7 @@ import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/ import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector'; +import { computeRecordGroupOptionsFilter } from '@/object-record/record-group/utils/computeRecordGroupOptionsFilter'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; @@ -12,7 +13,6 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho import { combineFilters, computeRecordGqlOperationFilter, - isDefined, turnAnyFieldFilterIntoRecordGqlFilter, } from 'twenty-shared/utils'; @@ -73,13 +73,10 @@ export const useRecordIndexGroupCommonQueryVariables = () => { (recordGroupDefinition) => recordGroupDefinition.value, ); - const recordGroupOptionsFilter = isDefined(recordGroupFieldMetadata) - ? { - [recordGroupFieldMetadata.name]: { - in: recordGroupValues, - }, - } - : {}; + const recordGroupOptionsFilter = computeRecordGroupOptionsFilter({ + recordGroupFieldMetadata, + recordGroupValues, + }); const combinedFilters = combineFilters([ anyFieldFilter,