]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21259 Show fixed issues in issues list of the target branch
authorstanislavh <stanislav.honcharov@sonarsource.com>
Wed, 20 Dec 2023 11:11:16 +0000 (12:11 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 17 Jan 2024 20:02:44 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
server/sonar-web/src/main/js/apps/issues/components/IssuesListTitle.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/test-utils.tsx
server/sonar-web/src/main/js/apps/issues/utils.ts
server/sonar-web/src/main/js/apps/overview/pullRequests/IssueMeasuresCard.tsx
server/sonar-web/src/main/js/helpers/mocks/issues.ts
server/sonar-web/src/main/js/queries/branch.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 18bd744d8665fdd341643154ce7db4c59b83ac36..5ebcbd4d4e6143bc4ead0f1b6c8630be5749b534 100644 (file)
@@ -66,6 +66,12 @@ describe('issues app', () => {
 
       expect(screen.getByText('issues.not_all_issue_show')).toBeInTheDocument();
     });
+
+    it('should show fixed issues message', async () => {
+      renderProjectIssuesApp('project/issues?id=my-project&fixedInPullRequest=01');
+
+      expect(await ui.fixedIssuesHeading.find()).toBeInTheDocument();
+    });
   });
 
   describe('navigation', () => {
index 28a2fbb9737de8988d535f451827df1ed3902dfc..7f580d8fe53b5e321809ec119a821fb7fae22a96 100644 (file)
@@ -72,6 +72,7 @@ describe('serialize/deserialize', () => {
         issueStatuses: [IssueStatus.Accepted, IssueStatus.Confirmed],
         tags: ['a', 'b'],
         types: ['a', 'b'],
+        fixedInPullRequest: '',
       }),
     ).toStrictEqual({
       assignees: 'a,b',
@@ -152,6 +153,7 @@ describe('serialize/deserialize', () => {
       issueStatuses: [],
       tags: [],
       types: [],
+      fixedInPullRequest: '',
     });
   });
 
index 29509e3b0e311ffab40bead619199bac8390bf0a..60362397ded4437a60e3bbbd0bc12b0d63ad1547 100644 (file)
@@ -52,7 +52,12 @@ import IssueTabViewer from '../../../components/rules/IssueTabViewer';
 import '../../../components/search-navigator.css';
 import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
 import Spinner from '../../../components/ui/Spinner';
-import { fillBranchLike, getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like';
+import {
+  fillBranchLike,
+  getBranchLikeQuery,
+  isPullRequest,
+  isSameBranchLike,
+} from '../../../helpers/branch-like';
 import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthentication';
 import { parseIssueFromResponse } from '../../../helpers/issues';
 import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
@@ -104,6 +109,7 @@ import IssueGuide from './IssueGuide';
 import IssueNewStatusAndTransitionGuide from './IssueNewStatusAndTransitionGuide';
 import IssueReviewHistoryAndComments from './IssueReviewHistoryAndComments';
 import IssuesList from './IssuesList';
+import IssuesListTitle from './IssuesListTitle';
 import IssuesSourceViewer from './IssuesSourceViewer';
 import NoIssues from './NoIssues';
 import NoMyIssues from './NoMyIssues';
@@ -112,6 +118,7 @@ import { PSEUDO_SHADOW_HEIGHT } from './StyledHeader';
 
 interface Props extends WithIndexationContextProps {
   branchLike?: BranchLike;
+  branchLikes?: BranchLike[];
   component?: Component;
   currentUser: CurrentUser;
   isFetchingBranch?: boolean;
@@ -1128,8 +1135,8 @@ export class App extends React.PureComponent<Props, State> {
   }
 
   renderList() {
-    const { branchLike, component, currentUser } = this.props;
-    const { issues, loading, loadingMore, openIssue, paging } = this.state;
+    const { branchLike, component, currentUser, branchLikes } = this.props;
+    const { issues, loading, loadingMore, openIssue, paging, query } = this.state;
     const selectedIndex = this.getSelectedIndex();
     const selectedIssue = selectedIndex !== undefined ? issues[selectedIndex] : undefined;
 
@@ -1151,7 +1158,11 @@ export class App extends React.PureComponent<Props, State> {
 
     return (
       <div>
-        <h2 className="sw-sr-only">{translate('list_of_issues')}</h2>
+        <IssuesListTitle
+          fixedInPullRequest={query.fixedInPullRequest}
+          pullRequests={branchLikes?.filter(isPullRequest) ?? []}
+          component={component}
+        />
 
         {issues.length > 0 && (
           <IssuesList
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesListTitle.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesListTitle.tsx
new file mode 100644 (file)
index 0000000..6214d26
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { DiscreetLink, LightPrimary, PullRequestIcon, SubTitle } from 'design-system';
+import React from 'react';
+import { useIntl } from 'react-intl';
+import { getPullRequestUrl } from '../../../helpers/urls';
+import { PullRequest } from '../../../types/branch-like';
+import { Component } from '../../../types/types';
+
+interface IssuesListTitleProps {
+  component?: Component;
+  fixedInPullRequest: string;
+  pullRequests: PullRequest[];
+}
+
+export default function IssuesListTitle({
+  fixedInPullRequest,
+  pullRequests,
+  component,
+}: Readonly<IssuesListTitleProps>) {
+  const intl = useIntl();
+  const pullRequest = pullRequests.find((pr) => pr.key === fixedInPullRequest);
+  const prSummaryUrl = getPullRequestUrl(component?.key ?? '', fixedInPullRequest);
+
+  return pullRequest && !component?.needIssueSync ? (
+    <>
+      <SubTitle className="sw-mt-6 sw-mb-2">
+        {intl.formatMessage({ id: 'issues.fixed_issues' })}
+      </SubTitle>
+      <LightPrimary className="sw-flex sw-items-center sw-gap-1 sw-mb-2">
+        {intl.formatMessage(
+          { id: 'issues.fixed_issues.description' },
+          {
+            pullRequest: (
+              <>
+                <PullRequestIcon />
+                <DiscreetLink to={prSummaryUrl} className="sw-mt-[3px]">
+                  {pullRequest.title}
+                </DiscreetLink>
+              </>
+            ),
+          },
+        )}
+      </LightPrimary>
+    </>
+  ) : (
+    <h2 className="sw-sr-only">{intl.formatMessage({ id: 'list_of_issues' })}</h2>
+  );
+}
index 9db0654d83b7240bf4ca618d160c6a0b485d1312..0c3ed20a5952593ab3555e9ee6a245a17064372d 100644 (file)
@@ -32,6 +32,7 @@ import {
   SoftwareImpactSeverity,
   SoftwareQuality,
 } from '../../types/clean-code-taxonomy';
+import { Feature } from '../../types/features';
 import { Component } from '../../types/types';
 import { NoticeType } from '../../types/users';
 import IssuesApp from './components/IssuesApp';
@@ -55,6 +56,8 @@ export const ui = {
 
   issueItems: byRole('region'),
 
+  fixedIssuesHeading: byRole('heading', { level: 2, name: 'issues.fixed_issues' }),
+
   issueItem1: byRole('region', { name: 'Issue with no location message' }),
   issueItem2: byRole('region', { name: 'FlowIssue' }),
   issueItem3: byRole('region', { name: 'Issue on file' }),
@@ -186,7 +189,7 @@ export function renderProjectIssuesApp(
         {projectIssuesRoutes()}
       </Route>
     ),
-    { navigateTo, currentUser },
+    { navigateTo, currentUser, featureList: [Feature.BranchSupport] },
     { component: mockComponent(overrides) },
   );
 }
index a002c3ffb222789c1875b0d9d3e0cb0e27c05788..a2e303bfb5ab05fd16c04ef60037b75d5346f1dd 100644 (file)
@@ -66,6 +66,7 @@ export interface Query {
   cwe: string[];
   directories: string[];
   files: string[];
+  fixedInPullRequest: string;
   impactSeverities: SoftwareImpactSeverity[];
   impactSoftwareQualities: SoftwareQuality[];
   issues: string[];
@@ -134,6 +135,7 @@ export function parseQuery(query: RawQuery): Query {
     tags: parseAsArray(query.tags, parseAsString),
     types: parseAsArray(query.types, parseAsString),
     codeVariants: parseAsArray(query.codeVariants, parseAsString),
+    fixedInPullRequest: parseAsString(query.fixedInPullRequest),
   };
 }
 
@@ -222,6 +224,7 @@ export function serializeQuery(query: Query): RawQuery {
     cwe: serializeStringArray(query.cwe),
     directories: serializeStringArray(query.directories),
     files: serializeStringArray(query.files),
+    fixedInPullRequest: serializeString(query.fixedInPullRequest),
     issues: serializeStringArray(query.issues),
     languages: serializeStringArray(query.languages),
     owaspTop10: serializeStringArray(query.owaspTop10),
index 12efb13cf8747d4644de6cece58dd158ed24d427..434b05076ee53e262f6ee5f5f4883e1e54a45eab 100644 (file)
@@ -71,7 +71,7 @@ export default function IssueMeasuresCard(
   });
   const fixedUrl = getComponentIssuesUrl(component.key, {
     branch: pullRequest.target,
-    fixedInPR: pullRequest.key,
+    fixedInPullRequest: pullRequest.key,
   });
   const acceptedUrl = getComponentIssuesUrl(component.key, {
     ...getBranchLikeQuery(pullRequest),
index 7d7cf3460f731d47f7405b9bebb6a9c5db2544fd..0379800351b449ded3420d2c98898e94764333e9 100644 (file)
@@ -66,6 +66,7 @@ export function mockQuery(overrides: Partial<Query> = {}): Query {
     cwe: [],
     directories: [],
     files: [],
+    fixedInPullRequest: '',
     issues: [],
     languages: [],
     owaspTop10: [],
index 918f0801fa803d48ee79cd5cd533747146f50347..a58ac2ceef6b1faa6061211c1714cd902fad0eac 100644 (file)
@@ -109,6 +109,7 @@ function getContext(key: ReturnType<typeof useBranchesQueryKey>) {
 export function useBranchesQuery(component?: LightComponent, refetchInterval?: number) {
   const features = useContext(AvailableFeaturesContext);
   const key = useBranchesQueryKey(InnerState.Details, component?.key);
+
   return useQuery({
     queryKey: key,
     queryFn: async ({ queryKey: [, key, prOrBranch, name] }) => {
index cbd79aa6161365ae5e701831b8a9e91f60936946..6651b1035ac3dfeb3e57230c26902d4f4168f82b 100644 (file)
@@ -1111,6 +1111,8 @@ issues.max_new_code_period=Max New Code Period
 issues.my_issues=My Issues
 issues.no_my_issues=There are no issues assigned to you.
 issues.no_issues=No Issues. Hooray!
+issues.fixed_issues=Fixed issues
+issues.fixed_issues.description=List of issues that will be fixed by {pullRequest}
 issues.x_more_locations=+ {0} more locations
 issues.not_all_issue_show=Not all issues are included
 issues.not_all_issue_show_why=You do not have access to all projects in this portfolio