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:
+26
@@ -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}
|
||||
|
||||
+3
-16
@@ -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
-1
@@ -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';
|
||||
|
||||
-1
@@ -23,7 +23,6 @@ type WorkflowStepFilterFieldSelectProps = {
|
||||
};
|
||||
|
||||
const NON_SELECTABLE_FIELD_TYPES = [
|
||||
FieldMetadataType.ACTOR,
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
FieldMetadataType.RATING,
|
||||
];
|
||||
|
||||
+35
-1
@@ -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}
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ const COMPOSITE_FIELD_METADATA_TYPES = [
|
||||
FieldMetadataType.EMAILS,
|
||||
FieldMetadataType.LINKS,
|
||||
FieldMetadataType.CURRENCY,
|
||||
FieldMetadataType.ACTOR,
|
||||
];
|
||||
|
||||
const isFilterableFieldType = (
|
||||
|
||||
+8
@@ -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;
|
||||
}
|
||||
|
||||
+16
@@ -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 = [],
|
||||
|
||||
Reference in New Issue
Block a user