From 0df07a766a188178a06f3ee44b59585b9942f883 Mon Sep 17 00:00:00 2001
From: Marie <51697796+ijreilly@users.noreply.github.com>
Date: Thu, 3 Apr 2025 10:41:50 +0200
Subject: [PATCH] Handle no value options in filters (#11351)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fixes #11323 - see description to reproduce
The issue was that the filter was malformed for no value options:
causing
after fix:
---
.../favorites/hooks/__mocks__/useFavorites.ts | 2 +
...ColumnDefinitionsFromFieldMetadata.test.ts | 2 +-
.../hooks/__mocks__/personFragments.ts | 1 +
.../__tests__/useToggleEditOnlyInput.test.tsx | 1 +
...omputeViewRecordGqlOperationFilter.test.ts | 152 ++++++++++++++++++
.../computeViewRecordGqlOperationFilter.ts | 91 ++++++++---
...jectRecordsSpreadsheetImportDialog.test.ts | 1 +
.../generated/mock-metadata-query-result.ts | 36 +++++
8 files changed, 266 insertions(+), 20 deletions(-)
diff --git a/packages/twenty-front/src/modules/favorites/hooks/__mocks__/useFavorites.ts b/packages/twenty-front/src/modules/favorites/hooks/__mocks__/useFavorites.ts
index 99c18e06b..f5e09ae32 100644
--- a/packages/twenty-front/src/modules/favorites/hooks/__mocks__/useFavorites.ts
+++ b/packages/twenty-front/src/modules/favorites/hooks/__mocks__/useFavorites.ts
@@ -141,6 +141,7 @@ mutation UpdateOneFavorite(
employees
id
idealCustomerProfile
+ internalCompetitions
introVideo {
primaryLinkUrl
primaryLinkLabel
@@ -510,6 +511,7 @@ export const mocks = [
employees
id
idealCustomerProfile
+ internalCompetitions
introVideo {
primaryLinkUrl
primaryLinkLabel
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts
index 1edaf1d60..ea7148b4b 100644
--- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts
@@ -67,6 +67,6 @@ describe('useColumnDefinitionsFromFieldMetadata', () => {
const { columnDefinitions } = result.current;
- expect(columnDefinitions.length).toBe(21);
+ expect(columnDefinitions.length).toBe(22);
});
});
diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragments.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragments.ts
index c392b1d3b..7d57d7b94 100644
--- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragments.ts
+++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragments.ts
@@ -127,6 +127,7 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
employees
id
idealCustomerProfile
+ internalCompetitions
introVideo {
primaryLinkUrl
primaryLinkLabel
diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx
index 1f8be4cad..824618748 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx
@@ -124,6 +124,7 @@ const mocks: MockedResponse[] = [
}
id
idealCustomerProfile
+ internalCompetitions
introVideo {
primaryLinkUrl
primaryLinkLabel
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 484e078d5..41ac1a722 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
@@ -933,4 +933,156 @@ describe('should work as expected for the different field types', () => {
],
});
});
+
+ it('select field type with empty options', () => {
+ const selectFieldMetadata = companyMockObjectMetadataItem.fields.find(
+ (field) => field.type === FieldMetadataType.SELECT,
+ );
+
+ if (!selectFieldMetadata) {
+ throw new Error(
+ `Select field metadata not found ${companyMockObjectMetadataItem.fields.map((field) => [field.name, field.type])}`,
+ );
+ }
+
+ const selectFilterIs: RecordFilter = {
+ id: 'company-select-filter-is',
+ value: '["option1",""]',
+ fieldMetadataId: selectFieldMetadata?.id,
+ displayValue: '["option1",""]',
+ operand: ViewFilterOperand.Is,
+ label: 'Select',
+ type: FieldMetadataType.SELECT,
+ };
+
+ const selectFilterIsNot: RecordFilter = {
+ id: 'company-select-filter-is-not',
+ value: '["option1",""]',
+ fieldMetadataId: selectFieldMetadata.id,
+ displayValue: '["option1",""]',
+ operand: ViewFilterOperand.IsNot,
+ label: 'Select',
+ type: FieldMetadataType.SELECT,
+ };
+
+ const result = computeRecordGqlOperationFilter({
+ filterValueDependencies: mockFilterValueDependencies,
+ recordFilters: [selectFilterIs, selectFilterIsNot],
+ recordFilterGroups: [],
+ fields: companyMockObjectMetadataItem.fields,
+ });
+
+ expect(result).toEqual({
+ and: [
+ {
+ or: [
+ {
+ [selectFieldMetadata.name]: {
+ in: ['option1'],
+ },
+ },
+ {
+ [selectFieldMetadata.name]: {
+ is: 'NULL',
+ },
+ },
+ ],
+ },
+ {
+ and: [
+ {
+ not: {
+ [selectFieldMetadata.name]: {
+ in: ['option1'],
+ },
+ },
+ },
+ {
+ not: {
+ [selectFieldMetadata.name]: {
+ is: 'NULL',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ });
+ });
+
+ it('multi-select field type with empty options', () => {
+ const multiSelectFieldMetadata = companyMockObjectMetadataItem.fields.find(
+ (field) => field.type === FieldMetadataType.MULTI_SELECT,
+ )!;
+
+ const multiSelectFilterContains: RecordFilter = {
+ id: 'company-multi-select-filter-contains',
+ value: '["option1",""]',
+ fieldMetadataId: multiSelectFieldMetadata.id,
+ displayValue: '["option1",""]',
+ operand: ViewFilterOperand.Contains,
+ label: 'MultiSelect',
+ type: FieldMetadataType.MULTI_SELECT,
+ };
+
+ const multiSelectFilterDoesNotContain: RecordFilter = {
+ id: 'company-multi-select-filter-does-not-contain',
+ value: '["option1",""]',
+ fieldMetadataId: multiSelectFieldMetadata.id,
+ displayValue: '["option1",""]',
+ operand: ViewFilterOperand.DoesNotContain,
+ label: 'MultiSelect',
+ type: FieldMetadataType.MULTI_SELECT,
+ };
+
+ const result = computeRecordGqlOperationFilter({
+ filterValueDependencies: mockFilterValueDependencies,
+ recordFilters: [
+ multiSelectFilterContains,
+ multiSelectFilterDoesNotContain,
+ ],
+ recordFilterGroups: [],
+ fields: companyMockObjectMetadataItem.fields,
+ });
+
+ expect(result).toEqual({
+ and: [
+ {
+ or: [
+ {
+ [multiSelectFieldMetadata.name]: {
+ containsAny: ['option1'],
+ },
+ },
+ {
+ [multiSelectFieldMetadata.name]: {
+ isEmptyArray: true,
+ },
+ },
+ ],
+ },
+ {
+ or: [
+ {
+ not: {
+ [multiSelectFieldMetadata.name]: {
+ containsAny: ['option1'],
+ },
+ },
+ },
+ {
+ [multiSelectFieldMetadata.name]: {
+ isEmptyArray: true,
+ },
+ },
+ {
+ [multiSelectFieldMetadata.name]: {
+ is: 'NULL',
+ },
+ },
+ ],
+ },
+ ],
+ });
+ });
});
diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts
index c7e28c0a8..af8a06b59 100644
--- a/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts
+++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts
@@ -584,20 +584,38 @@ export const computeFilterRecordGqlOperationFilter = ({
if (options.length === 0) return;
+ const emptyOptions = options.filter((option: string) => option === '');
+ const nonEmptyOptions = options.filter((option: string) => option !== '');
+
switch (filter.operand) {
- case RecordFilterOperand.Contains:
- return {
- [correspondingField.name]: {
- containsAny: options,
- } as MultiSelectFilter,
- };
+ case RecordFilterOperand.Contains: {
+ const conditions = [];
+
+ if (nonEmptyOptions.length > 0) {
+ conditions.push({
+ [correspondingField.name]: {
+ containsAny: nonEmptyOptions,
+ } as MultiSelectFilter,
+ });
+ }
+
+ if (emptyOptions.length > 0) {
+ conditions.push({
+ [correspondingField.name]: {
+ isEmptyArray: true,
+ } as MultiSelectFilter,
+ });
+ }
+
+ return conditions.length === 1 ? conditions[0] : { or: conditions };
+ }
case RecordFilterOperand.DoesNotContain:
return {
or: [
{
not: {
[correspondingField.name]: {
- containsAny: options,
+ containsAny: nonEmptyOptions,
} as MultiSelectFilter,
},
},
@@ -624,21 +642,56 @@ export const computeFilterRecordGqlOperationFilter = ({
if (options.length === 0) return;
+ const emptyOptions = options.filter((option: string) => option === '');
+ const nonEmptyOptions = options.filter((option: string) => option !== '');
+
switch (filter.operand) {
- case RecordFilterOperand.Is:
- return {
- [correspondingField.name]: {
- in: options,
- } as SelectFilter,
- };
- case RecordFilterOperand.IsNot:
- return {
- not: {
+ case RecordFilterOperand.Is: {
+ const conditions = [];
+
+ if (nonEmptyOptions.length > 0) {
+ conditions.push({
[correspondingField.name]: {
- in: options,
+ in: nonEmptyOptions,
} as SelectFilter,
- },
- };
+ });
+ }
+
+ if (emptyOptions.length > 0) {
+ conditions.push({
+ [correspondingField.name]: {
+ is: 'NULL',
+ } as SelectFilter,
+ });
+ }
+
+ return conditions.length === 1 ? conditions[0] : { or: conditions };
+ }
+ case RecordFilterOperand.IsNot: {
+ const conditions = [];
+
+ if (nonEmptyOptions.length > 0) {
+ conditions.push({
+ not: {
+ [correspondingField.name]: {
+ in: nonEmptyOptions,
+ } as SelectFilter,
+ },
+ });
+ }
+
+ if (emptyOptions.length > 0) {
+ conditions.push({
+ not: {
+ [correspondingField.name]: {
+ is: 'NULL',
+ } as SelectFilter,
+ },
+ });
+ }
+
+ return conditions.length === 1 ? conditions[0] : { and: conditions };
+ }
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filterType} filter`,
diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreadsheetImportDialog.test.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreadsheetImportDialog.test.ts
index dbdad7c09..c53074a51 100644
--- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreadsheetImportDialog.test.ts
+++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreadsheetImportDialog.test.ts
@@ -124,6 +124,7 @@ const companyMocks = [
}
id
idealCustomerProfile
+ internalCompetitions
introVideo {
primaryLinkUrl
primaryLinkLabel
diff --git a/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts b/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts
index 00a9abbac..fe2659209 100644
--- a/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts
+++ b/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts
@@ -3876,6 +3876,42 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
label: "Work Policy",
description: "Company's Work Policy"
},
+ {
+ __typename: "Field",
+ id: "c6dfcc2d-dc84-4553-94df-3b75cccee53c",
+ type: "SELECT",
+ name: "internalCompetitions",
+ icon: "IconHome",
+ isCustom: true,
+ isActive: true,
+ isSystem: false,
+ isNullable: true,
+ isUnique: false,
+ createdAt: "2025-02-11T09:14:38.892Z",
+ updatedAt: "2025-02-11T09:14:38.892Z",
+ defaultValue: null,
+ options: [
+ {
+ id: "ee1b741b-0359-4ffd-b866-506e7b9c0cd9",
+ color: "green",
+ label: "Best employy",
+ value: "BEST_EMPLOYEE",
+ position: 0
+ },
+ {
+ id: "3b2ed882-ec07-43fd-96e6-0fca8669c1f5",
+ color: "turquoise",
+ label: "Ultimate debugger",
+ value: "ULTIMATE_DEBUGGER",
+ position: 1
+ },
+ ],
+ settings: null,
+ isLabelSyncedWithName: false,
+ relationDefinition: null,
+ label: "Internal competitions",
+ description: "Internal competitions"
+ },
{
__typename: "Field",
id: "8b1b88c0-a802-4c5d-8632-a4e343f3c8f1",