Analyze Context of Issue #1586 Using GitHub MCP (#15058)

# Implement "Tidy Up" Action for Workflow Diagram

**Task Link:** https://twill.ai/twentyhq/ENG/tasks/6

## Summary

This PR adds a "Tidy Up" action to the workflow diagram interface,
allowing users to automatically organize and clean up the layout of
workflow nodes and connections.

## Changes Made

- **Added new workflow action**: Created
`TidyUpWorkflowSingleRecordAction` component to provide a UI action for
tidying up workflow diagrams
- **Refactored tidy-up logic**: Extracted core tidy-up functionality
into a reusable `useTidyUp` hook, separating layout logic from workflow
version persistence
- **Updated action menu configuration**: Integrated the new tidy-up
action into `WorkflowActionsConfig` with proper permissions and keyboard
shortcuts
- **Enhanced right-click menu**: Updated
`WorkflowDiagramRightClickCommandMenu` to use the refactored tidy-up
hook
- **Improved hook architecture**: Simplified `useTidyUpWorkflowVersion`
to delegate layout calculations to the new `useTidyUp` hook

## Key Features

- Users can now trigger workflow diagram tidy-up from the action menu
- Consistent tidy-up behavior across both right-click menu and action
menu interfaces
- Better separation of concerns between layout calculation and data
persistence

<details>
<summary>📸 Screenshots</summary>

Playwright test screenshots captured during development:

### command-menu-with-tidy-up-workflow.png


![command-menu-with-tidy-up-workflow.png](https://storage.googleapis.com/twill-screenshots-twill-469020/screenshots/1760356112795-command-menu-with-tidy-up-workflow.png)

### tidy-up-workflow-command-menu.png


![tidy-up-workflow-command-menu.png](https://storage.googleapis.com/twill-screenshots-twill-469020/screenshots/1760356113168-tidy-up-workflow-command-menu.png)

### tidy-up-workflow-error-toast.png


![tidy-up-workflow-error-toast.png](https://storage.googleapis.com/twill-screenshots-twill-469020/screenshots/1760356113407-tidy-up-workflow-error-toast.png)

</details>

---------

Co-authored-by: Twill <agent@twill.ai>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
This commit is contained in:
twill-hq[bot]
2025-10-16 16:59:48 +00:00
committed by GitHub
co-authored by Twill Claude Thomas Trompette
parent 0d0cce7bb1
commit ce045db15a
7 changed files with 129 additions and 42 deletions
@@ -4,11 +4,12 @@ import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithC
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import { isDefined } from 'twenty-shared/utils';
export const useGetUpdatableWorkflowVersionOrThrow = () => {
export const useGetUpdatableWorkflowVersionOrThrow = (instanceId?: string) => {
const { createDraftFromWorkflowVersion } =
useCreateDraftFromWorkflowVersion();
const workflowVisualizerWorkflowId = useRecoilComponentValue(
workflowVisualizerWorkflowIdComponentState,
instanceId,
);
const workflow = useWorkflowWithCurrentVersion(workflowVisualizerWorkflowId);
@@ -3,7 +3,7 @@ import { useCloseRightClickMenu } from '@/workflow/workflow-diagram/hooks/useClo
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
import { useWorkflowDiagramScreenToFlowPosition } from '@/workflow/workflow-diagram/hooks/useWorkflowDiagramScreenToFlowPosition';
import { workflowDiagramRightClickMenuPositionState } from '@/workflow/workflow-diagram/states/workflowDiagramRightClickMenuPositionState';
import { useTidyUpWorkflowVersion } from '@/workflow/workflow-version/hooks/useTidyUpWorkflowVersion';
import { useTidyUp } from '@/workflow/workflow-version/hooks/useTidyUp';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useRef } from 'react';
@@ -41,10 +41,10 @@ export const WorkflowDiagramRightClickCommandMenu = () => {
workflowDiagramRightClickMenuPositionState,
);
const { tidyUpWorkflowVersion } = useTidyUpWorkflowVersion();
const { tidyUp } = useTidyUp();
const handleReorderWorkflowDiagram = async () => {
await tidyUpWorkflowVersion();
await tidyUp();
closeRightClickMenu();
};
@@ -0,0 +1,34 @@
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
import { useTidyUpWorkflowVersion } from '@/workflow/workflow-version/hooks/useTidyUpWorkflowVersion';
import { isDefined } from 'twenty-shared/utils';
export const useTidyUp = () => {
const [workflowDiagram, setWorkflowDiagram] = useRecoilComponentState(
workflowDiagramComponentState,
);
const { tidyUpWorkflowVersion } = useTidyUpWorkflowVersion();
const { getUpdatableWorkflowVersion } =
useGetUpdatableWorkflowVersionOrThrow();
const tidyUp = async () => {
if (!isDefined(workflowDiagram)) {
return;
}
const workflowVersionId = await getUpdatableWorkflowVersion();
const tidiedUpDiagram = await tidyUpWorkflowVersion(
workflowVersionId,
workflowDiagram,
);
if (isDefined(tidiedUpDiagram)) {
setWorkflowDiagram(tidiedUpDiagram);
}
};
return { tidyUp };
};
@@ -1,5 +1,5 @@
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
import { type WorkflowVersion } from '@/workflow/types/Workflow';
import { type WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { useMutation } from '@apollo/client';
import { isDefined } from 'twenty-shared/utils';
import {
@@ -15,16 +15,10 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
import { getOrganizedDiagram } from '@/workflow/workflow-diagram/utils/getOrganizedDiagram';
import { UPDATE_WORKFLOW_VERSION_POSITIONS } from '@/workflow/workflow-version/graphql/mutations/updateWorkflowVersionPositions';
export const useTidyUpWorkflowVersion = () => {
const [workflowDiagram, setWorkflowDiagram] = useRecoilComponentState(
workflowDiagramComponentState,
);
const apolloCoreClient = useApolloCoreClient();
const { objectMetadataItems } = useObjectMetadataItems();
@@ -42,14 +36,10 @@ export const useTidyUpWorkflowVersion = () => {
UpdateWorkflowVersionPositionsMutationVariables
>(UPDATE_WORKFLOW_VERSION_POSITIONS, { client: apolloCoreClient });
const { getUpdatableWorkflowVersion } =
useGetUpdatableWorkflowVersionOrThrow();
const updateWorkflowVersionPosition = async (
workflowVersionId: string,
positions: { id: string; position: { x: number; y: number } }[],
) => {
const workflowVersionId = await getUpdatableWorkflowVersion();
await mutate({ variables: { input: { workflowVersionId, positions } } });
const cachedRecord = getRecordFromCache<WorkflowVersion>(workflowVersionId);
@@ -100,7 +90,10 @@ export const useTidyUpWorkflowVersion = () => {
});
};
const tidyUpWorkflowVersion = async () => {
const tidyUpWorkflowVersion = async (
workflowVersionId: string,
workflowDiagram: WorkflowDiagram,
) => {
if (!isDefined(workflowDiagram)) {
return;
}
@@ -112,9 +105,9 @@ export const useTidyUpWorkflowVersion = () => {
position: node.position,
}));
await updateWorkflowVersionPosition(positions);
await updateWorkflowVersionPosition(workflowVersionId, positions);
setWorkflowDiagram(tidiedUpDiagram);
return tidiedUpDiagram;
};
return { tidyUpWorkflowVersion };