Skip to content

Commit 747a154

Browse files
fix: defaultHomePagePath to be last visited page or alphatically first active object with the name (#6629)
### ISSUE - Closes #6612 - Closes #6125 - Closes #5949 - Closes #6652 ### Description - [x] need to check changes in jest test. - [x] fallback to alphabetically firstActiveObject with the name if no last visited exist https://github.com/user-attachments/assets/dd11480b-c47f-4393-9857-8a55467061e3 - [x] fallback to last visited page with the last visited view by default if no views would have toggled with subNav or viewChangeDropdown it will fallback to INDEX or if no INDEX view then zero position view, works with both subNavViewBar and viewChangeDropdown. https://github.com/user-attachments/assets/33e97e55-2aa2-4c45-a3ab-fc8e43f4964c https://github.com/user-attachments/assets/d1db76a2-da59-4cd2-81bf-d6119408fbbf - [x] lastVisited view across the objects have been persisted so now navigating back from x object to y or z to x will open always last visited view and defaults to index or zero position view. https://github.com/user-attachments/assets/70a01a11-a7ef-4031-926e-02923551466c - [x] lastVisited Page with view has been persisted across the workspace, scope is per workspace so jumping between workspace will also work to have lastvisited object of particular workspace. https://github.com/user-attachments/assets/25107339-8ec1-4421-9f6e-1da43b8f4816 - [x] when lastVisitedObject has been deactivated and going back from settings should have a fallback Object that is alphabetically First activeObject. https://github.com/user-attachments/assets/6b24a933-b139-49ac-82b2-eac5e4848516 - [x] Creation of new View of **anyType** should also get persist and that should get lastVisitedObject with View in case the user leaves from there right away. https://github.com/user-attachments/assets/80ff7114-051d-4e9b-ab58-0e1e3a7d328c - [x] Similarly deleted view also works. https://github.com/user-attachments/assets/cb0b8043-fba4-4a66-941d-b3fa0a57eb22 - [x] fixed active subnav background when opening object directly with root path **/** , it wasn't showing active subNav background. Before: https://github.com/user-attachments/assets/db341c4a-f1f9-43c4-9838-37d1a1f5ab8e Fixed: https://github.com/user-attachments/assets/0f0fd492-bc5d-4efe-b695-bee4e3f41d4e --------- Co-authored-by: Lucas Bordeau <[email protected]>
1 parent 5deb0ab commit 747a154

21 files changed

+457
-66
lines changed

packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { useIsLogged } from '@/auth/hooks/useIsLogged';
2+
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
23
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
34
import { AppPath } from '@/types/AppPath';
45
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
56
import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql';
6-
import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath';
7+
78
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
89
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
910
import { UNTESTED_APP_PATHS } from '~/testing/constants/UntestedAppPaths';
@@ -38,7 +39,7 @@ const setupMockIsLogged = (isLogged: boolean) => {
3839

3940
const defaultHomePagePath = '/objects/companies';
4041

41-
jest.mock('~/hooks/useDefaultHomePagePath');
42+
jest.mock('@/navigation/hooks/useDefaultHomePagePath');
4243
jest.mocked(useDefaultHomePagePath).mockReturnValue({
4344
defaultHomePagePath,
4445
});

packages/twenty-front/src/hooks/useDefaultHomePagePath.tsx

-31
This file was deleted.

packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { useIsLogged } from '@/auth/hooks/useIsLogged';
2+
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
23
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
34
import { AppPath } from '@/types/AppPath';
45
import { SettingsPath } from '@/types/SettingsPath';
56
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
67
import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql';
7-
import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath';
88
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
99

1010
export const usePageChangeEffectNavigateLocation = () => {
@@ -107,12 +107,13 @@ export const usePageChangeEffectNavigateLocation = () => {
107107

108108
if (
109109
onboardingStatus === OnboardingStatus.Completed &&
110-
isMatchingOnboardingRoute
110+
isMatchingOnboardingRoute &&
111+
isLoggedIn
111112
) {
112113
return defaultHomePagePath;
113114
}
114115

115-
if (isMatchingLocation(AppPath.Index)) {
116+
if (isMatchingLocation(AppPath.Index) && isLoggedIn) {
116117
return defaultHomePagePath;
117118
}
118119

packages/twenty-front/src/hooks/__tests__/useDefaultHomePagePath.test.ts renamed to packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts

+13-10
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,16 @@ import { renderHook } from '@testing-library/react';
22
import { RecoilRoot, useSetRecoilState } from 'recoil';
33

44
import { currentUserState } from '@/auth/states/currentUserState';
5-
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
6-
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
5+
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
6+
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
7+
import {
8+
COMPANY_OBJECT_METADATA_ID,
9+
getObjectMetadataItemsMock,
10+
} from '@/object-metadata/utils/getObjectMetadataItemsMock';
711
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
812
import { AppPath } from '@/types/AppPath';
9-
import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath';
1013
import { mockedUserData } from '~/testing/mock-data/users';
1114

12-
const objectMetadataItem = getObjectMetadataItemsMock()[0];
13-
jest.mock('@/object-metadata/hooks/useObjectMetadataItem');
14-
jest.mocked(useObjectMetadataItem).mockReturnValue({
15-
objectMetadataItem,
16-
});
17-
1815
jest.mock('@/prefetch/hooks/usePrefetchedData');
1916
const setupMockPrefetchedData = (viewId?: string) => {
2017
jest.mocked(usePrefetchedData).mockReturnValue({
@@ -24,7 +21,7 @@ const setupMockPrefetchedData = (viewId?: string) => {
2421
{
2522
id: viewId,
2623
__typename: 'object',
27-
objectMetadataId: objectMetadataItem.id,
24+
objectMetadataId: COMPANY_OBJECT_METADATA_ID,
2825
},
2926
]
3027
: [],
@@ -35,6 +32,12 @@ const renderHooks = (withCurrentUser: boolean) => {
3532
const { result } = renderHook(
3633
() => {
3734
const setCurrentUser = useSetRecoilState(currentUserState);
35+
const setObjectMetadataItems = useSetRecoilState(
36+
objectMetadataItemsState,
37+
);
38+
39+
setObjectMetadataItems(getObjectMetadataItemsMock());
40+
3841
if (withCurrentUser) {
3942
setCurrentUser(mockedUserData);
4043
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { currentUserState } from '@/auth/states/currentUserState';
2+
import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisitedObjectMetadataItem';
3+
import { ObjectPathInfo } from '@/navigation/types/ObjectPathInfo';
4+
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
5+
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
6+
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
7+
import { AppPath } from '@/types/AppPath';
8+
import { View } from '@/views/types/View';
9+
import { useCallback, useMemo } from 'react';
10+
import { useRecoilValue } from 'recoil';
11+
import { isDefined } from '~/utils/isDefined';
12+
13+
export const useDefaultHomePagePath = () => {
14+
const currentUser = useRecoilValue(currentUserState);
15+
const { activeObjectMetadataItems, alphaSortedActiveObjectMetadataItems } =
16+
useFilteredObjectMetadataItems();
17+
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
18+
const { lastVisitedObjectMetadataItemId } =
19+
useLastVisitedObjectMetadataItem();
20+
21+
const getActiveObjectMetadataItemMatchingId = useCallback(
22+
(objectMetadataId: string) => {
23+
return activeObjectMetadataItems.find(
24+
(item) => item.id === objectMetadataId,
25+
);
26+
},
27+
[activeObjectMetadataItems],
28+
);
29+
30+
const getFirstView = useCallback(
31+
(objectMetadataItemId: string | undefined | null) =>
32+
views.find((view) => view.objectMetadataId === objectMetadataItemId),
33+
[views],
34+
);
35+
36+
const firstObjectPathInfo = useMemo<ObjectPathInfo | null>(() => {
37+
const [firstObjectMetadataItem] = alphaSortedActiveObjectMetadataItems;
38+
39+
if (!isDefined(firstObjectMetadataItem)) {
40+
return null;
41+
}
42+
43+
const view = getFirstView(firstObjectMetadataItem?.id);
44+
45+
return { objectMetadataItem: firstObjectMetadataItem, view };
46+
}, [alphaSortedActiveObjectMetadataItems, getFirstView]);
47+
48+
const defaultObjectPathInfo = useMemo<ObjectPathInfo | null>(() => {
49+
if (!isDefined(lastVisitedObjectMetadataItemId)) {
50+
return firstObjectPathInfo;
51+
}
52+
53+
const lastVisitedObjectMetadataItem = getActiveObjectMetadataItemMatchingId(
54+
lastVisitedObjectMetadataItemId,
55+
);
56+
57+
if (isDefined(lastVisitedObjectMetadataItem)) {
58+
return {
59+
view: getFirstView(lastVisitedObjectMetadataItemId),
60+
objectMetadataItem: lastVisitedObjectMetadataItem,
61+
};
62+
}
63+
64+
return firstObjectPathInfo;
65+
}, [
66+
firstObjectPathInfo,
67+
getActiveObjectMetadataItemMatchingId,
68+
getFirstView,
69+
lastVisitedObjectMetadataItemId,
70+
]);
71+
72+
const defaultHomePagePath = useMemo(() => {
73+
if (!isDefined(currentUser)) {
74+
return AppPath.SignInUp;
75+
}
76+
77+
if (!isDefined(defaultObjectPathInfo)) {
78+
return AppPath.NotFound;
79+
}
80+
81+
const namePlural = defaultObjectPathInfo.objectMetadataItem?.namePlural;
82+
const viewParam = defaultObjectPathInfo.view
83+
? `?view=${defaultObjectPathInfo.view.id}`
84+
: '';
85+
86+
return `/objects/${namePlural}${viewParam}`;
87+
}, [currentUser, defaultObjectPathInfo]);
88+
89+
return { defaultHomePagePath };
90+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
2+
import { lastVisitedObjectMetadataItemIdStateSelector } from '@/navigation/states/selectors/lastVisitedObjectMetadataItemIdStateSelector';
3+
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
4+
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
5+
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
6+
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
7+
import { isDefined } from 'twenty-ui';
8+
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
9+
10+
export const useLastVisitedObjectMetadataItem = () => {
11+
const currentWorkspace = useRecoilValue(currentWorkspaceState);
12+
const scopeId = currentWorkspace?.id ?? '';
13+
14+
const lastVisitedObjectMetadataItemIdState = extractComponentState(
15+
lastVisitedObjectMetadataItemIdStateSelector,
16+
scopeId,
17+
);
18+
19+
const [lastVisitedObjectMetadataItemId, setLastVisitedObjectMetadataItemId] =
20+
useRecoilState(lastVisitedObjectMetadataItemIdState);
21+
22+
const {
23+
findActiveObjectMetadataItemBySlug,
24+
alphaSortedActiveObjectMetadataItems,
25+
} = useFilteredObjectMetadataItems();
26+
27+
const setNavigationMemorizedUrl = useSetRecoilState(
28+
navigationMemorizedUrlState,
29+
);
30+
31+
const setFallbackForLastVisitedObjectMetadataItem = (
32+
objectMetadataItemId: string,
33+
) => {
34+
const isDeactivateDefault = isDeeplyEqual(
35+
lastVisitedObjectMetadataItemId,
36+
objectMetadataItemId,
37+
);
38+
39+
const [newFallbackObjectMetadataItem] =
40+
alphaSortedActiveObjectMetadataItems.filter(
41+
(item) => item.id !== objectMetadataItemId,
42+
);
43+
44+
if (isDeactivateDefault) {
45+
setLastVisitedObjectMetadataItemId(newFallbackObjectMetadataItem.id);
46+
setNavigationMemorizedUrl(
47+
`/objects/${newFallbackObjectMetadataItem.namePlural}`,
48+
);
49+
}
50+
};
51+
52+
const setLastVisitedObjectMetadataItem = (objectNamePlural: string) => {
53+
const fallbackObjectMetadataItem =
54+
findActiveObjectMetadataItemBySlug(objectNamePlural);
55+
56+
if (isDefined(fallbackObjectMetadataItem)) {
57+
setLastVisitedObjectMetadataItemId(fallbackObjectMetadataItem.id);
58+
}
59+
};
60+
61+
return {
62+
lastVisitedObjectMetadataItemId,
63+
setLastVisitedObjectMetadataItem,
64+
setFallbackForLastVisitedObjectMetadataItem,
65+
};
66+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
2+
import { lastVisitedObjectMetadataItemIdStateSelector } from '@/navigation/states/selectors/lastVisitedObjectMetadataItemIdStateSelector';
3+
import { lastVisitedViewPerObjectMetadataItemStateSelector } from '@/navigation/states/selectors/lastVisitedViewPerObjectMetadataItemStateSelector';
4+
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
5+
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
6+
import { useRecoilState, useRecoilValue } from 'recoil';
7+
import { isDefined } from 'twenty-ui';
8+
9+
export const useLastVisitedView = () => {
10+
const currentWorkspace = useRecoilValue(currentWorkspaceState);
11+
const scopeId = currentWorkspace?.id ?? '';
12+
13+
const lastVisitedObjectMetadataItemIdState = extractComponentState(
14+
lastVisitedObjectMetadataItemIdStateSelector,
15+
scopeId,
16+
);
17+
18+
const lastVisitedViewPerObjectMetadataItemState = extractComponentState(
19+
lastVisitedViewPerObjectMetadataItemStateSelector,
20+
scopeId,
21+
);
22+
23+
const lastVisitedObjectMetadataItemId = useRecoilValue(
24+
lastVisitedObjectMetadataItemIdState,
25+
);
26+
27+
const [
28+
lastVisitedViewPerObjectMetadataItem,
29+
setLastVisitedViewPerObjectMetadataItem,
30+
] = useRecoilState(lastVisitedViewPerObjectMetadataItemState);
31+
32+
const { findActiveObjectMetadataItemBySlug } =
33+
useFilteredObjectMetadataItems();
34+
35+
const setFallbackForLastVisitedView = (objectMetadataItemId: string) => {
36+
/* ...{} allows us to pass value as undefined to remove that particular key
37+
even though param type is of type Record<string,string> */
38+
setLastVisitedViewPerObjectMetadataItem({
39+
...{},
40+
[objectMetadataItemId]: undefined,
41+
});
42+
};
43+
44+
const setLastVisitedView = ({
45+
objectNamePlural,
46+
viewId,
47+
}: {
48+
objectNamePlural: string;
49+
viewId: string;
50+
}) => {
51+
const fallbackObjectMetadataItem =
52+
findActiveObjectMetadataItemBySlug(objectNamePlural);
53+
54+
if (isDefined(fallbackObjectMetadataItem)) {
55+
/* when both are equal meaning there was change in view else
56+
there was a object page change from nav
57+
*/
58+
const fallbackViewId =
59+
lastVisitedObjectMetadataItemId === fallbackObjectMetadataItem.id
60+
? viewId
61+
: (lastVisitedViewPerObjectMetadataItem?.[
62+
fallbackObjectMetadataItem.id
63+
] ?? viewId);
64+
65+
setLastVisitedViewPerObjectMetadataItem({
66+
[fallbackObjectMetadataItem.id]: fallbackViewId,
67+
});
68+
}
69+
};
70+
71+
const getLastVisitedViewIdFromObjectNamePlural = (
72+
objectNamePlural: string,
73+
) => {
74+
const objectMetadataItemId: string | undefined =
75+
findActiveObjectMetadataItemBySlug(objectNamePlural)?.id;
76+
return objectMetadataItemId
77+
? lastVisitedViewPerObjectMetadataItem?.[objectMetadataItemId]
78+
: undefined;
79+
};
80+
81+
const getLastVisitedViewIdFromObjectMetadataItemId = (
82+
objectMetadataItemId: string,
83+
) => {
84+
return lastVisitedViewPerObjectMetadataItem?.[objectMetadataItemId];
85+
};
86+
return {
87+
setLastVisitedView,
88+
getLastVisitedViewIdFromObjectNamePlural,
89+
getLastVisitedViewIdFromObjectMetadataItemId,
90+
setFallbackForLastVisitedView,
91+
};
92+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
2+
import { localStorageEffect } from '~/utils/recoil-effects';
3+
4+
export const lastVisitedObjectMetadataItemIdState = createComponentState<Record<
5+
string,
6+
string
7+
> | null>({
8+
key: 'lastVisitedObjectMetadataItemIdState',
9+
defaultValue: null,
10+
effects: [localStorageEffect()],
11+
});

0 commit comments

Comments
 (0)