]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-17324 Display file name above secondary locations (multi-file issues) (#7284)
authorDavid Cho-Lerat <117642976+david-cho-lerat-sonarsource@users.noreply.github.com>
Wed, 4 Jan 2023 10:41:31 +0000 (11:41 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 4 Jan 2023 20:02:52 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
server/sonar-web/src/main/js/components/locations/FlowsList.tsx
server/sonar-web/src/main/js/components/locations/__tests__/FlowsList-test.tsx [new file with mode: 0644]

index e599e5a8daa71cc7dc3b7932d095ea33d4b63489..a04129e4853f5375a9cac16ce92344912f6fe6c4 100644 (file)
@@ -68,7 +68,7 @@ export default function HotspotListItem(props: HotspotListItemProps) {
       {selected && (
         <LocationsList
           locations={locations}
-          showCrossFile={false} // To removed once we support multi file location
+          showCrossFile={false} // To be removed once we support multi file location
           componentKey={hotspot.component}
           onLocationSelect={props.onLocationClick}
           selectedLocationIndex={selectedHotspotLocation}
index 5ad5833932fdc6c6cc94c4914b9e29c6ddb9b9f9..bb5d3be0bf7b1218289eb4c92f8a78c0fa662524 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import 'FlowsList.css';
+import { uniq } from 'lodash';
 import * as React from 'react';
 import ConciseIssueLocationBadge from '../../apps/issues/conciseIssuesList/ConciseIssueLocationBadge';
 import { translate } from '../../helpers/l10n';
 import { Flow, FlowType } from '../../types/types';
 import BoxedGroupAccordion from '../controls/BoxedGroupAccordion';
+import CrossFileLocationNavigator from './CrossFileLocationNavigator';
 import SingleFileLocationNavigator from './SingleFileLocationNavigator';
 
 const FLOW_ORDER_MAP = {
@@ -46,6 +48,39 @@ export default function FlowsList(props: Props) {
     <div className="issue-flows little-padded-top" role="list">
       {flows.map((flow, index) => {
         const open = selectedFlowIndex === index;
+
+        const locationComponents = flow.locations.map((location) => location.component);
+        const isCrossFile = uniq(locationComponents).length > 1;
+
+        let fileLocationNavigator;
+
+        if (isCrossFile) {
+          fileLocationNavigator = (
+            <CrossFileLocationNavigator
+              locations={flow.locations}
+              onLocationSelect={props.onLocationSelect}
+              selectedLocationIndex={selectedLocationIndex}
+            />
+          );
+        } else {
+          fileLocationNavigator = (
+            <ul>
+              {flow.locations.map((location, locIndex) => (
+                // eslint-disable-next-line react/no-array-index-key
+                <li className="display-flex-column" key={locIndex}>
+                  <SingleFileLocationNavigator
+                    index={locIndex}
+                    message={location.msg}
+                    messageFormattings={location.msgFormattings}
+                    onClick={props.onLocationSelect}
+                    selected={locIndex === selectedLocationIndex}
+                  />
+                </li>
+              ))}
+            </ul>
+          );
+        }
+
         return (
           <BoxedGroupAccordion
             className="spacer-top"
@@ -67,20 +102,7 @@ export default function FlowsList(props: Props) {
               />
             )}
           >
-            <ul>
-              {flow.locations.map((location, locIndex) => (
-                // eslint-disable-next-line react/no-array-index-key
-                <li className="display-flex-column" key={locIndex}>
-                  <SingleFileLocationNavigator
-                    index={locIndex}
-                    message={location.msg}
-                    messageFormattings={location.msgFormattings}
-                    onClick={props.onLocationSelect}
-                    selected={locIndex === selectedLocationIndex}
-                  />
-                </li>
-              ))}
-            </ul>
+            {fileLocationNavigator}
           </BoxedGroupAccordion>
         );
       })}
diff --git a/server/sonar-web/src/main/js/components/locations/__tests__/FlowsList-test.tsx b/server/sonar-web/src/main/js/components/locations/__tests__/FlowsList-test.tsx
new file mode 100644 (file)
index 0000000..7b85d51
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * 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 { render, screen } from '@testing-library/react';
+import React from 'react';
+import { FlowLocation, FlowType } from '../../../types/types';
+import FlowsList, { Props } from '../FlowsList';
+
+const componentName1 = 'file1';
+const componentName2 = 'file2';
+
+const component1 = `project:dir1/dir2/${componentName1}`;
+const component2 = `project:dir1/dir2/${componentName2}`;
+
+const mockLocation: FlowLocation = {
+  msg: 'location message',
+  component: component1,
+  textRange: { startLine: 1, startOffset: 2, endLine: 3, endOffset: 4 },
+};
+
+it('should display file names for multi-file issues', () => {
+  renderComponent({
+    flows: [
+      {
+        locations: [
+          { ...mockLocation, component: component1, componentName: componentName1 },
+          { ...mockLocation, component: component1, componentName: componentName1 },
+          { ...mockLocation, component: component2, componentName: componentName2 },
+        ],
+        type: FlowType.EXECUTION,
+      },
+      { locations: [], type: FlowType.DATA },
+    ],
+    selectedFlowIndex: 1,
+  });
+
+  expect(screen.getByText(componentName1)).toBeInTheDocument();
+  expect(screen.getByText(componentName2)).toBeInTheDocument();
+});
+
+it('should not display file names for single-file issues', () => {
+  renderComponent({
+    flows: [
+      {
+        locations: [
+          { ...mockLocation, component: component1, componentName: componentName1 },
+          { ...mockLocation, component: component1, componentName: componentName1 },
+        ],
+        type: FlowType.EXECUTION,
+      },
+      { locations: [], type: FlowType.DATA },
+    ],
+    selectedFlowIndex: 1,
+  });
+
+  expect(screen.queryByText(componentName1)).not.toBeInTheDocument();
+});
+
+const renderComponent = (props?: Partial<Props>) =>
+  render(<FlowsList flows={[]} onFlowSelect={jest.fn()} onLocationSelect={jest.fn()} {...props} />);