fix(page-layout): respect tab layoutMode on standalone pages (#20856)

Standalone-page tabs with `layoutMode: CANVAS` were silently rendering
as GRID (border, padding, scroll). Now they render full-bleed, matching
the CANVAS contract elsewhere.

Three layered fixes:
- `getTabLayoutMode`: respect `tab.layoutMode` for `STANDALONE_PAGE`
(was hardcoded to GRID for any non-`RECORD_PAGE`)
- `getWidgetCardVariant`: CANVAS now wins regardless of page type —
refactored to early-return + exhaustive switch on `pageLayoutType`
- `FrontComponentWidgetRenderer`: removed hardcoded `overflow: auto`
(workflow/tasks/timeline widgets don't have it either)

New `getTabLayoutMode.test.ts`. Variant tests refactored to declarative
+ parameterized.

QA:

<img width="3024" height="1654" alt="CleanShot 2026-05-22 at 20 46
50@2x"
src="https://github.com/user-attachments/assets/cc61d459-6bc6-48de-ac79-d63a2ccd8957"
/>


https://github.com/user-attachments/assets/a3374e18-ad1b-4888-ab2b-d07730edccac
This commit is contained in:
nitin
2026-05-25 17:29:31 +05:30
committed by GitHub
parent b8b115f4e3
commit 0b38b4ffc4
5 changed files with 142 additions and 118 deletions
@@ -0,0 +1,46 @@
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
import { getTabLayoutMode } from '@/page-layout/utils/getTabLayoutMode';
import {
PageLayoutTabLayoutMode,
PageLayoutType,
} from '~/generated-metadata/graphql';
const buildTab = (layoutMode: PageLayoutTabLayoutMode) =>
({ layoutMode }) as unknown as PageLayoutTab;
describe('getTabLayoutMode', () => {
describe('page types that respect tab.layoutMode', () => {
it.each([PageLayoutType.RECORD_PAGE, PageLayoutType.STANDALONE_PAGE])(
"returns the tab's layoutMode for %s",
(pageLayoutType) => {
const tab = buildTab(PageLayoutTabLayoutMode.CANVAS);
expect(getTabLayoutMode({ tab, pageLayoutType })).toBe(
PageLayoutTabLayoutMode.CANVAS,
);
},
);
it.each([PageLayoutType.RECORD_PAGE, PageLayoutType.STANDALONE_PAGE])(
'throws when tab is undefined for %s',
(pageLayoutType) => {
expect(() =>
getTabLayoutMode({ tab: undefined, pageLayoutType }),
).toThrow('Tab layout mode is not defined');
},
);
});
describe('page types that ignore tab.layoutMode and default to GRID', () => {
it.each([PageLayoutType.DASHBOARD, PageLayoutType.RECORD_INDEX])(
'returns GRID for %s regardless of tab.layoutMode',
(pageLayoutType) => {
const tab = buildTab(PageLayoutTabLayoutMode.CANVAS);
expect(getTabLayoutMode({ tab, pageLayoutType })).toBe(
PageLayoutTabLayoutMode.GRID,
);
},
);
});
});
@@ -14,7 +14,10 @@ export const getTabLayoutMode = ({
tab,
pageLayoutType,
}: GetTabLayoutModeParams): PageLayoutTabLayoutMode => {
if (pageLayoutType === PageLayoutType.RECORD_PAGE) {
if (
pageLayoutType === PageLayoutType.RECORD_PAGE ||
pageLayoutType === PageLayoutType.STANDALONE_PAGE
) {
assertPageLayoutTabHasDefinedLayoutModeOrThrow(tab);
return tab.layoutMode;
@@ -3,15 +3,20 @@ import { Suspense, lazy } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext';
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { PageLayoutWidgetNoDataDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay';
import { isWidgetConfigurationOfType } from '@/side-panel/pages/page-layout/utils/isWidgetConfigurationOfType';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { PageLayoutTabLayoutMode } from '~/generated-metadata/graphql';
const StyledContainer = styled.div<{ isInEditMode: boolean }>`
const StyledContainer = styled.div<{
isCanvasLayout: boolean;
isInEditMode: boolean;
}>`
height: 100%;
overflow: auto;
overflow: ${({ isCanvasLayout }) => (isCanvasLayout ? 'visible' : 'auto')};
pointer-events: ${({ isInEditMode }) => (isInEditMode ? 'none' : 'auto')};
width: 100%;
`;
@@ -30,6 +35,7 @@ export const FrontComponentWidgetRenderer = ({
widget,
}: FrontComponentWidgetRendererProps) => {
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
const { layoutMode } = usePageLayoutContentContext();
const { targetRecordIdentifier } = useLayoutRenderingContext();
const configuration = widget.configuration;
@@ -47,7 +53,10 @@ export const FrontComponentWidgetRenderer = ({
: undefined;
return (
<StyledContainer isInEditMode={isPageLayoutInEditMode}>
<StyledContainer
isCanvasLayout={layoutMode === PageLayoutTabLayoutMode.CANVAS}
isInEditMode={isPageLayoutInEditMode}
>
<Suspense fallback={null}>
<FrontComponentRenderer
frontComponentId={frontComponentId}
@@ -4,112 +4,79 @@ import {
PageLayoutType,
} from '~/generated-metadata/graphql';
const baseParams = {
isInPinnedTab: false,
isMobile: false,
isInSidePanel: false,
};
describe('getWidgetCardVariant', () => {
it('should return dashboard for DASHBOARD page layout type', () => {
expect(
getWidgetCardVariant({
layoutMode: PageLayoutTabLayoutMode.GRID,
isInPinnedTab: false,
pageLayoutType: PageLayoutType.DASHBOARD,
isMobile: false,
isInSidePanel: false,
}),
).toBe('dashboard');
describe('when layoutMode is CANVAS', () => {
it.each([
PageLayoutType.RECORD_PAGE,
PageLayoutType.STANDALONE_PAGE,
PageLayoutType.DASHBOARD,
PageLayoutType.RECORD_INDEX,
])(
"returns 'canvas' regardless of pageLayoutType (%s)",
(pageLayoutType) => {
expect(
getWidgetCardVariant({
...baseParams,
layoutMode: PageLayoutTabLayoutMode.CANVAS,
pageLayoutType,
}),
).toBe('canvas');
},
);
});
it('should return standalone for STANDALONE_PAGE page layout type', () => {
expect(
getWidgetCardVariant({
layoutMode: PageLayoutTabLayoutMode.GRID,
isInPinnedTab: false,
pageLayoutType: PageLayoutType.STANDALONE_PAGE,
isMobile: false,
isInSidePanel: false,
}),
).toBe('standalone');
describe('when layoutMode is GRID', () => {
it("returns 'dashboard' for DASHBOARD page", () => {
expect(
getWidgetCardVariant({
...baseParams,
layoutMode: PageLayoutTabLayoutMode.GRID,
pageLayoutType: PageLayoutType.DASHBOARD,
}),
).toBe('dashboard');
});
it("returns 'standalone' for STANDALONE_PAGE", () => {
expect(
getWidgetCardVariant({
...baseParams,
layoutMode: PageLayoutTabLayoutMode.GRID,
pageLayoutType: PageLayoutType.STANDALONE_PAGE,
}),
).toBe('standalone');
});
it("returns 'record-page' for RECORD_PAGE by default", () => {
expect(
getWidgetCardVariant({
...baseParams,
layoutMode: PageLayoutTabLayoutMode.GRID,
pageLayoutType: PageLayoutType.RECORD_PAGE,
}),
).toBe('record-page');
});
});
it('should prioritize standalone over canvas', () => {
expect(
getWidgetCardVariant({
layoutMode: PageLayoutTabLayoutMode.CANVAS,
isInPinnedTab: false,
pageLayoutType: PageLayoutType.STANDALONE_PAGE,
isMobile: false,
isInSidePanel: false,
}),
).toBe('standalone');
});
it('should return canvas for CANVAS layout mode', () => {
expect(
getWidgetCardVariant({
layoutMode: PageLayoutTabLayoutMode.CANVAS,
isInPinnedTab: false,
pageLayoutType: PageLayoutType.RECORD_PAGE,
isMobile: false,
isInSidePanel: false,
}),
).toBe('canvas');
});
it('should return side-column when isInPinnedTab is true', () => {
expect(
getWidgetCardVariant({
layoutMode: PageLayoutTabLayoutMode.GRID,
isInPinnedTab: true,
pageLayoutType: PageLayoutType.RECORD_PAGE,
isMobile: false,
isInSidePanel: false,
}),
).toBe('side-column');
});
it('should return side-column when isMobile is true', () => {
expect(
getWidgetCardVariant({
layoutMode: PageLayoutTabLayoutMode.GRID,
isInPinnedTab: false,
pageLayoutType: PageLayoutType.RECORD_PAGE,
isMobile: true,
isInSidePanel: false,
}),
).toBe('side-column');
});
it('should return side-column when isInSidePanel is true', () => {
expect(
getWidgetCardVariant({
layoutMode: PageLayoutTabLayoutMode.GRID,
isInPinnedTab: false,
pageLayoutType: PageLayoutType.RECORD_PAGE,
isMobile: false,
isInSidePanel: true,
}),
).toBe('side-column');
});
it('should return record-page as default', () => {
expect(
getWidgetCardVariant({
layoutMode: PageLayoutTabLayoutMode.GRID,
isInPinnedTab: false,
pageLayoutType: PageLayoutType.RECORD_PAGE,
isMobile: false,
isInSidePanel: false,
}),
).toBe('record-page');
});
it('should prioritize dashboard over canvas', () => {
expect(
getWidgetCardVariant({
layoutMode: PageLayoutTabLayoutMode.CANVAS,
isInPinnedTab: false,
pageLayoutType: PageLayoutType.DASHBOARD,
isMobile: false,
isInSidePanel: false,
}),
).toBe('dashboard');
describe('side-column context for record pages', () => {
it.each([
['isInPinnedTab', { isInPinnedTab: true }],
['isMobile', { isMobile: true }],
['isInSidePanel', { isInSidePanel: true }],
])("returns 'side-column' when %s is true", (_label, override) => {
expect(
getWidgetCardVariant({
...baseParams,
...override,
layoutMode: PageLayoutTabLayoutMode.GRID,
pageLayoutType: PageLayoutType.RECORD_PAGE,
}),
).toBe('side-column');
});
});
});
@@ -19,21 +19,20 @@ export const getWidgetCardVariant = ({
isMobile,
isInSidePanel,
}: GetWidgetCardVariantParams): WidgetCardVariant => {
if (pageLayoutType === PageLayoutType.DASHBOARD) {
return 'dashboard';
}
if (pageLayoutType === PageLayoutType.STANDALONE_PAGE) {
return 'standalone';
}
if (layoutMode === PageLayoutTabLayoutMode.CANVAS) {
return 'canvas';
}
if (isInPinnedTab || isMobile || isInSidePanel) {
return 'side-column';
}
const isSideColumnContext = isInPinnedTab || isMobile || isInSidePanel;
return 'record-page';
switch (pageLayoutType) {
case PageLayoutType.DASHBOARD:
return 'dashboard';
case PageLayoutType.STANDALONE_PAGE:
return 'standalone';
case PageLayoutType.RECORD_PAGE:
case PageLayoutType.RECORD_INDEX:
case null:
return isSideColumnContext ? 'side-column' : 'record-page';
}
};