Fix race condition while loading metadata on sign in (#9027)

This commit is contained in:
Charles Bochet 2024-12-11 18:56:02 +01:00 committed by GitHub
parent 183fd877c4
commit 90c26643a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 89 additions and 33 deletions

View File

@ -8,6 +8,7 @@ import { ClientConfigProvider } from '@/client-config/components/ClientConfigPro
import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect'; import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect'; import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider'; import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider';
import { ObjectMetadataItemsGater } from '@/object-metadata/components/ObjectMetadataItemsGater';
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider'; import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager'; import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager';
@ -15,6 +16,7 @@ import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogMa
import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider'; import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider';
import { UserThemeProviderEffect } from '@/ui/theme/components/AppThemeProvider'; import { UserThemeProviderEffect } from '@/ui/theme/components/AppThemeProvider';
import { BaseThemeProvider } from '@/ui/theme/components/BaseThemeProvider'; import { BaseThemeProvider } from '@/ui/theme/components/BaseThemeProvider';
import { PageFavicon } from '@/ui/utilities/page-favicon/components/PageFavicon';
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';
@ -22,7 +24,6 @@ import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProvide
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { Outlet, useLocation } from 'react-router-dom'; import { Outlet, useLocation } from 'react-router-dom';
import { getPageTitleFromPath } from '~/utils/title-utils'; import { getPageTitleFromPath } from '~/utils/title-utils';
import { PageFavicon } from '@/ui/utilities/page-favicon/components/PageFavicon';
export const AppRouterProviders = () => { export const AppRouterProviders = () => {
const { pathname } = useLocation(); const { pathname } = useLocation();
@ -41,22 +42,24 @@ export const AppRouterProviders = () => {
<AuthProvider> <AuthProvider>
<ApolloMetadataClientProvider> <ApolloMetadataClientProvider>
<ObjectMetadataItemsProvider> <ObjectMetadataItemsProvider>
<PrefetchDataProvider> <ObjectMetadataItemsGater>
<UserThemeProviderEffect /> <PrefetchDataProvider>
<SnackBarProvider> <UserThemeProviderEffect />
<DialogManagerScope dialogManagerScopeId="dialog-manager"> <SnackBarProvider>
<DialogManager> <DialogManagerScope dialogManagerScopeId="dialog-manager">
<StrictMode> <DialogManager>
<PromiseRejectionEffect /> <StrictMode>
<GotoHotkeysEffectsProvider /> <PromiseRejectionEffect />
<PageTitle title={pageTitle} /> <GotoHotkeysEffectsProvider />
<PageFavicon /> <PageTitle title={pageTitle} />
<Outlet /> <PageFavicon />
</StrictMode> <Outlet />
</DialogManager> </StrictMode>
</DialogManagerScope> </DialogManager>
</SnackBarProvider> </DialogManagerScope>
</PrefetchDataProvider> </SnackBarProvider>
</PrefetchDataProvider>
</ObjectMetadataItemsGater>
<PageChangeEffect /> <PageChangeEffect />
</ObjectMetadataItemsProvider> </ObjectMetadataItemsProvider>
</ApolloMetadataClientProvider> </ApolloMetadataClientProvider>

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { import {
setSessionId, setSessionId,
@ -8,12 +8,14 @@ import {
} from '@/analytics/hooks/useEventTracker'; } from '@/analytics/hooks/useEventTracker';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken'; import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState'; import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { AppBasePath } from '@/types/AppBasePath'; import { AppBasePath } from '@/types/AppBasePath';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useDebouncedCallback } from 'use-debounce';
import { useCleanRecoilState } from '~/hooks/useCleanRecoilState'; import { useCleanRecoilState } from '~/hooks/useCleanRecoilState';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation'; import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
@ -50,11 +52,27 @@ export const PageChangeEffect = () => {
} }
}, [location, previousLocation]); }, [location, previousLocation]);
const setIsAppWaitingForFreshObjectMetadata = useSetRecoilState(
isAppWaitingForFreshObjectMetadataState,
);
const setIsAppWaitingForFreshObjectMetadataDebounced = useDebouncedCallback(
() => {
setIsAppWaitingForFreshObjectMetadata(false);
},
100,
);
useEffect(() => { useEffect(() => {
if (isDefined(pageChangeEffectNavigateLocation)) { if (isDefined(pageChangeEffectNavigateLocation)) {
navigate(pageChangeEffectNavigateLocation); navigate(pageChangeEffectNavigateLocation);
setIsAppWaitingForFreshObjectMetadataDebounced();
} }
}, [navigate, pageChangeEffectNavigateLocation]); }, [
navigate,
pageChangeEffectNavigateLocation,
setIsAppWaitingForFreshObjectMetadataDebounced,
]);
useEffect(() => { useEffect(() => {
switch (true) { switch (true) {

View File

@ -3,7 +3,9 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
import { useAuth } from '@/auth/hooks/useAuth'; import { useAuth } from '@/auth/hooks/useAuth';
import { useIsLogged } from '@/auth/hooks/useIsLogged'; import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { useSetRecoilState } from 'recoil';
export const VerifyEffect = () => { export const VerifyEffect = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@ -14,11 +16,16 @@ export const VerifyEffect = () => {
const { verify } = useAuth(); const { verify } = useAuth();
const setIsAppWaitingForFreshObjectMetadata = useSetRecoilState(
isAppWaitingForFreshObjectMetadataState,
);
useEffect(() => { useEffect(() => {
const getTokens = async () => { const getTokens = async () => {
if (!loginToken) { if (!loginToken) {
navigate(AppPath.SignInUp); navigate(AppPath.SignInUp);
} else { } else {
setIsAppWaitingForFreshObjectMetadata(true);
await verify(loginToken); await verify(loginToken);
} }
}; };

View File

@ -47,6 +47,7 @@ import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hook
import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain'; import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain';
import { useReadWorkspaceSubdomainFromCurrentLocation } from '@/domain-manager/hooks/useReadWorkspaceSubdomainFromCurrentLocation'; import { useReadWorkspaceSubdomainFromCurrentLocation } from '@/domain-manager/hooks/useReadWorkspaceSubdomainFromCurrentLocation';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState'; import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
export const useAuth = () => { export const useAuth = () => {
const setTokenPair = useSetRecoilState(tokenPairState); const setTokenPair = useSetRecoilState(tokenPairState);
@ -54,6 +55,9 @@ export const useAuth = () => {
const setCurrentWorkspaceMember = useSetRecoilState( const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState, currentWorkspaceMemberState,
); );
const setIsAppWaitingForFreshObjectMetadataState = useSetRecoilState(
isAppWaitingForFreshObjectMetadataState,
);
const setCurrentWorkspaceMembers = useSetRecoilState( const setCurrentWorkspaceMembers = useSetRecoilState(
currentWorkspaceMembersState, currentWorkspaceMembersState,
); );
@ -240,6 +244,7 @@ export const useAuth = () => {
setWorkspaces(validWorkspaces); setWorkspaces(validWorkspaces);
} }
setIsAppWaitingForFreshObjectMetadataState(true);
return { return {
user, user,
@ -254,6 +259,7 @@ export const useAuth = () => {
setCurrentUser, setCurrentUser,
setCurrentWorkspace, setCurrentWorkspace,
isOnAWorkspaceSubdomain, isOnAWorkspaceSubdomain,
setIsAppWaitingForFreshObjectMetadataState,
setCurrentWorkspaceMembers, setCurrentWorkspaceMembers,
setCurrentWorkspaceMember, setCurrentWorkspaceMember,
setDateTimeFormat, setDateTimeFormat,

View File

@ -0,0 +1,19 @@
import React from 'react';
import { useRecoilValue } from 'recoil';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
export const ObjectMetadataItemsGater = ({
children,
}: React.PropsWithChildren) => {
const isAppWaitingForFreshObjectMetadata = useRecoilValue(
isAppWaitingForFreshObjectMetadataState,
);
const shouldDisplayChildren = !isAppWaitingForFreshObjectMetadata;
return (
<>{shouldDisplayChildren ? <>{children}</> : <UserOrMetadataLoader />}</>
);
};

View File

@ -0,0 +1,6 @@
import { createState } from 'twenty-ui';
export const isAppWaitingForFreshObjectMetadataState = createState<boolean>({
key: 'isAppWaitingForFreshObjectMetadataState',
defaultValue: false,
});

View File

@ -21,17 +21,14 @@ export const useShowAuthModal = () => {
); );
return useMemo(() => { return useMemo(() => {
if (isMatchingLocation(AppPath.SignInUp)) {
return true;
}
if (isMatchingLocation(AppPath.Verify)) { if (isMatchingLocation(AppPath.Verify)) {
return false; return false;
} }
if ( if (
isMatchingLocation(AppPath.Invite) || isMatchingLocation(AppPath.Invite) ||
isMatchingLocation(AppPath.ResetPassword) isMatchingLocation(AppPath.ResetPassword) ||
isMatchingLocation(AppPath.SignInUp)
) { ) {
return isDefaultLayoutAuthModalVisible; return isDefaultLayoutAuthModalVisible;
} }

View File

@ -2,5 +2,5 @@ import { createState } from 'twenty-ui';
export const isDefaultLayoutAuthModalVisibleState = createState<boolean>({ export const isDefaultLayoutAuthModalVisibleState = createState<boolean>({
key: 'isDefaultLayoutAuthModalVisibleState', key: 'isDefaultLayoutAuthModalVisibleState',
defaultValue: false, defaultValue: true,
}); });

View File

@ -5,21 +5,21 @@ import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { SignInUpStep } from '@/auth/states/signInUpStepState'; import { SignInUpStep } from '@/auth/states/signInUpStepState';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState'; import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { SignInUpGlobalScopeForm } from '@/auth/sign-in-up/components/SignInUpGlobalScopeForm';
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
import { AnimatedEaseIn } from 'twenty-ui';
import { Logo } from '@/auth/components/Logo'; import { Logo } from '@/auth/components/Logo';
import { Title } from '@/auth/components/Title'; import { Title } from '@/auth/components/Title';
import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm'; import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName'; import { SignInUpGlobalScopeForm } from '@/auth/sign-in-up/components/SignInUpGlobalScopeForm';
import { SignInUpSSOIdentityProviderSelection } from '@/auth/sign-in-up/components/SignInUpSSOIdentityProviderSelection'; import { SignInUpSSOIdentityProviderSelection } from '@/auth/sign-in-up/components/SignInUpSSOIdentityProviderSelection';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm';
import { useMemo } from 'react';
import { isDefined } from '~/utils/isDefined';
import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeFormEffect'; import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeFormEffect';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useGetPublicWorkspaceDataBySubdomain } from '@/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain'; import { useGetPublicWorkspaceDataBySubdomain } from '@/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain';
import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain'; import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain';
import { useIsCurrentLocationOnDefaultDomain } from '@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain'; import { useIsCurrentLocationOnDefaultDomain } from '@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain';
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
import { useMemo } from 'react';
import { AnimatedEaseIn } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
export const SignInUp = () => { export const SignInUp = () => {
const { form } = useSignInUpForm(); const { form } = useSignInUpForm();