Skip to content

Commit 08d0b6f

Browse files
Fix duplicate settings titles in page frame
1 parent a928c1a commit 08d0b6f

File tree

4 files changed

+138
-10
lines changed

4 files changed

+138
-10
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {OrganizationFixture} from 'sentry-fixture/organization';
2+
3+
import {render, screen, within} from 'sentry-test/reactTestingLibrary';
4+
5+
import {useRoutes} from 'sentry/utils/useRoutes';
6+
import {TopBar} from 'sentry/views/navigation/topBar';
7+
import {SettingsBreadcrumb} from 'sentry/views/settings/components/settingsBreadcrumb';
8+
import {BreadcrumbProvider} from 'sentry/views/settings/components/settingsBreadcrumb/context';
9+
10+
import {SettingsPageHeader} from './settingsPageHeader';
11+
12+
jest.mock('sentry/utils/useRoutes');
13+
14+
const mockUseRoutes = jest.mocked(useRoutes);
15+
16+
describe('SettingsPageHeader', () => {
17+
const routes = [
18+
{name: 'One', path: '/one/'},
19+
{name: 'Two', path: '/two/'},
20+
{name: 'Three', path: '/three/'},
21+
];
22+
23+
beforeEach(() => {
24+
mockUseRoutes.mockReturnValue(routes);
25+
});
26+
27+
it('replaces the last breadcrumb in page-frame mode instead of rendering a duplicate title', () => {
28+
render(
29+
<TopBar.Slot.Provider>
30+
<BreadcrumbProvider>
31+
<TopBar.Slot name="title">
32+
<SettingsBreadcrumb params={{}} routes={routes} />
33+
</TopBar.Slot>
34+
<TopBar.Slot.Outlet name="title">
35+
{props => <div {...props} data-test-id="topbar-title-slot" />}
36+
</TopBar.Slot.Outlet>
37+
<SettingsPageHeader title="Projects" />
38+
</BreadcrumbProvider>
39+
</TopBar.Slot.Provider>,
40+
{
41+
organization: OrganizationFixture({features: ['page-frame']}),
42+
}
43+
);
44+
45+
const topbarTitleSlot = screen.getByTestId('topbar-title-slot');
46+
47+
expect(within(topbarTitleSlot).getAllByText('Projects')).toHaveLength(1);
48+
expect(screen.queryByRole('heading', {name: 'Projects'})).not.toBeInTheDocument();
49+
});
50+
});

static/app/views/settings/components/settingsPageHeader.tsx

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import styled from '@emotion/styled';
22

33
import * as Layout from 'sentry/components/layouts/thirds';
4+
import {useRoutes} from 'sentry/utils/useRoutes';
45
import {TopBar} from 'sentry/views/navigation/topBar';
56
import {useHasPageFrameFeature} from 'sentry/views/navigation/useHasPageFrameFeature';
7+
import {BreadcrumbTitle} from 'sentry/views/settings/components/settingsBreadcrumb/breadcrumbTitle';
68

