Use view filters operands in step filters + migrate to twenty-shared (#13137)

Step operand will more or less be the same as view filter operand. 

This PR:
- moves `ViewFilterOperand` to twenty-shared
- use it as step operand
- check what operand should be available based on the selected field
type in filter action
- rewrite the function that evaluates filters so it uses
ViewFilterOperand instead

ViewFilterOperand may be renamed in a future PR.
This commit is contained in:
Thomas Trompette
2025-07-10 10:36:37 +02:00
committed by GitHub
parent d808cbeed9
commit 50e402af07
55 changed files with 677 additions and 665 deletions
@@ -10,7 +10,7 @@ import {
StepFilter,
StepFilterGroup,
StepLogicalOperator,
StepOperand,
ViewFilterOperand,
} from 'twenty-shared/src/types';
import { isDefined } from 'twenty-shared/utils';
import { IconLibraryPlus, IconPlus } from 'twenty-ui/display';
@@ -21,6 +21,17 @@ type WorkflowStepFilterAddFilterRuleSelectProps = {
stepFilterGroup: StepFilterGroup;
};
const BASE_NEW_STEP_FILTER = {
id: v4(),
type: 'unknown',
label: '',
value: '',
operand: ViewFilterOperand.Is,
displayValue: '',
stepFilterGroupId: '',
stepOutputKey: '',
};
export const WorkflowStepFilterAddFilterRuleSelect = ({
stepFilterGroup,
}: WorkflowStepFilterAddFilterRuleSelectProps) => {
@@ -42,16 +53,10 @@ export const WorkflowStepFilterAddFilterRuleSelect = ({
closeDropdown(dropdownId);
const newStepFilter = {
id: v4(),
type: 'text',
label: 'New Filter',
value: '',
operand: StepOperand.EQ,
displayValue: '',
...BASE_NEW_STEP_FILTER,
stepFilterGroupId: stepFilterGroup.id,
stepOutputKey: '',
positionInStepFilterGroup: newPositionInStepFilterGroup,
};
} satisfies StepFilter;
upsertStepFilterSettings({
stepFilterToUpsert: newStepFilter,
@@ -71,15 +76,9 @@ export const WorkflowStepFilterAddFilterRuleSelect = ({
};
const newStepFilter: StepFilter = {
id: v4(),
type: 'text',
operand: StepOperand.EQ,
value: '',
displayValue: '',
...BASE_NEW_STEP_FILTER,
stepFilterGroupId: newStepFilterGroupId,
positionInStepFilterGroup: 1,
label: 'New Filter',
stepOutputKey: '',
};
upsertStepFilterSettings({
@@ -8,7 +8,7 @@ import { extractRawVariableNamePart } from '@/workflow/workflow-variables/utils/
import { searchVariableThroughOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughOutputSchema';
import { useLingui } from '@lingui/react/macro';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { StepFilter } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -30,6 +30,7 @@ export const WorkflowStepFilterFieldSelect = ({
rawVariableName: stepFilter.stepOutputKey,
part: 'stepId',
});
const stepsOutputSchema = useRecoilValue(
stepsOutputSchemaFamilySelector({
workflowVersionId,
@@ -37,6 +38,41 @@ export const WorkflowStepFilterFieldSelect = ({
}),
);
const handleChange = useRecoilCallback(
({ snapshot }) =>
(variableName: string) => {
const stepId = extractRawVariableNamePart({
rawVariableName: variableName,
part: 'stepId',
});
const currentStepOutputSchema = snapshot
.getLoadable(
stepsOutputSchemaFamilySelector({
workflowVersionId,
stepIds: [stepId],
}),
)
.getValue();
const { variableLabel, variableType } =
searchVariableThroughOutputSchema({
stepOutputSchema: currentStepOutputSchema?.[0],
rawVariableName: variableName,
isFullRecord: false,
});
upsertStepFilterSettings({
stepFilterToUpsert: {
...stepFilter,
stepOutputKey: variableName,
displayValue: variableLabel ?? '',
type: variableType ?? 'unknown',
},
});
},
[upsertStepFilterSettings, stepFilter, workflowVersionId],
);
if (!isDefined(stepId)) {
return null;
}
@@ -50,16 +86,6 @@ export const WorkflowStepFilterFieldSelect = ({
const isSelectedFieldNotFound = !isDefined(variableLabel);
const label = isSelectedFieldNotFound ? t`No Field Selected` : variableLabel;
const handleChange = (variableName: string) => {
upsertStepFilterSettings({
stepFilterToUpsert: {
...stepFilter,
stepOutputKey: variableName,
displayValue: label,
},
});
};
return (
<WorkflowVariablesDropdown
instanceId={`step-filter-field-${stepFilter.id}`}
@@ -1,36 +1,31 @@
import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
import { getOperandLabel } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
import { Select } from '@/ui/input/components/Select';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useUpsertStepFilterSettings';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
import { getViewFilterOperands } from '@/workflow/workflow-steps/workflow-actions/filter-action/utils/getStepFilterOperands';
import { useContext } from 'react';
import { StepFilter, StepOperand } from 'twenty-shared/src/types';
import { StepFilter, ViewFilterOperand } from 'twenty-shared/src/types';
type WorkflowStepFilterOperandSelectProps = {
stepFilter: StepFilter;
};
const STEP_OPERAND_OPTIONS = [
{ value: StepOperand.EQ, label: 'Equals' },
{ value: StepOperand.NE, label: 'Not equals' },
{ value: StepOperand.GT, label: 'Greater than' },
{ value: StepOperand.GTE, label: 'Greater than or equal' },
{ value: StepOperand.LT, label: 'Less than' },
{ value: StepOperand.LTE, label: 'Less than or equal' },
{ value: StepOperand.LIKE, label: 'Contains' },
{ value: StepOperand.ILIKE, label: 'Contains (case insensitive)' },
{ value: StepOperand.IN, label: 'In' },
{ value: StepOperand.IS, label: 'Is' },
];
export const WorkflowStepFilterOperandSelect = ({
stepFilter,
}: WorkflowStepFilterOperandSelectProps) => {
const { readonly } = useContext(WorkflowStepFilterContext);
const { upsertStepFilterSettings } = useUpsertStepFilterSettings();
const operands = getViewFilterOperands({ filterType: stepFilter.type });
const handleChange = (operand: StepOperand) => {
const options = operands.map((operand) => ({
value: operand,
label: getOperandLabel(operand),
}));
const handleChange = (operand: ViewFilterOperand) => {
upsertStepFilterSettings({
stepFilterToUpsert: {
...stepFilter,
@@ -45,7 +40,7 @@ export const WorkflowStepFilterOperandSelect = ({
dropdownWidth={GenericDropdownContentWidth.Medium}
dropdownId={`step-filter-operand-${stepFilter.id}`}
value={stepFilter.operand}
options={STEP_OPERAND_OPTIONS}
options={options}
onChange={handleChange}
disabled={readonly}
dropdownOffset={DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET}
@@ -4,7 +4,7 @@ import {
StepFilter,
StepFilterGroup,
StepLogicalOperator,
StepOperand,
ViewFilterOperand,
} from 'twenty-shared/types';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
@@ -28,7 +28,7 @@ const TEXT_STEP_FILTER: StepFilter = {
type: 'text',
label: 'Company Name',
value: 'Acme',
operand: StepOperand.LIKE,
operand: ViewFilterOperand.Contains,
};
const meta: Meta<typeof WorkflowStepFilterColumn> = {
@@ -1,6 +1,6 @@
import { WorkflowStepFilterDecorator } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/decorators/WorkflowStepFilterDecorator';
import { Meta, StoryObj } from '@storybook/react';
import { StepFilter, StepOperand } from 'twenty-shared/types';
import { StepFilter, ViewFilterOperand } from 'twenty-shared/types';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
@@ -16,7 +16,7 @@ const DEFAULT_STEP_FILTER: StepFilter = {
displayValue: '',
type: 'text',
label: 'New Filter',
operand: StepOperand.EQ,
operand: ViewFilterOperand.Is,
value: '',
positionInStepFilterGroup: 0,
};
@@ -1,14 +1,14 @@
import { WorkflowStepFilterDecorator } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/decorators/WorkflowStepFilterDecorator';
import { WorkflowStepFilterOperandSelect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterOperandSelect';
import { Meta, StoryObj } from '@storybook/react';
import { expect, within } from '@storybook/test';
import { StepFilter, StepOperand } from 'twenty-shared/types';
import { StepFilter, ViewFilterOperand } from 'twenty-shared/types';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { WorkflowStepFilterOperandSelect } from '../WorkflowStepFilterOperandSelect';
const DEFAULT_STEP_FILTER: StepFilter = {
id: 'filter-1',
@@ -17,23 +17,11 @@ const DEFAULT_STEP_FILTER: StepFilter = {
displayValue: 'Company Name',
type: 'text',
label: 'Company Name',
operand: StepOperand.EQ,
operand: ViewFilterOperand.Contains,
value: '',
positionInStepFilterGroup: 0,
};
const LIKE_OPERAND_FILTER: StepFilter = {
id: 'filter-1',
stepFilterGroupId: 'filter-group-1',
stepOutputKey: 'company.name',
displayValue: 'Company Name',
type: 'text',
label: 'Company Name',
operand: StepOperand.LIKE,
value: 'Acme',
positionInStepFilterGroup: 0,
};
const GREATER_THAN_FILTER: StepFilter = {
id: 'filter-1',
stepFilterGroupId: 'filter-group-1',
@@ -41,7 +29,7 @@ const GREATER_THAN_FILTER: StepFilter = {
displayValue: 'Employee Count',
type: 'number',
label: 'Employee Count',
operand: StepOperand.GT,
operand: ViewFilterOperand.GreaterThanOrEqual,
value: '100',
positionInStepFilterGroup: 0,
};
@@ -69,17 +57,6 @@ export default meta;
type Story = StoryObj<typeof WorkflowStepFilterOperandSelect>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(await canvas.findByText('Equals')).toBeVisible();
},
};
export const WithLikeOperand: Story = {
args: {
stepFilter: LIKE_OPERAND_FILTER,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
@@ -94,6 +71,8 @@ export const WithGreaterThanOperand: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(await canvas.findByText('Greater than')).toBeVisible();
await expect(
await canvas.findByText('Greater than or equal'),
).toBeVisible();
},
};
@@ -1,7 +1,7 @@
import { WorkflowStepFilterDecorator } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/decorators/WorkflowStepFilterDecorator';
import { Meta, StoryObj } from '@storybook/react';
import { expect, within } from '@storybook/test';
import { StepFilter, StepOperand } from 'twenty-shared/types';
import { StepFilter, ViewFilterOperand } from 'twenty-shared/types';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
@@ -17,7 +17,7 @@ const TEXT_FILTER: StepFilter = {
displayValue: 'Company Name',
type: 'text',
label: 'Company Name',
operand: StepOperand.LIKE,
operand: ViewFilterOperand.Contains,
value: 'Acme',
positionInStepFilterGroup: 0,
};
@@ -29,7 +29,7 @@ const NUMBER_FILTER: StepFilter = {
displayValue: 'Employee Count',
type: 'number',
label: 'Employee Count',
operand: StepOperand.GT,
operand: ViewFilterOperand.GreaterThanOrEqual,
value: '100',
positionInStepFilterGroup: 0,
};
@@ -11,7 +11,7 @@ import {
StepFilter,
StepFilterGroup,
StepLogicalOperator,
StepOperand,
ViewFilterOperand,
} from 'twenty-shared/types';
import { v4 } from 'uuid';
@@ -48,10 +48,10 @@ export const useAddRootStepFilter = () => {
const newStepFilter: StepFilter = {
id: v4(),
type: 'text',
type: 'unknown',
label: 'New Filter',
value: '',
operand: StepOperand.EQ,
operand: ViewFilterOperand.Is,
displayValue: '',
stepFilterGroupId: newStepFilterGroup.id,
stepOutputKey: '',
@@ -0,0 +1,115 @@
import { ViewFilterOperand } from 'twenty-shared/src/types';
const emptyOperands = [
ViewFilterOperand.IsEmpty,
ViewFilterOperand.IsNotEmpty,
] as const;
const relationOperands = [
ViewFilterOperand.Is,
ViewFilterOperand.IsNot,
] as const;
const defaultOperands = [
ViewFilterOperand.Is,
ViewFilterOperand.IsNot,
ViewFilterOperand.Contains,
ViewFilterOperand.DoesNotContain,
ViewFilterOperand.GreaterThanOrEqual,
ViewFilterOperand.LessThanOrEqual,
...emptyOperands,
] as const;
export const FILTER_OPERANDS_MAP = {
TEXT: [
ViewFilterOperand.Contains,
ViewFilterOperand.DoesNotContain,
...emptyOperands,
],
NUMBER: [
ViewFilterOperand.GreaterThanOrEqual,
ViewFilterOperand.LessThanOrEqual,
...emptyOperands,
],
RAW_JSON: [
ViewFilterOperand.Contains,
ViewFilterOperand.DoesNotContain,
...emptyOperands,
],
DATE_TIME: [
ViewFilterOperand.Is,
ViewFilterOperand.IsRelative,
ViewFilterOperand.IsInPast,
ViewFilterOperand.IsInFuture,
ViewFilterOperand.IsToday,
ViewFilterOperand.IsBefore,
ViewFilterOperand.IsAfter,
...emptyOperands,
],
DATE: [
ViewFilterOperand.Is,
ViewFilterOperand.IsRelative,
ViewFilterOperand.IsInPast,
ViewFilterOperand.IsInFuture,
ViewFilterOperand.IsToday,
ViewFilterOperand.IsBefore,
ViewFilterOperand.IsAfter,
...emptyOperands,
],
RATING: [ViewFilterOperand.Is, ViewFilterOperand.IsNot, ...emptyOperands],
RELATION: [...relationOperands, ...emptyOperands],
MULTI_SELECT: [
ViewFilterOperand.Contains,
ViewFilterOperand.DoesNotContain,
...emptyOperands,
],
SELECT: [ViewFilterOperand.Is, ViewFilterOperand.IsNot, ...emptyOperands],
ARRAY: [
ViewFilterOperand.Contains,
ViewFilterOperand.DoesNotContain,
...emptyOperands,
],
BOOLEAN: [ViewFilterOperand.Is],
UUID: [ViewFilterOperand.Is],
NUMERIC: [
ViewFilterOperand.GreaterThanOrEqual,
ViewFilterOperand.LessThanOrEqual,
...emptyOperands,
],
};
export const getViewFilterOperands = ({
filterType,
}: {
filterType: string;
}): readonly ViewFilterOperand[] => {
switch (filterType) {
case 'TEXT':
return FILTER_OPERANDS_MAP.TEXT;
case 'NUMBER':
return FILTER_OPERANDS_MAP.NUMBER;
case 'RAW_JSON':
return FILTER_OPERANDS_MAP.RAW_JSON;
case 'DATE_TIME':
case 'DATE':
return FILTER_OPERANDS_MAP.DATE_TIME;
case 'RATING':
return FILTER_OPERANDS_MAP.RATING;
case 'RELATION':
return FILTER_OPERANDS_MAP.RELATION;
case 'MULTI_SELECT':
return FILTER_OPERANDS_MAP.MULTI_SELECT;
case 'SELECT':
return FILTER_OPERANDS_MAP.SELECT;
case 'ARRAY':
return FILTER_OPERANDS_MAP.ARRAY;
case 'BOOLEAN':
return FILTER_OPERANDS_MAP.BOOLEAN;
case 'UUID':
return FILTER_OPERANDS_MAP.UUID;
case 'NUMERIC':
return FILTER_OPERANDS_MAP.NUMERIC;
default:
return defaultOperands;
}
};