feat: Add If/Else node (#16833)
Closes [#1265](https://github.com/twentyhq/core-team-issues/issues/1265)
This commit is contained in:
@@ -1343,6 +1343,7 @@ export enum FeatureFlagKey {
|
||||
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
|
||||
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
|
||||
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
|
||||
IS_IF_ELSE_ENABLED = 'IS_IF_ELSE_ENABLED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
IS_PAGE_LAYOUT_ENABLED = 'IS_PAGE_LAYOUT_ENABLED',
|
||||
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
|
||||
@@ -5010,6 +5011,7 @@ export enum WorkflowActionType {
|
||||
FIND_RECORDS = 'FIND_RECORDS',
|
||||
FORM = 'FORM',
|
||||
HTTP_REQUEST = 'HTTP_REQUEST',
|
||||
IF_ELSE = 'IF_ELSE',
|
||||
ITERATOR = 'ITERATOR',
|
||||
SEND_EMAIL = 'SEND_EMAIL',
|
||||
UPDATE_RECORD = 'UPDATE_RECORD',
|
||||
|
||||
@@ -1326,6 +1326,7 @@ export enum FeatureFlagKey {
|
||||
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
|
||||
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
|
||||
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
|
||||
IS_IF_ELSE_ENABLED = 'IS_IF_ELSE_ENABLED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
IS_PAGE_LAYOUT_ENABLED = 'IS_PAGE_LAYOUT_ENABLED',
|
||||
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
|
||||
@@ -4829,6 +4830,7 @@ export enum WorkflowActionType {
|
||||
FIND_RECORDS = 'FIND_RECORDS',
|
||||
FORM = 'FORM',
|
||||
HTTP_REQUEST = 'HTTP_REQUEST',
|
||||
IF_ELSE = 'IF_ELSE',
|
||||
ITERATOR = 'ITERATOR',
|
||||
SEND_EMAIL = 'SEND_EMAIL',
|
||||
UPDATE_RECORD = 'UPDATE_RECORD',
|
||||
|
||||
+6
-1
@@ -17,6 +17,9 @@ export const CommandMenuWorkflowSelectAction = ({
|
||||
onActionSelected: (actionType: WorkflowActionType) => void;
|
||||
}) => {
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
const isIfElseEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_IF_ELSE_ENABLED,
|
||||
);
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -46,7 +49,9 @@ export const CommandMenuWorkflowSelectAction = ({
|
||||
{t`Flow`}
|
||||
</RightDrawerWorkflowSelectStepTitle>
|
||||
<WorkflowActionMenuItems
|
||||
actions={FLOW_ACTIONS}
|
||||
actions={FLOW_ACTIONS.filter(
|
||||
(action) => action.type !== 'IF_ELSE' || isIfElseEnabled,
|
||||
)}
|
||||
onClick={onActionSelected}
|
||||
/>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type workflowFindRecordsActionSchema,
|
||||
type workflowFormActionSchema,
|
||||
type workflowHttpRequestActionSchema,
|
||||
type workflowIfElseActionSchema,
|
||||
type workflowIteratorActionSchema,
|
||||
type workflowManualTriggerSchema,
|
||||
type workflowRunSchema,
|
||||
@@ -50,6 +51,7 @@ export type WorkflowFindRecordsAction = z.infer<
|
||||
export type WorkflowDelayAction = z.infer<typeof workflowDelayActionSchema>;
|
||||
export type WorkflowFilterAction = z.infer<typeof workflowFilterActionSchema>;
|
||||
export type WorkflowFormAction = z.infer<typeof workflowFormActionSchema>;
|
||||
export type WorkflowIfElseAction = z.infer<typeof workflowIfElseActionSchema>;
|
||||
export type WorkflowHttpRequestAction = z.infer<
|
||||
typeof workflowHttpRequestActionSchema
|
||||
>;
|
||||
@@ -68,6 +70,7 @@ export type WorkflowAction =
|
||||
| WorkflowUpsertRecordAction
|
||||
| WorkflowFindRecordsAction
|
||||
| WorkflowFilterAction
|
||||
| WorkflowIfElseAction
|
||||
| WorkflowFormAction
|
||||
| WorkflowHttpRequestAction
|
||||
| WorkflowAiAgentAction
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
type WorkflowIfElseAction,
|
||||
type WorkflowStep,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
|
||||
import {
|
||||
type WorkflowDiagramEdge,
|
||||
type WorkflowDiagramNode,
|
||||
type WorkflowDiagramStepNodeData,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getEdgePathStrategy } from '@/workflow/workflow-diagram/utils/getEdgePathStrategy';
|
||||
import { getEdgeTypeBetweenTwoNodes } from '@/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes';
|
||||
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration';
|
||||
import { WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultSourceHandleId';
|
||||
import { WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramNodeDefaultTargetHandleId';
|
||||
import { getBranchLabel } from '@/workflow/workflow-steps/workflow-actions/if-else-action/utils/getBranchLabel';
|
||||
import { Position } from '@xyflow/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const generateNodesAndEdgesForIfElseNode = ({
|
||||
step,
|
||||
steps,
|
||||
xPos,
|
||||
yPos,
|
||||
nodes,
|
||||
edges,
|
||||
workflowContext,
|
||||
}: {
|
||||
step: WorkflowIfElseAction;
|
||||
steps: WorkflowStep[];
|
||||
yPos: number;
|
||||
xPos: number;
|
||||
nodes: readonly WorkflowDiagramNode[];
|
||||
edges: readonly WorkflowDiagramEdge[];
|
||||
workflowContext: WorkflowContext;
|
||||
}): {
|
||||
nodes: Array<WorkflowDiagramNode>;
|
||||
edges: Array<WorkflowDiagramEdge>;
|
||||
} => {
|
||||
const edgeTypeBetweenTwoNodes = getEdgeTypeBetweenTwoNodes({
|
||||
workflowContext,
|
||||
});
|
||||
|
||||
const updatedNodes = [...nodes];
|
||||
const updatedEdges = [...edges];
|
||||
|
||||
const ifElseNode: WorkflowDiagramNode = {
|
||||
id: step.id,
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: step.type,
|
||||
name: step.name,
|
||||
hasNextStepIds: true,
|
||||
stepId: step.id,
|
||||
position: step.position ?? {
|
||||
x: xPos,
|
||||
y: yPos,
|
||||
},
|
||||
} satisfies WorkflowDiagramStepNodeData,
|
||||
position: step.position ?? {
|
||||
x: xPos,
|
||||
y: yPos,
|
||||
},
|
||||
};
|
||||
|
||||
updatedNodes.push(ifElseNode);
|
||||
|
||||
const branches = step.settings.input.branches;
|
||||
const totalBranches = branches.length;
|
||||
|
||||
branches.forEach((branch, branchIndex) => {
|
||||
const label = getBranchLabel({
|
||||
branchIndex,
|
||||
totalBranches,
|
||||
branch,
|
||||
});
|
||||
|
||||
const nextStepIds = branch.nextStepIds;
|
||||
for (const nextStepId of nextStepIds) {
|
||||
const nextStep = steps.find((s) => s.id === nextStepId);
|
||||
if (!isDefined(nextStep)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
updatedEdges.push({
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||
type: edgeTypeBetweenTwoNodes,
|
||||
id: v4(),
|
||||
source: step.id,
|
||||
sourceHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_SOURCE_HANDLE_ID,
|
||||
target: nextStepId,
|
||||
deletable: false,
|
||||
selectable: false,
|
||||
reconnectable: false,
|
||||
targetHandle: WORKFLOW_DIAGRAM_NODE_DEFAULT_TARGET_HANDLE_ID,
|
||||
data: {
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION.data,
|
||||
labelOptions: {
|
||||
position: Position.Bottom,
|
||||
label,
|
||||
},
|
||||
edgePathStrategy: getEdgePathStrategy({
|
||||
step,
|
||||
steps,
|
||||
nextStepId,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
nodes: updatedNodes,
|
||||
edges: updatedEdges,
|
||||
};
|
||||
};
|
||||
+18
@@ -12,6 +12,7 @@ import {
|
||||
type WorkflowDiagramNode,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { generateNodesAndEdgesForDefaultNode } from '@/workflow/workflow-diagram/utils/generateNodesAndEdgesForDefaultNode';
|
||||
import { generateNodesAndEdgesForIfElseNode } from '@/workflow/workflow-diagram/utils/generateNodesAndEdgesForIfElseNode';
|
||||
import { generateNodesAndEdgesForIteratorNode } from '@/workflow/workflow-diagram/utils/generateNodesAndEdgesForIteratorNode';
|
||||
import { getEdgeTypeBetweenTwoNodes } from '@/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes';
|
||||
import { getWorkflowDiagramTriggerNode } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode';
|
||||
@@ -87,6 +88,23 @@ export const generateWorkflowDiagram = ({
|
||||
|
||||
break;
|
||||
}
|
||||
case 'IF_ELSE': {
|
||||
const { nodes: ifElseNodes, edges: ifElseEdges } =
|
||||
generateNodesAndEdgesForIfElseNode({
|
||||
step,
|
||||
steps,
|
||||
xPos,
|
||||
yPos: levelYPos,
|
||||
nodes,
|
||||
edges,
|
||||
workflowContext,
|
||||
});
|
||||
|
||||
nodes = ifElseNodes;
|
||||
edges = ifElseEdges;
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const { nodes: defaultNodes, edges: defaultEdges } =
|
||||
generateNodesAndEdgesForDefaultNode({
|
||||
|
||||
+38
-29
@@ -34,11 +34,14 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
|
||||
markerStart,
|
||||
markerEnd,
|
||||
data,
|
||||
deletable,
|
||||
}: WorkflowDiagramDefaultEdgeEditableProps) => {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const { isEdgeHovered } = useEdgeState();
|
||||
|
||||
const isEditable = deletable !== false;
|
||||
|
||||
const {
|
||||
segments,
|
||||
overlayPosition: [labelX, labelY],
|
||||
@@ -107,42 +110,48 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
|
||||
sourceX={sourceX}
|
||||
sourceY={sourceY}
|
||||
position={data.labelOptions.position}
|
||||
centerX={labelX}
|
||||
centerY={labelY}
|
||||
>
|
||||
<WorkflowDiagramEdgeLabel label={i18n._(data.labelOptions.label)} />
|
||||
</WorkflowDiagramEdgeLabelContainer>
|
||||
)}
|
||||
|
||||
<WorkflowDiagramEdgeV2Container
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
||||
labelX={labelX}
|
||||
labelY={labelY}
|
||||
>
|
||||
<WorkflowDiagramEdgeV2VisibilityContainer
|
||||
shouldDisplay={
|
||||
nodeCreationStarted ||
|
||||
isEdgeHovered({
|
||||
source,
|
||||
target,
|
||||
sourceHandle: sourceHandleId,
|
||||
targetHandle: targetHandleId,
|
||||
})
|
||||
{isEditable && (
|
||||
<WorkflowDiagramEdgeV2Container
|
||||
data-click-outside-id={
|
||||
WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID
|
||||
}
|
||||
labelX={labelX}
|
||||
labelY={labelY}
|
||||
>
|
||||
<WorkflowDiagramEdgeButtonGroup
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconPlus,
|
||||
onClick: handleNodeButtonClick,
|
||||
},
|
||||
{
|
||||
Icon: IconTrash,
|
||||
onClick: handleDeleteBranch,
|
||||
},
|
||||
]}
|
||||
selected={nodeCreationStarted}
|
||||
/>
|
||||
</WorkflowDiagramEdgeV2VisibilityContainer>
|
||||
</WorkflowDiagramEdgeV2Container>
|
||||
<WorkflowDiagramEdgeV2VisibilityContainer
|
||||
shouldDisplay={
|
||||
nodeCreationStarted ||
|
||||
isEdgeHovered({
|
||||
source,
|
||||
target,
|
||||
sourceHandle: sourceHandleId,
|
||||
targetHandle: targetHandleId,
|
||||
})
|
||||
}
|
||||
>
|
||||
<WorkflowDiagramEdgeButtonGroup
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconPlus,
|
||||
onClick: handleNodeButtonClick,
|
||||
},
|
||||
{
|
||||
Icon: IconTrash,
|
||||
onClick: handleDeleteBranch,
|
||||
},
|
||||
]}
|
||||
selected={nodeCreationStarted}
|
||||
/>
|
||||
</WorkflowDiagramEdgeV2VisibilityContainer>
|
||||
</WorkflowDiagramEdgeV2Container>
|
||||
)}
|
||||
</EdgeLabelRenderer>
|
||||
</>
|
||||
);
|
||||
|
||||
+6
-1
@@ -23,7 +23,10 @@ export const WorkflowDiagramDefaultEdgeReadonly = ({
|
||||
}: WorkflowDiagramDefaultEdgeReadonlyProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { segments } = getEdgePath({
|
||||
const {
|
||||
segments,
|
||||
overlayPosition: [labelX, labelY],
|
||||
} = getEdgePath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
@@ -53,6 +56,8 @@ export const WorkflowDiagramDefaultEdgeReadonly = ({
|
||||
sourceX={sourceX}
|
||||
sourceY={sourceY}
|
||||
position={data.labelOptions.position}
|
||||
centerX={labelX}
|
||||
centerY={labelY}
|
||||
>
|
||||
<WorkflowDiagramEdgeLabel label={i18n._(data.labelOptions.label)} />
|
||||
</WorkflowDiagramEdgeLabelContainer>
|
||||
|
||||
+10
-1
@@ -1,16 +1,25 @@
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Position } from '@xyflow/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledWorkflowDiagramEdgeLabelContainer = styled.div<{
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
position: Position;
|
||||
centerX?: number;
|
||||
centerY?: number;
|
||||
}>`
|
||||
position: absolute;
|
||||
width: fit-content;
|
||||
|
||||
${({ position, sourceX, sourceY }) => {
|
||||
${({ position, sourceX, sourceY, centerX, centerY }) => {
|
||||
if (isDefined(centerX) && isDefined(centerY)) {
|
||||
return css`
|
||||
transform: translate(-50%, -50%) translate(${centerX}px, ${centerY}px);
|
||||
`;
|
||||
}
|
||||
|
||||
switch (position) {
|
||||
case Position.Right: {
|
||||
return css`
|
||||
|
||||
+2
-2
@@ -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';
|
||||
+12
@@ -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
|
||||
|
||||
+10
@@ -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
|
||||
|
||||
+4
-4
@@ -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,
|
||||
+2
-2
@@ -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,
|
||||
+2
-2
@@ -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';
|
||||
+38
-7
@@ -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
-2
@@ -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';
|
||||
+4
-4
@@ -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';
|
||||
+4
-4
@@ -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
-2
@@ -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';
|
||||
+7
-4
@@ -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
|
||||
+3
-3
@@ -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
-2
@@ -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';
|
||||
+1
-1
@@ -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';
|
||||
+3
-3
@@ -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';
|
||||
+1
-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 { 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',
|
||||
+1
-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:
|
||||
+1
-1
@@ -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',
|
||||
+1
-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',
|
||||
+1
-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
-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';
|
||||
+1
-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 { 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',
|
||||
+5
-5
@@ -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 {
|
||||
+2
-2
@@ -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';
|
||||
|
||||
+3
-3
@@ -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);
|
||||
+3
-3
@@ -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';
|
||||
+3
-3
@@ -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';
|
||||
+4
-2
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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 }>({
|
||||
+2
-2
@@ -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 =
|
||||
+6
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
+6
-2
@@ -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];
|
||||
|
||||
+11
@@ -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',
|
||||
};
|
||||
+3
-9
@@ -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,
|
||||
|
||||
+9
-9
@@ -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';
|
||||
|
||||
+2
-2
@@ -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) => {
|
||||
|
||||
+52
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+299
@@ -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>
|
||||
);
|
||||
};
|
||||
+102
@@ -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>
|
||||
);
|
||||
};
|
||||
+30
@@ -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,
|
||||
};
|
||||
};
|
||||
+41
@@ -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;
|
||||
}, []);
|
||||
};
|
||||
+37
@@ -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,
|
||||
};
|
||||
};
|
||||
+28
@@ -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`;
|
||||
};
|
||||
+27
@@ -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)
|
||||
);
|
||||
});
|
||||
};
|
||||
+21
@@ -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';
|
||||
});
|
||||
};
|
||||
+1
@@ -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';
|
||||
|
||||
+1
@@ -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':
|
||||
|
||||
+1
-1
@@ -109,5 +109,5 @@ export const useTidyUpWorkflowVersion = () => {
|
||||
return tidiedUpDiagram;
|
||||
};
|
||||
|
||||
return { tidyUpWorkflowVersion };
|
||||
return { tidyUpWorkflowVersion, updateWorkflowVersionPosition };
|
||||
};
|
||||
|
||||
+8
@@ -20,6 +20,14 @@ export const PUBLIC_FEATURE_FLAGS: PublicFeatureFlag[] = [
|
||||
imagePath: 'https://twenty.com/images/lab/is-dashboards-enabled.png',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_IF_ELSE_ENABLED,
|
||||
metadata: {
|
||||
label: 'If/Else Workflow Node',
|
||||
description: 'Enable if/else conditional branching in workflows',
|
||||
imagePath: 'https://twenty.com/images/lab/is-if-else-enabled.png',
|
||||
},
|
||||
},
|
||||
...(process.env.CLOUDFLARE_API_KEY
|
||||
? [
|
||||
// {
|
||||
|
||||
+1
@@ -15,4 +15,5 @@ export enum FeatureFlagKey {
|
||||
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
|
||||
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
|
||||
IS_WORKSPACE_CREATION_V2_ENABLED = 'IS_WORKSPACE_CREATION_V2_ENABLED',
|
||||
IS_IF_ELSE_ENABLED = 'IS_IF_ELSE_ENABLED',
|
||||
}
|
||||
|
||||
+1
@@ -225,6 +225,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED: false,
|
||||
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED: false,
|
||||
IS_WORKSPACE_CREATION_V2_ENABLED: false,
|
||||
IS_IF_ELSE_ENABLED: false,
|
||||
},
|
||||
userWorkspaceRoleMap: {},
|
||||
eventEmitterService: {
|
||||
|
||||
+5
@@ -86,6 +86,11 @@ export const seedFeatureFlags = async ({
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_IF_ELSE_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
};
|
||||
|
||||
+166
-1
@@ -1,8 +1,16 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
StepLogicalOperator,
|
||||
ViewFilterOperand,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||
import {
|
||||
IF_ELSE_BRANCH_POSITION_OFFSETS,
|
||||
type StepIfElseBranch,
|
||||
} from 'twenty-shared/workflow';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@@ -427,6 +435,48 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
additionalCreatedSteps: [emptyNodeStep],
|
||||
};
|
||||
}
|
||||
case WorkflowActionType.IF_ELSE: {
|
||||
const { ifEmptyNode, elseEmptyNode, ifFilterGroupId, branches } =
|
||||
await this.createEmptyNodesForIfElseStep({
|
||||
workflowVersionId,
|
||||
workspaceId,
|
||||
ifElsePosition: position,
|
||||
});
|
||||
|
||||
const initialFilterId = v4();
|
||||
|
||||
return {
|
||||
builtStep: {
|
||||
...baseStep,
|
||||
name: 'If/Else',
|
||||
type: WorkflowActionType.IF_ELSE,
|
||||
settings: {
|
||||
...BASE_STEP_DEFINITION,
|
||||
input: {
|
||||
stepFilterGroups: [
|
||||
{
|
||||
id: ifFilterGroupId,
|
||||
logicalOperator: StepLogicalOperator.AND,
|
||||
},
|
||||
],
|
||||
stepFilters: [
|
||||
{
|
||||
id: initialFilterId,
|
||||
type: 'unknown',
|
||||
stepOutputKey: '',
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: '',
|
||||
stepFilterGroupId: ifFilterGroupId,
|
||||
positionInStepFilterGroup: 0,
|
||||
},
|
||||
],
|
||||
branches,
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalCreatedSteps: [ifEmptyNode, elseEmptyNode],
|
||||
};
|
||||
}
|
||||
case WorkflowActionType.DELAY: {
|
||||
return {
|
||||
builtStep: {
|
||||
@@ -448,6 +498,20 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
},
|
||||
};
|
||||
}
|
||||
case WorkflowActionType.EMPTY: {
|
||||
return {
|
||||
builtStep: {
|
||||
...baseStep,
|
||||
name: 'Add an Action',
|
||||
type: WorkflowActionType.EMPTY,
|
||||
valid: true,
|
||||
settings: {
|
||||
...BASE_STEP_DEFINITION,
|
||||
input: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new WorkflowVersionStepException(
|
||||
`WorkflowActionType '${type}' unknown`,
|
||||
@@ -715,6 +779,107 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
);
|
||||
}
|
||||
|
||||
async createEmptyNodesForIfElseStep({
|
||||
workflowVersionId,
|
||||
workspaceId,
|
||||
ifElsePosition,
|
||||
}: {
|
||||
workflowVersionId: string;
|
||||
workspaceId: string;
|
||||
ifElsePosition?: WorkflowStepPositionInput;
|
||||
}): Promise<{
|
||||
ifEmptyNode: WorkflowAction;
|
||||
elseEmptyNode: WorkflowAction;
|
||||
ifFilterGroupId: string;
|
||||
branches: StepIfElseBranch[];
|
||||
}> {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
authContext,
|
||||
async () => {
|
||||
const workflowVersionRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowVersion = await workflowVersionRepository.findOne({
|
||||
where: {
|
||||
id: workflowVersionId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(workflowVersion)) {
|
||||
throw new WorkflowVersionStepException(
|
||||
'WorkflowVersion not found',
|
||||
WorkflowVersionStepExceptionCode.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const existingSteps = workflowVersion.steps ?? [];
|
||||
|
||||
const ifEmptyNode: WorkflowEmptyAction = {
|
||||
id: v4(),
|
||||
name: 'Add an Action',
|
||||
type: WorkflowActionType.EMPTY,
|
||||
valid: true,
|
||||
settings: {
|
||||
...BASE_STEP_DEFINITION,
|
||||
input: {},
|
||||
},
|
||||
position: {
|
||||
x: (ifElsePosition?.x ?? 0) + IF_ELSE_BRANCH_POSITION_OFFSETS.IF.x,
|
||||
y: (ifElsePosition?.y ?? 0) + IF_ELSE_BRANCH_POSITION_OFFSETS.IF.y,
|
||||
},
|
||||
};
|
||||
|
||||
const elseEmptyNode: WorkflowEmptyAction = {
|
||||
id: v4(),
|
||||
name: 'Add an Action',
|
||||
type: WorkflowActionType.EMPTY,
|
||||
valid: true,
|
||||
settings: {
|
||||
...BASE_STEP_DEFINITION,
|
||||
input: {},
|
||||
},
|
||||
position: {
|
||||
x:
|
||||
(ifElsePosition?.x ?? 0) + IF_ELSE_BRANCH_POSITION_OFFSETS.ELSE.x,
|
||||
y:
|
||||
(ifElsePosition?.y ?? 0) + IF_ELSE_BRANCH_POSITION_OFFSETS.ELSE.y,
|
||||
},
|
||||
};
|
||||
|
||||
await workflowVersionRepository.update(workflowVersion.id, {
|
||||
steps: [...existingSteps, ifEmptyNode, elseEmptyNode],
|
||||
});
|
||||
|
||||
const ifFilterGroupId = v4();
|
||||
|
||||
const branches: StepIfElseBranch[] = [
|
||||
{
|
||||
id: v4(),
|
||||
filterGroupId: ifFilterGroupId,
|
||||
nextStepIds: [ifEmptyNode.id],
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
nextStepIds: [elseEmptyNode.id],
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
ifEmptyNode,
|
||||
elseEmptyNode,
|
||||
ifFilterGroupId,
|
||||
branches,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async createDraftStep({
|
||||
step,
|
||||
workspaceId,
|
||||
|
||||
+4
@@ -12,6 +12,7 @@ import { DelayWorkflowAction } from 'src/modules/workflow/workflow-executor/work
|
||||
import { EmptyWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/empty/empty.workflow-action';
|
||||
import { FilterWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/filter.workflow-action';
|
||||
import { FormWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/form/form.workflow-action';
|
||||
import { IfElseWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/if-else/if-else.workflow-action';
|
||||
import { IteratorWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/iterator.workflow-action';
|
||||
import { CreateRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action';
|
||||
import { DeleteRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action';
|
||||
@@ -32,6 +33,7 @@ export class WorkflowActionFactory {
|
||||
private readonly findRecordsWorkflowAction: FindRecordsWorkflowAction,
|
||||
private readonly formWorkflowAction: FormWorkflowAction,
|
||||
private readonly filterWorkflowAction: FilterWorkflowAction,
|
||||
private readonly ifElseWorkflowAction: IfElseWorkflowAction,
|
||||
private readonly iteratorWorkflowAction: IteratorWorkflowAction,
|
||||
private readonly toolExecutorWorkflowAction: ToolExecutorWorkflowAction,
|
||||
private readonly aiAgentWorkflowAction: AiAgentWorkflowAction,
|
||||
@@ -59,6 +61,8 @@ export class WorkflowActionFactory {
|
||||
return this.formWorkflowAction;
|
||||
case WorkflowActionType.FILTER:
|
||||
return this.filterWorkflowAction;
|
||||
case WorkflowActionType.IF_ELSE:
|
||||
return this.ifElseWorkflowAction;
|
||||
case WorkflowActionType.ITERATOR:
|
||||
return this.iteratorWorkflowAction;
|
||||
case WorkflowActionType.HTTP_REQUEST:
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import {
|
||||
type WorkflowAction,
|
||||
WorkflowActionType,
|
||||
type WorkflowIfElseAction,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
export const isWorkflowIfElseAction = (
|
||||
action: WorkflowAction,
|
||||
): action is WorkflowIfElseAction => action.type === WorkflowActionType.IF_ELSE;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { IfElseWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/if-else/if-else.workflow-action';
|
||||
|
||||
@Module({
|
||||
providers: [IfElseWorkflowAction],
|
||||
exports: [IfElseWorkflowAction],
|
||||
})
|
||||
export class IfElseActionModule {}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { resolveInput } from 'twenty-shared/utils';
|
||||
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
|
||||
|
||||
import {
|
||||
WorkflowStepExecutorException,
|
||||
WorkflowStepExecutorExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
||||
import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
|
||||
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
||||
import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util';
|
||||
import { isWorkflowIfElseAction } from 'src/modules/workflow/workflow-executor/workflow-actions/if-else/guards/is-workflow-if-else-action.guard';
|
||||
import { findMatchingBranch } from 'src/modules/workflow/workflow-executor/workflow-actions/if-else/utils/find-matching-branch.util';
|
||||
|
||||
@Injectable()
|
||||
export class IfElseWorkflowAction implements WorkflowAction {
|
||||
async execute(input: WorkflowActionInput): Promise<WorkflowActionOutput> {
|
||||
const { currentStepId, steps, context } = input;
|
||||
|
||||
const step = findStepOrThrow({
|
||||
stepId: currentStepId,
|
||||
steps,
|
||||
});
|
||||
|
||||
if (!isWorkflowIfElseAction(step)) {
|
||||
throw new WorkflowStepExecutorException(
|
||||
'Step is not an if-else action',
|
||||
WorkflowStepExecutorExceptionCode.INVALID_STEP_TYPE,
|
||||
);
|
||||
}
|
||||
|
||||
const { stepFilterGroups, stepFilters, branches } = step.settings.input;
|
||||
|
||||
if (!branches || branches.length === 0) {
|
||||
throw new WorkflowStepExecutorException(
|
||||
'If-else action must have at least one branch',
|
||||
WorkflowStepExecutorExceptionCode.INVALID_STEP_TYPE,
|
||||
);
|
||||
}
|
||||
|
||||
if (!stepFilterGroups || !stepFilters) {
|
||||
throw new WorkflowStepExecutorException(
|
||||
'If-else action must have stepFilterGroups and stepFilters defined',
|
||||
WorkflowStepExecutorExceptionCode.INVALID_STEP_TYPE,
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedFilters = stepFilters.map((filter) => ({
|
||||
...filter,
|
||||
rightOperand: resolveInput(filter.value, context),
|
||||
leftOperand: resolveInput(filter.stepOutputKey, context),
|
||||
}));
|
||||
|
||||
const matchingBranch = findMatchingBranch({
|
||||
branches,
|
||||
stepFilterGroups,
|
||||
resolvedFilters,
|
||||
});
|
||||
|
||||
return {
|
||||
result: {
|
||||
matchingBranchId: matchingBranch.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { type StepFilter, type StepFilterGroup } from 'twenty-shared/types';
|
||||
import { type StepIfElseBranch } from 'twenty-shared/workflow';
|
||||
|
||||
import { type BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
|
||||
|
||||
export type WorkflowIfElseActionSettings = BaseWorkflowActionSettings & {
|
||||
input: {
|
||||
stepFilterGroups: StepFilterGroup[];
|
||||
stepFilters: StepFilter[];
|
||||
branches: StepIfElseBranch[];
|
||||
};
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export type WorkflowIfElseResult = {
|
||||
matchingBranchId: string;
|
||||
};
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { type StepFilter, type StepFilterGroup } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type StepIfElseBranch } from 'twenty-shared/workflow';
|
||||
|
||||
import {
|
||||
WorkflowStepExecutorException,
|
||||
WorkflowStepExecutorExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
||||
import { evaluateFilterConditions } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util';
|
||||
|
||||
export type ResolvedFilter = Omit<StepFilter, 'value' | 'stepOutputKey'> & {
|
||||
rightOperand: unknown;
|
||||
leftOperand: unknown;
|
||||
};
|
||||
|
||||
const collectAllDescendantGroups = (
|
||||
rootGroupId: string,
|
||||
allGroups: StepFilterGroup[],
|
||||
collectedGroups: Set<StepFilterGroup> = new Set(),
|
||||
): Set<StepFilterGroup> => {
|
||||
const rootGroup = allGroups.find((group) => group.id === rootGroupId);
|
||||
|
||||
if (!rootGroup) {
|
||||
return collectedGroups;
|
||||
}
|
||||
|
||||
collectedGroups.add(rootGroup);
|
||||
|
||||
const childGroups = allGroups.filter(
|
||||
(group) => group.parentStepFilterGroupId === rootGroupId,
|
||||
);
|
||||
|
||||
for (const childGroup of childGroups) {
|
||||
collectAllDescendantGroups(childGroup.id, allGroups, collectedGroups);
|
||||
}
|
||||
|
||||
return collectedGroups;
|
||||
};
|
||||
|
||||
export const findMatchingBranch = ({
|
||||
branches,
|
||||
stepFilterGroups,
|
||||
resolvedFilters,
|
||||
}: {
|
||||
branches: StepIfElseBranch[];
|
||||
stepFilterGroups: StepFilterGroup[];
|
||||
resolvedFilters: ResolvedFilter[];
|
||||
}): StepIfElseBranch => {
|
||||
const matchingBranch = branches.find((branch) => {
|
||||
if (!isDefined(branch.filterGroupId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const branchFilterGroups = Array.from(
|
||||
collectAllDescendantGroups(branch.filterGroupId, stepFilterGroups),
|
||||
);
|
||||
|
||||
const branchFilterGroupIds = new Set(branchFilterGroups.map((g) => g.id));
|
||||
const branchFilters = resolvedFilters.filter((filter) =>
|
||||
branchFilterGroupIds.has(filter.stepFilterGroupId),
|
||||
);
|
||||
|
||||
return evaluateFilterConditions({
|
||||
filterGroups: branchFilterGroups,
|
||||
filters: branchFilters,
|
||||
});
|
||||
});
|
||||
|
||||
if (!isDefined(matchingBranch)) {
|
||||
throw new WorkflowStepExecutorException(
|
||||
'No matching branch found in if-else action',
|
||||
WorkflowStepExecutorExceptionCode.INTERNAL_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
return matchingBranch;
|
||||
};
|
||||
+2
@@ -5,6 +5,7 @@ import { type WorkflowDelayActionSettings } from 'src/modules/workflow/workflow-
|
||||
import { type WorkflowFilterActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/types/workflow-filter-action-settings.type';
|
||||
import { type WorkflowFormActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
|
||||
import { type WorkflowHttpRequestActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/http-request/types/workflow-http-request-action-settings.type';
|
||||
import { type WorkflowIfElseActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/if-else/types/workflow-if-else-action-settings.type';
|
||||
import { type WorkflowIteratorActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/types/workflow-iterator-action-settings.type';
|
||||
import { type WorkflowSendEmailActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/types/workflow-send-email-action-settings.type';
|
||||
import {
|
||||
@@ -37,6 +38,7 @@ export type WorkflowActionSettings =
|
||||
| WorkflowFindRecordsActionSettings
|
||||
| WorkflowFormActionSettings
|
||||
| WorkflowFilterActionSettings
|
||||
| WorkflowIfElseActionSettings
|
||||
| WorkflowHttpRequestActionSettings
|
||||
| WorkflowAiAgentActionSettings
|
||||
| WorkflowDelayActionSettings
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ export enum WorkflowActionType {
|
||||
FIND_RECORDS = 'FIND_RECORDS',
|
||||
FORM = 'FORM',
|
||||
FILTER = 'FILTER',
|
||||
IF_ELSE = 'IF_ELSE',
|
||||
HTTP_REQUEST = 'HTTP_REQUEST',
|
||||
AI_AGENT = 'AI_AGENT',
|
||||
ITERATOR = 'ITERATOR',
|
||||
|
||||
+7
@@ -4,6 +4,7 @@ import { type WorkflowDelayActionSettings } from 'src/modules/workflow/workflow-
|
||||
import { type WorkflowFilterActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/types/workflow-filter-action-settings.type';
|
||||
import { type WorkflowFormActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
|
||||
import { type WorkflowHttpRequestActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/http-request/types/workflow-http-request-action-settings.type';
|
||||
import { type WorkflowIfElseActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/if-else/types/workflow-if-else-action-settings.type';
|
||||
import { type WorkflowIteratorActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/types/workflow-iterator-action-settings.type';
|
||||
import { type WorkflowSendEmailActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/types/workflow-send-email-action-settings.type';
|
||||
import {
|
||||
@@ -79,6 +80,11 @@ export type WorkflowFilterAction = BaseWorkflowAction & {
|
||||
settings: WorkflowFilterActionSettings;
|
||||
};
|
||||
|
||||
export type WorkflowIfElseAction = BaseWorkflowAction & {
|
||||
type: WorkflowActionType.IF_ELSE;
|
||||
settings: WorkflowIfElseActionSettings;
|
||||
};
|
||||
|
||||
export type WorkflowHttpRequestAction = BaseWorkflowAction & {
|
||||
type: WorkflowActionType.HTTP_REQUEST;
|
||||
settings: WorkflowHttpRequestActionSettings;
|
||||
@@ -113,6 +119,7 @@ export type WorkflowAction =
|
||||
| WorkflowFindRecordsAction
|
||||
| WorkflowFormAction
|
||||
| WorkflowFilterAction
|
||||
| WorkflowIfElseAction
|
||||
| WorkflowHttpRequestAction
|
||||
| WorkflowAiAgentAction
|
||||
| WorkflowIteratorAction
|
||||
|
||||
+2
@@ -11,6 +11,7 @@ import { DelayActionModule } from 'src/modules/workflow/workflow-executor/workfl
|
||||
import { EmptyActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/empty/empty-action.module';
|
||||
import { FilterActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/filter-action.module';
|
||||
import { FormActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/form/form-action.module';
|
||||
import { IfElseActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/if-else/if-else-action.module';
|
||||
import { IteratorActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/iterator-action.module';
|
||||
import { RecordCRUDActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/record-crud-action.module';
|
||||
import { ToolExecutorWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action';
|
||||
@@ -27,6 +28,7 @@ import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow
|
||||
FormActionModule,
|
||||
BillingModule,
|
||||
FilterActionModule,
|
||||
IfElseActionModule,
|
||||
IteratorActionModule,
|
||||
AiAgentActionModule,
|
||||
EmptyActionModule,
|
||||
|
||||
+16
@@ -31,6 +31,8 @@ import { shouldExecuteStep } from 'src/modules/workflow/workflow-executor/utils/
|
||||
import { shouldSkipStepExecution } from 'src/modules/workflow/workflow-executor/utils/should-skip-step-execution.util';
|
||||
import { workflowShouldFail } from 'src/modules/workflow/workflow-executor/utils/workflow-should-fail.util';
|
||||
import { workflowShouldKeepRunning } from 'src/modules/workflow/workflow-executor/utils/workflow-should-keep-running.util';
|
||||
import { isWorkflowIfElseAction } from 'src/modules/workflow/workflow-executor/workflow-actions/if-else/guards/is-workflow-if-else-action.guard';
|
||||
import { type WorkflowIfElseResult } from 'src/modules/workflow/workflow-executor/workflow-actions/if-else/types/workflow-if-else-result.type';
|
||||
import { isWorkflowIteratorAction } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/guards/is-workflow-iterator-action.guard';
|
||||
import { WorkflowIteratorResult } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/types/workflow-iterator-result.type';
|
||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
@@ -204,6 +206,20 @@ export class WorkflowExecutorWorkspaceService {
|
||||
}
|
||||
}
|
||||
|
||||
if (isWorkflowIfElseAction(executedStep)) {
|
||||
const ifElseResult = executedStepResult.result as
|
||||
| WorkflowIfElseResult
|
||||
| undefined;
|
||||
|
||||
if (ifElseResult?.matchingBranchId) {
|
||||
const matchingBranch = executedStep.settings.input.branches.find(
|
||||
(branch) => branch.id === ifElseResult.matchingBranchId,
|
||||
);
|
||||
|
||||
return matchingBranch?.nextStepIds;
|
||||
}
|
||||
}
|
||||
|
||||
return executedStep.nextStepIds;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const IF_ELSE_BRANCH_POSITION_OFFSETS = {
|
||||
IF: {
|
||||
x: -200,
|
||||
y: 120,
|
||||
},
|
||||
ELSE: {
|
||||
x: 200,
|
||||
y: 120,
|
||||
},
|
||||
};
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
export { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from './constants/CaptureAllVariableTagInnerRegex';
|
||||
export { CONTENT_TYPE_VALUES_HTTP_REQUEST } from './constants/ContentTypeValuesHttpRequest';
|
||||
export { IF_ELSE_BRANCH_POSITION_OFFSETS } from './constants/IfElseBranchPositionOffsets';
|
||||
export { TRIGGER_STEP_ID } from './constants/TriggerStepId';
|
||||
export { workflowAiAgentActionSchema } from './schemas/ai-agent-action-schema';
|
||||
export { workflowAiAgentActionSettingsSchema } from './schemas/ai-agent-action-settings-schema';
|
||||
@@ -33,12 +34,19 @@ export { workflowFormActionSchema } from './schemas/form-action-schema';
|
||||
export { workflowFormActionSettingsSchema } from './schemas/form-action-settings-schema';
|
||||
export { workflowHttpRequestActionSchema } from './schemas/http-request-action-schema';
|
||||
export { workflowHttpRequestActionSettingsSchema } from './schemas/http-request-action-settings-schema';
|
||||
export { workflowIfElseActionSchema } from './schemas/if-else-action-schema';
|
||||
export {
|
||||
stepIfElseBranchSchema,
|
||||
workflowIfElseActionSettingsSchema,
|
||||
} from './schemas/if-else-action-settings-schema';
|
||||
export { workflowIteratorActionSchema } from './schemas/iterator-action-schema';
|
||||
export { workflowIteratorActionSettingsSchema } from './schemas/iterator-action-settings-schema';
|
||||
export { workflowManualTriggerSchema } from './schemas/manual-trigger-schema';
|
||||
export { objectRecordSchema } from './schemas/object-record-schema';
|
||||
export { workflowSendEmailActionSchema } from './schemas/send-email-action-schema';
|
||||
export { workflowSendEmailActionSettingsSchema } from './schemas/send-email-action-settings-schema';
|
||||
export { stepFilterGroupSchema } from './schemas/step-filter-group-schema';
|
||||
export { stepFilterSchema } from './schemas/step-filter-schema';
|
||||
export { workflowUpdateRecordActionSchema } from './schemas/update-record-action-schema';
|
||||
export { workflowUpdateRecordActionSettingsSchema } from './schemas/update-record-action-settings-schema';
|
||||
export { workflowUpsertRecordActionSchema } from './schemas/upsert-record-action-schema';
|
||||
@@ -55,6 +63,7 @@ export { workflowRunStateStepInfosSchema } from './schemas/workflow-run-state-st
|
||||
export { workflowRunStatusSchema } from './schemas/workflow-run-status-schema';
|
||||
export { workflowRunStepStatusSchema } from './schemas/workflow-run-step-status-schema';
|
||||
export { workflowTriggerSchema } from './schemas/workflow-trigger-schema';
|
||||
export type { StepIfElseBranch } from './types/StepIfElseBranch';
|
||||
export type { BodyType } from './types/workflowHttpRequestStep';
|
||||
export type {
|
||||
WorkflowRunStepInfo,
|
||||
|
||||
@@ -1,34 +1,12 @@
|
||||
import { ViewFilterOperandDeprecated } from '@/types';
|
||||
import { z } from 'zod';
|
||||
import { StepLogicalOperator } from '../../types/StepFilters';
|
||||
import { ViewFilterOperand } from '../../types/ViewFilterOperand';
|
||||
import { baseWorkflowActionSettingsSchema } from './base-workflow-action-settings-schema';
|
||||
import { stepFilterGroupSchema } from './step-filter-group-schema';
|
||||
import { stepFilterSchema } from './step-filter-schema';
|
||||
|
||||
export const workflowFilterActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
stepFilterGroups: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
logicalOperator: z.enum(StepLogicalOperator),
|
||||
parentStepFilterGroupId: z.string().optional(),
|
||||
positionInStepFilterGroup: z.number().optional(),
|
||||
}),
|
||||
),
|
||||
stepFilters: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
stepOutputKey: z.string(),
|
||||
operand: z
|
||||
.enum(ViewFilterOperand)
|
||||
.or(z.enum(ViewFilterOperandDeprecated)),
|
||||
value: z.string(),
|
||||
stepFilterGroupId: z.string(),
|
||||
positionInStepFilterGroup: z.number().optional(),
|
||||
fieldMetadataId: z.string().optional(),
|
||||
compositeFieldSubFieldName: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
stepFilterGroups: z.array(stepFilterGroupSchema),
|
||||
stepFilters: z.array(stepFilterSchema),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
import { baseWorkflowActionSchema } from './base-workflow-action-schema';
|
||||
import { workflowIfElseActionSettingsSchema } from './if-else-action-settings-schema';
|
||||
|
||||
export const workflowIfElseActionSchema = baseWorkflowActionSchema.extend({
|
||||
type: z.literal('IF_ELSE'),
|
||||
settings: workflowIfElseActionSettingsSchema,
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { z } from 'zod';
|
||||
import { baseWorkflowActionSettingsSchema } from './base-workflow-action-settings-schema';
|
||||
import { stepFilterGroupSchema } from './step-filter-group-schema';
|
||||
import { stepFilterSchema } from './step-filter-schema';
|
||||
|
||||
export const stepIfElseBranchSchema = z.object({
|
||||
id: z.string(),
|
||||
nextStepIds: z.array(z.string()),
|
||||
filterGroupId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const workflowIfElseActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
stepFilterGroups: z.array(stepFilterGroupSchema),
|
||||
stepFilters: z.array(stepFilterSchema),
|
||||
branches: z.array(stepIfElseBranchSchema),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { z } from 'zod';
|
||||
import { StepLogicalOperator } from '../../types/StepFilters';
|
||||
|
||||
export const stepFilterGroupSchema = z.object({
|
||||
id: z.string(),
|
||||
logicalOperator: z.enum(StepLogicalOperator),
|
||||
parentStepFilterGroupId: z.string().optional(),
|
||||
positionInStepFilterGroup: z.number().optional(),
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ViewFilterOperandDeprecated } from '@/types';
|
||||
import { z } from 'zod';
|
||||
import { ViewFilterOperand } from '../../types/ViewFilterOperand';
|
||||
|
||||
export const stepFilterSchema = z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
stepOutputKey: z.string(),
|
||||
operand: z.enum(ViewFilterOperand).or(z.enum(ViewFilterOperandDeprecated)),
|
||||
value: z.string(),
|
||||
stepFilterGroupId: z.string(),
|
||||
positionInStepFilterGroup: z.number().optional(),
|
||||
fieldMetadataId: z.string().optional(),
|
||||
compositeFieldSubFieldName: z.string().optional(),
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import { workflowFilterActionSchema } from './filter-action-schema';
|
||||
import { workflowFindRecordsActionSchema } from './find-records-action-schema';
|
||||
import { workflowFormActionSchema } from './form-action-schema';
|
||||
import { workflowHttpRequestActionSchema } from './http-request-action-schema';
|
||||
import { workflowIfElseActionSchema } from './if-else-action-schema';
|
||||
import { workflowIteratorActionSchema } from './iterator-action-schema';
|
||||
import { workflowSendEmailActionSchema } from './send-email-action-schema';
|
||||
import { workflowUpdateRecordActionSchema } from './update-record-action-schema';
|
||||
@@ -26,6 +27,7 @@ export const workflowActionSchema = z.discriminatedUnion('type', [
|
||||
workflowHttpRequestActionSchema,
|
||||
workflowAiAgentActionSchema,
|
||||
workflowFilterActionSchema,
|
||||
workflowIfElseActionSchema,
|
||||
workflowIteratorActionSchema,
|
||||
workflowDelayActionSchema,
|
||||
workflowEmptyActionSchema,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export type StepIfElseBranch = {
|
||||
id: string;
|
||||
nextStepIds: string[];
|
||||
filterGroupId?: string;
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
Reference in New Issue
Block a user