--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 React from 'react';
+
+export interface IssueSourceViewerScrollContextInterface {
+ registerPrimaryLocationRef: React.Ref<HTMLElement>;
+ registerSelectedSecondaryLocationRef: React.Ref<HTMLElement>;
+}
+
+export const IssueSourceViewerScrollContext = React.createContext<
+ IssueSourceViewerScrollContextInterface | undefined
+>(undefined);
parseQuery,
Query,
saveMyIssues,
- scrollToIssue,
serializeQuery,
shouldOpenSonarSourceSecurityFacet,
shouldOpenStandardsChildFacet,
) {
this.fetchFirstIssues();
this.setState({ checkAll: false });
- } else if (
- !this.state.openIssue &&
- (prevState.selected !== this.state.selected || prevState.openIssue)
- ) {
- // if user simply selected another issue
- // or if user went from the source code back to the list of issues
- this.scrollToSelectedIssue();
} else if (openIssue && openIssue.key !== this.state.selected) {
this.setState({
locationsNavigator: false,
};
if (this.state.openIssue) {
if (path.query.open && path.query.open === this.state.openIssue.key) {
- this.setState(
- {
- locationsNavigator: false,
- selectedFlowIndex: undefined,
- selectedLocationIndex: undefined
- },
- this.scrollToSelectedIssue
- );
+ this.setState({
+ locationsNavigator: false,
+ selectedFlowIndex: undefined,
+ selectedLocationIndex: undefined
+ });
} else {
this.props.router.replace(path);
}
}
};
- scrollToSelectedIssue = (smooth = true) => {
- const { selected } = this.state;
- if (selected) {
- scrollToIssue(selected, smooth);
- }
- };
-
createdAfterIncludesTime = () => Boolean(this.props.location.query.createdAfter?.includes('T'));
fetchIssuesHelper = (query: RawQuery) => {
import { Issue } from '../../../types/types';
import CrossComponentSourceViewer from '../crossComponentSourceViewer/CrossComponentSourceViewer';
import { getLocations, getSelectedLocation } from '../utils';
+import { IssueSourceViewerScrollContext } from './IssueSourceViewerScrollContext';
export interface IssuesSourceViewerProps {
branchLike: BranchLike | undefined;
selectedLocationIndex: number | undefined;
}
-export default function IssuesSourceViewer(props: IssuesSourceViewerProps) {
- const {
- openIssue,
- selectedFlowIndex,
- selectedLocationIndex,
- locationsNavigator,
- branchLike,
- issues
- } = props;
+export default class IssuesSourceViewer extends React.PureComponent<IssuesSourceViewerProps> {
+ primaryLocationRef?: HTMLElement;
+ selectedSecondaryLocationRef?: HTMLElement;
- const locations = getLocations(openIssue, selectedFlowIndex).map((loc, index) => {
- loc.index = index;
- return loc;
- });
- const selectedLocation = getSelectedLocation(openIssue, selectedFlowIndex, selectedLocationIndex);
+ componentDidUpdate() {
+ if (this.props.selectedLocationIndex === -1) {
+ this.refreshScroll();
+ }
+ }
- const highlightedLocationMessage =
- locationsNavigator && selectedLocationIndex !== undefined
- ? selectedLocation && { index: selectedLocationIndex, text: selectedLocation.msg }
- : undefined;
- return (
- <div>
- <CrossComponentSourceViewer
- branchLike={branchLike}
- highlightedLocationMessage={highlightedLocationMessage}
- issue={openIssue}
- issues={issues}
- locations={locations}
- onIssueSelect={props.onIssueSelect}
- onLocationSelect={props.onLocationSelect}
- selectedFlowIndex={selectedFlowIndex}
- selectedLocationIndex={selectedLocationIndex}
- />
- </div>
- );
+ registerPrimaryLocationRef = (ref: HTMLElement) => {
+ this.primaryLocationRef = ref;
+
+ if (ref) {
+ this.refreshScroll();
+ }
+ };
+
+ registerSelectedSecondaryLocationRef = (ref: HTMLElement) => {
+ this.selectedSecondaryLocationRef = ref;
+
+ if (ref) {
+ this.refreshScroll();
+ }
+ };
+
+ refreshScroll() {
+ const { selectedLocationIndex } = this.props;
+
+ if (
+ selectedLocationIndex !== undefined &&
+ selectedLocationIndex !== -1 &&
+ this.selectedSecondaryLocationRef
+ ) {
+ this.selectedSecondaryLocationRef.scrollIntoView({
+ behavior: 'smooth',
+ block: 'center',
+ inline: 'nearest'
+ });
+ } else if (this.primaryLocationRef) {
+ this.primaryLocationRef.scrollIntoView({
+ block: 'center',
+ inline: 'nearest'
+ });
+ }
+ }
+
+ render() {
+ const {
+ openIssue,
+ selectedFlowIndex,
+ selectedLocationIndex,
+ locationsNavigator,
+ branchLike,
+ issues
+ } = this.props;
+
+ const locations = getLocations(openIssue, selectedFlowIndex).map((loc, index) => {
+ loc.index = index;
+ return loc;
+ });
+
+ const selectedLocation = getSelectedLocation(
+ openIssue,
+ selectedFlowIndex,
+ selectedLocationIndex
+ );
+
+ const highlightedLocationMessage =
+ locationsNavigator && selectedLocationIndex !== undefined
+ ? selectedLocation && { index: selectedLocationIndex, text: selectedLocation.msg }
+ : undefined;
+
+ return (
+ <IssueSourceViewerScrollContext.Provider
+ value={{
+ registerPrimaryLocationRef: this.registerPrimaryLocationRef,
+ registerSelectedSecondaryLocationRef: this.registerSelectedSecondaryLocationRef
+ }}>
+ <CrossComponentSourceViewer
+ branchLike={branchLike}
+ highlightedLocationMessage={highlightedLocationMessage}
+ issue={openIssue}
+ issues={issues}
+ locations={locations}
+ onIssueSelect={this.props.onIssueSelect}
+ onLocationSelect={this.props.onLocationSelect}
+ selectedFlowIndex={selectedFlowIndex}
+ />
+ </IssueSourceViewerScrollContext.Provider>
+ );
+ }
}
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render CrossComponentSourceViewer correctly 1`] = `
-<div>
+<ContextProvider
+ value={
+ Object {
+ "registerPrimaryLocationRef": [Function],
+ "registerSelectedSecondaryLocationRef": [Function],
+ }
+ }
+>
<CrossComponentSourceViewer
branchLike={
Object {
onIssueSelect={[MockFunction]}
onLocationSelect={[MockFunction]}
/>
-</div>
+</ContextProvider>
`;
exports[`should render SourceViewer correctly: all secondary locations on same line 1`] = `
-<div>
+<ContextProvider
+ value={
+ Object {
+ "registerPrimaryLocationRef": [Function],
+ "registerSelectedSecondaryLocationRef": [Function],
+ }
+ }
+>
<CrossComponentSourceViewer
branchLike={
Object {
onIssueSelect={[MockFunction]}
onLocationSelect={[MockFunction]}
/>
-</div>
+</ContextProvider>
`;
exports[`should render SourceViewer correctly: default 1`] = `
-<div>
+<ContextProvider
+ value={
+ Object {
+ "registerPrimaryLocationRef": [Function],
+ "registerSelectedSecondaryLocationRef": [Function],
+ }
+ }
+>
<CrossComponentSourceViewer
branchLike={
Object {
onIssueSelect={[MockFunction]}
onLocationSelect={[MockFunction]}
/>
-</div>
+</ContextProvider>
`;
exports[`should render SourceViewer correctly: single secondary location 1`] = `
-<div>
+<ContextProvider
+ value={
+ Object {
+ "registerPrimaryLocationRef": [Function],
+ "registerSelectedSecondaryLocationRef": [Function],
+ }
+ }
+>
<CrossComponentSourceViewer
branchLike={
Object {
onIssueSelect={[MockFunction]}
onLocationSelect={[MockFunction]}
/>
-</div>
+</ContextProvider>
`;
SourceLine,
SourceViewerFile
} from '../../../types/types';
+import { IssueSourceViewerScrollContext } from '../components/IssueSourceViewerScrollContext';
import IssueSourceViewerHeader from './IssueSourceViewerHeader';
import SnippetViewer from './SnippetViewer';
import {
line: number
) => React.ReactNode;
snippetGroup: SnippetGroup;
- selectedLocationIndex: number | undefined;
}
interface State {
};
renderIssuesList = (line: SourceLine) => {
- const {
- isLastOccurenceOfPrimaryComponent,
- issue,
- issuesByLine,
- snippetGroup,
- selectedLocationIndex
- } = this.props;
+ const { isLastOccurenceOfPrimaryComponent, issue, issuesByLine, snippetGroup } = this.props;
const locations =
issue.component === snippetGroup.component.key && issue.textRange !== undefined
? locationsByLine([issue])
return (
issuesForLine.length > 0 && (
<div>
- {issuesForLine.map(issueToDisplay => (
- <IssueMessageBox
- selected={!!(issueToDisplay.key === issue.key && issueLocations.length > 0)}
- key={issueToDisplay.key}
- issue={issueToDisplay}
- onClick={this.props.onIssueSelect}
- selectedLocationIndex={selectedLocationIndex}
- />
- ))}
+ {issuesForLine.map(issueToDisplay => {
+ const isSelectedIssue = issueToDisplay.key === issue.key;
+ return (
+ <IssueSourceViewerScrollContext.Consumer key={issueToDisplay.key}>
+ {ctx => (
+ <IssueMessageBox
+ selected={!!(isSelectedIssue && issueLocations.length > 0)}
+ issue={issueToDisplay}
+ onClick={this.props.onIssueSelect}
+ ref={isSelectedIssue ? ctx?.registerPrimaryLocationRef : undefined}
+ />
+ )}
+ </IssueSourceViewerScrollContext.Consumer>
+ );
+ })}
</div>
)
);
isLastOccurenceOfPrimaryComponent,
issue,
lastSnippetGroup,
- snippetGroup,
- selectedLocationIndex
+ snippetGroup
} = this.props;
const { additionalLines, loading, snippets } = this.state;
const locations =
/>
{issue.component === snippetGroup.component.key && issue.textRange === undefined && (
- <IssueMessageBox
- selected={true}
- issue={issue}
- onClick={this.props.onIssueSelect}
- selectedLocationIndex={selectedLocationIndex}
- />
+ <IssueSourceViewerScrollContext.Consumer>
+ {ctx => (
+ <IssueMessageBox
+ selected={true}
+ issue={issue}
+ onClick={this.props.onIssueSelect}
+ ref={ctx?.registerPrimaryLocationRef}
+ />
+ )}
+ </IssueSourceViewerScrollContext.Consumer>
)}
{snippetLines.map((snippet, index) => (
<SnippetViewer
onIssueSelect: (issueKey: string) => void;
onLocationSelect: (index: number) => void;
selectedFlowIndex: number | undefined;
- selectedLocationIndex: number | undefined;
}
interface State {
render() {
const { loading, notAccessible } = this.state;
- const { selectedLocationIndex } = this.props;
if (loading) {
return (
onLocationSelect={this.props.onLocationSelect}
renderDuplicationPopup={this.renderDuplicationPopup}
snippetGroup={snippetGroup}
- selectedLocationIndex={selectedLocationIndex}
/>
</SourceViewerContext.Provider>
);
}
});
- expect(wrapper.find(IssueMessageBox).exists()).toBe(true);
+ expect(
+ wrapper
+ .find('ContextConsumer')
+ .dive()
+ .find(IssueMessageBox)
+ .exists()
+ ).toBe(true);
});
it('should expand block', async () => {
<ComponentSourceSnippetGroupViewer
branchLike={mockMainBranch()}
highlightedLocationMessage={{ index: 0, text: '' }}
- selectedLocationIndex={0}
isLastOccurenceOfPrimaryComponent={true}
issue={mockIssue()}
issuesByLine={{}}
<CrossComponentSourceViewer
branchLike={undefined}
highlightedLocationMessage={undefined}
- selectedLocationIndex={undefined}
issue={mockIssue(true, {
key: '1',
component: 'project:main.js',
return getLocations(issue, selectedFlowIndex).every(location => !location.msg);
}
+// TODO: drop as it's useless now
export function scrollToIssue(issue: string, smooth = true) {
const element = document.querySelector(`[data-issue="${issue}"]`);
if (element) {
*/
import classNames from 'classnames';
import * as React from 'react';
+import { IssueSourceViewerScrollContext } from '../../../apps/issues/components/IssueSourceViewerScrollContext';
import { LinearIssueLocation, SourceLine } from '../../../types/types';
import LocationIndex from '../../common/LocationIndex';
import Tooltip from '../../controls/Tooltip';
}
export default class LineCode extends React.PureComponent<React.PropsWithChildren<Props>> {
- activeMarkerNode?: HTMLElement | null;
symbols?: NodeListOf<HTMLElement>;
- componentDidMount() {
- if (this.activeMarkerNode) {
- this.activeMarkerNode.scrollIntoView({
- behavior: 'smooth',
- block: 'center',
- inline: 'center'
- });
- }
- }
-
- componentDidUpdate(prevProps: Props) {
- if (
- this.props.highlightedLocationMessage &&
- (!prevProps.highlightedLocationMessage ||
- prevProps.highlightedLocationMessage.index !==
- this.props.highlightedLocationMessage.index) &&
- this.activeMarkerNode
- ) {
- this.activeMarkerNode.scrollIntoView({
- behavior: 'smooth',
- block: 'center',
- inline: 'center'
- });
- }
- }
-
nodeNodeRef = (el: HTMLElement | null) => {
if (el) {
this.attachEvents(el);
renderMarker(index: number, message: string | undefined, selected: boolean, leading: boolean) {
const { onLocationSelect } = this.props;
const onClick = onLocationSelect ? () => onLocationSelect(index) : undefined;
- const ref = selected ? (node: HTMLElement | null) => (this.activeMarkerNode = node) : undefined;
return (
<Tooltip key={`marker-${index}`} overlay={message} placement="top">
onClick={onClick}
selected={selected}
aria-label={message ? `${index + 1}-${message}` : index + 1}>
- <span ref={ref}>{index + 1}</span>
+ <IssueSourceViewerScrollContext.Consumer>
+ {ctx => (
+ <span ref={selected ? ctx?.registerSelectedSecondaryLocationRef : undefined}>
+ {index + 1}
+ </span>
+ )}
+ </IssueSourceViewerScrollContext.Consumer>
</LocationIndex>
</Tooltip>
);
onClick={[Function]}
selected={false}
>
- <span>
- 2
- </span>
+ <ContextConsumer>
+ <Component />
+ </ContextConsumer>
</LocationIndex>
</Tooltip>
<span
selected: boolean;
issue: Issue;
onClick: (issueKey: string) => void;
- selectedLocationIndex?: number;
}
-export default class IssueMessageBox extends React.Component<IssueMessageBoxProps> {
- messageBoxRef: React.RefObject<HTMLDivElement> = React.createRef();
+export function IssueMessageBox(props: IssueMessageBoxProps, ref: React.ForwardedRef<any>) {
+ const { issue, selected } = props;
- componentDidMount() {
- if (this.props.selected && this.messageBoxRef.current) {
- this.messageBoxRef.current.scrollIntoView({
- block: 'center'
- });
- }
- }
-
- componentDidUpdate(prevProps: IssueMessageBoxProps) {
- if (
- this.messageBoxRef.current &&
- ((prevProps.selected !== this.props.selected && this.props.selected) ||
- (prevProps.selectedLocationIndex !== this.props.selectedLocationIndex &&
- this.props.selectedLocationIndex === -1))
- ) {
- this.messageBoxRef.current.scrollIntoView({
- block: 'center'
- });
- }
- }
-
- render() {
- const { issue, selected } = this.props;
- return (
- <div
- className={classNames(
- 'issue-message-box display-flex-row display-flex-center padded-right',
- {
- 'selected big-padded-top big-padded-bottom': selected,
- 'secondary-issue padded-top padded-bottom': !selected
- }
- )}
- key={issue.key}
- onClick={() => this.props.onClick(issue.key)}
- role="region"
- ref={this.messageBoxRef}
- aria-label={issue.message}>
- <IssueTypeIcon
- className="big-spacer-right spacer-left"
- fill={colors.baseFontColor}
- query={issue.type}
- />
- {issue.message}
- </div>
- );
- }
+ return (
+ <div
+ className={classNames('issue-message-box display-flex-row display-flex-center padded-right', {
+ 'selected big-padded-top big-padded-bottom': selected,
+ 'secondary-issue padded-top padded-bottom': !selected
+ })}
+ key={issue.key}
+ onClick={() => props.onClick(issue.key)}
+ role="region"
+ ref={ref}
+ aria-label={issue.message}>
+ <IssueTypeIcon
+ className="big-spacer-right spacer-left"
+ fill={colors.baseFontColor}
+ query={issue.type}
+ />
+ {issue.message}
+ </div>
+ );
}
+
+export default React.forwardRef(IssueMessageBox);
this.detachScrollEvent();
}
- computeState = (prevState: State, resetSelectedTab: boolean = false) => {
+ computeState = (prevState: State, resetSelectedTab = false) => {
const {
ruleDetails,
currentUser: { isLoggedIn, dismissedNotices }