diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx
index 1cf20a2c2..f3b6dc5e8 100644
--- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterDropdownFilterInput.tsx
@@ -83,7 +83,7 @@ export const AdvancedFilterDropdownFilterInput = ({
recordFilter.subFieldName,
) ? (
<>
-
+
>
) : (
<>>
diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts
index 3eac11ace..f1c04c298 100644
--- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts
+++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts
@@ -104,6 +104,8 @@ export type EmailsFilter = {
export type PhonesFilter = {
primaryPhoneNumber?: StringFilter;
+ primaryPhoneCallingCode?: StringFilter;
+ additionalPhones?: RawJsonFilter;
};
export type SelectFilter = {
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect.tsx
index dd79785a8..329330c92 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect.tsx
@@ -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 = ({
}}
/>
-
+
{filteredSelectedItems?.map((item) => {
return (
{
},
},
},
+ {
+ 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: '[]' },
},
},
],
diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable.ts
index 79af110c0..5205c3736 100644
--- a/packages/twenty-front/src/modules/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable.ts
+++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/areCompositeTypeSubFieldsFilterable.ts
@@ -5,6 +5,7 @@ const COMPOSITE_TYPES_FILTERABLE = [
'FULL_NAME',
'CURRENCY',
'ADDRESS',
+ 'PHONES',
] satisfies FieldType[];
type FilterableCompositeFieldType = (typeof COMPOSITE_TYPES_FILTERABLE)[number];
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 ac34a8061..9d1e9758f 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
@@ -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`,
);
}
}
diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter.ts
index f5f3d2cb3..51fb37acc 100644
--- a/packages/twenty-front/src/modules/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter.ts
+++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter.ts
@@ -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: [
{
diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts
index ebf2a0803..0f700d998 100644
--- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts
@@ -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',
diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenu.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenu.tsx
index f969c0e31..1eb0dd3bd 100644
--- a/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenu.tsx
+++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownFieldSelectMenu.tsx
@@ -79,7 +79,7 @@ export const ViewBarFilterDropdownFieldSelectMenu = () => {
selectableItemIdArray={selectableFieldMetadataItemIds}
selectableListInstanceId={FILTER_FIELD_LIST_ID}
>
-
+
{selectableVisibleFieldMetadataItems.map(
(visibleFieldMetadataItem) => (