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:
@ -141,6 +141,7 @@ mutation UpdateOneFavorite(
|
|||||||
employees
|
employees
|
||||||
id
|
id
|
||||||
idealCustomerProfile
|
idealCustomerProfile
|
||||||
|
internalCompetitions
|
||||||
introVideo {
|
introVideo {
|
||||||
primaryLinkUrl
|
primaryLinkUrl
|
||||||
primaryLinkLabel
|
primaryLinkLabel
|
||||||
@ -510,6 +511,7 @@ export const mocks = [
|
|||||||
employees
|
employees
|
||||||
id
|
id
|
||||||
idealCustomerProfile
|
idealCustomerProfile
|
||||||
|
internalCompetitions
|
||||||
introVideo {
|
introVideo {
|
||||||
primaryLinkUrl
|
primaryLinkUrl
|
||||||
primaryLinkLabel
|
primaryLinkLabel
|
||||||
|
|||||||
@ -67,6 +67,6 @@ describe('useColumnDefinitionsFromFieldMetadata', () => {
|
|||||||
|
|
||||||
const { columnDefinitions } = result.current;
|
const { columnDefinitions } = result.current;
|
||||||
|
|
||||||
expect(columnDefinitions.length).toBe(21);
|
expect(columnDefinitions.length).toBe(22);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -127,6 +127,7 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
|
|||||||
employees
|
employees
|
||||||
id
|
id
|
||||||
idealCustomerProfile
|
idealCustomerProfile
|
||||||
|
internalCompetitions
|
||||||
introVideo {
|
introVideo {
|
||||||
primaryLinkUrl
|
primaryLinkUrl
|
||||||
primaryLinkLabel
|
primaryLinkLabel
|
||||||
|
|||||||
@ -124,6 +124,7 @@ const mocks: MockedResponse[] = [
|
|||||||
}
|
}
|
||||||
id
|
id
|
||||||
idealCustomerProfile
|
idealCustomerProfile
|
||||||
|
internalCompetitions
|
||||||
introVideo {
|
introVideo {
|
||||||
primaryLinkUrl
|
primaryLinkUrl
|
||||||
primaryLinkLabel
|
primaryLinkLabel
|
||||||
|
|||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -584,20 +584,38 @@ export const computeFilterRecordGqlOperationFilter = ({
|
|||||||
|
|
||||||
if (options.length === 0) return;
|
if (options.length === 0) return;
|
||||||
|
|
||||||
|
const emptyOptions = options.filter((option: string) => option === '');
|
||||||
|
const nonEmptyOptions = options.filter((option: string) => option !== '');
|
||||||
|
|
||||||
switch (filter.operand) {
|
switch (filter.operand) {
|
||||||
case RecordFilterOperand.Contains:
|
case RecordFilterOperand.Contains: {
|
||||||
return {
|
const conditions = [];
|
||||||
[correspondingField.name]: {
|
|
||||||
containsAny: options,
|
if (nonEmptyOptions.length > 0) {
|
||||||
} as MultiSelectFilter,
|
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:
|
case RecordFilterOperand.DoesNotContain:
|
||||||
return {
|
return {
|
||||||
or: [
|
or: [
|
||||||
{
|
{
|
||||||
not: {
|
not: {
|
||||||
[correspondingField.name]: {
|
[correspondingField.name]: {
|
||||||
containsAny: options,
|
containsAny: nonEmptyOptions,
|
||||||
} as MultiSelectFilter,
|
} as MultiSelectFilter,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -624,21 +642,56 @@ export const computeFilterRecordGqlOperationFilter = ({
|
|||||||
|
|
||||||
if (options.length === 0) return;
|
if (options.length === 0) return;
|
||||||
|
|
||||||
|
const emptyOptions = options.filter((option: string) => option === '');
|
||||||
|
const nonEmptyOptions = options.filter((option: string) => option !== '');
|
||||||
|
|
||||||
switch (filter.operand) {
|
switch (filter.operand) {
|
||||||
case RecordFilterOperand.Is:
|
case RecordFilterOperand.Is: {
|
||||||
return {
|
const conditions = [];
|
||||||
[correspondingField.name]: {
|
|
||||||
in: options,
|
if (nonEmptyOptions.length > 0) {
|
||||||
} as SelectFilter,
|
conditions.push({
|
||||||
};
|
|
||||||
case RecordFilterOperand.IsNot:
|
|
||||||
return {
|
|
||||||
not: {
|
|
||||||
[correspondingField.name]: {
|
[correspondingField.name]: {
|
||||||
in: options,
|
in: nonEmptyOptions,
|
||||||
} as SelectFilter,
|
} 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:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown operand ${filter.operand} for ${filterType} filter`,
|
`Unknown operand ${filter.operand} for ${filterType} filter`,
|
||||||
|
|||||||
@ -124,6 +124,7 @@ const companyMocks = [
|
|||||||
}
|
}
|
||||||
id
|
id
|
||||||
idealCustomerProfile
|
idealCustomerProfile
|
||||||
|
internalCompetitions
|
||||||
introVideo {
|
introVideo {
|
||||||
primaryLinkUrl
|
primaryLinkUrl
|
||||||
primaryLinkLabel
|
primaryLinkLabel
|
||||||
|
|||||||
@ -3876,6 +3876,42 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
label: "Work Policy",
|
label: "Work Policy",
|
||||||
description: "Company's 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",
|
__typename: "Field",
|
||||||
id: "8b1b88c0-a802-4c5d-8632-a4e343f3c8f1",
|
id: "8b1b88c0-a802-4c5d-8632-a4e343f3c8f1",
|
||||||
|
|||||||
Reference in New Issue
Block a user