diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2022-03-10 11:35:33 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-03-11 10:30:55 +0000 |
commit | 430ffc7c4f96676636d3f03e47e90035f3c9d3e5 (patch) | |
tree | 0447a65d9ba1458860c98ff4c0156b35192a3707 /server/sonar-web/src | |
parent | 77e7247fc9a7a32d9ce6a67a7a55f95702e155a8 (diff) | |
download | sonarqube-430ffc7c4f96676636d3f03e47e90035f3c9d3e5.tar.gz sonarqube-430ffc7c4f96676636d3f03e47e90035f3c9d3e5.zip |
SONAR-16069 Scroll to primary hotspot location
Diffstat (limited to 'server/sonar-web/src')
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> |