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
parent 0d0cce7bb1
commit ce045db15a
7 changed files with 129 additions and 42 deletions
@@ -11,6 +11,7 @@ import { SeeActiveVersionWorkflowSingleRecordAction } from '@/action-menu/action
import { SeeRunsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/components/SeeRunsWorkflowSingleRecordAction';
import { SeeVersionsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/components/SeeVersionsWorkflowSingleRecordAction';
import { TestWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/components/TestWorkflowSingleRecordAction';
import { TidyUpWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/components/TidyUpWorkflowSingleRecordAction';
import { WorkflowSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/workflow-actions/types/WorkflowSingleRecordActionsKeys';
import { inheritActionsFromDefaultConfig } from '@/action-menu/actions/record-actions/utils/inheritActionsFromDefaultConfig';
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
@@ -32,6 +33,7 @@ import {
IconPlayerPlay,
IconPlus,
IconPower,
IconReorder,
IconVersions,
} from 'twenty-ui/display';
@@ -209,6 +211,21 @@ export const WORKFLOW_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({
availableOn: [ActionViewType.SHOW_PAGE],
component: <AddNodeWorkflowSingleRecordAction />,
},
[WorkflowSingleRecordActionKeys.TIDY_UP]: {
key: WorkflowSingleRecordActionKeys.TIDY_UP,
label: msg`Tidy up workflow`,
shortLabel: msg`Tidy up`,
isPinned: false,
position: 9,
Icon: IconReorder,
type: ActionType.Standard,
scope: ActionScope.RecordSelection,
shouldBeRegistered: ({ selectedRecord, workflowWithCurrentVersion }) =>
areWorkflowTriggerAndStepsDefined(workflowWithCurrentVersion) &&
!isDefined(selectedRecord?.deletedAt),
availableOn: [ActionViewType.SHOW_PAGE],
component: <TidyUpWorkflowSingleRecordAction />,
},
[NoSelectionWorkflowRecordActionKeys.GO_TO_RUNS]: {
type: ActionType.Navigation,
scope: ActionScope.Global,
@@ -260,82 +277,82 @@ export const WORKFLOW_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({
label: msg`Create new workflow`,
},
[SingleRecordActionKeys.ADD_TO_FAVORITES]: {
position: 9,
},
[SingleRecordActionKeys.REMOVE_FROM_FAVORITES]: {
position: 10,
},
[SingleRecordActionKeys.DELETE]: {
[SingleRecordActionKeys.REMOVE_FROM_FAVORITES]: {
position: 11,
},
[SingleRecordActionKeys.DELETE]: {
position: 12,
label: msg`Delete workflow`,
},
[MultipleRecordsActionKeys.DELETE]: {
position: 12,
position: 13,
label: msg`Delete workflows`,
},
[SingleRecordActionKeys.DESTROY]: {
position: 13,
position: 14,
label: msg`Permanently destroy workflow`,
},
[SingleRecordActionKeys.EXPORT_FROM_RECORD_INDEX]: {
position: 14,
position: 15,
label: msg`Export workflow`,
shouldBeRegistered: ({ selectedRecord }) =>
!isDefined(selectedRecord?.deletedAt),
},
[SingleRecordActionKeys.EXPORT_FROM_RECORD_SHOW]: {
position: 15,
position: 16,
label: msg`Export workflow`,
},
[MultipleRecordsActionKeys.EXPORT]: {
position: 16,
position: 17,
label: msg`Export workflows`,
},
[NoSelectionRecordActionKeys.EXPORT_VIEW]: {
position: 16,
position: 17,
label: msg`Export view`,
},
[MultipleRecordsActionKeys.DESTROY]: {
position: 17,
position: 18,
label: msg`Permanently destroy workflows`,
},
[SingleRecordActionKeys.NAVIGATE_TO_PREVIOUS_RECORD]: {
position: 18,
position: 19,
label: msg`Navigate to previous workflow`,
},
[SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD]: {
position: 19,
position: 20,
label: msg`Navigate to next workflow`,
},
[NoSelectionRecordActionKeys.SEE_DELETED_RECORDS]: {
position: 20,
position: 21,
label: msg`See deleted workflows`,
},
[NoSelectionRecordActionKeys.HIDE_DELETED_RECORDS]: {
position: 21,
position: 22,
label: msg`Hide deleted workflows`,
},
[NoSelectionRecordActionKeys.IMPORT_RECORDS]: {
position: 22,
position: 23,
label: msg`Import workflows`,
},
[NoSelectionRecordActionKeys.GO_TO_PEOPLE]: {
position: 23,
},
[NoSelectionRecordActionKeys.GO_TO_COMPANIES]: {
position: 24,
},
[NoSelectionRecordActionKeys.GO_TO_OPPORTUNITIES]: {
[NoSelectionRecordActionKeys.GO_TO_COMPANIES]: {
position: 25,
},
[NoSelectionRecordActionKeys.GO_TO_SETTINGS]: {
[NoSelectionRecordActionKeys.GO_TO_OPPORTUNITIES]: {
position: 26,
},
[NoSelectionRecordActionKeys.GO_TO_TASKS]: {
[NoSelectionRecordActionKeys.GO_TO_SETTINGS]: {
position: 27,
},
[NoSelectionRecordActionKeys.GO_TO_NOTES]: {
[NoSelectionRecordActionKeys.GO_TO_TASKS]: {
position: 28,
},
[NoSelectionRecordActionKeys.GO_TO_NOTES]: {
position: 29,
},
},
});
@@ -0,0 +1,41 @@
import { Action } from '@/action-menu/actions/components/Action';
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId';
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
import { useTidyUpWorkflowVersion } from '@/workflow/workflow-version/hooks/useTidyUpWorkflowVersion';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const TidyUpWorkflowSingleRecordAction = () => {
const recordId = useSelectedRecordIdOrThrow();
const { tidyUpWorkflowVersion } = useTidyUpWorkflowVersion();
const instanceId = getWorkflowVisualizerComponentInstanceId({
recordId,
});
const { getUpdatableWorkflowVersion } =
useGetUpdatableWorkflowVersionOrThrow(instanceId);
const onClick = useRecoilCallback(
({ snapshot }) =>
async () => {
const workflowDiagramState = workflowDiagramComponentState.atomFamily({
instanceId,
});
const workflowDiagram = snapshot
.getLoadable(workflowDiagramState)
.getValue();
if (!isDefined(workflowDiagram)) {
return;
}
const workflowVersionId = await getUpdatableWorkflowVersion();
await tidyUpWorkflowVersion(workflowVersionId, workflowDiagram);
},
[getUpdatableWorkflowVersion, tidyUpWorkflowVersion, instanceId],
);
return <Action onClick={onClick} />;
};
@@ -7,4 +7,5 @@ export enum WorkflowSingleRecordActionKeys {
SEE_VERSIONS = 'see-versions-workflow-single-record',
TEST = 'test-workflow-single-record',
ADD_NODE = 'add-node-workflow-single-record',
TIDY_UP = 'tidy-up-workflow-single-record',
}