Display a generic fallback component when initial config load fails (#8588)
Fixes: #8487 #5027
1. Summary
The purpose of these changes is to elevate the dev/user experience when
the initial config load call fails for whatever reason by displaying a
fallback component.
2. Solution
I ended up making more changes than I initially planned. I had to update
the order of the contexts a bit because `GenericErrorFallback` is
dependent on `AppThemeProvider` for styling and `AppThemeProvider` is
dependent on `ObjectMetadataItemsProvider` for
[`useObjectMetadataItem`](ae2f193d68/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts (L22))
hook (`AppThemeProvider` -> `useColorScheme` -> `useUpdateOneRecord` ->
`useObjectMetadataItem`). I had to create a wrapper component for
`AppThemeProvider` and stylize it in a way that it looks responsive on
both mobile and desktop devices. Finally, I had to introduce the
`isErrored` flag to differentiate the loading and error states.
There are some improvements we can make later -
- Display a loading state for the initial config load
- Implement a refetch logic for the initial config loading failure
3. Recording
https://github.com/user-attachments/assets/c2f43573-8006-4118-8e18-8576099d78fd
https://github.com/user-attachments/assets/9c5853d3-539b-4880-aa38-c416c3e13594
---------
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@ -3,7 +3,6 @@ import styled from '@emotion/styled';
|
|||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { ChangeEvent, useRef } from 'react';
|
import { ChangeEvent, useRef } from 'react';
|
||||||
|
|
||||||
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
|
|
||||||
import { Button } from 'twenty-ui';
|
import { Button } from 'twenty-ui';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
@ -90,33 +89,26 @@ export const FileBlock = createReactBlockSpec(
|
|||||||
|
|
||||||
if (isNonEmptyString(block.props.url)) {
|
if (isNonEmptyString(block.props.url)) {
|
||||||
return (
|
return (
|
||||||
<AppThemeProvider>
|
<StyledFileLine>
|
||||||
<StyledFileLine>
|
<AttachmentIcon
|
||||||
<AttachmentIcon
|
attachmentType={block.props.fileType as AttachmentType}
|
||||||
attachmentType={block.props.fileType as AttachmentType}
|
></AttachmentIcon>
|
||||||
></AttachmentIcon>
|
<StyledLink href={block.props.url} target="__blank">
|
||||||
<StyledLink href={block.props.url} target="__blank">
|
{block.props.name}
|
||||||
{block.props.name}
|
</StyledLink>
|
||||||
</StyledLink>
|
</StyledFileLine>
|
||||||
</StyledFileLine>
|
|
||||||
</AppThemeProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppThemeProvider>
|
<StyledUploadFileContainer>
|
||||||
<StyledUploadFileContainer>
|
<StyledFileInput
|
||||||
<StyledFileInput
|
ref={inputFileRef}
|
||||||
ref={inputFileRef}
|
onChange={handleFileChange}
|
||||||
onChange={handleFileChange}
|
type="file"
|
||||||
type="file"
|
/>
|
||||||
/>
|
<Button onClick={handleUploadFileClick} title="Upload File"></Button>
|
||||||
<Button
|
</StyledUploadFileContainer>
|
||||||
onClick={handleUploadFileClick}
|
|
||||||
title="Upload File"
|
|
||||||
></Button>
|
|
||||||
</StyledUploadFileContainer>
|
|
||||||
</AppThemeProvider>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -13,7 +13,8 @@ import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider
|
|||||||
import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager';
|
import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager';
|
||||||
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
|
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
|
||||||
import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider';
|
import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider';
|
||||||
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
|
import { UserThemeProviderEffect } from '@/ui/theme/components/AppThemeProvider';
|
||||||
|
import { BaseThemeProvider } from '@/ui/theme/components/BaseThemeProvider';
|
||||||
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
|
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
|
||||||
import { UserProvider } from '@/users/components/UserProvider';
|
import { UserProvider } from '@/users/components/UserProvider';
|
||||||
import { UserProviderEffect } from '@/users/components/UserProviderEffect';
|
import { UserProviderEffect } from '@/users/components/UserProviderEffect';
|
||||||
@ -27,17 +28,18 @@ export const AppRouterProviders = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ApolloProvider>
|
<ApolloProvider>
|
||||||
<ClientConfigProviderEffect />
|
<BaseThemeProvider>
|
||||||
<ClientConfigProvider>
|
<ClientConfigProviderEffect />
|
||||||
<ChromeExtensionSidecarEffect />
|
<ClientConfigProvider>
|
||||||
<ChromeExtensionSidecarProvider>
|
<ChromeExtensionSidecarEffect />
|
||||||
<UserProviderEffect />
|
<ChromeExtensionSidecarProvider>
|
||||||
<UserProvider>
|
<UserProviderEffect />
|
||||||
<AuthProvider>
|
<UserProvider>
|
||||||
<ApolloMetadataClientProvider>
|
<AuthProvider>
|
||||||
<ObjectMetadataItemsProvider>
|
<ApolloMetadataClientProvider>
|
||||||
<PrefetchDataProvider>
|
<ObjectMetadataItemsProvider>
|
||||||
<AppThemeProvider>
|
<PrefetchDataProvider>
|
||||||
|
<UserThemeProviderEffect />
|
||||||
<SnackBarProvider>
|
<SnackBarProvider>
|
||||||
<DialogManagerScope dialogManagerScopeId="dialog-manager">
|
<DialogManagerScope dialogManagerScopeId="dialog-manager">
|
||||||
<DialogManager>
|
<DialogManager>
|
||||||
@ -50,15 +52,15 @@ export const AppRouterProviders = () => {
|
|||||||
</DialogManager>
|
</DialogManager>
|
||||||
</DialogManagerScope>
|
</DialogManagerScope>
|
||||||
</SnackBarProvider>
|
</SnackBarProvider>
|
||||||
</AppThemeProvider>
|
</PrefetchDataProvider>
|
||||||
</PrefetchDataProvider>
|
<PageChangeEffect />
|
||||||
<PageChangeEffect />
|
</ObjectMetadataItemsProvider>
|
||||||
</ObjectMetadataItemsProvider>
|
</ApolloMetadataClientProvider>
|
||||||
</ApolloMetadataClientProvider>
|
</AuthProvider>
|
||||||
</AuthProvider>
|
</UserProvider>
|
||||||
</UserProvider>
|
</ChromeExtensionSidecarProvider>
|
||||||
</ChromeExtensionSidecarProvider>
|
</ClientConfigProvider>
|
||||||
</ClientConfigProvider>
|
</BaseThemeProvider>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import { workspacesState } from '@/auth/states/workspaces';
|
|||||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||||
import { billingState } from '@/client-config/states/billingState';
|
import { billingState } from '@/client-config/states/billingState';
|
||||||
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
|
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
|
||||||
import { isClientConfigLoadedState } from '@/client-config/states/isClientConfigLoadedState';
|
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||||
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
||||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||||
@ -229,8 +229,8 @@ export const useAuth = () => {
|
|||||||
const captchaProvider = snapshot
|
const captchaProvider = snapshot
|
||||||
.getLoadable(captchaProviderState)
|
.getLoadable(captchaProviderState)
|
||||||
.getValue();
|
.getValue();
|
||||||
const isClientConfigLoaded = snapshot
|
const clientConfigApiStatus = snapshot
|
||||||
.getLoadable(isClientConfigLoadedState)
|
.getLoadable(clientConfigApiStatusState)
|
||||||
.getValue();
|
.getValue();
|
||||||
const isCurrentUserLoaded = snapshot
|
const isCurrentUserLoaded = snapshot
|
||||||
.getLoadable(isCurrentUserLoadedState)
|
.getLoadable(isCurrentUserLoadedState)
|
||||||
@ -244,7 +244,7 @@ export const useAuth = () => {
|
|||||||
set(supportChatState, supportChat);
|
set(supportChatState, supportChat);
|
||||||
set(isDebugModeState, isDebugMode);
|
set(isDebugModeState, isDebugMode);
|
||||||
set(captchaProviderState, captchaProvider);
|
set(captchaProviderState, captchaProvider);
|
||||||
set(isClientConfigLoadedState, isClientConfigLoaded);
|
set(clientConfigApiStatusState, clientConfigApiStatus);
|
||||||
set(isCurrentUserLoadedState, isCurrentUserLoaded);
|
set(isCurrentUserLoadedState, isCurrentUserLoaded);
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,11 +1,21 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { isClientConfigLoadedState } from '@/client-config/states/isClientConfigLoadedState';
|
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||||
|
import { ClientConfigError } from '@/error-handler/components/ClientConfigError';
|
||||||
|
|
||||||
export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const isClientConfigLoaded = useRecoilValue(isClientConfigLoadedState);
|
const { isLoaded, isErrored, error } = useRecoilValue(
|
||||||
|
clientConfigApiStatusState,
|
||||||
|
);
|
||||||
|
|
||||||
return isClientConfigLoaded ? <>{children}</> : <></>;
|
// TODO: Implement a better loading strategy
|
||||||
|
if (!isLoaded) return null;
|
||||||
|
|
||||||
|
return isErrored && error instanceof Error ? (
|
||||||
|
<ClientConfigError error={error} />
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { authProvidersState } from '@/client-config/states/authProvidersState';
|
|||||||
import { billingState } from '@/client-config/states/billingState';
|
import { billingState } from '@/client-config/states/billingState';
|
||||||
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
|
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
|
||||||
import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState';
|
import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState';
|
||||||
|
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||||
import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
|
import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
|
||||||
import { isClientConfigLoadedState } from '@/client-config/states/isClientConfigLoadedState';
|
|
||||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||||
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
||||||
import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState';
|
import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState';
|
||||||
@ -27,8 +27,8 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
const setSupportChat = useSetRecoilState(supportChatState);
|
const setSupportChat = useSetRecoilState(supportChatState);
|
||||||
|
|
||||||
const setSentryConfig = useSetRecoilState(sentryConfigState);
|
const setSentryConfig = useSetRecoilState(sentryConfigState);
|
||||||
const [isClientConfigLoaded, setIsClientConfigLoaded] = useRecoilState(
|
const [clientConfigApiStatus, setClientConfigApiStatus] = useRecoilState(
|
||||||
isClientConfigLoadedState,
|
clientConfigApiStatusState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const setCaptchaProvider = useSetRecoilState(captchaProviderState);
|
const setCaptchaProvider = useSetRecoilState(captchaProviderState);
|
||||||
@ -37,42 +37,64 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
|
|
||||||
const setApiConfig = useSetRecoilState(apiConfigState);
|
const setApiConfig = useSetRecoilState(apiConfigState);
|
||||||
|
|
||||||
const { data, loading } = useGetClientConfigQuery({
|
const { data, loading, error } = useGetClientConfigQuery({
|
||||||
skip: isClientConfigLoaded,
|
skip: clientConfigApiStatus.isLoaded,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && isDefined(data?.clientConfig)) {
|
if (loading) return;
|
||||||
setIsClientConfigLoaded(true);
|
setClientConfigApiStatus((currentStatus) => ({
|
||||||
setAuthProviders({
|
...currentStatus,
|
||||||
google: data?.clientConfig.authProviders.google,
|
isLoaded: true,
|
||||||
microsoft: data?.clientConfig.authProviders.microsoft,
|
}));
|
||||||
password: data?.clientConfig.authProviders.password,
|
|
||||||
magicLink: false,
|
|
||||||
sso: data?.clientConfig.authProviders.sso,
|
|
||||||
});
|
|
||||||
setIsDebugMode(data?.clientConfig.debugMode);
|
|
||||||
setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled);
|
|
||||||
setIsSignInPrefilled(data?.clientConfig.signInPrefilled);
|
|
||||||
setIsSignUpDisabled(data?.clientConfig.signUpDisabled);
|
|
||||||
|
|
||||||
setBilling(data?.clientConfig.billing);
|
if (error instanceof Error) {
|
||||||
setSupportChat(data?.clientConfig.support);
|
setClientConfigApiStatus((currentStatus) => ({
|
||||||
|
...currentStatus,
|
||||||
setSentryConfig({
|
isErrored: true,
|
||||||
dsn: data?.clientConfig?.sentry?.dsn,
|
error,
|
||||||
release: data?.clientConfig?.sentry?.release,
|
}));
|
||||||
environment: data?.clientConfig?.sentry?.environment,
|
return;
|
||||||
});
|
|
||||||
|
|
||||||
setCaptchaProvider({
|
|
||||||
provider: data?.clientConfig?.captcha?.provider,
|
|
||||||
siteKey: data?.clientConfig?.captcha?.siteKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
setChromeExtensionId(data?.clientConfig?.chromeExtensionId);
|
|
||||||
setApiConfig(data?.clientConfig?.api);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isDefined(data?.clientConfig)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setClientConfigApiStatus((currentStatus) => ({
|
||||||
|
...currentStatus,
|
||||||
|
isErrored: false,
|
||||||
|
error: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setAuthProviders({
|
||||||
|
google: data?.clientConfig.authProviders.google,
|
||||||
|
microsoft: data?.clientConfig.authProviders.microsoft,
|
||||||
|
password: data?.clientConfig.authProviders.password,
|
||||||
|
magicLink: false,
|
||||||
|
sso: data?.clientConfig.authProviders.sso,
|
||||||
|
});
|
||||||
|
setIsDebugMode(data?.clientConfig.debugMode);
|
||||||
|
setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled);
|
||||||
|
setIsSignInPrefilled(data?.clientConfig.signInPrefilled);
|
||||||
|
setIsSignUpDisabled(data?.clientConfig.signUpDisabled);
|
||||||
|
|
||||||
|
setBilling(data?.clientConfig.billing);
|
||||||
|
setSupportChat(data?.clientConfig.support);
|
||||||
|
|
||||||
|
setSentryConfig({
|
||||||
|
dsn: data?.clientConfig?.sentry?.dsn,
|
||||||
|
release: data?.clientConfig?.sentry?.release,
|
||||||
|
environment: data?.clientConfig?.sentry?.environment,
|
||||||
|
});
|
||||||
|
|
||||||
|
setCaptchaProvider({
|
||||||
|
provider: data?.clientConfig?.captcha?.provider,
|
||||||
|
siteKey: data?.clientConfig?.captcha?.siteKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
setChromeExtensionId(data?.clientConfig?.chromeExtensionId);
|
||||||
|
setApiConfig(data?.clientConfig?.api);
|
||||||
}, [
|
}, [
|
||||||
data,
|
data,
|
||||||
setAuthProviders,
|
setAuthProviders,
|
||||||
@ -83,11 +105,12 @@ export const ClientConfigProviderEffect = () => {
|
|||||||
setBilling,
|
setBilling,
|
||||||
setSentryConfig,
|
setSentryConfig,
|
||||||
loading,
|
loading,
|
||||||
setIsClientConfigLoaded,
|
setClientConfigApiStatus,
|
||||||
setCaptchaProvider,
|
setCaptchaProvider,
|
||||||
setChromeExtensionId,
|
setChromeExtensionId,
|
||||||
setApiConfig,
|
setApiConfig,
|
||||||
setIsAnalyticsEnabled,
|
setIsAnalyticsEnabled,
|
||||||
|
error,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
type ClientConfigApiStatus = {
|
||||||
|
isLoaded: boolean;
|
||||||
|
isErrored: boolean;
|
||||||
|
error?: Error;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clientConfigApiStatusState = createState<ClientConfigApiStatus>({
|
||||||
|
key: 'clientConfigApiStatus',
|
||||||
|
defaultValue: { isLoaded: false, isErrored: false, error: undefined },
|
||||||
|
});
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { createState } from 'twenty-ui';
|
|
||||||
|
|
||||||
export const isClientConfigLoadedState = createState<boolean>({
|
|
||||||
key: 'isClientConfigLoadedState',
|
|
||||||
defaultValue: false,
|
|
||||||
});
|
|
||||||
@ -12,10 +12,16 @@ export const AppErrorBoundary = ({ children }: { children: ReactNode }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Implement a better reset strategy, hard reload for now
|
||||||
|
const handleReset = () => {
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
FallbackComponent={GenericErrorFallback}
|
FallbackComponent={GenericErrorFallback}
|
||||||
onError={handleError}
|
onError={handleError}
|
||||||
|
onReset={handleReset}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -15,11 +15,16 @@ import {
|
|||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
|
||||||
type GenericErrorFallbackProps = FallbackProps;
|
type GenericErrorFallbackProps = FallbackProps & {
|
||||||
|
title?: string;
|
||||||
|
hidePageHeader?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export const GenericErrorFallback = ({
|
export const GenericErrorFallback = ({
|
||||||
error,
|
error,
|
||||||
resetErrorBoundary,
|
resetErrorBoundary,
|
||||||
|
title = 'Sorry, something went wrong',
|
||||||
|
hidePageHeader = false,
|
||||||
}: GenericErrorFallbackProps) => {
|
}: GenericErrorFallbackProps) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@ -33,13 +38,14 @@ export const GenericErrorFallback = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<PageHeader />
|
{!hidePageHeader && <PageHeader />}
|
||||||
|
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<AnimatedPlaceholderEmptyContainer>
|
<AnimatedPlaceholderEmptyContainer>
|
||||||
<AnimatedPlaceholder type="errorIndex" />
|
<AnimatedPlaceholder type="errorIndex" />
|
||||||
<AnimatedPlaceholderEmptyTextContainer>
|
<AnimatedPlaceholderEmptyTextContainer>
|
||||||
<AnimatedPlaceholderEmptyTitle>
|
<AnimatedPlaceholderEmptyTitle>
|
||||||
Server’s on a coffee break
|
{title}
|
||||||
</AnimatedPlaceholderEmptyTitle>
|
</AnimatedPlaceholderEmptyTitle>
|
||||||
<AnimatedPlaceholderEmptySubTitle>
|
<AnimatedPlaceholderEmptySubTitle>
|
||||||
{error.message}
|
{error.message}
|
||||||
@ -49,7 +55,7 @@ export const GenericErrorFallback = ({
|
|||||||
Icon={IconRefresh}
|
Icon={IconRefresh}
|
||||||
title="Reload"
|
title="Reload"
|
||||||
variant={'secondary'}
|
variant={'secondary'}
|
||||||
onClick={() => resetErrorBoundary()}
|
onClick={resetErrorBoundary}
|
||||||
/>
|
/>
|
||||||
</AnimatedPlaceholderEmptyContainer>
|
</AnimatedPlaceholderEmptyContainer>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
|
|||||||
@ -1,32 +1,17 @@
|
|||||||
import { useEffect } from 'react';
|
import { useContext, useEffect } from 'react';
|
||||||
import { ThemeProvider } from '@emotion/react';
|
|
||||||
import { THEME_DARK, THEME_LIGHT, ThemeContextProvider } from 'twenty-ui';
|
|
||||||
|
|
||||||
|
import { ThemeSchemeContext } from '@/ui/theme/components/BaseThemeProvider';
|
||||||
|
import { useSystemColorScheme } from '@/ui/theme/hooks/useSystemColorScheme';
|
||||||
import { useColorScheme } from '../hooks/useColorScheme';
|
import { useColorScheme } from '../hooks/useColorScheme';
|
||||||
import { useSystemColorScheme } from '../hooks/useSystemColorScheme';
|
|
||||||
|
|
||||||
type AppThemeProviderProps = {
|
|
||||||
children: JSX.Element;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AppThemeProvider = ({ children }: AppThemeProviderProps) => {
|
|
||||||
const systemColorScheme = useSystemColorScheme();
|
|
||||||
|
|
||||||
|
export const UserThemeProviderEffect = () => {
|
||||||
const { colorScheme } = useColorScheme();
|
const { colorScheme } = useColorScheme();
|
||||||
|
const systemColorScheme = useSystemColorScheme();
|
||||||
const computedColorScheme =
|
const setThemeScheme = useContext(ThemeSchemeContext);
|
||||||
colorScheme === 'System' ? systemColorScheme : colorScheme;
|
|
||||||
|
|
||||||
const theme = computedColorScheme === 'Dark' ? THEME_DARK : THEME_LIGHT;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.className =
|
setThemeScheme(colorScheme === 'System' ? systemColorScheme : colorScheme);
|
||||||
theme.name === 'dark' ? 'dark' : 'light';
|
}, [colorScheme, setThemeScheme, systemColorScheme]);
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
return (
|
return <></>;
|
||||||
<ThemeProvider theme={theme}>
|
|
||||||
<ThemeContextProvider theme={theme}>{children}</ThemeContextProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { ThemeProvider } from '@emotion/react';
|
||||||
|
import { createContext, useState } from 'react';
|
||||||
|
import {
|
||||||
|
ColorScheme,
|
||||||
|
THEME_DARK,
|
||||||
|
THEME_LIGHT,
|
||||||
|
ThemeContextProvider,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
|
||||||
|
import { useSystemColorScheme } from '../hooks/useSystemColorScheme';
|
||||||
|
|
||||||
|
type BaseThemeProviderProps = {
|
||||||
|
children: JSX.Element | JSX.Element[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ThemeSchemeContext = createContext<(theme: ColorScheme) => void>(
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const BaseThemeProvider = ({ children }: BaseThemeProviderProps) => {
|
||||||
|
const systemColorScheme = useSystemColorScheme();
|
||||||
|
const [themeScheme, setThemeScheme] = useState(systemColorScheme);
|
||||||
|
|
||||||
|
document.documentElement.className =
|
||||||
|
themeScheme === 'Dark' ? 'dark' : 'light';
|
||||||
|
|
||||||
|
const theme = themeScheme === 'Dark' ? THEME_DARK : THEME_LIGHT;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeSchemeContext.Provider value={setThemeScheme}>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<ThemeContextProvider theme={theme}>{children}</ThemeContextProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</ThemeSchemeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user