From 3ee8fc097349ecc160fe72cc9cc3777dc03f8714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:09:16 +0200 Subject: [PATCH] Add front component skeleton loader (#23261) Front components (dashboard widget, side panel, settings preview) showed blank space during their entire load. They now show a shimmering full-area skeleton continuously, from the lazy chunk load through metadata fetch, token/SDK wait, and worker boot, until the real UI mounts. The skeleton is threaded down as an optional `loadingFallback` prop so the shared `twenty-front-component-renderer` package stays dependency-free (react-loading-skeleton stays in twenty-front). The command-menu headless component opts out by not passing a fallback, so it stays blank as before. Review in cubic --- .../components/FrontComponentRenderer.tsx | 10 +++++-- .../components/FrontComponentRenderer.tsx | 13 ++++++-- .../FrontComponentSkeletonLoader.tsx | 30 +++++++++++++++++++ .../FrontComponentWidgetRenderer.tsx | 4 ++- .../SidePanelFrontComponentPage.tsx | 4 ++- ...ngsApplicationFrontComponentPreviewTab.tsx | 9 ++++-- 6 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 packages/twenty-front/src/modules/front-components/components/FrontComponentSkeletonLoader.tsx diff --git a/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx b/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx index 3585fa1bcb..f45cba7e97 100644 --- a/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx +++ b/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx @@ -11,7 +11,7 @@ import { type RemoteReceiver, RemoteRootRenderer, } from '@remote-dom/react/host'; -import { useState } from 'react'; +import { type ReactNode, useState } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { isDefined } from 'twenty-shared/utils'; @@ -35,6 +35,7 @@ type FrontComponentRendererProps = { frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi; onError: (error?: Error) => void; colorScheme: 'light' | 'dark'; + loadingFallback?: ReactNode; }; export const FrontComponentRenderer = ({ @@ -48,6 +49,7 @@ export const FrontComponentRenderer = ({ frontComponentHostCommunicationApi, onError, colorScheme, + loadingFallback, }: FrontComponentRendererProps) => { const [receiver, setReceiver] = useState(null); const [thread, setThread] = useState(null); @@ -55,6 +57,8 @@ export const FrontComponentRenderer = ({ const [isExecutionContextInitialized, setIsExecutionContextInitialized] = useState(false); + const isReady = isDefined(receiver) && isExecutionContextInitialized; + return ( <> )} - {isDefined(receiver) && isExecutionContextInitialized && ( + {!isDefined(error) && !isReady && loadingFallback} + + {isReady && ( { const { data, loading, error } = useQuery(FindOneFrontComponentDocument, { variables: { id: frontComponentId }, @@ -55,11 +58,13 @@ export const FrontComponentRenderer = ({ return ( <> + {loading && loadingFallback} {!loading && isDefined(frontComponent) && ( )} @@ -70,6 +75,7 @@ const FrontComponentRendererContent = ({ frontComponent, commandMenuItemId, selectedRecordIds, + loadingFallback, }: FrontComponentRendererContentProps) => { const { colorScheme } = useContext(ThemeContext); const { enqueueErrorSnackBar } = useSnackBar(); @@ -127,6 +133,7 @@ const FrontComponentRendererContent = ({ const applicationVariables = frontComponent.applicationVariables ?? undefined; const isSdkClientReady = !usesSdkClient || !sdkClientChecksumsLoading; + const isReadyToRender = isDefined(applicationTokenPair) && isSdkClientReady; return ( <> @@ -134,7 +141,8 @@ const FrontComponentRendererContent = ({ frontComponentId={frontComponentId} applicationTokenPair={applicationTokenPair} /> - {isDefined(applicationTokenPair) && isSdkClientReady && ( + {!isReadyToRender && loadingFallback} + {isReadyToRender && ( )} diff --git a/packages/twenty-front/src/modules/front-components/components/FrontComponentSkeletonLoader.tsx b/packages/twenty-front/src/modules/front-components/components/FrontComponentSkeletonLoader.tsx new file mode 100644 index 0000000000..edc76970d2 --- /dev/null +++ b/packages/twenty-front/src/modules/front-components/components/FrontComponentSkeletonLoader.tsx @@ -0,0 +1,30 @@ +import { styled } from '@linaria/react'; +import { useContext } from 'react'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; +import { ThemeContext } from 'twenty-ui/theme-constants'; + +const StyledContainer = styled.div` + height: 100%; + width: 100%; + + & > span { + display: block; + height: 100%; + } +`; + +export const FrontComponentSkeletonLoader = () => { + const { theme } = useContext(ThemeContext); + + return ( + + + + + + ); +}; 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 664db2ccbb..9b2c629f49 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,6 +3,7 @@ import { Suspense, lazy } from 'react'; import { isDefined } from 'twenty-shared/utils'; +import { FrontComponentSkeletonLoader } from '@/front-components/components/FrontComponentSkeletonLoader'; import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext'; import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode'; import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; @@ -57,10 +58,11 @@ export const FrontComponentWidgetRenderer = ({ isCanvasLayout={layoutMode === PageLayoutTabLayoutMode.CANVAS} isInEditMode={isPageLayoutInEditMode} > - + }> } /> diff --git a/packages/twenty-front/src/modules/side-panel/pages/front-component/components/SidePanelFrontComponentPage.tsx b/packages/twenty-front/src/modules/side-panel/pages/front-component/components/SidePanelFrontComponentPage.tsx index 142ba5934b..19d22d7aca 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/front-component/components/SidePanelFrontComponentPage.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/front-component/components/SidePanelFrontComponentPage.tsx @@ -1,5 +1,6 @@ import { Suspense, lazy } from 'react'; +import { FrontComponentSkeletonLoader } from '@/front-components/components/FrontComponentSkeletonLoader'; import { viewableFrontComponentIdComponentState } from '@/side-panel/pages/front-component/states/viewableFrontComponentIdComponentState'; import { viewableFrontComponentRecordContextComponentState } from '@/side-panel/pages/front-component/states/viewableFrontComponentRecordContextComponentState'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; @@ -31,10 +32,11 @@ export const SidePanelFrontComponentPage = () => { : undefined; return ( - + }> } /> ); diff --git a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationFrontComponentPreviewTab.tsx b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationFrontComponentPreviewTab.tsx index 8cf18ae9e8..95897b1859 100644 --- a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationFrontComponentPreviewTab.tsx +++ b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationFrontComponentPreviewTab.tsx @@ -4,6 +4,8 @@ import { Suspense, lazy } from 'react'; import { Section } from 'twenty-ui/layout'; import { themeCssVariables } from 'twenty-ui/theme-constants'; +import { FrontComponentSkeletonLoader } from '@/front-components/components/FrontComponentSkeletonLoader'; + const FrontComponentRenderer = lazy(() => import('@/front-components/components/FrontComponentRenderer').then( (module) => ({ default: module.FrontComponentRenderer }), @@ -64,8 +66,11 @@ export const SettingsApplicationFrontComponentPreviewTab = ({ ) : ( - - + }> + } + /> )}