diff --git a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts index ad989ea4f..95c2a58b7 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 8332f5113..000000000 --- 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 ead328402..5d637c77b 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 709c071cc..8efee5d0c 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 000000000..8f3afb3e1 --- /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 000000000..45e05d9ab --- /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 000000000..080fbc9d7 --- /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 000000000..a615fe1c7 --- /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 000000000..f8f2176b9 --- /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 000000000..fde862f47 --- /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 000000000..efb5c6594 --- /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 000000000..21e28efec --- /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 1eb2eff27..b1bb3a151 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 5b9d532ae..d65eb003b 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 2ac05fa4f..2cf99ae12 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 230d09c96..b754fc1b4 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 374591898..6161fbcdf 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 04e6c6723..4d3ee0cfa 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 0d29e4c26..fad4149ed 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 a7130fb3b..ddd25463b 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 09f5cce95..eb7866d8d 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) => {