Skip to content

Commit 36d9ca1

Browse files
Hartesicsonartech
authored andcommitted
SONAR-25927 Allow users to create a Jira ticket from a single SQS issue
GitOrigin-RevId: e03ba2403ced18a4228d9b7b1bdd91b5dba3589d
1 parent e85842c commit 36d9ca1

File tree

9 files changed

+132
-67
lines changed

9 files changed

+132
-67
lines changed

apps/sq-server/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import { screen, waitFor, within } from '@testing-library/react';
2222
import userEvent from '@testing-library/user-event';
23+
import { ComponentProps } from 'react';
2324
import { renderOwaspTop102021Category } from '~shared/helpers/security-standards';
2425
import { mockLoggedInUser, mockRawIssue } from '~sq-server-commons/helpers/testMocks';
2526
import { Feature } from '~sq-server-commons/types/features';
@@ -39,7 +40,7 @@ import IssuesList from '../components/IssuesList';
3940
import { renderIssueApp, renderProjectIssuesApp } from '../test-utils';
4041

4142
jest.mock('../components/IssuesList', () => {
42-
const fakeIssueList = (props: IssuesList['props']) => {
43+
const fakeIssueList = (props: ComponentProps<typeof IssuesList>) => {
4344
return (
4445
<>
4546
{props.issues.map((i) => (

apps/sq-server/src/main/js/apps/issues/components/IssueDetails.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import styled from '@emotion/styled';
2222
import { Spinner } from '@sonarsource/echoes-react';
23+
import { ComponentProps, useMemo } from 'react';
2324
import { Helmet } from 'react-helmet-async';
2425
import { useIntl } from 'react-intl';
2526
import {
@@ -34,6 +35,7 @@ import StyledNavFix from '~shared/components/nav/NavFix';
3435
import { isPortfolioLike } from '~shared/helpers/component';
3536
import { ComponentQualifier } from '~shared/types/component';
3637
import { Paging } from '~shared/types/paging';
38+
import { addons } from '~sq-server-addons/index';
3739
import ScreenPositionHelper from '~sq-server-commons/components/common/ScreenPositionHelper';
3840
import { AiCodeFixTab } from '~sq-server-commons/components/rules/AiCodeFixTab';
3941
import IssueTabViewer from '~sq-server-commons/components/rules/IssueTabViewer';
@@ -86,6 +88,21 @@ export default function IssueDetails({
8688

8789
const intl = useIntl();
8890

91+
const additionalIssueActions = useMemo(() => {
92+
const additionalActions = [] as Required<
93+
ComponentProps<typeof IssueTabViewer>
94+
>['additionalIssueActions'];
95+
96+
if (addons.jira !== undefined && component !== undefined) {
97+
const { IssueJiraWorkItem } = addons.jira;
98+
additionalActions.push(({ issue }) => (
99+
<IssueJiraWorkItem component={component} issue={issue} />
100+
));
101+
}
102+
103+
return additionalActions;
104+
}, [component]);
105+
89106
const warning = !canBrowseAllChildProjects && isPortfolioLike(qualifier) && (
90107
<FlagMessage
91108
className="it__portfolio_warning sw-flex"
@@ -166,6 +183,7 @@ export default function IssueDetails({
166183
onChange={handleIssueChange}
167184
/>
168185
}
186+
additionalIssueActions={additionalIssueActions}
169187
codeTabContent={
170188
<IssuesSourceViewer
171189
branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}

apps/sq-server/src/main/js/apps/issues/components/IssuesList.tsx

Lines changed: 64 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@
2121
import { Spinner } from '@sonarsource/echoes-react';
2222
import { groupBy } from 'lodash';
2323
import * as React from 'react';
24+
import { addons } from '~sq-server-addons/index';
2425
import IssueItem from '~sq-server-commons/components/issue/Issue';
2526
import { BranchLike } from '~sq-server-commons/types/branch-like';
2627
import { Component, Issue } from '~sq-server-commons/types/types';
2728
import ComponentBreadcrumbs from './ComponentBreadcrumbs';
2829

29-
interface Props {
30+
interface IssuesListProps {
3031
branchLike: BranchLike | undefined;
3132
checked: string[];
3233
component: Component | undefined;
@@ -39,66 +40,74 @@ interface Props {
3940
selectedIssue: Issue | undefined;
4041
}
4142

42-
interface State {
43-
prerender: boolean;
44-
}
43+
export default function IssuesList({
44+
branchLike,
45+
checked,
46+
component,
47+
issues,
48+
onIssueChange,
49+
onIssueCheck,
50+
onIssueSelect,
51+
onPopupToggle,
52+
openPopup,
53+
selectedIssue,
54+
}: Readonly<IssuesListProps>) {
55+
const [prerender, setPrerender] = React.useState(true);
4556

46-
export default class IssuesList extends React.PureComponent<Props, State> {
47-
state: State = {
48-
prerender: true,
49-
};
57+
const issuesByComponent = React.useMemo(
58+
() => groupBy(issues, (issue) => `(${issue.component} : ${issue.branch})`),
59+
[issues],
60+
);
5061

51-
componentDidMount() {
52-
if (this.props.issues.length > 0) {
53-
this.setState({ prerender: false });
54-
}
55-
}
62+
const additionalIssueActions = React.useMemo(() => {
63+
const additionalActions = [] as Required<
64+
React.ComponentProps<typeof IssueItem>
65+
>['additionalIssueActions'];
5666

57-
componentDidUpdate() {
58-
if (this.props.issues.length > 0) {
59-
this.setState({ prerender: false });
67+
if (addons.jira !== undefined && component !== undefined) {
68+
const JiraWorkItemComponent = addons.jira.IssueJiraWorkItem;
69+
additionalActions.push(({ issue }) => (
70+
<JiraWorkItemComponent component={component} issue={issue} />
71+
));
6072
}
61-
}
6273

63-
renderIssueComponentList = ([key, issues]: [string, Issue[]]) => {
64-
const { branchLike, checked, component, openPopup, selectedIssue } = this.props;
65-
return (
66-
<li key={key}>
67-
<ComponentBreadcrumbs component={component} issue={issues[0]} />
68-
<ul>
69-
{issues.map((issue) => (
70-
<IssueItem
71-
branchLike={branchLike}
72-
checked={checked.includes(issue.key)}
73-
issue={issue}
74-
key={issue.key}
75-
onChange={this.props.onIssueChange}
76-
onCheck={this.props.onIssueCheck}
77-
onPopupToggle={this.props.onPopupToggle}
78-
onSelect={this.props.onIssueSelect}
79-
openPopup={openPopup && openPopup.issue === issue.key ? openPopup.name : undefined}
80-
selected={selectedIssue != null && selectedIssue.key === issue.key}
81-
/>
82-
))}
83-
</ul>
84-
</li>
85-
);
86-
};
74+
return additionalActions;
75+
}, [component]);
8776

88-
render() {
89-
const { issues } = this.props;
90-
const { prerender } = this.state;
91-
92-
if (prerender) {
93-
return (
94-
<div>
95-
<Spinner />
96-
</div>
97-
);
77+
React.useEffect(() => {
78+
if (issues.length > 0) {
79+
setPrerender(false);
9880
}
81+
}, [issues]);
9982

100-
const issuesByComponent = groupBy(issues, (issue) => `(${issue.component} : ${issue.branch})`);
101-
102-
return <ul>{Object.entries(issuesByComponent).map(this.renderIssueComponentList)}</ul>;
103-
}
83+
return (
84+
<Spinner isLoading={prerender}>
85+
<ul>
86+
{Object.entries(issuesByComponent).map(([key, issues]: [string, Issue[]]) => (
87+
<li key={key}>
88+
<ComponentBreadcrumbs component={component} issue={issues[0]} />
89+
<ul>
90+
{issues.map((issue) => (
91+
<IssueItem
92+
additionalIssueActions={additionalIssueActions}
93+
branchLike={branchLike}
94+
checked={checked.includes(issue.key)}
95+
issue={issue}
96+
key={issue.key}
97+
onChange={onIssueChange}
98+
onCheck={onIssueCheck}
99+
onPopupToggle={onPopupToggle}
100+
onSelect={onIssueSelect}
101+
openPopup={
102+
openPopup && openPopup.issue === issue.key ? openPopup.name : undefined
103+
}
104+
selected={selectedIssue != null && selectedIssue.key === issue.key}
105+
/>
106+
))}
107+
</ul>
108+
</li>
109+
))}
110+
</ul>
111+
</Spinner>
112+
);
104113
}

libs/sq-server-commons/src/components/issue/Issue.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { updateIssue } from './actions';
3232
import IssueView from './components/IssueView';
3333

3434
interface Props {
35+
additionalIssueActions?: React.ComponentType<{ issue: TypeIssue }>[];
3536
branchLike?: BranchLike;
3637
checked?: boolean;
3738
displayWhyIsThisAnIssue?: boolean;
@@ -46,14 +47,15 @@ interface Props {
4647

4748
function Issue(props: Readonly<Props>) {
4849
const {
49-
selected = false,
50-
issue,
50+
additionalIssueActions,
5151
branchLike,
5252
checked,
53-
openPopup,
5453
displayWhyIsThisAnIssue,
54+
issue,
5555
onCheck,
5656
onPopupToggle,
57+
openPopup,
58+
selected = false,
5759
} = props;
5860

5961
const { component } = useComponent();
@@ -145,6 +147,7 @@ function Issue(props: Readonly<Props>) {
145147

146148
return (
147149
<IssueView
150+
additionalIssueActions={additionalIssueActions}
148151
branchLike={branchLike}
149152
checked={checked}
150153
currentPopup={openPopup}

libs/sq-server-commons/src/components/issue/components/IssueActionsBar.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import IssueTransition from './IssueTransition';
2727
import SonarLintBadge from './SonarLintBadge';
2828

2929
interface Props {
30+
additionalIssueActions?: React.ComponentType<{ issue: Issue }>[];
3031
canSetTags?: boolean;
3132
currentPopup?: string;
3233
issue: Issue;
@@ -39,14 +40,15 @@ interface Props {
3940

4041
export default function IssueActionsBar(props: Readonly<Props>) {
4142
const {
42-
issue,
43+
additionalIssueActions,
44+
canSetTags,
4345
currentPopup,
46+
issue,
4447
onAssign,
4548
onChange,
46-
togglePopup,
4749
showSonarLintBadge,
4850
showTags,
49-
canSetTags,
51+
togglePopup,
5052
} = props;
5153

5254
const canAssign = issue.actions.includes(IssueActions.Assign);
@@ -78,6 +80,12 @@ export default function IssueActionsBar(props: Readonly<Props>) {
7880
/>
7981
</li>
8082

83+
{additionalIssueActions?.map((ActionComponent) => (
84+
<li key={`${ActionComponent.displayName}-${issue.key}`}>
85+
<ActionComponent issue={issue} />
86+
</li>
87+
))}
88+
8189
{showTags && (
8290
<li>
8391
<IssueTags

libs/sq-server-commons/src/components/issue/components/IssueView.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import IssueTags from './IssueTags';
4545
import IssueTitleBar from './IssueTitleBar';
4646

4747
interface Props {
48+
additionalIssueActions?: React.ComponentType<{ issue: Issue }>[];
4849
branchLike?: BranchLike;
4950
checked?: boolean;
5051
currentPopup?: string;
@@ -60,17 +61,18 @@ interface Props {
6061

6162
export default function IssueView(props: Readonly<Props>) {
6263
const {
63-
issue,
64+
additionalIssueActions,
6465
branchLike,
6566
checked,
6667
currentPopup,
6768
displayWhyIsThisAnIssue,
69+
issue,
6870
onAssign,
6971
onChange,
72+
onCheck,
7073
onSelect,
71-
togglePopup,
7274
selected,
73-
onCheck,
75+
togglePopup,
7476
} = props;
7577
const intl = useIntl();
7678
const nodeRef = useRef<HTMLLIElement>(null);
@@ -210,6 +212,7 @@ export default function IssueView(props: Readonly<Props>) {
210212

211213
<div className="sw-flex sw-gap-2 sw-flex-nowrap sw-items-center sw-justify-between">
212214
<IssueActionsBar
215+
additionalIssueActions={additionalIssueActions}
213216
currentPopup={currentPopup}
214217
issue={issue}
215218
onAssign={onAssign}

libs/sq-server-commons/src/components/issues/IssueHeader.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import IssueHeaderMeta from './IssueHeaderMeta';
4545
import IssueHeaderSide from './IssueHeaderSide';
4646

4747
interface Props {
48+
additionalIssueActions?: React.ComponentType<{ issue: Issue }>[];
4849
branchLike?: BranchLike;
4950
issue: Issue;
5051
onIssueChange: (issue: Issue) => void;
@@ -199,7 +200,7 @@ export default class IssueHeader extends React.PureComponent<Props, State> {
199200
};
200201

201202
render() {
202-
const { issue, branchLike } = this.props;
203+
const { additionalIssueActions, issue, branchLike } = this.props;
203204
const { issuePopupName } = this.state;
204205
const issueUrl = getComponentIssuesUrl(issue.project, {
205206
...getBranchLikeQuery(branchLike),
@@ -240,6 +241,7 @@ export default class IssueHeader extends React.PureComponent<Props, State> {
240241
<BasicSeparator />
241242

242243
<IssueActionsBar
244+
additionalIssueActions={additionalIssueActions}
243245
canSetTags={canSetTags}
244246
currentPopup={issuePopupName}
245247
issue={issue}

libs/sq-server-commons/src/components/rules/IssueTabViewer.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { TabSelectorContext } from './TabSelectorContext';
4242

4343
interface IssueTabViewerProps extends CurrentUserContextInterface {
4444
activityTabContent?: React.ReactNode;
45+
additionalIssueActions?: React.ComponentType<{ issue: Issue }>[];
4546
aiSuggestionAvailable: boolean;
4647
codeTabContent?: React.ReactNode;
4748
currentUser: CurrentUser;
@@ -363,7 +364,7 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
363364
};
364365

365366
render() {
366-
const { issue, ruleDetails } = this.props;
367+
const { additionalIssueActions, issue, ruleDetails } = this.props;
367368
const { tabs, selectedTab } = this.state;
368369

369370
if (!tabs || tabs.length === 0 || !selectedTab) {
@@ -385,6 +386,7 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
385386
>
386387
<div className="sw-p-6 sw-pb-4" ref={(node) => (this.headerNode = node)}>
387388
<IssueHeader
389+
additionalIssueActions={additionalIssueActions}
388390
branchLike={fillBranchLike(issue.branch, issue.pullRequest)}
389391
issue={issue}
390392
onIssueChange={this.props.onIssueChange}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8879,4 +8879,23 @@ export const defaultMessages = {
88798879
'{count} SonarQube {count, plural, one {issue} other {issues}} and corresponding Jira work items will no longer be synchronized. However, existing Jira work items will not be deleted.',
88808880
'project_settings.jira_binding.unbind_confirm.alert':
88818881
'Previous connections cannot be restored. Rebinding or creating a new project binding with Jira can lead to duplicate Jira work items.',
8882+
8883+
'issue.jira.work_item_deletion_error': 'Failed removing link to Jira issue {0}',
8884+
'issue.jira.work_item_deletion_link': 'Remove link',
8885+
'issue.jira.work_item_deletion_tooltip': 'Remove link to Jira issue',
8886+
'issue.jira.work_item_deletion_modal_description':
8887+
'This action cannot be undone. Deleting this connection only removes the link between SonarQube and Jira; it will not delete any work items in Jira itself.',
8888+
'issue.jira.work_item_deletion_modal_title': 'Are you sure you want to remove the Jira link?',
8889+
'issue.jira.work_item_deletion_success': 'Link to Jira issue {0} has been removed',
8890+
'issue.jira.work_item_creation': 'Push to Jira',
8891+
'issue.jira.work_item_creation.in_progress': 'Pushing to Jira',
8892+
'issue.jira.work_item_creation.count':
8893+
'Push {count} {count, plural, one {issue} other {issues}} to Jira',
8894+
'issue.jira.work_item_creation_success': 'Work item {0} created in Jira',
8895+
'issue.jira.work_type_dropdown': 'Choose Jira work type',
8896+
'issue.jira.view_work_item': 'View work item',
8897+
'issue.jira.work_items_creation_failure':
8898+
"An error occurred. Please make sure you don't try to push the same issue twice to Jira.",
8899+
'issue.jira.work_items_creation.success':
8900+
'Jira work {count, plural, one {item} other {items}} <link>{jiraWorkItemKey}</link> has been successfully created and linked to {count} SonarQube {count, plural, one {issue} other {issues}}',
88828901
};

0 commit comments

Comments
 (0)