fb800e7a2d
## 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**  **After**  ### Table records print **Before**  **After**  ## 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. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22272?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
126 lines
3.2 KiB
TypeScript
126 lines
3.2 KiB
TypeScript
import { CommandMenuForMobile } from '@/command-menu/components/CommandMenuForMobile';
|
|
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
|
|
import { SidePanelForDesktop } from '@/side-panel/components/SidePanelForDesktop';
|
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
|
import { styled } from '@linaria/react';
|
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
import { useLocation, useOutlet } from 'react-router-dom';
|
|
import { AppPath } from 'twenty-shared/types';
|
|
|
|
const APP_TO_SETTINGS_TRANSITION_DURATION_IN_SECONDS = 0.3;
|
|
|
|
const SETTINGS_PATH_PREFIX = `/${AppPath.Settings}`;
|
|
|
|
const getMainAppLayoutRouteSection = (pathname: string) =>
|
|
pathname === SETTINGS_PATH_PREFIX ||
|
|
pathname.startsWith(`${SETTINGS_PATH_PREFIX}/`)
|
|
? 'settings'
|
|
: 'app';
|
|
|
|
const StyledRow = styled.div`
|
|
display: flex;
|
|
flex: 1;
|
|
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`
|
|
display: flex;
|
|
flex: 1 1 0;
|
|
min-height: 0;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
|
|
@media print {
|
|
display: block;
|
|
min-height: auto;
|
|
min-width: auto;
|
|
overflow: visible;
|
|
}
|
|
`;
|
|
|
|
const StyledContentTransitionContainer = styled.div`
|
|
display: grid;
|
|
flex: 1 1 0;
|
|
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)`
|
|
display: flex;
|
|
grid-area: 1 / 1;
|
|
min-height: 0;
|
|
min-width: 0;
|
|
width: 100%;
|
|
|
|
@media print {
|
|
display: block;
|
|
min-height: auto;
|
|
min-width: auto;
|
|
}
|
|
`;
|
|
|
|
const MainAppLayoutOutlet = () => {
|
|
const { pathname } = useLocation();
|
|
const outlet = useOutlet();
|
|
const routeSection = getMainAppLayoutRouteSection(pathname);
|
|
|
|
return (
|
|
<StyledContentTransitionContainer>
|
|
<AnimatePresence initial={false}>
|
|
<StyledContentTransitionPage
|
|
key={routeSection}
|
|
initial={{ opacity: 0, y: 4 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
// The outgoing page shares this grid cell with the incoming one, so it
|
|
// must not capture pointer/scroll events — including when AnimatePresence
|
|
// leaves a stale exit node mounted on top of the active page.
|
|
exit={{ opacity: 0, y: -4, pointerEvents: 'none' }}
|
|
transition={{
|
|
duration: APP_TO_SETTINGS_TRANSITION_DURATION_IN_SECONDS,
|
|
ease: 'easeInOut',
|
|
}}
|
|
>
|
|
{outlet}
|
|
</StyledContentTransitionPage>
|
|
</AnimatePresence>
|
|
</StyledContentTransitionContainer>
|
|
);
|
|
};
|
|
|
|
export const MainAppLayoutWithSidePanel = () => {
|
|
const isMobile = useIsMobile();
|
|
|
|
useCommandMenuHotKeys();
|
|
|
|
return (
|
|
<StyledRow>
|
|
<StyledContent>
|
|
<MainAppLayoutOutlet />
|
|
</StyledContent>
|
|
{isMobile ? <CommandMenuForMobile /> : <SidePanelForDesktop />}
|
|
</StyledRow>
|
|
);
|
|
};
|