Improve AppError boundaries (#11107)
## What This PR aims to make sure all application exceptions are captured through react-error-boundaries Once merged we will have: - Root Level: AppErrorBoundary at the highest level (full screen) ==> this one needs to be working in any case, not relying on Theme, was not working - Route Level: AppErrorBoundary in DefaultLayout (full screen) ==> this was missing and it seems that error are not propagated outside of the router, making errors triggered in CommandMenu or NavigationDrawer missing - Page Level: AppErrorBoundary in DefaultLayout write around the Page itself (lower than CommandMenu + NavigationDrawer) - Manually triggered: example in ClientConfigProvider ## Screenshots App level (ex throw in IconsProvider) <img width="1512" alt="image" src="https://github.com/user-attachments/assets/18a14815-a203-4edf-b931-43068c3436ec" /> Route level (ex throw in CommandMenu) <img width="1512" alt="image" src="https://github.com/user-attachments/assets/ca066627-14c7-438e-a432-f0999a1f3b84" /> Page level (ex throw in RecordTable) <img width="1512" alt="image" src="https://github.com/user-attachments/assets/ffeaa935-02af-4762-8859-7a0ccf8b77e1" /> Manually Triggered (clientConfig, ex when backend is not up) <img width="1512" alt="image" src="https://github.com/user-attachments/assets/062d6d84-097a-4ed9-b6ce-763b8c27c659" />
This commit is contained in:
@ -2,6 +2,7 @@ import { AppRouter } from '@/app/components/AppRouter';
|
|||||||
import { ApolloDevLogEffect } from '@/debug/components/ApolloDevLogEffect';
|
import { ApolloDevLogEffect } from '@/debug/components/ApolloDevLogEffect';
|
||||||
import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver';
|
import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver';
|
||||||
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
|
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
|
||||||
|
import { AppRootErrorFallback } from '@/error-handler/components/AppRootErrorFallback';
|
||||||
import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider';
|
import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider';
|
||||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||||
import { i18n } from '@lingui/core';
|
import { i18n } from '@lingui/core';
|
||||||
@ -18,7 +19,10 @@ export const App = () => {
|
|||||||
return (
|
return (
|
||||||
<RecoilRoot>
|
<RecoilRoot>
|
||||||
<RecoilURLSyncJSON location={{ part: 'queryParams' }}>
|
<RecoilURLSyncJSON location={{ part: 'queryParams' }}>
|
||||||
<AppErrorBoundary>
|
<AppErrorBoundary
|
||||||
|
resetOnLocationChange={false}
|
||||||
|
FallbackComponent={AppRootErrorFallback}
|
||||||
|
>
|
||||||
<I18nProvider i18n={i18n}>
|
<I18nProvider i18n={i18n}>
|
||||||
<RecoilDebugObserverEffect />
|
<RecoilDebugObserverEffect />
|
||||||
<ApolloDevLogEffect />
|
<ApolloDevLogEffect />
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { CaptchaProvider } from '@/captcha/components/CaptchaProvider';
|
|
||||||
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
|
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
|
||||||
import { GotoHotkeysEffectsProvider } from '@/app/effect-components/GotoHotkeysEffectsProvider';
|
import { GotoHotkeysEffectsProvider } from '@/app/effect-components/GotoHotkeysEffectsProvider';
|
||||||
import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect';
|
import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect';
|
||||||
import { AuthProvider } from '@/auth/components/AuthProvider';
|
import { AuthProvider } from '@/auth/components/AuthProvider';
|
||||||
|
import { CaptchaProvider } from '@/captcha/components/CaptchaProvider';
|
||||||
import { ChromeExtensionSidecarEffect } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect';
|
import { ChromeExtensionSidecarEffect } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect';
|
||||||
import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider';
|
import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider';
|
||||||
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
|
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||||
import { ClientConfigError } from '@/error-handler/components/ClientConfigError';
|
import { AppFullScreenErrorFallback } from '@/error-handler/components/AppFullScreenErrorFallback';
|
||||||
|
|
||||||
export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@ -14,7 +14,13 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
if (!isLoaded) return null;
|
if (!isLoaded) return null;
|
||||||
|
|
||||||
return isErrored && error instanceof Error ? (
|
return isErrored && error instanceof Error ? (
|
||||||
<ClientConfigError error={error} />
|
<AppFullScreenErrorFallback
|
||||||
|
error={error}
|
||||||
|
resetErrorBoundary={() => {
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
title="Unable to Reach Back-end"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
children
|
children
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,25 +1,43 @@
|
|||||||
|
import { AppErrorBoundaryEffect } from '@/error-handler/components/internal/AppErrorBoundaryEffect';
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import { ErrorInfo, ReactNode } from 'react';
|
import { ErrorInfo, ReactNode } from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
||||||
|
|
||||||
import { GenericErrorFallback } from '@/error-handler/components/GenericErrorFallback';
|
type AppErrorBoundaryProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
FallbackComponent: React.ComponentType<FallbackProps>;
|
||||||
|
resetOnLocationChange?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export const AppErrorBoundary = ({ children }: { children: ReactNode }) => {
|
export const AppErrorBoundary = ({
|
||||||
const handleError = (_error: Error, _info: ErrorInfo) => {
|
children,
|
||||||
Sentry.captureException(_error, (scope) => {
|
FallbackComponent,
|
||||||
scope.setExtras({ _info });
|
resetOnLocationChange = true,
|
||||||
|
}: AppErrorBoundaryProps) => {
|
||||||
|
const handleError = (error: Error, info: ErrorInfo) => {
|
||||||
|
Sentry.captureException(error, (scope) => {
|
||||||
|
scope.setExtras({ info });
|
||||||
return scope;
|
return scope;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Implement a better reset strategy, hard reload for now
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
FallbackComponent={GenericErrorFallback}
|
FallbackComponent={({ error, resetErrorBoundary }) => (
|
||||||
|
<>
|
||||||
|
{resetOnLocationChange && (
|
||||||
|
<AppErrorBoundaryEffect resetErrorBoundary={resetErrorBoundary} />
|
||||||
|
)}
|
||||||
|
<FallbackComponent
|
||||||
|
error={error}
|
||||||
|
resetErrorBoundary={resetErrorBoundary}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
onError={handleError}
|
onError={handleError}
|
||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { AppErrorDisplay } from '@/error-handler/components/internal/AppErrorDisplay';
|
||||||
|
import { AppErrorDisplayProps } from '@/error-handler/types/AppErrorDisplayProps';
|
||||||
|
import { PageBody } from '@/ui/layout/page/components/PageBody';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
type AppFullScreenErrorFallbackProps = AppErrorDisplayProps;
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
background: ${({ theme }) => theme.background.noisy};
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
padding-top: ${({ theme }) => theme.spacing(3)};
|
||||||
|
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const AppFullScreenErrorFallback = ({
|
||||||
|
error,
|
||||||
|
resetErrorBoundary,
|
||||||
|
title = 'Sorry, something went wrong',
|
||||||
|
}: AppFullScreenErrorFallbackProps) => {
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<PageBody>
|
||||||
|
<AppErrorDisplay
|
||||||
|
error={error}
|
||||||
|
resetErrorBoundary={resetErrorBoundary}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
</PageBody>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { AppErrorDisplay } from '@/error-handler/components/internal/AppErrorDisplay';
|
||||||
|
import { AppErrorDisplayProps } from '@/error-handler/types/AppErrorDisplayProps';
|
||||||
|
import { PageBody } from '@/ui/layout/page/components/PageBody';
|
||||||
|
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
|
||||||
|
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
|
||||||
|
|
||||||
|
type AppPageErrorFallbackProps = AppErrorDisplayProps;
|
||||||
|
|
||||||
|
export const AppPageErrorFallback = ({
|
||||||
|
error,
|
||||||
|
resetErrorBoundary,
|
||||||
|
title = 'Sorry, something went wrong',
|
||||||
|
}: AppPageErrorFallbackProps) => {
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<PageHeader />
|
||||||
|
|
||||||
|
<PageBody>
|
||||||
|
<AppErrorDisplay
|
||||||
|
error={error}
|
||||||
|
resetErrorBoundary={resetErrorBoundary}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
</PageBody>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,131 @@
|
|||||||
|
import { AppErrorDisplayProps } from '@/error-handler/types/AppErrorDisplayProps';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import LightNoise from '@ui/theme/assets/light-noise.png';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { GRAY_SCALE, IconReload } from 'twenty-ui';
|
||||||
|
|
||||||
|
type AppRootErrorFallbackProps = AppErrorDisplayProps;
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
background: url(${LightNoise.toString()});
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
padding: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledPanel = styled.div`
|
||||||
|
background: ${GRAY_SCALE.gray0};
|
||||||
|
border: 1px solid ${GRAY_SCALE.gray20};
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmptyContainer = styled(motion.div)`
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledImageContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledBackgroundImage = styled.img`
|
||||||
|
max-height: 160px;
|
||||||
|
max-width: 160px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledInnerImage = styled.img`
|
||||||
|
max-height: 130px;
|
||||||
|
position: absolute;
|
||||||
|
max-width: 130px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmptyTextContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmptyTitle = styled.div`
|
||||||
|
color: ${GRAY_SCALE.gray60};
|
||||||
|
font-size: 1.23rem;
|
||||||
|
font-weight: 600;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmptySubTitle = styled.div`
|
||||||
|
color: ${GRAY_SCALE.gray50};
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
max-height: 2.8em;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 50%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledButton = styled.button`
|
||||||
|
align-items: center;
|
||||||
|
background: ${GRAY_SCALE.gray0};
|
||||||
|
border: 1px solid ${GRAY_SCALE.gray20};
|
||||||
|
color: ${GRAY_SCALE.gray60};
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 16px;
|
||||||
|
padding: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIcon = styled(IconReload)`
|
||||||
|
color: ${GRAY_SCALE.gray60};
|
||||||
|
margin-right: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const AppRootErrorFallback = ({
|
||||||
|
error,
|
||||||
|
resetErrorBoundary,
|
||||||
|
title = 'Sorry, something went wrong',
|
||||||
|
}: AppRootErrorFallbackProps) => {
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledPanel>
|
||||||
|
<StyledEmptyContainer>
|
||||||
|
<StyledImageContainer>
|
||||||
|
<StyledBackgroundImage
|
||||||
|
src="/images/placeholders/background/error_index_bg.png"
|
||||||
|
alt="Background"
|
||||||
|
/>
|
||||||
|
<StyledInnerImage
|
||||||
|
src="/images/placeholders/moving-image/error_index.png"
|
||||||
|
alt="Inner"
|
||||||
|
/>
|
||||||
|
</StyledImageContainer>
|
||||||
|
<StyledEmptyTextContainer>
|
||||||
|
<StyledEmptyTitle>{title}</StyledEmptyTitle>
|
||||||
|
<StyledEmptySubTitle>{error.message}</StyledEmptySubTitle>
|
||||||
|
</StyledEmptyTextContainer>
|
||||||
|
<StyledButton onClick={resetErrorBoundary}>
|
||||||
|
<StyledIcon size={16} />
|
||||||
|
Reload
|
||||||
|
</StyledButton>
|
||||||
|
</StyledEmptyContainer>
|
||||||
|
</StyledPanel>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
import { MOBILE_VIEWPORT } from 'twenty-ui';
|
|
||||||
import { GenericErrorFallback } from './GenericErrorFallback';
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
|
||||||
background: ${({ theme }) => theme.background.noisy};
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
height: 100dvh;
|
|
||||||
width: 100%;
|
|
||||||
padding-top: ${({ theme }) => theme.spacing(3)};
|
|
||||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
|
||||||
padding-bottom: 0;
|
|
||||||
|
|
||||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
|
||||||
padding-left: 0;
|
|
||||||
padding-bottom: ${({ theme }) => theme.spacing(3)};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
type ClientConfigErrorProps = {
|
|
||||||
error: Error;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ClientConfigError = ({ error }: ClientConfigErrorProps) => {
|
|
||||||
// TODO: Implement a better loading strategy
|
|
||||||
const handleReset = () => {
|
|
||||||
window.location.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledContainer>
|
|
||||||
<GenericErrorFallback
|
|
||||||
error={error}
|
|
||||||
resetErrorBoundary={handleReset}
|
|
||||||
title="Unable to Reach Back-end"
|
|
||||||
hidePageHeader
|
|
||||||
/>
|
|
||||||
</StyledContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
import { PageBody } from '@/ui/layout/page/components/PageBody';
|
|
||||||
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
|
|
||||||
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { FallbackProps } from 'react-error-boundary';
|
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
AnimatedPlaceholder,
|
|
||||||
AnimatedPlaceholderEmptyContainer,
|
|
||||||
AnimatedPlaceholderEmptySubTitle,
|
|
||||||
AnimatedPlaceholderEmptyTextContainer,
|
|
||||||
AnimatedPlaceholderEmptyTitle,
|
|
||||||
Button,
|
|
||||||
IconRefresh,
|
|
||||||
} from 'twenty-ui';
|
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
|
||||||
|
|
||||||
type GenericErrorFallbackProps = FallbackProps & {
|
|
||||||
title?: string;
|
|
||||||
hidePageHeader?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GenericErrorFallback = ({
|
|
||||||
error,
|
|
||||||
resetErrorBoundary,
|
|
||||||
title = 'Sorry, something went wrong',
|
|
||||||
hidePageHeader = false,
|
|
||||||
}: GenericErrorFallbackProps) => {
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const [previousLocation] = useState(location);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isDeeplyEqual(previousLocation, location)) {
|
|
||||||
resetErrorBoundary();
|
|
||||||
}
|
|
||||||
}, [previousLocation, location, resetErrorBoundary]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer>
|
|
||||||
{!hidePageHeader && <PageHeader />}
|
|
||||||
|
|
||||||
<PageBody>
|
|
||||||
<AnimatedPlaceholderEmptyContainer>
|
|
||||||
<AnimatedPlaceholder type="errorIndex" />
|
|
||||||
<AnimatedPlaceholderEmptyTextContainer>
|
|
||||||
<AnimatedPlaceholderEmptyTitle>
|
|
||||||
{title}
|
|
||||||
</AnimatedPlaceholderEmptyTitle>
|
|
||||||
<AnimatedPlaceholderEmptySubTitle>
|
|
||||||
{error.message}
|
|
||||||
</AnimatedPlaceholderEmptySubTitle>
|
|
||||||
</AnimatedPlaceholderEmptyTextContainer>
|
|
||||||
<Button
|
|
||||||
Icon={IconRefresh}
|
|
||||||
title="Reload"
|
|
||||||
variant={'secondary'}
|
|
||||||
onClick={resetErrorBoundary}
|
|
||||||
/>
|
|
||||||
</AnimatedPlaceholderEmptyContainer>
|
|
||||||
</PageBody>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { FallbackProps } from 'react-error-boundary';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
|
||||||
|
type AppErrorBoundaryEffectProps = Pick<FallbackProps, 'resetErrorBoundary'>;
|
||||||
|
|
||||||
|
export const AppErrorBoundaryEffect = ({
|
||||||
|
resetErrorBoundary,
|
||||||
|
}: AppErrorBoundaryEffectProps) => {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const [previousLocation] = useState(location);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDeeplyEqual(previousLocation, location)) {
|
||||||
|
resetErrorBoundary();
|
||||||
|
}
|
||||||
|
}, [previousLocation, location, resetErrorBoundary]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { AppErrorDisplayProps } from '@/error-handler/types/AppErrorDisplayProps';
|
||||||
|
import {
|
||||||
|
AnimatedPlaceholder,
|
||||||
|
AnimatedPlaceholderEmptyContainer,
|
||||||
|
AnimatedPlaceholderEmptySubTitle,
|
||||||
|
AnimatedPlaceholderEmptyTextContainer,
|
||||||
|
AnimatedPlaceholderEmptyTitle,
|
||||||
|
Button,
|
||||||
|
IconRefresh,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
|
||||||
|
export const AppErrorDisplay = ({
|
||||||
|
error,
|
||||||
|
resetErrorBoundary,
|
||||||
|
title = 'Sorry, something went wrong',
|
||||||
|
}: AppErrorDisplayProps) => {
|
||||||
|
return (
|
||||||
|
<AnimatedPlaceholderEmptyContainer>
|
||||||
|
<AnimatedPlaceholder type="errorIndex" />
|
||||||
|
<AnimatedPlaceholderEmptyTextContainer>
|
||||||
|
<AnimatedPlaceholderEmptyTitle>{title}</AnimatedPlaceholderEmptyTitle>
|
||||||
|
<AnimatedPlaceholderEmptySubTitle>
|
||||||
|
{error.message}
|
||||||
|
</AnimatedPlaceholderEmptySubTitle>
|
||||||
|
</AnimatedPlaceholderEmptyTextContainer>
|
||||||
|
<Button
|
||||||
|
Icon={IconRefresh}
|
||||||
|
title="Reload"
|
||||||
|
variant={'secondary'}
|
||||||
|
onClick={resetErrorBoundary}
|
||||||
|
/>
|
||||||
|
</AnimatedPlaceholderEmptyContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { FallbackProps } from 'react-error-boundary';
|
||||||
|
|
||||||
|
export type AppErrorDisplayProps = FallbackProps & {
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import { AuthModal } from '@/auth/components/AuthModal';
|
import { AuthModal } from '@/auth/components/AuthModal';
|
||||||
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
|
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
|
||||||
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
|
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
|
||||||
|
import { AppFullScreenErrorFallback } from '@/error-handler/components/AppFullScreenErrorFallback';
|
||||||
|
import { AppPageErrorFallback } from '@/error-handler/components/AppPageErrorFallback';
|
||||||
import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu';
|
import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu';
|
||||||
import { AppNavigationDrawer } from '@/navigation/components/AppNavigationDrawer';
|
import { AppNavigationDrawer } from '@/navigation/components/AppNavigationDrawer';
|
||||||
import { MobileNavigationBar } from '@/navigation/components/MobileNavigationBar';
|
import { MobileNavigationBar } from '@/navigation/components/MobileNavigationBar';
|
||||||
@ -82,51 +84,52 @@ export const DefaultLayout = () => {
|
|||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
<StyledLayout>
|
<StyledLayout>
|
||||||
{!showAuthModal && (
|
<AppErrorBoundary FallbackComponent={AppFullScreenErrorFallback}>
|
||||||
<>
|
<StyledPageContainer
|
||||||
<CommandMenuRouter />
|
animate={{
|
||||||
<KeyboardShortcutMenu />
|
marginLeft:
|
||||||
</>
|
isSettingsPage && !isMobile && !useShowFullScreen
|
||||||
)}
|
? (windowsWidth -
|
||||||
|
(OBJECT_SETTINGS_WIDTH +
|
||||||
<StyledPageContainer
|
NAV_DRAWER_WIDTHS.menu.desktop.expanded +
|
||||||
animate={{
|
64)) /
|
||||||
marginLeft:
|
2
|
||||||
isSettingsPage && !isMobile && !useShowFullScreen
|
: 0,
|
||||||
? (windowsWidth -
|
}}
|
||||||
(OBJECT_SETTINGS_WIDTH +
|
transition={{ duration: theme.animation.duration.normal }}
|
||||||
NAV_DRAWER_WIDTHS.menu.desktop.expanded +
|
>
|
||||||
64)) /
|
{!showAuthModal && (
|
||||||
2
|
<>
|
||||||
: 0,
|
<CommandMenuRouter />
|
||||||
}}
|
<KeyboardShortcutMenu />
|
||||||
transition={{ duration: theme.animation.duration.normal }}
|
</>
|
||||||
>
|
)}
|
||||||
{showAuthModal ? (
|
{showAuthModal ? (
|
||||||
<StyledAppNavigationDrawerMock />
|
<StyledAppNavigationDrawerMock />
|
||||||
) : useShowFullScreen ? null : (
|
) : useShowFullScreen ? null : (
|
||||||
<StyledAppNavigationDrawer />
|
<StyledAppNavigationDrawer />
|
||||||
)}
|
)}
|
||||||
{showAuthModal ? (
|
{showAuthModal ? (
|
||||||
<>
|
<>
|
||||||
<SignInBackgroundMockPage />
|
<SignInBackgroundMockPage />
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<LayoutGroup>
|
<LayoutGroup>
|
||||||
<AuthModal>
|
<AuthModal>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</AuthModal>
|
</AuthModal>
|
||||||
</LayoutGroup>
|
</LayoutGroup>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<StyledMainContainer>
|
<StyledMainContainer>
|
||||||
<AppErrorBoundary>
|
<AppErrorBoundary FallbackComponent={AppPageErrorFallback}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</AppErrorBoundary>
|
</AppErrorBoundary>
|
||||||
</StyledMainContainer>
|
</StyledMainContainer>
|
||||||
)}
|
)}
|
||||||
</StyledPageContainer>
|
</StyledPageContainer>
|
||||||
{isMobile && <MobileNavigationBar />}
|
{isMobile && <MobileNavigationBar />}
|
||||||
|
</AppErrorBoundary>
|
||||||
</StyledLayout>
|
</StyledLayout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user