From 747a1549e9d8423cfa622682c2455207e03ad1ce Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras <65061890+Nabhag8848@users.noreply.github.com> Date: Wed, 28 Aug 2024 20:45:54 +0530 Subject: [PATCH] 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 --- ...sePageChangeEffectNavigateLocation.test.ts | 5 +- .../src/hooks/useDefaultHomePagePath.tsx | 31 ------- .../usePageChangeEffectNavigateLocation.ts | 7 +- .../__tests__/useDefaultHomePagePath.test.ts | 23 +++-- .../hooks/useDefaultHomePagePath.ts | 90 ++++++++++++++++++ .../hooks/useLastVisitedObjectMetadataItem.ts | 66 +++++++++++++ .../navigation/hooks/useLastVisitedView.ts | 92 +++++++++++++++++++ .../lastVisitedObjectMetadataItemIdState.ts | 11 +++ ...stVisitedViewPerObjectMetadataItemState.ts | 9 ++ ...isitedObjectMetadataItemIdStateSelector.ts | 24 +++++ ...dViewPerObjectMetadataItemStateSelector.ts | 34 +++++++ .../navigation/types/ObjectPathInfo.ts | 7 ++ .../ObjectMetadataItemsLoadEffect.tsx | 4 +- .../components/ObjectMetadataNavItems.tsx | 14 +-- .../hooks/useFilteredObjectMetadataItems.ts | 14 +++ .../utils/getObjectMetadataItemsMock.ts | 1 + .../components/SettingsObjectSummaryCard.tsx | 8 ++ .../components/NavigationDrawerItem.tsx | 9 +- .../components/NavigationDrawerSubItem.tsx | 3 + .../components/QueryParamsViewIdEffect.tsx | 69 ++++++++++++-- .../components/ViewPickerListContent.tsx | 2 - 21 files changed, 457 insertions(+), 66 deletions(-) delete mode 100644 packages/twenty-front/src/hooks/useDefaultHomePagePath.tsx rename packages/twenty-front/src/{ => modules/navigation}/hooks/__tests__/useDefaultHomePagePath.test.ts (79%) create mode 100644 packages/twenty-front/src/modules/navigation/hooks/useDefaultHomePagePath.ts create mode 100644 packages/twenty-front/src/modules/navigation/hooks/useLastVisitedObjectMetadataItem.ts create mode 100644 packages/twenty-front/src/modules/navigation/hooks/useLastVisitedView.ts create mode 100644 packages/twenty-front/src/modules/navigation/states/lastVisitedObjectMetadataItemIdState.ts create mode 100644 packages/twenty-front/src/modules/navigation/states/lastVisitedViewPerObjectMetadataItemState.ts create mode 100644 packages/twenty-front/src/modules/navigation/states/selectors/lastVisitedObjectMetadataItemIdStateSelector.ts create mode 100644 packages/twenty-front/src/modules/navigation/states/selectors/lastVisitedViewPerObjectMetadataItemStateSelector.ts create mode 100644 packages/twenty-front/src/modules/navigation/types/ObjectPathInfo.ts diff --git a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts index ad989ea4f695..95c2a58b79c6 100644 --- a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts +++ b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts @@ -1,9 +1,10 @@ import { useIsLogged } from '@/auth/hooks/useIsLogged'; +import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath'; import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus'; import { AppPath } from '@/types/AppPath'; import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql'; -import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath'; + import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation'; import { UNTESTED_APP_PATHS } from '~/testing/constants/UntestedAppPaths'; @@ -38,7 +39,7 @@ const setupMockIsLogged = (isLogged: boolean) => { const defaultHomePagePath = '/objects/companies'; -jest.mock('~/hooks/useDefaultHomePagePath'); +jest.mock('@/navigation/hooks/useDefaultHomePagePath'); jest.mocked(useDefaultHomePagePath).mockReturnValue({ defaultHomePagePath, }); diff --git a/packages/twenty-front/src/hooks/useDefaultHomePagePath.tsx b/packages/twenty-front/src/hooks/useDefaultHomePagePath.tsx deleted file mode 100644 index 8332f5113e6a..000000000000 --- a/packages/twenty-front/src/hooks/useDefaultHomePagePath.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useRecoilValue } from 'recoil'; - -import { currentUserState } from '@/auth/states/currentUserState'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; -import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; -import { AppPath } from '@/types/AppPath'; -import { isDefined } from '~/utils/isDefined'; - -export const useDefaultHomePagePath = () => { - const currentUser = useRecoilValue(currentUserState); - const { objectMetadataItem: companyObjectMetadataItem } = - useObjectMetadataItem({ - objectNameSingular: CoreObjectNameSingular.Company, - }); - const { records } = usePrefetchedData(PrefetchKey.AllViews); - - if (!isDefined(currentUser)) { - return { defaultHomePagePath: AppPath.SignInUp }; - } - - const companyViewId = records.find( - (view: any) => view?.objectMetadataId === companyObjectMetadataItem.id, - )?.id; - - return { - defaultHomePagePath: - '/objects/companies' + (companyViewId ? `?view=${companyViewId}` : ''), - }; -}; diff --git a/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts b/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts index ead32840253a..5d637c77b5fb 100644 --- a/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts +++ b/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts @@ -1,10 +1,10 @@ import { useIsLogged } from '@/auth/hooks/useIsLogged'; +import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath'; import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus'; import { AppPath } from '@/types/AppPath'; import { SettingsPath } from '@/types/SettingsPath'; import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql'; -import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; export const usePageChangeEffectNavigateLocation = () => { @@ -107,12 +107,13 @@ export const usePageChangeEffectNavigateLocation = () => { if ( onboardingStatus === OnboardingStatus.Completed && - isMatchingOnboardingRoute + isMatchingOnboardingRoute && + isLoggedIn ) { return defaultHomePagePath; } - if (isMatchingLocation(AppPath.Index)) { + if (isMatchingLocation(AppPath.Index) && isLoggedIn) { return defaultHomePagePath; } diff --git a/packages/twenty-front/src/hooks/__tests__/useDefaultHomePagePath.test.ts b/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts similarity index 79% rename from packages/twenty-front/src/hooks/__tests__/useDefaultHomePagePath.test.ts rename to packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts index 709c071cce76..8efee5d0c42d 100644 --- a/packages/twenty-front/src/hooks/__tests__/useDefaultHomePagePath.test.ts +++ b/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts @@ -2,19 +2,16 @@ import { renderHook } from '@testing-library/react'; import { RecoilRoot, useSetRecoilState } from 'recoil'; import { currentUserState } from '@/auth/states/currentUserState'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; +import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { + COMPANY_OBJECT_METADATA_ID, + getObjectMetadataItemsMock, +} from '@/object-metadata/utils/getObjectMetadataItemsMock'; import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; import { AppPath } from '@/types/AppPath'; -import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath'; import { mockedUserData } from '~/testing/mock-data/users'; -const objectMetadataItem = getObjectMetadataItemsMock()[0]; -jest.mock('@/object-metadata/hooks/useObjectMetadataItem'); -jest.mocked(useObjectMetadataItem).mockReturnValue({ - objectMetadataItem, -}); - jest.mock('@/prefetch/hooks/usePrefetchedData'); const setupMockPrefetchedData = (viewId?: string) => { jest.mocked(usePrefetchedData).mockReturnValue({ @@ -24,7 +21,7 @@ const setupMockPrefetchedData = (viewId?: string) => { { id: viewId, __typename: 'object', - objectMetadataId: objectMetadataItem.id, + objectMetadataId: COMPANY_OBJECT_METADATA_ID, }, ] : [], @@ -35,6 +32,12 @@ const renderHooks = (withCurrentUser: boolean) => { const { result } = renderHook( () => { const setCurrentUser = useSetRecoilState(currentUserState); + const setObjectMetadataItems = useSetRecoilState( + objectMetadataItemsState, + ); + + setObjectMetadataItems(getObjectMetadataItemsMock()); + if (withCurrentUser) { setCurrentUser(mockedUserData); } diff --git a/packages/twenty-front/src/modules/navigation/hooks/useDefaultHomePagePath.ts b/packages/twenty-front/src/modules/navigation/hooks/useDefaultHomePagePath.ts new file mode 100644 index 000000000000..8f3afb3e193b --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/hooks/useDefaultHomePagePath.ts @@ -0,0 +1,90 @@ +import { currentUserState } from '@/auth/states/currentUserState'; +import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisitedObjectMetadataItem'; +import { ObjectPathInfo } from '@/navigation/types/ObjectPathInfo'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; +import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; +import { AppPath } from '@/types/AppPath'; +import { View } from '@/views/types/View'; +import { useCallback, useMemo } from 'react'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from '~/utils/isDefined'; + +export const useDefaultHomePagePath = () => { + const currentUser = useRecoilValue(currentUserState); + const { activeObjectMetadataItems, alphaSortedActiveObjectMetadataItems } = + useFilteredObjectMetadataItems(); + const { records: views } = usePrefetchedData(PrefetchKey.AllViews); + const { lastVisitedObjectMetadataItemId } = + useLastVisitedObjectMetadataItem(); + + const getActiveObjectMetadataItemMatchingId = useCallback( + (objectMetadataId: string) => { + return activeObjectMetadataItems.find( + (item) => item.id === objectMetadataId, + ); + }, + [activeObjectMetadataItems], + ); + + const getFirstView = useCallback( + (objectMetadataItemId: string | undefined | null) => + views.find((view) => view.objectMetadataId === objectMetadataItemId), + [views], + ); + + const firstObjectPathInfo = useMemo(() => { + const [firstObjectMetadataItem] = alphaSortedActiveObjectMetadataItems; + + if (!isDefined(firstObjectMetadataItem)) { + return null; + } + + const view = getFirstView(firstObjectMetadataItem?.id); + + return { objectMetadataItem: firstObjectMetadataItem, view }; + }, [alphaSortedActiveObjectMetadataItems, getFirstView]); + + const defaultObjectPathInfo = useMemo(() => { + if (!isDefined(lastVisitedObjectMetadataItemId)) { + return firstObjectPathInfo; + } + + const lastVisitedObjectMetadataItem = getActiveObjectMetadataItemMatchingId( + lastVisitedObjectMetadataItemId, + ); + + if (isDefined(lastVisitedObjectMetadataItem)) { + return { + view: getFirstView(lastVisitedObjectMetadataItemId), + objectMetadataItem: lastVisitedObjectMetadataItem, + }; + } + + return firstObjectPathInfo; + }, [ + firstObjectPathInfo, + getActiveObjectMetadataItemMatchingId, + getFirstView, + lastVisitedObjectMetadataItemId, + ]); + + const defaultHomePagePath = useMemo(() => { + if (!isDefined(currentUser)) { + return AppPath.SignInUp; + } + + if (!isDefined(defaultObjectPathInfo)) { + return AppPath.NotFound; + } + + const namePlural = defaultObjectPathInfo.objectMetadataItem?.namePlural; + const viewParam = defaultObjectPathInfo.view + ? `?view=${defaultObjectPathInfo.view.id}` + : ''; + + return `/objects/${namePlural}${viewParam}`; + }, [currentUser, defaultObjectPathInfo]); + + return { defaultHomePagePath }; +}; diff --git a/packages/twenty-front/src/modules/navigation/hooks/useLastVisitedObjectMetadataItem.ts b/packages/twenty-front/src/modules/navigation/hooks/useLastVisitedObjectMetadataItem.ts new file mode 100644 index 000000000000..45e05d9ab78d --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/hooks/useLastVisitedObjectMetadataItem.ts @@ -0,0 +1,66 @@ +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { lastVisitedObjectMetadataItemIdStateSelector } from '@/navigation/states/selectors/lastVisitedObjectMetadataItemIdStateSelector'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; +import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-ui'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; + +export const useLastVisitedObjectMetadataItem = () => { + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const scopeId = currentWorkspace?.id ?? ''; + + const lastVisitedObjectMetadataItemIdState = extractComponentState( + lastVisitedObjectMetadataItemIdStateSelector, + scopeId, + ); + + const [lastVisitedObjectMetadataItemId, setLastVisitedObjectMetadataItemId] = + useRecoilState(lastVisitedObjectMetadataItemIdState); + + const { + findActiveObjectMetadataItemBySlug, + alphaSortedActiveObjectMetadataItems, + } = useFilteredObjectMetadataItems(); + + const setNavigationMemorizedUrl = useSetRecoilState( + navigationMemorizedUrlState, + ); + + const setFallbackForLastVisitedObjectMetadataItem = ( + objectMetadataItemId: string, + ) => { + const isDeactivateDefault = isDeeplyEqual( + lastVisitedObjectMetadataItemId, + objectMetadataItemId, + ); + + const [newFallbackObjectMetadataItem] = + alphaSortedActiveObjectMetadataItems.filter( + (item) => item.id !== objectMetadataItemId, + ); + + if (isDeactivateDefault) { + setLastVisitedObjectMetadataItemId(newFallbackObjectMetadataItem.id); + setNavigationMemorizedUrl( + `/objects/${newFallbackObjectMetadataItem.namePlural}`, + ); + } + }; + + const setLastVisitedObjectMetadataItem = (objectNamePlural: string) => { + const fallbackObjectMetadataItem = + findActiveObjectMetadataItemBySlug(objectNamePlural); + + if (isDefined(fallbackObjectMetadataItem)) { + setLastVisitedObjectMetadataItemId(fallbackObjectMetadataItem.id); + } + }; + + return { + lastVisitedObjectMetadataItemId, + setLastVisitedObjectMetadataItem, + setFallbackForLastVisitedObjectMetadataItem, + }; +}; diff --git a/packages/twenty-front/src/modules/navigation/hooks/useLastVisitedView.ts b/packages/twenty-front/src/modules/navigation/hooks/useLastVisitedView.ts new file mode 100644 index 000000000000..080fbc9d7320 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/hooks/useLastVisitedView.ts @@ -0,0 +1,92 @@ +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { lastVisitedObjectMetadataItemIdStateSelector } from '@/navigation/states/selectors/lastVisitedObjectMetadataItemIdStateSelector'; +import { lastVisitedViewPerObjectMetadataItemStateSelector } from '@/navigation/states/selectors/lastVisitedViewPerObjectMetadataItemStateSelector'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const useLastVisitedView = () => { + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const scopeId = currentWorkspace?.id ?? ''; + + const lastVisitedObjectMetadataItemIdState = extractComponentState( + lastVisitedObjectMetadataItemIdStateSelector, + scopeId, + ); + + const lastVisitedViewPerObjectMetadataItemState = extractComponentState( + lastVisitedViewPerObjectMetadataItemStateSelector, + scopeId, + ); + + const lastVisitedObjectMetadataItemId = useRecoilValue( + lastVisitedObjectMetadataItemIdState, + ); + + const [ + lastVisitedViewPerObjectMetadataItem, + setLastVisitedViewPerObjectMetadataItem, + ] = useRecoilState(lastVisitedViewPerObjectMetadataItemState); + + const { findActiveObjectMetadataItemBySlug } = + useFilteredObjectMetadataItems(); + + const setFallbackForLastVisitedView = (objectMetadataItemId: string) => { + /* ...{} allows us to pass value as undefined to remove that particular key + even though param type is of type Record */ + setLastVisitedViewPerObjectMetadataItem({ + ...{}, + [objectMetadataItemId]: undefined, + }); + }; + + const setLastVisitedView = ({ + objectNamePlural, + viewId, + }: { + objectNamePlural: string; + viewId: string; + }) => { + const fallbackObjectMetadataItem = + findActiveObjectMetadataItemBySlug(objectNamePlural); + + if (isDefined(fallbackObjectMetadataItem)) { + /* when both are equal meaning there was change in view else + there was a object page change from nav + */ + const fallbackViewId = + lastVisitedObjectMetadataItemId === fallbackObjectMetadataItem.id + ? viewId + : (lastVisitedViewPerObjectMetadataItem?.[ + fallbackObjectMetadataItem.id + ] ?? viewId); + + setLastVisitedViewPerObjectMetadataItem({ + [fallbackObjectMetadataItem.id]: fallbackViewId, + }); + } + }; + + const getLastVisitedViewIdFromObjectNamePlural = ( + objectNamePlural: string, + ) => { + const objectMetadataItemId: string | undefined = + findActiveObjectMetadataItemBySlug(objectNamePlural)?.id; + return objectMetadataItemId + ? lastVisitedViewPerObjectMetadataItem?.[objectMetadataItemId] + : undefined; + }; + + const getLastVisitedViewIdFromObjectMetadataItemId = ( + objectMetadataItemId: string, + ) => { + return lastVisitedViewPerObjectMetadataItem?.[objectMetadataItemId]; + }; + return { + setLastVisitedView, + getLastVisitedViewIdFromObjectNamePlural, + getLastVisitedViewIdFromObjectMetadataItemId, + setFallbackForLastVisitedView, + }; +}; diff --git a/packages/twenty-front/src/modules/navigation/states/lastVisitedObjectMetadataItemIdState.ts b/packages/twenty-front/src/modules/navigation/states/lastVisitedObjectMetadataItemIdState.ts new file mode 100644 index 000000000000..a615fe1c75fd --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/states/lastVisitedObjectMetadataItemIdState.ts @@ -0,0 +1,11 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { localStorageEffect } from '~/utils/recoil-effects'; + +export const lastVisitedObjectMetadataItemIdState = createComponentState | null>({ + key: 'lastVisitedObjectMetadataItemIdState', + defaultValue: null, + effects: [localStorageEffect()], +}); diff --git a/packages/twenty-front/src/modules/navigation/states/lastVisitedViewPerObjectMetadataItemState.ts b/packages/twenty-front/src/modules/navigation/states/lastVisitedViewPerObjectMetadataItemState.ts new file mode 100644 index 000000000000..f8f2176b9d6f --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/states/lastVisitedViewPerObjectMetadataItemState.ts @@ -0,0 +1,9 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { localStorageEffect } from '~/utils/recoil-effects'; + +export const lastVisitedViewPerObjectMetadataItemState = + createComponentState | null>({ + key: 'lastVisitedViewPerObjectMetadataItemState', + defaultValue: null, + effects: [localStorageEffect()], + }); diff --git a/packages/twenty-front/src/modules/navigation/states/selectors/lastVisitedObjectMetadataItemIdStateSelector.ts b/packages/twenty-front/src/modules/navigation/states/selectors/lastVisitedObjectMetadataItemIdStateSelector.ts new file mode 100644 index 000000000000..fde862f47b7f --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/states/selectors/lastVisitedObjectMetadataItemIdStateSelector.ts @@ -0,0 +1,24 @@ +import { lastVisitedObjectMetadataItemIdState } from '@/navigation/states/lastVisitedObjectMetadataItemIdState'; +import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector'; + +export const lastVisitedObjectMetadataItemIdStateSelector = + createComponentSelector({ + key: 'lastVisitedObjectMetadataItemIdStateSelector', + get: + ({ scopeId }: { scopeId: string }) => + ({ get }) => { + const state = get(lastVisitedObjectMetadataItemIdState({ scopeId })); + return state?.['last_visited_object'] + ? state['last_visited_object'] + : null; + }, + set: + ({ scopeId }: { scopeId: string }) => + ({ set }, newValue) => { + set(lastVisitedObjectMetadataItemIdState({ scopeId }), { + ...(typeof newValue === 'string' && { + last_visited_object: newValue, + }), + }); + }, + }); diff --git a/packages/twenty-front/src/modules/navigation/states/selectors/lastVisitedViewPerObjectMetadataItemStateSelector.ts b/packages/twenty-front/src/modules/navigation/states/selectors/lastVisitedViewPerObjectMetadataItemStateSelector.ts new file mode 100644 index 000000000000..efb5c6594e4c --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/states/selectors/lastVisitedViewPerObjectMetadataItemStateSelector.ts @@ -0,0 +1,34 @@ +import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState'; +import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector'; +import { isDefined } from 'twenty-ui'; + +export const lastVisitedViewPerObjectMetadataItemStateSelector = + createComponentSelector | null>({ + key: 'lastVisitedViewPerObjectMetadataItemStateSelector', + get: + ({ scopeId }: { scopeId: string }) => + ({ get }) => { + const state = get( + lastVisitedViewPerObjectMetadataItemState({ scopeId }), + ); + + if (isDefined(state?.['last_visited_object'])) { + const { last_visited_object: _last_visited_object, ...rest } = state; + return rest; + } + + return state; + }, + set: + ({ scopeId }: { scopeId: string }) => + ({ set, get }, newValue) => { + const currentLastVisitedViewPerObjectMetadataItems = get( + lastVisitedViewPerObjectMetadataItemStateSelector({ scopeId }), + ); + + set(lastVisitedViewPerObjectMetadataItemState({ scopeId }), { + ...currentLastVisitedViewPerObjectMetadataItems, + ...newValue, + }); + }, + }); diff --git a/packages/twenty-front/src/modules/navigation/types/ObjectPathInfo.ts b/packages/twenty-front/src/modules/navigation/types/ObjectPathInfo.ts new file mode 100644 index 000000000000..21e28efecfda --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/types/ObjectPathInfo.ts @@ -0,0 +1,7 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { View } from '@/views/types/View'; + +export type ObjectPathInfo = { + objectMetadataItem: ObjectMetadataItem; + view: View | undefined; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx index 1eb2eff27b1d..b1bb3a151ad9 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx @@ -1,6 +1,7 @@ import { useEffect } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; +import { useIsLogged } from '@/auth/hooks/useIsLogged'; import { currentUserState } from '@/auth/states/currentUserState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useFindManyObjectMetadataItems } from '@/object-metadata/hooks/useFindManyObjectMetadataItems'; @@ -13,10 +14,11 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const ObjectMetadataItemsLoadEffect = () => { const currentUser = useRecoilValue(currentUserState); const currentWorkspace = useRecoilValue(currentWorkspaceState); + const isLoggedIn = useIsLogged(); const { objectMetadataItems: newObjectMetadataItems, loading } = useFindManyObjectMetadataItems({ - skip: isUndefinedOrNull(currentUser), + skip: !isLoggedIn, }); const [objectMetadataItems, setObjectMetadataItems] = useRecoilState( diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx index 5b9d532ae087..d65eb003b804 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx @@ -4,6 +4,7 @@ import { useRecoilValue } from 'recoil'; import { isDefined, useIcons } from 'twenty-ui'; import { currentUserState } from '@/auth/states/currentUserState'; +import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; import { ObjectMetadataNavItemsSkeletonLoader } from '@/object-metadata/components/ObjectMetadataNavItemsSkeletonLoader'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; @@ -52,12 +53,12 @@ export const ObjectMetadataNavItems = ({ isRemote }: { isRemote: boolean }) => { ); const { getIcon } = useIcons(); const currentPath = useLocation().pathname; - const currentPathWithSearch = currentPath + useLocation().search; const { records: views } = usePrefetchedData(PrefetchKey.AllViews); const loading = useIsPrefetchLoading(); const theme = useTheme(); + const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView(); if (loading && isDefined(currentUser)) { return ; @@ -106,7 +107,11 @@ export const ObjectMetadataNavItems = ({ isRemote }: { isRemote: boolean }) => { objectMetadataItem.id, views, ); - const viewId = objectMetadataViews[0]?.id; + const lastVisitedViewId = + getLastVisitedViewIdFromObjectMetadataItemId( + objectMetadataItem.id, + ); + const viewId = lastVisitedViewId ?? objectMetadataViews[0]?.id; const navigationPath = `/objects/${objectMetadataItem.namePlural}${ viewId ? `?view=${viewId}` : '' @@ -146,10 +151,7 @@ export const ObjectMetadataNavItems = ({ isRemote }: { isRemote: boolean }) => { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useFilteredObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useFilteredObjectMetadataItems.ts index 2ac05fa4f9d3..2cf99ae1230c 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useFilteredObjectMetadataItems.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useFilteredObjectMetadataItems.ts @@ -10,6 +10,19 @@ export const useFilteredObjectMetadataItems = () => { const activeObjectMetadataItems = objectMetadataItems.filter( ({ isActive, isSystem }) => isActive && !isSystem, ); + + const alphaSortedActiveObjectMetadataItems = activeObjectMetadataItems.sort( + (a, b) => { + if (a.nameSingular < b.nameSingular) { + return -1; + } + if (a.nameSingular > b.nameSingular) { + return 1; + } + return 0; + }, + ); + const inactiveObjectMetadataItems = objectMetadataItems.filter( ({ isActive, isSystem }) => !isActive && !isSystem, ); @@ -37,5 +50,6 @@ export const useFilteredObjectMetadataItems = () => { findObjectMetadataItemByNamePlural, inactiveObjectMetadataItems, objectMetadataItems, + alphaSortedActiveObjectMetadataItems, }; }; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts index 230d09c965aa..b754fc1b4ac6 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts @@ -1,6 +1,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; export const COMPANY_LABEL_IDENTIFIER_FIELD_METADATA_ID = '39403bee-314b-4f14-bc91-70d500397517'; +export const COMPANY_OBJECT_METADATA_ID = 'f1231579-8e7d-4b84-9a60-41844902f2c4'; export const getObjectMetadataItemsMock = () => { const mockArray = [ diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectSummaryCard.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectSummaryCard.tsx index 374591898e0f..6161fbcdf3a3 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectSummaryCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectSummaryCard.tsx @@ -2,6 +2,8 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { IconArchive, IconDotsVertical, IconPencil, useIcons } from 'twenty-ui'; +import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisitedObjectMetadataItem'; +import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { SettingsSummaryCard } from '@/settings/components/SettingsSummaryCard'; import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/SettingsDataModelObjectTypeTag'; @@ -38,8 +40,12 @@ export const SettingsObjectSummaryCard = ({ const theme = useTheme(); const { getIcon } = useIcons(); const Icon = getIcon(iconKey); + const objectMetadataItemId = objectMetadataItem.id; const { closeDropdown } = useDropdown(dropdownId); + const { setFallbackForLastVisitedView } = useLastVisitedView(); + const { setFallbackForLastVisitedObjectMetadataItem } = + useLastVisitedObjectMetadataItem(); const handleEdit = () => { onEdit(); @@ -47,6 +53,8 @@ export const SettingsObjectSummaryCard = ({ }; const handleDeactivate = () => { + setFallbackForLastVisitedObjectMetadataItem(objectMetadataItemId); + setFallbackForLastVisitedView(objectMetadataItemId); onDeactivate(); closeDropdown(); }; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx index 04e6c6723399..4d3ee0cfa208 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx @@ -1,3 +1,5 @@ +import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import isPropValid from '@emotion/is-prop-valid'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; @@ -5,9 +7,6 @@ import { isNonEmptyString } from '@sniptt/guards'; import { Link, useNavigate } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; import { IconComponent, MOBILE_VIEWPORT, Pill } from 'twenty-ui'; - -import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState'; -import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { isDefined } from '~/utils/isDefined'; export type NavigationDrawerItemProps = { @@ -147,7 +146,9 @@ export const NavigationDrawerItem = ({ return; } - if (isNonEmptyString(to)) navigate(to); + if (isNonEmptyString(to)) { + navigate(to); + } }; return ( diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem.tsx index 0d29e4c2698a..fad4149edbfd 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem.tsx @@ -5,6 +5,9 @@ import { import styled from '@emotion/styled'; const StyledItem = styled.div` + &:not(:last-child) { + margin-bottom: ${({ theme }) => theme.spacing(0.5)}; + } margin-left: ${({ theme }) => theme.spacing(4)}; `; diff --git a/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx b/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx index a7130fb3bf17..ddd25463b81c 100644 --- a/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx +++ b/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx @@ -1,35 +1,90 @@ -import { useEffect } from 'react'; -import { isUndefined } from '@sniptt/guards'; -import { useRecoilState } from 'recoil'; - +import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisitedObjectMetadataItem'; +import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams'; import { useViewStates } from '@/views/hooks/internal/useViewStates'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; +import { isUndefined } from '@sniptt/guards'; +import { useEffect } from 'react'; +import { useRecoilState } from 'recoil'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isDefined } from '~/utils/isDefined'; export const QueryParamsViewIdEffect = () => { const { getFiltersFromQueryParams, viewIdQueryParam } = useViewFromQueryParams(); - const { currentViewIdState } = useViewStates(); + const { currentViewIdState, componentId: objectNamePlural } = useViewStates(); const [currentViewId, setCurrentViewId] = useRecoilState(currentViewIdState); const { viewsOnCurrentObject } = useGetCurrentView(); + const { findObjectMetadataItemByNamePlural } = + useFilteredObjectMetadataItems(); + const objectMetadataItemId = + findObjectMetadataItemByNamePlural(objectNamePlural); + const { getLastVisitedViewIdFromObjectNamePlural, setLastVisitedView } = + useLastVisitedView(); + const { lastVisitedObjectMetadataItemId, setLastVisitedObjectMetadataItem } = + useLastVisitedObjectMetadataItem(); + + const lastVisitedViewId = + getLastVisitedViewIdFromObjectNamePlural(objectNamePlural); + const isLastVisitedObjectMetadataItemDifferent = !isDeeplyEqual( + objectMetadataItemId?.id, + lastVisitedObjectMetadataItemId, + ); useEffect(() => { const indexView = viewsOnCurrentObject.find((view) => view.key === 'INDEX'); - if (isUndefined(viewIdQueryParam) && isDefined(indexView)) { - setCurrentViewId(indexView.id); + if (isUndefined(viewIdQueryParam) && isDefined(lastVisitedViewId)) { + if (isLastVisitedObjectMetadataItemDifferent) { + setLastVisitedObjectMetadataItem(objectNamePlural); + setLastVisitedView({ + objectNamePlural, + viewId: lastVisitedViewId, + }); + } + setCurrentViewId(lastVisitedViewId); return; } if (isDefined(viewIdQueryParam)) { + if (isLastVisitedObjectMetadataItemDifferent) { + setLastVisitedObjectMetadataItem(objectNamePlural); + } + if (!isDeeplyEqual(viewIdQueryParam, lastVisitedViewId)) { + setLastVisitedView({ + objectNamePlural, + viewId: viewIdQueryParam, + }); + } setCurrentViewId(viewIdQueryParam); + return; + } + + if (isDefined(indexView)) { + if (isLastVisitedObjectMetadataItemDifferent) { + setLastVisitedObjectMetadataItem(objectNamePlural); + } + if (!isDeeplyEqual(indexView.id, lastVisitedViewId)) { + setLastVisitedView({ + objectNamePlural, + viewId: indexView.id, + }); + } + setCurrentViewId(indexView.id); + return; } }, [ currentViewId, getFiltersFromQueryParams, + isLastVisitedObjectMetadataItemDifferent, + lastVisitedViewId, + objectMetadataItemId?.id, + objectNamePlural, setCurrentViewId, + setLastVisitedObjectMetadataItem, + setLastVisitedView, viewIdQueryParam, viewsOnCurrentObject, ]); diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx index 09f5cce957d6..eb7866d8db21 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx @@ -28,7 +28,6 @@ export const ViewPickerListContent = () => { const { currentViewWithCombinedFiltersAndSorts, viewsOnCurrentObject } = useGetCurrentView(); - const { viewPickerReferenceViewIdState } = useViewPickerStates(); const setViewPickerReferenceViewId = useSetRecoilState( viewPickerReferenceViewIdState, @@ -37,7 +36,6 @@ export const ViewPickerListContent = () => { const { setViewPickerMode } = useViewPickerMode(); const { closeDropdown } = useDropdown(VIEW_PICKER_DROPDOWN_ID); - const { updateView } = useHandleViews(); const handleViewSelect = (viewId: string) => {