diff --git a/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useOpenRecordInSidePanel.test.tsx b/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useOpenRecordInSidePanel.test.tsx index 6af5b33a29..f28d91b77d 100644 --- a/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useOpenRecordInSidePanel.test.tsx +++ b/packages/twenty-front/src/modules/side-panel/hooks/__tests__/useOpenRecordInSidePanel.test.tsx @@ -17,7 +17,11 @@ import { sidePanelNavigationMorphItemsByPageState } from '@/side-panel/states/si import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore'; -import { ContextStorePageType, SidePanelPages } from 'twenty-shared/types'; +import { + AppPath, + ContextStorePageType, + SidePanelPages, +} from 'twenty-shared/types'; import { useIcons } from 'twenty-ui/icon'; import { PageLayoutType } from '~/generated-metadata/graphql'; import { getJestMetadataAndApolloMocksAndCommandMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndCommandMenuWrapper'; @@ -45,6 +49,17 @@ jest.mock( }), ); +const mockNavigateApp = jest.fn(); +jest.mock('~/hooks/useNavigateApp', () => ({ + useNavigateApp: () => mockNavigateApp, +})); + +let mockIsMobile = false; +jest.mock('twenty-ui/utilities', () => ({ + ...jest.requireActual('twenty-ui/utilities'), + useIsMobile: () => mockIsMobile, +})); + const personMockObjectMetadataItem = getTestEnrichedObjectMetadataItemsMock().find( (item) => item.nameSingular === 'person', @@ -117,6 +132,7 @@ const renderHooks = () => { describe('useOpenRecordInSidePanel', () => { beforeEach(() => { jest.clearAllMocks(); + mockIsMobile = false; }); it('should set the correct states and navigate to the record page', () => { @@ -256,4 +272,88 @@ describe('useOpenRecordInSidePanel', () => { expect(mockOpenNewRecordTitleCell).not.toHaveBeenCalled(); }); + + it('should navigate to the record page instead of the side panel on mobile', () => { + mockIsMobile = true; + const { result } = renderHooks(); + + act(() => { + result.current.openRecordInSidePanel({ + recordId: 'record-123', + objectNameSingular: 'person', + }); + }); + + expect(mockNavigateApp).toHaveBeenCalledWith( + AppPath.RecordShowPage, + { objectNameSingular: 'person', objectRecordId: 'record-123' }, + undefined, + undefined, + ); + expect(mockNavigateSidePanel).not.toHaveBeenCalled(); + }); + + it('should forward new record state to the record page on mobile', () => { + mockIsMobile = true; + const { result } = renderHooks(); + + act(() => { + result.current.openRecordInSidePanel({ + recordId: 'new-record-123', + objectNameSingular: 'person', + isNewRecord: true, + }); + }); + + expect(mockNavigateApp).toHaveBeenCalledWith( + AppPath.RecordShowPage, + { objectNameSingular: 'person', objectRecordId: 'new-record-123' }, + undefined, + { + state: { + isNewRecord: true, + objectRecordId: 'new-record-123', + labelIdentifierFieldName: getLabelIdentifierFieldMetadataItem( + personMockObjectMetadataItem, + )?.name, + }, + }, + ); + expect(mockOpenNewRecordTitleCell).not.toHaveBeenCalled(); + }); + + it('should preset the record page active tab on mobile', () => { + mockIsMobile = true; + const { result } = renderHooks(); + + const recordId = 'record-123'; + const objectNameSingular = 'person'; + + act(() => { + result.current.openRecordInSidePanel({ + recordId, + objectNameSingular, + tab: 'tab-emails', + }); + }); + + const tabListInstanceId = getTabListInstanceIdFromPageLayoutAndRecord({ + pageLayoutId: getDefaultRecordPageLayoutId({ + targetObjectNameSingular: objectNameSingular, + }), + layoutType: PageLayoutType.RECORD_PAGE, + targetRecordIdentifier: { + id: recordId, + targetObjectNameSingular: objectNameSingular, + }, + }); + + expect( + jotaiStore.get( + activeTabIdComponentState.atomFamily({ + instanceId: tabListInstanceId, + }), + ), + ).toBe('tab-emails'); + }); }); diff --git a/packages/twenty-front/src/modules/side-panel/hooks/useOpenRecordInSidePanel.ts b/packages/twenty-front/src/modules/side-panel/hooks/useOpenRecordInSidePanel.ts index 6806c2e0f2..9c170a01f9 100644 --- a/packages/twenty-front/src/modules/side-panel/hooks/useOpenRecordInSidePanel.ts +++ b/packages/twenty-front/src/modules/side-panel/hooks/useOpenRecordInSidePanel.ts @@ -16,6 +16,7 @@ import { viewableRecordIdState } from '@/object-record/record-side-panel/states/ import { useOpenNewRecordTitleCell } from '@/object-record/record-title-cell/hooks/useOpenNewRecordTitleCell'; import { setRecordPageActiveTabId } from '@/page-layout/utils/setRecordPageActiveTabId'; import { + AppPath, ContextStorePageType, CoreObjectNameSingular, SidePanelPages, @@ -27,17 +28,22 @@ import { useStore } from 'jotai'; import { useCallback } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/icon'; +import { useIsMobile } from 'twenty-ui/utilities'; import { v4 } from 'uuid'; +import { useNavigateApp } from '~/hooks/useNavigateApp'; export const useOpenRecordInSidePanel = () => { const store = useStore(); const { getIcon } = useIcons(); - const { navigateSidePanelMenu } = useSidePanelMenu(); + const { navigateSidePanelMenu, closeSidePanelMenu } = useSidePanelMenu(); const { runWorkflowRunOpeningInSidePanelEffects } = useRunWorkflowRunOpeningInSidePanelEffects(); const { openNewRecordTitleCell } = useOpenNewRecordTitleCell(); + const isMobile = useIsMobile(); + const navigate = useNavigateApp(); + const openRecordInSidePanel = useCallback( ({ recordId, @@ -61,6 +67,38 @@ export const useOpenRecordInSidePanel = () => { }); } + if (isMobile) { + const objectMetadataItemForRecordPage = store.get( + objectMetadataItemFamilySelector.selectorFamily({ + objectName: objectNameSingular, + objectNameType: 'singular', + }), + ); + + const labelIdentifierField = isDefined(objectMetadataItemForRecordPage) + ? getLabelIdentifierFieldMetadataItem(objectMetadataItemForRecordPage) + : undefined; + + closeSidePanelMenu(); + + navigate( + AppPath.RecordShowPage, + { objectNameSingular, objectRecordId: recordId }, + undefined, + isNewRecord + ? { + state: { + isNewRecord: true, + objectRecordId: recordId, + labelIdentifierFieldName: labelIdentifierField?.name, + }, + } + : undefined, + ); + + return; + } + const navigationStack = store.get(sidePanelNavigationStackState.atom); const currentNavigationStackItem = navigationStack.at(-1); @@ -206,7 +244,10 @@ export const useOpenRecordInSidePanel = () => { } }, [ + closeSidePanelMenu, getIcon, + isMobile, + navigate, navigateSidePanelMenu, openNewRecordTitleCell, runWorkflowRunOpeningInSidePanelEffects,