9426 migrate workflow pages to command menu (#9515)

Closes twentyhq/core-team-issues#53 

- Removes command menu top bar text input when the user is not on root
page
- Fixes bug when resetting command menu context
- Added animations on command menu open and close
- Refactored workflow visualizer code to remove unnecessary rerenders
and props drilling


https://github.com/user-attachments/assets/1da3adb8-220b-407b-9279-30354d3100d3
This commit is contained in:
Raphaël Bosi
2025-01-13 16:53:57 +01:00
committed by GitHub
parent 330addbc0b
commit 530a18558b
22 changed files with 328 additions and 168 deletions

View File

@ -1,13 +1,11 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose';
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
import { WorkflowVersionStatusTag } from '@/workflow/workflow-diagram/components/WorkflowVersionStatusTag';
import { useRightDrawerState } from '@/workflow/workflow-diagram/hooks/useRightDrawerState';
import { workflowDiagramState } from '@/workflow/workflow-diagram/states/workflowDiagramState';
import { workflowReactFlowRefState } from '@/workflow/workflow-diagram/states/workflowReactFlowRefState';
import {
WorkflowDiagram,
WorkflowDiagramEdge,
WorkflowDiagramNode,
WorkflowDiagramNodeType,
@ -16,21 +14,21 @@ import { getOrganizedDiagram } from '@/workflow/workflow-diagram/utils/getOrgani
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
applyEdgeChanges,
applyNodeChanges,
Background,
EdgeChange,
FitViewOptions,
getNodesBounds,
NodeChange,
NodeProps,
ReactFlow,
applyEdgeChanges,
applyNodeChanges,
getNodesBounds,
useReactFlow,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import React, { useEffect, useMemo, useRef } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined, THEME_COMMON } from 'twenty-ui';
import { THEME_COMMON, isDefined } from 'twenty-ui';
const StyledResetReactflowStyles = styled.div`
height: 100%;
@ -68,12 +66,10 @@ const defaultFitViewOptions = {
} satisfies FitViewOptions;
export const WorkflowDiagramCanvasBase = ({
diagram,
status,
nodeTypes,
children,
}: {
diagram: WorkflowDiagram;
status: WorkflowVersionStatus;
nodeTypes: Partial<
Record<
@ -95,22 +91,17 @@ export const WorkflowDiagramCanvasBase = ({
workflowReactFlowRefState,
);
const workflowDiagram = useRecoilValue(workflowDiagramState);
const { nodes, edges } = useMemo(
() => getOrganizedDiagram(diagram),
[diagram],
() =>
isDefined(workflowDiagram)
? getOrganizedDiagram(workflowDiagram)
: { nodes: [], edges: [] },
[workflowDiagram],
);
const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState);
const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState);
const isMobile = useIsMobile();
const rightDrawerState = !isRightDrawerOpen
? 'closed'
: isRightDrawerMinimized
? 'minimized'
: isMobile
? 'fullScreen'
: 'normal';
const { rightDrawerState } = useRightDrawerState();
const rightDrawerWidth = Number(
THEME_COMMON.rightDrawerWidth.replace('px', ''),
@ -187,6 +178,8 @@ export const WorkflowDiagramCanvasBase = ({
);
}, [reactflow, rightDrawerState, rightDrawerWidth]);
const { closeCommandMenu } = useCommandMenu();
return (
<StyledResetReactflowStyles ref={containerRef}>
<ReactFlow
@ -220,6 +213,7 @@ export const WorkflowDiagramCanvasBase = ({
nodesFocusable={false}
edgesFocusable={false}
nodesDraggable={false}
onPaneClick={closeCommandMenu}
paneClickDistance={10} // Fix small unwanted user dragging does not select node
>
<Background color={theme.border.color.medium} size={2} />

View File

@ -4,29 +4,24 @@ import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/workflow-diagram
import { WorkflowDiagramCreateStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepNode';
import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger';
import { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable';
import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { ReactFlowProvider } from '@xyflow/react';
export const WorkflowDiagramCanvasEditable = ({
diagram,
workflowWithCurrentVersion,
}: {
diagram: WorkflowDiagram;
workflowWithCurrentVersion: WorkflowWithCurrentVersion;
}) => {
return (
<ReactFlowProvider>
<WorkflowDiagramCanvasBase
diagram={diagram}
status={workflowWithCurrentVersion.currentVersion.status}
nodeTypes={{
default: WorkflowDiagramStepNodeEditable,
'create-step': WorkflowDiagramCreateStepNode,
'empty-trigger': WorkflowDiagramEmptyTrigger,
}}
>
<WorkflowDiagramCanvasEditableEffect />
</WorkflowDiagramCanvasBase>
/>
<WorkflowDiagramCanvasEditableEffect />
</ReactFlowProvider>
);
};

View File

@ -1,3 +1,4 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
@ -17,6 +18,8 @@ export const WorkflowDiagramCanvasEditableEffect = () => {
const { startNodeCreation } = useStartNodeCreation();
const { openRightDrawer, closeRightDrawer } = useRightDrawer();
const { closeCommandMenu } = useCommandMenu();
const setHotkeyScope = useSetHotkeyScope();
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
@ -28,7 +31,7 @@ export const WorkflowDiagramCanvasEditableEffect = () => {
if (isClosingStep) {
closeRightDrawer();
closeCommandMenu();
return;
}
@ -55,10 +58,11 @@ export const WorkflowDiagramCanvasEditableEffect = () => {
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
},
[
setHotkeyScope,
closeRightDrawer,
openRightDrawer,
setWorkflowSelectedNode,
setHotkeyScope,
openRightDrawer,
closeRightDrawer,
closeCommandMenu,
startNodeCreation,
],
);

View File

@ -3,28 +3,23 @@ import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/component
import { WorkflowDiagramCanvasReadonlyEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect';
import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger';
import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly';
import { WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { ReactFlowProvider } from '@xyflow/react';
export const WorkflowDiagramCanvasReadonly = ({
diagram,
workflowVersion,
}: {
diagram: WorkflowDiagram;
workflowVersion: WorkflowVersion;
}) => {
return (
<ReactFlowProvider>
<WorkflowDiagramCanvasBase
diagram={diagram}
status={workflowVersion.status}
nodeTypes={{
default: WorkflowDiagramStepNodeReadonly,
'empty-trigger': WorkflowDiagramEmptyTrigger,
}}
>
<WorkflowDiagramCanvasReadonlyEffect />
</WorkflowDiagramCanvasBase>
/>
<WorkflowDiagramCanvasReadonlyEffect />
</ReactFlowProvider>
);
};

View File

@ -1,3 +1,4 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
@ -14,6 +15,7 @@ export const WorkflowDiagramCanvasReadonlyEffect = () => {
const { openRightDrawer, closeRightDrawer } = useRightDrawer();
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
const setHotkeyScope = useSetHotkeyScope();
const { closeCommandMenu } = useCommandMenu();
const handleSelectionChange = useCallback(
({ nodes }: OnSelectionChangeParams) => {
@ -22,7 +24,7 @@ export const WorkflowDiagramCanvasReadonlyEffect = () => {
if (isClosingStep) {
closeRightDrawer();
closeCommandMenu();
return;
}
@ -31,10 +33,11 @@ export const WorkflowDiagramCanvasReadonlyEffect = () => {
openRightDrawer(RightDrawerPages.WorkflowStepView);
},
[
closeRightDrawer,
openRightDrawer,
setWorkflowSelectedNode,
setHotkeyScope,
openRightDrawer,
closeRightDrawer,
closeCommandMenu,
],
);

View File

@ -1,8 +1,6 @@
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
import { WorkflowDiagramCanvasReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly';
import { workflowDiagramState } from '@/workflow/workflow-diagram/states/workflowDiagramState';
import '@xyflow/react/dist/style.css';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
export const WorkflowVersionVisualizer = ({
@ -12,12 +10,7 @@ export const WorkflowVersionVisualizer = ({
}) => {
const workflowVersion = useWorkflowVersion(workflowVersionId);
const workflowDiagram = useRecoilValue(workflowDiagramState);
return isDefined(workflowDiagram) && isDefined(workflowVersion) ? (
<WorkflowDiagramCanvasReadonly
diagram={workflowDiagram}
workflowVersion={workflowVersion}
/>
return isDefined(workflowVersion) ? (
<WorkflowDiagramCanvasReadonly workflowVersion={workflowVersion} />
) : null;
};

View File

@ -2,9 +2,7 @@ import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableE
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { WorkflowDiagramCanvasEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable';
import { WorkflowDiagramEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramEffect';
import { workflowDiagramState } from '@/workflow/workflow-diagram/states/workflowDiagramState';
import '@xyflow/react/dist/style.css';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
export const WorkflowVisualizer = ({
@ -15,7 +13,6 @@ export const WorkflowVisualizer = ({
const workflowId = targetableObject.id;
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
const workflowDiagram = useRecoilValue(workflowDiagramState);
return (
<>
@ -23,9 +20,8 @@ export const WorkflowVisualizer = ({
workflowWithCurrentVersion={workflowWithCurrentVersion}
/>
{isDefined(workflowDiagram) && isDefined(workflowWithCurrentVersion) ? (
{isDefined(workflowWithCurrentVersion) ? (
<WorkflowDiagramCanvasEditable
diagram={workflowDiagram}
workflowWithCurrentVersion={workflowWithCurrentVersion}
/>
) : null}

View File

@ -0,0 +1,43 @@
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant';
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
import { RightDrawerAnimationVariant } from '@/ui/layout/right-drawer/types/RightDrawerAnimationVariant';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil';
import { useIsMobile } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
export const useRightDrawerState = (): {
rightDrawerState: RightDrawerAnimationVariant | CommandMenuAnimationVariant;
} => {
const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState);
const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState);
const isMobile = useIsMobile();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
if (isMobile) {
return {
rightDrawerState: 'fullScreen',
};
}
if (isCommandMenuV2Enabled) {
return {
rightDrawerState: isCommandMenuOpened ? 'normal' : 'closed',
};
}
return {
rightDrawerState: !isRightDrawerOpen
? 'closed'
: isRightDrawerMinimized
? 'minimized'
: 'normal',
};
};

View File

@ -1,6 +1,7 @@
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
import { renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
const mockCloseRightDrawer = jest.fn();
const mockCreateNewWorkflowVersion = jest.fn();
@ -13,12 +14,6 @@ jest.mock('@/object-record/hooks/useUpdateOneRecord', () => ({
}),
}));
jest.mock('recoil', () => ({
useRecoilValue: () => 'parent-step-id',
useSetRecoilState: () => jest.fn(),
atom: (params: any) => params,
}));
jest.mock('@/ui/layout/right-drawer/hooks/useRightDrawer', () => ({
useRightDrawer: () => ({
closeRightDrawer: mockCloseRightDrawer,
@ -50,10 +45,12 @@ describe('useDeleteStep', () => {
};
it('should delete step in draft version', async () => {
const { result } = renderHook(() =>
useDeleteStep({
workflow: mockWorkflow as unknown as WorkflowWithCurrentVersion,
}),
const { result } = renderHook(
() =>
useDeleteStep({
workflow: mockWorkflow as unknown as WorkflowWithCurrentVersion,
}),
{ wrapper: RecoilRoot },
);
await result.current.deleteStep('1');

View File

@ -1,3 +1,4 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
@ -22,9 +23,11 @@ export const useDeleteStep = ({
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
const { closeRightDrawer } = useRightDrawer();
const { closeCommandMenu } = useCommandMenu();
const deleteStep = async (stepId: string) => {
closeRightDrawer();
closeCommandMenu();
const workflowVersion = await getUpdatableWorkflowVersion(workflow);
if (stepId === TRIGGER_STEP_ID) {
await updateOneWorkflowVersion({