Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
026f07e
feat(settings): Add scope navigation rail
priscilawebdev Oct 21, 2026
183561b
fix(settings): Match scope header and back button design
priscilawebdev Oct 21, 2026
177add1
fix(settings): Keep scope header and sections in sync
priscilawebdev Oct 21, 2026
706ffdd
Merge branch 'master' into feat/settings-scope-navigation
priscilawebdev Oct 22, 2026
048cf4c
feat(settings): Refine V3 scope navigation and project selection
priscilawebdev Oct 22, 2026
3171838
ref(settings): Simplify scope navigation and touch handling
priscilawebdev Oct 22, 2026
c266529
ref(settings): Use router matching for settings navigation
priscilawebdev Oct 22, 2026
f455a47
docs(nav): Explain secondary navigation header props
priscilawebdev Oct 22, 2026
2762093
fix(settings): Restore the organization projects link
priscilawebdev Oct 22, 2026
c74a099
fix(settings): Preserve project list and selector navigation
priscilawebdev Oct 22, 2026
32c7f47
docs(settings): Explain account navigation filtering
priscilawebdev Oct 22, 2026
b0339b6
ref(nav): Extract the secondary navigation toggle
priscilawebdev Oct 22, 2026
4353148
ref(nav): Introduce an expandable secondary navigation header
priscilawebdev Oct 22, 2026
495a067
ref(nav): Clarify the header with sidebar toggle name
priscilawebdev Oct 22, 2026
0302cec
ref(nav): Use Disclosure for collapsible navigation sections
priscilawebdev Oct 22, 2026
ca19ff3
revert(nav): Keep the existing collapsible navigation sections
priscilawebdev Oct 22, 2026
86f7eb6
ref(settings): Clarify navigation item title selection
priscilawebdev Oct 22, 2026
29d19cf
Merge branch 'master' into feat/settings-scope-navigation
priscilawebdev Oct 22, 2026
850c643
ref(navigation): Compose secondary navigation headers
priscilawebdev Oct 22, 2026
b33605d
fix(settings): Restore navigation controls and safe project switching
priscilawebdev Oct 22, 2026
47dc77c
fix(settings): Hide sidebar toggle pending design review
priscilawebdev Oct 22, 2026
0c936b1
fix(settings): Keep the mobile navigation close control
priscilawebdev Oct 22, 2026
274f1e1
ref(settings): Reuse the router handle type
priscilawebdev Oct 22, 2026
3a51d2f
fix(settings): Reset navigation scope and preserve the current project
priscilawebdev Oct 22, 2026
e1b60a8
fix(settings): Always show the sidebar toggle
priscilawebdev Oct 22, 2026
fba5756
ref(navigation): Render the sidebar toggle inside the header
priscilawebdev Oct 22, 2026
4e757e3
fix(navigation): Align sidebar header controls consistently
priscilawebdev Oct 22, 2026
20a9547
Merge remote-tracking branch 'origin/master' into feat/settings-scope…
priscilawebdev Oct 27, 2026
5c09b84
ref(settings): Supply the General nav title from navigation config
priscilawebdev Oct 27, 2026
2922cdd
ref(settings): Add a shared project switch route handle
priscilawebdev Oct 27, 2026
12380ce
feat(settings): Return to the page Settings was entered from
priscilawebdev Oct 27, 2026
7f60cf6
ref(settings): Share settings scope derivation in one hook
priscilawebdev Oct 27, 2026
ab98f8b
fix(navigation): Drop the active group override when the route group …
priscilawebdev Oct 27, 2026
5491c6c
ref(settings): Declare project pages with a single settingsProject ha…
priscilawebdev Oct 27, 2026
b316f59
Revert "ref(settings): Declare project pages with a single settingsPr…
priscilawebdev Oct 27, 2026
bfaeaaf
ref(navigation): Derive section collapsibility from the title
priscilawebdev Oct 27, 2026
a1358bb
ref(navigation): Make the return path hook generic and link Back to app
priscilawebdev Oct 27, 2026
3320823
fix(navigation): Scope the settings return path by organization
priscilawebdev Oct 27, 2026
a5f9891
Merge branch 'master' into feat/settings-scope-navigation
priscilawebdev Oct 27, 2026
a21e5b2
fix(navigation): Clear stale active group overrides and derive projec…
priscilawebdev Oct 27, 2026
250366c
Merge remote-tracking branch 'origin/feat/settings-scope-navigation' …
priscilawebdev Oct 27, 2026
4f74e88
fix(settings): Only derive a settings scope inside settings
priscilawebdev Oct 27, 2026
1284e7e
fix(navigation): Keep the Seer secondary navigation title styled
sentry-junior[bot] Oct 27, 2026
62849e3
fix(settings): Keep account pages out of the V3 Org sidebar
sentry-junior[bot] Oct 27, 2026
a0abe69
ref(settings): Drop unreachable project settings navigation branches
sentry-junior[bot] Oct 27, 2026
12dc9be
test(settings): Simplify settings v3 navigation tests
sentry-junior[bot] Oct 27, 2026
ec4f39b
fix(navigation): Only return to same-site paths from settings
sentry-junior[bot] Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion static/app/router/routes.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ describe('buildRoutes()', () => {
{
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
switchTo: '/settings/:orgId/projects/:projectId/keys/',
},
{
type: 'link',
Expand Down
52 changes: 46 additions & 6 deletions static/app/router/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -533,8 +533,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
switchTo: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {switchTo: '/settings/:orgId/projects/:projectId/'},
},
index: true,
name: t('General'),
Expand All @@ -549,8 +549,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
switchTo: '/settings/:orgId/projects/:projectId/teams/',
},
settingsProjectSwitch: {switchTo: '/settings/:orgId/projects/:projectId/teams/'},

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping the settings prefix for convention, but we can rename this later. When settings-v3 is GA we can remove the project crumb (type: 'project' entries and their to), but settingsBreadcrumb stays for the other crumbs

},
path: 'teams/',
name: t('Teams'),
Expand All @@ -561,8 +561,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
switchTo: '/settings/:orgId/projects/:projectId/alerts/',
},
settingsProjectSwitch: {switchTo: '/settings/:orgId/projects/:projectId/alerts/'},
},
path: 'alerts/',
name: t('Alerts'),
Expand Down Expand Up @@ -603,6 +603,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/attributes/',
},
},
Expand All @@ -615,8 +617,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
switchTo: '/settings/:orgId/projects/:projectId/tags/',
},
settingsProjectSwitch: {switchTo: '/settings/:orgId/projects/:projectId/tags/'},
},
path: 'tags/',
name: t('Tags & Context'),
Expand All @@ -627,6 +629,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/environments/',
},
},
Expand All @@ -647,6 +651,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/ownership/',
},
},
Expand All @@ -663,6 +669,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/user-feedback/',
},
},
Expand All @@ -675,6 +683,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/toolbar/',
},
},
Expand All @@ -687,6 +697,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/filters/',
},
},
Expand All @@ -708,6 +720,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/security-and-privacy/',
},
},
Expand All @@ -733,6 +747,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/issue-grouping/',
},
},
Expand All @@ -745,6 +761,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/debug-symbols/',
},
},
Expand All @@ -757,6 +775,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/proguard/',
},
},
Expand All @@ -769,6 +789,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/source-maps/',
},
},
Expand Down Expand Up @@ -806,6 +828,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/performance/',
},
},
Expand All @@ -822,6 +846,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/replays/',
},
},
Expand All @@ -834,8 +860,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
switchTo: '/settings/:orgId/projects/:projectId/logs/',
},
settingsProjectSwitch: {switchTo: '/settings/:orgId/projects/:projectId/logs/'},
},
path: 'logs/',
name: t('Logs'),
Expand All @@ -846,6 +872,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/playstation/',
},
},
Expand All @@ -858,6 +886,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/mobile-builds/',
},
},
Expand All @@ -870,6 +900,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/snapshots/',
},
},
Expand All @@ -882,8 +914,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
switchTo: '/settings/:orgId/projects/:projectId/keys/',
},
settingsProjectSwitch: {switchTo: '/settings/:orgId/projects/:projectId/keys/'},
},
path: 'keys/',
name: t('Client Keys'),
Expand Down Expand Up @@ -913,6 +945,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/loader-script/',
},
},
Expand All @@ -925,6 +959,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/release-tracking/',
},
},
Expand All @@ -939,6 +975,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/security-headers/',
},
},
Expand Down Expand Up @@ -974,6 +1012,8 @@ function buildRoutes(): RouteObject[] {
settingsBreadcrumb: {
type: 'project',
to: '/settings/:orgId/projects/:projectId/',
},
settingsProjectSwitch: {
switchTo: '/settings/:orgId/projects/:projectId/legacy-webhooks/',
},
},
Expand Down
4 changes: 3 additions & 1 deletion static/app/router/types.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {SettingsBreadcrumb} from 'sentry/views/settings/components/settingsBreadcrumb/types';
import type {SettingsRouteHandle} from 'sentry/views/settings/types';

