From 0b38b4ffc4d383e37a72f8558521104bd286eae0 Mon Sep 17 00:00:00 2001
From: nitin <142569587+ehconitin@users.noreply.github.com>
Date: Mon, 25 May 2026 17:29:31 +0530
Subject: [PATCH] fix(page-layout): respect tab layoutMode on standalone pages
(#20856)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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:
https://github.com/user-attachments/assets/a3374e18-ad1b-4888-ab2b-d07730edccac
---
.../utils/__tests__/getTabLayoutMode.test.ts | 46 +++++
.../page-layout/utils/getTabLayoutMode.ts | 5 +-
.../FrontComponentWidgetRenderer.tsx | 15 +-
.../__tests__/getWidgetCardVariant.test.ts | 171 +++++++-----------
.../widgets/utils/getWidgetCardVariant.ts | 23 ++-
5 files changed, 142 insertions(+), 118 deletions(-)
create mode 100644 packages/twenty-front/src/modules/page-layout/utils/__tests__/getTabLayoutMode.test.ts
diff --git a/packages/twenty-front/src/modules/page-layout/utils/__tests__/getTabLayoutMode.test.ts b/packages/twenty-front/src/modules/page-layout/utils/__tests__/getTabLayoutMode.test.ts
new file mode 100644
index 0000000000..5e1fd165e4
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/utils/__tests__/getTabLayoutMode.test.ts
@@ -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,
+ );
+ },
+ );
+ });
+});
diff --git a/packages/twenty-front/src/modules/page-layout/utils/getTabLayoutMode.ts b/packages/twenty-front/src/modules/page-layout/utils/getTabLayoutMode.ts
index ac2cb38cde..6a7397a18d 100644
--- a/packages/twenty-front/src/modules/page-layout/utils/getTabLayoutMode.ts
+++ b/packages/twenty-front/src/modules/page-layout/utils/getTabLayoutMode.ts
@@ -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;
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/front-component/components/FrontComponentWidgetRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/front-component/components/FrontComponentWidgetRenderer.tsx
index 5841d8f3a4..664db2ccbb 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/front-component/components/FrontComponentWidgetRenderer.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/front-component/components/FrontComponentWidgetRenderer.tsx
@@ -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 (
-
+
{
- 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');
+ });
});
});
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/utils/getWidgetCardVariant.ts b/packages/twenty-front/src/modules/page-layout/widgets/utils/getWidgetCardVariant.ts
index 7d909bcc94..a91dc591ab 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/utils/getWidgetCardVariant.ts
+++ b/packages/twenty-front/src/modules/page-layout/widgets/utils/getWidgetCardVariant.ts
@@ -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';
+ }
};