diff --git a/static/app/views/navigation/constants.tsx b/static/app/views/navigation/constants.tsx index 61bd1b81c52a..f5e70784513f 100644 --- a/static/app/views/navigation/constants.tsx +++ b/static/app/views/navigation/constants.tsx @@ -34,3 +34,7 @@ export const SETTINGS_PATH_PREFIX = '/settings'; // Scoped by organization so "Back to app" never returns to another organization. export const getSettingsReturnPathKey = (organizationSlug: string) => `settings-return-path:${organizationSlug}`; + +// The project the settings rail links to after the user leaves project settings. +export const getSettingsLastProjectKey = (organizationSlug: string) => + `settings-last-project:${organizationSlug}`; diff --git a/static/app/views/navigation/index.desktop.spec.tsx b/static/app/views/navigation/index.desktop.spec.tsx index 23c1b7c12514..08e01d6a2762 100644 --- a/static/app/views/navigation/index.desktop.spec.tsx +++ b/static/app/views/navigation/index.desktop.spec.tsx @@ -17,7 +17,10 @@ import { import {ConfigStore} from 'sentry/stores/configStore'; import {ProjectsStore} from 'sentry/stores/projectsStore'; import {Navigation} from 'sentry/views/navigation'; -import {NAVIGATION_SIDEBAR_COLLAPSED_LOCAL_STORAGE_KEY} from 'sentry/views/navigation/constants'; +import { + NAVIGATION_SIDEBAR_COLLAPSED_LOCAL_STORAGE_KEY, + getSettingsLastProjectKey, +} from 'sentry/views/navigation/constants'; import {PrimaryNavigationContextProvider} from 'sentry/views/navigation/primaryNavigationContext'; const ALL_AVAILABLE_FEATURES = [ @@ -472,6 +475,86 @@ describe('desktop navigation', () => { } ); + it('returns to the last selected project after visiting org settings', async () => { + const {router} = render( + + + , + navigationContext({ + organization: settingsV3, + initialRouterConfig: { + location: {pathname: '/settings/org-slug/projects/project-slug/'}, + routes: ['/settings/:orgId/', '/settings/:orgId/projects/:projectId/'], + }, + }) + ); + + await userEvent.click(screen.getByRole('button', {name: 'Select a project'})); + await userEvent.click(await screen.findByRole('option', {name: 'second-project'})); + expect(router.location.pathname).toBe( + '/settings/org-slug/projects/second-project/' + ); + + await userEvent.click(screen.getByRole('link', {name: 'Org'})); + expect(router.location.pathname).toBe('/settings/org-slug/'); + expect(screen.getByRole('link', {name: 'Project'})).toHaveAttribute( + 'href', + '/settings/org-slug/projects/second-project/' + ); + + await userEvent.click(screen.getByRole('link', {name: 'Project'})); + expect(router.location.pathname).toBe( + '/settings/org-slug/projects/second-project/' + ); + }); + + it('links to the remembered project by ID after it was renamed', async () => { + localStorage.setItem(getSettingsLastProjectKey('org-slug'), JSON.stringify('999')); + ProjectsStore.loadInitialData([ + ProjectFixture(), + ProjectFixture({id: '999', slug: 'renamed-project'}), + ]); + render( + + + , + navigationContext({ + organization: settingsV3, + initialRouterConfig: {location: {pathname: '/settings/org-slug/'}}, + }) + ); + + await waitFor(() => + expect(screen.getByRole('link', {name: 'Project'})).toHaveAttribute( + 'href', + '/settings/org-slug/projects/renamed-project/' + ) + ); + }); + + it('falls back to the first project when the remembered one is unavailable', async () => { + localStorage.setItem( + getSettingsLastProjectKey('org-slug'), + JSON.stringify('12345') + ); + render( + + + , + navigationContext({ + organization: settingsV3, + initialRouterConfig: {location: {pathname: '/settings/org-slug/'}}, + }) + ); + + await waitFor(() => + expect(screen.getByRole('link', {name: 'Project'})).toHaveAttribute( + 'href', + '/settings/org-slug/projects/project-slug/' + ) + ); + }); + it('switches from project key details to the new project key list', async () => { const {router} = render( diff --git a/static/app/views/navigation/navigation.tsx b/static/app/views/navigation/navigation.tsx index 8712f6c9e846..6c2c0481d841 100644 --- a/static/app/views/navigation/navigation.tsx +++ b/static/app/views/navigation/navigation.tsx @@ -1,4 +1,11 @@ -import {Fragment, type PropsWithChildren, type RefObject, useMemo, useRef} from 'react'; +import { + Fragment, + type PropsWithChildren, + type RefObject, + useEffect, + useMemo, + useRef, +} from 'react'; import {mergeProps} from '@react-aria/utils'; import {IconArrow} from '@sentry/icons/arrow'; import {IconBuilding} from '@sentry/icons/building'; @@ -22,6 +29,7 @@ import Feature from 'sentry/components/acl/feature'; import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {Override} from 'sentry/components/override'; import {t} from 'sentry/locale'; +import {useLocalStorageState} from 'sentry/utils/useLocalStorageState'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useProjects} from 'sentry/utils/useProjects'; import {getReturnPath} from 'sentry/utils/useReturnPath'; @@ -35,6 +43,7 @@ import { NAVIGATION_SIDEBAR_SECONDARY_WIDTH_LOCAL_STORAGE_KEY, PRIMARY_SIDEBAR_WIDTH, SECONDARY_SIDEBAR_WIDTH, + getSettingsLastProjectKey, getSettingsReturnPathKey, } from 'sentry/views/navigation/constants'; import { @@ -205,7 +214,22 @@ export function SettingsPrimaryNavigationItems({ }) { const {projects} = useProjects(); const {scope: activeScope, projectSlug: activeProjectSlug} = useSettingsScope(); - const projectSlug = activeProjectSlug ?? projects[0]?.slug; + // Stored by ID, not slug, so renaming the project does not lose it. + const [lastProjectId, setLastProjectId] = useLocalStorageState( + getSettingsLastProjectKey(organizationSlug), + null + ); + const activeProjectId = projects.find(({slug}) => slug === activeProjectSlug)?.id; + + useEffect(() => { + if (activeProjectId && activeProjectId !== lastProjectId) { + setLastProjectId(activeProjectId); + } + }, [activeProjectId, lastProjectId, setLastProjectId]); + + // The remembered project can be deleted or lose access since it was stored. + const rememberedProjectSlug = projects.find(({id}) => id === lastProjectId)?.slug; + const projectSlug = activeProjectSlug ?? rememberedProjectSlug ?? projects[0]?.slug; const projectSettingsPath = `/settings/${organizationSlug}/projects/`; const items = [ {