/**
* This is our "custom" route object. It varies a bit from React Router's
Expand All @@ -15,7 +16,8 @@ interface BaseRouteObject {
*/
customerDomainOnlyRoute?: true;

handle?: Record<string, unknown> & {settingsBreadcrumb?: SettingsBreadcrumb};
handle?: Record<string, unknown> &
SettingsRouteHandle & {settingsBreadcrumb?: SettingsBreadcrumb};
/**
* Is a index route
*/
Expand Down
48 changes: 48 additions & 0 deletions static/app/utils/useReturnPath.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {renderHookWithProviders} from 'sentry-test/reactTestingLibrary';

import {getReturnPath, useCaptureReturnPath} from 'sentry/utils/useReturnPath';

const KEY = 'test-return-path';
const options = {key: KEY, excludePrefix: '/settings'};

describe('useReturnPath', () => {
beforeEach(() => {
sessionStorage.clear();
});

it('captures locations outside the excluded prefix', () => {
renderHookWithProviders(() => useCaptureReturnPath(options), {
initialRouterConfig: {
location: {pathname: '/issues/', query: {query: 'is:unresolved'}},
},
});

expect(getReturnPath(KEY, '/fallback/')).toBe('/issues/?query=is%3Aunresolved');
});

it('does not capture without a key', () => {
renderHookWithProviders(
() => useCaptureReturnPath({key: null, excludePrefix: '/settings'}),
{initialRouterConfig: {location: {pathname: '/issues/'}}}
);

expect(getReturnPath(KEY, '/fallback/')).toBe('/fallback/');
});

it('does not capture locations under the excluded prefix', () => {
renderHookWithProviders(() => useCaptureReturnPath(options), {
initialRouterConfig: {location: {pathname: '/settings/org-slug/members/'}},
});

expect(getReturnPath(KEY, '/fallback/')).toBe('/fallback/');
});

it.each(['//attacker.example/', '/\\attacker.example/', 'https://attacker.example/'])(
'falls back instead of returning to another site via %s',
path => {
sessionStorage.setItem(KEY, path);

expect(getReturnPath(KEY, '/fallback/')).toBe('/fallback/');
}
);
});
50 changes: 50 additions & 0 deletions static/app/utils/useReturnPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {useEffect} from 'react';
import {matchPath} from 'react-router';

