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