[Page Layouts] Focus specific tabs on mobile and side panel (#15984)
## On mobile https://github.com/user-attachments/assets/4a8017b2-56a9-4759-bc18-8d99fed9f80a ## In side panel https://github.com/user-attachments/assets/f36f0827-699a-4f76-8e84-a8ceaeb71396 ## Dashboards Untouched, but keep working. https://github.com/user-attachments/assets/fe583f80-ea8d-453e-95ea-6b99175d1899 ## Other Record Page Layouts Untouched, but keep working. https://github.com/user-attachments/assets/c64f731f-5d28-4585-8cb5-c3940fb1ab6f
This commit is contained in:
committed by
GitHub
parent
0a2d42e79f
commit
3e187b4ce3
@@ -116,6 +116,9 @@ export const PageLayoutRendererContent = () => {
|
||||
<PageLayoutTabListEffect
|
||||
tabs={sortedTabs}
|
||||
componentInstanceId={tabListInstanceId}
|
||||
defaultTabIdToFocusOnMobileAndSidePanel={
|
||||
currentPageLayout.defaultTabIdToFocusOnMobileAndSidePanel
|
||||
}
|
||||
/>
|
||||
{(sortedTabs.length > 1 || isPageLayoutInEditMode) && (
|
||||
<StyledPageLayoutTabList
|
||||
|
||||
+15
-2
@@ -1,6 +1,9 @@
|
||||
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
|
||||
import { getPageLayoutTabListInitialActiveTabId } from '@/page-layout/utils/getPageLayoutTabListInitialActiveTabId';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { type TabListProps } from '@/ui/layout/tab-list/types/TabListProps';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
@@ -9,20 +12,30 @@ type PageLayoutTabListEffectProps = Pick<
|
||||
'componentInstanceId' | 'onChangeTab'
|
||||
> & {
|
||||
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);
|
||||
|
||||
+1
@@ -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)
|
||||
{
|
||||
|
||||
+1
@@ -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)
|
||||
{
|
||||
|
||||
@@ -3,4 +3,5 @@ import { type PageLayout as PageLayoutGenerated } from '~/generated/graphql';
|
||||
|
||||
export type PageLayout = Omit<PageLayoutGenerated, 'tabs'> & {
|
||||
tabs: PageLayoutTab[];
|
||||
defaultTabIdToFocusOnMobileAndSidePanel?: string;
|
||||
};
|
||||
|
||||
+226
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
+41
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user