Support actor filtering in workflows (#16783)

Fixes https://github.com/twentyhq/twenty/issues/15782

Find records
<img width="422" height="423" alt="Capture d’écran 2025-12-23 à 16 23
49"
src="https://github.com/user-attachments/assets/5856de06-d608-46f6-97a6-8f382f355a44"
/>

Filters
<img width="422" height="271" alt="Capture d’écran 2025-12-23 à 16 23
32"
src="https://github.com/user-attachments/assets/fcc10b6e-f1ec-497a-8593-d599164d4e4b"
/>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Adds full support for filtering on `ACTOR` composite fields across UI
and server.
> 
> - Front-end: handles `ACTOR` in advanced filters and workflow filters;
`source` uses multi-select options from `FieldActorSource`,
`workspaceMemberId` uses `FormSingleRecordPicker`, others default to
text
> - Updates operand mapping for `ACTOR` (`source`→`SELECT`,
`workspaceMemberId`→`RELATION`, else `TEXT`)
> - Server: adds `ACTOR` evaluation with subfield-specific logic
(delegates to select/relation/text evaluators)
> - Simplifies `AdvancedFilterFieldSelectMenu` by removing
workflow-specific field filtering
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
7a8164cd2d4cad321ff1009bc7d1c4295a6672ca. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Thomas Trompette
2025-12-30 10:34:08 +01:00
committed by GitHub
parent d39f105a03
commit a2827494a1
8 changed files with 90 additions and 19 deletions
@@ -8,8 +8,17 @@ import { type RecordFilter } from '@/object-record/record-filter/types/RecordFil
import { CURRENCIES } from '@/settings/data-model/constants/Currencies';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useContext } from 'react';
import { FieldActorSource } from 'twenty-shared/types';
import { type SelectOption } from 'twenty-ui/input';
import { type JsonValue } from 'type-fest';
const ACTOR_SOURCE_OPTIONS: SelectOption[] = Object.values(
FieldActorSource,
).map((source) => ({
label: source.charAt(0) + source.slice(1).toLowerCase(),
value: source,
}));
export const AdvancedFilterCommandMenuValueFormCompositeFieldInput = ({
recordFilter,
onChange,
@@ -78,6 +87,23 @@ export const AdvancedFilterCommandMenuValueFormCompositeFieldInput = ({
readonly={readonly}
/>
)
) : filterType === 'ACTOR' ? (
recordFilter.subFieldName === 'source' ? (
<FormMultiSelectFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
options={ACTOR_SOURCE_OPTIONS}
readonly={readonly}
VariablePicker={VariablePicker}
/>
) : (
<FormTextFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
/>
)
) : (
<FormTextFieldInput
defaultValue={recordFilter.value}
@@ -25,7 +25,6 @@ import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/Dropdo
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
import { useLingui } from '@lingui/react/macro';
import { useContext } from 'react';
import { getFilterTypeFromFieldType } from 'twenty-shared/utils';
@@ -46,22 +45,10 @@ export const AdvancedFilterFieldSelectMenu = ({
objectFilterDropdownSearchInputComponentState,
);
const { isWorkflowFindRecords, objectMetadataItem } = useContext(
AdvancedFilterContext,
);
const { objectMetadataItem } = useContext(AdvancedFilterContext);
const {
filterableFieldMetadataItems: filterableFieldMetadataItemsForRecordIndex,
} = useFilterableFieldMetadataItems(objectMetadataItem.id);
const filterableFieldMetadataItems = isWorkflowFindRecords
? filterableFieldMetadataItemsForRecordIndex.filter((fieldMetadataItem) =>
shouldDisplayFormField({
fieldMetadataItem,
actionType: 'FIND_RECORDS',
}),
)
: filterableFieldMetadataItemsForRecordIndex;
const { filterableFieldMetadataItems: filterableFieldMetadataItems } =
useFilterableFieldMetadataItems(objectMetadataItem.id);
const visibleRecordFields = useRecoilComponentValue(
visibleRecordFieldsComponentSelector,
@@ -1,4 +1,3 @@
import { t } from '@lingui/core/macro';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputInnerContainer';
@@ -17,6 +16,7 @@ import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useCallback, useId } from 'react';
import { CustomError, isDefined, isValidUuid } from 'twenty-shared/utils';
@@ -23,7 +23,6 @@ type WorkflowStepFilterFieldSelectProps = {
};
const NON_SELECTABLE_FIELD_TYPES = [
FieldMetadataType.ACTOR,
FieldMetadataType.RICH_TEXT_V2,
FieldMetadataType.RATING,
];
@@ -1,14 +1,23 @@
import { FormCountryMultiSelectInput } from '@/object-record/record-field/ui/form-types/components/FormCountryMultiSelectInput';
import { FormMultiSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput';
import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput';
import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import { CURRENCIES } from '@/settings/data-model/constants/Currencies';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { useContext } from 'react';
import { type StepFilter } from 'twenty-shared/types';
import { FieldActorSource, type StepFilter } from 'twenty-shared/types';
import { type SelectOption } from 'twenty-ui/input';
import { type JsonValue } from 'type-fest';
const ACTOR_SOURCE_OPTIONS: SelectOption[] = Object.values(
FieldActorSource,
).map((source) => ({
label: source.charAt(0) + source.slice(1).toLowerCase(),
value: source,
}));
export const WorkflowStepFilterValueCompositeInput = ({
stepFilter,
onChange,
@@ -71,6 +80,31 @@ export const WorkflowStepFilterValueCompositeInput = ({
readonly={readonly}
/>
)
) : filterType === 'ACTOR' ? (
subFieldName === 'source' ? (
<FormMultiSelectFieldInput
defaultValue={stepFilter.value}
onChange={onChange}
options={ACTOR_SOURCE_OPTIONS}
readonly={readonly}
VariablePicker={WorkflowVariablePicker}
/>
) : subFieldName === 'workspaceMemberId' ? (
<FormSingleRecordPicker
objectNameSingulars={['workspaceMember']}
defaultValue={stepFilter.value}
onChange={onChange}
disabled={readonly}
VariablePicker={WorkflowVariablePicker}
/>
) : (
<FormTextFieldInput
defaultValue={stepFilter.value}
onChange={onChange}
VariablePicker={WorkflowVariablePicker}
readonly={readonly}
/>
)
) : (
<FormTextFieldInput
defaultValue={stepFilter.value}
@@ -43,6 +43,7 @@ const COMPOSITE_FIELD_METADATA_TYPES = [
FieldMetadataType.EMAILS,
FieldMetadataType.LINKS,
FieldMetadataType.CURRENCY,
FieldMetadataType.ACTOR,
];
const isFilterableFieldType = (
@@ -137,6 +137,14 @@ export const getStepFilterOperands = ({
return FILTER_OPERANDS_MAP.UUID;
case 'NUMERIC':
return FILTER_OPERANDS_MAP.NUMERIC;
case 'ACTOR': {
if (subFieldName === 'source') {
return FILTER_OPERANDS_MAP.SELECT;
} else if (subFieldName === 'workspaceMemberId') {
return FILTER_OPERANDS_MAP.RELATION;
}
return FILTER_OPERANDS_MAP.TEXT;
}
default:
return defaultOperands;
}
@@ -79,6 +79,8 @@ function evaluateFilter(
return evaluateRelationFilter(filterWithConvertedOperand);
case 'CURRENCY':
return evaluateCurrencyFilter(filterWithConvertedOperand);
case 'ACTOR':
return evaluateActorFilter(filterWithConvertedOperand);
default:
return evaluateDefaultFilter(filterWithConvertedOperand);
}
@@ -398,6 +400,20 @@ function evaluateSelectFilter(filter: ResolvedFilter): boolean {
}
}
function evaluateActorFilter(filter: ResolvedFilter): boolean {
const { compositeFieldSubFieldName } = filter;
if (compositeFieldSubFieldName === 'source') {
return evaluateSelectFilter(filter);
}
if (compositeFieldSubFieldName === 'workspaceMemberId') {
return evaluateRelationFilter(filter);
}
return evaluateTextAndArrayFilter(filter, 'TEXT', compositeFieldSubFieldName);
}
export function evaluateFilterConditions({
filterGroups = [],
filters = [],