From 6ac26f56652022c72d7c087e67d238e3d6f8699e Mon Sep 17 00:00:00 2001
From: "sentry-junior[bot]"
<264270552+sentry-junior[bot]@users.noreply.github.com>
Date: Tue, 3 Nov 2026 09:40:37 +0000
Subject: [PATCH 1/2] fix(settings): Remember last selected project in settings
v3 rail
The Project link in the settings v3 rail fell back to the first project
whenever the user was outside project settings, so switching to org
settings and back dropped the selected project. Remember the last
project per organization and fall back to the first project only when
the remembered one is no longer available.
Co-Authored-By: Priscila Andre de Oliveira <29228205+priscilawebdev@users.noreply.github.com>
---
static/app/views/navigation/constants.tsx | 4 ++
.../views/navigation/index.desktop.spec.tsx | 61 ++++++++++++++++++-
static/app/views/navigation/navigation.tsx | 28 ++++++++-
3 files changed, 90 insertions(+), 3 deletions(-)
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..571d0dc060eb 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,62 @@ 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('falls back to the first project when the remembered one is unavailable', async () => {
+ localStorage.setItem(
+ getSettingsLastProjectKey('org-slug'),
+ JSON.stringify('deleted-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/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..5ac0e6962f43 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;
+ const [lastProjectSlug, setLastProjectSlug] = useLocalStorageState(
+ getSettingsLastProjectKey(organizationSlug),
+ null
+ );
+
+ useEffect(() => {
+ if (activeProjectSlug && activeProjectSlug !== lastProjectSlug) {
+ setLastProjectSlug(activeProjectSlug);
+ }
+ }, [activeProjectSlug, lastProjectSlug, setLastProjectSlug]);
+
+ // The remembered project can be deleted or lose access since it was stored.
+ const rememberedProjectSlug = projects.some(({slug}) => slug === lastProjectSlug)
+ ? lastProjectSlug
+ : undefined;
+ const projectSlug = activeProjectSlug ?? rememberedProjectSlug ?? projects[0]?.slug;
const projectSettingsPath = `/settings/${organizationSlug}/projects/`;
const items = [
{
From c6c75b21c9ddf7f154abe951634a341a2a98b86d Mon Sep 17 00:00:00 2001
From: "sentry-junior[bot]"
<264270552+sentry-junior[bot]@users.noreply.github.com>
Date: Tue, 3 Nov 2026 10:01:21 +0000
Subject: [PATCH 2/2] ref(settings): Remember settings project by ID instead of
slug
Page filters persist project IDs, so a renamed project is still found.
Do the same for the settings rail's remembered project.
Co-Authored-By: Priscila Andre de Oliveira <29228205+priscilawebdev@users.noreply.github.com>
---
.../views/navigation/index.desktop.spec.tsx | 26 ++++++++++++++++++-
static/app/views/navigation/navigation.tsx | 14 +++++-----
2 files changed, 32 insertions(+), 8 deletions(-)
diff --git a/static/app/views/navigation/index.desktop.spec.tsx b/static/app/views/navigation/index.desktop.spec.tsx
index 571d0dc060eb..08e01d6a2762 100644
--- a/static/app/views/navigation/index.desktop.spec.tsx
+++ b/static/app/views/navigation/index.desktop.spec.tsx
@@ -508,10 +508,34 @@ describe('desktop navigation', () => {
);
});
+ 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('deleted-project')
+ JSON.stringify('12345')
);
render(
diff --git a/static/app/views/navigation/navigation.tsx b/static/app/views/navigation/navigation.tsx
index 5ac0e6962f43..6c2c0481d841 100644
--- a/static/app/views/navigation/navigation.tsx
+++ b/static/app/views/navigation/navigation.tsx
@@ -214,21 +214,21 @@ export function SettingsPrimaryNavigationItems({
}) {
const {projects} = useProjects();
const {scope: activeScope, projectSlug: activeProjectSlug} = useSettingsScope();
- const [lastProjectSlug, setLastProjectSlug] = useLocalStorageState(
+ // 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 (activeProjectSlug && activeProjectSlug !== lastProjectSlug) {
- setLastProjectSlug(activeProjectSlug);
+ if (activeProjectId && activeProjectId !== lastProjectId) {
+ setLastProjectId(activeProjectId);
}
- }, [activeProjectSlug, lastProjectSlug, setLastProjectSlug]);
+ }, [activeProjectId, lastProjectId, setLastProjectId]);
// The remembered project can be deleted or lose access since it was stored.
- const rememberedProjectSlug = projects.some(({slug}) => slug === lastProjectSlug)
- ? lastProjectSlug
- : undefined;
+ const rememberedProjectSlug = projects.find(({id}) => id === lastProjectId)?.slug;
const projectSlug = activeProjectSlug ?? rememberedProjectSlug ?? projects[0]?.slug;
const projectSettingsPath = `/settings/${organizationSlug}/projects/`;
const items = [