Fix "PageChangeEffect does not run when changing view on the same object" (#12196)
Fixes https://github.com/twentyhq/core-team-issues/issues/950 This issue was due to the memoization inside `useIsMatchingLocation`, which was rerendered only if the pathname changed but not the search params. After discussion with @lucasbordeau, we decided to remove the hook `useIsMatchingLocation` and to create an equivalent util function which takes the location as an argument. --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -11,8 +11,8 @@ import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
import { workspacesState } from '@/auth/states/workspaces';
|
||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
@ -25,7 +25,6 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
||||
const [isDebugMode] = useRecoilState(isDebugModeState);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { isMatchingLocation } = useIsMatchingLocation();
|
||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
||||
currentWorkspaceState,
|
||||
@ -74,10 +73,10 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
||||
setCurrentUserWorkspace(null);
|
||||
setWorkspaces([]);
|
||||
if (
|
||||
!isMatchingLocation(AppPath.Verify) &&
|
||||
!isMatchingLocation(AppPath.SignInUp) &&
|
||||
!isMatchingLocation(AppPath.Invite) &&
|
||||
!isMatchingLocation(AppPath.ResetPassword)
|
||||
!isMatchingLocation(location, AppPath.Verify) &&
|
||||
!isMatchingLocation(location, AppPath.SignInUp) &&
|
||||
!isMatchingLocation(location, AppPath.Invite) &&
|
||||
!isMatchingLocation(location, AppPath.ResetPassword)
|
||||
) {
|
||||
setPreviousUrl(`${location.pathname}${location.search}`);
|
||||
navigate(AppPath.SignInUp);
|
||||
|
||||
@ -36,15 +36,14 @@ import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { AnalyticsType } from '~/generated/graphql';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
|
||||
import { useInitializeQueryParamState } from '~/modules/app/hooks/useInitializeQueryParamState';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
import { getPageTitleFromPath } from '~/utils/title-utils';
|
||||
// TODO: break down into smaller functions and / or hooks
|
||||
// - moved usePageChangeEffectNavigateLocation into dedicated hook
|
||||
export const PageChangeEffect = () => {
|
||||
const navigate = useNavigate();
|
||||
const { isMatchingLocation } = useIsMatchingLocation();
|
||||
|
||||
const [previousLocation, setPreviousLocation] = useState('');
|
||||
|
||||
@ -126,7 +125,6 @@ export const PageChangeEffect = () => {
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isMatchingLocation,
|
||||
previousLocation,
|
||||
resetTableSelections,
|
||||
unfocusRecordTableRow,
|
||||
@ -139,28 +137,28 @@ export const PageChangeEffect = () => {
|
||||
|
||||
useEffect(() => {
|
||||
switch (true) {
|
||||
case isMatchingLocation(AppPath.RecordIndexPage): {
|
||||
case isMatchingLocation(location, AppPath.RecordIndexPage): {
|
||||
setHotkeyScope(RecordIndexHotkeyScope.RecordIndex, {
|
||||
goto: true,
|
||||
keyboardShortcutMenu: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case isMatchingLocation(AppPath.RecordShowPage): {
|
||||
case isMatchingLocation(location, AppPath.RecordShowPage): {
|
||||
setHotkeyScope(PageHotkeyScope.CompanyShowPage, {
|
||||
goto: true,
|
||||
keyboardShortcutMenu: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case isMatchingLocation(AppPath.OpportunitiesPage): {
|
||||
case isMatchingLocation(location, AppPath.OpportunitiesPage): {
|
||||
setHotkeyScope(PageHotkeyScope.OpportunitiesPage, {
|
||||
goto: true,
|
||||
keyboardShortcutMenu: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case isMatchingLocation(AppPath.TasksPage): {
|
||||
case isMatchingLocation(location, AppPath.TasksPage): {
|
||||
setHotkeyScope(PageHotkeyScope.TaskPage, {
|
||||
goto: true,
|
||||
keyboardShortcutMenu: true,
|
||||
@ -168,42 +166,50 @@ export const PageChangeEffect = () => {
|
||||
break;
|
||||
}
|
||||
|
||||
case isMatchingLocation(AppPath.SignInUp): {
|
||||
case isMatchingLocation(location, AppPath.SignInUp): {
|
||||
setHotkeyScope(PageHotkeyScope.SignInUp);
|
||||
break;
|
||||
}
|
||||
case isMatchingLocation(AppPath.Invite): {
|
||||
case isMatchingLocation(location, AppPath.Invite): {
|
||||
setHotkeyScope(PageHotkeyScope.SignInUp);
|
||||
break;
|
||||
}
|
||||
case isMatchingLocation(AppPath.CreateProfile): {
|
||||
case isMatchingLocation(location, AppPath.CreateProfile): {
|
||||
setHotkeyScope(PageHotkeyScope.CreateProfile);
|
||||
break;
|
||||
}
|
||||
case isMatchingLocation(AppPath.CreateWorkspace): {
|
||||
case isMatchingLocation(location, AppPath.CreateWorkspace): {
|
||||
setHotkeyScope(PageHotkeyScope.CreateWorkspace);
|
||||
break;
|
||||
}
|
||||
case isMatchingLocation(AppPath.SyncEmails): {
|
||||
case isMatchingLocation(location, AppPath.SyncEmails): {
|
||||
setHotkeyScope(PageHotkeyScope.SyncEmail);
|
||||
break;
|
||||
}
|
||||
case isMatchingLocation(AppPath.InviteTeam): {
|
||||
case isMatchingLocation(location, AppPath.InviteTeam): {
|
||||
setHotkeyScope(PageHotkeyScope.InviteTeam);
|
||||
break;
|
||||
}
|
||||
case isMatchingLocation(AppPath.PlanRequired): {
|
||||
case isMatchingLocation(location, AppPath.PlanRequired): {
|
||||
setHotkeyScope(PageHotkeyScope.PlanRequired);
|
||||
break;
|
||||
}
|
||||
case isMatchingLocation(SettingsPath.ProfilePage, AppBasePath.Settings): {
|
||||
case isMatchingLocation(
|
||||
location,
|
||||
SettingsPath.ProfilePage,
|
||||
AppBasePath.Settings,
|
||||
): {
|
||||
setHotkeyScope(PageHotkeyScope.ProfilePage, {
|
||||
goto: true,
|
||||
keyboardShortcutMenu: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case isMatchingLocation(SettingsPath.Domain, AppBasePath.Settings): {
|
||||
case isMatchingLocation(
|
||||
location,
|
||||
SettingsPath.Domain,
|
||||
AppBasePath.Settings,
|
||||
): {
|
||||
setHotkeyScope(PageHotkeyScope.Settings, {
|
||||
goto: false,
|
||||
keyboardShortcutMenu: true,
|
||||
@ -211,6 +217,7 @@ export const PageChangeEffect = () => {
|
||||
break;
|
||||
}
|
||||
case isMatchingLocation(
|
||||
location,
|
||||
SettingsPath.WorkspaceMembersPage,
|
||||
AppBasePath.Settings,
|
||||
): {
|
||||
@ -221,7 +228,7 @@ export const PageChangeEffect = () => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [isMatchingLocation, setHotkeyScope]);
|
||||
}, [location, setHotkeyScope]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { SubmitHandler, UseFormReturn } from 'react-hook-form';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useLocation, useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { signInUpModeState } from '@/auth/states/signInUpModeState';
|
||||
@ -15,7 +15,7 @@ import { AppPath } from '@/types/AppPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
|
||||
export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
@ -24,14 +24,16 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
|
||||
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
|
||||
|
||||
const { isMatchingLocation } = useIsMatchingLocation();
|
||||
const location = useLocation();
|
||||
|
||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||
const [searchParams] = useSearchParams();
|
||||
const workspacePersonalInviteToken =
|
||||
searchParams.get('inviteToken') ?? undefined;
|
||||
|
||||
const [isInviteMode] = useState(() => isMatchingLocation(AppPath.Invite));
|
||||
const [isInviteMode] = useState(() =>
|
||||
isMatchingLocation(location, AppPath.Invite),
|
||||
);
|
||||
|
||||
const {
|
||||
signInWithCredentials,
|
||||
|
||||
@ -3,7 +3,8 @@ import { useRecoilValue } from 'recoil';
|
||||
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||
import { AppFullScreenErrorFallback } from '@/error-handler/components/AppFullScreenErrorFallback';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
@ -12,13 +13,13 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
clientConfigApiStatusState,
|
||||
);
|
||||
|
||||
const { isMatchingLocation } = useIsMatchingLocation();
|
||||
const location = useLocation();
|
||||
|
||||
// TODO: Implement a better loading strategy
|
||||
if (
|
||||
!isLoaded &&
|
||||
!isMatchingLocation(AppPath.Verify) &&
|
||||
!isMatchingLocation(AppPath.VerifyEmail)
|
||||
!isMatchingLocation(location, AppPath.Verify) &&
|
||||
!isMatchingLocation(location, AppPath.VerifyEmail)
|
||||
)
|
||||
return null;
|
||||
|
||||
|
||||
@ -5,10 +5,10 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
|
||||
import { prefetchIndexViewIdFromObjectMetadataItemFamilySelector } from '@/prefetch/states/selector/prefetchIndexViewIdFromObjectMetadataItemFamilySelector';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useLocation, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
const getViewId = (
|
||||
viewIdFromQueryParams: string | null,
|
||||
@ -31,9 +31,12 @@ const getViewId = (
|
||||
};
|
||||
|
||||
export const MainContextStoreProvider = () => {
|
||||
const { isMatchingLocation } = useIsMatchingLocation();
|
||||
const isRecordIndexPage = isMatchingLocation(AppPath.RecordIndexPage);
|
||||
const isRecordShowPage = isMatchingLocation(AppPath.RecordShowPage);
|
||||
const location = useLocation();
|
||||
const isRecordIndexPage = isMatchingLocation(
|
||||
location,
|
||||
AppPath.RecordIndexPage,
|
||||
);
|
||||
const isRecordShowPage = isMatchingLocation(location, AppPath.RecordShowPage);
|
||||
const isSettingsPage = useIsSettingsPage();
|
||||
|
||||
const objectNamePlural = useParams().objectNamePlural ?? '';
|
||||
|
||||
@ -1,19 +1,23 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
export const useShowFullscreen = () => {
|
||||
const { isMatchingLocation } = useIsMatchingLocation();
|
||||
const location = useLocation();
|
||||
|
||||
return useMemo(() => {
|
||||
if (
|
||||
isMatchingLocation('settings/' + SettingsPath.RestPlayground + '/*') ||
|
||||
isMatchingLocation('settings/' + SettingsPath.GraphQLPlayground)
|
||||
isMatchingLocation(
|
||||
location,
|
||||
'settings/' + SettingsPath.RestPlayground + '/*',
|
||||
) ||
|
||||
isMatchingLocation(location, 'settings/' + SettingsPath.GraphQLPlayground)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [isMatchingLocation]);
|
||||
}, [location]);
|
||||
};
|
||||
|
||||
@ -1,17 +1,25 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import * as reactRouterDom from 'react-router-dom';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
jest.mock('~/hooks/useIsMatchingLocation');
|
||||
const mockUseIsMatchingLocation = jest.mocked(useIsMatchingLocation);
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useLocation: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseLocation = reactRouterDom.useLocation as jest.Mock;
|
||||
|
||||
jest.mock('~/utils/isMatchingLocation');
|
||||
const mockIsMatchingLocation = jest.mocked(isMatchingLocation);
|
||||
|
||||
const setupMockIsMatchingLocation = (pathname: string) => {
|
||||
mockUseIsMatchingLocation.mockReturnValueOnce({
|
||||
isMatchingLocation: (path: string) => path === pathname,
|
||||
});
|
||||
mockUseLocation.mockReturnValue({ pathname });
|
||||
mockIsMatchingLocation.mockImplementation(
|
||||
(_location, path) => path === pathname,
|
||||
);
|
||||
};
|
||||
|
||||
const getResult = () =>
|
||||
|
||||
@ -1,28 +1,29 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
export const useShowAuthModal = () => {
|
||||
const { isMatchingLocation } = useIsMatchingLocation();
|
||||
const location = useLocation();
|
||||
|
||||
return useMemo(() => {
|
||||
if (
|
||||
isMatchingLocation(AppPath.Invite) ||
|
||||
isMatchingLocation(AppPath.InviteTeam) ||
|
||||
isMatchingLocation(AppPath.CreateProfile) ||
|
||||
isMatchingLocation(AppPath.SyncEmails) ||
|
||||
isMatchingLocation(AppPath.ResetPassword) ||
|
||||
isMatchingLocation(AppPath.VerifyEmail) ||
|
||||
isMatchingLocation(AppPath.Verify) ||
|
||||
isMatchingLocation(AppPath.SignInUp) ||
|
||||
isMatchingLocation(AppPath.CreateWorkspace) ||
|
||||
isMatchingLocation(AppPath.PlanRequired) ||
|
||||
isMatchingLocation(AppPath.PlanRequiredSuccess)
|
||||
isMatchingLocation(location, AppPath.Invite) ||
|
||||
isMatchingLocation(location, AppPath.InviteTeam) ||
|
||||
isMatchingLocation(location, AppPath.CreateProfile) ||
|
||||
isMatchingLocation(location, AppPath.SyncEmails) ||
|
||||
isMatchingLocation(location, AppPath.ResetPassword) ||
|
||||
isMatchingLocation(location, AppPath.VerifyEmail) ||
|
||||
isMatchingLocation(location, AppPath.Verify) ||
|
||||
isMatchingLocation(location, AppPath.SignInUp) ||
|
||||
isMatchingLocation(location, AppPath.CreateWorkspace) ||
|
||||
isMatchingLocation(location, AppPath.PlanRequired) ||
|
||||
isMatchingLocation(location, AppPath.PlanRequiredSuccess)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [isMatchingLocation]);
|
||||
}, [location]);
|
||||
};
|
||||
|
||||
@ -5,19 +5,20 @@ import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingStat
|
||||
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
export const UserProvider = ({ children }: React.PropsWithChildren) => {
|
||||
const isCurrentUserLoaded = useRecoilValue(isCurrentUserLoadedState);
|
||||
const { isMatchingLocation } = useIsMatchingLocation();
|
||||
const location = useLocation();
|
||||
|
||||
const dateTimeFormat = useRecoilValue(dateTimeFormatState);
|
||||
|
||||
return !isCurrentUserLoaded &&
|
||||
!isMatchingLocation(AppPath.Verify) &&
|
||||
!isMatchingLocation(AppPath.VerifyEmail) &&
|
||||
!isMatchingLocation(AppPath.CreateWorkspace) ? (
|
||||
!isMatchingLocation(location, AppPath.Verify) &&
|
||||
!isMatchingLocation(location, AppPath.VerifyEmail) &&
|
||||
!isMatchingLocation(location, AppPath.CreateWorkspace) ? (
|
||||
<UserOrMetadataLoader />
|
||||
) : (
|
||||
<UserContext.Provider
|
||||
|
||||
@ -19,16 +19,17 @@ import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDa
|
||||
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceMember } from '~/generated-metadata/graphql';
|
||||
import { useGetCurrentUserQuery } from '~/generated/graphql';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
export const UserProviderEffect = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { isMatchingLocation } = useIsMatchingLocation();
|
||||
const location = useLocation();
|
||||
|
||||
const [isCurrentUserLoaded, setIsCurrentUserLoaded] = useRecoilState(
|
||||
isCurrentUserLoadedState,
|
||||
@ -53,8 +54,8 @@ export const UserProviderEffect = () => {
|
||||
const { loading: queryLoading, data: queryData } = useGetCurrentUserQuery({
|
||||
skip:
|
||||
isCurrentUserLoaded ||
|
||||
isMatchingLocation(AppPath.Verify) ||
|
||||
isMatchingLocation(AppPath.VerifyEmail),
|
||||
isMatchingLocation(location, AppPath.Verify) ||
|
||||
isMatchingLocation(location, AppPath.VerifyEmail),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user