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:
+46
@@ -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;
|
||||
|
||||
+12
-3
@@ -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}
|
||||
|
||||
+69
-102
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+11
-12
@@ -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';
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user