From 9ac503e3afb3eba6fc9154f6cc8ce53d2aa1cefc Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Thu, 7 May 2026 09:51:55 +0200 Subject: [PATCH] fix(front): defer default home redirect when object metadata is not loaded (#20330) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes the merge-queue E2E failures introduced after #20308. After login, users were being silently redirected to `/settings/profile` instead of their workspace home, which broke every dependent E2E test that re-uses the post-login URL (`workflow-creation.spec.ts`, `authentication/signup_invite_email.spec.ts`, etc.). ## Root cause `useDefaultHomePagePath` falls back to `/settings/profile` when `readableNonSystemObjectMetadataItems` is empty. That list is empty in two cases: 1. The user genuinely has no readable objects → `/settings/profile` is the intended fallback. 2. Object metadata simply hasn't been loaded yet (transient post-login window). Before #20308 the frontend always loaded mocked metadata for authenticated users, so case (2) never happened. After #20308 mocked metadata is gone, and during the post-verify window (`handleLoadWorkspaceAfterAuthentication` finishes, `setIsAppEffectRedirectEnabled(true)` re-enables redirects, `PageChangeEffect` fires) the metadata store is still empty. The hook then returns `/settings/profile`. Because that path is not in `ONBOARDING_PATHS` / `ONGOING_USER_CREATION_PATHS`, `usePageChangeEffectNavigateLocation` doesn't fire a corrective redirect once metadata finally loads — the user is stranded. `login.setup.ts` captures `process.env.LINK = page.url()` after verify, so subsequent tests `goto(LINK)` end up in Settings looking for app navigation that isn't there → click timeouts. ## Fix Distinguish the two empty cases by reading `metadataStoreState('objectMetadataItems').status`. If it isn't `'up-to-date'` we defer to `AppPath.Index` instead of `/settings/profile`. The memo recomputes when the status flips, and the user is then routed to their actual home page. A regression test is added in `useDefaultHomePagePath.test.ts` for the not-loaded-yet case. ## Test plan - [x] Unit: `npx jest src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts` (5/5 pass, including new regression case) - [ ] CI: Playwright E2E (`workflow-creation.spec.ts`, `authentication/signup_invite_email.spec.ts`) pass on this branch - [ ] Manual: log in to a fresh local instance and confirm landing page is the workspace home, not `/settings/profile` --- .../__tests__/useDefaultHomePagePath.test.ts | 33 ++++++++++++++++--- .../hooks/useDefaultHomePagePath.ts | 17 ++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts b/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts index 0a41f9d3cb..83e9b4c7e5 100644 --- a/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts +++ b/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts @@ -1,5 +1,6 @@ import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; +import { metadataStoreState } from '@/metadata-store/states/metadataStoreState'; import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath'; import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; @@ -25,14 +26,24 @@ const Wrapper = ({ children }: { children: ReactNode }) => const renderHooks = ({ withCurrentUser, withExistingView, + withObjectMetadataLoaded = true, }: { withCurrentUser: boolean; withExistingView: boolean; + withObjectMetadataLoaded?: boolean; }) => { - setTestObjectMetadataItemsInMetadataStore( - jotaiStore, - getTestEnrichedObjectMetadataItemsMock(), - ); + if (withObjectMetadataLoaded) { + setTestObjectMetadataItemsInMetadataStore( + jotaiStore, + getTestEnrichedObjectMetadataItemsMock(), + ); + } else { + jotaiStore.set(metadataStoreState.atomFamily('objectMetadataItems'), { + current: [], + draft: [], + status: 'empty', + }); + } const { result } = renderHook( () => { @@ -129,4 +140,18 @@ describe('useDefaultHomePagePath', () => { ); }); }); + // Regression: during the post-login transition window object metadata may + // not yet be loaded. We must not redirect the user to /settings/profile + // (the genuine empty-fallback) until metadata has actually loaded. + it('should defer to AppPath.Index when currentUser is defined but object metadata is not loaded yet', async () => { + const { result } = renderHooks({ + withCurrentUser: true, + withExistingView: false, + withObjectMetadataLoaded: false, + }); + + await waitFor(() => { + expect(result.current.defaultHomePagePath).toEqual(AppPath.Index); + }); + }); }); diff --git a/packages/twenty-front/src/modules/navigation/hooks/useDefaultHomePagePath.ts b/packages/twenty-front/src/modules/navigation/hooks/useDefaultHomePagePath.ts index 72802253f8..516d226c87 100644 --- a/packages/twenty-front/src/modules/navigation/hooks/useDefaultHomePagePath.ts +++ b/packages/twenty-front/src/modules/navigation/hooks/useDefaultHomePagePath.ts @@ -1,9 +1,11 @@ import { currentUserState } from '@/auth/states/currentUserState'; +import { metadataStoreState } from '@/metadata-store/states/metadataStoreState'; import { lastVisitedObjectMetadataItemIdState } from '@/navigation/states/lastVisitedObjectMetadataItemIdState'; import { type ObjectPathInfo } from '@/navigation/types/ObjectPathInfo'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { filterReadableActiveObjectMetadataItems } from '@/object-metadata/utils/filterReadableActiveObjectMetadataItems'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { viewsSelector } from '@/views/states/selectors/viewsSelector'; import isEmpty from 'lodash.isempty'; @@ -16,6 +18,11 @@ export const useDefaultHomePagePath = () => { const store = useStore(); const currentUser = useAtomStateValue(currentUserState); const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const metadataStore = useAtomFamilyStateValue( + metadataStoreState, + 'objectMetadataItems', + ); + const areObjectMetadataItemsLoaded = metadataStore.status === 'up-to-date'; const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); @@ -94,6 +101,15 @@ export const useDefaultHomePagePath = () => { } if (isEmpty(readableNonSystemObjectMetadataItems)) { + // Object metadata may legitimately be empty for a user with no readable + // objects, in which case /settings/profile is the intended fallback. + // It can also be transiently empty during the post-login window before + // workspace metadata has finished loading. Defer to AppPath.Index in + // that case so the user isn't stranded on /settings/profile once + // metadata becomes available. + if (!areObjectMetadataItemsLoaded) { + return AppPath.Index; + } return getSettingsPath(SettingsPath.ProfilePage); } @@ -115,6 +131,7 @@ export const useDefaultHomePagePath = () => { currentUser, getDefaultObjectPathInfo, readableNonSystemObjectMetadataItems, + areObjectMetadataItemsLoaded, ]); return { defaultHomePagePath };