From 51c0a8dd863dc3648f5ed74fd7edc16a618cd717 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Mon, 8 Dec 2025 10:47:46 +0100 Subject: [PATCH] [Page layouts] Sync tabs with URL hash (#16341) There are three identified scenarios. ## First scenario The user uses a desktop. They click on a company, which is opened in the side panel. They click on a tab, like "Tasks". The user chooses to open the record in fullscreen. The "Tasks" tab is opened by default in the fullscreen record page. If the user refreshes the page, the default tab is shown: "Timeline". **This was the behavior on the show pages, and I kept it.** If we wanted to show the "Tasks" tab here, we would have to put the id of the "Tasks" tab in the URL hash when we open the record in fullscreen and an active tab id is already set. https://github.com/user-attachments/assets/205776ac-9ad7-4e79-af9a-6b44360a5d77 ## Second scenario The user uses a desktop or a mobile device and interacts with a company record. They click on a tab. If they refresh, the selected tab will be displayed by default. ### Desktop https://github.com/user-attachments/assets/d4e1908b-a591-42f6-bb82-5aec592e2778 ### Mobile https://github.com/user-attachments/assets/7bab73cc-a2ff-4ce8-9bc6-325efef2d500 ## Third scenario The user uses a desktop. They click on a dashboard, which is opened in fullscreen mode by default. They select tabs. If they refresh, the last selected tab is opened by default. When the user turns edit mode on, **the url hash is removed**. The last selected tab remains selected. If the user selects another tab, the url hash isn't set, but the newly selected tab is displayed correctly. If the user saves their changes, the selected tab remains selected. However, if they refresh the page, the default tab replaces the previously selected tab. Not setting the url hash when editing a page layout simplifies the code. I'm okay with this tradeoff as the feature is primarily meant to let users share specific tabs of their records. **This behavior will also be used for record page layout once edit mode is supported.** https://github.com/user-attachments/assets/869657a1-6895-4ade-816c-972c77aaab9e Closes https://github.com/twentyhq/core-team-issues/issues/1784 --- .../components/EditDashboardSingleRecordAction.tsx | 4 ++++ .../components/PageLayoutRendererContent.tsx | 4 ++-- .../page-layout/components/PageLayoutTabList.tsx | 5 +++-- packages/twenty-ui/src/utilities/index.ts | 1 + .../navigation/hooks/useResetLocationHash.ts | 13 +++++++++++++ 5 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 packages/twenty-ui/src/utilities/navigation/hooks/useResetLocationHash.ts diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/EditDashboardSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/EditDashboardSingleRecordAction.tsx index bcdb9b90ac..dd9676dede 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/EditDashboardSingleRecordAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/dashboard-actions/components/EditDashboardSingleRecordAction.tsx @@ -3,6 +3,7 @@ import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode'; import { useRecoilValue } from 'recoil'; +import { useResetLocationHash } from 'twenty-ui/utilities'; export const EditDashboardSingleRecordAction = () => { const recordId = useSelectedRecordIdOrThrow(); @@ -14,8 +15,11 @@ export const EditDashboardSingleRecordAction = () => { const { setIsPageLayoutInEditMode } = useSetIsPageLayoutInEditMode(pageLayoutId); + const { resetLocationHash } = useResetLocationHash(); + const handleClick = () => { setIsPageLayoutInEditMode(true); + resetLocationHash(); }; return ; 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 5e56df43c3..40d3f7d79d 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx @@ -18,12 +18,12 @@ import { getTabsWithVisibleWidgets } from '@/page-layout/utils/getTabsWithVisibl import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition'; import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; -import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import styled from '@emotion/styled'; import { isDefined } from 'twenty-shared/utils'; +import { useIsMobile } from 'twenty-ui/utilities'; const StyledContainer = styled.div<{ hasPinnedTab: boolean }>` display: grid; @@ -120,7 +120,7 @@ export const PageLayoutRendererContent = () => { {(sortedTabs.length > 1 || isPageLayoutInEditMode) && ( & { isReorderEnabled: boolean; onAddTab?: () => void; onReorder?: (result: DropResult, provided: ResponderProvided) => boolean; + behaveAsLinks: boolean; }; export const PageLayoutTabList = ({ tabs, loading, - behaveAsLinks = true, + behaveAsLinks, isInRightDrawer, className, componentInstanceId, diff --git a/packages/twenty-ui/src/utilities/index.ts b/packages/twenty-ui/src/utilities/index.ts index 9513838988..832ad50195 100644 --- a/packages/twenty-ui/src/utilities/index.ts +++ b/packages/twenty-ui/src/utilities/index.ts @@ -26,6 +26,7 @@ export { getOsShortcutSeparator } from './device/getOsShortcutSeparator'; export { getUserDevice } from './device/getUserDevice'; export { AutogrowWrapper } from './dimensions/components/AutogrowWrapper'; export { useMouseDownNavigation } from './navigation/hooks/useMouseDownNavigation'; +export { useResetLocationHash } from './navigation/hooks/useResetLocationHash'; export { isNavigationModifierPressed } from './navigation/isNavigationModifierPressed'; export type { TriggerEventType } from './navigation/types/trigger-event.type'; export { useIsMobile } from './responsive/hooks/useIsMobile'; diff --git a/packages/twenty-ui/src/utilities/navigation/hooks/useResetLocationHash.ts b/packages/twenty-ui/src/utilities/navigation/hooks/useResetLocationHash.ts new file mode 100644 index 0000000000..c6173c1eff --- /dev/null +++ b/packages/twenty-ui/src/utilities/navigation/hooks/useResetLocationHash.ts @@ -0,0 +1,13 @@ +import { useLocation, useNavigate } from 'react-router-dom'; + +export const useResetLocationHash = () => { + const navigate = useNavigate(); + const location = useLocation(); + + // eslint-disable-next-line @nx/workspace-no-navigate-prefer-link + const resetLocationHash = () => { + navigate(location.pathname, { replace: true }); + }; + + return { resetLocationHash }; +};