diff --git a/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx b/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx index 5c8de03869d5..685fdc0c8bb6 100644 --- a/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx +++ b/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx @@ -3,7 +3,6 @@ import styled from '@emotion/styled'; import { isNonEmptyString } from '@sniptt/guards'; import { ChangeEvent, useRef } from 'react'; -import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; import { Button } from 'twenty-ui'; import { isDefined } from '~/utils/isDefined'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; @@ -90,33 +89,26 @@ export const FileBlock = createReactBlockSpec( if (isNonEmptyString(block.props.url)) { return ( - - - - - {block.props.name} - - - + + + + {block.props.name} + + ); } return ( - - - - - - + + + + ); }, }, diff --git a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx index 7f64849a27e0..1fe08501ab8e 100644 --- a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx +++ b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx @@ -13,7 +13,8 @@ import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager'; import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; 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 { UserProvider } from '@/users/components/UserProvider'; import { UserProviderEffect } from '@/users/components/UserProviderEffect'; @@ -27,17 +28,18 @@ export const AppRouterProviders = () => { return ( - - - - - - - - - - - + + + + + + + + + + + + @@ -50,15 +52,15 @@ export const AppRouterProviders = () => { - - - - - - - - - + + + + + + + + + ); }; diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index ae13d831fb7a..e8ef0aab3606 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -17,7 +17,7 @@ import { workspacesState } from '@/auth/states/workspaces'; import { authProvidersState } from '@/client-config/states/authProvidersState'; import { billingState } from '@/client-config/states/billingState'; 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 { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState'; import { supportChatState } from '@/client-config/states/supportChatState'; @@ -229,8 +229,8 @@ export const useAuth = () => { const captchaProvider = snapshot .getLoadable(captchaProviderState) .getValue(); - const isClientConfigLoaded = snapshot - .getLoadable(isClientConfigLoadedState) + const clientConfigApiStatus = snapshot + .getLoadable(clientConfigApiStatusState) .getValue(); const isCurrentUserLoaded = snapshot .getLoadable(isCurrentUserLoadedState) @@ -244,7 +244,7 @@ export const useAuth = () => { set(supportChatState, supportChat); set(isDebugModeState, isDebugMode); set(captchaProviderState, captchaProvider); - set(isClientConfigLoadedState, isClientConfigLoaded); + set(clientConfigApiStatusState, clientConfigApiStatus); set(isCurrentUserLoadedState, isCurrentUserLoaded); return undefined; }); diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx index 671842687435..d363c191eb56 100644 --- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx +++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx @@ -1,11 +1,21 @@ 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 = ({ 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 ? ( + + ) : ( + children + ); }; diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx index bf11d6713c2c..c0fcbda1f50b 100644 --- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx +++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx @@ -3,8 +3,8 @@ import { authProvidersState } from '@/client-config/states/authProvidersState'; import { billingState } from '@/client-config/states/billingState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState'; import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState'; +import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState'; import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState'; -import { isClientConfigLoadedState } from '@/client-config/states/isClientConfigLoadedState'; import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState'; import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState'; @@ -27,8 +27,8 @@ export const ClientConfigProviderEffect = () => { const setSupportChat = useSetRecoilState(supportChatState); const setSentryConfig = useSetRecoilState(sentryConfigState); - const [isClientConfigLoaded, setIsClientConfigLoaded] = useRecoilState( - isClientConfigLoadedState, + const [clientConfigApiStatus, setClientConfigApiStatus] = useRecoilState( + clientConfigApiStatusState, ); const setCaptchaProvider = useSetRecoilState(captchaProviderState); @@ -37,42 +37,64 @@ export const ClientConfigProviderEffect = () => { const setApiConfig = useSetRecoilState(apiConfigState); - const { data, loading } = useGetClientConfigQuery({ - skip: isClientConfigLoaded, + const { data, loading, error } = useGetClientConfigQuery({ + skip: clientConfigApiStatus.isLoaded, }); useEffect(() => { - if (!loading && isDefined(data?.clientConfig)) { - setIsClientConfigLoaded(true); - 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); + if (loading) return; + setClientConfigApiStatus((currentStatus) => ({ + ...currentStatus, + isLoaded: true, + })); + + if (error instanceof Error) { + setClientConfigApiStatus((currentStatus) => ({ + ...currentStatus, + isErrored: true, + error, + })); + return; + } + + 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, setAuthProviders, @@ -83,11 +105,12 @@ export const ClientConfigProviderEffect = () => { setBilling, setSentryConfig, loading, - setIsClientConfigLoaded, + setClientConfigApiStatus, setCaptchaProvider, setChromeExtensionId, setApiConfig, setIsAnalyticsEnabled, + error, ]); return <>; diff --git a/packages/twenty-front/src/modules/client-config/states/clientConfigApiStatusState.ts b/packages/twenty-front/src/modules/client-config/states/clientConfigApiStatusState.ts new file mode 100644 index 000000000000..08793188ff1a --- /dev/null +++ b/packages/twenty-front/src/modules/client-config/states/clientConfigApiStatusState.ts @@ -0,0 +1,12 @@ +import { createState } from 'twenty-ui'; + +type ClientConfigApiStatus = { + isLoaded: boolean; + isErrored: boolean; + error?: Error; +}; + +export const clientConfigApiStatusState = createState({ + key: 'clientConfigApiStatus', + defaultValue: { isLoaded: false, isErrored: false, error: undefined }, +}); diff --git a/packages/twenty-front/src/modules/client-config/states/isClientConfigLoadedState.ts b/packages/twenty-front/src/modules/client-config/states/isClientConfigLoadedState.ts deleted file mode 100644 index 7b6cff0b3612..000000000000 --- a/packages/twenty-front/src/modules/client-config/states/isClientConfigLoadedState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from 'twenty-ui'; - -export const isClientConfigLoadedState = createState({ - key: 'isClientConfigLoadedState', - defaultValue: false, -}); diff --git a/packages/twenty-front/src/modules/error-handler/components/AppErrorBoundary.tsx b/packages/twenty-front/src/modules/error-handler/components/AppErrorBoundary.tsx index fc6c822441c3..a00f61fa6863 100644 --- a/packages/twenty-front/src/modules/error-handler/components/AppErrorBoundary.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/AppErrorBoundary.tsx @@ -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 ( {children} diff --git a/packages/twenty-front/src/modules/error-handler/components/ClientConfigError.tsx b/packages/twenty-front/src/modules/error-handler/components/ClientConfigError.tsx new file mode 100644 index 000000000000..47141a532063 --- /dev/null +++ b/packages/twenty-front/src/modules/error-handler/components/ClientConfigError.tsx @@ -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 ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/error-handler/components/GenericErrorFallback.tsx b/packages/twenty-front/src/modules/error-handler/components/GenericErrorFallback.tsx index 8b3c647054bc..2d2e8efeb165 100644 --- a/packages/twenty-front/src/modules/error-handler/components/GenericErrorFallback.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/GenericErrorFallback.tsx @@ -15,11 +15,16 @@ import { } from 'twenty-ui'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; -type GenericErrorFallbackProps = FallbackProps; +type GenericErrorFallbackProps = FallbackProps & { + title?: string; + hidePageHeader?: boolean; +}; export const GenericErrorFallback = ({ error, resetErrorBoundary, + title = 'Sorry, something went wrong', + hidePageHeader = false, }: GenericErrorFallbackProps) => { const location = useLocation(); @@ -33,13 +38,14 @@ export const GenericErrorFallback = ({ return ( - + {!hidePageHeader && } + - Server’s on a coffee break + {title} {error.message} @@ -49,7 +55,7 @@ export const GenericErrorFallback = ({ Icon={IconRefresh} title="Reload" variant={'secondary'} - onClick={() => resetErrorBoundary()} + onClick={resetErrorBoundary} /> diff --git a/packages/twenty-front/src/modules/ui/theme/components/AppThemeProvider.tsx b/packages/twenty-front/src/modules/ui/theme/components/AppThemeProvider.tsx index abfbe9e20668..3b9e09b25f88 100644 --- a/packages/twenty-front/src/modules/ui/theme/components/AppThemeProvider.tsx +++ b/packages/twenty-front/src/modules/ui/theme/components/AppThemeProvider.tsx @@ -1,32 +1,17 @@ -import { useEffect } from 'react'; -import { ThemeProvider } from '@emotion/react'; -import { THEME_DARK, THEME_LIGHT, ThemeContextProvider } from 'twenty-ui'; +import { useContext, useEffect } from 'react'; +import { ThemeSchemeContext } from '@/ui/theme/components/BaseThemeProvider'; +import { useSystemColorScheme } from '@/ui/theme/hooks/useSystemColorScheme'; 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 computedColorScheme = - colorScheme === 'System' ? systemColorScheme : colorScheme; - - const theme = computedColorScheme === 'Dark' ? THEME_DARK : THEME_LIGHT; + const systemColorScheme = useSystemColorScheme(); + const setThemeScheme = useContext(ThemeSchemeContext); useEffect(() => { - document.documentElement.className = - theme.name === 'dark' ? 'dark' : 'light'; - }, [theme]); + setThemeScheme(colorScheme === 'System' ? systemColorScheme : colorScheme); + }, [colorScheme, setThemeScheme, systemColorScheme]); - return ( - - {children} - - ); + return <>; }; diff --git a/packages/twenty-front/src/modules/ui/theme/components/BaseThemeProvider.tsx b/packages/twenty-front/src/modules/ui/theme/components/BaseThemeProvider.tsx new file mode 100644 index 000000000000..99962eaf5bc0 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/theme/components/BaseThemeProvider.tsx @@ -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 ( + + + {children} + + + ); +}; diff --git a/packages/twenty-ui/src/display/index.ts b/packages/twenty-ui/src/display/index.ts index 45144332b2bf..e8f248ba7a31 100644 --- a/packages/twenty-ui/src/display/index.ts +++ b/packages/twenty-ui/src/display/index.ts @@ -49,8 +49,8 @@ export * from './icon/types/IconComponent'; export * from './info/components/Info'; export * from './status/components/Status'; export * from './tag/components/Tag'; -export * from './text/components/SeparatorLineText'; export * from './text/components/HorizontalSeparator'; +export * from './text/components/SeparatorLineText'; export * from './tooltip/AppTooltip'; export * from './tooltip/OverflowingTextWithTooltip'; export * from './typography/components/H1Title';