From b346f4fb596e091d3edd203fd6d3b2e71c40e42e Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Wed, 11 Mar 2026 14:56:49 +0100 Subject: [PATCH] Add common loader (#18556) To avoid white screens on reload, building a shared skeleton. Before https://github.com/user-attachments/assets/42bd0667-141d-4df4-9072-4077192cc71d After https://github.com/user-attachments/assets/e6031a72-2e25-47e3-a873-b89aaddfbd3a --- .../components/PageContentSkeletonLoader.tsx | 56 ++++++++++++ .../components/RightPanelSkeletonLoader.tsx | 90 +------------------ .../components/UserOrMetadataLoader.tsx | 11 ++- .../src/modules/app/components/LazyRoute.tsx | 10 ++- .../components/RecordIndexSkeletonLoader.tsx | 8 ++ .../pages/object-record/RecordIndexPage.tsx | 5 +- 6 files changed, 86 insertions(+), 94 deletions(-) create mode 100644 packages/twenty-front/src/loading/components/PageContentSkeletonLoader.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexSkeletonLoader.tsx diff --git a/packages/twenty-front/src/loading/components/PageContentSkeletonLoader.tsx b/packages/twenty-front/src/loading/components/PageContentSkeletonLoader.tsx new file mode 100644 index 00000000000..2035d2dc9eb --- /dev/null +++ b/packages/twenty-front/src/loading/components/PageContentSkeletonLoader.tsx @@ -0,0 +1,56 @@ +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; +import { PageBody } from '@/ui/layout/page/components/PageBody'; +import { PAGE_BAR_MIN_HEIGHT } from '@/ui/layout/page/constants/PageBarMinHeight'; +import { styled } from '@linaria/react'; +import { useContext } from 'react'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; +import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; + +const StyledHeaderSkeleton = styled.div` + align-items: center; + background: ${themeCssVariables.background.noisy}; + display: flex; + flex-direction: row; + gap: ${themeCssVariables.spacing[2]}; + justify-content: space-between; + min-height: ${PAGE_BAR_MIN_HEIGHT}px; + padding: ${themeCssVariables.spacing[3]}; +`; + +const StyledHeaderLeft = styled.div` + flex: 1; +`; + +export const PageContentSkeletonLoader = () => { + const { theme } = useContext(ThemeContext); + + return ( + <> + + + + + + + + + + + {null} + + ); +}; diff --git a/packages/twenty-front/src/loading/components/RightPanelSkeletonLoader.tsx b/packages/twenty-front/src/loading/components/RightPanelSkeletonLoader.tsx index bd14d6f9cdf..aa182aed4f0 100644 --- a/packages/twenty-front/src/loading/components/RightPanelSkeletonLoader.tsx +++ b/packages/twenty-front/src/loading/components/RightPanelSkeletonLoader.tsx @@ -1,100 +1,14 @@ -import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; +import { PageContentSkeletonLoader } from '~/loading/components/PageContentSkeletonLoader'; import { styled } from '@linaria/react'; -import { useContext } from 'react'; -import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; -import { - MOBILE_VIEWPORT, - ThemeContext, - themeCssVariables, -} from 'twenty-ui/theme-constants'; -const StyledMainContainer = styled.div` - background: ${themeCssVariables.background.noisy}; - box-sizing: border-box; - display: flex; - flex: 1 1 auto; - flex-direction: row; - gap: 8px; - min-height: 0; - padding-left: 0; - width: 100%; - - @media (max-width: ${MOBILE_VIEWPORT}px) { - padding-left: 12px; - padding-bottom: 0; - } -`; - -const StyledPanel = styled.div` - background: ${themeCssVariables.background.primary}; - border: 1px solid ${themeCssVariables.border.color.medium}; - border-radius: ${themeCssVariables.border.radius.md}; - height: 100%; - overflow: auto; - width: 100%; -`; - -const StyledHeaderContainer = styled.div` - flex: 1; -`; const StyledRightPanelContainer = styled.div` display: flex; flex-direction: column; width: 100%; `; -const StyledRightPanelFlexContainer = styled.div` - align-items: center; - display: flex; - flex-direction: row; - height: 32px; - justify-content: flex-end; - margin-bottom: 12px; -`; - -const StyledSkeletonHeaderLoader = () => { - const { theme } = useContext(ThemeContext); - return ( - - - - - - ); -}; - -const StyledSkeletonAddLoader = () => { - const { theme } = useContext(ThemeContext); - return ( - - - - ); -}; - -const RightPanelSkeleton = () => ( - - - -); - export const RightPanelSkeletonLoader = () => ( - - - - - + ); diff --git a/packages/twenty-front/src/loading/components/UserOrMetadataLoader.tsx b/packages/twenty-front/src/loading/components/UserOrMetadataLoader.tsx index e49c4407df9..7ca89983591 100644 --- a/packages/twenty-front/src/loading/components/UserOrMetadataLoader.tsx +++ b/packages/twenty-front/src/loading/components/UserOrMetadataLoader.tsx @@ -13,11 +13,9 @@ const StyledContainer = styled.div` box-sizing: border-box; display: flex; flex-direction: row; - gap: 12px; height: 100dvh; min-width: ${NAVIGATION_DRAWER_CONSTRAINTS.default}px; overflow: hidden; - padding: 12px 8px 12px 8px; width: 100%; @media (max-width: ${MOBILE_VIEWPORT}px) { @@ -25,6 +23,11 @@ const StyledContainer = styled.div` } `; +const StyledLeftPanelWrapper = styled.div` + flex-shrink: 0; + padding: 12px 0 12px 8px; +`; + export const UserOrMetadataLoader = () => { const showAuthModal = useShowAuthModal(); @@ -36,7 +39,9 @@ export const UserOrMetadataLoader = () => { backdropZIndex={RootStackingContextZIndices.RootModalBackDrop} /> )} - + + + ); diff --git a/packages/twenty-front/src/modules/app/components/LazyRoute.tsx b/packages/twenty-front/src/modules/app/components/LazyRoute.tsx index 19fea5b4ba8..15a942a6110 100644 --- a/packages/twenty-front/src/modules/app/components/LazyRoute.tsx +++ b/packages/twenty-front/src/modules/app/components/LazyRoute.tsx @@ -1,9 +1,17 @@ +import { PageContainer } from '@/ui/layout/page/components/PageContainer'; import { type ReactNode, Suspense } from 'react'; +import { PageContentSkeletonLoader } from '~/loading/components/PageContentSkeletonLoader'; type LazyRouteProps = { children: ReactNode; }; +const LazyRouteFallback = () => ( + + + +); + export const LazyRoute = ({ children }: LazyRouteProps) => ( - }>{children} + }>{children} ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexSkeletonLoader.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexSkeletonLoader.tsx new file mode 100644 index 00000000000..e7d4d01f4b2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexSkeletonLoader.tsx @@ -0,0 +1,8 @@ +import { PageContainer } from '@/ui/layout/page/components/PageContainer'; +import { PageContentSkeletonLoader } from '~/loading/components/PageContentSkeletonLoader'; + +export const RecordIndexSkeletonLoader = () => ( + + + +); diff --git a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx index 595c8d1dfeb..48e93966ccb 100644 --- a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx @@ -3,6 +3,7 @@ import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { RecordIndexContainerGater } from '@/object-record/record-index/components/RecordIndexContainerGater'; +import { RecordIndexSkeletonLoader } from '@/object-record/record-index/components/RecordIndexSkeletonLoader'; import { PageContainer } from '@/ui/layout/page/components/PageContainer'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { isUndefined } from '@sniptt/guards'; @@ -16,7 +17,7 @@ export const RecordIndexPage = () => { const { objectMetadataItems } = useObjectMetadataItems(); if (isUndefined(contextStoreCurrentObjectMetadataItemId)) { - return <>; + return ; } const objectMetadataItem = objectMetadataItems.find( @@ -25,7 +26,7 @@ export const RecordIndexPage = () => { ); if (isUndefined(objectMetadataItem)) { - return <>; + return ; } return (