Implemented PHONES sub-field filtering (#11953)
This PR implements sub-field filtering for the PHONES field type. What was tricky was to have filtering work correctly on the additionalPhones sub-field, which is an array of objects and is treated as a RawJsonFilter. Now that it works for this sub-field, we can implement the same logic for other similar sub-field like additionalEmails and secondaryLinks.
This commit is contained in:
@ -83,7 +83,7 @@ export const AdvancedFilterDropdownFilterInput = ({
|
||||
recordFilter.subFieldName,
|
||||
) ? (
|
||||
<>
|
||||
<ObjectFilterDropdownCurrencySelect dropdownWidth={280} />
|
||||
<ObjectFilterDropdownCurrencySelect />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
|
||||
@ -104,6 +104,8 @@ export type EmailsFilter = {
|
||||
|
||||
export type PhonesFilter = {
|
||||
primaryPhoneNumber?: StringFilter;
|
||||
primaryPhoneCallingCode?: StringFilter;
|
||||
additionalPhones?: RawJsonFilter;
|
||||
};
|
||||
|
||||
export type SelectFilter = {
|
||||
|
||||
@ -18,13 +18,7 @@ import { MenuItem, MenuItemMultiSelectAvatar } from 'twenty-ui/navigation';
|
||||
export const EMPTY_FILTER_VALUE = '[]';
|
||||
export const MAX_ITEMS_TO_DISPLAY = 3;
|
||||
|
||||
type ObjectFilterDropdownCurrencySelectProps = {
|
||||
dropdownWidth?: number;
|
||||
};
|
||||
|
||||
export const ObjectFilterDropdownCurrencySelect = ({
|
||||
dropdownWidth,
|
||||
}: ObjectFilterDropdownCurrencySelectProps) => {
|
||||
export const ObjectFilterDropdownCurrencySelect = () => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const objectFilterDropdownCurrentRecordFilter = useRecoilComponentValueV2(
|
||||
@ -112,7 +106,7 @@ export const ObjectFilterDropdownCurrencySelect = ({
|
||||
}}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight width={dropdownWidth ?? 200}>
|
||||
<DropdownMenuItemsContainer hasMaxHeight width="auto">
|
||||
{filteredSelectedItems?.map((item) => {
|
||||
return (
|
||||
<MenuItemMultiSelectAvatar
|
||||
|
||||
@ -640,6 +640,20 @@ describe('should work as expected for the different field types', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
phones: {
|
||||
primaryPhoneCallingCode: {
|
||||
ilike: '%1234567890%',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
phones: {
|
||||
additionalPhones: {
|
||||
like: '%1234567890%',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -653,6 +667,35 @@ describe('should work as expected for the different field types', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
not: {
|
||||
phones: {
|
||||
primaryPhoneCallingCode: {
|
||||
ilike: '%1234567890%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
phones: {
|
||||
additionalPhones: {
|
||||
like: `%1234567890%`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
phones: {
|
||||
additionalPhones: {
|
||||
is: 'NULL',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -661,16 +704,40 @@ describe('should work as expected for the different field types', () => {
|
||||
or: [
|
||||
{
|
||||
phones: {
|
||||
primaryPhoneNumber: {
|
||||
is: 'NULL',
|
||||
},
|
||||
primaryPhoneNumber: { is: 'NULL' },
|
||||
},
|
||||
},
|
||||
{
|
||||
phones: {
|
||||
primaryPhoneNumber: {
|
||||
ilike: '',
|
||||
},
|
||||
primaryPhoneNumber: { ilike: '' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
or: [
|
||||
{
|
||||
phones: {
|
||||
primaryPhoneCallingCode: { is: 'NULL' },
|
||||
},
|
||||
},
|
||||
{
|
||||
phones: {
|
||||
primaryPhoneCallingCode: { ilike: '' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
or: [
|
||||
{
|
||||
phones: {
|
||||
additionalPhones: { is: 'NULL' },
|
||||
},
|
||||
},
|
||||
{
|
||||
phones: {
|
||||
additionalPhones: { like: '[]' },
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -684,16 +751,40 @@ describe('should work as expected for the different field types', () => {
|
||||
or: [
|
||||
{
|
||||
phones: {
|
||||
primaryPhoneNumber: {
|
||||
is: 'NULL',
|
||||
},
|
||||
primaryPhoneNumber: { is: 'NULL' },
|
||||
},
|
||||
},
|
||||
{
|
||||
phones: {
|
||||
primaryPhoneNumber: {
|
||||
ilike: '',
|
||||
},
|
||||
primaryPhoneNumber: { ilike: '' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
or: [
|
||||
{
|
||||
phones: {
|
||||
primaryPhoneCallingCode: { is: 'NULL' },
|
||||
},
|
||||
},
|
||||
{
|
||||
phones: {
|
||||
primaryPhoneCallingCode: { ilike: '' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
or: [
|
||||
{
|
||||
phones: {
|
||||
additionalPhones: { is: 'NULL' },
|
||||
},
|
||||
},
|
||||
{
|
||||
phones: {
|
||||
additionalPhones: { like: '[]' },
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -5,6 +5,7 @@ const COMPOSITE_TYPES_FILTERABLE = [
|
||||
'FULL_NAME',
|
||||
'CURRENCY',
|
||||
'ADDRESS',
|
||||
'PHONES',
|
||||
] satisfies FieldType[];
|
||||
|
||||
type FilterableCompositeFieldType = (typeof COMPOSITE_TYPES_FILTERABLE)[number];
|
||||
|
||||
@ -1030,25 +1030,146 @@ export const computeFilterRecordGqlOperationFilter = ({
|
||||
);
|
||||
}
|
||||
case 'PHONES': {
|
||||
const filterValue = filter.value.replace(/[^0-9]/g, '');
|
||||
if (!isSubFieldFilter) {
|
||||
const filterValue = filter.value.replace(/[^0-9]/g, '');
|
||||
|
||||
switch (filter.operand) {
|
||||
case RecordFilterOperand.Contains:
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
if (!isNonEmptyString(filterValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (filter.operand) {
|
||||
case RecordFilterOperand.Contains:
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
primaryPhoneNumber: {
|
||||
ilike: `%${filterValue}%`,
|
||||
},
|
||||
} as PhonesFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
primaryPhoneCallingCode: {
|
||||
ilike: `%${filterValue}%`,
|
||||
},
|
||||
} as PhonesFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
additionalPhones: {
|
||||
like: `%${filterValue}%`,
|
||||
},
|
||||
} as PhonesFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
case RecordFilterOperand.DoesNotContain:
|
||||
return {
|
||||
and: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
primaryPhoneNumber: {
|
||||
ilike: `%${filterValue}%`,
|
||||
},
|
||||
} as PhonesFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
primaryPhoneCallingCode: {
|
||||
ilike: `%${filterValue}%`,
|
||||
},
|
||||
} as PhonesFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
additionalPhones: {
|
||||
like: `%${filterValue}%`,
|
||||
},
|
||||
} as PhonesFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
additionalPhones: {
|
||||
is: 'NULL',
|
||||
} as PhonesFilter,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filterType} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const filterValue = filter.value;
|
||||
|
||||
switch (subFieldName) {
|
||||
case 'additionalPhones': {
|
||||
switch (filter.operand) {
|
||||
case RecordFilterOperand.Contains:
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
additionalPhones: {
|
||||
like: `%${filterValue}%`,
|
||||
},
|
||||
} as PhonesFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
case RecordFilterOperand.DoesNotContain:
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
additionalPhones: {
|
||||
like: `%${filterValue}%`,
|
||||
},
|
||||
} as PhonesFilter,
|
||||
},
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
additionalPhones: {
|
||||
is: 'NULL',
|
||||
} as PhonesFilter,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filterType} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
case 'primaryPhoneNumber': {
|
||||
switch (filter.operand) {
|
||||
case RecordFilterOperand.Contains:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
primaryPhoneNumber: {
|
||||
ilike: `%${filterValue}%`,
|
||||
},
|
||||
} as PhonesFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
case RecordFilterOperand.DoesNotContain:
|
||||
return {
|
||||
and: [
|
||||
{
|
||||
};
|
||||
case RecordFilterOperand.DoesNotContain:
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
primaryPhoneNumber: {
|
||||
@ -1056,12 +1177,42 @@ export const computeFilterRecordGqlOperationFilter = ({
|
||||
},
|
||||
} as PhonesFilter,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filterType} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
case 'primaryPhoneCallingCode': {
|
||||
switch (filter.operand) {
|
||||
case RecordFilterOperand.Contains:
|
||||
return {
|
||||
[correspondingField.name]: {
|
||||
primaryPhoneCallingCode: {
|
||||
ilike: `%${filterValue}%`,
|
||||
},
|
||||
} as PhonesFilter,
|
||||
};
|
||||
case RecordFilterOperand.DoesNotContain:
|
||||
return {
|
||||
not: {
|
||||
[correspondingField.name]: {
|
||||
primaryPhoneCallingCode: {
|
||||
ilike: `%${filterValue}%`,
|
||||
},
|
||||
} as PhonesFilter,
|
||||
},
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filterType} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filterType} filter`,
|
||||
`Unknown subfield ${subFieldName} for ${filterType} filter`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
EmailsFilter,
|
||||
FloatFilter,
|
||||
MultiSelectFilter,
|
||||
PhonesFilter,
|
||||
RatingFilter,
|
||||
RawJsonFilter,
|
||||
RecordGqlOperationFilter,
|
||||
@ -37,7 +38,7 @@ export const getEmptyRecordGqlOperationFilter = ({
|
||||
|
||||
const compositeFieldName = recordFilter.subFieldName;
|
||||
|
||||
const isCompositeField = isNonEmptyString(compositeFieldName);
|
||||
const isSubFieldFilter = isNonEmptyString(compositeFieldName);
|
||||
|
||||
const filterType = getFilterTypeFromFieldType(correspondingField.type);
|
||||
|
||||
@ -51,35 +52,98 @@ export const getEmptyRecordGqlOperationFilter = ({
|
||||
};
|
||||
break;
|
||||
case 'PHONES': {
|
||||
if (!isCompositeField) {
|
||||
const phonesFilter = generateILikeFiltersForCompositeFields(
|
||||
'',
|
||||
correspondingField.name,
|
||||
['primaryPhoneNumber'],
|
||||
true,
|
||||
);
|
||||
|
||||
if (!isSubFieldFilter) {
|
||||
emptyRecordFilter = {
|
||||
and: phonesFilter,
|
||||
};
|
||||
break;
|
||||
} else {
|
||||
emptyRecordFilter = {
|
||||
or: [
|
||||
and: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
[compositeFieldName]: { ilike: '' },
|
||||
} as StringFilter,
|
||||
or: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
primaryPhoneNumber: { is: 'NULL' },
|
||||
} as PhonesFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
primaryPhoneNumber: { ilike: '' },
|
||||
} as PhonesFilter,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
[compositeFieldName]: { is: 'NULL' },
|
||||
} as StringFilter,
|
||||
or: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
primaryPhoneCallingCode: { is: 'NULL' },
|
||||
} as PhonesFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
primaryPhoneCallingCode: { ilike: '' },
|
||||
} as PhonesFilter,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
or: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
additionalPhones: { is: 'NULL' },
|
||||
} as PhonesFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
additionalPhones: { like: '[]' },
|
||||
} as PhonesFilter,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
break;
|
||||
} else {
|
||||
switch (compositeFieldName) {
|
||||
case 'primaryPhoneNumber':
|
||||
case 'primaryPhoneCallingCode': {
|
||||
emptyRecordFilter = {
|
||||
or: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
[compositeFieldName]: { is: 'NULL' },
|
||||
} as PhonesFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
[compositeFieldName]: { ilike: '' },
|
||||
} as PhonesFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'additionalPhones': {
|
||||
emptyRecordFilter = {
|
||||
or: [
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
additionalPhones: { is: 'NULL' },
|
||||
} as PhonesFilter,
|
||||
},
|
||||
{
|
||||
[correspondingField.name]: {
|
||||
additionalPhones: { like: '[]' },
|
||||
} as PhonesFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Unsupported composite field name ${compositeFieldName} for filter type ${filterType}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CURRENCY':
|
||||
emptyRecordFilter = {
|
||||
@ -93,7 +157,7 @@ export const getEmptyRecordGqlOperationFilter = ({
|
||||
};
|
||||
break;
|
||||
case 'FULL_NAME': {
|
||||
if (!isCompositeField) {
|
||||
if (!isSubFieldFilter) {
|
||||
const fullNameFilters = generateILikeFiltersForCompositeFields(
|
||||
'',
|
||||
correspondingField.name,
|
||||
@ -123,7 +187,7 @@ export const getEmptyRecordGqlOperationFilter = ({
|
||||
break;
|
||||
}
|
||||
case 'LINKS': {
|
||||
if (!isCompositeField) {
|
||||
if (!isSubFieldFilter) {
|
||||
const linksFilters = generateILikeFiltersForCompositeFields(
|
||||
'',
|
||||
correspondingField.name,
|
||||
@ -153,7 +217,7 @@ export const getEmptyRecordGqlOperationFilter = ({
|
||||
break;
|
||||
}
|
||||
case 'ADDRESS':
|
||||
if (!isCompositeField) {
|
||||
if (!isSubFieldFilter) {
|
||||
emptyRecordFilter = {
|
||||
and: [
|
||||
{
|
||||
|
||||
@ -102,9 +102,14 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
|
||||
subFields: [
|
||||
'primaryPhoneNumber',
|
||||
'primaryPhoneCountryCode',
|
||||
'primaryPhoneCallingCode',
|
||||
'additionalPhones',
|
||||
],
|
||||
filterableSubFields: [
|
||||
'primaryPhoneNumber',
|
||||
'primaryPhoneCallingCode',
|
||||
'additionalPhones',
|
||||
],
|
||||
filterableSubFields: ['primaryPhoneNumber', 'primaryPhoneCountryCode'],
|
||||
labelBySubField: {
|
||||
primaryPhoneNumber: 'Primary Phone Number',
|
||||
primaryPhoneCountryCode: 'Primary Phone Country Code',
|
||||
|
||||
@ -79,7 +79,7 @@ export const ViewBarFilterDropdownFieldSelectMenu = () => {
|
||||
selectableItemIdArray={selectableFieldMetadataItemIds}
|
||||
selectableListInstanceId={FILTER_FIELD_LIST_ID}
|
||||
>
|
||||
<DropdownMenuItemsContainer>
|
||||
<DropdownMenuItemsContainer width="auto">
|
||||
{selectableVisibleFieldMetadataItems.map(
|
||||
(visibleFieldMetadataItem) => (
|
||||
<ViewBarFilterDropdownFieldSelectMenuItem
|
||||
|
||||
Reference in New Issue
Block a user