feat: Add If/Else node (#16833)

Closes [#1265](https://github.com/twentyhq/core-team-issues/issues/1265)
This commit is contained in:
Abdul Rahman
2026-01-01 19:35:56 +05:30
committed by GitHub
parent 5d5fd5fca5
commit dff5e3cd7b
95 changed files with 1523 additions and 163 deletions
@@ -10,8 +10,8 @@ import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useWorkflowVersionIdOrThrow } from '@/workflow/hooks/useWorkflowVersionIdOrThrow';
import { stepsOutputSchemaFamilySelector } from '@/workflow/states/selectors/stepsOutputSchemaFamilySelector';
import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useUpsertStepFilterSettings';
import { getStepFilterOperands } from '@/workflow/workflow-steps/workflow-actions/filter-action/utils/getStepFilterOperands';
import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/filters/hooks/useUpsertStepFilterSettings';
import { getStepFilterOperands } from '@/workflow/workflow-steps/filters/utils/getStepFilterOperands';
import { useVariableDropdown } from '@/workflow/workflow-variables/hooks/useVariableDropdown';
import { isRecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/guards/isRecordOutputSchemaV2';
import { type StepOutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2';
@@ -15,6 +15,7 @@ import { WorkflowEditActionUpsertRecord } from '@/workflow/workflow-steps/workfl
import { WorkflowEditActionDelay } from '@/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay';
import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords';
import { WorkflowEditActionIfElse } from '@/workflow/workflow-steps/workflow-actions/if-else-action/components/WorkflowEditActionIfElse';
import { WorkflowEditActionFormFiller } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFiller';
import { WorkflowEditActionHttpRequest } from '@/workflow/workflow-steps/workflow-actions/http-request-action/components/WorkflowEditActionHttpRequest';
import { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/components/WorkflowEditActionIterator';
@@ -230,6 +231,17 @@ export const WorkflowRunStepNodeDetail = ({
/>
);
}
case 'IF_ELSE': {
return (
<WorkflowEditActionIfElse
key={stepId}
action={stepDefinition.definition}
actionOptions={{
readonly: true,
}}
/>
);
}
case 'ITERATOR': {
return (
<WorkflowEditActionIterator
@@ -14,6 +14,7 @@ import { WorkflowEditActionUpsertRecord } from '@/workflow/workflow-steps/workfl
import { WorkflowEditActionDelay } from '@/workflow/workflow-steps/workflow-actions/delay-actions/components/WorkflowEditActionDelay';
import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords';
import { WorkflowEditActionIfElse } from '@/workflow/workflow-steps/workflow-actions/if-else-action/components/WorkflowEditActionIfElse';
import { WorkflowEditActionFormBuilder } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder';
import { WorkflowEditActionHttpRequest } from '@/workflow/workflow-steps/workflow-actions/http-request-action/components/WorkflowEditActionHttpRequest';
import { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/components/WorkflowEditActionIterator';
@@ -210,6 +211,15 @@ export const WorkflowStepDetail = ({
/>
);
}
case 'IF_ELSE': {
return (
<WorkflowEditActionIfElse
key={stepId}
action={stepDefinition.definition}
actionOptions={props}
/>
);
}
case 'ITERATOR': {
return (
<WorkflowEditActionIterator
@@ -1,9 +1,9 @@
import { useRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyState';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFilterGroupsComponentState';
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFiltersComponentState';
import { hasInitializedCurrentStepFilterGroupsComponentFamilyState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/hasInitializedCurrentStepFilterGroupsComponentFamilyState';
import { hasInitializedCurrentStepFiltersComponentFamilyState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/hasInitializedCurrentStepFiltersComponentFamilyState';
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFilterGroupsComponentState';
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFiltersComponentState';
import { hasInitializedCurrentStepFilterGroupsComponentFamilyState } from '@/workflow/workflow-steps/filters/states/hasInitializedCurrentStepFilterGroupsComponentFamilyState';
import { hasInitializedCurrentStepFiltersComponentFamilyState } from '@/workflow/workflow-steps/filters/states/hasInitializedCurrentStepFiltersComponentFamilyState';
import { useEffect, useMemo } from 'react';
import {
type StepFilterGroup,
@@ -4,8 +4,8 @@ import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useChildStepFiltersAndChildStepFilterGroups';
import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useUpsertStepFilterSettings';
import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow-steps/filters/hooks/useChildStepFiltersAndChildStepFilterGroups';
import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/filters/hooks/useUpsertStepFilterSettings';
import {
StepLogicalOperator,
ViewFilterOperand,
@@ -1,5 +1,5 @@
import { useAddRootStepFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useAddRootStepFilter';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
import { useAddRootStepFilter } from '@/workflow/workflow-steps/filters/hooks/useAddRootStepFilter';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useContext } from 'react';
@@ -1,18 +1,24 @@
import { AdvancedFilterCommandMenuColumn } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuColumn';
import { WorkflowStepFilterFieldSelect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterFieldSelect';
import { WorkflowStepFilterLogicalOperatorCell } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterLogicalOperatorCell';
import { WorkflowStepFilterOperandSelect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterOperandSelect';
import { WorkflowStepFilterOptionsDropdown } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterOptionsDropdown';
import { WorkflowStepFilterValueInput } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { WorkflowStepFilterFieldSelect } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterFieldSelect';
import { WorkflowStepFilterLogicalOperatorCell } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterLogicalOperatorCell';
import { WorkflowStepFilterOperandSelect } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterOperandSelect';
import { WorkflowStepFilterOptionsDropdown } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterOptionsDropdown';
import { WorkflowStepFilterValueInput } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterValueInput';
import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow-steps/filters/hooks/useChildStepFiltersAndChildStepFilterGroups';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFilterGroupsComponentState';
import styled from '@emotion/styled';
import { useContext } from 'react';
import { type StepFilter, type StepFilterGroup } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
type WorkflowStepFilterColumnProps = {
stepFilterGroup: StepFilterGroup;
stepFilter: StepFilter;
stepFilterIndex: number;
isIfBranch?: boolean;
firstFilterLabel?: string;
};
const StyledContainer = styled.div`
@@ -26,17 +32,42 @@ export const WorkflowStepFilterColumn = ({
stepFilterGroup,
stepFilter,
stepFilterIndex,
isIfBranch,
firstFilterLabel,
}: WorkflowStepFilterColumnProps) => {
const { readonly } = useContext(WorkflowStepFilterContext);
const stepFilterGroups = useRecoilComponentValue(
currentStepFilterGroupsComponentState,
);
const rootStepFilterGroup = stepFilterGroups?.find(
(filterGroup) => !isDefined(filterGroup.parentStepFilterGroupId),
);
const { childStepFilters, childStepFilterGroups } =
useChildStepFiltersAndChildStepFilterGroups({
stepFilterGroupId: rootStepFilterGroup?.id ?? '',
});
const isLastFilterInIfBranch =
isIfBranch &&
isDefined(rootStepFilterGroup) &&
stepFilter.stepFilterGroupId === rootStepFilterGroup.id &&
childStepFilters.length === 1 &&
childStepFilterGroups.length === 0;
const shouldShowDropdown = !readonly && !isLastFilterInIfBranch;
return (
<AdvancedFilterCommandMenuColumn>
<StyledContainer>
<WorkflowStepFilterLogicalOperatorCell
index={stepFilterIndex}
stepFilterGroup={stepFilterGroup}
firstFilterLabel={firstFilterLabel}
/>
{!readonly && (
{shouldShowDropdown && (
<WorkflowStepFilterOptionsDropdown stepFilterId={stepFilter.id} />
)}
</StyledContainer>
@@ -2,8 +2,8 @@ import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetada
import { SelectControl } from '@/ui/input/components/SelectControl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { WorkflowDropdownStepOutputItems } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowDropdownStepOutputItems';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
import { WorkflowDropdownStepOutputItems } from '@/workflow/workflow-steps/components/WorkflowDropdownStepOutputItems';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import { WorkflowVariablesDropdownSteps } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownSteps';
import { useAvailableVariablesInWorkflowStep } from '@/workflow/workflow-variables/hooks/useAvailableVariablesInWorkflowStep';
import { useSearchVariable } from '@/workflow/workflow-variables/hooks/useSearchVariable';
@@ -1,7 +1,7 @@
import { WorkflowStepFilterAddFilterRuleSelect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterAddFilterRuleSelect';
import { WorkflowStepFilterColumn } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterColumn';
import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useChildStepFiltersAndChildStepFilterGroups';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
import { WorkflowStepFilterAddFilterRuleSelect } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterAddFilterRuleSelect';
import { WorkflowStepFilterColumn } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterColumn';
import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow-steps/filters/hooks/useChildStepFiltersAndChildStepFilterGroups';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import styled from '@emotion/styled';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
@@ -1,8 +1,8 @@
import { AdvancedFilterCommandMenuColumn } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuColumn';
import { WorkflowStepFilterGroupChildren } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupChildren';
import { WorkflowStepFilterGroupOptionsDropdown } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupOptionsDropdown';
import { WorkflowStepFilterLogicalOperatorCell } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterLogicalOperatorCell';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
import { WorkflowStepFilterGroupChildren } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupChildren';
import { WorkflowStepFilterGroupOptionsDropdown } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupOptionsDropdown';
import { WorkflowStepFilterLogicalOperatorCell } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterLogicalOperatorCell';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import styled from '@emotion/styled';
import { useContext } from 'react';
import { type StepFilterGroup } from 'twenty-shared/types';
@@ -2,8 +2,8 @@ import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advance
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useRemoveStepFilterGroup } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useRemoveStepFilterGroup';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
import { useRemoveStepFilterGroup } from '@/workflow/workflow-steps/filters/hooks/useRemoveStepFilterGroup';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import { useContext } from 'react';
import { t } from '@lingui/core/macro';
import { IconDotsVertical, IconTrash } from 'twenty-ui/display';
@@ -1,11 +1,11 @@
import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
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 { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/filters/hooks/useUpsertStepFilterSettings';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import { useLingui } from '@lingui/react/macro';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useContext, useMemo } from 'react';
import { StepLogicalOperator, type StepFilterGroup } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
@@ -27,14 +27,17 @@ const StyledContainer = styled.div`
type WorkflowStepFilterLogicalOperatorCellProps = {
index: number;
stepFilterGroup: StepFilterGroup;
firstFilterLabel?: string;
};
export const WorkflowStepFilterLogicalOperatorCell = ({
index,
stepFilterGroup,
firstFilterLabel,
}: WorkflowStepFilterLogicalOperatorCellProps) => {
const { readonly } = useContext(WorkflowStepFilterContext);
const { t } = useLingui();
const defaultFirstFilterLabel = t`Where`;
const { upsertStepFilterSettings } = useUpsertStepFilterSettings();
@@ -66,7 +69,7 @@ export const WorkflowStepFilterLogicalOperatorCell = ({
return (
<StyledContainer>
{index === 0 ? (
<StyledText>{t`Where`}</StyledText>
<StyledText>{firstFilterLabel ?? defaultFirstFilterLabel}</StyledText>
) : index === 1 ? (
readonly ? (
<Select
@@ -5,9 +5,9 @@ import { Select } from '@/ui/input/components/Select';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { WORKFLOW_TIMEZONE } from '@/workflow/constants/WorkflowTimeZone';
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 { getStepFilterOperands } from '@/workflow/workflow-steps/workflow-actions/filter-action/utils/getStepFilterOperands';
import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/filters/hooks/useUpsertStepFilterSettings';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import { getStepFilterOperands } from '@/workflow/workflow-steps/filters/utils/getStepFilterOperands';
import { useContext } from 'react';
import { DEFAULT_RELATIVE_DATE_FILTER_VALUE } from 'twenty-shared/constants';
import { ViewFilterOperand, type StepFilter } from 'twenty-shared/types';
@@ -2,8 +2,8 @@ import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advance
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useRemoveStepFilter } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useRemoveStepFilter';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
import { useRemoveStepFilter } from '@/workflow/workflow-steps/filters/hooks/useRemoveStepFilter';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import { useContext } from 'react';
import { t } from '@lingui/core/macro';
import { IconDotsVertical, IconTrash } from 'twenty-ui/display';
@@ -4,7 +4,7 @@ import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types
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 { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { useContext } from 'react';
import { FieldActorSource, type StepFilter } from 'twenty-shared/types';
@@ -12,9 +12,9 @@ import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldM
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
import { WORKFLOW_TIMEZONE } from '@/workflow/constants/WorkflowTimeZone';
import { WorkflowStepFilterValueCompositeInput } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueCompositeInput';
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 { WorkflowStepFilterValueCompositeInput } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterValueCompositeInput';
import { useUpsertStepFilterSettings } from '@/workflow/workflow-steps/filters/hooks/useUpsertStepFilterSettings';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { useLingui } from '@lingui/react/macro';
import { isObject, isString } from '@sniptt/guards';
@@ -8,7 +8,7 @@ import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/Workflow
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { WorkflowStepFilterAddFilterRuleSelect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterAddFilterRuleSelect';
import { WorkflowStepFilterAddFilterRuleSelect } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterAddFilterRuleSelect';
const STEP_FILTER_GROUP: StepFilterGroup = {
id: 'filter-group-1',
@@ -7,7 +7,7 @@ import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/Workflow
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { WorkflowStepFilterAddRootStepFilterButton } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterAddRootStepFilterButton';
import { WorkflowStepFilterAddRootStepFilterButton } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterAddRootStepFilterButton';
const meta: Meta<typeof WorkflowStepFilterAddRootStepFilterButton> = {
title:
@@ -12,7 +12,7 @@ import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/Workflow
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { WorkflowStepFilterColumn } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterColumn';
import { WorkflowStepFilterColumn } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterColumn';
const STEP_FILTER_GROUP: StepFilterGroup = {
id: 'filter-group-1',
@@ -7,7 +7,7 @@ import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/Workflow
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { WorkflowStepFilterFieldSelect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterFieldSelect';
import { WorkflowStepFilterFieldSelect } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterFieldSelect';
const DEFAULT_STEP_FILTER: StepFilter = {
id: 'filter-1',
@@ -8,7 +8,7 @@ import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/Workflow
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { WorkflowStepFilterLogicalOperatorCell } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterLogicalOperatorCell';
import { WorkflowStepFilterLogicalOperatorCell } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterLogicalOperatorCell';
const AND_STEP_FILTER_GROUP: StepFilterGroup = {
id: 'filter-group-1',
@@ -1,5 +1,5 @@
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 { WorkflowStepFilterOperandSelect } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterOperandSelect';
import { type Meta, type StoryObj } from '@storybook/react';
import { expect, within } from '@storybook/test';
import { type StepFilter, ViewFilterOperand } from 'twenty-shared/types';
@@ -8,7 +8,7 @@ import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/Workflow
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { WorkflowStepFilterValueInput } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterValueInput';
import { WorkflowStepFilterValueInput } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterValueInput';
const TEXT_FILTER: StepFilter = {
id: 'filter-1',
@@ -1,10 +1,10 @@
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useSetRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyState';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFilterGroupsComponentState';
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFiltersComponentState';
import { hasInitializedCurrentStepFilterGroupsComponentFamilyState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/hasInitializedCurrentStepFilterGroupsComponentFamilyState';
import { hasInitializedCurrentStepFiltersComponentFamilyState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/hasInitializedCurrentStepFiltersComponentFamilyState';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFilterGroupsComponentState';
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFiltersComponentState';
import { hasInitializedCurrentStepFilterGroupsComponentFamilyState } from '@/workflow/workflow-steps/filters/states/hasInitializedCurrentStepFilterGroupsComponentFamilyState';
import { hasInitializedCurrentStepFiltersComponentFamilyState } from '@/workflow/workflow-steps/filters/states/hasInitializedCurrentStepFiltersComponentFamilyState';
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import {
@@ -1,6 +1,6 @@
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFilterGroupsComponentState';
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFiltersComponentState';
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFilterGroupsComponentState';
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFiltersComponentState';
import { type StepFilter, type StepFilterGroup } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -1,11 +1,11 @@
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFilterGroupsComponentState';
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFiltersComponentState';
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFilterGroupsComponentState';
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFiltersComponentState';
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
export const useRemoveStepFilter = () => {
const { onFilterSettingsUpdate } = useContext(WorkflowStepFilterContext);
@@ -1,8 +1,8 @@
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFilterGroupsComponentState';
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFiltersComponentState';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFilterGroupsComponentState';
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFiltersComponentState';
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@@ -1,8 +1,8 @@
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFilterGroupsComponentState';
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFiltersComponentState';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFilterGroupsComponentState';
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFiltersComponentState';
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { type StepFilter, type StepFilterGroup } from 'twenty-shared/types';
@@ -1,9 +1,11 @@
import { type FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
import { type FilterSettings } from '@/workflow/workflow-steps/filters/types/FilterSettings';
import { createContext } from 'react';
type WorkflowStepFilterContextType = {
stepId: string;
onFilterSettingsUpdate: (filterSettings: FilterSettings) => void;
onFilterSettingsUpdate: (
filterSettings: FilterSettings,
) => void | Promise<void>;
readonly?: boolean;
};
@@ -1,5 +1,5 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFilterGroupsComponentInstanceContext';
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/filters/states/context/StepFilterGroupsComponentInstanceContext';
import { type StepFilterGroup } from 'twenty-shared/types';
export const currentStepFilterGroupsComponentState = createComponentState<
@@ -1,5 +1,5 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFiltersComponentInstanceContext';
import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/filters/states/context/StepFiltersComponentInstanceContext';
import { type StepFilter } from 'twenty-shared/types';
export const currentStepFiltersComponentState = createComponentState<
@@ -1,5 +1,5 @@
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFilterGroupsComponentInstanceContext';
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/filters/states/context/StepFilterGroupsComponentInstanceContext';
export const hasInitializedCurrentStepFilterGroupsComponentFamilyState =
createComponentFamilyState<boolean, { stepId: string }>({
@@ -1,5 +1,5 @@
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFiltersComponentInstanceContext';
import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/filters/states/context/StepFiltersComponentInstanceContext';
export const hasInitializedCurrentStepFiltersComponentFamilyState =
createComponentFamilyState<boolean, { stepId: string }>({
@@ -1,6 +1,6 @@
import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector';
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFilterGroupsComponentInstanceContext';
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/currentStepFilterGroupsComponentState';
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/filters/states/context/StepFilterGroupsComponentInstanceContext';
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFilterGroupsComponentState';
import { isDefined } from 'twenty-shared/utils';
export const rootLevelStepFilterGroupComponentSelector =
@@ -0,0 +1,6 @@
import { type StepFilter, type StepFilterGroup } from 'twenty-shared/types';
export type FilterSettings = {
stepFilterGroups?: StepFilterGroup[];
stepFilters?: StepFilter[];
};
@@ -34,12 +34,16 @@ export const useCreateStep = () => {
nextStepId,
position,
connectionOptions,
shouldSelectNode = true,
workflowVersionId: providedWorkflowVersionId,
}: {
newStepType: WorkflowActionType;
parentStepId: string | undefined;
nextStepId: string | undefined;
position?: { x: number; y: number };
connectionOptions?: WorkflowStepConnectionOptions;
shouldSelectNode?: boolean;
workflowVersionId?: string;
}) => {
if (isLoading === true) {
return;
@@ -48,7 +52,8 @@ export const useCreateStep = () => {
setIsLoading(true);
try {
const workflowVersionId = await getUpdatableWorkflowVersion();
const workflowVersionId =
providedWorkflowVersionId ?? (await getUpdatableWorkflowVersion());
const id = v4();
const workflowVersionStepChanges = (
@@ -77,7 +82,9 @@ export const useCreateStep = () => {
throw new Error("Couldn't create step");
}
setWorkflowSelectedNode(id);
if (shouldSelectNode) {
setWorkflowSelectedNode(id);
}
setWorkflowLastCreatedStepId(id);
return isDefined(createdFirstStepDiff)
@@ -3,8 +3,10 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import { type WorkflowIfElseAction } from '@/workflow/types/Workflow';
import { useDeleteWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useDeleteWorkflowVersionStep';
import { useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose';
import { getEmptyChildStepIds } from '@/workflow/workflow-steps/workflow-actions/if-else-action/utils/getEmptyChildStepIds';
import { useStepsOutputSchema } from '@/workflow/workflow-variables/hooks/useStepsOutputSchema';
import { isDefined } from 'twenty-shared/utils';
@@ -25,11 +27,35 @@ export const useDeleteStep = () => {
const deleteStep = async (stepId: string) => {
const workflowVersionId = await getUpdatableWorkflowVersion();
const isAiAgentStep =
isDefined(workflow?.currentVersion?.steps) &&
workflow.currentVersion.steps.some(
(step) => step.id === stepId && step.type === 'AI_AGENT',
);
const steps = workflow?.currentVersion?.steps;
const stepToDelete = isDefined(steps)
? steps.find((step) => step.id === stepId)
: undefined;
if (
isDefined(stepToDelete) &&
isDefined(steps) &&
stepToDelete.type === 'IF_ELSE'
) {
const emptyChildStepIds = getEmptyChildStepIds({
ifElseAction: stepToDelete as WorkflowIfElseAction,
allSteps: steps,
});
for (const emptyChildStepId of emptyChildStepIds) {
await deleteWorkflowVersionStep({
workflowVersionId,
stepId: emptyChildStepId,
});
}
if (emptyChildStepIds.length > 0) {
deleteStepsOutputSchema({
stepIds: emptyChildStepIds,
workflowVersionId,
});
}
}
await deleteWorkflowVersionStep({
workflowVersionId,
@@ -43,7 +69,7 @@ export const useDeleteStep = () => {
workflowVersionId,
});
if (isAiAgentStep) {
if (isDefined(stepToDelete) && stepToDelete.type === 'AI_AGENT') {
resetPermissionState();
}
};
@@ -1,10 +1,14 @@
import { type WorkflowActionType } from '@/workflow/types/Workflow';
import { DELAY_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/DelayAction';
import { FILTER_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/FilterAction';
import { IF_ELSE_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/IfElseAction';
import { ITERATOR_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/IteratorAction';
export const FLOW_ACTIONS: Array<{
defaultLabel: string;
type: Extract<WorkflowActionType, 'ITERATOR' | 'FILTER' | 'DELAY'>;
type: Extract<
WorkflowActionType,
'ITERATOR' | 'FILTER' | 'IF_ELSE' | 'DELAY'
>;
icon: string;
}> = [ITERATOR_ACTION, FILTER_ACTION, DELAY_ACTION];
}> = [ITERATOR_ACTION, FILTER_ACTION, IF_ELSE_ACTION, DELAY_ACTION];
@@ -0,0 +1,11 @@
import { type WorkflowActionType } from '@/workflow/types/Workflow';
export const IF_ELSE_ACTION: {
defaultLabel: string;
type: Extract<WorkflowActionType, 'IF_ELSE'>;
icon: string;
} = {
defaultLabel: 'If/else',
type: 'IF_ELSE',
icon: 'IconArrowsSplit',
};
@@ -1,10 +1,9 @@
import { type WorkflowFilterAction } from '@/workflow/types/Workflow';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { WorkflowEditActionFilterBodyEffect } from '@/workflow/workflow-steps/filters/components/WorkflowEditActionFilterBodyEffect';
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/filters/states/context/StepFilterGroupsComponentInstanceContext';
import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/filters/states/context/StepFiltersComponentInstanceContext';
import { WorkflowEditActionFilterBody } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBody';
import { WorkflowEditActionFilterBodyEffect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBodyEffect';
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFilterGroupsComponentInstanceContext';
import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFiltersComponentInstanceContext';
import { type StepFilter, type StepFilterGroup } from 'twenty-shared/types';
type WorkflowEditActionFilterProps = {
action: WorkflowFilterAction;
@@ -18,11 +17,6 @@ type WorkflowEditActionFilterProps = {
};
};
export type FilterSettings = {
stepFilterGroups?: StepFilterGroup[];
stepFilters?: StepFilter[];
};
export const WorkflowEditActionFilter = ({
action,
actionOptions,
@@ -2,15 +2,15 @@ import { InputLabel } from '@/ui/input/components/InputLabel';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { type WorkflowFilterAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { type FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
import { WorkflowStepFilterAddFilterRuleSelect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterAddFilterRuleSelect';
import { WorkflowStepFilterAddRootStepFilterButton } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterAddRootStepFilterButton';
import { WorkflowStepFilterColumn } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterColumn';
import { WorkflowStepFilterGroupColumn } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterGroupColumn';
import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useChildStepFiltersAndChildStepFilterGroups';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/WorkflowStepFilterContext';
import { rootLevelStepFilterGroupComponentSelector } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/rootLevelStepFilterGroupComponentSelector';
import { isStepFilterGroupChildAStepFilterGroup } from '@/workflow/workflow-steps/workflow-actions/filter-action/utils/isStepFilterGroupChildAStepFilterGroup';
import { WorkflowStepFilterAddFilterRuleSelect } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterAddFilterRuleSelect';
import { WorkflowStepFilterAddRootStepFilterButton } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterAddRootStepFilterButton';
import { WorkflowStepFilterColumn } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterColumn';
import { WorkflowStepFilterGroupColumn } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupColumn';
import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow-steps/filters/hooks/useChildStepFiltersAndChildStepFilterGroups';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import { rootLevelStepFilterGroupComponentSelector } from '@/workflow/workflow-steps/filters/states/rootLevelStepFilterGroupComponentSelector';
import { type FilterSettings } from '@/workflow/workflow-steps/filters/types/FilterSettings';
import { isStepFilterGroupChildAStepFilterGroup } from '@/workflow/workflow-steps/filters/utils/isStepFilterGroupChildAStepFilterGroup';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
@@ -1,5 +1,5 @@
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFilterGroupsComponentInstanceContext';
import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFiltersComponentInstanceContext';
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/filters/states/context/StepFilterGroupsComponentInstanceContext';
import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/filters/states/context/StepFiltersComponentInstanceContext';
import { type Decorator } from '@storybook/react';
export const WorkflowStepFilterDecorator: Decorator = (Story) => {
@@ -0,0 +1,52 @@
import { type WorkflowIfElseAction } from '@/workflow/types/Workflow';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { WorkflowEditActionFilterBodyEffect } from '@/workflow/workflow-steps/filters/components/WorkflowEditActionFilterBodyEffect';
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/filters/states/context/StepFilterGroupsComponentInstanceContext';
import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/filters/states/context/StepFiltersComponentInstanceContext';
import { WorkflowEditActionIfElseBody } from '@/workflow/workflow-steps/workflow-actions/if-else-action/components/WorkflowEditActionIfElseBody';
type WorkflowEditActionIfElseProps = {
action: WorkflowIfElseAction;
actionOptions:
| {
readonly: true;
}
| {
readonly?: false;
onActionUpdate: (action: WorkflowIfElseAction) => void;
};
};
export const WorkflowEditActionIfElse = ({
action,
actionOptions,
}: WorkflowEditActionIfElseProps) => {
return (
<>
<StepFiltersComponentInstanceContext.Provider
value={{
instanceId: action.id,
}}
>
<StepFilterGroupsComponentInstanceContext.Provider
value={{
instanceId: action.id,
}}
>
<WorkflowEditActionIfElseBody
action={action}
actionOptions={actionOptions}
/>
<WorkflowEditActionFilterBodyEffect
stepId={action.id}
defaultValue={{
stepFilterGroups: action.settings.input.stepFilterGroups,
stepFilters: action.settings.input.stepFilters,
}}
/>
</StepFilterGroupsComponentInstanceContext.Provider>
</StepFiltersComponentInstanceContext.Provider>
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
</>
);
};
@@ -0,0 +1,299 @@
import { InputLabel } from '@/ui/input/components/InputLabel';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import {
type WorkflowIfElseAction,
type WorkflowStep,
} from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { currentStepFilterGroupsComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFilterGroupsComponentState';
import { currentStepFiltersComponentState } from '@/workflow/workflow-steps/filters/states/currentStepFiltersComponentState';
import { type FilterSettings } from '@/workflow/workflow-steps/filters/types/FilterSettings';
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
import { useDeleteWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useDeleteWorkflowVersionStep';
import { WorkflowIfElseBranchEditor } from '@/workflow/workflow-steps/workflow-actions/if-else-action/components/WorkflowIfElseBranchEditor';
import { calculateElseIfBranchPosition } from '@/workflow/workflow-steps/workflow-actions/if-else-action/utils/calculateElseIfBranchPosition';
import { calculateExistingBranchPositions } from '@/workflow/workflow-steps/workflow-actions/if-else-action/utils/calculateExistingBranchPositions';
import { createElseIfBranch } from '@/workflow/workflow-steps/workflow-actions/if-else-action/utils/createElseIfBranch';
import { getBranchesToDelete } from '@/workflow/workflow-steps/workflow-actions/if-else-action/utils/getBranchesToDelete';
import { getBranchLabel } from '@/workflow/workflow-steps/workflow-actions/if-else-action/utils/getBranchLabel';
import { useStepsOutputSchema } from '@/workflow/workflow-variables/hooks/useStepsOutputSchema';
import { useTidyUpWorkflowVersion } from '@/workflow/workflow-version/hooks/useTidyUpWorkflowVersion';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { Fragment } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type StepIfElseBranch } from 'twenty-shared/workflow';
import { HorizontalSeparator, IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
const StyledContainer = styled.div`
align-items: start;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
`;
const StyledBodyContainer = styled(WorkflowStepBody)`
gap: ${({ theme }) => theme.spacing(2)};
`;
type WorkflowEditActionIfElseBodyProps = {
action: WorkflowIfElseAction;
actionOptions:
| {
readonly: true;
}
| {
readonly?: false;
onActionUpdate: (action: WorkflowIfElseAction) => void;
};
};
export const WorkflowEditActionIfElseBody = ({
action,
actionOptions,
}: WorkflowEditActionIfElseBodyProps) => {
const branches = action.settings.input.branches;
const stepFilterGroups = action.settings.input.stepFilterGroups ?? [];
const stepFilters = action.settings.input.stepFilters ?? [];
const isReadonly = actionOptions.readonly === true;
const { createStep } = useCreateStep();
const { getUpdatableWorkflowVersion } =
useGetUpdatableWorkflowVersionOrThrow();
const { updateWorkflowVersionPosition } = useTidyUpWorkflowVersion();
const { deleteWorkflowVersionStep } = useDeleteWorkflowVersionStep();
const { deleteStepsOutputSchema } = useStepsOutputSchema();
const workflowVisualizerWorkflowId = useRecoilComponentValue(
workflowVisualizerWorkflowIdComponentState,
);
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
const currentStepFilters = useRecoilComponentValue(
currentStepFiltersComponentState,
);
const currentStepFilterGroups = useRecoilComponentValue(
currentStepFilterGroupsComponentState,
);
const setCurrentStepFilters = useSetRecoilComponentState(
currentStepFiltersComponentState,
);
const setCurrentStepFilterGroups = useSetRecoilComponentState(
currentStepFilterGroupsComponentState,
);
const cleanupEmptyChildStepsFromDeletedBranches = async ({
branchesToDelete,
allSteps,
}: {
branchesToDelete: StepIfElseBranch[];
allSteps?: WorkflowStep[];
}): Promise<void> => {
if (branchesToDelete.length === 0 || !isDefined(allSteps)) {
return;
}
const workflowVersionId = await getUpdatableWorkflowVersion();
const childStepIdsFromDeletedBranches = branchesToDelete.flatMap(
(branch) => branch.nextStepIds,
);
const emptyChildStepIds = childStepIdsFromDeletedBranches.filter(
(childStepId) => {
const childStep = allSteps.find((step) => step.id === childStepId);
return isDefined(childStep) && childStep.type === 'EMPTY';
},
);
for (const emptyChildStepId of emptyChildStepIds) {
await deleteWorkflowVersionStep({
workflowVersionId,
stepId: emptyChildStepId,
});
}
if (emptyChildStepIds.length > 0) {
deleteStepsOutputSchema({
stepIds: emptyChildStepIds,
workflowVersionId,
});
}
};
const onFilterSettingsUpdate = async (newFilterSettings: FilterSettings) => {
if (isReadonly) {
return;
}
const updatedStepFilterGroups = newFilterSettings.stepFilterGroups ?? [];
const updatedStepFilters = newFilterSettings.stepFilters ?? [];
const remainingFilterGroupIds = new Set(
updatedStepFilterGroups.map((g) => g.id),
);
const branchesToDelete = getBranchesToDelete(
branches,
remainingFilterGroupIds,
);
const updatedBranches =
branchesToDelete.length > 0
? branches.filter((branch) => !branchesToDelete.includes(branch))
: branches;
await cleanupEmptyChildStepsFromDeletedBranches({
branchesToDelete,
allSteps: workflow?.currentVersion?.steps ?? undefined,
});
setCurrentStepFilterGroups(updatedStepFilterGroups);
setCurrentStepFilters(updatedStepFilters);
actionOptions.onActionUpdate({
...action,
settings: {
...action.settings,
input: {
...action.settings.input,
stepFilterGroups: updatedStepFilterGroups,
stepFilters: updatedStepFilters,
branches: updatedBranches,
},
},
});
};
const handleAddRoute = async (event?: React.MouseEvent<HTMLElement>) => {
if (isDefined(event)) {
event.preventDefault();
event.stopPropagation();
}
if (isReadonly) {
return;
}
const { filterGroup, filter, branchId, filterGroupId } =
createElseIfBranch();
const totalBranches = branches.length + 1;
const elseIfBranchIndex = branches.length - 1;
const ifElseStepPosition = action.position ?? { x: 0, y: 0 };
const workflowVersionId = await getUpdatableWorkflowVersion();
const existingBranchPositions = calculateExistingBranchPositions({
branches,
elseIfBranchIndex,
totalBranches,
ifElseStepPosition,
});
const newEmptyNodePosition = calculateElseIfBranchPosition(
elseIfBranchIndex,
totalBranches,
ifElseStepPosition,
);
if (existingBranchPositions.length > 0) {
await updateWorkflowVersionPosition(
workflowVersionId,
existingBranchPositions,
);
}
const emptyNode = await createStep({
newStepType: 'EMPTY',
parentStepId: undefined,
nextStepId: undefined,
position: newEmptyNodePosition,
shouldSelectNode: false,
workflowVersionId,
});
if (!isDefined(emptyNode)) {
return;
}
const newBranch = {
id: branchId,
filterGroupId,
nextStepIds: [emptyNode.id],
};
const updatedBranches = [...branches];
updatedBranches.splice(branches.length - 1, 0, newBranch);
const updatedStepFilterGroups = [...stepFilterGroups, filterGroup];
const updatedStepFilters = [...stepFilters, filter];
setCurrentStepFilterGroups([...currentStepFilterGroups, filterGroup]);
setCurrentStepFilters([...currentStepFilters, filter]);
actionOptions.onActionUpdate({
...action,
settings: {
...action.settings,
input: {
...action.settings.input,
stepFilterGroups: updatedStepFilterGroups,
stepFilters: updatedStepFilters,
branches: updatedBranches,
},
},
});
};
return (
<StyledBodyContainer>
<InputLabel>{t`Conditions`}</InputLabel>
<StyledContainer>
{branches.map((branch, branchIndex) => {
const branchFilterGroup = isDefined(branch.filterGroupId)
? stepFilterGroups.find((g) => g.id === branch.filterGroupId)
: undefined;
const isElse =
branchIndex === branches.length - 1 &&
!isDefined(branch.filterGroupId);
return (
<Fragment key={branch.id}>
{branchIndex > 0 && !isElse && <HorizontalSeparator noMargin />}
{isElse && !isReadonly && (
<>
<HorizontalSeparator noMargin />
<Button
Icon={IconPlus}
title={t`Add route`}
variant="secondary"
size="small"
onClick={(event) => handleAddRoute(event)}
/>
</>
)}
{isElse && <HorizontalSeparator noMargin />}
<WorkflowIfElseBranchEditor
action={action}
branch={branch}
branchIndex={branchIndex}
branchLabel={getBranchLabel({
branchIndex,
totalBranches: branches.length,
branch,
})}
branchFilterGroup={branchFilterGroup}
readonly={isReadonly}
onFilterSettingsUpdate={onFilterSettingsUpdate}
/>
</Fragment>
);
})}
</StyledContainer>
</StyledBodyContainer>
);
};
@@ -0,0 +1,102 @@
import { type WorkflowIfElseAction } from '@/workflow/types/Workflow';
import { WorkflowStepFilterAddFilterRuleSelect } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterAddFilterRuleSelect';
import { WorkflowStepFilterColumn } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterColumn';
import { WorkflowStepFilterGroupColumn } from '@/workflow/workflow-steps/filters/components/WorkflowStepFilterGroupColumn';
import { useChildStepFiltersAndChildStepFilterGroups } from '@/workflow/workflow-steps/filters/hooks/useChildStepFiltersAndChildStepFilterGroups';
import { WorkflowStepFilterContext } from '@/workflow/workflow-steps/filters/states/context/WorkflowStepFilterContext';
import { type FilterSettings } from '@/workflow/workflow-steps/filters/types/FilterSettings';
import { isStepFilterGroupChildAStepFilterGroup } from '@/workflow/workflow-steps/filters/utils/isStepFilterGroupChildAStepFilterGroup';
import styled from '@emotion/styled';
import { i18n, type MessageDescriptor } from '@lingui/core';
import { type StepFilterGroup } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type StepIfElseBranch } from 'twenty-shared/workflow';
const StyledBranchContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledFiltersContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
width: 100%;
`;
type WorkflowIfElseBranchEditorProps = {
action: WorkflowIfElseAction;
branch: StepIfElseBranch;
branchIndex: number;
branchLabel: MessageDescriptor;
branchFilterGroup: StepFilterGroup | undefined;
readonly: boolean;
onFilterSettingsUpdate: (filterSettings: FilterSettings) => Promise<void>;
};
export const WorkflowIfElseBranchEditor = ({
action,
branch,
branchIndex,
branchLabel,
branchFilterGroup,
readonly,
onFilterSettingsUpdate,
}: WorkflowIfElseBranchEditorProps) => {
const isElseBranch = !isDefined(branch.filterGroupId);
const { childStepFiltersAndChildStepFilterGroups } =
useChildStepFiltersAndChildStepFilterGroups({
stepFilterGroupId: branchFilterGroup?.id ?? '',
});
if (isElseBranch) {
return null;
}
const isIfBranch = branchIndex === 0;
return (
<WorkflowStepFilterContext.Provider
value={{
stepId: action.id,
readonly,
onFilterSettingsUpdate,
}}
>
<StyledBranchContainer>
<StyledFiltersContainer>
{isDefined(branchFilterGroup) &&
childStepFiltersAndChildStepFilterGroups.map(
(stepFilterGroupChild, stepFilterGroupChildIndex) =>
isStepFilterGroupChildAStepFilterGroup(stepFilterGroupChild) ? (
<WorkflowStepFilterGroupColumn
key={stepFilterGroupChild.id}
parentStepFilterGroup={branchFilterGroup}
stepFilterGroup={stepFilterGroupChild}
stepFilterGroupIndex={stepFilterGroupChildIndex}
/>
) : (
<WorkflowStepFilterColumn
key={stepFilterGroupChild.id}
stepFilterGroup={branchFilterGroup}
stepFilter={stepFilterGroupChild}
stepFilterIndex={stepFilterGroupChildIndex}
isIfBranch={isIfBranch}
firstFilterLabel={i18n._(branchLabel)}
/>
),
)}
</StyledFiltersContainer>
{!readonly && isDefined(branchFilterGroup) && (
<WorkflowStepFilterAddFilterRuleSelect
stepFilterGroup={branchFilterGroup}
/>
)}
</StyledBranchContainer>
</WorkflowStepFilterContext.Provider>
);
};
@@ -0,0 +1,30 @@
import { IF_ELSE_BRANCH_POSITION_OFFSETS } from 'twenty-shared/workflow';
const BRANCH_HORIZONTAL_SPACING = 200;
export const calculateElseIfBranchPosition = (
branchIndex: number,
totalBranches: number,
ifElseStepPosition: { x: number; y: number },
): { x: number; y: number } => {
const isIfBranch = branchIndex === 0;
const isElseBranch = branchIndex === totalBranches - 1;
const totalWidth = (totalBranches - 1) * BRANCH_HORIZONTAL_SPACING;
let positionX: number;
if (isIfBranch) {
positionX = -totalWidth / 2;
} else if (isElseBranch) {
positionX = totalWidth / 2;
} else {
const spacing = totalWidth / (totalBranches - 1);
positionX = -totalWidth / 2 + branchIndex * spacing;
}
return {
x: ifElseStepPosition.x + positionX,
y: ifElseStepPosition.y + IF_ELSE_BRANCH_POSITION_OFFSETS.IF.y,
};
};
@@ -0,0 +1,41 @@
import { isDefined } from 'twenty-shared/utils';
import { type StepIfElseBranch } from 'twenty-shared/workflow';
import { calculateElseIfBranchPosition } from './calculateElseIfBranchPosition';
export const calculateExistingBranchPositions = ({
branches,
elseIfBranchIndex,
totalBranches,
ifElseStepPosition,
}: {
branches: StepIfElseBranch[];
elseIfBranchIndex: number;
totalBranches: number;
ifElseStepPosition: { x: number; y: number };
}): Array<{ id: string; position: { x: number; y: number } }> => {
return branches.reduce<
Array<{ id: string; position: { x: number; y: number } }>
>((acc, branch, branchIndex) => {
const firstChildStepId = branch.nextStepIds[0];
if (!isDefined(firstChildStepId)) {
return acc;
}
const adjustedBranchIndex =
branchIndex < elseIfBranchIndex ? branchIndex : branchIndex + 1;
const branchNodePosition = calculateElseIfBranchPosition(
adjustedBranchIndex,
totalBranches,
ifElseStepPosition,
);
acc.push({
id: firstChildStepId,
position: branchNodePosition,
});
return acc;
}, []);
};
@@ -0,0 +1,37 @@
import {
StepLogicalOperator,
ViewFilterOperand,
type StepFilter,
type StepFilterGroup,
} from 'twenty-shared/types';
import { v4 } from 'uuid';
export const createElseIfBranch = (): {
filterGroup: StepFilterGroup;
filter: StepFilter;
branchId: string;
filterGroupId: string;
} => {
const newFilterGroupId = v4();
const newFilterId = v4();
const newBranchId = v4();
return {
filterGroup: {
id: newFilterGroupId,
logicalOperator: StepLogicalOperator.AND,
positionInStepFilterGroup: 0,
},
filter: {
id: newFilterId,
type: 'unknown',
stepOutputKey: '',
operand: ViewFilterOperand.IS,
value: '',
stepFilterGroupId: newFilterGroupId,
positionInStepFilterGroup: 0,
},
branchId: newBranchId,
filterGroupId: newFilterGroupId,
};
};
@@ -0,0 +1,28 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { type StepIfElseBranch } from 'twenty-shared/workflow';
import { isDefined } from 'twenty-shared/utils';
export const getBranchLabel = ({
branchIndex,
totalBranches,
branch,
}: {
branchIndex: number;
totalBranches: number;
branch?: StepIfElseBranch;
}): MessageDescriptor => {
if (branchIndex === 0) {
return msg`if`;
}
const isElseBranch =
branchIndex === totalBranches - 1 &&
(!isDefined(branch) || !isDefined(branch.filterGroupId));
if (isElseBranch) {
return msg`else`;
}
return msg`else if`;
};
@@ -0,0 +1,27 @@
import { type StepIfElseBranch } from 'twenty-shared/workflow';
import { isDefined } from 'twenty-shared/utils';
export const getBranchesToDelete = (
branches: StepIfElseBranch[],
remainingFilterGroupIds: Set<string>,
): StepIfElseBranch[] => {
const existingBranchFilterGroupIds = new Set(
branches.map((b) => b.filterGroupId).filter(isDefined),
);
return branches.filter((branch, branchIndex) => {
const isIfBranch = branchIndex === 0;
const isElseBranch =
branchIndex === branches.length - 1 && !isDefined(branch.filterGroupId);
if (isIfBranch || isElseBranch) {
return false;
}
return (
isDefined(branch.filterGroupId) &&
existingBranchFilterGroupIds.has(branch.filterGroupId) &&
!remainingFilterGroupIds.has(branch.filterGroupId)
);
});
};
@@ -0,0 +1,21 @@
import {
type WorkflowIfElseAction,
type WorkflowStep,
} from '@/workflow/types/Workflow';
import { isDefined } from 'twenty-shared/utils';
export const getEmptyChildStepIds = ({
ifElseAction,
allSteps,
}: {
ifElseAction: WorkflowIfElseAction;
allSteps: WorkflowStep[];
}): string[] => {
const branches = ifElseAction.settings.input.branches;
const childStepIds = branches.flatMap((branch) => branch.nextStepIds);
return childStepIds.filter((childStepId) => {
const childStep = allSteps.find((step) => step.id === childStepId);
return isDefined(childStep) && childStep.type === 'EMPTY';
});
};
@@ -24,6 +24,7 @@ export const getActionIcon = (actionType: WorkflowActionType) => {
case 'ITERATOR':
case 'DELAY':
case 'FILTER':
case 'IF_ELSE':
return FLOW_ACTIONS.find((item) => item.type === actionType)?.icon;
case 'EMPTY':
return 'IconSettingsAutomation';
@@ -25,6 +25,7 @@ export const getActionIconColorOrThrow = ({
case 'ITERATOR':
case 'EMPTY':
case 'FILTER':
case 'IF_ELSE':
case 'DELAY':
return theme.color.green12;
case 'AI_AGENT':