diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx index 25b7538167..cd632779f4 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx @@ -116,6 +116,9 @@ export const PageLayoutRendererContent = () => { {(sortedTabs.length > 1 || isPageLayoutInEditMode) && ( & { tabs: PageLayoutTab[]; + defaultTabIdToFocusOnMobileAndSidePanel?: string; }; export const PageLayoutTabListEffect = ({ tabs, onChangeTab, componentInstanceId, + defaultTabIdToFocusOnMobileAndSidePanel, }: PageLayoutTabListEffectProps) => { const [activeTabId, setActiveTabId] = useRecoilComponentState( activeTabIdComponentState, componentInstanceId, ); - const activeTabExists = tabs.some((tab) => tab.id === activeTabId); - const initialActiveTabId = activeTabExists ? activeTabId : tabs[0]?.id; + const isMobile = useIsMobile(); + const { isInRightDrawer } = useLayoutRenderingContext(); + + const initialActiveTabId = getPageLayoutTabListInitialActiveTabId({ + activeTabId, + tabs, + defaultTabIdToFocusOnMobileAndSidePanel, + isMobile, + isInRightDrawer, + }); useEffect(() => { setActiveTabId(initialActiveTabId); diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowRunPageLayout.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowRunPageLayout.ts index c882e6c11c..d19be39f4f 100644 --- a/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowRunPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowRunPageLayout.ts @@ -15,6 +15,7 @@ export const DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT: PageLayout = { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), deletedAt: null, + defaultTabIdToFocusOnMobileAndSidePanel: 'workflow-run-tab-flow', tabs: [ // Fields tab (position 100) { diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowVersionPageLayout.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowVersionPageLayout.ts index d65c28bd51..b2538e98b9 100644 --- a/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowVersionPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultWorkflowVersionPageLayout.ts @@ -15,6 +15,7 @@ export const DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT: PageLayout = { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), deletedAt: null, + defaultTabIdToFocusOnMobileAndSidePanel: 'workflow-version-tab-flow', tabs: [ // Fields tab (position 100) { diff --git a/packages/twenty-front/src/modules/page-layout/types/PageLayout.ts b/packages/twenty-front/src/modules/page-layout/types/PageLayout.ts index 017aacf957..a06ca3d6a5 100644 --- a/packages/twenty-front/src/modules/page-layout/types/PageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/types/PageLayout.ts @@ -3,4 +3,5 @@ import { type PageLayout as PageLayoutGenerated } from '~/generated/graphql'; export type PageLayout = Omit & { tabs: PageLayoutTab[]; + defaultTabIdToFocusOnMobileAndSidePanel?: string; }; diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/getPageLayoutTabListInitialActiveTabId.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/getPageLayoutTabListInitialActiveTabId.test.ts new file mode 100644 index 0000000000..457ce48477 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/getPageLayoutTabListInitialActiveTabId.test.ts @@ -0,0 +1,226 @@ +import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab'; +import { getPageLayoutTabListInitialActiveTabId } from '../getPageLayoutTabListInitialActiveTabId'; + +describe('getPageLayoutTabListInitialActiveTabId', () => { + const createMockTab = (id: string): PageLayoutTab => ({ + id, + pageLayoutId: 'page-layout-1', + title: `Tab ${id}`, + position: 0, + widgets: [], + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + }); + + const mockTabs: PageLayoutTab[] = [ + createMockTab('tab-1'), + createMockTab('tab-2'), + createMockTab('tab-3'), + ]; + + describe('when activeTabId exists in tabs', () => { + it('should return activeTabId regardless of context', () => { + const result = getPageLayoutTabListInitialActiveTabId({ + activeTabId: 'tab-2', + tabs: mockTabs, + defaultTabIdToFocusOnMobileAndSidePanel: 'tab-3', + isMobile: true, + isInRightDrawer: true, + }); + + expect(result).toBe('tab-2'); + }); + + it('should return activeTabId even when not on mobile or in drawer', () => { + const result = getPageLayoutTabListInitialActiveTabId({ + activeTabId: 'tab-1', + tabs: mockTabs, + defaultTabIdToFocusOnMobileAndSidePanel: 'tab-3', + isMobile: false, + isInRightDrawer: false, + }); + + expect(result).toBe('tab-1'); + }); + }); + + describe('when activeTabId does not exist in tabs', () => { + describe('on mobile or in right drawer', () => { + it('should return defaultTabIdToFocusOnMobileAndSidePanel when on mobile and default exists', () => { + const result = getPageLayoutTabListInitialActiveTabId({ + activeTabId: 'non-existent-tab', + tabs: mockTabs, + defaultTabIdToFocusOnMobileAndSidePanel: 'tab-3', + isMobile: true, + isInRightDrawer: false, + }); + + expect(result).toBe('tab-3'); + }); + + it('should return defaultTabIdToFocusOnMobileAndSidePanel when in right drawer and default exists', () => { + const result = getPageLayoutTabListInitialActiveTabId({ + activeTabId: null, + tabs: mockTabs, + defaultTabIdToFocusOnMobileAndSidePanel: 'tab-2', + isMobile: false, + isInRightDrawer: true, + }); + + expect(result).toBe('tab-2'); + }); + + it('should return defaultTabIdToFocusOnMobileAndSidePanel when on mobile and in right drawer', () => { + const result = getPageLayoutTabListInitialActiveTabId({ + activeTabId: null, + tabs: mockTabs, + defaultTabIdToFocusOnMobileAndSidePanel: 'tab-1', + isMobile: true, + isInRightDrawer: true, + }); + + expect(result).toBe('tab-1'); + }); + + it('should fallback to first tab when defaultTabIdToFocusOnMobileAndSidePanel does not exist', () => { + const result = getPageLayoutTabListInitialActiveTabId({ + activeTabId: null, + tabs: mockTabs, + defaultTabIdToFocusOnMobileAndSidePanel: 'non-existent-tab', + isMobile: true, + isInRightDrawer: false, + }); + + expect(result).toBe('tab-1'); + }); + + it('should fallback to first tab when defaultTabIdToFocusOnMobileAndSidePanel is undefined', () => { + const result = getPageLayoutTabListInitialActiveTabId({ + activeTabId: null, + tabs: mockTabs, + defaultTabIdToFocusOnMobileAndSidePanel: undefined, + isMobile: true, + isInRightDrawer: false, + }); + + expect(result).toBe('tab-1'); + }); + }); + + describe('not on mobile and not in right drawer', () => { + it('should return first tab when default is provided', () => { + const result = getPageLayoutTabListInitialActiveTabId({ + activeTabId: null, + tabs: mockTabs, + defaultTabIdToFocusOnMobileAndSidePanel: 'tab-3', + isMobile: false, + isInRightDrawer: false, + }); + + expect(result).toBe('tab-1'); + }); + + it('should return first tab when default is not provided', () => { + const result = getPageLayoutTabListInitialActiveTabId({ + activeTabId: null, + tabs: mockTabs, + defaultTabIdToFocusOnMobileAndSidePanel: undefined, + isMobile: false, + isInRightDrawer: false, + }); + + expect(result).toBe('tab-1'); + }); + }); + }); + + describe('edge cases', () => { + it('should return null when tabs array is empty', () => { + const result = getPageLayoutTabListInitialActiveTabId({ + activeTabId: null, + tabs: [], + defaultTabIdToFocusOnMobileAndSidePanel: 'tab-1', + isMobile: false, + isInRightDrawer: false, + }); + + expect(result).toBe(null); + }); + + it('should return null when tabs array is empty even with default on mobile', () => { + const result = getPageLayoutTabListInitialActiveTabId({ + activeTabId: null, + tabs: [], + defaultTabIdToFocusOnMobileAndSidePanel: 'tab-1', + isMobile: true, + isInRightDrawer: false, + }); + + expect(result).toBe(null); + }); + + it('should handle activeTabId being null', () => { + const result = getPageLayoutTabListInitialActiveTabId({ + activeTabId: null, + tabs: mockTabs, + defaultTabIdToFocusOnMobileAndSidePanel: undefined, + isMobile: false, + isInRightDrawer: false, + }); + + expect(result).toBe('tab-1'); + }); + + it('should handle single tab array', () => { + const singleTab = [createMockTab('only-tab')]; + + const result = getPageLayoutTabListInitialActiveTabId({ + activeTabId: null, + tabs: singleTab, + defaultTabIdToFocusOnMobileAndSidePanel: undefined, + isMobile: false, + isInRightDrawer: false, + }); + + expect(result).toBe('only-tab'); + }); + }); + + describe('priority order', () => { + it('should prioritize activeTabId over default and context', () => { + const result = getPageLayoutTabListInitialActiveTabId({ + activeTabId: 'tab-1', + tabs: mockTabs, + defaultTabIdToFocusOnMobileAndSidePanel: 'tab-2', + isMobile: true, + isInRightDrawer: true, + }); + + expect(result).toBe('tab-1'); + }); + + it('should prioritize valid default over first tab when on mobile', () => { + const result = getPageLayoutTabListInitialActiveTabId({ + activeTabId: 'non-existent', + tabs: mockTabs, + defaultTabIdToFocusOnMobileAndSidePanel: 'tab-3', + isMobile: true, + isInRightDrawer: false, + }); + + expect(result).toBe('tab-3'); + }); + + it('should fallback to first tab when default is invalid on mobile', () => { + const result = getPageLayoutTabListInitialActiveTabId({ + activeTabId: 'non-existent', + tabs: mockTabs, + defaultTabIdToFocusOnMobileAndSidePanel: 'invalid-tab', + isMobile: true, + isInRightDrawer: false, + }); + + expect(result).toBe('tab-1'); + }); + }); +}); diff --git a/packages/twenty-front/src/modules/page-layout/utils/getPageLayoutTabListInitialActiveTabId.ts b/packages/twenty-front/src/modules/page-layout/utils/getPageLayoutTabListInitialActiveTabId.ts new file mode 100644 index 0000000000..e4b580fc92 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/getPageLayoutTabListInitialActiveTabId.ts @@ -0,0 +1,41 @@ +import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab'; +import { isDefined } from 'twenty-shared/utils'; + +type GetPageLayoutTabListInitialActiveTabIdParams = { + activeTabId: string | null; + tabs: PageLayoutTab[]; + defaultTabIdToFocusOnMobileAndSidePanel?: string; + isMobile: boolean; + isInRightDrawer: boolean; +}; + +export const getPageLayoutTabListInitialActiveTabId = ({ + activeTabId, + tabs, + defaultTabIdToFocusOnMobileAndSidePanel, + isMobile, + isInRightDrawer, +}: GetPageLayoutTabListInitialActiveTabIdParams): string | null => { + const activeTabExists = tabs.some((tab) => tab.id === activeTabId); + + if (activeTabExists) { + return activeTabId; + } + + const isOnMobileOrSidePanel = isMobile || isInRightDrawer; + + if ( + isOnMobileOrSidePanel && + isDefined(defaultTabIdToFocusOnMobileAndSidePanel) + ) { + const defaultTabExists = tabs.some( + (tab) => tab.id === defaultTabIdToFocusOnMobileAndSidePanel, + ); + + if (defaultTabExists) { + return defaultTabIdToFocusOnMobileAndSidePanel; + } + } + + return tabs[0]?.id ?? null; +};