Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions static/app/views/navigation/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
85 changes: 84 additions & 1 deletion static/app/views/navigation/index.desktop.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -472,6 +475,86 @@ describe('desktop navigation', () => {
}
);

it('returns to the last selected project after visiting org settings', async () => {
const {router} = render(
<PrimaryNavigationContextProvider>
<Navigation />
</PrimaryNavigationContextProvider>,
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(
<PrimaryNavigationContextProvider>
<Navigation />
</PrimaryNavigationContextProvider>,
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(
<PrimaryNavigationContextProvider>
<Navigation />
</PrimaryNavigationContextProvider>,
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(
<PrimaryNavigationContextProvider>
Expand Down
28 changes: 26 additions & 2 deletions static/app/views/navigation/navigation.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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<string | null>(
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 = [
{
Expand Down
Loading