From b9bdb12b3cac85f1e2852b8a7d361290e55608f0 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Tue, 28 Oct 2025 12:50:32 +0100 Subject: [PATCH] Hide page layout tab bar when there are less than two tabs to display (#15385) - DIsplay the tab bar only if more than 1 tabs can be displayed - Remove the selfDisplayMode feature: the first tab is now considered to be the pinned tab on record pages https://github.com/user-attachments/assets/3f61931d-fde4-4065-b646-5b3a7bdebcc0 --- .../components/PageLayoutRendererContent.tsx | 20 +- .../components/PageLayoutTabList.tsx | 10 +- .../components/PageLayoutTabListEffect.tsx | 32 +++ .../__stories__/PageLayoutTabList.stories.tsx | 6 + .../DefaultCompanyRecordPageLayout.ts | 1 - .../constants/DefaultNoteRecordPageLayout.ts | 1 - .../DefaultOpportunityRecordPageLayout.ts | 1 - .../DefaultPersonRecordPageLayout.ts | 1 - .../constants/DefaultRecordPageLayout.ts | 1 - .../constants/DefaultTaskRecordPageLayout.ts | 1 - .../page-layout/types/PageLayoutTab.ts | 1 - .../__tests__/getTabsByDisplayMode.test.ts | 201 +++++++----------- .../page-layout/utils/getTabsByDisplayMode.ts | 8 +- 13 files changed, 127 insertions(+), 157 deletions(-) create mode 100644 packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListEffect.tsx 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 8ef38145cc..cc9ea319cb 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx @@ -2,6 +2,7 @@ import { PageLayoutContent } from '@/page-layout/components/PageLayoutContent'; import { PageLayoutLeftPanel } from '@/page-layout/components/PageLayoutLeftPanel'; import { PageLayoutTabHeader } from '@/page-layout/components/PageLayoutTabHeader'; import { PageLayoutTabList } from '@/page-layout/components/PageLayoutTabList'; +import { PageLayoutTabListEffect } from '@/page-layout/components/PageLayoutTabListEffect'; import { useCreatePageLayoutTab } from '@/page-layout/hooks/useCreatePageLayoutTab'; import { useCurrentPageLayout } from '@/page-layout/hooks/useCurrentPageLayout'; import { useReorderPageLayoutTabs } from '@/page-layout/hooks/useReorderPageLayoutTabs'; @@ -35,7 +36,7 @@ const StyledShowPageRightContainer = styled.div` overflow: auto; `; -const StyledTabList = styled(PageLayoutTabList)` +const StyledPageLayoutTabList = styled(PageLayoutTabList)` padding-left: ${({ theme }) => theme.spacing(2)}; `; @@ -87,14 +88,21 @@ export const PageLayoutRendererContent = () => { - + {(sortedTabs.length > 1 || isPageLayoutInEditMode) && ( + + )} + tab.id === activeTabId); - const initialActiveTabId = activeTabExists ? activeTabId : visibleTabs[0]?.id; - const { visibleTabCount, hiddenTabs, @@ -118,11 +115,6 @@ export const PageLayoutTabList = ({ return hiddenTabs.some((tab) => tab.id === activeTabId); }, [hasHiddenTabs, hiddenTabs, activeTabId]); - useEffect(() => { - setActiveTabId(initialActiveTabId); - onChangeTab?.(initialActiveTabId || ''); - }, [initialActiveTabId, setActiveTabId, onChangeTab]); - const selectTab = useCallback( (tabId: string) => { setActiveTabId(tabId); diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListEffect.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListEffect.tsx new file mode 100644 index 0000000000..b256f693bc --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListEffect.tsx @@ -0,0 +1,32 @@ +import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { type TabListProps } from '@/ui/layout/tab-list/types/TabListProps'; +import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; +import { useEffect } from 'react'; + +type PageLayoutTabListEffectProps = Pick< + TabListProps, + 'componentInstanceId' | 'tabs' | 'onChangeTab' +>; + +export const PageLayoutTabListEffect = ({ + tabs, + onChangeTab, + componentInstanceId, +}: PageLayoutTabListEffectProps) => { + const [activeTabId, setActiveTabId] = useRecoilComponentState( + activeTabIdComponentState, + componentInstanceId, + ); + + const visibleTabs = tabs.filter((tab) => !tab.hide); + + const activeTabExists = visibleTabs.some((tab) => tab.id === activeTabId); + const initialActiveTabId = activeTabExists ? activeTabId : visibleTabs[0]?.id; + + useEffect(() => { + setActiveTabId(initialActiveTabId); + onChangeTab?.(initialActiveTabId || ''); + }, [initialActiveTabId, onChangeTab, setActiveTabId]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutTabList.stories.tsx b/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutTabList.stories.tsx index c5902b040e..acecf8fb75 100644 --- a/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutTabList.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/__stories__/PageLayoutTabList.stories.tsx @@ -9,6 +9,7 @@ import { ComponentWithRouterDecorator } from 'twenty-ui/testing'; import { calculateNewPosition } from '@/favorites/utils/calculateNewPosition'; import { PageLayoutTabList } from '@/page-layout/components/PageLayoutTabList'; import { PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS } from '@/page-layout/components/PageLayoutTabListDroppableIds'; +import { PageLayoutTabListEffect } from '@/page-layout/components/PageLayoutTabListEffect'; import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps'; @@ -117,6 +118,11 @@ const PageLayoutTabListPlayground = ({ return ( + + & { * Only available behind IS_RECORD_PAGE_LAYOUT_ENABLED for now. */ layoutMode?: 'grid' | 'vertical-list'; - selfDisplayMode?: 'pinned-left'; }; diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/getTabsByDisplayMode.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/getTabsByDisplayMode.test.ts index 6eb0516d9b..ff7a2de432 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/__tests__/getTabsByDisplayMode.test.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/getTabsByDisplayMode.test.ts @@ -4,10 +4,7 @@ import { PageLayoutType } from '~/generated/graphql'; import { getTabsByDisplayMode } from '../getTabsByDisplayMode'; describe('getTabsByDisplayMode', () => { - const createMockTab = ( - id: string, - selfDisplayMode?: 'pinned-left', - ): PageLayoutTab => ({ + const createMockTab = (id: string): PageLayoutTab => ({ id, pageLayoutId: 'page-layout-1', title: `Tab ${id}`, @@ -15,7 +12,6 @@ describe('getTabsByDisplayMode', () => { widgets: [], createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', - selfDisplayMode, }); const createMockPageLayout = (tabs: PageLayoutTab[]): DraftPageLayout => ({ @@ -31,7 +27,7 @@ describe('getTabsByDisplayMode', () => { const tabs = [ createMockTab('tab-1'), createMockTab('tab-2'), - createMockTab('tab-3', 'pinned-left'), + createMockTab('tab-3'), ]; const pageLayout = createMockPageLayout(tabs); @@ -46,10 +42,7 @@ describe('getTabsByDisplayMode', () => { }); it('should return undefined for pinnedLeftTab', () => { - const tabs = [ - createMockTab('tab-1'), - createMockTab('tab-2', 'pinned-left'), - ]; + const tabs = [createMockTab('tab-1'), createMockTab('tab-2')]; const pageLayout = createMockPageLayout(tabs); const result = getTabsByDisplayMode({ @@ -74,11 +67,8 @@ describe('getTabsByDisplayMode', () => { expect(result.pinnedLeftTab).toBeUndefined(); }); - it('should return all tabs even when all are pinned-left', () => { - const tabs = [ - createMockTab('tab-1', 'pinned-left'), - createMockTab('tab-2', 'pinned-left'), - ]; + it('should return all tabs including the first one', () => { + const tabs = [createMockTab('tab-1'), createMockTab('tab-2')]; const pageLayout = createMockPageLayout(tabs); const result = getTabsByDisplayMode({ @@ -97,7 +87,7 @@ describe('getTabsByDisplayMode', () => { const tabs = [ createMockTab('tab-1'), createMockTab('tab-2'), - createMockTab('tab-3', 'pinned-left'), + createMockTab('tab-3'), ]; const pageLayout = createMockPageLayout(tabs); @@ -112,10 +102,7 @@ describe('getTabsByDisplayMode', () => { }); it('should return undefined for pinnedLeftTab', () => { - const tabs = [ - createMockTab('tab-1'), - createMockTab('tab-2', 'pinned-left'), - ]; + const tabs = [createMockTab('tab-1'), createMockTab('tab-2')]; const pageLayout = createMockPageLayout(tabs); const result = getTabsByDisplayMode({ @@ -140,11 +127,8 @@ describe('getTabsByDisplayMode', () => { expect(result.pinnedLeftTab).toBeUndefined(); }); - it('should return all tabs even when all are pinned-left', () => { - const tabs = [ - createMockTab('tab-1', 'pinned-left'), - createMockTab('tab-2', 'pinned-left'), - ]; + it('should return all tabs including the first one', () => { + const tabs = [createMockTab('tab-1'), createMockTab('tab-2')]; const pageLayout = createMockPageLayout(tabs); const result = getTabsByDisplayMode({ @@ -159,63 +143,7 @@ describe('getTabsByDisplayMode', () => { }); describe('when isMobile is false and isInRightDrawer is false', () => { - it('should filter out pinned-left tabs from tabsToRenderInTabList', () => { - const tabs = [ - createMockTab('tab-1'), - createMockTab('tab-2', 'pinned-left'), - createMockTab('tab-3'), - ]; - const pageLayout = createMockPageLayout(tabs); - - const result = getTabsByDisplayMode({ - pageLayout, - isMobile: false, - isInRightDrawer: false, - }); - - expect(result.tabsToRenderInTabList).toHaveLength(2); - expect(result.tabsToRenderInTabList).toEqual([tabs[0], tabs[2]]); - expect( - result.tabsToRenderInTabList.every( - (tab) => tab.selfDisplayMode !== 'pinned-left', - ), - ).toBe(true); - }); - - it('should return the pinned-left tab in pinnedLeftTab', () => { - const tabs = [ - createMockTab('tab-1'), - createMockTab('tab-2', 'pinned-left'), - createMockTab('tab-3'), - ]; - const pageLayout = createMockPageLayout(tabs); - - const result = getTabsByDisplayMode({ - pageLayout, - isMobile: false, - isInRightDrawer: false, - }); - - expect(result.pinnedLeftTab).toBeDefined(); - expect(result.pinnedLeftTab?.id).toBe('tab-2'); - expect(result.pinnedLeftTab?.selfDisplayMode).toBe('pinned-left'); - }); - - it('should return undefined for pinnedLeftTab when no pinned tab exists', () => { - const tabs = [createMockTab('tab-1'), createMockTab('tab-2')]; - const pageLayout = createMockPageLayout(tabs); - - const result = getTabsByDisplayMode({ - pageLayout, - isMobile: false, - isInRightDrawer: false, - }); - - expect(result.pinnedLeftTab).toBeUndefined(); - expect(result.tabsToRenderInTabList).toEqual(tabs); - }); - - it('should return all tabs in tabsToRenderInTabList when no pinned tabs exist', () => { + it('should return first tab as pinnedLeftTab and rest in tabsToRenderInTabList', () => { const tabs = [ createMockTab('tab-1'), createMockTab('tab-2'), @@ -229,8 +157,56 @@ describe('getTabsByDisplayMode', () => { isInRightDrawer: false, }); - expect(result.tabsToRenderInTabList).toEqual(tabs); - expect(result.tabsToRenderInTabList).toHaveLength(3); + expect(result.pinnedLeftTab).toBeDefined(); + expect(result.pinnedLeftTab?.id).toBe('tab-1'); + expect(result.tabsToRenderInTabList).toHaveLength(2); + expect(result.tabsToRenderInTabList).toEqual([tabs[1], tabs[2]]); + }); + + it('should return first tab as pinnedLeftTab', () => { + const tabs = [ + createMockTab('tab-1'), + createMockTab('tab-2'), + createMockTab('tab-3'), + ]; + const pageLayout = createMockPageLayout(tabs); + + const result = getTabsByDisplayMode({ + pageLayout, + isMobile: false, + isInRightDrawer: false, + }); + + expect(result.pinnedLeftTab).toBeDefined(); + expect(result.pinnedLeftTab?.id).toBe('tab-1'); + }); + + it('should return undefined for pinnedLeftTab when no tabs exist', () => { + const pageLayout = createMockPageLayout([]); + + const result = getTabsByDisplayMode({ + pageLayout, + isMobile: false, + isInRightDrawer: false, + }); + + expect(result.pinnedLeftTab).toBeUndefined(); + expect(result.tabsToRenderInTabList).toEqual([]); + }); + + it('should handle single tab', () => { + const tabs = [createMockTab('tab-1')]; + const pageLayout = createMockPageLayout(tabs); + + const result = getTabsByDisplayMode({ + pageLayout, + isMobile: false, + isInRightDrawer: false, + }); + + expect(result.pinnedLeftTab).toBeDefined(); + expect(result.pinnedLeftTab?.id).toBe('tab-1'); + expect(result.tabsToRenderInTabList).toEqual([]); }); it('should handle empty tabs array', () => { @@ -246,12 +222,8 @@ describe('getTabsByDisplayMode', () => { expect(result.pinnedLeftTab).toBeUndefined(); }); - it('should return first pinned-left tab when multiple exist', () => { - const tabs = [ - createMockTab('tab-1'), - createMockTab('tab-2', 'pinned-left'), - createMockTab('tab-3', 'pinned-left'), - ]; + it('should handle two tabs', () => { + const tabs = [createMockTab('tab-1'), createMockTab('tab-2')]; const pageLayout = createMockPageLayout(tabs); const result = getTabsByDisplayMode({ @@ -260,26 +232,9 @@ describe('getTabsByDisplayMode', () => { isInRightDrawer: false, }); - expect(result.pinnedLeftTab?.id).toBe('tab-2'); - expect(result.tabsToRenderInTabList).toHaveLength(1); - expect(result.tabsToRenderInTabList[0].id).toBe('tab-1'); - }); - - it('should return empty array when all tabs are pinned-left', () => { - const tabs = [ - createMockTab('tab-1', 'pinned-left'), - createMockTab('tab-2', 'pinned-left'), - ]; - const pageLayout = createMockPageLayout(tabs); - - const result = getTabsByDisplayMode({ - pageLayout, - isMobile: false, - isInRightDrawer: false, - }); - - expect(result.tabsToRenderInTabList).toEqual([]); expect(result.pinnedLeftTab?.id).toBe('tab-1'); + expect(result.tabsToRenderInTabList).toHaveLength(1); + expect(result.tabsToRenderInTabList[0].id).toBe('tab-2'); }); }); @@ -302,12 +257,12 @@ describe('getTabsByDisplayMode', () => { expect(resultMobile.tabsToRenderInTabList).toEqual(tabs); expect(resultMobile.pinnedLeftTab).toBeUndefined(); - expect(resultDesktop.tabsToRenderInTabList).toEqual(tabs); - expect(resultDesktop.pinnedLeftTab).toBeUndefined(); + expect(resultDesktop.tabsToRenderInTabList).toEqual([]); + expect(resultDesktop.pinnedLeftTab).toEqual(tabs[0]); }); it('should handle single tab with pinned-left display mode', () => { - const tabs = [createMockTab('tab-1', 'pinned-left')]; + const tabs = [createMockTab('tab-1')]; const pageLayout = createMockPageLayout(tabs); const resultMobile = getTabsByDisplayMode({ @@ -329,10 +284,7 @@ describe('getTabsByDisplayMode', () => { }); it('should not mutate the original page layout', () => { - const tabs = [ - createMockTab('tab-1'), - createMockTab('tab-2', 'pinned-left'), - ]; + const tabs = [createMockTab('tab-1'), createMockTab('tab-2')]; const pageLayout = createMockPageLayout(tabs); const originalTabsLength = pageLayout.tabs.length; @@ -359,16 +311,13 @@ describe('getTabsByDisplayMode', () => { isInRightDrawer: false, }); - expect(result.tabsToRenderInTabList[0].layoutMode).toBe('grid'); + expect(result.pinnedLeftTab?.layoutMode).toBe('grid'); }); }); describe('consistency between mobile and desktop', () => { it('should return consistent results for the same input', () => { - const tabs = [ - createMockTab('tab-1'), - createMockTab('tab-2', 'pinned-left'), - ]; + const tabs = [createMockTab('tab-1'), createMockTab('tab-2')]; const pageLayout = createMockPageLayout(tabs); const result1 = getTabsByDisplayMode({ @@ -386,10 +335,7 @@ describe('getTabsByDisplayMode', () => { }); it('should show different results for mobile vs desktop', () => { - const tabs = [ - createMockTab('tab-1'), - createMockTab('tab-2', 'pinned-left'), - ]; + const tabs = [createMockTab('tab-1'), createMockTab('tab-2')]; const pageLayout = createMockPageLayout(tabs); const mobileResult = getTabsByDisplayMode({ @@ -413,10 +359,7 @@ describe('getTabsByDisplayMode', () => { describe('when both isMobile and isInRightDrawer are true', () => { it('should behave the same as when only one is true', () => { - const tabs = [ - createMockTab('tab-1'), - createMockTab('tab-2', 'pinned-left'), - ]; + const tabs = [createMockTab('tab-1'), createMockTab('tab-2')]; const pageLayout = createMockPageLayout(tabs); const resultBothTrue = getTabsByDisplayMode({ diff --git a/packages/twenty-front/src/modules/page-layout/utils/getTabsByDisplayMode.ts b/packages/twenty-front/src/modules/page-layout/utils/getTabsByDisplayMode.ts index f10f4b98f8..4587365a4a 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/getTabsByDisplayMode.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/getTabsByDisplayMode.ts @@ -24,12 +24,8 @@ export const getTabsByDisplayMode = ({ }; } - const tabsToRenderInTabList = pageLayout.tabs.filter( - (tab) => tab.selfDisplayMode !== 'pinned-left', - ); - const pinnedLeftTab = pageLayout.tabs.find( - (tab) => tab.selfDisplayMode === 'pinned-left', - ); + const tabsToRenderInTabList = pageLayout.tabs.slice(1); + const pinnedLeftTab = pageLayout.tabs[0]; return { tabsToRenderInTabList,