aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2022-03-10 11:35:33 +0100
committersonartech <sonartech@sonarsource.com>2022-03-11 10:30:55 +0000
commit430ffc7c4f96676636d3f03e47e90035f3c9d3e5 (patch)
tree0447a65d9ba1458860c98ff4c0156b35192a3707 /server/sonar-web/src
parent77e7247fc9a7a32d9ce6a67a7a55f95702e155a8 (diff)
downloadsonarqube-430ffc7c4f96676636d3f03e47e90035f3c9d3e5.tar.gz
sonarqube-430ffc7c4f96676636d3f03e47e90035f3c9d3e5.zip
SONAR-16069 Scroll to primary hotspot location
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotListItem-test.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotPrimaryLocationBox-test.tsx35
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap4
11 files changed, 84 insertions, 15 deletions
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
index 845dcf1e591..66c0f94b6a7 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
@@ -471,9 +471,10 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
.catch(this.handleCallFailure);
};
- handleLocationClick = (locationIndex: number) => {
+ handleLocationClick = (locationIndex?: number) => {
const { selectedHotspotLocationIndex } = this.state;
- if (locationIndex === selectedHotspotLocationIndex) {
+
+ if (locationIndex === undefined || locationIndex === selectedHotspotLocationIndex) {
this.setState({
selectedHotspotLocationIndex: undefined
});
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
index 4de97e96492..efdbc427ba0 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
@@ -56,7 +56,7 @@ export interface SecurityHotspotsAppRendererProps {
loadingMore: boolean;
onChangeFilters: (filters: Partial<HotspotFilters>) => void;
onHotspotClick: (hotspot: RawHotspot) => void;
- onLocationClick: (index: number) => void;
+ onLocationClick: (index?: number) => void;
onLoadMore: () => void;
onShowAllHotspots: () => void;
onSwitchStatusFilter: (option: HotspotStatusFilter) => void;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
index c530c789ff6..1ab4d4ade68 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
@@ -19,12 +19,14 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { mockHtmlElement } from '../../../helpers/mocks/dom';
import { getMeasures } from '../../../api/measures';
import { getSecurityHotspotList, getSecurityHotspots } from '../../../api/security-hotspots';
+import { KeyboardCodes } from '../../../helpers/keycodes';
import { mockBranch, mockPullRequest } from '../../../helpers/mocks/branch-like';
import { mockComponent } from '../../../helpers/mocks/component';
+import { mockHtmlElement } from '../../../helpers/mocks/dom';
import { mockRawHotspot, mockStandards } from '../../../helpers/mocks/security-hotspots';
+import { scrollToElement } from '../../../helpers/scrolling';
import { getStandards } from '../../../helpers/security-standard';
import {
mockCurrentUser,
@@ -43,8 +45,6 @@ import {
} from '../../../types/security-hotspots';
import { SecurityHotspotsApp } from '../SecurityHotspotsApp';
import SecurityHotspotsAppRenderer from '../SecurityHotspotsAppRenderer';
-import { scrollToElement } from '../../../helpers/scrolling';
-import { KeyboardCodes } from '../../../helpers/keycodes';
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
@@ -448,6 +448,10 @@ it('should handle secondary location click', () => {
wrapper.instance().handleLocationClick(1);
expect(wrapper.instance().state.selectedHotspotLocationIndex).toBeUndefined();
+
+ wrapper.setState({ selectedHotspotLocationIndex: 2 });
+ wrapper.instance().handleLocationClick();
+ expect(wrapper.instance().state.selectedHotspotLocationIndex).toBeUndefined();
});
it('should handle scroll properly', async () => {
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx
index 3bac3c57ebe..4ae08aa223e 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx
@@ -37,7 +37,7 @@ interface Props {
loadingMore: boolean;
onHotspotClick: (hotspot: RawHotspot) => void;
onLoadMore: () => void;
- onLocationClick: (index: number) => void;
+ onLocationClick: (index?: number) => void;
onScroll: (element: Element) => void;
securityCategories: StandardSecurityCategories;
selectedHotspot: RawHotspot;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
index 67662ca6c12..ae34502f027 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
@@ -28,7 +28,7 @@ import { getFilePath, getLocations } from '../utils';
export interface HotspotListItemProps {
hotspot: RawHotspot;
onClick: (hotspot: RawHotspot) => void;
- onLocationClick: (index: number) => void;
+ onLocationClick: (index?: number) => void;
onScroll: (element: Element) => void;
selected: boolean;
selectedHotspotLocation?: number;
@@ -44,7 +44,12 @@ export default function HotspotListItem(props: HotspotListItemProps) {
className={classNames('hotspot-item', { highlight: selected })}
href="#"
onClick={() => !selected && props.onClick(hotspot)}>
- <div className="little-spacer-left text-bold">{hotspot.message}</div>
+ <div
+ className={classNames('little-spacer-left text-bold', { 'cursor-pointer': selected })}
+ onClick={selected ? () => props.onLocationClick() : undefined}
+ role={selected ? 'button' : undefined}>
+ {hotspot.message}
+ </div>
<div className="display-flex-center">
<QualifierIcon qualifier={ComponentQualifier.File} />
<div
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.tsx
index 120d9154cd9..6ae043aa9ca 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotPrimaryLocationBox.tsx
@@ -32,10 +32,21 @@ export interface HotspotPrimaryLocationBoxProps {
onCommentClick: () => void;
currentUser: CurrentUser;
scroll: (element: HTMLElement, offset?: number) => void;
+ secondaryLocationSelected: boolean;
}
export function HotspotPrimaryLocationBox(props: HotspotPrimaryLocationBoxProps) {
- const { hotspot, currentUser } = props;
+ const { hotspot, currentUser, secondaryLocationSelected } = props;
+
+ const locationRef = React.useRef<HTMLDivElement>(null);
+
+ React.useEffect(() => {
+ const { current } = locationRef;
+ if (current && !secondaryLocationSelected) {
+ props.scroll(current);
+ }
+ });
+
return (
<div
className={classNames(
@@ -43,7 +54,7 @@ export function HotspotPrimaryLocationBox(props: HotspotPrimaryLocationBoxProps)
'display-flex-space-between display-flex-center padded-top padded-bottom big-padded-left big-padded-right',
`hotspot-risk-exposure-${hotspot.rule.vulnerabilityProbability}`
)}
- ref={element => element && props.scroll(element)}>
+ ref={locationRef}>
<div className="text-bold">{hotspot.message}</div>
{isLoggedIn(currentUser) && (
<ButtonLink
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx
index ca7c81cd7a3..8f012882eb5 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx
@@ -42,7 +42,7 @@ export interface HotspotSimpleListProps {
hotspotsTotal: number;
loadingMore: boolean;
onHotspotClick: (hotspot: RawHotspot) => void;
- onLocationClick: (index: number) => void;
+ onLocationClick: (index?: number) => void;
onScroll: (element: Element) => void;
onLoadMore: () => void;
selectedHotspot: RawHotspot;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx
index bd3d2dd1ef7..49d6d804a73 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSnippetContainerRenderer.tsx
@@ -93,6 +93,8 @@ export default function HotspotSnippetContainerRenderer(
const scrollableRef = React.useRef<HTMLDivElement>(null);
+ const secondaryLocationSelected = selectedHotspotLocation !== undefined;
+
/* Use memo is important to not rerender and trigger additional scrolls */
const hotspotPrimaryLocationBox = React.useMemo(
() => (
@@ -100,9 +102,10 @@ export default function HotspotSnippetContainerRenderer(
hotspot={hotspot}
onCommentClick={props.onCommentButtonClick}
scroll={getScrollHandler(scrollableRef)}
+ secondaryLocationSelected={secondaryLocationSelected}
/>
),
- [hotspot, props.onCommentButtonClick]
+ [hotspot, secondaryLocationSelected, props.onCommentButtonClick]
);
const renderHotspotBoxInLine = (lineNumber: number) =>
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotListItem-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotListItem-test.tsx
index 09187d1f218..99cfe471fbd 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotListItem-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotListItem-test.tsx
@@ -37,6 +37,16 @@ it('should handle click', () => {
expect(onClick).toBeCalledWith(hotspot);
});
+it('should handle click on the title', () => {
+ const hotspot = mockRawHotspot({ key: 'hotspotKey' });
+ const onLocationClick = jest.fn();
+ const wrapper = shallowRender({ hotspot, onLocationClick, selected: true });
+
+ wrapper.find('div.cursor-pointer').simulate('click');
+
+ expect(onLocationClick).toBeCalledWith();
+});
+
function shallowRender(props: Partial<HotspotListItemProps> = {}) {
return shallow(
<HotspotListItem
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotPrimaryLocationBox-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotPrimaryLocationBox-test.tsx
index 1676c79015c..c39742d07f2 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotPrimaryLocationBox-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotPrimaryLocationBox-test.tsx
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
-import * as React from 'react';
+import React from 'react';
import { ButtonLink } from '../../../../components/controls/buttons';
import { mockHotspot, mockHotspotRule } from '../../../../helpers/mocks/security-hotspots';
import { mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks';
@@ -53,6 +53,38 @@ it('should handle click', () => {
expect(onCommentClick).toBeCalled();
});
+it('should scroll on load if no secondary locations selected', () => {
+ const node = document.createElement('div');
+ jest.spyOn(React, 'useRef').mockImplementationOnce(() => ({ current: node }));
+ jest.spyOn(React, 'useEffect').mockImplementationOnce(f => f());
+
+ const scroll = jest.fn();
+ shallowRender({ scroll });
+
+ expect(scroll).toBeCalled();
+});
+
+it('should not scroll on load if a secondary location is selected', () => {
+ const node = document.createElement('div');
+ jest.spyOn(React, 'useRef').mockImplementationOnce(() => ({ current: node }));
+ jest.spyOn(React, 'useEffect').mockImplementationOnce(f => f());
+
+ const scroll = jest.fn();
+ shallowRender({ scroll, secondaryLocationSelected: true });
+
+ expect(scroll).not.toBeCalled();
+});
+
+it('should not scroll on load if node is not defined', () => {
+ jest.spyOn(React, 'useRef').mockImplementationOnce(() => ({ current: undefined }));
+ jest.spyOn(React, 'useEffect').mockImplementationOnce(f => f());
+
+ const scroll = jest.fn();
+ shallowRender({ scroll });
+
+ expect(scroll).not.toBeCalled();
+});
+
function shallowRender(props: Partial<HotspotPrimaryLocationBoxProps> = {}) {
return shallow(
<HotspotPrimaryLocationBox
@@ -60,6 +92,7 @@ function shallowRender(props: Partial<HotspotPrimaryLocationBoxProps> = {}) {
hotspot={mockHotspot()}
onCommentClick={jest.fn()}
scroll={jest.fn()}
+ secondaryLocationSelected={false}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
index 41a9dbae849..12b6518b12a 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
@@ -37,7 +37,9 @@ exports[`should render correctly 2`] = `
onClick={[Function]}
>
<div
- className="little-spacer-left text-bold"
+ className="little-spacer-left text-bold cursor-pointer"
+ onClick={[Function]}
+ role="button"
>
'3' is a magic number.
</div>