(duplicationsCount && duplicationsByLine && duplicationsByLine[line.line]) || [];
const isSinkLine = issuesForLine.some(i => i.key === this.props.issue.key);
+ const firstLineNumber = snippet && snippet.length ? snippet[0].line : 0;
const noop = () => {};
return (
displaySCM={displaySCM}
duplications={lineDuplications}
duplicationsCount={duplicationsCount}
+ firstLineNumber={firstLineNumber}
highlighted={false}
highlightedLocationMessage={optimizeLocationMessage(
this.props.highlightedLocationMessage,
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={5}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={5}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={5}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={10}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={10}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={10}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={10}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={1}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={1}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={1}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={1}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={1}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={1}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayLocationMarkers={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={1}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displaySCM={false}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={5}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displaySCM={false}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={5}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displaySCM={false}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={5}
highlighted={false}
issueLocations={Array []}
issues={Array []}
const duplicationsCount = this.props.duplications ? this.props.duplications.length : 0;
const issuesForLine = this.getIssuesForLine(line);
+ const firstLineNumber = sources && sources.length ? sources[0].line : 0;
let scrollToUncoveredLine = false;
if (
displayLocationMarkers={this.props.displayLocationMarkers}
duplications={this.getDuplicationsForLine(line)}
duplicationsCount={duplicationsCount}
+ firstLineNumber={firstLineNumber}
highlighted={line.line === this.props.highlightedLine}
highlightedLocationMessage={optimizeLocationMessage(
highlightedLocationMessage,
displayIssues={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={16}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayIssues={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={16}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayIssues={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={16}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayIssues={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={16}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayIssues={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={16}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayIssues={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={16}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayIssues={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={16}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayIssues={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={16}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayIssues={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={16}
highlighted={false}
issueLocations={Array []}
issues={Array []}
displayIssues={true}
duplications={Array []}
duplicationsCount={0}
+ firstLineNumber={16}
highlighted={false}
issueLocations={Array []}
issues={Array []}
.source-line-duplicated {
background-color: #797979 !important;
}
+
+.source-viewer-bubble-popup a {
+ font-family: var(--baseFontFamily);
+ font-size: var(--baseFontSize);
+ text-align: left;
+ user-select: text;
+ border-bottom: none;
+ transition: none;
+ color: unset;
+}
displaySCM?: boolean;
duplications: number[];
duplicationsCount: number;
+ firstLineNumber: number;
highlighted: boolean;
highlightedLocationMessage: { index: number; text: string | undefined } | undefined;
highlightedSymbols: string[] | undefined;
displaySCM = true,
duplications,
duplicationsCount,
+ firstLineNumber,
highlighted,
highlightedSymbols,
issueLocations,
return (
<tr className={className} data-line-number={line.line}>
- <LineNumber line={line} />
+ <LineNumber firstLineNumber={firstLineNumber} line={line} />
{displaySCM && <LineSCM line={line} previousLine={previousLine} />}
{displayIssues && !displayAllIssues ? (
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import Dropdown from 'sonar-ui-common/components/controls/Dropdown';
-import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
+import Toggler from 'sonar-ui-common/components/controls/Toggler';
import { translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import LineOptionsPopup from './LineOptionsPopup';
export interface LineNumberProps {
+ firstLineNumber: number;
line: T.SourceLine;
}
-export function LineNumber({ line }: LineNumberProps) {
+export function LineNumber({ firstLineNumber, line }: LineNumberProps) {
+ const [isOpen, setOpen] = React.useState<boolean>(false);
const { line: lineNumber } = line;
const hasLineNumber = !!lineNumber;
+
return hasLineNumber ? (
<td className="source-meta source-line-number" data-line-number={lineNumber}>
- <Dropdown
- overlay={<LineOptionsPopup line={line} />}
- overlayPlacement={PopupPlacement.RightTop}>
+ <Toggler
+ closeOnClickOutside={true}
+ onRequestClose={() => setOpen(false)}
+ open={isOpen}
+ overlay={<LineOptionsPopup firstLineNumber={firstLineNumber} line={line} />}>
<span
+ aria-expanded={isOpen}
+ aria-haspopup={true}
aria-label={translateWithParameters('source_viewer.line_X', lineNumber)}
- role="button">
+ onClick={() => setOpen(true)}
+ role="button"
+ tabIndex={0}>
{lineNumber}
</span>
- </Dropdown>
+ </Toggler>
</td>
) : (
<td className="source-meta source-line-number" />
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { Link } from 'react-router';
+import { ActionsDropdownItem } from 'sonar-ui-common/components/controls/ActionsDropdown';
+import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
+import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
import { translate } from 'sonar-ui-common/helpers/l10n';
+import { getPathUrlAsString } from 'sonar-ui-common/helpers/urls';
import { getCodeUrl } from '../../../helpers/urls';
import { SourceViewerContext } from '../SourceViewerContext';
-interface LineOptionsPopupProps {
+export interface LineOptionsPopupProps {
+ firstLineNumber: number;
line: T.SourceLine;
}
-export function LineOptionsPopup({ line }: LineOptionsPopupProps) {
+export function LineOptionsPopup({ firstLineNumber, line }: LineOptionsPopupProps) {
return (
<SourceViewerContext.Consumer>
- {({ branchLike, file }) => (
- <div className="source-viewer-bubble-popup nowrap">
- <Link
- className="js-get-permalink"
- rel="noopener noreferrer"
- target="_blank"
- to={getCodeUrl(file.project, branchLike, file.key, line.line)}>
- {translate('component_viewer.get_permalink')}
- </Link>
- </div>
- )}
+ {({ branchLike, file }) => {
+ const codeLocation = getCodeUrl(file.project, branchLike, file.key, line.line);
+ const codeUrl = getPathUrlAsString(codeLocation, false);
+ const isAtTop = line.line - 4 < firstLineNumber;
+ return (
+ <DropdownOverlay
+ className="big-spacer-left"
+ noPadding={true}
+ placement={isAtTop ? PopupPlacement.BottomLeft : PopupPlacement.TopLeft}>
+ <ul className="padded source-viewer-bubble-popup nowrap">
+ <ActionsDropdownItem copyValue={codeUrl}>
+ {translate('component_viewer.copy_permalink')}
+ </ActionsDropdownItem>
+ </ul>
+ </DropdownOverlay>
+ );
+ }}
</SourceViewerContext.Consumer>
);
}
displayLocationMarkers={false}
duplications={[]}
duplicationsCount={0}
+ firstLineNumber={1}
highlighted={false}
highlightedLocationMessage={undefined}
highlightedSymbols={undefined}
it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ line: { line: 0 } })).toMatchSnapshot('no line number');
+ expect(shallowRender({ line: { line: 12 } })).toMatchSnapshot('first line');
});
function shallowRender(props: Partial<LineNumberProps> = {}) {
- return shallow(<LineNumber line={{ line: 3 }} {...props} />);
+ return shallow(<LineNumber firstLineNumber={10} line={{ line: 20 }} {...props} />);
}
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockBranch } from '../../../../helpers/mocks/branch-like';
-import { LineOptionsPopup } from '../LineOptionsPopup';
+import { mockSourceLine } from '../../../../helpers/testMocks';
+import { LineOptionsPopup, LineOptionsPopupProps } from '../LineOptionsPopup';
jest.mock('../../SourceViewerContext', () => ({
SourceViewerContext: {
}));
it('should render correctly', () => {
- const line = { line: 3 };
- const wrapper = shallow(<LineOptionsPopup line={line} />).dive();
- expect(wrapper).toMatchSnapshot();
+ expect(shallowRender({ line: { line: 10 } }).dive()).toMatchSnapshot();
+ expect(shallowRender({ line: { line: 2 } }).dive()).toMatchSnapshot('first line');
});
+
+function shallowRender(props: Partial<LineOptionsPopupProps> = {}) {
+ return shallow(<LineOptionsPopup firstLineNumber={1} line={mockSourceLine()} {...props} />);
+}
data-line-number={16}
>
<Memo(LineNumber)
+ firstLineNumber={1}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
data-line-number={16}
>
<Memo(LineNumber)
+ firstLineNumber={1}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
data-line-number={16}
>
<Memo(LineNumber)
+ firstLineNumber={1}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
data-line-number={16}
>
<Memo(LineNumber)
+ firstLineNumber={1}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
data-line-number={16}
>
<Memo(LineNumber)
+ firstLineNumber={1}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
data-line-number={16}
>
<Memo(LineNumber)
+ firstLineNumber={1}
line={
Object {
"code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
exports[`should render correctly: default 1`] = `
<td
className="source-meta source-line-number"
- data-line-number={3}
+ data-line-number={20}
>
- <Dropdown
+ <Toggler
+ closeOnClickOutside={true}
+ onRequestClose={[Function]}
+ open={false}
overlay={
<Memo(LineOptionsPopup)
+ firstLineNumber={10}
line={
Object {
- "line": 3,
+ "line": 20,
}
}
/>
}
- overlayPlacement="right-top"
>
<span
- aria-label="source_viewer.line_X.3"
+ aria-expanded={false}
+ aria-haspopup={true}
+ aria-label="source_viewer.line_X.20"
+ onClick={[Function]}
role="button"
+ tabIndex={0}
>
- 3
+ 20
</span>
- </Dropdown>
+ </Toggler>
+</td>
+`;
+
+exports[`should render correctly: first line 1`] = `
+<td
+ className="source-meta source-line-number"
+ data-line-number={12}
+>
+ <Toggler
+ closeOnClickOutside={true}
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <Memo(LineOptionsPopup)
+ firstLineNumber={10}
+ line={
+ Object {
+ "line": 12,
+ }
+ }
+ />
+ }
+ >
+ <span
+ aria-expanded={false}
+ aria-haspopup={true}
+ aria-label="source_viewer.line_X.12"
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+ >
+ 12
+ </span>
+ </Toggler>
</td>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render correctly 1`] = `
-<div
- className="source-viewer-bubble-popup nowrap"
+<DropdownOverlay
+ className="big-spacer-left"
+ noPadding={true}
+ placement="top-left"
>
- <Link
- className="js-get-permalink"
- onlyActiveOnIndex={false}
- rel="noopener noreferrer"
- style={Object {}}
- target="_blank"
- to={
- Object {
- "pathname": "/code",
- "query": Object {
- "branch": "feature",
- "id": "prj",
- "line": 3,
- "selected": "foo",
- },
- }
- }
+ <ul
+ className="padded source-viewer-bubble-popup nowrap"
>
- component_viewer.get_permalink
- </Link>
-</div>
+ <ActionsDropdownItem
+ copyValue="http://localhost/code?id=prj&branch=feature&selected=foo&line=10"
+ >
+ component_viewer.copy_permalink
+ </ActionsDropdownItem>
+ </ul>
+</DropdownOverlay>
+`;
+
+exports[`should render correctly: first line 1`] = `
+<DropdownOverlay
+ className="big-spacer-left"
+ noPadding={true}
+ placement="bottom-left"
+>
+ <ul
+ className="padded source-viewer-bubble-popup nowrap"
+ >
+ <ActionsDropdownItem
+ copyValue="http://localhost/code?id=prj&branch=feature&selected=foo&line=2"
+ >
+ component_viewer.copy_permalink
+ </ActionsDropdownItem>
+ </ul>
+</DropdownOverlay>
`;
border-top: 1px solid var(--barBorderColor);
}
-.source-viewer-bubble-popup {
- font-family: var(--baseFontFamily);
- font-size: var(--baseFontSize);
- text-align: left;
- user-select: text;
-}
-
.issue-location.highlighted {
border-color: var(--issueLocationHighlighted);
background-color: var(--issueLocationHighlighted);
branchLike?: BranchLike,
selected?: string,
line?: number
-) {
+): Location {
return {
pathname: '/code',
- query: { id: project, ...getBranchLikeQuery(branchLike), selected, line }
+ query: { id: project, ...getBranchLikeQuery(branchLike), selected, line: line?.toFixed() }
};
}
component_viewer.more_actions=More Actions
component_viewer.new_window=Open in New Window
component_viewer.open_in_workspace=Pin This File
-component_viewer.get_permalink=Get Permalink
+component_viewer.copy_permalink=Copy Permalink
component_viewer.covered_lines=Covered Lines
component_viewer.show_details=Show Measures
component_viewer.file_measures=File measures