79
type Props = {
810
/**
@@ -43,20 +45,34 @@ function UnstyledSettingsPageHeader({
4345
...props
4446
}: Props) {
4547
const hasPageFrame = useHasPageFrameFeature();
48+
const routes = useRoutes();
49+
const hasTitle = title !== null && title !== undefined && title !== false;
50+
const hasSubtitle = subtitle !== null && subtitle !== undefined && subtitle !== false;
51+
const showVisibleTitle = hasTitle && !hasPageFrame;
52+
const showHeaderText = showVisibleTitle || hasSubtitle;
4653
// If Header is narrow, use align-items to center <Action>.
4754
// Otherwise, use a fixed margin to prevent an odd alignment.
4855
// This is needed as Actions could be a button or a dropdown.
49-
const isNarrow = !subtitle;
56+
const isNarrow = !hasSubtitle;
57+
const breadcrumbTitle =
58+
hasPageFrame && typeof title === 'string' ? (
59+
<BreadcrumbTitle routes={routes} title={title} />
60+
) : null;
5061

5162
return (
5263
<div {...props}>
64+
{breadcrumbTitle}
5365
<TitleAndActions isNarrow={isNarrow}>
5466
<TitleWrapper>
55-
{icon && <Icon>{icon}</Icon>}
56-
{title && (
67+
{showHeaderText && icon && <Icon>{icon}</Icon>}
68+
{showHeaderText && (
5769
<Title tabs={tabs} styled={noTitleStyles}>
58-
<Layout.Title>{title}</Layout.Title>
59-
{subtitle && <Subtitle colorSubtitle={colorSubtitle}>{subtitle}</Subtitle>}
70+
{showVisibleTitle && <Layout.Title>{title}</Layout.Title>}
71+
{hasSubtitle && (
72+
<Subtitle colorSubtitle={colorSubtitle} hasTitle={showVisibleTitle}>
73+
{subtitle}
74+
</Subtitle>
75+
)}
6076
</Title>
6177
)}
6278
</TitleWrapper>
@@ -94,12 +110,12 @@ const Title = styled('div')<TitleProps>`
94110
margin: ${p => p.theme.space['3xl']} ${p => p.theme.space.xl}
95111
${p => p.theme.space['2xl']} 0;
96112
`;
97-
const Subtitle = styled('div')<{colorSubtitle?: boolean}>`
113+
const Subtitle = styled('div')<{colorSubtitle?: boolean; hasTitle?: boolean}>`
98114
color: ${p =>
99115
p.colorSubtitle ? p.theme.tokens.content.accent : p.theme.colors.gray500};
100116
font-weight: ${p => p.theme.font.weight.sans.regular};
101117
font-size: ${p => p.theme.font.size.md};
102-
padding: ${p => p.theme.space.lg} 0 0;
118+
padding: ${p => (p.hasTitle ? `${p.theme.space.lg} 0 0` : 0)};
103119
`;
104120

105121
const Icon = styled('div')`
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {OrganizationFixture} from 'sentry-fixture/organization';
2+
3+
import {render, screen} from 'sentry-test/reactTestingLibrary';
4+
5+
import {TopBar} from 'sentry/views/navigation/topBar';
6+
7+
import PreprodSettings from './index';
8+
9+
jest.mock('sentry/views/preprod/components/preprodQuotaAlert', () => ({
10+
PreprodQuotaAlert: () => <div data-test-id="preprod-quota-alert" />,
11+
}));
12+
13+
jest.mock('./featureFilter', () => ({
14+
FeatureFilter: () => <div data-test-id="feature-filter" />,
15+
}));
16+
17+
jest.mock('./prCommentsToggle', () => ({
18+
PrCommentsToggle: () => <div data-test-id="pr-comments-toggle" />,
19+
}));
20+
21+
jest.mock('./snapshotPrCommentsToggle', () => ({
22+
SnapshotPrCommentsToggle: () => <div data-test-id="snapshot-pr-comments-toggle" />,
23+
}));
24+
25+
jest.mock('./snapshotStatusChecks', () => ({
26+
SnapshotStatusChecks: () => <div data-test-id="snapshot-status-checks" />,
27+
}));
28+
29+
jest.mock('./statusCheckRules', () => ({
30+
StatusCheckRules: () => <div data-test-id="status-check-rules" />,
31+
}));
32+
33+
describe('PreprodSettings', () => {
34+
const pageDescription =
35+
'Configure status checks and thresholds for your mobile build size analysis.';
36+
37+
it('renders the mobile builds title outside page-frame mode', () => {
38+
render(<PreprodSettings />, {
39+
organization: OrganizationFixture(),
40+
});
41+
42+
expect(screen.getByRole('heading', {name: 'Mobile Builds'})).toBeInTheDocument();
43+
expect(screen.getByText(pageDescription)).toBeInTheDocument();
44+
});
45+
46+
it('hides the duplicate mobile builds title in page-frame mode', () => {
47+
render(
48+
<TopBar.Slot.Provider>
49+
<PreprodSettings />
50+
</TopBar.Slot.Provider>,
51+
{
52+
organization: OrganizationFixture({features: ['page-frame']}),
53+
}
54+
);
55+
56+
expect(
57+
screen.queryByRole('heading', {name: 'Mobile Builds'})
58+
).not.toBeInTheDocument();
59+
expect(screen.getByText(pageDescription)).toBeInTheDocument();
60+
});
61+
});

static/app/views/settings/project/preprod/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ export default function PreprodSettings() {
4242
const navigate = useNavigate();
4343
const organization = useOrganization();
4444
const hasPageFrameFeature = useHasPageFrameFeature();
45+
const pageDescription = t(
46+
'Configure status checks and thresholds for your mobile build size analysis.'
47+
);
4548

4649
const hasSnapshots = organization.features.includes('preprod-snapshots');
4750

@@ -62,9 +65,7 @@ export default function PreprodSettings() {
6265
<SentryDocumentTitle title={t('Mobile Builds')} />
6366
<SettingsPageHeader
6467
title={t('Mobile Builds')}
65-
subtitle={t(
66-
'Configure status checks and thresholds for your mobile build size analysis.'
67-
)}
68+
subtitle={pageDescription}
6869
action={
6970
<Grid flow="column" align="center" gap="lg">
7071
{hasPageFrameFeature ? (

0 commit comments

Comments
 (0)