From 530a18558be63576e1e2cfc686b46910d0f48949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Mon, 13 Jan 2025 16:53:57 +0100 Subject: [PATCH] 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 --- .../activities/emails/hooks/useEmailThread.ts | 5 +- .../components/CommandMenuContainer.tsx | 37 ++++- .../components/CommandMenuPages.ts | 4 + .../components/CommandMenuRouter.tsx | 10 +- .../components/CommandMenuTopBar.tsx | 41 +++-- .../constants/CommandMenuAnimationVariants.ts | 25 +++ .../constants/CommandMenuPagesConfig.tsx | 14 ++ .../command-menu/hooks/useCommandMenu.ts | 146 +++++++++++------- .../hooks/useCommandMenuHotKeys.ts | 32 ++-- .../types/CommandMenuAnimationVariant.ts | 4 + .../mapRightDrawerPageToCommandMenuPage.ts | 8 + .../hooks/useSelectableList.ts | 11 +- .../components/WorkflowDiagramCanvasBase.tsx | 40 ++--- .../WorkflowDiagramCanvasEditable.tsx | 9 +- .../WorkflowDiagramCanvasEditableEffect.tsx | 12 +- .../WorkflowDiagramCanvasReadonly.tsx | 9 +- .../WorkflowDiagramCanvasReadonlyEffect.tsx | 9 +- .../components/WorkflowVersionVisualizer.tsx | 11 +- .../components/WorkflowVisualizer.tsx | 6 +- .../hooks/useRightDrawerState.ts | 43 ++++++ .../hooks/__tests__/useDeleteStep.test.ts | 17 +- .../workflow-steps/hooks/useDeleteStep.ts | 3 + 22 files changed, 328 insertions(+), 168 deletions(-) create mode 100644 packages/twenty-front/src/modules/command-menu/constants/CommandMenuAnimationVariants.ts create mode 100644 packages/twenty-front/src/modules/command-menu/types/CommandMenuAnimationVariant.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useRightDrawerState.ts diff --git a/packages/twenty-front/src/modules/activities/emails/hooks/useEmailThread.ts b/packages/twenty-front/src/modules/activities/emails/hooks/useEmailThread.ts index c2fa722ed..e46e8fc58 100644 --- a/packages/twenty-front/src/modules/activities/emails/hooks/useEmailThread.ts +++ b/packages/twenty-front/src/modules/activities/emails/hooks/useEmailThread.ts @@ -1,12 +1,14 @@ import { useRecoilCallback } from 'recoil'; import { useOpenEmailThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer'; +import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState'; export const useEmailThread = () => { const { closeRightDrawer } = useRightDrawer(); + const { closeCommandMenu } = useCommandMenu(); const openEmailThreadRightDrawer = useOpenEmailThreadRightDrawer(); const openEmailThread = useRecoilCallback( @@ -23,13 +25,14 @@ export const useEmailThread = () => { if (isRightDrawerOpen && viewableEmailThreadId === threadId) { set(viewableRecordIdState, null); closeRightDrawer(); + closeCommandMenu(); return; } openEmailThreadRightDrawer(); set(viewableRecordIdState, threadId); }, - [closeRightDrawer, openEmailThreadRightDrawer], + [closeRightDrawer, closeCommandMenu, openEmailThreadRightDrawer], ); return { openEmailThread }; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx index 3fa14a772..8f10f298b 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx @@ -3,20 +3,25 @@ import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record- import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals'; import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { COMMAND_MENU_ANIMATION_VARIANTS } from '@/command-menu/constants/CommandMenuAnimationVariants'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; +import { CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; +import { workflowReactFlowRefState } from '@/workflow/workflow-diagram/states/workflowReactFlowRefState'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { motion } from 'framer-motion'; import { useRef } from 'react'; import { useRecoilValue } from 'recoil'; import { useIsMobile } from 'twenty-ui'; import { FeatureFlagKey } from '~/generated/graphql'; -const StyledCommandMenu = styled.div` +const StyledCommandMenu = styled(motion.div)` background: ${({ theme }) => theme.background.secondary}; border-left: 1px solid ${({ theme }) => theme.border.color.medium}; box-shadow: ${({ theme }) => theme.boxShadow.strong}; @@ -27,8 +32,9 @@ const StyledCommandMenu = styled.div` position: fixed; right: 0%; top: 0%; - width: ${() => (useIsMobile() ? '100%' : '500px')}; z-index: 30; + display: flex; + flex-direction: column; `; export const CommandMenuContainer = ({ @@ -45,15 +51,28 @@ export const CommandMenuContainer = ({ const commandMenuRef = useRef(null); + const workflowReactFlowRef = useRecoilValue(workflowReactFlowRefState); + useCommandMenuHotKeys(); useListenClickOutside({ - refs: [commandMenuRef], + refs: [ + commandMenuRef, + ...(workflowReactFlowRef ? [workflowReactFlowRef] : []), + ], callback: closeCommandMenu, listenerId: 'COMMAND_MENU_LISTENER_ID', hotkeyScope: AppHotkeyScope.CommandMenuOpen, }); + const isMobile = useIsMobile(); + + const targetVariantForAnimation: CommandMenuAnimationVariant = isMobile + ? 'fullScreen' + : 'normal'; + + const theme = useTheme(); + return ( } {isCommandMenuOpened && ( - + {children} )} diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPages.ts b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPages.ts index 378585df6..b06286450 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPages.ts +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPages.ts @@ -3,4 +3,8 @@ export enum CommandMenuPages { ViewRecord = 'view-record', ViewEmailThread = 'view-email-thread', ViewCalendarEvent = 'view-calendar-event', + WorkflowStepSelectTriggerType = 'workflow-step-select-trigger-type', + WorkflowStepSelectAction = 'workflow-step-select-action', + WorkflowStepView = 'workflow-step-view', + WorkflowStepEdit = 'workflow-step-edit', } diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx index 9c90f6e47..d5aee4267 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx @@ -2,9 +2,15 @@ import { CommandMenuContainer } from '@/command-menu/components/CommandMenuConta import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar'; import { COMMAND_MENU_PAGES_CONFIG } from '@/command-menu/constants/CommandMenuPagesConfig'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; +import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-ui'; +const StyledCommandMenuContent = styled.div` + flex: 1; + overflow-y: auto; +`; + export const CommandMenuRouter = () => { const commandMenuPage = useRecoilValue(commandMenuPageState); @@ -17,7 +23,9 @@ export const CommandMenuRouter = () => { return ( - {commandMenuPageComponent} + + {commandMenuPageComponent} + ); }; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx index 47cd9459a..f7052151f 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx @@ -1,12 +1,14 @@ import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip'; +import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages'; import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight'; import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import styled from '@emotion/styled'; -import { useRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { IconX, LightIconButton, isDefined, useIsMobile } from 'twenty-ui'; const StyledInputContainer = styled.div` @@ -17,6 +19,7 @@ const StyledInputContainer = styled.div` border-radius: 0; display: flex; + justify-content: space-between; font-size: ${({ theme }) => theme.font.size.lg}; height: ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px; margin: 0; @@ -25,6 +28,7 @@ const StyledInputContainer = styled.div` padding: 0 ${({ theme }) => theme.spacing(COMMAND_MENU_SEARCH_BAR_PADDING)}; gap: ${({ theme }) => theme.spacing(1)}; + flex-shrink: 0; `; const StyledInput = styled.input` @@ -45,6 +49,13 @@ const StyledInput = styled.input` } `; +const StyledContentContainer = styled.div` + align-items: center; + display: flex; + flex: 1; + gap: ${({ theme }) => theme.spacing(1)}; +`; + const StyledCloseButtonContainer = styled.div` align-items: center; display: flex; @@ -69,19 +80,25 @@ export const CommandMenuTopBar = () => { contextStoreCurrentObjectMetadataIdComponentState, ); + const commandMenuPage = useRecoilValue(commandMenuPageState); + return ( - {isDefined(contextStoreCurrentObjectMetadataId) && ( - - )} - + + {isDefined(contextStoreCurrentObjectMetadataId) && ( + + )} + {commandMenuPage === CommandMenuPages.Root && ( + + )} + {!isMobile && ( ], [CommandMenuPages.ViewEmailThread, ], [CommandMenuPages.ViewCalendarEvent, ], + [ + CommandMenuPages.WorkflowStepSelectTriggerType, + , + ], + [ + CommandMenuPages.WorkflowStepSelectAction, + , + ], + [CommandMenuPages.WorkflowStepEdit, ], + [CommandMenuPages.WorkflowStepView, ], ]); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts index fc23fbf96..f5a6b0715 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts @@ -1,4 +1,4 @@ -import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilCallback, useRecoilValue } from 'recoil'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; @@ -17,10 +17,10 @@ import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-sto import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId'; import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; +import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent'; import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState'; export const useCommandMenu = () => { - const setIsCommandMenuOpened = useSetRecoilState(isCommandMenuOpenedState); const { resetSelectedItem } = useSelectableList('command-menu-list'); const { setHotkeyScopeAndMemorizePreviousScope, @@ -141,13 +141,12 @@ export const useCommandMenu = () => { actionMenuEntries, ); - setIsCommandMenuOpened(true); + set(isCommandMenuOpenedState, true); setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen); }, [ mainContextStoreComponentInstanceId, setHotkeyScopeAndMemorizePreviousScope, - setIsCommandMenuOpened, ], ); @@ -158,67 +157,69 @@ export const useCommandMenu = () => { .getLoadable(isCommandMenuOpenedState) .getValue(); - set( - contextStoreCurrentObjectMetadataIdComponentState.atomFamily({ - instanceId: 'command-menu', - }), - null, - ); - - set( - contextStoreTargetedRecordsRuleComponentState.atomFamily({ - instanceId: 'command-menu', - }), - { - mode: 'selection', - selectedRecordIds: [], - }, - ); - - set( - contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ - instanceId: 'command-menu', - }), - 0, - ); - - set( - contextStoreFiltersComponentState.atomFamily({ - instanceId: 'command-menu', - }), - [], - ); - - set( - contextStoreCurrentViewIdComponentState.atomFamily({ - instanceId: 'command-menu', - }), - null, - ); - - set( - contextStoreCurrentViewTypeComponentState.atomFamily({ - instanceId: 'command-menu', - }), - null, - ); - - set( - actionMenuEntriesComponentState.atomFamily({ - instanceId: 'command-menu', - }), - new Map(), - ); - if (isCommandMenuOpened) { + set( + contextStoreCurrentObjectMetadataIdComponentState.atomFamily({ + instanceId: 'command-menu', + }), + null, + ); + + set( + contextStoreTargetedRecordsRuleComponentState.atomFamily({ + instanceId: 'command-menu', + }), + { + mode: 'selection', + selectedRecordIds: [], + }, + ); + + set( + contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ + instanceId: 'command-menu', + }), + 0, + ); + + set( + contextStoreFiltersComponentState.atomFamily({ + instanceId: 'command-menu', + }), + [], + ); + + set( + contextStoreCurrentViewIdComponentState.atomFamily({ + instanceId: 'command-menu', + }), + null, + ); + + set( + contextStoreCurrentViewTypeComponentState.atomFamily({ + instanceId: 'command-menu', + }), + null, + ); + + set( + actionMenuEntriesComponentState.atomFamily({ + instanceId: 'command-menu', + }), + new Map(), + ); + set(viewableRecordIdState, null); set(commandMenuPageState, CommandMenuPages.Root); - setIsCommandMenuOpened(false); + set(isCommandMenuOpenedState, false); resetSelectedItem(); goBackToPreviousHotkeyScope(); + + emitRightDrawerCloseEvent(); } }, - [goBackToPreviousHotkeyScope, resetSelectedItem, setIsCommandMenuOpened], + [goBackToPreviousHotkeyScope, resetSelectedItem], ); const toggleCommandMenu = useRecoilCallback( @@ -250,10 +251,39 @@ export const useCommandMenu = () => { [openCommandMenu], ); + const setGlobalCommandMenuContext = useRecoilCallback(({ set }) => { + return () => { + set( + contextStoreTargetedRecordsRuleComponentState.atomFamily({ + instanceId: 'command-menu', + }), + { + mode: 'selection', + selectedRecordIds: [], + }, + ); + + set( + contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ + instanceId: 'command-menu', + }), + 0, + ); + + set( + contextStoreCurrentViewTypeComponentState.atomFamily({ + instanceId: 'command-menu', + }), + null, + ); + }; + }, []); + return { openCommandMenu, closeCommandMenu, openRecordInCommandMenu, toggleCommandMenu, + resetCommandMenuContext: setGlobalCommandMenuContext, }; }; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts index d237a4f41..ccc39f699 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts @@ -1,32 +1,24 @@ +import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; -import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; -import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; -import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { isNonEmptyString } from '@sniptt/guards'; import { useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; export const useCommandMenuHotKeys = () => { - const { closeCommandMenu, toggleCommandMenu } = useCommandMenu(); + const { closeCommandMenu, toggleCommandMenu, resetCommandMenuContext } = + useCommandMenu(); const commandMenuSearch = useRecoilValue(commandMenuSearchState); - const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2( - contextStoreTargetedRecordsRuleComponentState, - 'command-menu', - ); - - const setContextStoreNumberOfSelectedRecords = useSetRecoilComponentStateV2( - contextStoreNumberOfSelectedRecordsComponentState, - 'command-menu', - ); - const { closeKeyboardShortcutMenu } = useKeyboardShortcutMenu(); + const commandMenuPage = useRecoilValue(commandMenuPageState); + useScopedHotkeys( 'ctrl+k,meta+k', () => { @@ -49,13 +41,11 @@ export const useCommandMenuHotKeys = () => { useScopedHotkeys( [Key.Backspace, Key.Delete], () => { - if (!isNonEmptyString(commandMenuSearch)) { - setContextStoreTargetedRecordsRule({ - mode: 'selection', - selectedRecordIds: [], - }); - - setContextStoreNumberOfSelectedRecords(0); + if ( + commandMenuPage === CommandMenuPages.Root && + !isNonEmptyString(commandMenuSearch) + ) { + resetCommandMenuContext(); } }, AppHotkeyScope.CommandMenuOpen, diff --git a/packages/twenty-front/src/modules/command-menu/types/CommandMenuAnimationVariant.ts b/packages/twenty-front/src/modules/command-menu/types/CommandMenuAnimationVariant.ts new file mode 100644 index 000000000..e5c8ebee8 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/types/CommandMenuAnimationVariant.ts @@ -0,0 +1,4 @@ +import { COMMAND_MENU_ANIMATION_VARIANTS } from '@/command-menu/constants/CommandMenuAnimationVariants'; + +export type CommandMenuAnimationVariant = + keyof typeof COMMAND_MENU_ANIMATION_VARIANTS; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/utils/mapRightDrawerPageToCommandMenuPage.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/utils/mapRightDrawerPageToCommandMenuPage.ts index 6a90ea080..76532961c 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/utils/mapRightDrawerPageToCommandMenuPage.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/utils/mapRightDrawerPageToCommandMenuPage.ts @@ -11,6 +11,14 @@ export const mapRightDrawerPageToCommandMenuPage = ( return CommandMenuPages.ViewEmailThread; case RightDrawerPages.ViewCalendarEvent: return CommandMenuPages.ViewCalendarEvent; + case RightDrawerPages.WorkflowStepSelectTriggerType: + return CommandMenuPages.WorkflowStepSelectTriggerType; + case RightDrawerPages.WorkflowStepSelectAction: + return CommandMenuPages.WorkflowStepSelectAction; + case RightDrawerPages.WorkflowStepView: + return CommandMenuPages.WorkflowStepView; + case RightDrawerPages.WorkflowStepEdit: + return CommandMenuPages.WorkflowStepEdit; default: return CommandMenuPages.Root; } diff --git a/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/useSelectableList.ts b/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/useSelectableList.ts index e48ac7bab..41a85a10a 100644 --- a/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/useSelectableList.ts +++ b/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/useSelectableList.ts @@ -34,13 +34,18 @@ export const useSelectableList = (selectableListId?: string) => { ); const setSelectedItemId = useRecoilCallback( - ({ set }) => + ({ set, snapshot }) => (itemId: string) => { - resetSelectedItem(); + const selectedItemId = getSnapshotValue(snapshot, selectedItemIdState); + + if (isDefined(selectedItemId)) { + set(isSelectedItemIdSelector(selectedItemId), false); + } + set(selectedItemIdState, itemId); set(isSelectedItemIdSelector(itemId), true); }, - [resetSelectedItem, selectedItemIdState, isSelectedItemIdSelector], + [selectedItemIdState, isSelectedItemIdSelector], ); return { 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 576fa288c..aa42468c7 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 @@ -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 ( diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx index e14ad6879..e9dee1613 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable.tsx @@ -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 ( - - + /> + ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx index c8b8c4dc8..f1844f78d 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect.tsx @@ -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, ], ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly.tsx index 8a7b42902..c48388e9c 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly.tsx @@ -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 ( - - + /> + ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect.tsx index f14acd87f..71cdd8e33 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect.tsx @@ -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, ], ); diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizer.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizer.tsx index 16070202c..e34bf6739 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizer.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVersionVisualizer.tsx @@ -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) ? ( - + return isDefined(workflowVersion) ? ( + ) : null; }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVisualizer.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVisualizer.tsx index e72356160..b717fb4b2 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVisualizer.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowVisualizer.tsx @@ -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) ? ( ) : null} diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useRightDrawerState.ts b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useRightDrawerState.ts new file mode 100644 index 000000000..dbf0e3187 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/hooks/useRightDrawerState.ts @@ -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', + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useDeleteStep.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useDeleteStep.test.ts index fc26dad3e..7e9513a63 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useDeleteStep.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useDeleteStep.test.ts @@ -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'); 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 12a03540b..c19594786 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 @@ -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({