[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
This commit is contained in:
Baptiste Devessier
2025-12-08 10:47:46 +01:00
committed by GitHub
parent 7f1e69740a
commit 51c0a8dd86
5 changed files with 23 additions and 4 deletions
@@ -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 <Action onClick={handleClick} />;
@@ -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) && (
<StyledPageLayoutTabList
tabs={sortedTabs}
behaveAsLinks={false}
behaveAsLinks={!isInRightDrawer && !isPageLayoutInEditMode}
componentInstanceId={tabListInstanceId}
onAddTab={handleAddTab}
isReorderEnabled={isPageLayoutInEditMode}
@@ -15,7 +15,6 @@ import { IconButton } from 'twenty-ui/input';
import { isPageLayoutTabDraggingComponentState } from '@/page-layout/states/isPageLayoutTabDraggingComponentState';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab-list/components/TabListFromUrlOptionalEffect';
import { TabListHiddenMeasurements } from '@/ui/layout/tab-list/components/TabListHiddenMeasurements';
import { TAB_LIST_GAP } from '@/ui/layout/tab-list/constants/TabListGap';
import { useTabListMeasurements } from '@/ui/layout/tab-list/hooks/useTabListMeasurements';
@@ -38,6 +37,7 @@ import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPag
import { pageLayoutTabListCurrentDragDroppableIdComponentState } from '@/page-layout/states/pageLayoutTabListCurrentDragDroppableIdComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab-list/components/TabListFromUrlOptionalEffect';
import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
@@ -74,12 +74,13 @@ type PageLayoutTabListProps = Omit<TabListProps, 'tabs'> & {
isReorderEnabled: boolean;
onAddTab?: () => void;
onReorder?: (result: DropResult, provided: ResponderProvided) => boolean;
behaveAsLinks: boolean;
};
export const PageLayoutTabList = ({
tabs,
loading,
behaveAsLinks = true,
behaveAsLinks,
isInRightDrawer,
className,
componentInstanceId,
@@ -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';
@@ -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 };
};