From 151731c05dadc4ee2f48761a49cad059800bfb26 Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Fri, 9 Jan 2026 22:50:58 +0530 Subject: [PATCH] If else node followup changes (#16974) Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../components/WorkflowDiagramCanvasBase.tsx | 30 ++++++- .../components/WorkflowStepFilterColumn.tsx | 28 +------ .../workflow-steps/hooks/useDeleteStep.ts | 27 ------ .../hooks/useDeleteWorkflowVersionStep.ts | 18 +++- .../utils/getWorkflowPreviousSteps.ts | 6 ++ .../components/WorkflowIfElseBranchEditor.tsx | 75 +++++++++++------ .../utils/remove-step.ts | 84 +++++++++++++++++-- 7 files changed, 182 insertions(+), 86 deletions(-) diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx index d4a6177672..0645af0dbf 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase.tsx @@ -208,6 +208,7 @@ export const WorkflowDiagramCanvasBase = ({ const [connectionStartInfo, setConnectionStartInfo] = useState<{ nodeId: string; handleId: string; + startPosition: { x: number; y: number }; } | null>(null); const { nodes, edges } = useMemo(() => { @@ -501,13 +502,31 @@ export const WorkflowDiagramCanvasBase = ({ }, [clearEdgeHover]); const handleConnectStart = ( - _: MouseEvent | TouchEvent, + event: MouseEvent | TouchEvent, params: OnConnectStartParams, ) => { if (isDefined(params.nodeId) && isDefined(params.handleId)) { + const clientX = + event instanceof MouseEvent ? event.clientX : event.touches[0]?.clientX; + const clientY = + event instanceof MouseEvent ? event.clientY : event.touches[0]?.clientY; + if ( + !isDefined(clientX) || + !isDefined(clientY) || + !isDefined(containerRef.current) + ) { + return; + } + + const bounds = containerRef.current.getBoundingClientRect(); + setConnectionStartInfo({ nodeId: params.nodeId, handleId: params.handleId, + startPosition: { + x: clientX - bounds.left, + y: clientY - bounds.top, + }, }); } }; @@ -542,6 +561,15 @@ export const WorkflowDiagramCanvasBase = ({ y: event.clientY - bounds.top, }; + const MIN_DRAG_DISTANCE = 5; + const deltaX = screenPosition.x - startInfo.startPosition.x; + const deltaY = screenPosition.y - startInfo.startPosition.y; + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + + if (distance < MIN_DRAG_DISTANCE) { + return; + } + const flowPosition = workflowDiagramScreenToFlowPosition(screenPosition); if (!isDefined(flowPosition)) { diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterColumn.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterColumn.tsx index 6488c68a6c..067b023ba7 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterColumn.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/filters/components/WorkflowStepFilterColumn.tsx @@ -1,23 +1,18 @@ import { AdvancedFilterCommandMenuColumn } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuColumn'; -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; }; @@ -32,32 +27,11 @@ 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; + const shouldShowDropdown = !readonly; return ( diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteStep.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteStep.ts index 21a5ccec5c..71e612fa2f 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteStep.ts @@ -3,10 +3,8 @@ 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'; @@ -32,31 +30,6 @@ export const useDeleteStep = () => { ? 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, stepId, diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteWorkflowVersionStep.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteWorkflowVersionStep.ts index 10054d7d9b..68c2d83193 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteWorkflowVersionStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useDeleteWorkflowVersionStep.ts @@ -1,4 +1,6 @@ import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQuery'; import { DELETE_WORKFLOW_VERSION_STEP } from '@/workflow/graphql/mutations/deleteWorkflowVersionStep'; import { useUpdateWorkflowVersionCache } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionCache'; import { useMutation } from '@apollo/client'; @@ -13,6 +15,11 @@ export const useDeleteWorkflowVersionStep = () => { const { updateWorkflowVersionCache } = useUpdateWorkflowVersionCache(); + const { findOneRecordQuery: findOneWorkflowVersionQuery } = + useFindOneRecordQuery({ + objectNameSingular: CoreObjectNameSingular.WorkflowVersion, + }); + const [mutate] = useMutation< DeleteWorkflowVersionStepMutation, DeleteWorkflowVersionStepMutationVariables @@ -23,7 +30,16 @@ export const useDeleteWorkflowVersionStep = () => { const deleteWorkflowVersionStep = async ( input: DeleteWorkflowVersionStepInput, ) => { - const result = await mutate({ variables: { input } }); + const result = await mutate({ + variables: { input }, + awaitRefetchQueries: true, + refetchQueries: [ + { + query: findOneWorkflowVersionQuery, + variables: { objectRecordId: input.workflowVersionId }, + }, + ], + }); const workflowVersionStepChanges = result?.data?.deleteWorkflowVersionStep; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowPreviousSteps.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowPreviousSteps.ts index bd5f88b21a..65a3ae6633 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowPreviousSteps.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/utils/getWorkflowPreviousSteps.ts @@ -18,6 +18,12 @@ const isParentStep = ({ ); } + if (potentialParentStep.type === 'IF_ELSE') { + return !!potentialParentStep.settings.input.branches?.some((branch) => + branch.nextStepIds?.includes(currentStep.id), + ); + } + if (currentStep.type === 'ITERATOR') { return !!( potentialParentStep.nextStepIds?.includes(currentStep.id) && diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/if-else-action/components/WorkflowIfElseBranchEditor.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/if-else-action/components/WorkflowIfElseBranchEditor.tsx index 6041952f41..33c544e2a6 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/if-else-action/components/WorkflowIfElseBranchEditor.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/if-else-action/components/WorkflowIfElseBranchEditor.tsx @@ -58,20 +58,47 @@ export const WorkflowIfElseBranchEditor = ({ const isIfBranch = branchIndex === 0; + const isLastFilterInIfBranch = (stepFilter: { + stepFilterGroupId: string; + }): boolean => { + const childStepFilters = childStepFiltersAndChildStepFilterGroups.filter( + (child) => !isStepFilterGroupChildAStepFilterGroup(child), + ); + const childStepFilterGroups = + childStepFiltersAndChildStepFilterGroups.filter( + isStepFilterGroupChildAStepFilterGroup, + ); + + return ( + isIfBranch && + isDefined(branchFilterGroup) && + stepFilter.stepFilterGroupId === branchFilterGroup.id && + childStepFilters.length === 1 && + childStepFilterGroups.length === 0 + ); + }; + return ( - - - - {isDefined(branchFilterGroup) && - childStepFiltersAndChildStepFilterGroups.map( - (stepFilterGroupChild, stepFilterGroupChildIndex) => - isStepFilterGroupChildAStepFilterGroup(stepFilterGroupChild) ? ( + + + {isDefined(branchFilterGroup) && + childStepFiltersAndChildStepFilterGroups.map( + (stepFilterGroupChild, stepFilterGroupChildIndex) => ( + + {isStepFilterGroupChildAStepFilterGroup( + stepFilterGroupChild, + ) ? ( - ), - )} - + )} + + ), + )} + - {!readonly && isDefined(branchFilterGroup) && ( - - )} - - + {!readonly && isDefined(branchFilterGroup) && ( + + )} + ); }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/utils/remove-step.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/utils/remove-step.ts index 4ac65b5c39..5a050c2df7 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/utils/remove-step.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/utils/remove-step.ts @@ -1,9 +1,11 @@ import { isDefined } from 'twenty-shared/utils'; -import { TRIGGER_STEP_ID } from 'twenty-shared/workflow'; +import { TRIGGER_STEP_ID, type StepIfElseBranch } from 'twenty-shared/workflow'; +import { isWorkflowEmptyAction } from 'src/modules/workflow/workflow-executor/workflow-actions/empty/guards/is-workflow-empty-action.guard'; +import { isWorkflowIfElseAction } from 'src/modules/workflow/workflow-executor/workflow-actions/if-else/guards/is-workflow-if-else-action.guard'; import { - type WorkflowAction, WorkflowActionType, + type WorkflowAction, } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; import { type WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; @@ -29,6 +31,22 @@ const computeUpdatedNextStepIds = ({ ]; }; +export const getEmptyChildStepIdsForIfElse = ({ + branches, + allSteps, +}: { + branches: StepIfElseBranch[]; + allSteps: WorkflowAction[]; +}): string[] => { + const childStepIds = branches.flatMap((branch) => branch.nextStepIds); + + return childStepIds.filter((childStepId) => { + const childStep = allSteps.find((step) => step.id === childStepId); + + return isDefined(childStep) && isWorkflowEmptyAction(childStep); + }); +}; + export const removeStep = ({ existingTrigger, existingSteps, @@ -48,17 +66,71 @@ export const removeStep = ({ }; } + const stepToDelete = existingSteps?.find( + (step) => step.id === stepIdToDelete, + ); + + let emptyChildStepIds: string[] = []; + + if ( + isDefined(stepToDelete) && + isWorkflowIfElseAction(stepToDelete) && + isDefined(existingSteps) + ) { + emptyChildStepIds = getEmptyChildStepIdsForIfElse({ + branches: stepToDelete.settings.input.branches, + allSteps: existingSteps, + }); + } + + const allRemovedStepIds = [stepIdToDelete, ...emptyChildStepIds]; + const updatedSteps = existingSteps - ?.filter((step) => step.id !== stepIdToDelete) + ?.filter((step) => !allRemovedStepIds.includes(step.id)) .map((step) => { + if ( + step.type === WorkflowActionType.IF_ELSE && + isWorkflowIfElseAction(step) + ) { + const updatedBranches = step.settings.input.branches.map((branch) => { + let updatedNextStepIds = branch.nextStepIds; + + if (branch.nextStepIds.includes(stepIdToDelete)) { + updatedNextStepIds = computeUpdatedNextStepIds({ + existingNextStepIds: branch.nextStepIds, + stepIdToRemove: stepIdToDelete, + stepToDeleteChildrenIds, + }); + } + + return { + ...branch, + nextStepIds: updatedNextStepIds.filter( + (id) => !allRemovedStepIds.includes(id), + ), + }; + }); + + return { + ...step, + settings: { + ...step.settings, + input: { + ...step.settings.input, + branches: updatedBranches, + }, + }, + }; + } + if (step.nextStepIds?.includes(stepIdToDelete)) { return { ...step, nextStepIds: computeUpdatedNextStepIds({ existingNextStepIds: step.nextStepIds, stepIdToRemove: stepIdToDelete, - stepToDeleteChildrenIds: stepToDeleteChildrenIds, + stepToDeleteChildrenIds, }), }; } @@ -77,7 +149,7 @@ export const removeStep = ({ initialLoopStepIds: computeUpdatedNextStepIds({ existingNextStepIds: step.settings.input.initialLoopStepIds, stepIdToRemove: stepIdToDelete, - stepToDeleteChildrenIds: stepToDeleteChildrenIds, + stepToDeleteChildrenIds, }), }, }, @@ -105,6 +177,6 @@ export const removeStep = ({ return { updatedSteps, updatedTrigger, - removedStepIds: [stepIdToDelete], + removedStepIds: allRemovedStepIds, }; };