Implemented ACTOR sub-field filtering (#11957)

This PR implements what's missing for ACTOR sub-field filtering,
filtering on the source sub-field was already working.

We can now filter on name sub-field. 

Since the sub-fields are different types and cannot be filtered both by
text, we consider that a simple filter on ACTOR is filtering on the
source, we have to go to advanced filter to have the name filter
sub-field.
This commit is contained in:
Lucas Bordeau
2025-05-09 17:49:04 +02:00
committed by GitHub
parent 1c0650fbd3
commit 3308ba56b2
8 changed files with 167 additions and 93 deletions

View File

@ -94,7 +94,7 @@ export const AdvancedFilterRecordFilterOperandSelect = ({
/>
}
dropdownComponents={
<DropdownMenuItemsContainer>
<DropdownMenuItemsContainer width="auto">
<SelectableList
hotkeyScope={dropdownId}
selectableItemIdArray={operandsForFilterType.map(
@ -125,6 +125,7 @@ export const AdvancedFilterRecordFilterOperandSelect = ({
dropdownHotkeyScope={{ scope: dropdownId }}
dropdownOffset={DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET}
dropdownPlacement="bottom-start"
dropdownWidth={200}
/>
</StyledContainer>
);

View File

@ -83,17 +83,31 @@ export const AdvancedFilterValueInput = ({
? ({ y: -33, x: 0 } satisfies DropdownOffset)
: DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET;
const showFilterTextInput =
(isDefined(filterType) &&
(TEXT_FILTER_TYPES.includes(filterType) ||
NUMBER_FILTER_TYPES.includes(filterType))) ||
isExpectedSubFieldName(
FieldMetadataType.CURRENCY,
'amountMicros',
recordFilter.subFieldName,
) ||
(filterType === 'ADDRESS' &&
subFieldNameUsedInDropdown !== 'addressCountry');
const isFilterableByTextValue =
isDefined(filterType) &&
(TEXT_FILTER_TYPES.includes(filterType) ||
NUMBER_FILTER_TYPES.includes(filterType));
const isCurrencyAmountMicrosFilter = isExpectedSubFieldName(
FieldMetadataType.CURRENCY,
'amountMicros',
recordFilter.subFieldName,
);
const isAddressFilterOnSubFieldOtherThanCountry =
filterType === 'ADDRESS' && subFieldNameUsedInDropdown !== 'addressCountry';
const isActorNameFilter = isExpectedSubFieldName(
FieldMetadataType.ACTOR,
'name',
recordFilter.subFieldName,
);
const showFilterTextInputInsteadOfDropdown =
isFilterableByTextValue ||
isCurrencyAmountMicrosFilter ||
isAddressFilterOnSubFieldOtherThanCountry ||
isActorNameFilter;
return (
<StyledValueDropdownContainer>
@ -103,7 +117,7 @@ export const AdvancedFilterValueInput = ({
<AdvancedFilterValueInputDropdownButtonClickableSelect
recordFilterId={recordFilterId}
/>
) : showFilterTextInput ? (
) : showFilterTextInputInsteadOfDropdown ? (
<AdvancedFilterDropdownTextInput recordFilter={recordFilter} />
) : (
<Dropdown

View File

@ -95,7 +95,7 @@ export type LinksFilter = {
export type ActorFilter = {
name?: StringFilter;
source?: IsFilter;
source?: SelectFilter;
};
export type EmailsFilter = {

View File

@ -4,13 +4,13 @@ import { ObjectFilterDropdownOptionSelect } from '@/object-record/object-filter-
import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect';
import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput';
import { ObjectFilterDropdownSourceSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { ObjectFilterDropdownBooleanSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect';
import { ObjectFilterDropdownCurrencySelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect';
import { ObjectFilterDropdownSourceSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect';
import { ObjectFilterDropdownTextInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextInput';
import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
import { NUMBER_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/NumberFilterTypes';
@ -19,7 +19,6 @@ import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-recor
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { isExpectedSubFieldName } from '@/object-record/object-filter-dropdown/utils/isExpectedSubFieldName';
import { isFilterOnActorSourceSubField } from '@/object-record/object-filter-dropdown/utils/isFilterOnActorSourceSubField';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@ -71,10 +70,6 @@ export const ObjectFilterDropdownFilterInput = ({
fieldMetadataItemUsedInDropdown.type,
);
const isActorSourceCompositeFilter = isFilterOnActorSourceSubField(
subFieldNameUsedInDropdown,
);
const isNotASubFieldFilter = !isDefined(subFieldNameUsedInDropdown);
return (
@ -100,16 +95,11 @@ export const ObjectFilterDropdownFilterInput = ({
/>
</>
)}
{filterType === 'ACTOR' &&
(isActorSourceCompositeFilter || isNotASubFieldFilter ? (
<>
<ObjectFilterDropdownSourceSelect />
</>
) : (
<>
<ObjectFilterDropdownTextInput />
</>
))}
{filterType === 'ACTOR' && (
<>
<ObjectFilterDropdownSourceSelect />
</>
)}
{filterType === 'ADDRESS' &&
(isNotASubFieldFilter ? (
<>

View File

@ -5,4 +5,6 @@ export const ICON_NAME_BY_SUB_FIELD: Partial<
> = {
currencyCode: 'IconCurrencyDollar',
amountMicros: 'IconNumber95Small',
name: 'IconAlignJustified',
source: 'IconFileArrowLeft',
};

View File

@ -927,73 +927,139 @@ export const computeFilterRecordGqlOperationFilter = ({
);
}
}
// TODO: fix this with a new composite field in ViewFilter entity
case 'ACTOR': {
switch (filter.operand) {
case RecordFilterOperand.Is: {
if (filter.value === '[]') {
return;
}
if (isSubFieldFilter) {
switch (subFieldName) {
case 'source': {
switch (filter.operand) {
case RecordFilterOperand.Is: {
if (filter.value === '[]') {
return;
}
const parsedRecordIds = JSON.parse(filter.value) as string[];
const parsedSources = JSON.parse(filter.value) as string[];
return {
[correspondingField.name]: {
source: {
in: parsedRecordIds,
} as RelationFilter,
},
};
}
case RecordFilterOperand.IsNot: {
if (filter.value === '[]') {
return;
}
const parsedRecordIds = JSON.parse(filter.value) as string[];
if (parsedRecordIds.length === 0) return;
return {
not: {
[correspondingField.name]: {
source: {
in: parsedRecordIds,
} as RelationFilter,
},
},
};
}
case RecordFilterOperand.Contains:
return {
or: [
{
[correspondingField.name]: {
name: {
ilike: `%${filter.value}%`,
},
} as ActorFilter,
},
],
};
case RecordFilterOperand.DoesNotContain:
return {
and: [
{
not: {
return {
[correspondingField.name]: {
name: {
ilike: `%${filter.value}%`,
source: {
in: parsedSources,
} satisfies RelationFilter,
},
};
}
case RecordFilterOperand.IsNot: {
if (filter.value === '[]') {
return;
}
const parsedSources = JSON.parse(filter.value) as string[];
if (parsedSources.length === 0) return;
return {
not: {
[correspondingField.name]: {
source: {
in: parsedSources,
} satisfies RelationFilter,
},
} as ActorFilter,
},
};
}
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.label} filter`,
);
}
}
case 'name': {
switch (filter.operand) {
case RecordFilterOperand.Contains:
return {
or: [
{
[correspondingField.name]: {
name: {
ilike: `%${filter.value}%`,
},
} satisfies ActorFilter,
},
],
};
case RecordFilterOperand.DoesNotContain:
return {
and: [
{
not: {
[correspondingField.name]: {
name: {
ilike: `%${filter.value}%`,
},
} satisfies ActorFilter,
},
},
],
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.label} filter`,
);
}
}
}
break;
} else {
if (filter.value === '[]') {
return;
}
const parsedSources = JSON.parse(filter.value) as string[];
if (parsedSources.length === 0) return;
switch (filter.operand) {
case RecordFilterOperand.Contains:
return {
or: [
{
[correspondingField.name]: {
source: {
in: parsedSources,
},
} satisfies ActorFilter,
},
},
],
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.label} filter`,
);
],
};
case RecordFilterOperand.DoesNotContain:
return {
and: [
{
or: [
{
not: {
[correspondingField.name]: {
source: {
in: parsedSources,
},
} satisfies ActorFilter,
},
},
{
[correspondingField.name]: {
source: {
is: 'NULL',
},
} satisfies ActorFilter,
},
],
},
],
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.label} filter`,
);
}
}
}
case 'EMAILS':

View File

@ -177,8 +177,8 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
label: 'Actor',
Icon: IllustrationIconSetting,
category: 'Basic',
subFields: ['source'],
filterableSubFields: ['source'],
subFields: ['source', 'name'],
filterableSubFields: ['source', 'name'],
labelBySubField: {
source: 'Source',
name: 'Name',

View File

@ -65,6 +65,7 @@ export const EditableFilterDropdownButton = ({
dropdownOffset={{ y: 8, x: 0 }}
dropdownPlacement="bottom-start"
onClickOutside={handleDropdownClickOutside}
dropdownWidth={200}
/>
</>
);