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. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23261?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. -->
This commit is contained in:
+8
-2
@@ -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<RemoteReceiver | null>(null);
|
||||
const [thread, setThread] = useState<FrontComponentThread | null>(null);
|
||||
@@ -55,6 +57,8 @@ export const FrontComponentRenderer = ({
|
||||
const [isExecutionContextInitialized, setIsExecutionContextInitialized] =
|
||||
useState(false);
|
||||
|
||||
const isReady = isDefined(receiver) && isExecutionContextInitialized;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FrontComponentWorkerEffect
|
||||
@@ -100,7 +104,9 @@ export const FrontComponentRenderer = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{isDefined(receiver) && isExecutionContextInitialized && (
|
||||
{!isDefined(error) && !isReady && loadingFallback}
|
||||
|
||||
{isReady && (
|
||||
<ThemeProvider colorScheme={colorScheme}>
|
||||
<ErrorBoundary
|
||||
onError={setError}
|
||||
|
||||
+11
-2
@@ -10,7 +10,7 @@ import { useGetLogicFunctionHttpUrl } from '@/settings/logic-functions/hooks/use
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { type ReactNode, useCallback, useContext, useMemo } from 'react';
|
||||
import { FrontComponentRenderer as SharedFrontComponentRenderer } from 'twenty-front-component-renderer';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
@@ -25,6 +25,7 @@ type FrontComponentRendererProps = {
|
||||
frontComponentId: string;
|
||||
commandMenuItemId?: string;
|
||||
selectedRecordIds?: string[];
|
||||
loadingFallback?: ReactNode;
|
||||
};
|
||||
|
||||
type ResolvedFrontComponent = NonNullable<
|
||||
@@ -35,12 +36,14 @@ type FrontComponentRendererContentProps = {
|
||||
frontComponent: ResolvedFrontComponent;
|
||||
commandMenuItemId?: string;
|
||||
selectedRecordIds?: string[];
|
||||
loadingFallback?: ReactNode;
|
||||
};
|
||||
|
||||
export const FrontComponentRenderer = ({
|
||||
frontComponentId,
|
||||
commandMenuItemId,
|
||||
selectedRecordIds,
|
||||
loadingFallback,
|
||||
}: FrontComponentRendererProps) => {
|
||||
const { data, loading, error } = useQuery(FindOneFrontComponentDocument, {
|
||||
variables: { id: frontComponentId },
|
||||
@@ -55,11 +58,13 @@ export const FrontComponentRenderer = ({
|
||||
return (
|
||||
<>
|
||||
<FrontComponentLoadErrorSnackBarEffect errorMessage={error?.message} />
|
||||
{loading && loadingFallback}
|
||||
{!loading && isDefined(frontComponent) && (
|
||||
<FrontComponentRendererContent
|
||||
frontComponent={frontComponent}
|
||||
commandMenuItemId={commandMenuItemId}
|
||||
selectedRecordIds={selectedRecordIds}
|
||||
loadingFallback={loadingFallback}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -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 && (
|
||||
<FrontComponentRendererProvider frontComponentId={frontComponentId}>
|
||||
<SharedFrontComponentRenderer
|
||||
colorScheme={colorScheme}
|
||||
@@ -151,6 +159,7 @@ const FrontComponentRendererContent = ({
|
||||
}
|
||||
applicationVariables={applicationVariables}
|
||||
onError={handleError}
|
||||
loadingFallback={loadingFallback}
|
||||
/>
|
||||
</FrontComponentRendererProvider>
|
||||
)}
|
||||
|
||||
+30
@@ -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 (
|
||||
<StyledContainer>
|
||||
<SkeletonTheme
|
||||
baseColor={theme.background.tertiary}
|
||||
highlightColor={theme.background.transparent.lighter}
|
||||
borderRadius={theme.border.radius.md}
|
||||
>
|
||||
<Skeleton height="100%" />
|
||||
</SkeletonTheme>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+3
-1
@@ -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}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<Suspense fallback={<FrontComponentSkeletonLoader />}>
|
||||
<FrontComponentRenderer
|
||||
frontComponentId={frontComponentId}
|
||||
selectedRecordIds={selectedRecordIds}
|
||||
loadingFallback={<FrontComponentSkeletonLoader />}
|
||||
/>
|
||||
</Suspense>
|
||||
</StyledContainer>
|
||||
|
||||
+3
-1
@@ -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 (
|
||||
<Suspense fallback={null}>
|
||||
<Suspense fallback={<FrontComponentSkeletonLoader />}>
|
||||
<FrontComponentRenderer
|
||||
frontComponentId={viewableFrontComponentId}
|
||||
selectedRecordIds={selectedRecordIds}
|
||||
loadingFallback={<FrontComponentSkeletonLoader />}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
+7
-2
@@ -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 = ({
|
||||
</StyledHeadlessNotice>
|
||||
) : (
|
||||
<StyledRendererContainer>
|
||||
<Suspense fallback={null}>
|
||||
<FrontComponentRenderer frontComponentId={frontComponentId} />
|
||||
<Suspense fallback={<FrontComponentSkeletonLoader />}>
|
||||
<FrontComponentRenderer
|
||||
frontComponentId={frontComponentId}
|
||||
loadingFallback={<FrontComponentSkeletonLoader />}
|
||||
/>
|
||||
</Suspense>
|
||||
</StyledRendererContainer>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user