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

View File

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

View File

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

View File

@ -5,4 +5,6 @@ export const ICON_NAME_BY_SUB_FIELD: Partial<
> = { > = {
currencyCode: 'IconCurrencyDollar', currencyCode: 'IconCurrencyDollar',
amountMicros: 'IconNumber95Small', 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': { case 'ACTOR': {
switch (filter.operand) { if (isSubFieldFilter) {
case RecordFilterOperand.Is: { switch (subFieldName) {
if (filter.value === '[]') { case 'source': {
return; 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 { 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: {
[correspondingField.name]: { [correspondingField.name]: {
name: { source: {
ilike: `%${filter.value}%`, 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,
}, },
}, ],
], };
}; case RecordFilterOperand.DoesNotContain:
default: return {
throw new Error( and: [
`Unknown operand ${filter.operand} for ${filter.label} filter`, {
); 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': case 'EMAILS':

View File

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

View File

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