9ac503e3af
## 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`
139 lines
4.9 KiB
TypeScript
139 lines
4.9 KiB
TypeScript
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';
|
|
import { useCallback, useMemo } from 'react';
|
|
import { AppPath, SettingsPath } from 'twenty-shared/types';
|
|
import { getAppPath, getSettingsPath, isDefined } from 'twenty-shared/utils';
|
|
import { useStore } from 'jotai';
|
|
|
|
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();
|
|
|
|
const readableNonSystemObjectMetadataItems = useMemo(
|
|
() =>
|
|
filterReadableActiveObjectMetadataItems(
|
|
activeObjectMetadataItems,
|
|
objectPermissionsByObjectMetadataId,
|
|
)
|
|
.filter((item) => !item.isSystem)
|
|
.sort((a, b) => a.nameSingular.localeCompare(b.nameSingular)),
|
|
[activeObjectMetadataItems, objectPermissionsByObjectMetadataId],
|
|
);
|
|
|
|
const getActiveObjectMetadataItemMatchingId = useCallback(
|
|
(objectMetadataId: string) => {
|
|
return readableNonSystemObjectMetadataItems.find(
|
|
(item) => item.id === objectMetadataId,
|
|
);
|
|
},
|
|
[readableNonSystemObjectMetadataItems],
|
|
);
|
|
|
|
const views = useAtomStateValue(viewsSelector);
|
|
|
|
const getFirstView = useCallback(
|
|
(objectMetadataItemId: string | undefined | null) => {
|
|
return views.find(
|
|
(view) => view.objectMetadataId === objectMetadataItemId,
|
|
);
|
|
},
|
|
[views],
|
|
);
|
|
|
|
const firstObjectPathInfo = useMemo<ObjectPathInfo | null>(() => {
|
|
const [firstObjectMetadataItem] = readableNonSystemObjectMetadataItems;
|
|
|
|
if (!isDefined(firstObjectMetadataItem)) {
|
|
return null;
|
|
}
|
|
|
|
const view = getFirstView(firstObjectMetadataItem?.id);
|
|
|
|
return { objectMetadataItem: firstObjectMetadataItem, view };
|
|
}, [getFirstView, readableNonSystemObjectMetadataItems]);
|
|
|
|
const getDefaultObjectPathInfo = useCallback(() => {
|
|
const lastVisitedObjectMetadataItemId = store.get(
|
|
lastVisitedObjectMetadataItemIdState.atom,
|
|
);
|
|
|
|
const lastVisitedObjectMetadataItem = isDefined(
|
|
lastVisitedObjectMetadataItemId,
|
|
)
|
|
? getActiveObjectMetadataItemMatchingId(lastVisitedObjectMetadataItemId)
|
|
: undefined;
|
|
|
|
if (isDefined(lastVisitedObjectMetadataItem)) {
|
|
return {
|
|
view: getFirstView(lastVisitedObjectMetadataItemId),
|
|
objectMetadataItem: lastVisitedObjectMetadataItem,
|
|
};
|
|
}
|
|
|
|
return firstObjectPathInfo;
|
|
}, [
|
|
firstObjectPathInfo,
|
|
getActiveObjectMetadataItemMatchingId,
|
|
getFirstView,
|
|
store,
|
|
]);
|
|
|
|
const defaultHomePagePath = useMemo(() => {
|
|
if (!isDefined(currentUser)) {
|
|
return AppPath.SignInUp;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
const defaultObjectPathInfo = getDefaultObjectPathInfo();
|
|
|
|
if (!isDefined(defaultObjectPathInfo)) {
|
|
return AppPath.NotFound;
|
|
}
|
|
|
|
const namePlural = defaultObjectPathInfo.objectMetadataItem?.namePlural;
|
|
const viewId = defaultObjectPathInfo.view?.id;
|
|
|
|
return getAppPath(
|
|
AppPath.RecordIndexPage,
|
|
{ objectNamePlural: namePlural },
|
|
viewId ? { viewId } : undefined,
|
|
);
|
|
}, [
|
|
currentUser,
|
|
getDefaultObjectPathInfo,
|
|
readableNonSystemObjectMetadataItems,
|
|
areObjectMetadataItemsLoaded,
|
|
]);
|
|
|
|
return { defaultHomePagePath };
|
|
};
|