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 { OnboardingStatus } from '~/generated/graphql';
|
||||||
|
|
||||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
|
||||||
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
|
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
|
||||||
import { UNTESTED_APP_PATHS } from '~/testing/constants/UntestedAppPaths';
|
import { UNTESTED_APP_PATHS } from '~/testing/constants/UntestedAppPaths';
|
||||||
|
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||||
|
|
||||||
jest.mock('@/onboarding/hooks/useOnboardingStatus');
|
jest.mock('@/onboarding/hooks/useOnboardingStatus');
|
||||||
const setupMockOnboardingStatus = (
|
const setupMockOnboardingStatus = (
|
||||||
@ -28,13 +28,13 @@ const setupMockIsWorkspaceActivationStatusEqualsTo = (
|
|||||||
.mockReturnValueOnce(isWorkspaceSuspended);
|
.mockReturnValueOnce(isWorkspaceSuspended);
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('~/hooks/useIsMatchingLocation');
|
jest.mock('~/utils/isMatchingLocation');
|
||||||
const mockUseIsMatchingLocation = jest.mocked(useIsMatchingLocation);
|
const mockIsMatchingLocation = jest.mocked(isMatchingLocation);
|
||||||
|
|
||||||
const setupMockIsMatchingLocation = (pathname: string) => {
|
const setupMockIsMatchingLocation = (pathname: string) => {
|
||||||
mockUseIsMatchingLocation.mockReturnValueOnce({
|
mockIsMatchingLocation.mockImplementation(
|
||||||
isMatchingLocation: (path: string) => path === pathname,
|
(_location, path) => path === pathname,
|
||||||
});
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('@/auth/hooks/useIsLogged');
|
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 { AppPath } from '@/types/AppPath';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||||
import { OnboardingStatus } from '~/generated/graphql';
|
import { OnboardingStatus } from '~/generated/graphql';
|
||||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||||
|
|
||||||
export const usePageChangeEffectNavigateLocation = () => {
|
export const usePageChangeEffectNavigateLocation = () => {
|
||||||
const { isMatchingLocation } = useIsMatchingLocation();
|
|
||||||
const isLoggedIn = useIsLogged();
|
const isLoggedIn = useIsLogged();
|
||||||
const onboardingStatus = useOnboardingStatus();
|
const onboardingStatus = useOnboardingStatus();
|
||||||
const isWorkspaceSuspended = useIsWorkspaceActivationStatusEqualsTo(
|
const isWorkspaceSuspended = useIsWorkspaceActivationStatusEqualsTo(
|
||||||
WorkspaceActivationStatus.SUSPENDED,
|
WorkspaceActivationStatus.SUSPENDED,
|
||||||
);
|
);
|
||||||
const { defaultHomePagePath } = useDefaultHomePagePath();
|
const { defaultHomePagePath } = useDefaultHomePagePath();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const someMatchingLocationOf = (appPaths: AppPath[]): boolean =>
|
const someMatchingLocationOf = (appPaths: AppPath[]): boolean =>
|
||||||
appPaths.some((appPath) => isMatchingLocation(appPath));
|
appPaths.some((appPath) => isMatchingLocation(location, appPath));
|
||||||
const onGoingUserCreationPaths = [
|
const onGoingUserCreationPaths = [
|
||||||
AppPath.Invite,
|
AppPath.Invite,
|
||||||
AppPath.SignInUp,
|
AppPath.SignInUp,
|
||||||
@ -61,7 +61,10 @@ export const usePageChangeEffectNavigateLocation = () => {
|
|||||||
return AppPath.PlanRequired;
|
return AppPath.PlanRequired;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isWorkspaceSuspended && !isMatchingLocation(AppPath.SettingsCatchAll)) {
|
if (
|
||||||
|
isWorkspaceSuspended &&
|
||||||
|
!isMatchingLocation(location, AppPath.SettingsCatchAll)
|
||||||
|
) {
|
||||||
return `${AppPath.SettingsCatchAll.replace('/*', '')}/${
|
return `${AppPath.SettingsCatchAll.replace('/*', '')}/${
|
||||||
SettingsPath.Billing
|
SettingsPath.Billing
|
||||||
}`;
|
}`;
|
||||||
@ -79,21 +82,21 @@ export const usePageChangeEffectNavigateLocation = () => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
onboardingStatus === OnboardingStatus.PROFILE_CREATION &&
|
onboardingStatus === OnboardingStatus.PROFILE_CREATION &&
|
||||||
!isMatchingLocation(AppPath.CreateProfile)
|
!isMatchingLocation(location, AppPath.CreateProfile)
|
||||||
) {
|
) {
|
||||||
return AppPath.CreateProfile;
|
return AppPath.CreateProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
onboardingStatus === OnboardingStatus.SYNC_EMAIL &&
|
onboardingStatus === OnboardingStatus.SYNC_EMAIL &&
|
||||||
!isMatchingLocation(AppPath.SyncEmails)
|
!isMatchingLocation(location, AppPath.SyncEmails)
|
||||||
) {
|
) {
|
||||||
return AppPath.SyncEmails;
|
return AppPath.SyncEmails;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
onboardingStatus === OnboardingStatus.INVITE_TEAM &&
|
onboardingStatus === OnboardingStatus.INVITE_TEAM &&
|
||||||
!isMatchingLocation(AppPath.InviteTeam)
|
!isMatchingLocation(location, AppPath.InviteTeam)
|
||||||
) {
|
) {
|
||||||
return AppPath.InviteTeam;
|
return AppPath.InviteTeam;
|
||||||
}
|
}
|
||||||
@ -101,18 +104,18 @@ export const usePageChangeEffectNavigateLocation = () => {
|
|||||||
if (
|
if (
|
||||||
onboardingStatus === OnboardingStatus.COMPLETED &&
|
onboardingStatus === OnboardingStatus.COMPLETED &&
|
||||||
someMatchingLocationOf([...onboardingPaths, ...onGoingUserCreationPaths]) &&
|
someMatchingLocationOf([...onboardingPaths, ...onGoingUserCreationPaths]) &&
|
||||||
!isMatchingLocation(AppPath.ResetPassword) &&
|
!isMatchingLocation(location, AppPath.ResetPassword) &&
|
||||||
isLoggedIn
|
isLoggedIn
|
||||||
) {
|
) {
|
||||||
return defaultHomePagePath;
|
return defaultHomePagePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMatchingLocation(AppPath.Index) && isLoggedIn) {
|
if (isMatchingLocation(location, AppPath.Index) && isLoggedIn) {
|
||||||
return defaultHomePagePath;
|
return defaultHomePagePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isMatchingLocation(AppPath.RecordIndexPage) &&
|
isMatchingLocation(location, AppPath.RecordIndexPage) &&
|
||||||
!isDefined(objectMetadataItem)
|
!isDefined(objectMetadataItem)
|
||||||
) {
|
) {
|
||||||
return AppPath.NotFound;
|
return AppPath.NotFound;
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import { tokenPairState } from '@/auth/states/tokenPairState';
|
|||||||
import { workspacesState } from '@/auth/states/workspaces';
|
import { workspacesState } from '@/auth/states/workspaces';
|
||||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
|
||||||
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
|
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
|
||||||
|
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||||
|
|
||||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
@ -25,7 +25,6 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
|||||||
const [isDebugMode] = useRecoilState(isDebugModeState);
|
const [isDebugMode] = useRecoilState(isDebugModeState);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isMatchingLocation } = useIsMatchingLocation();
|
|
||||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||||
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
||||||
currentWorkspaceState,
|
currentWorkspaceState,
|
||||||
@ -74,10 +73,10 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
|
|||||||
setCurrentUserWorkspace(null);
|
setCurrentUserWorkspace(null);
|
||||||
setWorkspaces([]);
|
setWorkspaces([]);
|
||||||
if (
|
if (
|
||||||
!isMatchingLocation(AppPath.Verify) &&
|
!isMatchingLocation(location, AppPath.Verify) &&
|
||||||
!isMatchingLocation(AppPath.SignInUp) &&
|
!isMatchingLocation(location, AppPath.SignInUp) &&
|
||||||
!isMatchingLocation(AppPath.Invite) &&
|
!isMatchingLocation(location, AppPath.Invite) &&
|
||||||
!isMatchingLocation(AppPath.ResetPassword)
|
!isMatchingLocation(location, AppPath.ResetPassword)
|
||||||
) {
|
) {
|
||||||
setPreviousUrl(`${location.pathname}${location.search}`);
|
setPreviousUrl(`${location.pathname}${location.search}`);
|
||||||
navigate(AppPath.SignInUp);
|
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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { AnalyticsType } from '~/generated/graphql';
|
import { AnalyticsType } from '~/generated/graphql';
|
||||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
|
||||||
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
|
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
|
||||||
import { useInitializeQueryParamState } from '~/modules/app/hooks/useInitializeQueryParamState';
|
import { useInitializeQueryParamState } from '~/modules/app/hooks/useInitializeQueryParamState';
|
||||||
|
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||||
import { getPageTitleFromPath } from '~/utils/title-utils';
|
import { getPageTitleFromPath } from '~/utils/title-utils';
|
||||||
// TODO: break down into smaller functions and / or hooks
|
// TODO: break down into smaller functions and / or hooks
|
||||||
// - moved usePageChangeEffectNavigateLocation into dedicated hook
|
// - moved usePageChangeEffectNavigateLocation into dedicated hook
|
||||||
export const PageChangeEffect = () => {
|
export const PageChangeEffect = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isMatchingLocation } = useIsMatchingLocation();
|
|
||||||
|
|
||||||
const [previousLocation, setPreviousLocation] = useState('');
|
const [previousLocation, setPreviousLocation] = useState('');
|
||||||
|
|
||||||
@ -126,7 +125,6 @@ export const PageChangeEffect = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
isMatchingLocation,
|
|
||||||
previousLocation,
|
previousLocation,
|
||||||
resetTableSelections,
|
resetTableSelections,
|
||||||
unfocusRecordTableRow,
|
unfocusRecordTableRow,
|
||||||
@ -139,28 +137,28 @@ export const PageChangeEffect = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case isMatchingLocation(AppPath.RecordIndexPage): {
|
case isMatchingLocation(location, AppPath.RecordIndexPage): {
|
||||||
setHotkeyScope(RecordIndexHotkeyScope.RecordIndex, {
|
setHotkeyScope(RecordIndexHotkeyScope.RecordIndex, {
|
||||||
goto: true,
|
goto: true,
|
||||||
keyboardShortcutMenu: true,
|
keyboardShortcutMenu: true,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppPath.RecordShowPage): {
|
case isMatchingLocation(location, AppPath.RecordShowPage): {
|
||||||
setHotkeyScope(PageHotkeyScope.CompanyShowPage, {
|
setHotkeyScope(PageHotkeyScope.CompanyShowPage, {
|
||||||
goto: true,
|
goto: true,
|
||||||
keyboardShortcutMenu: true,
|
keyboardShortcutMenu: true,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppPath.OpportunitiesPage): {
|
case isMatchingLocation(location, AppPath.OpportunitiesPage): {
|
||||||
setHotkeyScope(PageHotkeyScope.OpportunitiesPage, {
|
setHotkeyScope(PageHotkeyScope.OpportunitiesPage, {
|
||||||
goto: true,
|
goto: true,
|
||||||
keyboardShortcutMenu: true,
|
keyboardShortcutMenu: true,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppPath.TasksPage): {
|
case isMatchingLocation(location, AppPath.TasksPage): {
|
||||||
setHotkeyScope(PageHotkeyScope.TaskPage, {
|
setHotkeyScope(PageHotkeyScope.TaskPage, {
|
||||||
goto: true,
|
goto: true,
|
||||||
keyboardShortcutMenu: true,
|
keyboardShortcutMenu: true,
|
||||||
@ -168,42 +166,50 @@ export const PageChangeEffect = () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case isMatchingLocation(AppPath.SignInUp): {
|
case isMatchingLocation(location, AppPath.SignInUp): {
|
||||||
setHotkeyScope(PageHotkeyScope.SignInUp);
|
setHotkeyScope(PageHotkeyScope.SignInUp);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppPath.Invite): {
|
case isMatchingLocation(location, AppPath.Invite): {
|
||||||
setHotkeyScope(PageHotkeyScope.SignInUp);
|
setHotkeyScope(PageHotkeyScope.SignInUp);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppPath.CreateProfile): {
|
case isMatchingLocation(location, AppPath.CreateProfile): {
|
||||||
setHotkeyScope(PageHotkeyScope.CreateProfile);
|
setHotkeyScope(PageHotkeyScope.CreateProfile);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppPath.CreateWorkspace): {
|
case isMatchingLocation(location, AppPath.CreateWorkspace): {
|
||||||
setHotkeyScope(PageHotkeyScope.CreateWorkspace);
|
setHotkeyScope(PageHotkeyScope.CreateWorkspace);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppPath.SyncEmails): {
|
case isMatchingLocation(location, AppPath.SyncEmails): {
|
||||||
setHotkeyScope(PageHotkeyScope.SyncEmail);
|
setHotkeyScope(PageHotkeyScope.SyncEmail);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppPath.InviteTeam): {
|
case isMatchingLocation(location, AppPath.InviteTeam): {
|
||||||
setHotkeyScope(PageHotkeyScope.InviteTeam);
|
setHotkeyScope(PageHotkeyScope.InviteTeam);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(AppPath.PlanRequired): {
|
case isMatchingLocation(location, AppPath.PlanRequired): {
|
||||||
setHotkeyScope(PageHotkeyScope.PlanRequired);
|
setHotkeyScope(PageHotkeyScope.PlanRequired);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(SettingsPath.ProfilePage, AppBasePath.Settings): {
|
case isMatchingLocation(
|
||||||
|
location,
|
||||||
|
SettingsPath.ProfilePage,
|
||||||
|
AppBasePath.Settings,
|
||||||
|
): {
|
||||||
setHotkeyScope(PageHotkeyScope.ProfilePage, {
|
setHotkeyScope(PageHotkeyScope.ProfilePage, {
|
||||||
goto: true,
|
goto: true,
|
||||||
keyboardShortcutMenu: true,
|
keyboardShortcutMenu: true,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(SettingsPath.Domain, AppBasePath.Settings): {
|
case isMatchingLocation(
|
||||||
|
location,
|
||||||
|
SettingsPath.Domain,
|
||||||
|
AppBasePath.Settings,
|
||||||
|
): {
|
||||||
setHotkeyScope(PageHotkeyScope.Settings, {
|
setHotkeyScope(PageHotkeyScope.Settings, {
|
||||||
goto: false,
|
goto: false,
|
||||||
keyboardShortcutMenu: true,
|
keyboardShortcutMenu: true,
|
||||||
@ -211,6 +217,7 @@ export const PageChangeEffect = () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case isMatchingLocation(
|
case isMatchingLocation(
|
||||||
|
location,
|
||||||
SettingsPath.WorkspaceMembersPage,
|
SettingsPath.WorkspaceMembersPage,
|
||||||
AppBasePath.Settings,
|
AppBasePath.Settings,
|
||||||
): {
|
): {
|
||||||
@ -221,7 +228,7 @@ export const PageChangeEffect = () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isMatchingLocation, setHotkeyScope]);
|
}, [location, setHotkeyScope]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { SubmitHandler, UseFormReturn } from 'react-hook-form';
|
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 { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||||
import { signInUpModeState } from '@/auth/states/signInUpModeState';
|
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 { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||||
import { useAuth } from '../../hooks/useAuth';
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
|
|
||||||
export const useSignInUp = (form: UseFormReturn<Form>) => {
|
export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||||
@ -24,14 +24,16 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
|||||||
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
|
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
|
||||||
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
|
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
|
||||||
|
|
||||||
const { isMatchingLocation } = useIsMatchingLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const workspacePersonalInviteToken =
|
const workspacePersonalInviteToken =
|
||||||
searchParams.get('inviteToken') ?? undefined;
|
searchParams.get('inviteToken') ?? undefined;
|
||||||
|
|
||||||
const [isInviteMode] = useState(() => isMatchingLocation(AppPath.Invite));
|
const [isInviteMode] = useState(() =>
|
||||||
|
isMatchingLocation(location, AppPath.Invite),
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
signInWithCredentials,
|
signInWithCredentials,
|
||||||
|
|||||||
@ -3,7 +3,8 @@ import { useRecoilValue } from 'recoil';
|
|||||||
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||||
import { AppFullScreenErrorFallback } from '@/error-handler/components/AppFullScreenErrorFallback';
|
import { AppFullScreenErrorFallback } from '@/error-handler/components/AppFullScreenErrorFallback';
|
||||||
import { AppPath } from '@/types/AppPath';
|
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> = ({
|
export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@ -12,13 +13,13 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
clientConfigApiStatusState,
|
clientConfigApiStatusState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isMatchingLocation } = useIsMatchingLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
// TODO: Implement a better loading strategy
|
// TODO: Implement a better loading strategy
|
||||||
if (
|
if (
|
||||||
!isLoaded &&
|
!isLoaded &&
|
||||||
!isMatchingLocation(AppPath.Verify) &&
|
!isMatchingLocation(location, AppPath.Verify) &&
|
||||||
!isMatchingLocation(AppPath.VerifyEmail)
|
!isMatchingLocation(location, AppPath.VerifyEmail)
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
|||||||
@ -5,10 +5,10 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
|
|||||||
import { prefetchIndexViewIdFromObjectMetadataItemFamilySelector } from '@/prefetch/states/selector/prefetchIndexViewIdFromObjectMetadataItemFamilySelector';
|
import { prefetchIndexViewIdFromObjectMetadataItemFamilySelector } from '@/prefetch/states/selector/prefetchIndexViewIdFromObjectMetadataItemFamilySelector';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
|
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 { useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||||
|
|
||||||
const getViewId = (
|
const getViewId = (
|
||||||
viewIdFromQueryParams: string | null,
|
viewIdFromQueryParams: string | null,
|
||||||
@ -31,9 +31,12 @@ const getViewId = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MainContextStoreProvider = () => {
|
export const MainContextStoreProvider = () => {
|
||||||
const { isMatchingLocation } = useIsMatchingLocation();
|
const location = useLocation();
|
||||||
const isRecordIndexPage = isMatchingLocation(AppPath.RecordIndexPage);
|
const isRecordIndexPage = isMatchingLocation(
|
||||||
const isRecordShowPage = isMatchingLocation(AppPath.RecordShowPage);
|
location,
|
||||||
|
AppPath.RecordIndexPage,
|
||||||
|
);
|
||||||
|
const isRecordShowPage = isMatchingLocation(location, AppPath.RecordShowPage);
|
||||||
const isSettingsPage = useIsSettingsPage();
|
const isSettingsPage = useIsSettingsPage();
|
||||||
|
|
||||||
const objectNamePlural = useParams().objectNamePlural ?? '';
|
const objectNamePlural = useParams().objectNamePlural ?? '';
|
||||||
|
|||||||
@ -1,19 +1,23 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||||
|
|
||||||
export const useShowFullscreen = () => {
|
export const useShowFullscreen = () => {
|
||||||
const { isMatchingLocation } = useIsMatchingLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (
|
if (
|
||||||
isMatchingLocation('settings/' + SettingsPath.RestPlayground + '/*') ||
|
isMatchingLocation(
|
||||||
isMatchingLocation('settings/' + SettingsPath.GraphQLPlayground)
|
location,
|
||||||
|
'settings/' + SettingsPath.RestPlayground + '/*',
|
||||||
|
) ||
|
||||||
|
isMatchingLocation(location, 'settings/' + SettingsPath.GraphQLPlayground)
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}, [isMatchingLocation]);
|
}, [location]);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,17 +1,25 @@
|
|||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import * as reactRouterDom from 'react-router-dom';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
|
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
|
||||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||||
|
|
||||||
jest.mock('~/hooks/useIsMatchingLocation');
|
jest.mock('react-router-dom', () => ({
|
||||||
const mockUseIsMatchingLocation = jest.mocked(useIsMatchingLocation);
|
useLocation: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockUseLocation = reactRouterDom.useLocation as jest.Mock;
|
||||||
|
|
||||||
|
jest.mock('~/utils/isMatchingLocation');
|
||||||
|
const mockIsMatchingLocation = jest.mocked(isMatchingLocation);
|
||||||
|
|
||||||
const setupMockIsMatchingLocation = (pathname: string) => {
|
const setupMockIsMatchingLocation = (pathname: string) => {
|
||||||
mockUseIsMatchingLocation.mockReturnValueOnce({
|
mockUseLocation.mockReturnValue({ pathname });
|
||||||
isMatchingLocation: (path: string) => path === pathname,
|
mockIsMatchingLocation.mockImplementation(
|
||||||
});
|
(_location, path) => path === pathname,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getResult = () =>
|
const getResult = () =>
|
||||||
|
|||||||
@ -1,28 +1,29 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||||
|
|
||||||
export const useShowAuthModal = () => {
|
export const useShowAuthModal = () => {
|
||||||
const { isMatchingLocation } = useIsMatchingLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (
|
if (
|
||||||
isMatchingLocation(AppPath.Invite) ||
|
isMatchingLocation(location, AppPath.Invite) ||
|
||||||
isMatchingLocation(AppPath.InviteTeam) ||
|
isMatchingLocation(location, AppPath.InviteTeam) ||
|
||||||
isMatchingLocation(AppPath.CreateProfile) ||
|
isMatchingLocation(location, AppPath.CreateProfile) ||
|
||||||
isMatchingLocation(AppPath.SyncEmails) ||
|
isMatchingLocation(location, AppPath.SyncEmails) ||
|
||||||
isMatchingLocation(AppPath.ResetPassword) ||
|
isMatchingLocation(location, AppPath.ResetPassword) ||
|
||||||
isMatchingLocation(AppPath.VerifyEmail) ||
|
isMatchingLocation(location, AppPath.VerifyEmail) ||
|
||||||
isMatchingLocation(AppPath.Verify) ||
|
isMatchingLocation(location, AppPath.Verify) ||
|
||||||
isMatchingLocation(AppPath.SignInUp) ||
|
isMatchingLocation(location, AppPath.SignInUp) ||
|
||||||
isMatchingLocation(AppPath.CreateWorkspace) ||
|
isMatchingLocation(location, AppPath.CreateWorkspace) ||
|
||||||
isMatchingLocation(AppPath.PlanRequired) ||
|
isMatchingLocation(location, AppPath.PlanRequired) ||
|
||||||
isMatchingLocation(AppPath.PlanRequiredSuccess)
|
isMatchingLocation(location, AppPath.PlanRequiredSuccess)
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}, [isMatchingLocation]);
|
}, [location]);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,19 +5,20 @@ import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingStat
|
|||||||
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
|
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { UserContext } from '@/users/contexts/UserContext';
|
import { UserContext } from '@/users/contexts/UserContext';
|
||||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
|
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
|
||||||
|
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||||
|
|
||||||
export const UserProvider = ({ children }: React.PropsWithChildren) => {
|
export const UserProvider = ({ children }: React.PropsWithChildren) => {
|
||||||
const isCurrentUserLoaded = useRecoilValue(isCurrentUserLoadedState);
|
const isCurrentUserLoaded = useRecoilValue(isCurrentUserLoadedState);
|
||||||
const { isMatchingLocation } = useIsMatchingLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const dateTimeFormat = useRecoilValue(dateTimeFormatState);
|
const dateTimeFormat = useRecoilValue(dateTimeFormatState);
|
||||||
|
|
||||||
return !isCurrentUserLoaded &&
|
return !isCurrentUserLoaded &&
|
||||||
!isMatchingLocation(AppPath.Verify) &&
|
!isMatchingLocation(location, AppPath.Verify) &&
|
||||||
!isMatchingLocation(AppPath.VerifyEmail) &&
|
!isMatchingLocation(location, AppPath.VerifyEmail) &&
|
||||||
!isMatchingLocation(AppPath.CreateWorkspace) ? (
|
!isMatchingLocation(location, AppPath.CreateWorkspace) ? (
|
||||||
<UserOrMetadataLoader />
|
<UserOrMetadataLoader />
|
||||||
) : (
|
) : (
|
||||||
<UserContext.Provider
|
<UserContext.Provider
|
||||||
|
|||||||
@ -19,16 +19,17 @@ import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDa
|
|||||||
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
|
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { WorkspaceMember } from '~/generated-metadata/graphql';
|
import { WorkspaceMember } from '~/generated-metadata/graphql';
|
||||||
import { useGetCurrentUserQuery } from '~/generated/graphql';
|
import { useGetCurrentUserQuery } from '~/generated/graphql';
|
||||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
|
||||||
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||||
|
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||||
|
|
||||||
export const UserProviderEffect = () => {
|
export const UserProviderEffect = () => {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const { isMatchingLocation } = useIsMatchingLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const [isCurrentUserLoaded, setIsCurrentUserLoaded] = useRecoilState(
|
const [isCurrentUserLoaded, setIsCurrentUserLoaded] = useRecoilState(
|
||||||
isCurrentUserLoadedState,
|
isCurrentUserLoadedState,
|
||||||
@ -53,8 +54,8 @@ export const UserProviderEffect = () => {
|
|||||||
const { loading: queryLoading, data: queryData } = useGetCurrentUserQuery({
|
const { loading: queryLoading, data: queryData } = useGetCurrentUserQuery({
|
||||||
skip:
|
skip:
|
||||||
isCurrentUserLoaded ||
|
isCurrentUserLoaded ||
|
||||||
isMatchingLocation(AppPath.Verify) ||
|
isMatchingLocation(location, AppPath.Verify) ||
|
||||||
isMatchingLocation(AppPath.VerifyEmail),
|
isMatchingLocation(location, AppPath.VerifyEmail),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
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