Handle no value options in filters (#11351)

Fixes #11323 - see description to reproduce

The issue was that the filter was malformed for no value options: 
<img width="649" alt="Capture d’écran 2025-04-02 à 14 56 25"
src="https://github.com/user-attachments/assets/5287c4f8-7eaf-4488-b692-4d7634236d3d"
/>
causing
<img width="333" alt="Capture d’écran 2025-04-02 à 14 56 43"
src="https://github.com/user-attachments/assets/aa1b7333-50da-4b7d-979b-70dab9a1ab41"
/>


after fix: 
<img width="653" alt="Capture d’écran 2025-04-02 à 14 39 56"
src="https://github.com/user-attachments/assets/1777c068-7231-4e14-bc41-84ef7909cf10"
/>
This commit is contained in:
Marie
2025-04-03 10:41:50 +02:00
committed by GitHub
parent 256a5c1a2b
commit 0df07a766a
8 changed files with 266 additions and 20 deletions

View File

@ -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

View File

@ -67,6 +67,6 @@ describe('useColumnDefinitionsFromFieldMetadata', () => {
const { columnDefinitions } = result.current;
expect(columnDefinitions.length).toBe(21);
expect(columnDefinitions.length).toBe(22);
});
});

View File

@ -127,6 +127,7 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
employees
id
idealCustomerProfile
internalCompetitions
introVideo {
primaryLinkUrl
primaryLinkLabel

View File

@ -124,6 +124,7 @@ const mocks: MockedResponse[] = [
}
id
idealCustomerProfile
internalCompetitions
introVideo {
primaryLinkUrl
primaryLinkLabel

View File

@ -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',
},
},
],
},
],
});
});
});

View File

@ -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`,

View File

@ -124,6 +124,7 @@ const companyMocks = [
}
id
idealCustomerProfile
internalCompetitions
introVideo {
primaryLinkUrl
primaryLinkLabel

View File

@ -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",