import {sessionStorageWrapper} from 'sentry/utils/sessionStorage';
import {useLocation} from 'sentry/utils/useLocation';

interface CaptureReturnPathOptions {
/**
* Locations under this path prefix (e.g. `/settings`) are not captured.
*/
excludePrefix: string;
/**
* The session storage key the return path is stored under. Nothing is
* captured while this is null, e.g. when the key depends on something that
* is not known yet.
*/
key: string | null;
}

/**
* Remembers the last page the user visited outside of a section of the app, so
* a "back" control inside that section can return there. Browser history is
* not reliable for this because users navigate between several pages of the
* section before leaving.
*
* Must be mounted outside of the section it excludes.
*/
export function useCaptureReturnPath({key, excludePrefix}: CaptureReturnPathOptions) {
const {pathname, search, hash} = useLocation();

useEffect(() => {
if (key === null || matchPath({path: excludePrefix, end: false}, pathname)) {
return;
}
sessionStorageWrapper.setItem(key, `${pathname}${search}${hash}`);
}, [key, excludePrefix, pathname, search, hash]);
}

// A path on this site. Links treat `//host` and `/\host` as another site, and
// unmatched routes can capture such a pathname.
const SAME_ORIGIN_PATH = /^\/(?![/\\])/;

/**
* The page captured by `useCaptureReturnPath`, or `fallback` when none was
* captured, e.g. when the section was entered directly.
*/
export function getReturnPath(key: string, fallback: string): string {
const path = sessionStorageWrapper.getItem(key);
return path && SAME_ORIGIN_PATH.test(path) ? path : fallback;
}
Comment thread
priscilawebdev marked this conversation as resolved.
8 changes: 8 additions & 0 deletions static/app/views/navigation/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ export const TOP_BAR_HEIGHT_CSS_VAR = '--top-bar-height';
// To be passed as the `source` parameter in router navigation state
// e.g. {pathname: '/issues/', state: {source: `sidebar`}}
export const SIDEBAR_NAVIGATION_SOURCE = 'sidebar';

// Settings has its own navigation, so "Back to app" returns to the last page
// visited outside of this path prefix.
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}`;
Loading
Loading