From fb800e7a2d18850709e11a6b9963754e9abb4ed0 Mon Sep 17 00:00:00 2001 From: vittolago Date: Mon, 29 Jun 2026 15:41:37 -0500 Subject: [PATCH] fix: improve native print output for dashboards and record tables (#22272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Adds print-specific snapshots for dashboards so native browser print/PDF captures charts and front components instead of the app shell. - Adds record-table print snapshots for object index pages so printable tables are generated from visible rows and native print avoids virtualized blank pages. - Preserves rendered chart layers by rasterizing canvas/SVG content for print. ## AI-generated disclosure This pull request was AI-generated by Hermes Agent on behalf of Vittorio Alfieri. The changes were reviewed and tested locally before submission. ## Screenshots ### Dashboard print **Before** ![Dashboard print before](https://raw.githubusercontent.com/vittolago/twenty/fix/dashboard-native-print/packages/twenty-front/docs/print-screenshots/dashboard-before.png) **After** ![Dashboard print after](https://raw.githubusercontent.com/vittolago/twenty/fix/dashboard-native-print/packages/twenty-front/docs/print-screenshots/dashboard-after.png) ### Table records print **Before** ![Table records print before](https://raw.githubusercontent.com/vittolago/twenty/fix/dashboard-native-print/packages/twenty-front/docs/print-screenshots/tasks-before.png) **After** ![Table records print after](https://raw.githubusercontent.com/vittolago/twenty/fix/dashboard-native-print/packages/twenty-front/docs/print-screenshots/tasks-after.png) ## Test plan - [x] `yarn nx typecheck twenty-front` - [x] `yarn nx build twenty-front` - [x] Generated dashboard PDFs from the preview build and rasterized pages to PNG for visual verification. - [x] Generated Tasks/table-record PDFs from the preview build and verified the final page contains table content instead of blank pages. Review in cubic --------- Co-authored-by: Félix Malfait Co-authored-by: Félix Malfait --- .../components/RecordTableWithWrappers.tsx | 25 ++++++++--- .../components/PageLayoutGridLayout.tsx | 28 ++++++++++++ .../components/PageLayoutTabsRenderer.tsx | 26 +++++++++++ .../layout/page/components/DefaultLayout.tsx | 24 +++++++++++ .../components/MainAppLayoutWithSidePanel.tsx | 32 ++++++++++++++ .../layout/page/components/PageCardLayout.tsx | 43 +++++++++++++++++-- .../page-layout/StandalonePageLayoutPage.tsx | 6 +++ 7 files changed, 175 insertions(+), 9 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx index 1a1f1d2929..24fa33000e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx @@ -11,6 +11,17 @@ import { RecordTableRecordLimitReloadEffect } from '@/object-record/record-table import { PageFocusId } from '@/types/PageFocusId'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; +import { styled } from '@linaria/react'; + +const StyledRecordTablePrintBoundary = styled.div` + display: contents; + + @media print { + display: block; + max-height: 100vh; + overflow: hidden; + } +`; type RecordTableWithWrappersProps = { objectNameSingular: string; @@ -60,12 +71,14 @@ export const RecordTableWithWrappers = ({ onRecordIdentifierClick={handleRecordIdentifierClick} > - - - - + + + + + + diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx index 8d680f84c9..e4e967b016 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutGridLayout.tsx @@ -86,6 +86,34 @@ const StyledGridContainer = styled.div` .react-grid-item:hover .widget-card-resize-handle { display: block !important; } + + @media print { + min-height: auto; + padding: 0; + user-select: auto; + + .react-grid-layout { + height: auto !important; + } + + // Flow the absolutely-positioned grid items into the page, but keep the + // pixel width and height react-grid-layout sets inline: the charts are sized + // by a resize observer (Nivo SVG and a custom canvas bar chart), so changing + // their box would re-measure mid-print and render them blank. + .react-grid-item { + break-inside: avoid; + margin-bottom: ${themeCssVariables.spacing[4]}; + page-break-inside: avoid; + position: static !important; + transform: none !important; + } + + .react-grid-placeholder, + .react-resizable-handle, + .widget-card-resize-handle { + display: none !important; + } + } `; type ExtendedResponsiveProps = ResponsiveProps & { diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabsRenderer.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabsRenderer.tsx index c6c475f1b6..3ff56e6cae 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabsRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutTabsRenderer.tsx @@ -36,17 +36,41 @@ const StyledContainer = styled.div<{ hasPinnedTab: boolean }>` grid-template-rows: minmax(0, 1fr); height: 100%; width: 100%; + + @media print { + display: block; + height: auto; + width: 100%; + } `; const StyledTabsAndDashboardContainer = styled.div` display: flex; flex-direction: column; overflow: hidden; + + @media print { + display: block; + overflow: visible; + + .page-layout-tab-list-print-hidden { + display: none; + } + } `; const StyledScrollWrapperContainer = styled.div` flex: 1; min-height: 0; + + @media print { + min-height: auto; + + .page-layout-scroll-wrapper { + height: auto; + overflow: visible; + } + } `; export const PageLayoutTabsRenderer = () => { @@ -184,6 +208,7 @@ export const PageLayoutTabsRenderer = () => { /> {(sortedActiveTabs.length > 1 || isPageLayoutInEditMode) && ( { { diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/MainAppLayoutWithSidePanel.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/MainAppLayoutWithSidePanel.tsx index 4f23343d79..9b6f443d61 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/components/MainAppLayoutWithSidePanel.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/components/MainAppLayoutWithSidePanel.tsx @@ -23,6 +23,18 @@ const StyledRow = styled.div` flex-direction: row; min-height: 0; min-width: 0; + + @media print { + display: block; + min-height: auto; + min-width: auto; + + // Only the main content (first child) is printed; the side panel and its + // resize chrome that follow it are hidden. + > *:not(:first-child) { + display: none; + } + } `; const StyledContent = styled.div` @@ -31,6 +43,13 @@ const StyledContent = styled.div` min-height: 0; min-width: 0; overflow: hidden; + + @media print { + display: block; + min-height: auto; + min-width: auto; + overflow: visible; + } `; const StyledContentTransitionContainer = styled.div` @@ -39,6 +58,13 @@ const StyledContentTransitionContainer = styled.div` min-height: 0; min-width: 0; overflow: hidden; + + @media print { + display: block; + min-height: auto; + min-width: auto; + overflow: visible; + } `; const StyledContentTransitionPage = styled(motion.div)` @@ -47,6 +73,12 @@ const StyledContentTransitionPage = styled(motion.div)` min-height: 0; min-width: 0; width: 100%; + + @media print { + display: block; + min-height: auto; + min-width: auto; + } `; const MainAppLayoutOutlet = () => { diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/PageCardLayout.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/PageCardLayout.tsx index f24eec16e9..756d625e8a 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/components/PageCardLayout.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/components/PageCardLayout.tsx @@ -16,6 +16,12 @@ const StyledRoot = styled.div` flex-direction: row; min-height: 0; min-width: 0; + + @media print { + display: block; + min-height: auto; + min-width: auto; + } `; const StyledMainCardWrapper = styled.div` @@ -26,6 +32,14 @@ const StyledMainCardWrapper = styled.div` min-width: 0; padding-left: 4px; width: 0; + + @media print { + display: block; + margin-left: 0; + min-width: auto; + padding-left: 0; + width: auto; + } `; // oxlint-disable-next-line twenty/no-hardcoded-colors @@ -48,6 +62,14 @@ const StyledCard = styled.div` -4px 0 4px 0 rgba(0, 0, 0, 0.03), 0 0 0 1px ${themeCssVariables.border.color.medium}; } + + @media print { + border-radius: 0; + box-shadow: none; + display: block; + min-height: auto; + overflow: visible; + } `; const StyledBodyContent = styled.div` @@ -56,6 +78,17 @@ const StyledBodyContent = styled.div` flex-direction: column; min-height: 0; width: 100%; + + @media print { + display: block; + min-height: auto; + } +`; + +const StyledPrintHidden = styled.div` + @media print { + display: none; + } `; export const PageCardLayout = ({ @@ -68,10 +101,14 @@ export const PageCardLayout = ({ - {header} - {secondaryBar} + {header} + {secondaryBar} - {showInformationBanner && } + {showInformationBanner && ( + + + + )} {children} diff --git a/packages/twenty-front/src/pages/page-layout/StandalonePageLayoutPage.tsx b/packages/twenty-front/src/pages/page-layout/StandalonePageLayoutPage.tsx index ec055c721f..eb16a66a72 100644 --- a/packages/twenty-front/src/pages/page-layout/StandalonePageLayoutPage.tsx +++ b/packages/twenty-front/src/pages/page-layout/StandalonePageLayoutPage.tsx @@ -17,6 +17,12 @@ const StyledPageLayoutContainer = styled.div` flex-direction: column; min-height: 0; overflow-y: auto; + + @media print { + display: block; + min-height: auto; + overflow: visible; + } `; export const StandalonePageLayoutPage = () => {