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:
@ -1,74 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useIsMatchingLocation } from '../useIsMatchingLocation';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { AppBasePath } from '@/types/AppBasePath';
|
||||
|
||||
const Wrapper =
|
||||
(initialIndex = 0) =>
|
||||
({ children }: { children: React.ReactNode }) => (
|
||||
<MemoryRouter
|
||||
initialEntries={['/example', '/other', `${AppBasePath.Settings}/example`]}
|
||||
initialIndex={initialIndex}
|
||||
>
|
||||
{children}
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
describe('useIsMatchingLocation', () => {
|
||||
it('returns true when paths match with no basePath', () => {
|
||||
const { result } = renderHook(() => useIsMatchingLocation(), {
|
||||
wrapper: Wrapper(),
|
||||
});
|
||||
|
||||
expect(result.current.isMatchingLocation('/example')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when paths do not match with no basePath', () => {
|
||||
const { result } = renderHook(() => useIsMatchingLocation(), {
|
||||
wrapper: Wrapper(),
|
||||
});
|
||||
|
||||
expect(result.current.isMatchingLocation('/non-match')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when paths match with basePath', () => {
|
||||
const { result } = renderHook(() => useIsMatchingLocation(), {
|
||||
wrapper: Wrapper(2),
|
||||
});
|
||||
|
||||
expect(
|
||||
result.current.isMatchingLocation('example', AppBasePath.Settings),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when paths do not match with basePath', () => {
|
||||
const { result } = renderHook(() => useIsMatchingLocation(), {
|
||||
wrapper: Wrapper(),
|
||||
});
|
||||
|
||||
expect(
|
||||
result.current.isMatchingLocation('non-match', AppBasePath.Settings),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('handles trailing slashes in basePath correctly', () => {
|
||||
const { result } = renderHook(() => useIsMatchingLocation(), {
|
||||
wrapper: Wrapper(2),
|
||||
});
|
||||
|
||||
expect(
|
||||
result.current.isMatchingLocation(
|
||||
'example',
|
||||
(AppBasePath.Settings + '/') as AppBasePath,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('handles without basePath correctly', () => {
|
||||
const { result } = renderHook(() => useIsMatchingLocation(), {
|
||||
wrapper: Wrapper(),
|
||||
});
|
||||
|
||||
expect(result.current.isMatchingLocation('example')).toBe(true);
|
||||
});
|
||||
});
|
||||
@ -8,9 +8,9 @@ import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { OnboardingStatus } from '~/generated/graphql';
|
||||
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
|
||||
import { UNTESTED_APP_PATHS } from '~/testing/constants/UntestedAppPaths';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
jest.mock('@/onboarding/hooks/useOnboardingStatus');
|
||||
const setupMockOnboardingStatus = (
|
||||
@ -28,13 +28,13 @@ const setupMockIsWorkspaceActivationStatusEqualsTo = (
|
||||
.mockReturnValueOnce(isWorkspaceSuspended);
|
||||
};
|
||||
|
||||
jest.mock('~/hooks/useIsMatchingLocation');
|
||||
const mockUseIsMatchingLocation = jest.mocked(useIsMatchingLocation);
|
||||
jest.mock('~/utils/isMatchingLocation');
|
||||
const mockIsMatchingLocation = jest.mocked(isMatchingLocation);
|
||||
|
||||
const setupMockIsMatchingLocation = (pathname: string) => {
|
||||
mockUseIsMatchingLocation.mockReturnValueOnce({
|
||||
isMatchingLocation: (path: string) => path === pathname,
|
||||
});
|
||||
mockIsMatchingLocation.mockImplementation(
|
||||
(_location, path) => path === pathname,
|
||||
);
|
||||
};
|
||||
|
||||
jest.mock('@/auth/hooks/useIsLogged');
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
|
||||
import { AppBasePath } from '@/types/AppBasePath';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const addTrailingSlash = (path: string) =>
|
||||
path.endsWith('/') ? path : path + '/';
|
||||
|
||||
const getConstructedPath = (path: string, basePath?: AppBasePath) => {
|
||||
if (!isNonEmptyString(basePath)) return path;
|
||||
|
||||
return addTrailingSlash(basePath) + path;
|
||||
};
|
||||
|
||||
export const useIsMatchingLocation = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const isMatchingLocation = useCallback(
|
||||
(path: string, basePath?: AppBasePath) => {
|
||||
const match = matchPath(
|
||||
getConstructedPath(path, basePath),
|
||||
location.pathname,
|
||||
);
|
||||
return isDefined(match);
|
||||
},
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
return {
|
||||
isMatchingLocation,
|
||||
};
|
||||
};
|
||||
@ -5,24 +5,24 @@ import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { OnboardingStatus } from '~/generated/graphql';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
export const usePageChangeEffectNavigateLocation = () => {
|
||||
const { isMatchingLocation } = useIsMatchingLocation();
|
||||
const isLoggedIn = useIsLogged();
|
||||
const onboardingStatus = useOnboardingStatus();
|
||||
const isWorkspaceSuspended = useIsWorkspaceActivationStatusEqualsTo(
|
||||
WorkspaceActivationStatus.SUSPENDED,
|
||||
);
|
||||
const { defaultHomePagePath } = useDefaultHomePagePath();
|
||||
const location = useLocation();
|
||||
|
||||
const someMatchingLocationOf = (appPaths: AppPath[]): boolean =>
|
||||
appPaths.some((appPath) => isMatchingLocation(appPath));
|
||||
appPaths.some((appPath) => isMatchingLocation(location, appPath));
|
||||
const onGoingUserCreationPaths = [
|
||||
AppPath.Invite,
|
||||
AppPath.SignInUp,
|
||||
@ -61,7 +61,10 @@ export const usePageChangeEffectNavigateLocation = () => {
|
||||
return AppPath.PlanRequired;
|
||||
}
|
||||
|
||||
if (isWorkspaceSuspended && !isMatchingLocation(AppPath.SettingsCatchAll)) {
|
||||
if (
|
||||
isWorkspaceSuspended &&
|
||||
!isMatchingLocation(location, AppPath.SettingsCatchAll)
|
||||
) {
|
||||
return `${AppPath.SettingsCatchAll.replace('/*', '')}/${
|
||||
SettingsPath.Billing
|
||||
}`;
|
||||
@ -79,21 +82,21 @@ export const usePageChangeEffectNavigateLocation = () => {
|
||||
|
||||
if (
|
||||
onboardingStatus === OnboardingStatus.PROFILE_CREATION &&
|
||||
!isMatchingLocation(AppPath.CreateProfile)
|
||||
!isMatchingLocation(location, AppPath.CreateProfile)
|
||||
) {
|
||||
return AppPath.CreateProfile;
|
||||
}
|
||||
|
||||
if (
|
||||
onboardingStatus === OnboardingStatus.SYNC_EMAIL &&
|
||||
!isMatchingLocation(AppPath.SyncEmails)
|
||||
!isMatchingLocation(location, AppPath.SyncEmails)
|
||||
) {
|
||||
return AppPath.SyncEmails;
|
||||
}
|
||||
|
||||
if (
|
||||
onboardingStatus === OnboardingStatus.INVITE_TEAM &&
|
||||
!isMatchingLocation(AppPath.InviteTeam)
|
||||
!isMatchingLocation(location, AppPath.InviteTeam)
|
||||
) {
|
||||
return AppPath.InviteTeam;
|
||||
}
|
||||
@ -101,18 +104,18 @@ export const usePageChangeEffectNavigateLocation = () => {
|
||||
if (
|
||||
onboardingStatus === OnboardingStatus.COMPLETED &&
|
||||
someMatchingLocationOf([...onboardingPaths, ...onGoingUserCreationPaths]) &&
|
||||
!isMatchingLocation(AppPath.ResetPassword) &&
|
||||
!isMatchingLocation(location, AppPath.ResetPassword) &&
|
||||
isLoggedIn
|
||||
) {
|
||||
return defaultHomePagePath;
|
||||
}
|
||||
|
||||
if (isMatchingLocation(AppPath.Index) && isLoggedIn) {
|
||||
if (isMatchingLocation(location, AppPath.Index) && isLoggedIn) {
|
||||
return defaultHomePagePath;
|
||||
}
|
||||
|
||||
if (
|
||||
isMatchingLocation(AppPath.RecordIndexPage) &&
|
||||
isMatchingLocation(location, AppPath.RecordIndexPage) &&
|
||||
!isDefined(objectMetadataItem)
|
||||
) {
|
||||
return AppPath.NotFound;
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -0,0 +1,89 @@
|
||||
import { AppBasePath } from '@/types/AppBasePath';
|
||||
import { isMatchingLocation } from '../isMatchingLocation';
|
||||
|
||||
describe('isMatchingLocation', () => {
|
||||
it('returns true when paths match with no basePath', () => {
|
||||
const location = {
|
||||
pathname: '/example',
|
||||
state: null,
|
||||
key: 'test-key',
|
||||
search: '',
|
||||
hash: '',
|
||||
};
|
||||
const result = isMatchingLocation(location, '/example');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when paths do not match with no basePath', () => {
|
||||
const location = {
|
||||
pathname: '/example',
|
||||
state: null,
|
||||
key: 'test-key',
|
||||
search: '',
|
||||
hash: '',
|
||||
};
|
||||
const result = isMatchingLocation(location, '/non-match');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when paths match with basePath', () => {
|
||||
const location = {
|
||||
pathname: `${AppBasePath.Settings}/example`,
|
||||
state: null,
|
||||
key: 'test-key',
|
||||
search: '',
|
||||
hash: '',
|
||||
};
|
||||
const result = isMatchingLocation(
|
||||
location,
|
||||
'example',
|
||||
AppBasePath.Settings,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when paths do not match with basePath', () => {
|
||||
const location = {
|
||||
pathname: `${AppBasePath.Settings}/example`,
|
||||
state: null,
|
||||
key: 'test-key',
|
||||
search: '',
|
||||
hash: '',
|
||||
};
|
||||
|
||||
const result = isMatchingLocation(
|
||||
location,
|
||||
'non-match',
|
||||
AppBasePath.Settings,
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('handles trailing slashes in basePath correctly', () => {
|
||||
const location = {
|
||||
pathname: `${AppBasePath.Settings}/example`,
|
||||
state: null,
|
||||
key: 'test-key',
|
||||
search: '',
|
||||
hash: '',
|
||||
};
|
||||
const result = isMatchingLocation(
|
||||
location,
|
||||
'example',
|
||||
(AppBasePath.Settings + '/') as AppBasePath,
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('handles paths without basePath correctly', () => {
|
||||
const location = {
|
||||
pathname: '/example',
|
||||
state: null,
|
||||
key: 'test-key',
|
||||
search: '',
|
||||
hash: '',
|
||||
};
|
||||
const result = isMatchingLocation(location, '/example');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
26
packages/twenty-front/src/utils/isMatchingLocation.ts
Normal file
26
packages/twenty-front/src/utils/isMatchingLocation.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Location, matchPath } from 'react-router-dom';
|
||||
|
||||
import { AppBasePath } from '@/types/AppBasePath';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const addTrailingSlash = (path: string) =>
|
||||
path.endsWith('/') ? path : path + '/';
|
||||
|
||||
const getConstructedPath = (path: string, basePath?: AppBasePath) => {
|
||||
if (!isNonEmptyString(basePath)) return path;
|
||||
|
||||
return addTrailingSlash(basePath) + path;
|
||||
};
|
||||
|
||||
export const isMatchingLocation = (
|
||||
location: Location,
|
||||
path: string,
|
||||
basePath?: AppBasePath,
|
||||
): boolean => {
|
||||
const match = matchPath(
|
||||
getConstructedPath(path, basePath),
|
||||
location.pathname,
|
||||
);
|
||||
return isDefined(match);
|
||||
};
|
||||
Reference in New Issue
Block a user