diff --git a/static/app/views/settings/settingsCommandPaletteActions.spec.tsx b/static/app/views/settings/settingsCommandPaletteActions.spec.tsx new file mode 100644 index 000000000000..940e4c3a0277 --- /dev/null +++ b/static/app/views/settings/settingsCommandPaletteActions.spec.tsx @@ -0,0 +1,64 @@ +import {ConfigStore} from 'sentry/stores/configStore'; +import type {Config} from 'sentry/types/system'; +import {getSettingsFieldSections} from 'sentry/views/settings/settingsCommandPaletteActions'; + +describe('getSettingsFieldSections', () => { + let configState: Config; + + beforeEach(() => { + configState = ConfigStore.getState(); + }); + + afterEach(() => { + ConfigStore.loadInitialData(configState); + }); + + it('includes the org slug in org-level route paths when not on a customer domain', () => { + // No customerDomain set — normalizeUrl is a no-op, so the slug should be substituted. + ConfigStore.set('customerDomain', null); + + const sections = getSettingsFieldSections('doogi'); + + const allPathnames = sections.flatMap(section => + section.fields.map(field => field.to.pathname) + ); + const orgPaths = allPathnames.filter( + p => p.startsWith('/settings/') && !p.startsWith('/settings/account/') + ); + + // Every org-level path must include the org slug and must NOT contain the literal `:orgId`. + for (const pathname of orgPaths) { + expect(pathname).toContain('doogi'); + expect(pathname).not.toContain(':orgId'); + } + }); + + it('does not include the org slug in org-level route paths when on a customer domain', () => { + // With customerDomain set, normalizeUrl strips the orgId segment from the + // template before the slug is substituted, so the resulting path should NOT + // contain the real slug (the org is identified by the subdomain instead). + ConfigStore.loadInitialData({ + ...configState, + customerDomain: { + subdomain: 'doogi', + organizationUrl: 'https://doogi.sentry.io', + sentryUrl: 'https://sentry.io', + }, + }); + + const sections = getSettingsFieldSections('doogi'); + + const allPathnames = sections.flatMap(section => + section.fields.map(field => field.to.pathname) + ); + const orgPaths = allPathnames.filter( + p => p.startsWith('/settings/') && !p.startsWith('/settings/account/') + ); + + // Org paths must not include the slug and must not contain `:orgId`. + for (const pathname of orgPaths) { + expect(pathname).not.toContain('doogi'); + expect(pathname).not.toContain(':orgId'); + } + }); +}); diff --git a/static/app/views/settings/settingsCommandPaletteActions.tsx b/static/app/views/settings/settingsCommandPaletteActions.tsx index afe65a11cd1d..8e531973768e 100644 --- a/static/app/views/settings/settingsCommandPaletteActions.tsx +++ b/static/app/views/settings/settingsCommandPaletteActions.tsx @@ -5,6 +5,7 @@ import {CommandPaletteSlot} from 'sentry/components/commandPalette/ui/commandPal import {IconLock, IconMail, IconSettings, IconSubscribed, IconUser} from 'sentry/icons'; import {t} from 'sentry/locale'; import {replaceRouterParams} from 'sentry/utils/replaceRouterParams'; +import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useOrganization} from 'sentry/utils/useOrganization'; import {FORM_FIELD_REGISTRY} from 'sentry/views/settings/fieldRegistry.generated'; import {getUserOrgNavigationConfiguration} from 'sentry/views/settings/organization/userOrgNavigationConfiguration'; @@ -26,7 +27,17 @@ function normalizeRouteForLookup(route: string): string { } function resolveRoutePath(route: string, orgSlug: string): string { - return replaceRouterParams(normalizeRouteForLookup(route), {orgId: orgSlug}); + // Apply normalizeUrl to the route template *before* substituting the real org + // slug. On customer-domain sessions, normalizeUrl strips the `:orgId` segment + // from the template (e.g. `/settings/:orgId/feature-flags/…` → + // `/settings/feature-flags/…`), so replaceRouterParams becomes a no-op and + // the stored URL is already canonical. Without this, the command palette's + // own normalizeUrl call (commandPalette.tsx) would strip a real slug that had + // already been substituted, producing a broken path such as + // `/settings/feature-flags/…` where `feature-flags` would be mis-matched as + // `:orgId` by the router. + const normalizedRoute = normalizeUrl(normalizeRouteForLookup(route)); + return replaceRouterParams(normalizedRoute, {orgId: orgSlug}); } function titleFromRoute(route: string): string { @@ -75,7 +86,7 @@ type SettingsFieldSection = { type FormFieldDefinition = (typeof FORM_FIELD_REGISTRY)[string]; -function getSettingsFieldSections(orgSlug: string): SettingsFieldSection[] { +export function getSettingsFieldSections(orgSlug: string): SettingsFieldSection[] { const routeTitleMap = new Map(); for (const section of getUserOrgNavigationConfiguration()) { for (const item of section.items) {