Skip to content

Commit e85842c

Browse files
jeremy-davis-sonarsourcesonartech
authored andcommitted
SC-33636 New branch & PR selection dropdown (#3752)
GitOrigin-RevId: 66fccca116dcd31be62e65c0fcc4e02c978cd20a
1 parent e70bca2 commit e85842c

File tree

14 files changed

+320
-210
lines changed

14 files changed

+320
-210
lines changed

apps/sq-server/src/main/js/app/components/nav/component/Menu.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { DropdownMenu } from '@sonarsource/echoes-react';
2222
import { pick } from 'lodash';
2323
import React from 'react';
2424
import { FormattedMessage, useIntl } from 'react-intl';
25-
import { useCurrentBranchQuery } from '~adapters/queries/branch';
25+
import { useCurrentBranchQuery, useProjectBranchesQuery } from '~adapters/queries/branch';
2626
import { useLocation } from '~shared/components/hoc/withRouter';
2727
import { getBranchLikeQuery, isPullRequest } from '~shared/helpers/branch-like';
2828
import { isApplication, isPortfolioLike, isProject } from '~shared/helpers/component';
@@ -43,7 +43,6 @@ import {
4343
getProjectQualityProfileSettingsUrl,
4444
getProjectQueryUrl,
4545
} from '~sq-server-commons/helpers/urls';
46-
import { useBranchesQuery } from '~sq-server-commons/queries/branch';
4746
import { useGetValueQuery } from '~sq-server-commons/queries/settings';
4847
import { Feature } from '~sq-server-commons/types/features';
4948
import { SettingsKey } from '~sq-server-commons/types/settings';
@@ -81,7 +80,7 @@ export function Menu(props: Readonly<Props>) {
8180
const { component, hasFeature, isInProgress, isPending } = props;
8281
const { extensions = [], canBrowseAllChildProjects, qualifier, configuration = {} } = component;
8382

84-
const { data: branchLikes = [] } = useBranchesQuery(component);
83+
const { data: branchLikes = [] } = useProjectBranchesQuery(component.key);
8584
const { data: branchLike } = useCurrentBranchQuery(component);
8685

8786
const { data: architectureOptIn, isLoading: isLoadingArchitectureOptIn } = useGetValueQuery({

apps/sq-server/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import styled from '@emotion/styled';
2222
import { Button } from '@sonarsource/echoes-react';
2323
import * as React from 'react';
24-
import { useCurrentBranchQuery } from '~adapters/queries/branch';
24+
import { useCurrentBranchQuery, useProjectBranchesQuery } from '~adapters/queries/branch';
2525
import { Popup, PopupPlacement, PopupZLevel } from '~design-system';
2626
import { isPullRequest } from '~shared/helpers/branch-like';
2727
import { isDefined } from '~shared/helpers/types';
@@ -32,7 +32,6 @@ import EscKeydownHandler from '~sq-server-commons/components/controls/EscKeydown
3232
import FocusOutHandler from '~sq-server-commons/components/controls/FocusOutHandler';
3333
import OutsideClickHandler from '~sq-server-commons/components/controls/OutsideClickHandler';
3434
import { useAvailableFeatures } from '~sq-server-commons/context/available-features/withAvailableFeatures';
35-
import { useBranchesQuery } from '~sq-server-commons/queries/branch';
3635
import { Feature } from '~sq-server-commons/types/features';
3736
import { Component } from '~sq-server-commons/types/types';
3837
import BranchHelpTooltip from './BranchHelpTooltip';
@@ -50,7 +49,7 @@ export function BranchLikeNavigation(props: BranchLikeNavigationProps) {
5049
component: { configuration },
5150
} = props;
5251

53-
const { data: branchLikes } = useBranchesQuery(component);
52+
const { data: branchLikes = [] } = useProjectBranchesQuery(component.key);
5453
const { data: currentBranchLike } = useCurrentBranchQuery(component);
5554

5655
const [isMenuOpen, setIsMenuOpen] = React.useState(false);

apps/sq-server/src/main/js/apps/overview/components/EmptyOverview.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import styled from '@emotion/styled';
2222
import { Spinner } from '@sonarsource/echoes-react';
2323
import * as React from 'react';
2424
import { Navigate } from 'react-router-dom';
25+
import { useProjectBranchesQuery } from '~adapters/queries/branch';
2526
import { FlagMessage, LargeCenteredLayout } from '~design-system';
2627
import { useLocation } from '~shared/components/hoc/withRouter';
2728
import { isBranch, isMainBranch } from '~shared/helpers/branch-like';
@@ -32,7 +33,6 @@ import { getBranchLikeDisplayName } from '~sq-server-commons/helpers/branch-like
3233
import { translate, translateWithParameters } from '~sq-server-commons/helpers/l10n';
3334
import { getProjectTutorialLocation } from '~sq-server-commons/helpers/urls';
3435
import { hasGlobalPermission } from '~sq-server-commons/helpers/users';
35-
import { useBranchesQuery } from '~sq-server-commons/queries/branch';
3636
import { useTaskForComponentQuery } from '~sq-server-commons/queries/component';
3737
import { AlmKeys } from '~sq-server-commons/types/alm-settings';
3838
import { BranchLike } from '~sq-server-commons/types/branch-like';
@@ -53,7 +53,7 @@ export function EmptyOverview(props: Readonly<EmptyOverviewProps>) {
5353
query: { id: urlComponentKey },
5454
} = useLocation();
5555

56-
const { data: branchLikes } = useBranchesQuery(component);
56+
const { data: branchLikes = [] } = useProjectBranchesQuery(component.key);
5757

5858
const [currentUserCanScanProject, setCurrentUserCanScanProject] = React.useState(
5959
hasGlobalPermission(currentUser, Permissions.Scan),
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* SonarQube
3+
* Copyright (C) 2009-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
import { IconBranch, IconGitBranch, IconProps, IconPullrequest } from '@sonarsource/echoes-react';
22+
import { isMainBranch, isPullRequest } from '../../helpers/branch-like';
23+
import { BranchLikeBase } from '../../types/branch-like';
24+
25+
interface Props extends IconProps {
26+
branchLike: BranchLikeBase;
27+
}
28+
29+
export function BranchLikeIcon({ branchLike, ...iconProps }: Readonly<Props>) {
30+
if (isMainBranch(branchLike)) {
31+
return <IconBranch {...iconProps} />;
32+
}
33+
34+
if (isPullRequest(branchLike)) {
35+
return <IconPullrequest {...iconProps} />;
36+
}
37+
38+
return <IconGitBranch {...iconProps} />;
39+
}

libs/shared/src/types/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ export interface Extension {
3232
}
3333

3434
export type QGStatus = 'ERROR' | 'NONE' | 'OK';
35+
export type QGStatusExtended = QGStatus | 'NOT_COMPUTED';

libs/sq-server-commons/src/components/icon-mappers/BranchLikeIcon.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ function PullRequestIcon(props: Readonly<IconProps>) {
4444
return <IconPullrequest data-testid="branch-like-icon-pull-request" {...props} />;
4545
}
4646

47+
/** @deprecated Use {@link ~shared/src/components/icon-mappers/BranchLikeIcon.tsx} instead */
4748
export default function BranchLikeIcon({ branchLike, ...props }: Readonly<BranchLikeIconProps>) {
4849
let Icon;
4950

libs/sq-server-commons/src/components/tutorials/TutorialSelectionRenderer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ import {
2828
Text,
2929
} from '@sonarsource/echoes-react';
3030
import * as React from 'react';
31+
import { useProjectBranchesQuery } from '~adapters/queries/branch';
3132
import { isMainBranch } from '~shared/helpers/branch-like';
3233
import { GreyCard } from '../../design-system';
3334
import { translate } from '../../helpers/l10n';
3435
import { getProjectTutorialLocation } from '../../helpers/urls';
35-
import { useBranchesQuery } from '../../queries/branch';
3636
import { Image } from '../../sq-server-adapters/components/common/Image';
3737
import { AlmKeys, AlmSettingsInstance, ProjectAlmBindingResponse } from '../../types/alm-settings';
3838
import { MainBranch } from '../../types/branch-like';
@@ -98,7 +98,7 @@ export default function TutorialSelectionRenderer(props: Readonly<TutorialSelect
9898
willRefreshAutomatically,
9999
} = props;
100100

101-
const { data: branchLikes = [] } = useBranchesQuery(component);
101+
const { data: branchLikes = [] } = useProjectBranchesQuery(component.key);
102102

103103
const mainBranchName =
104104
(branchLikes.find((b) => isMainBranch(b)) as MainBranch | undefined)?.name ||

libs/sq-server-commons/src/design-system/components/QualityGateIndicator.tsx

Lines changed: 1 addition & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -18,165 +18,4 @@
1818
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
*/
2020

21-
import { useTheme } from '@emotion/react';
22-
import React from 'react';
23-
import { useIntl } from 'react-intl';
24-
import { theme as twTheme } from 'twin.macro';
25-
import { BasePlacement, PopupPlacement } from '../helpers/positioning';
26-
import { themeColor, themeContrast } from '../helpers/theme';
27-
import { QGStatus } from '../types/quality-gates';
28-
29-
const SIZE = {
30-
sm: twTheme('spacing.4'),
31-
md: twTheme('spacing.6'),
32-
xl: twTheme('spacing.16'),
33-
};
34-
35-
interface Props {
36-
className?: string;
37-
size?: keyof typeof SIZE;
38-
status: QGStatus;
39-
tooltipPlacement?: BasePlacement;
40-
}
41-
42-
const RX_4 = 4;
43-
const RX_2 = 2;
44-
45-
const formatQualityGateTitle = (intl: ReturnType<typeof useIntl>, status: QGStatus) => {
46-
const formatted = intl.formatMessage({ id: `metric.level.${status}` });
47-
return intl.formatMessage({ id: 'overview.quality_gate_x' }, { status: formatted });
48-
};
49-
50-
export function QualityGateIndicator(props: Props) {
51-
const { className, size = 'md', status, tooltipPlacement = PopupPlacement.Right } = props;
52-
const iconProps = {
53-
className,
54-
height: SIZE[size],
55-
rx: size === 'xl' ? RX_4 : RX_2,
56-
size,
57-
tooltipPlacement,
58-
width: SIZE[size],
59-
};
60-
let StatusComponent: React.ReactNode;
61-
switch (status) {
62-
case 'NONE':
63-
case 'NOT_COMPUTED':
64-
StatusComponent = <QGNotComputed {...iconProps} />;
65-
break;
66-
case 'OK':
67-
StatusComponent = <QGPassed {...iconProps} />;
68-
break;
69-
case 'ERROR':
70-
StatusComponent = <QGFailed {...iconProps} />;
71-
break;
72-
}
73-
return <div className="sw-flex sw-justify-center sw-items-center">{StatusComponent}</div>;
74-
}
75-
76-
const COMMON_PROPS = {
77-
fill: 'none',
78-
role: 'img',
79-
xmlns: 'http://www.w3.org/2000/svg',
80-
};
81-
82-
interface IconProps {
83-
className?: string;
84-
height: string;
85-
rx: number;
86-
size: keyof typeof SIZE;
87-
tooltipPlacement?: BasePlacement;
88-
width: string;
89-
}
90-
91-
function QGNotComputed({ className, rx, size, tooltipPlacement, ...sizeProps }: IconProps) {
92-
const theme = useTheme();
93-
const contrastColor = themeContrast('qgIndicatorNotComputed')({ theme });
94-
const intl = useIntl();
95-
const title = formatQualityGateTitle(intl, 'NONE');
96-
97-
return (
98-
<svg className={className} {...COMMON_PROPS} {...sizeProps}>
99-
<title>{title}</title>
100-
<rect fill={themeColor('qgIndicatorNotComputed')({ theme })} rx={rx} {...sizeProps} />
101-
{
102-
{
103-
xl: <path d="M42 31v3H22v-3z" fill={contrastColor} />,
104-
md: <path d="M18 12v1.5H6V12z" fill={contrastColor} />,
105-
sm: <path d="M12 8v1H4V8z" fill={contrastColor} />,
106-
}[size]
107-
}
108-
</svg>
109-
);
110-
}
111-
112-
function QGPassed({ className, rx, size, tooltipPlacement, ...sizeProps }: IconProps) {
113-
const theme = useTheme();
114-
const contrastColor = themeContrast('qgIndicatorPassed')({ theme });
115-
const intl = useIntl();
116-
const title = formatQualityGateTitle(intl, 'OK');
117-
118-
return (
119-
<svg className={className} {...COMMON_PROPS} {...sizeProps}>
120-
<title>{title}</title>
121-
<rect fill={themeColor('qgIndicatorPassed')({ theme })} rx={rx} {...sizeProps} />
122-
{
123-
{
124-
xl: (
125-
<>
126-
<path d="M38.974 25 41 27.026 28.847 39.178l-2.025-2.025z" fill={contrastColor} />
127-
<path d="M30.974 37.153 28.95 39.18 22 32.229l2.026-2.025z" fill={contrastColor} />
128-
</>
129-
),
130-
md: (
131-
<>
132-
<path d="m16.95 7.5 1.308 1.307-7.84 7.84-1.308-1.306z" fill={contrastColor} />
133-
<path d="m11.79 15.34-1.307 1.307-4.484-4.483 1.307-1.306z" fill={contrastColor} />
134-
</>
135-
),
136-
sm: (
137-
<>
138-
<path d="m11.3 5 .871.87-5.227 5.228-.87-.871z" fill={contrastColor} />
139-
<path d="m7.86 10.227-.872.871L4 8.11l.871-.871z" fill={contrastColor} />
140-
</>
141-
),
142-
}[size]
143-
}
144-
</svg>
145-
);
146-
}
147-
148-
function QGFailed({ className, rx, size, tooltipPlacement, ...sizeProps }: IconProps) {
149-
const theme = useTheme();
150-
const contrastColor = themeContrast('qgIndicatorFailed')({ theme });
151-
const intl = useIntl();
152-
const title = formatQualityGateTitle(intl, 'ERROR');
153-
154-
return (
155-
<svg className={className} {...COMMON_PROPS} {...sizeProps}>
156-
<title>{title}</title>
157-
<rect fill={themeColor('qgIndicatorFailed')({ theme })} rx={rx} {...sizeProps} />
158-
{
159-
{
160-
xl: (
161-
<>
162-
<path d="m37.153 25 2.026 2.026-12.153 12.152L25 37.153z" fill={contrastColor} />
163-
<path d="m39.178 37.153-2.025 2.026L25 27.026 27.026 25z" fill={contrastColor} />
164-
</>
165-
),
166-
md: (
167-
<>
168-
<path d="m15.34 7.5 1.307 1.307-7.84 7.84L7.5 15.34z" fill={contrastColor} />
169-
<path d="m16.647 15.34-1.307 1.307-7.84-7.84L8.806 7.5z" fill={contrastColor} />
170-
</>
171-
),
172-
sm: (
173-
<>
174-
<path d="m10.227 5 .871.871-5.227 5.227L5 10.227z" fill={contrastColor} />
175-
<path d="m11.098 10.227-.871.87L5 5.872 5.87 5z" fill={contrastColor} />
176-
</>
177-
),
178-
}[size]
179-
}
180-
</svg>
181-
);
182-
}
21+
export { QualityGateIndicator } from '~adapters/components/ui/QualityGateIndicator';

libs/sq-server-commons/src/helpers/branch-like.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { isBranch, isMainBranch, isPullRequest } from '~shared/helpers/branch-li
2323
import { PullRequest } from '~shared/types/branch-like';
2424
import { Branch, BranchLike, BranchLikeTree } from '../types/branch-like';
2525

26+
export { isSameBranchLike } from '~adapters/helpers/branch-like';
27+
2628
export function sortBranches(branches: Branch[]) {
2729
return orderBy(branches, [(b) => b.isMain, (b) => b.name], ['desc', 'asc']);
2830
}
@@ -43,26 +45,6 @@ export function getBranchLikeKey(branchLike: BranchLike) {
4345
return isPullRequest(branchLike) ? `pull-request-${branchLike.key}` : `branch-${branchLike.name}`;
4446
}
4547

46-
export function isSameBranchLike(a: BranchLike | undefined, b: BranchLike | undefined) {
47-
// main branches are always equal
48-
if (isMainBranch(a) && isMainBranch(b)) {
49-
return true;
50-
}
51-
52-
// Branches are compared by name
53-
if (isBranch(a) && isBranch(b)) {
54-
return a.name === b.name;
55-
}
56-
57-
// pull requests are compared by id
58-
if (isPullRequest(a) && isPullRequest(b)) {
59-
return a.key === b.key;
60-
}
61-
62-
// finally if both parameters are `undefined`, consider them equal
63-
return a === b;
64-
}
65-
6648
export function getBrancheLikesAsTree(branchLikes: BranchLike[]): BranchLikeTree {
6749
const mainBranch = branchLikes.find(isMainBranch);
6850
const branches = orderBy(

libs/sq-server-commons/src/l10n/default.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export const defaultMessages = {
5858
bold: 'Bold',
5959
branch: 'Branch',
6060
'branch.small': 'branch',
61+
branches: 'Branches',
6162
breadcrumbs: 'Breadcrumbs',
6263
expand_breadcrumbs: 'Expand breadcrumbs',
6364
by_: 'by',
@@ -232,6 +233,8 @@ export const defaultMessages = {
232233
project_plural: 'projects',
233234
projects_management: 'Projects Management',
234235
'pull_request.small': 'pull request',
236+
pull_requests: 'Pull Requests',
237+
quality_gate: 'Quality Gate',
235238
quality_profile: 'Quality Profile',
236239
raw: 'Raw',
237240
recent_history: 'Recent History',
@@ -3365,6 +3368,17 @@ export const defaultMessages = {
33653368
'project_navigation.binding_status.bind.tooltip':
33663369
'Enable automatically configured PR decoration and privacy settings based on your DevOps platform by binding your project',
33673370

3371+
//------------------------------------------------------------------------------
3372+
//
3373+
// PROJECT BRANCH SELECTOR
3374+
//
3375+
//------------------------------------------------------------------------------
3376+
3377+
'project_branch_selector.title': 'Branches and pull requests',
3378+
'project_branch_selector.filters': 'Filter by branches or pull requests',
3379+
'project_branch_selector.search': 'Search branches or pull requests',
3380+
'project_branch_selector.none': 'No results',
3381+
33683382
//------------------------------------------------------------------------------
33693383
//
33703384
// PROJECT ACTIVITY/HISTORY SERVICE

0 commit comments

Comments
 (0)