From 299cebedac5ef4a6a17dd18782c2b1a2a79f08d5 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Fri, 2 Mar 2018 16:24:37 +0100 Subject: [PATCH] rewrite remaining popups in react (#3109) * extract baseFontFamily * rewrite favorites store in ts * add new types and change existing ones * rewrite SourceViewer helpers in ts * rewrite SourceViewer in ts and its popups in react * drop popups * fix iterating over nodelist * fix quality flaws --- server/sonar-web/config/webpack.config.js | 2 +- .../src/main/js/app/styles/init/type.css | 2 +- server/sonar-web/src/main/js/app/theme.js | 1 + server/sonar-web/src/main/js/app/types.ts | 106 +++ .../main/js/apps/component/components/App.tsx | 2 +- .../{SourceViewer.js => SourceViewer.tsx} | 17 +- ...urceViewerBase.js => SourceViewerBase.tsx} | 719 ++++++++++-------- ...urceViewerCode.js => SourceViewerCode.tsx} | 193 ++--- .../SourceViewer/components/CoveragePopup.tsx | 153 ++++ .../components/DuplicationPopup.tsx | 158 ++++ .../components/{Line.js => Line.tsx} | 143 ++-- .../components/{LineCode.js => LineCode.tsx} | 140 ++-- .../SourceViewer/components/LineCoverage.js | 67 -- .../SourceViewer/components/LineCoverage.tsx | 102 +++ .../components/LineDuplicationBlock.js | 71 -- .../components/LineDuplicationBlock.tsx | 90 +++ ...neDuplications.js => LineDuplications.tsx} | 29 +- ...esIndicator.js => LineIssuesIndicator.tsx} | 32 +- .../SourceViewer/components/LineIssuesList.js | 64 -- .../components/LineIssuesList.tsx | 57 ++ .../SourceViewer/components/LineNumber.js | 53 -- .../SourceViewer/components/LineNumber.tsx | 69 ++ .../components/LineOptionsPopup.tsx | 48 ++ .../SourceViewer/components/LineSCM.js | 64 -- .../SourceViewer/components/LineSCM.tsx | 80 ++ .../SourceViewer/components/SCMPopup.tsx | 42 + .../{LineCode-test.js => LineCode-test.tsx} | 37 +- ...Coverage-test.js => LineCoverage-test.tsx} | 61 +- ...-test.js => LineDuplicationBlock-test.tsx} | 25 +- ...ions-test.js => LineDuplications-test.tsx} | 2 +- ...r-test.js => LineIssuesIndicator-test.tsx} | 32 +- .../__tests__/LineIssuesList-test.tsx | 62 ++ ...LineNumber-test.js => LineNumber-test.tsx} | 25 +- ...List-test.js => LineOptionsPopup-test.tsx} | 19 +- .../{LineSCM-test.js => LineSCM-test.tsx} | 34 +- .../__tests__/SCMPopup-test.tsx} | 31 +- .../__snapshots__/LineCode-test.js.snap | 55 -- .../__snapshots__/LineCode-test.tsx.snap | 92 +++ .../__snapshots__/LineCoverage-test.js.snap | 47 -- .../__snapshots__/LineCoverage-test.tsx.snap | 65 ++ .../LineDuplicationBlock-test.js.snap | 35 - .../LineDuplicationBlock-test.tsx.snap | 38 + ...js.snap => LineDuplications-test.tsx.snap} | 0 ...snap => LineIssuesIndicator-test.tsx.snap} | 4 +- .../__snapshots__/LineIssuesList-test.js.snap | 36 - .../LineIssuesList-test.tsx.snap | 72 ++ .../__snapshots__/LineNumber-test.js.snap | 17 - .../__snapshots__/LineNumber-test.tsx.snap | 34 + .../LineOptionsPopup-test.tsx.snap | 29 + .../__snapshots__/LineSCM-test.js.snap | 52 -- .../__snapshots__/LineSCM-test.tsx.snap | 90 +++ .../__snapshots__/SCMPopup-test.tsx.snap | 20 + .../{highlight-test.js => highlight-test.ts} | 1 - .../{indexing-test.js => indexing-test.ts} | 0 ...overageStatus.js => getCoverageStatus.tsx} | 7 +- .../helpers/{highlight.js => highlight.ts} | 53 +- .../helpers/{indexing.js => indexing.ts} | 56 +- .../SourceViewer/helpers/issueLocations.js | 69 -- .../issueLocations.tsx} | 37 +- .../helpers/{loadIssues.js => loadIssues.tsx} | 38 +- .../SourceViewer/popups/coverage-popup.js | 60 -- .../SourceViewer/popups/duplication-popup.js | 61 -- .../source-viewer-coverage-popup.hbs | 33 - .../source-viewer-duplication-popup.hbs | 45 -- .../source-viewer-line-options-popup.hbs | 7 - .../templates/source-viewer-scm-popup.hbs | 15 - .../js/components/SourceViewer/styles.css | 13 +- .../components/common/BubblePopupHelper.tsx | 8 +- .../js/components/common/LocationIndex.css | 2 +- .../js/components/common/LocationMessage.css | 2 +- .../src/main/js/components/common/popup.js | 73 -- .../Issue.d.ts} | 31 +- .../sonar-web/src/main/js/helpers/issues.ts | 5 +- .../js/store/favorites/{duck.js => duck.ts} | 87 +-- 74 files changed, 2391 insertions(+), 1830 deletions(-) rename server/sonar-web/src/main/js/components/SourceViewer/{SourceViewer.js => SourceViewer.tsx} (80%) rename server/sonar-web/src/main/js/components/SourceViewer/{SourceViewerBase.js => SourceViewerBase.tsx} (50%) rename server/sonar-web/src/main/js/components/SourceViewer/{SourceViewerCode.js => SourceViewerCode.tsx} (60%) create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx rename server/sonar-web/src/main/js/components/SourceViewer/components/{Line.js => Line.tsx} (56%) rename server/sonar-web/src/main/js/components/SourceViewer/components/{LineCode.js => LineCode.tsx} (68%) delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx rename server/sonar-web/src/main/js/components/SourceViewer/components/{LineDuplications.js => LineDuplications.tsx} (82%) rename server/sonar-web/src/main/js/components/SourceViewer/components/{LineIssuesIndicator.js => LineIssuesIndicator.tsx} (77%) delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx rename server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/{LineCode-test.js => LineCode-test.tsx} (64%) rename server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/{LineCoverage-test.js => LineCoverage-test.tsx} (50%) rename server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/{LineDuplicationBlock-test.js => LineDuplicationBlock-test.tsx} (73%) rename server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/{LineDuplications-test.js => LineDuplications-test.tsx} (97%) rename server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/{LineIssuesIndicator-test.js => LineIssuesIndicator-test.tsx} (72%) create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.tsx rename server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/{LineNumber-test.js => LineNumber-test.tsx} (75%) rename server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/{LineIssuesList-test.js => LineOptionsPopup-test.tsx} (69%) rename server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/{LineSCM-test.js => LineSCM-test.tsx} (66%) rename server/sonar-web/src/main/js/components/SourceViewer/{types.js => components/__tests__/SCMPopup-test.tsx} (68%) delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap rename server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/{LineDuplications-test.js.snap => LineDuplications-test.tsx.snap} (100%) rename server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/{LineIssuesIndicator-test.js.snap => LineIssuesIndicator-test.tsx.snap} (96%) delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.js.snap create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.js.snap create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap rename server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/{highlight-test.js => highlight-test.ts} (99%) rename server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/{indexing-test.js => indexing-test.ts} (100%) rename server/sonar-web/src/main/js/components/SourceViewer/helpers/{getCoverageStatus.js => getCoverageStatus.tsx} (87%) rename server/sonar-web/src/main/js/components/SourceViewer/helpers/{highlight.js => highlight.ts} (76%) rename server/sonar-web/src/main/js/components/SourceViewer/helpers/{indexing.js => indexing.ts} (70%) delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js rename server/sonar-web/src/main/js/components/SourceViewer/{popups/scm-popup.js => helpers/issueLocations.tsx} (59%) rename server/sonar-web/src/main/js/components/SourceViewer/helpers/{loadIssues.js => loadIssues.tsx} (77%) delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs delete mode 100644 server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs delete mode 100644 server/sonar-web/src/main/js/components/common/popup.js rename server/sonar-web/src/main/js/components/{SourceViewer/popups/line-actions-popup.js => issue/Issue.d.ts} (58%) rename server/sonar-web/src/main/js/store/favorites/{duck.js => duck.ts} (50%) diff --git a/server/sonar-web/config/webpack.config.js b/server/sonar-web/config/webpack.config.js index f120681011f..3d1e7aba4e6 100644 --- a/server/sonar-web/config/webpack.config.js +++ b/server/sonar-web/config/webpack.config.js @@ -68,7 +68,7 @@ module.exports = ({ production = true, fast = false }) => ({ app: [ './src/main/js/app/utils/setPublicPath.js', './src/main/js/app/index.js', - './src/main/js/components/SourceViewer/SourceViewer.js' + './src/main/js/components/SourceViewer/SourceViewer' ] }, output: { diff --git a/server/sonar-web/src/main/js/app/styles/init/type.css b/server/sonar-web/src/main/js/app/styles/init/type.css index 80b61e7dbf3..28ab828201f 100644 --- a/server/sonar-web/src/main/js/app/styles/init/type.css +++ b/server/sonar-web/src/main/js/app/styles/init/type.css @@ -23,7 +23,7 @@ body { } body { - font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif; + font-family: var(--baseFontFamily); font-size: var(--baseFontSize); line-height: 1.23076923; } diff --git a/server/sonar-web/src/main/js/app/theme.js b/server/sonar-web/src/main/js/app/theme.js index 04f518b8060..dd39fe0a445 100644 --- a/server/sonar-web/src/main/js/app/theme.js +++ b/server/sonar-web/src/main/js/app/theme.js @@ -73,6 +73,7 @@ module.exports = { pagePadding: '20px', // different + baseFontFamily: "'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif", defaultShadow: '0 6px 12px rgba(0, 0, 0, 0.175)', // z-index diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 252f6c135a2..92136630ff4 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -112,6 +112,25 @@ export interface CustomMeasure { updatedAt?: string; } +export interface Duplication { + blocks: DuplicationBlock[]; +} + +export interface DuplicationBlock { + _ref: string; + from: number; + size: number; +} + +export interface DuplicatedFile { + key: string; + name: string; + project: string; + projectName: string; + subProject?: string; + subProjectName?: string; +} + export interface Extension { key: string; name: string; @@ -122,6 +141,11 @@ export interface FacetValue { val: string; } +export interface FlowLocation { + msg: string; + textRange: TextRange; +} + export interface Group { default?: boolean; description?: string; @@ -174,12 +198,72 @@ export function isSameHomePage(a: HomePage, b: HomePage) { ); } +export interface Issue { + actions?: string[]; + assignee?: string; + assigneeActive?: string; + assigneeAvatar?: string; + assigneeLogin?: string; + assigneeName?: string; + author?: string; + comments?: IssueComment[]; + component: string; + componentLongName: string; + componentQualifier: string; + componentUuid: string; + creationDate: string; + effort?: string; + key: string; + flows: FlowLocation[][]; + line?: number; + message: string; + organization: string; + project: string; + projectName: string; + projectOrganization: string; + projectUuid: string; + resolution?: string; + rule: string; + ruleName: string; + secondaryLocations: FlowLocation[]; + severity: string; + status: string; + subProject?: string; + subProjectName?: string; + subProjectUuid?: string; + tags?: string[]; + textRange?: TextRange; + transitions?: string[]; + type: string; +} + +export interface IssueComment { + author?: string; + authorActive?: boolean; + authorAvatar?: string; + authorLogin?: string; + authorName?: string; + createdAt: string; + htmlText: string; + key: string; + markdown: string; + updatable: boolean; +} + export interface LightComponent { key: string; organization: string; qualifier: string; } +export interface LinearIssueLocation { + from: number; + index?: number; + line: number; + startLine?: number; + to: number; +} + export interface LoggedInUser extends CurrentUser { avatar?: string; email?: string; @@ -349,9 +433,24 @@ export interface ShortLivingBranch { type: BranchType.SHORT; } +export interface SourceLine { + code?: string; + conditions?: number; + coverageStatus?: string; + coveredConditions?: number; + duplicated?: boolean; + line: number; + lineHits?: number; + scmAuthor?: string; + scmDate?: string; + scmRevision?: string; +} + export interface SourceViewerFile { canMarkAsFavorite?: boolean; + fav?: boolean; key: string; + leakPeriodDate?: string; measures: { coverage?: string; duplicationDensity?: string; @@ -381,6 +480,13 @@ export interface TestCase { status: string; } +export interface TextRange { + startLine: number; + startOffset: number; + endLine: number; + endOffset: number; +} + export interface User { active: boolean; avatar?: string; diff --git a/server/sonar-web/src/main/js/apps/component/components/App.tsx b/server/sonar-web/src/main/js/apps/component/components/App.tsx index 06d009a07fa..96ff380b906 100644 --- a/server/sonar-web/src/main/js/apps/component/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/component/components/App.tsx @@ -47,7 +47,7 @@ export default class App extends React.PureComponent { render() { const { branch, id, line } = this.props.location.query; - const finalLine = line != null ? Number(line) : null; + const finalLine = line ? Number(line) : undefined; return (
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx similarity index 80% rename from server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js rename to server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx index 974eb8e4b0f..3b40ed80dae 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx @@ -17,20 +17,19 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow +import { Dispatch } from 'redux'; import { connect } from 'react-redux'; import SourceViewerBase from './SourceViewerBase'; +import { SourceViewerFile } from '../../app/types'; import { receiveFavorites } from '../../store/favorites/duck'; const mapStateToProps = null; -const onReceiveComponent = ( - component /*: { - key: string, - canMarkAsFavorite: boolean, - fav: boolean -} */ -) => dispatch => { +interface DispatchProps { + onReceiveComponent: (component: SourceViewerFile) => void; +} + +const onReceiveComponent = (component: SourceViewerFile) => (dispatch: Dispatch) => { if (component.canMarkAsFavorite) { const favorites = []; const notFavorites = []; @@ -43,6 +42,6 @@ const onReceiveComponent = ( } }; -const mapDispatchToProps = { onReceiveComponent }; +const mapDispatchToProps: DispatchProps = { onReceiveComponent }; export default connect(mapStateToProps, mapDispatchToProps)(SourceViewerBase); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx similarity index 50% rename from server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js rename to server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx index 6f267452fa0..6e487e2558a 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx @@ -17,161 +17,131 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; -import classNames from 'classnames'; +import * as React from 'react'; +import * as classNames from 'classnames'; import { intersection, uniqBy } from 'lodash'; import SourceViewerHeader from './SourceViewerHeader'; import SourceViewerCode from './SourceViewerCode'; -import CoveragePopupView from './popups/coverage-popup'; -import DuplicationPopupView from './popups/duplication-popup'; -import LineActionsPopupView from './popups/line-actions-popup'; -import SCMPopupView from './popups/scm-popup'; -import loadIssues from './helpers/loadIssues'; +import DuplicationPopup from './components/DuplicationPopup'; +import defaultLoadIssues from './helpers/loadIssues'; import getCoverageStatus from './helpers/getCoverageStatus'; import { + duplicationsByLine, issuesByLine, locationsByLine, - duplicationsByLine, symbolsByLine } from './helpers/indexing'; -/*:: import type { LinearIssueLocation } from './helpers/indexing'; */ import { - getComponentForSourceViewer, getComponentData, - getSources, + getComponentForSourceViewer, getDuplications, - getTests + getSources } from '../../api/components'; +import { + Duplication, + FlowLocation, + Issue, + LinearIssueLocation, + SourceLine, + SourceViewerFile, + DuplicatedFile +} from '../../app/types'; import { parseDate } from '../../helpers/dates'; import { translate } from '../../helpers/l10n'; -import { scrollToElement } from '../../helpers/scrolling'; -/*:: import type { SourceLine } from './types'; */ -/*:: import type { Issue, FlowLocation } from '../issue/types'; */ import './styles.css'; // TODO react-virtualized -/*:: -type Props = { - aroundLine?: number, - branch?: string, - component: string, - displayAllIssues: boolean, +interface Props { + aroundLine?: number; + branch: string | undefined; + component: string; + displayAllIssues?: boolean; displayIssueLocationsCount?: boolean; displayIssueLocationsLink?: boolean; displayLocationMarkers?: boolean; - highlightedLine?: number, - highlightedLocations?: Array, - highlightedLocationMessage?: { index: number, text: string }, - loadComponent: (component: string, branch?: string) => Promise<*>, - loadIssues: (component: string, from: number, to: number, branch?: string) => Promise<*>, - loadSources: (component: string, from: number, to: number, branch?: string) => Promise<*>, - onLoaded?: (component: Object, sources: Array<*>, issues: Array<*>) => void, - onLocationSelect?: number => void, - onIssueChange?: Issue => void, - onIssueSelect?: string => void, - onIssueUnselect?: () => void, - onReceiveComponent: ({ canMarkAsFavorite: boolean, fav: boolean, key: string }) => void, - scroll?: HTMLElement => void, - selectedIssue?: string -}; -*/ - -/*:: -type State = { - component?: Object, - displayDuplications: boolean, - duplications?: Array<{ - blocks: Array<{ - _ref: string, - from: number, - size: number - }> - }>, - duplicationsByLine: { [number]: Array }, - duplicatedFiles?: Array<{ key: string }>, - hasSourcesAfter: boolean, - highlightedLine: number | null, - highlightedSymbols: Array, - issues?: Array, - issuesByLine: { [number]: Array }, - issueLocationsByLine: { [number]: Array }, - loading: boolean, - loadingSourcesAfter: boolean, - loadingSourcesBefore: boolean, - notAccessible: boolean, - notExist: boolean, - openIssuesByLine: { [number]: boolean }, - openPopup: ?{ - issue: string, - name: string - }, - selectedIssue?: string, - sources?: Array, - sourceRemoved: boolean, - symbolsByLine: { [number]: Array } -}; -*/ - -const LINES = 500; - -function loadComponent(key /*: string */, branch /*: string | void */) /*: Promise<*> */ { - return Promise.all([ - getComponentForSourceViewer(key, branch), - getComponentData(key, branch) - ]).then(([component, data]) => ({ - ...component, - leakPeriodDate: data.leakPeriodDate && parseDate(data.leakPeriodDate) - })); + highlightedLine?: number; + highlightedLocations?: FlowLocation[]; + highlightedLocationMessage?: { index: number; text: string }; + loadComponent?: (component: string, branch: string | undefined) => Promise; + loadIssues?: ( + component: string, + from: number, + to: number, + branch: string | undefined + ) => Promise; + loadSources?: ( + component: string, + from: number, + to: number, + branch: string | undefined + ) => Promise; + onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void; + onLocationSelect?: (index: number) => void; + onIssueChange?: (issue: Issue) => void; + onIssueSelect?: (issueKey: string) => void; + onIssueUnselect?: () => void; + onReceiveComponent: (component: SourceViewerFile) => void; + scroll?: (element: HTMLElement) => void; + selectedIssue?: string; } -function loadSources( - key /*: string */, - from /*: ?number */, - to /*: ?number */, - branch /*: string | void */ -) /*: Promise> */ { - return getSources(key, from, to, branch); +interface State { + component?: SourceViewerFile; + displayDuplications: boolean; + duplications?: Duplication[]; + duplicationsByLine: { [line: number]: number[] }; + duplicatedFiles?: { [ref: string]: DuplicatedFile }; + hasSourcesAfter: boolean; + highlightedLine?: number; + highlightedSymbols: string[]; + issues?: Issue[]; + issuesByLine: { [line: number]: Issue[] }; + issueLocationsByLine: { [line: number]: LinearIssueLocation[] }; + linePopup?: { index?: number; line: number; name: string }; + loading: boolean; + loadingSourcesAfter: boolean; + loadingSourcesBefore: boolean; + notAccessible: boolean; + notExist: boolean; + openIssuesByLine: { [line: number]: boolean }; + issuePopup?: { issue: string; name: string }; + selectedIssue?: string; + sources?: SourceLine[]; + sourceRemoved: boolean; + symbolsByLine: { [line: number]: string[] }; } -export default class SourceViewerBase extends React.PureComponent { - /*:: mounted: boolean; */ - /*:: node: HTMLElement; */ - /*:: props: Props; */ - /*:: state: State; */ +const LINES = 500; + +export default class SourceViewerBase extends React.PureComponent { + node?: HTMLElement | null; + mounted = false; static defaultProps = { displayAllIssues: false, displayIssueLocationsCount: true, displayIssueLocationsLink: true, - displayLocationMarkers: true, - loadComponent, - loadIssues, - loadSources + displayLocationMarkers: true }; - constructor(props /*: Props */) { + constructor(props: Props) { super(props); this.state = { displayDuplications: false, duplicationsByLine: {}, hasSourcesAfter: false, - highlightedLine: props.highlightedLine || null, + highlightedLine: props.highlightedLine, highlightedSymbols: [], issuesByLine: {}, issueLocationsByLine: {}, - issueSecondaryLocationsByIssueByLine: {}, - issueSecondaryLocationMessagesByIssueByLine: {}, loading: true, loadingSourcesAfter: false, loadingSourcesBefore: false, notAccessible: false, notExist: false, openIssuesByLine: {}, - openPopup: null, selectedIssue: props.selectedIssue, - selectedIssueLocation: null, sourceRemoved: false, symbolsByLine: {} }; @@ -182,17 +152,20 @@ export default class SourceViewerBase extends React.PureComponent { this.fetchComponent(); } - componentWillReceiveProps(nextProps /*: Props */) { - if (nextProps.onIssueSelect != null && nextProps.selectedIssue !== this.props.selectedIssue) { + componentWillReceiveProps(nextProps: Props) { + if ( + nextProps.onIssueSelect !== undefined && + nextProps.selectedIssue !== this.props.selectedIssue + ) { this.setState({ selectedIssue: nextProps.selectedIssue }); } } - componentDidUpdate(prevProps /*: Props */) { + componentDidUpdate(prevProps: Props) { if (prevProps.component !== this.props.component || prevProps.branch !== this.props.branch) { this.fetchComponent(); } else if ( - this.props.aroundLine != null && + this.props.aroundLine !== undefined && prevProps.aroundLine !== this.props.aroundLine && this.isLineOutsideOfRange(this.props.aroundLine) ) { @@ -201,9 +174,9 @@ export default class SourceViewerBase extends React.PureComponent { const { selectedIssue } = this.props; const { issues } = this.state; if ( - selectedIssue != null && - issues != null && - issues.find(issue => issue.key === selectedIssue) == null + selectedIssue !== undefined && + issues !== undefined && + issues.find(issue => issue.key === selectedIssue) === undefined ) { this.reloadIssues(); } @@ -214,22 +187,28 @@ export default class SourceViewerBase extends React.PureComponent { this.mounted = false; } - scrollToLine(line /*: number */) { - const lineElement = this.node.querySelector( - `.source-line-code[data-line-number="${line}"] .source-line-issue-locations` - ); - if (lineElement) { - scrollToElement(lineElement, { topOffset: 125, bottomOffset: 75 }); - } + // react typings do not take `defaultProps` into account, + // so use these getters to get type-safe methods + + get safeLoadComponent() { + return this.props.loadComponent || defaultLoadComponent; + } + + get safeLoadIssues() { + return this.props.loadIssues || defaultLoadIssues; + } + + get safeLoadSources() { + return this.props.loadSources || defaultLoadSources; } - computeCoverageStatus(lines /*: Array */) /*: Array */ { + computeCoverageStatus(lines: SourceLine[]) { return lines.map(line => ({ ...line, coverageStatus: getCoverageStatus(line) })); } - isLineOutsideOfRange(lineNumber /*: number */) { + isLineOutsideOfRange(lineNumber: number) { const { sources } = this.state; - if (sources != null && sources.length > 0) { + if (sources && sources.length > 0) { const firstLine = sources[0]; const lastList = sources[sources.length - 1]; return lineNumber < firstLine.line || lineNumber > lastList.line; @@ -240,35 +219,40 @@ export default class SourceViewerBase extends React.PureComponent { fetchComponent() { this.setState({ loading: true }); - const loadIssues = (component, sources) => { - this.props.loadIssues(this.props.component, 1, LINES, this.props.branch).then(issues => { - if (this.mounted) { - const finalSources = sources.slice(0, LINES); - this.setState( - { - component, - issues, - issuesByLine: issuesByLine(issues), - issueLocationsByLine: locationsByLine(issues), - loading: false, - notAccessible: false, - notExist: false, - hasSourcesAfter: sources.length > LINES, - sources: this.computeCoverageStatus(finalSources), - sourceRemoved: false, - symbolsByLine: symbolsByLine(sources.slice(0, LINES)) - }, - () => { - if (this.props.onLoaded) { - this.props.onLoaded(component, finalSources, issues); + const loadIssues = (component: SourceViewerFile, sources: SourceLine[]) => { + this.safeLoadIssues(this.props.component, 1, LINES, this.props.branch).then( + issues => { + if (this.mounted) { + const finalSources = sources.slice(0, LINES); + this.setState( + { + component, + issues, + issuesByLine: issuesByLine(issues), + issueLocationsByLine: locationsByLine(issues), + loading: false, + notAccessible: false, + notExist: false, + hasSourcesAfter: sources.length > LINES, + sources: this.computeCoverageStatus(finalSources), + sourceRemoved: false, + symbolsByLine: symbolsByLine(sources.slice(0, LINES)) + }, + () => { + if (this.props.onLoaded) { + this.props.onLoaded(component, finalSources, issues); + } } - } - ); + ); + } + }, + () => { + // TODO } - }); + ); }; - const onFailLoadComponent = ({ response }) => { + const onFailLoadComponent = ({ response }: { response: Response }) => { // TODO handle other statuses if (this.mounted) { if (response.status === 403) { @@ -279,7 +263,7 @@ export default class SourceViewerBase extends React.PureComponent { } }; - const onFailLoadSources = (response, component) => { + const onFailLoadSources = (response: Response, component: SourceViewerFile) => { // TODO handle other statuses if (this.mounted) { if (response.status === 403) { @@ -290,7 +274,7 @@ export default class SourceViewerBase extends React.PureComponent { } }; - const onResolve = component => { + const onResolve = (component: SourceViewerFile) => { this.props.onReceiveComponent(component); const sourcesRequest = component.q === 'FIL' || component.q === 'UTS' ? this.loadSources() : Promise.resolve([]); @@ -300,29 +284,34 @@ export default class SourceViewerBase extends React.PureComponent { ); }; - this.props - .loadComponent(this.props.component, this.props.branch) - .then(onResolve, onFailLoadComponent); + this.safeLoadComponent(this.props.component, this.props.branch).then( + onResolve, + onFailLoadComponent + ); } fetchSources() { - this.loadSources().then(sources => { - if (this.mounted) { - const finalSources = sources.slice(0, LINES); - this.setState( - { - sources: sources.slice(0, LINES), - hasSourcesAfter: sources.length > LINES - }, - () => { - if (this.props.onLoaded) { - // $FlowFixMe - this.props.onLoaded(this.state.component, finalSources, this.state.issues); + this.loadSources().then( + sources => { + if (this.mounted) { + const finalSources = sources.slice(0, LINES); + this.setState( + { + sources: sources.slice(0, LINES), + hasSourcesAfter: sources.length > LINES + }, + () => { + if (this.props.onLoaded && this.state.component && this.state.issues) { + this.props.onLoaded(this.state.component, finalSources, this.state.issues); + } } - } - ); + ); + } + }, + () => { + // TODO } - }); + ); } reloadIssues() { @@ -331,13 +320,13 @@ export default class SourceViewerBase extends React.PureComponent { } const firstSourceLine = this.state.sources[0]; const lastSourceLine = this.state.sources[this.state.sources.length - 1]; - this.props - .loadIssues( - this.props.component, - firstSourceLine && firstSourceLine.line, - lastSourceLine && lastSourceLine.line - ) - .then(issues => { + this.safeLoadIssues( + this.props.component, + firstSourceLine && firstSourceLine.line, + lastSourceLine && lastSourceLine.line, + this.props.branch + ).then( + issues => { if (this.mounted) { this.setState({ issues, @@ -345,12 +334,16 @@ export default class SourceViewerBase extends React.PureComponent { issueLocationsByLine: locationsByLine(issues) }); } - }); + }, + () => { + // TODO + } + ); } - loadSources() { + loadSources = (): Promise => { return new Promise((resolve, reject) => { - const onFailLoadSources = ({ response }) => { + const onFailLoadSources = ({ response }: { response: Response }) => { // TODO handle other statuses if (this.mounted) { if ([403, 404].includes(response.status)) { @@ -371,11 +364,12 @@ export default class SourceViewerBase extends React.PureComponent { // request one additional line to define `hasSourcesAfter` to++; - return this.props - .loadSources(this.props.component, from, to, this.props.branch) - .then(sources => resolve(sources), onFailLoadSources); + return this.safeLoadSources(this.props.component, from, to, this.props.branch).then( + sources => resolve(sources), + onFailLoadSources + ); }); - } + }; loadSourcesBefore = () => { if (!this.state.sources) { @@ -384,25 +378,46 @@ export default class SourceViewerBase extends React.PureComponent { const firstSourceLine = this.state.sources[0]; this.setState({ loadingSourcesBefore: true }); const from = Math.max(1, firstSourceLine.line - LINES); - this.props - .loadSources(this.props.component, from, firstSourceLine.line - 1, this.props.branch) - .then(sources => { - this.props.loadIssues(this.props.component, from, firstSourceLine.line - 1).then(issues => { - if (this.mounted) { - this.setState(prevState => { - const nextIssues = uniqBy([...issues, ...prevState.issues], issue => issue.key); - return { - issues: nextIssues, - issuesByLine: issuesByLine(nextIssues), - issueLocationsByLine: locationsByLine(nextIssues), - loadingSourcesBefore: false, - sources: [...this.computeCoverageStatus(sources), ...prevState.sources], - symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources) } - }; - }); + this.safeLoadSources( + this.props.component, + from, + firstSourceLine.line - 1, + this.props.branch + ).then( + sources => { + this.safeLoadIssues( + this.props.component, + from, + firstSourceLine.line - 1, + this.props.branch + ).then( + issues => { + if (this.mounted) { + this.setState(prevState => { + const nextIssues = uniqBy( + [...issues, ...(prevState.issues || [])], + issue => issue.key + ); + return { + issues: nextIssues, + issuesByLine: issuesByLine(nextIssues), + issueLocationsByLine: locationsByLine(nextIssues), + loadingSourcesBefore: false, + sources: [...this.computeCoverageStatus(sources), ...(prevState.sources || [])], + symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources) } + }; + }); + } + }, + () => { + // TODO } - }); - }); + ); + }, + () => { + // TODO + } + ); }; loadSourcesAfter = () => { @@ -414,131 +429,113 @@ export default class SourceViewerBase extends React.PureComponent { const fromLine = lastSourceLine.line + 1; // request one additional line to define `hasSourcesAfter` const toLine = lastSourceLine.line + LINES + 1; - this.props - .loadSources(this.props.component, fromLine, toLine, this.props.branch) - .then(sources => { - this.props.loadIssues(this.props.component, fromLine, toLine).then(issues => { - if (this.mounted) { - this.setState(prevState => { - const nextIssues = uniqBy([...prevState.issues, ...issues], issue => issue.key); - return { - issues: nextIssues, - issuesByLine: issuesByLine(nextIssues), - issueLocationsByLine: locationsByLine(nextIssues), - hasSourcesAfter: sources.length > LINES, - loadingSourcesAfter: false, - sources: [ - ...prevState.sources, - ...this.computeCoverageStatus(sources.slice(0, LINES)) - ], - symbolsByLine: { - ...prevState.symbolsByLine, - ...symbolsByLine(sources.slice(0, LINES)) - } - }; - }); - } - }); - }); - }; - - loadDuplications = (line /*: SourceLine */) => { - getDuplications(this.props.component, this.props.branch).then(r => { - if (this.mounted) { - this.setState( - { - displayDuplications: true, - duplications: r.duplications, - duplicationsByLine: duplicationsByLine(r.duplications), - duplicatedFiles: r.files + this.safeLoadSources(this.props.component, fromLine, toLine, this.props.branch).then( + sources => { + this.safeLoadIssues(this.props.component, fromLine, toLine, this.props.branch).then( + issues => { + if (this.mounted) { + this.setState(prevState => { + const nextIssues = uniqBy( + [...(prevState.issues || []), ...issues], + issue => issue.key + ); + return { + issues: nextIssues, + issuesByLine: issuesByLine(nextIssues), + issueLocationsByLine: locationsByLine(nextIssues), + hasSourcesAfter: sources.length > LINES, + loadingSourcesAfter: false, + sources: [ + ...(prevState.sources || []), + ...this.computeCoverageStatus(sources.slice(0, LINES)) + ], + symbolsByLine: { + ...prevState.symbolsByLine, + ...symbolsByLine(sources.slice(0, LINES)) + } + }; + }); + } }, () => { - // immediately show dropdown popup if there is only one duplicated block - if (r.duplications.length === 1) { - this.handleDuplicationClick(0, line.line); - } + // TODO } ); + }, + () => { + // TODO } - }); - }; - - handleCoverageClick = (line /*: SourceLine */, element /*: HTMLElement */) => { - getTests(this.props.component, line.line, this.props.branch).then(tests => { - const popup = new CoveragePopupView({ - line, - tests, - triggerEl: element, - branch: this.props.branch - }); - popup.render(); - }); + ); }; - handleDuplicationClick = (index /*: number */, line /*: number */) => { - const duplication = this.state.duplications && this.state.duplications[index]; - let blocks = (duplication && duplication.blocks) || []; - const inRemovedComponent = blocks.some(b => b._ref == null); - let foundOne = false; - blocks = blocks.filter(b => { - const outOfBounds = b.from > line || b.from + b.size < line; - const currentFile = b._ref === '1'; - const shouldDisplayForCurrentFile = outOfBounds || foundOne; - const shouldDisplay = !currentFile || shouldDisplayForCurrentFile; - const isOk = b._ref != null && shouldDisplay; - if (b._ref === '1' && !outOfBounds) { - foundOne = true; + loadDuplications = (line: SourceLine) => { + getDuplications(this.props.component, this.props.branch).then( + r => { + if (this.mounted) { + this.setState(() => { + const changes: Partial = { + displayDuplications: true, + duplications: r.duplications, + duplicationsByLine: duplicationsByLine(r.duplications), + duplicatedFiles: r.files + }; + if (r.duplications.length === 1) { + changes.linePopup = { index: 0, line: line.line, name: 'duplications' }; + } + return changes; + }); + } + }, + () => { + // TODO } - return isOk; - }); - - const element = this.node.querySelector( - `.source-line-duplications-extra[data-line-number="${line}"]` ); - if (element) { - const popup = new DuplicationPopupView({ - blocks, - inRemovedComponent, - component: this.state.component, - files: this.state.duplicatedFiles, - triggerEl: element, - branch: this.props.branch - }); - popup.render(); - } }; - handlePopupToggle = (issue /*: string */, popupName /*: string */, open /*: ?boolean */) => { - this.setState((state /*: State */) => { + handleLinePopupToggle = ({ + index, + line, + name, + open + }: { + index?: number; + line: number; + name: string; + open?: boolean; + }) => { + this.setState((state: State) => { const samePopup = - state.openPopup && state.openPopup.name === popupName && state.openPopup.issue === issue; + state.linePopup !== undefined && + state.linePopup.name === name && + state.linePopup.line === line && + state.linePopup.index === index; if (open !== false && !samePopup) { - return { openPopup: { issue, name: popupName } }; + return { linePopup: { index, line, name } }; } else if (open !== true && samePopup) { - return { openPopup: null }; + return { linePopup: undefined }; } - return state; + return null; }); }; - displayLinePopup(line /*: number */, element /*: HTMLElement */) { - const popup = new LineActionsPopupView({ - line, - triggerEl: element, - component: this.state.component, - branch: this.props.branch - }); - popup.render(); - } + closeLinePopup = () => { + this.setState({ linePopup: undefined }); + }; - handleLineClick = (line /*: SourceLine */, element /*: HTMLElement */) => { - this.setState(prevState => ({ - highlightedLine: prevState.highlightedLine === line.line ? null : line - })); - this.displayLinePopup(line.line, element); + handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => { + this.setState((state: State) => { + const samePopup = + state.issuePopup && state.issuePopup.name === popupName && state.issuePopup.issue === issue; + if (open !== false && !samePopup) { + return { issuePopup: { issue, name: popupName } }; + } else if (open !== true && samePopup) { + return { issuePopup: undefined }; + } + return null; + }); }; - handleSymbolClick = (symbols /*: Array */) => { + handleSymbolClick = (symbols: string[]) => { this.setState(state => { const shouldDisable = intersection(state.highlightedSymbols, symbols).length > 0; const highlightedSymbols = shouldDisable ? [] : symbols; @@ -546,12 +543,7 @@ export default class SourceViewerBase extends React.PureComponent { }); }; - handleSCMClick = (line /*: SourceLine */, element /*: HTMLElement */) => { - const popup = new SCMPopupView({ triggerEl: element, line }); - popup.render(); - }; - - handleIssueSelect = (issue /*: string */) => { + handleIssueSelect = (issue: string) => { if (this.props.onIssueSelect) { this.props.onIssueSelect(issue); } else { @@ -567,79 +559,111 @@ export default class SourceViewerBase extends React.PureComponent { } }; - handleOpenIssues = (line /*: SourceLine */) => { + handleOpenIssues = (line: SourceLine) => { this.setState(state => ({ openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true } })); }; - handleCloseIssues = (line /*: SourceLine */) => { + handleCloseIssues = (line: SourceLine) => { this.setState(state => ({ openIssuesByLine: { ...state.openIssuesByLine, [line.line]: false } })); }; - handleIssueChange = (issue /*: Issue */) => { - this.setState(state => { - const issues = state.issues.map( - candidate => (candidate.key === issue.key ? issue : candidate) - ); - return { issues, issuesByLine: issuesByLine(issues) }; + handleIssueChange = (issue: Issue) => { + this.setState(({ issues = [] }) => { + const newIssues = issues.map(candidate => (candidate.key === issue.key ? issue : candidate)); + return { issues: newIssues, issuesByLine: issuesByLine(newIssues) }; }); if (this.props.onIssueChange) { this.props.onIssueChange(issue); } }; - handleFilterLine = (line /*: SourceLine */) => { + handleFilterLine = (line: SourceLine) => { const { component } = this.state; const leakPeriodDate = component && component.leakPeriodDate; return leakPeriodDate - ? line.scmDate != null && parseDate(line.scmDate) > leakPeriodDate + ? line.scmDate !== undefined && parseDate(line.scmDate) > parseDate(leakPeriodDate) : false; }; - renderCode(sources /*: Array */) { + renderDuplicationPopup = (index: number, line: number) => { + const { component, duplicatedFiles, duplications } = this.state; + + if (!component || !duplicatedFiles) return <>; + + const duplication = duplications && duplications[index]; + let blocks = (duplication && duplication.blocks) || []; + /* eslint-disable no-underscore-dangle */ + const inRemovedComponent = blocks.some(b => b._ref === undefined); + let foundOne = false; + blocks = blocks.filter(b => { + const outOfBounds = b.from > line || b.from + b.size < line; + const currentFile = b._ref === '1'; + const shouldDisplayForCurrentFile = outOfBounds || foundOne; + const shouldDisplay = !currentFile || shouldDisplayForCurrentFile; + const isOk = b._ref !== undefined && shouldDisplay; + if (b._ref === '1' && !outOfBounds) { + foundOne = true; + } + return isOk; + }); + /* eslint-enable no-underscore-dangle */ + + return ( + + ); + }; + + renderCode(sources: SourceLine[]) { const hasSourcesBefore = sources.length > 0 && sources[0].line > 1; return ( (this.node = node)}> - + {this.state.component && ( + + )} {sourceRemoved && (
{translate('code_viewer.no_source_code_displayed_due_to_source_removed')}
)} - {!sourceRemoved && sources != null && this.renderCode(sources)} + {!sourceRemoved && sources !== undefined && this.renderCode(sources)}
); } } + +function defaultLoadComponent(key: string, branch: string | undefined) { + return Promise.all([ + getComponentForSourceViewer(key, branch), + getComponentData(key, branch) + ]).then(([component, data]) => ({ + ...component, + leakPeriodDate: data.leakPeriodDate && parseDate(data.leakPeriodDate) + })); +} + +function defaultLoadSources( + key: string, + from: number | undefined, + to: number | undefined, + branch: string | undefined +) { + return getSources(key, from, to, branch); +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx similarity index 60% rename from server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js rename to server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx index b3285d2ff0c..15db6dd0961 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx @@ -17,17 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; +import * as React from 'react'; import { intersection } from 'lodash'; import Line from './components/Line'; import { getLinearLocations } from './helpers/issueLocations'; +import { Duplication, FlowLocation, Issue, LinearIssueLocation, SourceLine } from '../../app/types'; import { translate } from '../../helpers/l10n'; -/*:: import type { Duplication, SourceLine } from './types'; */ -/*:: import type { Issue, FlowLocation } from '../issue/types'; */ -/*:: import type { LinearIssueLocation } from './helpers/indexing'; */ +import { Button } from '../ui/buttons'; -const EMPTY_ARRAY = []; +const EMPTY_ARRAY: any[] = []; const ZERO_LINE = { code: '', @@ -35,88 +33,90 @@ const ZERO_LINE = { line: 0 }; -export default class SourceViewerCode extends React.PureComponent { - /*:: props: {| - branch?: string, - displayAllIssues: boolean, - displayIssueLocationsCount?: boolean; - displayIssueLocationsLink?: boolean; - displayLocationMarkers?: boolean; - duplications?: Array, - duplicationsByLine: { [number]: Array }, - duplicatedFiles?: Array<{ key: string }>, - filterLine?: SourceLine => boolean, - hasSourcesAfter: boolean, - hasSourcesBefore: boolean, - highlightedLine: number | null, - highlightedLocations?: Array, - highlightedLocationMessage?: { index: number, text: string }, - highlightedSymbols: Array, - issues: Array, - issuesByLine: { [number]: Array }, - issueLocationsByLine: { [number]: Array }, - loadDuplications: SourceLine => void, - loadSourcesAfter: () => void, - loadSourcesBefore: () => void, - loadingSourcesAfter: boolean, - loadingSourcesBefore: boolean, - onCoverageClick: (SourceLine, HTMLElement) => void, - onDuplicationClick: (number, number) => void, - onIssueChange: Issue => void, - onIssueSelect: string => void, - onIssueUnselect: () => void, - onIssuesOpen: SourceLine => void, - onIssuesClose: SourceLine => void, - onLineClick: (SourceLine, HTMLElement) => void, - onLocationSelect?: number => void, - onSCMClick: (SourceLine, HTMLElement) => void, - onSymbolClick: (Array) => void, - openIssuesByLine: { [number]: boolean }, - onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void, - openPopup: ?{ issue: string, name: string}, - scroll?: HTMLElement => void, - selectedIssue: string | null, - sources: Array, - symbolsByLine: { [number]: Array } - |}; -*/ +interface Props { + branch: string | undefined; + componentKey: string; + displayAllIssues?: boolean; + displayIssueLocationsCount?: boolean; + displayIssueLocationsLink?: boolean; + displayLocationMarkers?: boolean; + duplications: Duplication[] | undefined; + duplicationsByLine: { [line: number]: number[] }; + filterLine?: (line: SourceLine) => boolean; + hasSourcesAfter: boolean; + hasSourcesBefore: boolean; + highlightedLine: number | undefined; + highlightedLocationMessage: { index: number; text: string } | undefined; + highlightedLocations: FlowLocation[] | undefined; + highlightedSymbols: string[]; + issueLocationsByLine: { [line: number]: LinearIssueLocation[] }; + issuePopup: { issue: string; name: string } | undefined; + issues: Issue[] | undefined; + issuesByLine: { [line: number]: Issue[] }; + linePopup: { index?: number; line: number; name: string } | undefined; + loadDuplications: (line: SourceLine) => void; + loadingSourcesAfter: boolean; + loadingSourcesBefore: boolean; + loadSourcesAfter: () => void; + loadSourcesBefore: () => void; + onIssueChange: (issue: Issue) => void; + onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void; + onIssuesClose: (line: SourceLine) => void; + onIssueSelect: (issueKey: string) => void; + onIssuesOpen: (line: SourceLine) => void; + onIssueUnselect: () => void; + onLinePopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + onLocationSelect: ((index: number) => void) | undefined; + onSymbolClick: (symbols: string[]) => void; + openIssuesByLine: { [line: number]: boolean }; + renderDuplicationPopup: (index: number, line: number) => JSX.Element; + scroll?: (element: HTMLElement) => void; + selectedIssue: string | undefined; + sources: SourceLine[]; + symbolsByLine: { [line: number]: string[] }; +} - getDuplicationsForLine(line /*: SourceLine */) { +export default class SourceViewerCode extends React.PureComponent { + getDuplicationsForLine = (line: SourceLine): number[] => { return this.props.duplicationsByLine[line.line] || EMPTY_ARRAY; - } + }; - getIssuesForLine(line /*: SourceLine */) /*: Array */ { + getIssuesForLine = (line: SourceLine): Issue[] => { return this.props.issuesByLine[line.line] || EMPTY_ARRAY; - } + }; - getIssueLocationsForLine(line /*: SourceLine */) { + getIssueLocationsForLine = (line: SourceLine): LinearIssueLocation[] => { return this.props.issueLocationsByLine[line.line] || EMPTY_ARRAY; - } + }; - getSecondaryIssueLocationsForLine( - line /*: SourceLine */ - ) /*: Array<{ from: number, to: number, line: number, index: number, startLine: number }> */ { + getSecondaryIssueLocationsForLine = (line: SourceLine): LinearIssueLocation[] => { const { highlightedLocations } = this.props; if (!highlightedLocations) { return EMPTY_ARRAY; } return highlightedLocations.reduce((locations, location, index) => { - const linearLocations = getLinearLocations(location.textRange) + const linearLocations: LinearIssueLocation[] = getLinearLocations(location.textRange) .filter(l => l.line === line.line) .map(l => ({ ...l, startLine: location.textRange.startLine, index })); return [...locations, ...linearLocations]; }, []); - } + }; - renderLine = ( - line /*: SourceLine */, - index /*: number */, - displayCoverage /*: boolean */, - displayDuplications /*: boolean */, - displayIssues /*: boolean */ - ) => { + renderLine = ({ + line, + index, + displayCoverage, + displayDuplications, + displayIssues + }: { + line: SourceLine; + index: number; + displayCoverage: boolean; + displayDuplications: boolean; + displayIssues: boolean; + }) => { const { filterLine, highlightedLocationMessage, selectedIssue, sources } = this.props; - const filtered = filterLine ? filterLine(line) : null; + const filtered = filterLine && filterLine(line); const secondaryIssueLocations = this.getSecondaryIssueLocationsForLine(line); @@ -127,15 +127,18 @@ export default class SourceViewerCode extends React.PureComponent { // for the following properties pass null if the line for sure is not impacted const symbolsForLine = this.props.symbolsByLine[line.line] || []; const { highlightedSymbols } = this.props; - let optimizedHighlightedSymbols = intersection(symbolsForLine, highlightedSymbols); + let optimizedHighlightedSymbols: string[] | undefined = intersection( + symbolsForLine, + highlightedSymbols + ); if (!optimizedHighlightedSymbols.length) { optimizedHighlightedSymbols = undefined; } const optimizedSelectedIssue = - selectedIssue != null && issuesForLine.find(issue => issue.key === selectedIssue) + selectedIssue !== undefined && issuesForLine.find(issue => issue.key === selectedIssue) ? selectedIssue - : null; + : undefined; const optimizedSecondaryIssueLocations = secondaryIssueLocations.length > 0 ? secondaryIssueLocations : EMPTY_ARRAY; @@ -151,12 +154,13 @@ export default class SourceViewerCode extends React.PureComponent { return ( 0 ? sources[index - 1] : undefined} + renderDuplicationPopup={this.props.renderDuplicationPopup} scroll={this.props.scroll} secondaryIssueLocations={optimizedSecondaryIssueLocations} selectedIssue={optimizedSelectedIssue} @@ -193,13 +196,13 @@ export default class SourceViewerCode extends React.PureComponent { }; render() { - const { sources } = this.props; + const { issues = [], sources } = this.props; - const hasCoverage = sources.some(s => s.coverageStatus != null); - const hasDuplications = sources.some(s => s.duplicated); - const hasIssues = this.props.issues.length > 0; + const displayCoverage = sources.some(s => s.coverageStatus != null); + const displayDuplications = sources.some(s => !!s.duplicated); + const displayIssues = issues.length > 0; - const hasFileIssues = hasIssues && this.props.issues.some(issue => !issue.textRange); + const hasFileIssues = displayIssues && issues.some(issue => !issue.textRange); return (
@@ -213,11 +216,11 @@ export default class SourceViewerCode extends React.PureComponent {
) : ( - + )} )} @@ -225,9 +228,15 @@ export default class SourceViewerCode extends React.PureComponent { {hasFileIssues && - this.renderLine(ZERO_LINE, -1, hasCoverage, hasDuplications, hasIssues)} + this.renderLine({ + line: ZERO_LINE, + index: -1, + displayCoverage, + displayDuplications, + displayIssues + })} {sources.map((line, index) => - this.renderLine(line, index, hasCoverage, hasDuplications, hasIssues) + this.renderLine({ line, index, displayCoverage, displayDuplications, displayIssues }) )}
@@ -242,11 +251,11 @@ export default class SourceViewerCode extends React.PureComponent { ) : ( - + )} )} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx new file mode 100644 index 00000000000..2edff725967 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx @@ -0,0 +1,153 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 * as React from 'react'; +import { groupBy } from 'lodash'; +import { getTests } from '../../../api/components'; +import { SourceLine, TestCase } from '../../../app/types'; +import BubblePopup from '../../common/BubblePopup'; +import TestStatusIcon from '../../shared/TestStatusIcon'; +import { translate } from '../../../helpers/l10n'; +import { collapsePath } from '../../../helpers/path'; + +interface Props { + branch: string | undefined; + componentKey: string; + line: SourceLine; + onClose: () => void; + popupPosition?: any; +} + +interface State { + loading: boolean; + testCases: TestCase[]; +} + +export default class CoveragePopup extends React.PureComponent { + mounted = false; + state: State = { loading: true, testCases: [] }; + + componentDidMount() { + this.mounted = true; + this.fetchTests(); + } + + componentDidUpdate(prevProps: Props) { + // TODO use branchLike + if ( + prevProps.branch !== this.props.branch || + prevProps.componentKey !== this.props.componentKey || + prevProps.line.line !== this.props.line.line + ) { + this.fetchTests(); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchTests = () => { + this.setState({ loading: true }); + getTests(this.props.componentKey, this.props.line.line, this.props.branch).then( + testCases => { + if (this.mounted) { + this.setState({ loading: false, testCases }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + }; + + handleTestClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + const { key } = event.currentTarget.dataset; + const Workspace = require('../../workspace/main').default; + Workspace.openComponent({ key, branch: this.props.branch }); + this.props.onClose(); + }; + + render() { + const { line } = this.props; + const testCasesByFile = groupBy(this.state.testCases || [], 'fileKey'); + const testFiles = Object.keys(testCasesByFile).map(fileKey => { + const testSet = testCasesByFile[fileKey]; + const test = testSet[0]; + return { + file: { key: test.fileKey, longName: test.fileName }, + tests: testSet + }; + }); + + return ( + +
+ {translate('source_viewer.covered')} + {!!line.conditions && ( +
+ {'('} + {line.coveredConditions || '0'} + {' of '} + {line.conditions} {translate('source_viewer.conditions')} + {')'} +
+ )} +
+ {this.state.loading ? ( + + ) : ( + <> + {testFiles.length === 0 && + translate('source_viewer.tooltip.no_information_about_tests')} + {testFiles.map(testFile => ( +
+ + {collapsePath(testFile.file.longName)} + +
    + {testFile.tests.map(testCase => ( +
  • + + {testCase.name} + {testCase.status !== 'SKIPPED' && ( + {testCase.durationInMs}ms + )} +
  • + ))} +
+
+ ))} + + )} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx new file mode 100644 index 00000000000..9c58a3e01ad --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx @@ -0,0 +1,158 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 * as React from 'react'; +import { Link } from 'react-router'; +import { groupBy, sortBy } from 'lodash'; +import { SourceViewerFile, DuplicationBlock, DuplicatedFile } from '../../../app/types'; +import BubblePopup from '../../common/BubblePopup'; +import QualifierIcon from '../../shared/QualifierIcon'; +import { translate } from '../../../helpers/l10n'; +import { collapsedDirFromPath, fileFromPath } from '../../../helpers/path'; +import { getProjectUrl } from '../../../helpers/urls'; + +interface Props { + blocks: DuplicationBlock[]; + // TODO use branchLike + branch: string | undefined; + duplicatedFiles?: { [ref: string]: DuplicatedFile }; + inRemovedComponent: boolean; + onClose: () => void; + popupPosition?: any; + sourceViewerFile: SourceViewerFile; +} + +export default class DuplicationPopup extends React.PureComponent { + isDifferentComponent = ( + a: { project: string; subProject?: string }, + b: { project: string; subProject?: string } + ) => { + return Boolean(a && b && (a.project !== b.project || a.subProject !== b.subProject)); + }; + + handleFileClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + const Workspace = require('../../workspace/main').default; + const { key, line } = event.currentTarget.dataset; + Workspace.openComponent({ key, line, branch: this.props.branch }); + this.props.onClose(); + }; + + render() { + const { duplicatedFiles = {}, sourceViewerFile } = this.props; + + const groupedBlocks = groupBy(this.props.blocks, '_ref'); + let duplications = Object.keys(groupedBlocks).map(fileRef => { + return { + blocks: groupedBlocks[fileRef], + file: duplicatedFiles[fileRef] + }; + }); + + // first duplications in the same file + // then duplications in the same sub-project + // then duplications in the same project + // then duplications in other projects + duplications = sortBy( + duplications, + d => d.file.projectName !== sourceViewerFile.projectName, + d => d.file.subProjectName !== sourceViewerFile.subProjectName, + d => d.file.key !== sourceViewerFile.key + ); + + return ( + +
+ {this.props.inRemovedComponent && ( +
+ {translate('duplications.dups_found_on_deleted_resource')} +
+ )} + {duplications.length > 0 && ( + <> +
+ {translate('component_viewer.transition.duplication')} +
+ {duplications.map(duplication => ( +
+
+ {this.isDifferentComponent(duplication.file, this.props.sourceViewerFile) && ( + <> +
+ + + {duplication.file.projectName} + +
+ {duplication.file.subProject && + duplication.file.subProjectName && ( +
+ + + {duplication.file.subProjectName} + +
+ )} + + )} + + {duplication.file.key !== this.props.sourceViewerFile.key && ( + + )} + +
+ {'Lines: '} + {duplication.blocks.map((block, index) => ( + + + {block.from} + {' – '} + {block.from + block.size - 1} + + {index < duplication.blocks.length - 1 && ', '} + + ))} +
+
+
+ ))} + + )} +
+
+ ); + } +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx similarity index 56% rename from server/sonar-web/src/main/js/components/SourceViewer/components/Line.js rename to server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx index ff74df479b1..a29255557dd 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx @@ -17,9 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; -import classNames from 'classnames'; +import * as React from 'react'; +import * as classNames from 'classnames'; import { times } from 'lodash'; import LineNumber from './LineNumber'; import LineSCM from './LineSCM'; @@ -28,60 +27,64 @@ import LineDuplications from './LineDuplications'; import LineDuplicationBlock from './LineDuplicationBlock'; import LineIssuesIndicator from './LineIssuesIndicator'; import LineCode from './LineCode'; -/*:: import type { SourceLine } from '../types'; */ -/*:: import type { LinearIssueLocation } from '../helpers/indexing'; */ -/*:: import type { Issue } from '../../issue/types'; */ +import { Issue, LinearIssueLocation, SourceLine } from '../../../app/types'; -/*:: -type Props = {| - branch?: string, - displayAllIssues: boolean, - displayCoverage: boolean, - displayDuplications: boolean, - displayIssues: boolean, +interface Props { + branch: string | undefined; + componentKey: string; + displayAllIssues?: boolean; + displayCoverage: boolean; + displayDuplications: boolean; displayIssueLocationsCount?: boolean; displayIssueLocationsLink?: boolean; + displayIssues: boolean; displayLocationMarkers?: boolean; - duplications: Array, - duplicationsCount: number, - filtered: boolean | null, - highlighted: boolean, - highlightedLocationMessage?: { index: number, text: string }, - highlightedSymbols?: Array, - issueLocations: Array, - issues: Array, - last: boolean, - line: SourceLine, - loadDuplications: SourceLine => void, - onClick: (SourceLine, HTMLElement) => void, - onCoverageClick: (SourceLine, HTMLElement) => void, - onDuplicationClick: (number, number) => void, - onIssueChange: Issue => void, - onIssueSelect: string => void, - onIssueUnselect: () => void, - onIssuesOpen: SourceLine => void, - onIssuesClose: SourceLine => void, - onLocationSelect?: number => void, - onSCMClick: (SourceLine, HTMLElement) => void, - onSymbolClick: (Array) => void, - openIssues: boolean, - onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void, - openPopup: ?{ issue: string, name: string}, - previousLine?: SourceLine, - scroll?: HTMLElement => void, + duplications: number[]; + duplicationsCount: number; + filtered: boolean | undefined; + highlighted: boolean; + highlightedLocationMessage: { index: number; text: string } | undefined; + highlightedSymbols: string[] | undefined; + issueLocations: LinearIssueLocation[]; + issuePopup: { issue: string; name: string } | undefined; + issues: Issue[]; + last: boolean; + line: SourceLine; + linePopup: { index?: number; line: number; name: string } | undefined; + loadDuplications: (line: SourceLine) => void; + onLinePopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + onIssueChange: (issue: Issue) => void; + onIssuePopupToggle: (issueKey: string, popupName: string, open?: boolean) => void; + onIssuesClose: (line: SourceLine) => void; + onIssueSelect: (issueKey: string) => void; + onIssuesOpen: (line: SourceLine) => void; + onIssueUnselect: () => void; + onLocationSelect: ((x: number) => void) | undefined; + onSymbolClick: (symbols: string[]) => void; + openIssues: boolean; + previousLine: SourceLine | undefined; + renderDuplicationPopup: (index: number, line: number) => JSX.Element; + scroll?: (element: HTMLElement) => void; secondaryIssueLocations: Array<{ - from: number, - to: number, - line: number, - index: number, - startLine: number - }>, - selectedIssue: string | null -|}; -*/ + from: number; + to: number; + line: number; + index: number; + startLine: number; + }>; + selectedIssue: string | undefined; +} -export default class Line extends React.PureComponent { - /*:: props: Props; */ +export default class Line extends React.PureComponent { + isPopupOpen = (name: string, index?: number) => { + const { line, linePopup } = this.props; + return ( + linePopup !== undefined && + linePopup.index === index && + linePopup.line === line.line && + linePopup.name === name + ); + }; handleIssuesIndicatorClick = () => { if (this.props.openIssues) { @@ -98,7 +101,14 @@ export default class Line extends React.PureComponent { }; render() { - const { line, duplications, displayCoverage, duplicationsCount, filtered } = this.props; + const { + displayCoverage, + duplications, + duplicationsCount, + filtered, + issuePopup, + line + } = this.props; const className = classNames('source-line', { 'source-line-highlighted': this.props.highlighted, 'source-line-filtered': filtered === true, @@ -110,16 +120,29 @@ export default class Line extends React.PureComponent { return ( - + {this.props.displayCoverage && ( - + )} {this.props.displayDuplications && ( @@ -132,7 +155,9 @@ export default class Line extends React.PureComponent { index={index} key={index} line={this.props.line} - onClick={this.props.onDuplicationClick} + onPopupToggle={this.props.onLinePopupToggle} + popupOpen={this.isPopupOpen('duplications', index)} + renderDuplicationPopup={this.props.renderDuplicationPopup} /> ))} @@ -152,15 +177,15 @@ export default class Line extends React.PureComponent { displayLocationMarkers={this.props.displayLocationMarkers} highlightedLocationMessage={this.props.highlightedLocationMessage} highlightedSymbols={this.props.highlightedSymbols} - issues={this.props.issues} issueLocations={this.props.issueLocations} + issuePopup={issuePopup} + issues={this.props.issues} line={line} onIssueChange={this.props.onIssueChange} + onIssuePopupToggle={this.props.onIssuePopupToggle} onIssueSelect={this.props.onIssueSelect} onLocationSelect={this.props.onLocationSelect} onSymbolClick={this.props.onSymbolClick} - onPopupToggle={this.props.onPopupToggle} - openPopup={this.props.openPopup} scroll={this.props.scroll} secondaryIssueLocations={this.props.secondaryIssueLocations} selectedIssue={this.props.selectedIssue} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx similarity index 68% rename from server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js rename to server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx index fc197bcd217..c91f58c43c2 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx @@ -17,62 +17,57 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; -import classNames from 'classnames'; +import * as React from 'react'; +import * as classNames from 'classnames'; import LineIssuesList from './LineIssuesList'; +import { Issue, LinearIssueLocation, SourceLine } from '../../../app/types'; import LocationIndex from '../../common/LocationIndex'; import LocationMessage from '../../common/LocationMessage'; -import { splitByTokens, highlightSymbol, highlightIssueLocations } from '../helpers/highlight'; -/*:: import type { Tokens } from '../helpers/highlight'; */ -/*:: import type { SourceLine } from '../types'; */ -/*:: import type { LinearIssueLocation } from '../helpers/indexing'; */ -/*:: import type { Issue } from '../../issue/types'; */ - -/*:: -type Props = {| - branch?: string, - displayIssueLocationsCount?: boolean, - displayIssueLocationsLink?: boolean, - displayLocationMarkers?: boolean, - highlightedLocationMessage?: { index: number, text: string }, - highlightedSymbols?: Array, - issues: Array, - issueLocations: Array, - line: SourceLine, - onIssueChange: Issue => void, - onIssueSelect: (issueKey: string) => void, - onLocationSelect?: number => void, - onSymbolClick: (Array) => void, - onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void, - openPopup: ?{ issue: string, name: string}, - scroll?: HTMLElement => void, +import { + highlightIssueLocations, + highlightSymbol, + splitByTokens, + Token +} from '../helpers/highlight'; + +interface Props { + branch: string | undefined; + displayIssueLocationsCount?: boolean; + displayIssueLocationsLink?: boolean; + displayLocationMarkers?: boolean; + highlightedLocationMessage: { index: number; text: string } | undefined; + highlightedSymbols: string[] | undefined; + issueLocations: LinearIssueLocation[]; + issuePopup: { issue: string; name: string } | undefined; + issues: Issue[]; + line: SourceLine; + onIssueChange: (issue: Issue) => void; + onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void; + onIssueSelect: (issueKey: string) => void; + onLocationSelect: ((index: number) => void) | undefined; + onSymbolClick: (symbols: Array) => void; + scroll?: (element: HTMLElement) => void; secondaryIssueLocations: Array<{ - from: number, - to: number, - line: number, - index: number, - startLine: number - }>, - selectedIssue: string | null, - showIssues: boolean -|}; -*/ - -/*:: -type State = { - tokens: Tokens -}; -*/ - -export default class LineCode extends React.PureComponent { - /*:: activeMarkerNode: ?HTMLElement; */ - /*:: codeNode: HTMLElement; */ - /*:: props: Props; */ - /*:: state: State; */ - /*:: symbols: NodeList; */ - - constructor(props /*: Props */) { + from: number; + to: number; + line: number; + index: number; + startLine: number; + }>; + selectedIssue: string | undefined; + showIssues?: boolean; +} + +interface State { + tokens: Token[]; +} + +export default class LineCode extends React.PureComponent { + activeMarkerNode?: HTMLElement | null; + codeNode?: HTMLElement | null; + symbols?: NodeListOf; + + constructor(props: Props) { super(props); this.state = { tokens: splitByTokens(props.line.code || '') @@ -86,7 +81,7 @@ export default class LineCode extends React.PureComponent { } } - componentWillReceiveProps(nextProps /*: Props */) { + componentWillReceiveProps(nextProps: Props) { if (nextProps.line.code !== this.props.line.code) { this.setState({ tokens: splitByTokens(nextProps.line.code || '') @@ -98,7 +93,7 @@ export default class LineCode extends React.PureComponent { this.detachEvents(); } - componentDidUpdate(prevProps /*: Props */) { + componentDidUpdate(prevProps: Props) { this.attachEvents(); if ( this.props.highlightedLocationMessage && @@ -117,41 +112,44 @@ export default class LineCode extends React.PureComponent { attachEvents() { if (this.codeNode) { this.symbols = this.codeNode.querySelectorAll('.sym'); - for (const symbol of this.symbols) { - symbol.addEventListener('click', this.handleSymbolClick); + if (this.symbols) { + for (let i = 0; i < this.symbols.length; i++) { + const symbol = this.symbols[i]; + symbol.addEventListener('click', this.handleSymbolClick); + } } } } detachEvents() { if (this.symbols) { - for (const symbol of this.symbols) { - symbol.removeEventListener('click', this.handleSymbolClick); + for (let i = 0; i < this.symbols.length; i++) { + const symbol = this.symbols[i]; + symbol.addEventListener('click', this.handleSymbolClick); } } } - handleSymbolClick = (e /*: Object */) => { - e.preventDefault(); - const keys = e.currentTarget.className.match(/sym-\d+/g); - if (keys.length > 0) { + handleSymbolClick = (event: MouseEvent) => { + event.preventDefault(); + const keys = (event.currentTarget as HTMLElement).className.match(/sym-\d+/g); + if (keys && keys.length > 0) { this.props.onSymbolClick(keys); } }; - renderMarker(index /*: number */, message /*: ?string */, leading /*: boolean */ = false) { + renderMarker(index: number, message: string | undefined, leading = false) { const { onLocationSelect } = this.props; const onClick = onLocationSelect ? () => onLocationSelect(index) : undefined; - const ref = message != null ? node => (this.activeMarkerNode = node) : undefined; + const ref = + message != null ? (node: HTMLElement | null) => (this.activeMarkerNode = node) : undefined; return ( - - {index + 1} - + {index + 1} {message != null && {message}} ); @@ -199,7 +197,7 @@ export default class LineCode extends React.PureComponent { 'has-issues': issues.length > 0 }); - const renderedTokens = []; + const renderedTokens: React.ReactNode[] = []; // track if the first marker is displayed before the source code // set `false` for the first token in a row @@ -211,7 +209,7 @@ export default class LineCode extends React.PureComponent { const message = highlightedLocationMessage != null && highlightedLocationMessage.index === marker ? highlightedLocationMessage.text - : null; + : undefined; renderedTokens.push(this.renderMarker(marker, message, leadingMarker)); }); } @@ -236,11 +234,11 @@ export default class LineCode extends React.PureComponent { branch={this.props.branch} displayIssueLocationsCount={this.props.displayIssueLocationsCount} displayIssueLocationsLink={this.props.displayIssueLocationsLink} + issuePopup={this.props.issuePopup} issues={issues} onIssueChange={this.props.onIssueChange} onIssueClick={onIssueSelect} - onPopupToggle={this.props.onPopupToggle} - openPopup={this.props.openPopup} + onIssuePopupToggle={this.props.onIssuePopupToggle} selectedIssue={selectedIssue} /> )} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js deleted file mode 100644 index 30fbf0d36a1..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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. - */ -// @flow -import React from 'react'; -import Tooltip from '../../controls/Tooltip'; -import { translate } from '../../../helpers/l10n'; -/*:: import type { SourceLine } from '../types'; */ - -/*:: -type Props = { - line: SourceLine, - onClick: (SourceLine, HTMLElement) => void -}; -*/ - -export default class LineCoverage extends React.PureComponent { - /*:: props: Props; */ - - handleClick = (e /*: SyntheticInputEvent */) => { - e.preventDefault(); - this.props.onClick(this.props.line, e.target); - }; - - render() { - const { line } = this.props; - const className = - 'source-meta source-line-coverage' + - (line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : ''); - const hasPopup = - line.coverageStatus === 'covered' || line.coverageStatus === 'partially-covered'; - const cell = ( - -
- - ); - - return line.coverageStatus != null ? ( - - {cell} - - ) : ( - cell - ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx new file mode 100644 index 00000000000..c6b9efb895d --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx @@ -0,0 +1,102 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 * as React from 'react'; +import CoveragePopup from './CoveragePopup'; +import { SourceLine } from '../../../app/types'; +import Tooltip from '../../controls/Tooltip'; +import { translate } from '../../../helpers/l10n'; +import BubblePopupHelper from '../../common/BubblePopupHelper'; + +interface Props { + branch: string | undefined; + componentKey: string; + line: SourceLine; + onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + popupOpen: boolean; +} + +export default class LineCoverage extends React.PureComponent { + handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + event.currentTarget.blur(); + this.props.onPopupToggle({ line: this.props.line.line, name: 'coverage' }); + }; + + handleTogglePopup = (open: boolean) => { + this.props.onPopupToggle({ line: this.props.line.line, name: 'coverage', open }); + }; + + closePopup = () => { + this.props.onPopupToggle({ line: this.props.line.line, name: 'coverage', open: false }); + }; + + render() { + const { branch, componentKey, line, popupOpen } = this.props; + + const className = + 'source-meta source-line-coverage' + + (line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : ''); + + const hasPopup = + line.coverageStatus === 'covered' || line.coverageStatus === 'partially-covered'; + + const cell = line.coverageStatus ? ( + +
+ + ) : ( +
+ ); + + if (hasPopup) { + return ( + + {cell} + + } + position="bottomright" + togglePopup={this.handleTogglePopup} + /> + + ); + } + + return ( + + {cell} + + ); + } +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js deleted file mode 100644 index 51c83c9c99c..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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. - */ -// @flow -import React from 'react'; -import classNames from 'classnames'; -import Tooltip from '../../controls/Tooltip'; -import { translate } from '../../../helpers/l10n'; -/*:: import type { SourceLine } from '../types'; */ - -/*:: -type Props = { - duplicated: boolean, - index: number, - line: SourceLine, - onClick: (index: number, lineNumber: number) => void -}; -*/ - -export default class LineDuplicationBlock extends React.PureComponent { - /*:: props: Props; */ - - handleClick = (e /*: SyntheticInputEvent */) => { - e.preventDefault(); - this.props.onClick(this.props.index, this.props.line.line); - }; - - render() { - const { duplicated, index, line } = this.props; - const className = classNames('source-meta', 'source-line-duplications-extra', { - 'source-line-duplicated': duplicated - }); - - const cell = ( - -
- - ); - - return duplicated ? ( - - {cell} - - ) : ( - cell - ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx new file mode 100644 index 00000000000..0121bfa868d --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx @@ -0,0 +1,90 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 * as React from 'react'; +import * as classNames from 'classnames'; +import { SourceLine } from '../../../app/types'; +import Tooltip from '../../controls/Tooltip'; +import { translate } from '../../../helpers/l10n'; +import BubblePopupHelper from '../../common/BubblePopupHelper'; + +interface Props { + duplicated: boolean; + index: number; + line: SourceLine; + onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + popupOpen: boolean; + renderDuplicationPopup: (index: number, line: number) => JSX.Element; +} + +export default class LineDuplicationBlock extends React.PureComponent { + handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + event.currentTarget.blur(); + this.props.onPopupToggle({ + index: this.props.index, + line: this.props.line.line, + name: 'duplications' + }); + }; + + handleTogglePopup = (open: boolean) => { + this.props.onPopupToggle({ + index: this.props.index, + line: this.props.line.line, + name: 'duplications', + open + }); + }; + + render() { + const { duplicated, index, line, popupOpen } = this.props; + const className = classNames('source-meta', 'source-line-duplications-extra', { + 'source-line-duplicated': duplicated + }); + + const cell =
; + + return duplicated ? ( + + + {cell} + + + + ) : ( + + {cell} + + ); + } +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.tsx similarity index 82% rename from server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js rename to server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.tsx index 7e463eab0b2..c51c49dcdca 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.tsx @@ -17,25 +17,20 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; -import classNames from 'classnames'; +import * as React from 'react'; +import * as classNames from 'classnames'; +import { SourceLine } from '../../../app/types'; import Tooltip from '../../controls/Tooltip'; import { translate } from '../../../helpers/l10n'; -/*:: import type { SourceLine } from '../types'; */ -/*:: -type Props = { - line: SourceLine, - onClick: SourceLine => void -}; -*/ - -export default class LineDuplications extends React.PureComponent { - /*:: props: Props; */ +interface Props { + line: SourceLine; + onClick: (line: SourceLine) => void; +} - handleClick = (e /*: SyntheticInputEvent */) => { - e.preventDefault(); +export default class LineDuplications extends React.PureComponent { + handleClick = (event: React.MouseEvent) => { + event.preventDefault(); this.props.onClick(this.props.line); }; @@ -48,9 +43,9 @@ export default class LineDuplications extends React.PureComponent { const cell = ( + tabIndex={line.duplicated ? 0 : undefined}>
); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx similarity index 77% rename from server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js rename to server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx index 2a7159bfc6b..e5434c0388c 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx @@ -17,27 +17,21 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; -import classNames from 'classnames'; +import * as React from 'react'; +import * as classNames from 'classnames'; import SeverityIcon from '../../shared/SeverityIcon'; import { sortBySeverity } from '../../../helpers/issues'; -/*:: import type { SourceLine } from '../types'; */ -/*:: import type { Issue } from '../../issue/types'; */ +import { Issue, SourceLine } from '../../../app/types'; -/*:: -type Props = { - issues: Array, - line: SourceLine, - onClick: () => void -}; -*/ - -export default class LineIssuesIndicator extends React.PureComponent { - /*:: props: Props; */ +interface Props { + issues: Issue[]; + line: SourceLine; + onClick: () => void; +} - handleClick = (e /*: SyntheticInputEvent */) => { - e.preventDefault(); +export default class LineIssuesIndicator extends React.PureComponent { + handleClick = (event: React.MouseEvent) => { + event.preventDefault(); this.props.onClick(); }; @@ -53,9 +47,9 @@ export default class LineIssuesIndicator extends React.PureComponent { + tabIndex={hasIssues ? 0 : undefined}> {mostImportantIssue != null && } {issues.length > 1 && {issues.length}} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js deleted file mode 100644 index 66ef7584321..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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. - */ -// @flow -import React from 'react'; -import Issue from '../../issue/Issue'; -/*:: import type { Issue as IssueType } from '../../issue/types'; */ - -/*:: -type Props = { - branch?: string, - displayIssueLocationsCount?: boolean; - displayIssueLocationsLink?: boolean; - issues: Array, - onIssueChange: IssueType => void, - onIssueClick: (issueKey: string) => void, - onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void, - openPopup: ?{ issue: string, name: string}, - selectedIssue: string | null -}; -*/ - -export default class LineIssuesList extends React.PureComponent { - /*:: props: Props; */ - - render() { - const { branch, issues, onIssueClick, openPopup, selectedIssue } = this.props; - - return ( -
- {issues.map(issue => ( - - ))} -
- ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx new file mode 100644 index 00000000000..231cc639171 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx @@ -0,0 +1,57 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 * as React from 'react'; +import { Issue as IssueType } from '../../../app/types'; +import Issue from '../../issue/Issue'; + +interface Props { + branch: string | undefined; + displayIssueLocationsCount?: boolean; + displayIssueLocationsLink?: boolean; + issuePopup: { issue: string; name: string } | undefined; + issues: IssueType[]; + onIssueChange: (issue: IssueType) => void; + onIssueClick: (issueKey: string) => void; + onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void; + selectedIssue: string | undefined; +} + +export default function LineIssuesList(props: Props) { + const { issuePopup } = props; + + return ( +
+ {props.issues.map(issue => ( + + ))} +
+ ); +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js deleted file mode 100644 index 801830c5e29..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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. - */ -// @flow -import React from 'react'; -/*:: import type { SourceLine } from '../types'; */ - -/*:: -type Props = { - line: SourceLine, - onClick: (SourceLine, HTMLElement) => void -}; -*/ - -export default class LineNumber extends React.PureComponent { - /*:: props: Props; */ - - handleClick = (e /*: SyntheticInputEvent */) => { - e.preventDefault(); - this.props.onClick(this.props.line, e.target); - }; - - render() { - const { line } = this.props.line; - - return ( - - ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx new file mode 100644 index 00000000000..f6126f5e7ec --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 * as React from 'react'; +import LineOptionsPopup from './LineOptionsPopup'; +import { SourceLine } from '../../../app/types'; +import BubblePopupHelper from '../../common/BubblePopupHelper'; + +interface Props { + // TODO use branchLike + branch: string | undefined; + componentKey: string; + line: SourceLine; + onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + popupOpen: boolean; +} + +export default class LineNumber extends React.PureComponent { + handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + event.currentTarget.blur(); + this.props.onPopupToggle({ line: this.props.line.line, name: 'line-number' }); + }; + + handleTogglePopup = (open: boolean) => { + this.props.onPopupToggle({ line: this.props.line.line, name: 'line-number', open }); + }; + + render() { + const { branch, componentKey, line, popupOpen } = this.props; + const { line: lineNumber } = line; + const hasLineNumber = !!lineNumber; + return hasLineNumber ? ( + + } + position="bottomright" + togglePopup={this.handleTogglePopup} + /> + + ) : ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx new file mode 100644 index 00000000000..db4634b2c28 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 * as React from 'react'; +import { Link } from 'react-router'; +import { SourceLine } from '../../../app/types'; +import BubblePopup from '../../common/BubblePopup'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + // TODO use branchLike + branch: string | undefined; + componentKey: string; + line: SourceLine; + popupPosition?: any; +} + +export default function LineOptionsPopup({ branch, componentKey, line, popupPosition }: Props) { + const permalink = { + pathname: '/component', + query: { branch, id: componentKey, line: line.line } + }; + return ( + +
+ + {translate('component_viewer.get_permalink')} + +
+
+ ); +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js deleted file mode 100644 index 274add2c12d..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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. - */ -// @flow -import React from 'react'; -/*:: import type { SourceLine } from '../types'; */ - -/*:: -type Props = { - line: SourceLine, - previousLine?: SourceLine, - onClick: (SourceLine, HTMLElement) => void -}; -*/ - -export default class LineSCM extends React.PureComponent { - /*:: props: Props; */ - - handleClick = (e /*: SyntheticInputEvent */) => { - e.preventDefault(); - this.props.onClick(this.props.line, e.target); - }; - - isSCMChanged(s /*: SourceLine */, p /*: ?SourceLine */) { - let changed = true; - if (p != null && s.scmAuthor != null && p.scmAuthor != null) { - changed = s.scmAuthor !== p.scmAuthor || s.scmDate !== p.scmDate; - } - return changed; - } - - render() { - const { line, previousLine } = this.props; - const clickable = !!line.line; - return ( - - {this.isSCMChanged(line, previousLine) && ( -
- )} - - ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx new file mode 100644 index 00000000000..c1de61e6ee2 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx @@ -0,0 +1,80 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 * as React from 'react'; +import SCMPopup from './SCMPopup'; +import { SourceLine } from '../../../app/types'; +import BubblePopupHelper from '../../common/BubblePopupHelper'; + +interface Props { + line: SourceLine; + onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + popupOpen: boolean; + previousLine: SourceLine | undefined; +} + +export default class LineSCM extends React.PureComponent { + handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + event.currentTarget.blur(); + this.props.onPopupToggle({ line: this.props.line.line, name: 'scm' }); + }; + + handleTogglePopup = (open: boolean) => { + this.props.onPopupToggle({ line: this.props.line.line, name: 'scm', open }); + }; + + render() { + const { line, popupOpen, previousLine } = this.props; + const hasPopup = !!line.line; + const cell = isSCMChanged(line, previousLine) && ( +
+ ); + return hasPopup ? ( + + {cell} + } + position="bottomright" + togglePopup={this.handleTogglePopup} + /> + + ) : ( + + {cell} + + ); + } +} + +function isSCMChanged(s: SourceLine, p: SourceLine | undefined) { + let changed = true; + if (p != null && s.scmAuthor != null && p.scmAuthor != null) { + changed = s.scmAuthor !== p.scmAuthor || s.scmDate !== p.scmDate; + } + return changed; +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx new file mode 100644 index 00000000000..1a94d48e657 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 * as React from 'react'; +import { SourceLine } from '../../../app/types'; +import BubblePopup from '../../common/BubblePopup'; +import DateFormatter from '../../intl/DateFormatter'; + +interface Props { + line: SourceLine; + popupPosition?: any; +} + +export default function SCMPopup({ line, popupPosition }: Props) { + return ( + +
{line.scmAuthor}
+ {line.scmDate && ( +
+ +
+ )} + {line.scmRevision &&
{line.scmRevision}
} +
+ ); +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx similarity index 64% rename from server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js rename to server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx index 3dc8487e896..a9127eae0ce 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx @@ -17,9 +17,32 @@ * 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'; +import * as React from 'react'; import { shallow } from 'enzyme'; import LineCode from '../LineCode'; +import { Issue } from '../../../../app/types'; + +const issueBase: Issue = { + component: '', + componentLongName: '', + componentQualifier: '', + componentUuid: '', + creationDate: '', + key: '', + flows: [], + message: '', + organization: '', + project: '', + projectName: '', + projectOrganization: '', + projectUuid: '', + rule: '', + ruleName: '', + secondaryLocations: [], + severity: '', + status: '', + type: '' +}; it('render code', () => { const line = { @@ -29,15 +52,19 @@ it('render code', () => { const issueLocations = [{ from: 0, to: 5, line: 3 }]; const wrapper = shallow( diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx similarity index 50% rename from server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js rename to server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx index 35cac165cd4..a737504b3bd 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx @@ -17,30 +17,67 @@ * 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'; +import * as React from 'react'; import { shallow } from 'enzyme'; -import { click } from '../../../../helpers/testUtils'; import LineCoverage from '../LineCoverage'; +import { click } from '../../../../helpers/testUtils'; +import { SourceLine } from '../../../../app/types'; it('render covered line', () => { - const line = { line: 3, coverageStatus: 'covered' }; - const onClick = jest.fn(); - const wrapper = shallow(); + const line: SourceLine = { line: 3, coverageStatus: 'covered' }; + const wrapper = shallow( + + ); expect(wrapper).toMatchSnapshot(); click(wrapper.find('[tabIndex]')); - expect(onClick).toHaveBeenCalled(); }); it('render uncovered line', () => { - const line = { line: 3, coverageStatus: 'uncovered' }; - const onClick = jest.fn(); - const wrapper = shallow(); + const line: SourceLine = { line: 3, coverageStatus: 'uncovered' }; + const wrapper = shallow( + + ); expect(wrapper).toMatchSnapshot(); }); it('render line with unknown coverage', () => { - const line = { line: 3 }; - const onClick = jest.fn(); - const wrapper = shallow(); + const line: SourceLine = { line: 3 }; + const wrapper = shallow( + + ); expect(wrapper).toMatchSnapshot(); }); + +it('should open coverage popup', () => { + const line: SourceLine = { line: 3, coverageStatus: 'covered' }; + const onPopupToggle = jest.fn(); + const wrapper = shallow( + + ); + click(wrapper.find('[role="button"]')); + expect(onPopupToggle).toBeCalledWith({ line: 3, name: 'coverage' }); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.tsx similarity index 73% rename from server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js rename to server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.tsx index cc2d147cd06..0854c4c4f95 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.tsx @@ -17,27 +17,40 @@ * 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'; +import * as React from 'react'; import { shallow } from 'enzyme'; import { click } from '../../../../helpers/testUtils'; import LineDuplicationBlock from '../LineDuplicationBlock'; it('render duplicated line', () => { const line = { line: 3, duplicated: true }; - const onClick = jest.fn(); + const onPopupToggle = jest.fn(); const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); click(wrapper.find('[tabIndex]')); - expect(onClick).toHaveBeenCalled(); + expect(onPopupToggle).toHaveBeenCalled(); }); it('render not duplicated line', () => { const line = { line: 3, duplicated: false }; - const onClick = jest.fn(); const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.tsx similarity index 97% rename from server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js rename to server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.tsx index aaa463ec776..b5fac8ef47d 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.tsx @@ -17,7 +17,7 @@ * 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'; +import * as React from 'react'; import { shallow } from 'enzyme'; import { click } from '../../../../helpers/testUtils'; import LineDuplications from '../LineDuplications'; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.tsx similarity index 72% rename from server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.js rename to server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.tsx index 9f22629af08..b4fd9fc6e70 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.tsx @@ -17,14 +17,40 @@ * 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'; +import * as React from 'react'; import { shallow } from 'enzyme'; import { click } from '../../../../helpers/testUtils'; import LineIssuesIndicator from '../LineIssuesIndicator'; +import { Issue } from '../../../../app/types'; + +const issueBase: Issue = { + component: '', + componentLongName: '', + componentQualifier: '', + componentUuid: '', + creationDate: '', + key: '', + flows: [], + message: '', + organization: '', + project: '', + projectName: '', + projectOrganization: '', + projectUuid: '', + rule: '', + ruleName: '', + secondaryLocations: [], + severity: '', + status: '', + type: '' +}; it('render highest severity', () => { const line = { line: 3 }; - const issues = [{ severity: 'MINOR' }, { severity: 'CRITICAL' }]; + const issues = [ + { ...issueBase, key: 'foo', severity: 'MINOR' }, + { ...issueBase, key: 'bar', severity: 'CRITICAL' } + ]; const onClick = jest.fn(); const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); @@ -39,7 +65,7 @@ it('render highest severity', () => { it('no issues', () => { const line = { line: 3 }; - const issues = []; + const issues: Issue[] = []; const onClick = jest.fn(); const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.tsx new file mode 100644 index 00000000000..8ab04fcda21 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.tsx @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import LineIssuesList from '../LineIssuesList'; +import { Issue } from '../../../../app/types'; + +const issueBase: Issue = { + component: '', + componentLongName: '', + componentQualifier: '', + componentUuid: '', + creationDate: '', + key: '', + flows: [], + message: '', + organization: '', + project: '', + projectName: '', + projectOrganization: '', + projectUuid: '', + rule: '', + ruleName: '', + secondaryLocations: [], + severity: '', + status: '', + type: '' +}; + +it('render issues list', () => { + const issues: Issue[] = [{ ...issueBase, key: 'foo' }, { ...issueBase, key: 'bar' }]; + const onIssueClick = jest.fn(); + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.tsx similarity index 75% rename from server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.js rename to server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.tsx index 2afee7b5898..bf40ca900c8 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.tsx @@ -17,23 +17,36 @@ * 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'; +import * as React from 'react'; import { shallow } from 'enzyme'; import { click } from '../../../../helpers/testUtils'; import LineNumber from '../LineNumber'; it('render line 3', () => { const line = { line: 3 }; - const onClick = jest.fn(); - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper).toMatchSnapshot(); click(wrapper); - expect(onClick).toHaveBeenCalled(); }); it('render line 0', () => { const line = { line: 0 }; - const onClick = jest.fn(); - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineOptionsPopup-test.tsx similarity index 69% rename from server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js rename to server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineOptionsPopup-test.tsx index 354c3deab9d..a5de2ec635e 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineOptionsPopup-test.tsx @@ -17,23 +17,12 @@ * 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'; +import * as React from 'react'; import { shallow } from 'enzyme'; -import LineIssuesList from '../LineIssuesList'; +import LineOptionsPopup from '../LineOptionsPopup'; -it('render issues list', () => { +it('should render', () => { const line = { line: 3 }; - const issues = [{ key: 'foo' }, { key: 'bar' }]; - const onIssueClick = jest.fn(); - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx similarity index 66% rename from server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.js rename to server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx index 99db276beba..29e82eb3d61 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx @@ -17,39 +17,43 @@ * 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'; +import * as React from 'react'; import { shallow } from 'enzyme'; -import { click } from '../../../../helpers/testUtils'; import LineSCM from '../LineSCM'; +import { click } from '../../../../helpers/testUtils'; it('render scm details', () => { const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; const previousLine = { line: 2, scmAuthor: 'bar', scmDate: '2017-01-02' }; - const onClick = jest.fn(); - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper).toMatchSnapshot(); - click(wrapper); - expect(onClick).toHaveBeenCalled(); }); it('render scm details for the first line', () => { const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; - const onClick = jest.fn(); - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper).toMatchSnapshot(); }); it('does not render scm details', () => { const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; const previousLine = { line: 2, scmAuthor: 'foo', scmDate: '2017-01-01' }; - const onClick = jest.fn(); - const wrapper = shallow(); + const wrapper = shallow( + + ); expect(wrapper).toMatchSnapshot(); }); -it('does not allow to click', () => { - const line = { scmAuthor: 'foo', scmDate: '2017-01-01' }; - const onClick = jest.fn(); - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); +it('should open popup', () => { + const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; + const onPopupToggle = jest.fn(); + const wrapper = shallow( + + ); + click(wrapper.find('[role="button"]')); + expect(onPopupToggle).toBeCalledWith({ line: 3, name: 'scm' }); }); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/types.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/SCMPopup-test.tsx similarity index 68% rename from server/sonar-web/src/main/js/components/SourceViewer/types.js rename to server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/SCMPopup-test.tsx index 63b638a93e6..6a6fba2acc3 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/types.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/SCMPopup-test.tsx @@ -17,28 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -/*:: -export type SourceLine = { - code: string, - conditions?: number, - coverageStatus?: string | null, - coveredConditions?: number, - duplicated: boolean, - line: number, - lineHits?: number, - scmAuthor?: string, - scmDate?: string, - scmRevision?: string -}; -*/ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import SCMPopup from '../SCMPopup'; -/*:: -export type Duplication = { - blocks: Array<{ - _ref: string, - from: number, - size: number - }> -}; -*/ +it('should render', () => { + const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; + expect(shallow()).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap deleted file mode 100644 index 1cb641e3679..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap +++ /dev/null @@ -1,55 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render code 1`] = ` - -
-
-      
-        class
-      
-      
-         
-      
-      
-        Foo
-      
-      
-         {
-      
-    
-
- - -`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap new file mode 100644 index 00000000000..f5e24cd2736 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap @@ -0,0 +1,92 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render code 1`] = ` + +
+
+      
+        class
+      
+      
+         
+      
+      
+        Foo
+      
+      
+         {
+      
+    
+
+ + +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap deleted file mode 100644 index abef0b03565..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap +++ /dev/null @@ -1,47 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render covered line 1`] = ` - - -
- - -`; - -exports[`render line with unknown coverage 1`] = ` - -
- -`; - -exports[`render uncovered line 1`] = ` - - -
- - -`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap new file mode 100644 index 00000000000..664a60ddf64 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render covered line 1`] = ` + + +
+ + + } + position="bottomright" + togglePopup={[Function]} + /> + +`; + +exports[`render line with unknown coverage 1`] = ` + +
+ +`; + +exports[`render uncovered line 1`] = ` + + +
+ + +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap deleted file mode 100644 index c8a4ee23981..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render duplicated line 1`] = ` - - -
- - -`; - -exports[`render not duplicated line 1`] = ` - -
- -`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap new file mode 100644 index 00000000000..9ea77716f15 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render duplicated line 1`] = ` + + +
+ + + +`; + +exports[`render not duplicated line 1`] = ` + +
+ +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.tsx.snap similarity index 100% rename from server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap rename to server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.tsx.snap diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.tsx.snap similarity index 96% rename from server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap rename to server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.tsx.snap index 5636d7a0381..e941c781cac 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.tsx.snap @@ -13,7 +13,7 @@ exports[`render highest severity 1`] = ` data-line-number={3} onClick={[Function]} role="button" - tabIndex="0" + tabIndex={0} > - - -
-`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.tsx.snap new file mode 100644 index 00000000000..d19a4e96ef0 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.tsx.snap @@ -0,0 +1,72 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render issues list 1`] = ` +
+ + +
+`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.js.snap deleted file mode 100644 index 86e1095667a..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.js.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render line 0 1`] = ` - -`; - -exports[`render line 3 1`] = ` - -`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap new file mode 100644 index 00000000000..477d94d1c3f --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render line 0 1`] = ` + +`; + +exports[`render line 3 1`] = ` + + + } + position="bottomright" + togglePopup={[Function]} + /> + +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap new file mode 100644 index 00000000000..13d7b6ee859 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` + +
+ + component_viewer.get_permalink + +
+
+`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.js.snap deleted file mode 100644 index ce8c0369fb1..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.js.snap +++ /dev/null @@ -1,52 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`does not allow to click 1`] = ` - -
- -`; - -exports[`does not render scm details 1`] = ` - -`; - -exports[`render scm details 1`] = ` - -
- -`; - -exports[`render scm details for the first line 1`] = ` - -
- -`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap new file mode 100644 index 00000000000..dcbaf7e2c9c --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap @@ -0,0 +1,90 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`does not render scm details 1`] = ` + + + } + position="bottomright" + togglePopup={[Function]} + /> + +`; + +exports[`render scm details 1`] = ` + +
+ + } + position="bottomright" + togglePopup={[Function]} + /> + +`; + +exports[`render scm details for the first line 1`] = ` + +
+ + } + position="bottomright" + togglePopup={[Function]} + /> + +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap new file mode 100644 index 00000000000..d8460698802 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` + +
+ foo +
+
+ +
+
+`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.ts similarity index 99% rename from server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.js rename to server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.ts index f252b171782..47900d5ef8f 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.ts @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow import { highlightSymbol } from '../highlight'; describe('highlightSymbol', () => { diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.ts similarity index 100% rename from server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.js rename to server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.ts diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.tsx similarity index 87% rename from server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.js rename to server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.tsx index 5a0ba749ca0..88fd50b2f1d 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.tsx @@ -17,11 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -/*:: import type { SourceLine } from '../types'; */ +import { SourceLine } from '../../../app/types'; -export default function getCoverageStatus(s /*: SourceLine */) /*: string | null */ { - let status = null; +export default function getCoverageStatus(s: SourceLine): string | undefined { + let status: string | undefined; if (s.lineHits != null && s.lineHits > 0) { status = 'partially-covered'; } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.ts similarity index 76% rename from server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js rename to server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.ts index afdd574fbb7..2f105f74884 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.ts @@ -17,30 +17,29 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import escapeHtml from 'escape-html'; import { uniq } from 'lodash'; +import { LinearIssueLocation } from '../../../app/types'; -/*:: -export type Token = { className: string, markers: Array, text: string }; -*/ -/*:: -export type Tokens = Array; */ +export interface Token { + className: string; + markers: number[]; + text: string; +} const ISSUE_LOCATION_CLASS = 'source-line-code-issue'; -export function splitByTokens(code /*: string */, rootClassName /*: string */ = '') /*: Tokens */ { +export function splitByTokens(code: string, rootClassName = ''): Token[] { const container = document.createElement('div'); - let tokens = []; + let tokens: Token[] = []; container.innerHTML = code; - [].forEach.call(container.childNodes, node => { + [].forEach.call(container.childNodes, (node: Element) => { if (node.nodeType === 1) { // ELEMENT NODE const fullClassName = rootClassName ? rootClassName + ' ' + node.className : node.className; const innerTokens = splitByTokens(node.innerHTML, fullClassName); tokens = tokens.concat(innerTokens); } - if (node.nodeType === 3) { + if (node.nodeType === 3 && node.nodeValue) { // TEXT NODE tokens.push({ className: rootClassName, markers: [], text: node.nodeValue }); } @@ -48,7 +47,7 @@ export function splitByTokens(code /*: string */, rootClassName /*: string */ = return tokens; } -export function highlightSymbol(tokens /*: Tokens */, symbol /*: string */) /*: Tokens */ { +export function highlightSymbol(tokens: Token[], symbol: string): Token[] { const symbolRegExp = new RegExp(`\\b${symbol}\\b`); return tokens.map( token => @@ -65,12 +64,7 @@ export function highlightSymbol(tokens /*: Tokens */, symbol /*: string */) /*: * @param s2 Start position of the second range * @param e2 End position of the second range */ -function intersect( - s1 /*: number */, - e1 /*: number */, - s2 /*: number */, - e2 /*: number */ -) /*: { from: number, to: number } */ { +function intersect(s1: number, e1: number, s2: number, e2: number) { return { from: Math.max(s1, s2), to: Math.min(e1, e2) }; } @@ -81,12 +75,7 @@ function intersect( * @param to "To" offset * @param acc Global offset to eliminate */ -function part( - str /*: string */, - from /*: number */, - to /*: number */, - acc /*: number */ -) /*: string */ { +function part(str: string, from: number, to: number, acc: number): string { // we do not want negative number as the first argument of `substr` return from >= acc ? str.substr(from - acc, to - from) : str.substr(0, to - from); } @@ -95,12 +84,12 @@ function part( * Highlight issue locations in the list of tokens */ export function highlightIssueLocations( - tokens /*: Tokens */, - issueLocations /*: Array<*> */, - rootClassName /*: string */ = ISSUE_LOCATION_CLASS -) /*: Tokens */ { + tokens: Token[], + issueLocations: LinearIssueLocation[], + rootClassName: string = ISSUE_LOCATION_CLASS +): Token[] { issueLocations.forEach(location => { - const nextTokens = []; + const nextTokens: Token[] = []; let acc = 0; let markerAdded = location.line !== location.startLine; tokens.forEach(token => { @@ -135,9 +124,3 @@ export function highlightIssueLocations( }); return tokens; } - -export function generateHTML(tokens /*: Tokens */) /*: string */ { - return tokens - .map(token => `${escapeHtml(token.text)}`) - .join(''); -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.ts similarity index 70% rename from server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js rename to server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.ts index c9364268e23..bf103f0cf34 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.ts @@ -17,40 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow import { flatten } from 'lodash'; import { splitByTokens } from './highlight'; import { getLinearLocations } from './issueLocations'; -/*:: import type { Issue } from '../../issue/types'; */ -/*:: import type { SourceLine } from '../types'; */ +import { Duplication, Issue, LinearIssueLocation, SourceLine } from '../../../app/types'; -/*:: -export type LinearIssueLocation = { - from: number, - line: number, - to: number, - index?: number -}; -*/ - -/*:: -export type IndexedIssueLocation = { - from: number, - line: number, - to: number -}; -*/ - -/*:: -export type IndexedIssueLocationMessage = { - flowIndex: number, - locationIndex: number, - msg?: string -}; -*/ - -export const issuesByLine = (issues /*: Array */) => { - const index = {}; +export function issuesByLine(issues: Issue[]) { + const index: { [line: number]: Issue[] } = {}; issues.forEach(issue => { const line = issue.textRange ? issue.textRange.endLine : 0; if (!(line in index)) { @@ -59,12 +32,10 @@ export const issuesByLine = (issues /*: Array */) => { index[line].push(issue); }); return index; -}; +} -export function locationsByLine( - issues /*: Array */ -) /*: { [number]: Array } */ { - const index = {}; +export function locationsByLine(issues: Issue[]) { + const index: { [line: number]: LinearIssueLocation[] } = {}; issues.forEach(issue => { getLinearLocations(issue.textRange).forEach(location => { if (!(location.line in index)) { @@ -76,15 +47,16 @@ export function locationsByLine( return index; } -export const duplicationsByLine = (duplications /*: Array<*> | null */) => { +export function duplicationsByLine(duplications: Duplication[] | undefined) { if (duplications == null) { return {}; } - const duplicationsByLine = {}; + const duplicationsByLine: { [line: number]: number[] } = {}; duplications.forEach(({ blocks }, duplicationIndex) => { blocks.forEach(block => { + // eslint-disable-next-line no-underscore-dangle if (block._ref === '1') { for (let line = block.from; line < block.from + block.size; line++) { if (!(line in duplicationsByLine)) { @@ -97,12 +69,12 @@ export const duplicationsByLine = (duplications /*: Array<*> | null */) => { }); return duplicationsByLine; -}; +} -export const symbolsByLine = (sources /*: Array */) => { - const index = {}; +export function symbolsByLine(sources: SourceLine[]) { + const index: { [line: number]: string[] } = {}; sources.forEach(line => { - const tokens = splitByTokens(line.code); + const tokens = splitByTokens(line.code || ''); const symbols = flatten( tokens.map(token => { const keys = token.className.match(/sym-\d+/g); @@ -112,4 +84,4 @@ export const symbolsByLine = (sources /*: Array */) => { index[line.line] = symbols.filter(key => key); }); return index; -}; +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js deleted file mode 100644 index 2e47fa64e8b..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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. - */ -// @flow -/*:: import type { TextRange, Issue } from '../../issue/types'; */ - -export function getLinearLocations( - textRange /*: ?TextRange */ -) /*: Array<{ line: number, from: number, to: number }> */ { - if (!textRange) { - return []; - } - const locations = []; - - // go through all lines of the `textRange` - for (let line = textRange.startLine; line <= textRange.endLine; line++) { - // TODO fix 999999 - const from = line === textRange.startLine ? textRange.startOffset : 0; - const to = line === textRange.endLine ? textRange.endOffset : 999999; - locations.push({ line, from, to }); - } - return locations; -} - -/*:: -type Location = { - msg: string, - flowIndex: number, - locationIndex: number, - textRange?: TextRange, - index?: number -} -*/ - -export function getIssueLocations(issue /*: Issue */) /*: Array */ { - const allLocations = []; - issue.flows.forEach((locations, flowIndex) => { - if (locations) { - const locationsCount = locations.length; - locations.forEach((location, index) => { - const flowLocation = { - ...location, - flowIndex, - locationIndex: index, - // set index only for real flows, do not set for just secondary locations - index: locationsCount > 1 ? locationsCount - index : undefined - }; - allLocations.push(flowLocation); - }); - } - }); - return allLocations; -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.tsx similarity index 59% rename from server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js rename to server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.tsx index 5478c6fc533..5c0f7ee9616 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.tsx @@ -17,29 +17,20 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import Template from './templates/source-viewer-scm-popup.hbs'; -import Popup from '../../common/popup'; +import { LinearIssueLocation, TextRange } from '../../../app/types'; -export default Popup.extend({ - template: Template, - - events: { - click: 'onClick' - }, - - onRender() { - Popup.prototype.onRender.apply(this, arguments); - this.$('.bubble-popup-container').isolatedScroll(); - }, - - onClick(e) { - e.stopPropagation(); - }, +export function getLinearLocations(textRange: TextRange | undefined): LinearIssueLocation[] { + if (!textRange) { + return []; + } + const locations = []; - serializeData() { - return { - ...Popup.prototype.serializeData.apply(this, arguments), - line: this.options.line - }; + // go through all lines of the `textRange` + for (let line = textRange.startLine; line <= textRange.endLine; line++) { + // TODO fix 999999 + const from = line === textRange.startLine ? textRange.startOffset : 0; + const to = line === textRange.endLine ? textRange.endOffset : 999999; + locations.push({ line, from, to }); } -}); + return locations; +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx similarity index 77% rename from server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js rename to server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx index f1950d1f583..8354099e6f3 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx @@ -17,21 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow import { searchIssues } from '../../../api/issues'; +import { Issue } from '../../../app/types'; import { parseIssueFromResponse } from '../../../helpers/issues'; - -/*:: -export type Query = { [string]: string | void }; -*/ - -/*:: -export type Issues = Array<*>; */ +import { RawQuery } from '../../../helpers/query'; // maximum possible value const PAGE_SIZE = 500; -function buildQuery(component /*: string */, branch /*: string | void */) /*: Query */ { +function buildQuery(component: string, branch: string | undefined) { return { additionalFields: '_all', resolved: 'false', @@ -41,11 +35,7 @@ function buildQuery(component /*: string */, branch /*: string | void */) /*: Qu }; } -export function loadPage( - query /*: Query */, - page /*: number */, - pageSize /*: number */ = PAGE_SIZE -) /*: Promise */ { +export function loadPage(query: RawQuery, page: number, pageSize = PAGE_SIZE): Promise { return searchIssues({ ...query, p: page, @@ -56,11 +46,11 @@ export function loadPage( } export function loadPageAndNext( - query /*: Query */, - toLine /*: number */, - page /*: number */, - pageSize /*: number */ = PAGE_SIZE -) /*: Promise */ { + query: RawQuery, + toLine: number, + page: number, + pageSize = PAGE_SIZE +): Promise { return loadPage(query, page).then(issues => { if (issues.length === 0) { return []; @@ -82,11 +72,11 @@ export function loadPageAndNext( } export default function loadIssues( - component /*: string */, - fromLine /*: number */, - toLine /*: number */, - branch /*: string | void */ -) /*: Promise */ { + component: string, + _fromLine: number, + toLine: number, + branch: string | undefined +): Promise { const query = buildQuery(component, branch); return new Promise(resolve => { loadPageAndNext(query, toLine, 1).then(issues => { diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js deleted file mode 100644 index 694182c0c60..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 $ from 'jquery'; -import { groupBy } from 'lodash'; -import Template from './templates/source-viewer-coverage-popup.hbs'; -import Popup from '../../common/popup'; - -export default Popup.extend({ - template: Template, - - events: { - 'click a[data-key]': 'goToFile' - }, - - onRender() { - Popup.prototype.onRender.apply(this, arguments); - this.$('.bubble-popup-container').isolatedScroll(); - }, - - goToFile(e) { - e.stopPropagation(); - const key = $(e.currentTarget).data('key'); - const Workspace = require('../../workspace/main').default; - Workspace.openComponent({ key, branch: this.options.branch }); - }, - - serializeData() { - const row = this.options.line || {}; - const tests = groupBy(this.options.tests, 'fileKey'); - const testFiles = Object.keys(tests).map(fileKey => { - const testSet = tests[fileKey]; - const test = testSet[0]; - return { - file: { - key: test.fileKey, - longName: test.fileName - }, - tests: testSet - }; - }); - return { testFiles, row }; - } -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js deleted file mode 100644 index f45a5dc25b2..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 $ from 'jquery'; -import { groupBy, sortBy } from 'lodash'; -import Template from './templates/source-viewer-duplication-popup.hbs'; -import Popup from '../../common/popup'; - -export default Popup.extend({ - template: Template, - - events: { - 'click a[data-key]': 'goToFile' - }, - - goToFile(e) { - e.stopPropagation(); - const key = $(e.currentTarget).data('key'); - const line = $(e.currentTarget).data('line'); - const Workspace = require('../../workspace/main').default; - Workspace.openComponent({ key, line, branch: this.options.branch }); - }, - - serializeData() { - const that = this; - const groupedBlocks = groupBy(this.options.blocks, '_ref'); - let duplications = Object.keys(groupedBlocks).map(fileRef => { - return { - blocks: groupedBlocks[fileRef], - file: this.options.files[fileRef] - }; - }); - duplications = sortBy(duplications, d => { - const a = d.file.projectName !== that.options.component.projectName; - const b = d.file.subProjectName !== that.options.component.subProjectName; - const c = d.file.key !== that.options.component.key; - return '' + a + b + c; - }); - return { - duplications, - component: this.options.component, - inRemovedComponent: this.options.inRemovedComponent - }; - } -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs deleted file mode 100644 index bad2779d285..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs +++ /dev/null @@ -1,33 +0,0 @@ -
-
- {{t 'source_viewer.covered'}} - {{#if row.conditions}} - ({{default row.coveredConditions 0}} of {{row.conditions}} {{t 'source_viewer.conditions'}}) - {{/if}} -
- {{#each testFiles}} -
- - {{collapsePath file.longName}} - -
    - {{#each tests}} -
  • - - - - {{name}} - - - {{durationInMs}}ms -
  • - {{/each}} -
-
- {{else}} - {{t 'source_viewer.tooltip.no_information_about_tests'}} - {{/each}} -
- -
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs deleted file mode 100644 index ea8fc2b2349..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs +++ /dev/null @@ -1,45 +0,0 @@ -
- {{#if inRemovedComponent}} -
{{t 'duplications.dups_found_on_deleted_resource'}}
- {{/if}} - {{#notEmpty duplications}} -
{{t 'component_viewer.transition.duplication'}}
- {{#each duplications}} -
-
- {{#notEqComponents file ../component}} - - {{#if file.subProjectName}} - - {{/if}} - {{/notEqComponents}} - - {{#notEq file.key ../component.key}} - - {{/notEq}} - -
- Lines: - {{#joinEach blocks ','}} - - {{this.from}} – {{sum from size -1}} - - {{/joinEach}} -
-
-
- {{/each}} - {{/notEmpty}} -
- -
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs deleted file mode 100644 index cefd31210bb..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs +++ /dev/null @@ -1,7 +0,0 @@ - - -
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs deleted file mode 100644 index dd82aca528c..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs +++ /dev/null @@ -1,15 +0,0 @@ -
-
- {{line.scmAuthor}} -
-
- {{dt line.scmDate}} -
- {{#if line.scmRevision}} -
- {{line.scmRevision}} -
- {{/if}} -
- -
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/styles.css b/server/sonar-web/src/main/js/components/SourceViewer/styles.css index 3bcdcdf4415..73521086ad3 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/styles.css +++ b/server/sonar-web/src/main/js/components/SourceViewer/styles.css @@ -94,7 +94,8 @@ } .source-viewer pre, -.source-meta { +.source-line-number, +.source-line-scm { line-height: 18px; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: var(--smallFontSize); @@ -134,6 +135,7 @@ } .source-meta { + position: relative; vertical-align: top; width: 1px; background-clip: padding-box; @@ -501,6 +503,15 @@ border-top: 1px solid var(--barBorderColor); } +.source-viewer-bubble-popup { + top: -16px; + left: 100%; + width: 480px; + font-family: var(--baseFontFamily); + font-size: var(--baseFontSize); + text-align: left; +} + .issue-location { display: inline-block; vertical-align: top; diff --git a/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx b/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx index 2cfb88cee4c..627dad9cde5 100644 --- a/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx +++ b/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx @@ -25,7 +25,7 @@ interface Props { children?: React.ReactNode; isOpen: boolean; offset?: { vertical: number; horizontal: number }; - popup: React.ReactElement; + popup: JSX.Element; position: 'bottomleft' | 'bottomright'; togglePopup: (show: boolean) => void; } @@ -92,10 +92,10 @@ export default class BubblePopupHelper extends React.PureComponent return (
(this.container = container)} onClick={this.handleClick} - tabIndex={0} - role="tooltip"> + ref={container => (this.container = container)} + role="tooltip" + tabIndex={0}> {this.props.children} {this.props.isOpen && (
(this.popupContainer = popupContainer)}> diff --git a/server/sonar-web/src/main/js/components/common/LocationIndex.css b/server/sonar-web/src/main/js/components/common/LocationIndex.css index 21e7535b690..97bdefa079a 100644 --- a/server/sonar-web/src/main/js/components/common/LocationIndex.css +++ b/server/sonar-web/src/main/js/components/common/LocationIndex.css @@ -27,7 +27,7 @@ border-radius: 2px; background-color: #d18582; color: #fff; - font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif; + font-family: var(--baseFontFamily); font-size: var(--smallFontSize); transition: background-color 0.3s ease; } diff --git a/server/sonar-web/src/main/js/components/common/LocationMessage.css b/server/sonar-web/src/main/js/components/common/LocationMessage.css index 19a9af84071..29fa3a17c3f 100644 --- a/server/sonar-web/src/main/js/components/common/LocationMessage.css +++ b/server/sonar-web/src/main/js/components/common/LocationMessage.css @@ -25,7 +25,7 @@ border-radius: 2px; background-color: #9e9e9e; color: #fff; - font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif; + font-family: var(--baseFontFamily); font-size: var(--smallFontSize); text-overflow: ellipsis; overflow: hidden; diff --git a/server/sonar-web/src/main/js/components/common/popup.js b/server/sonar-web/src/main/js/components/common/popup.js deleted file mode 100644 index b9a988b1a9a..00000000000 --- a/server/sonar-web/src/main/js/components/common/popup.js +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 $ from 'jquery'; -import Marionette from 'backbone.marionette'; -import key from 'keymaster'; - -export default Marionette.ItemView.extend({ - className: 'bubble-popup', - - onRender() { - this.$el.detach().appendTo($('body')); - const triggerEl = $(this.options.triggerEl); - if (this.options.bottom) { - this.$el.addClass('bubble-popup-bottom'); - this.$el.css({ - top: triggerEl.offset().top + triggerEl.outerHeight(), - left: triggerEl.offset().left - }); - } else if (this.options.bottomRight) { - this.$el.addClass('bubble-popup-bottom-right'); - this.$el.css({ - top: triggerEl.offset().top + triggerEl.outerHeight(), - right: $(window).width() - triggerEl.offset().left - triggerEl.outerWidth() - }); - } else { - this.$el.css({ - top: triggerEl.offset().top, - left: triggerEl.offset().left + triggerEl.outerWidth() - }); - } - this.attachCloseEvents(); - }, - - attachCloseEvents() { - const that = this; - const triggerEl = $(this.options.triggerEl); - key('escape', () => { - that.destroy(); - }); - $('body').on('click.bubble-popup', () => { - $('body').off('click.bubble-popup'); - that.destroy(); - }); - triggerEl.on('click.bubble-popup', e => { - triggerEl.off('click.bubble-popup'); - e.stopPropagation(); - that.destroy(); - }); - }, - - onDestroy() { - $('body').off('click.bubble-popup'); - const triggerEl = $(this.options.triggerEl); - triggerEl.off('click.bubble-popup'); - } -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js b/server/sonar-web/src/main/js/components/issue/Issue.d.ts similarity index 58% rename from server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js rename to server/sonar-web/src/main/js/components/issue/Issue.d.ts index 33d44cfb1f5..5abf041951b 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js +++ b/server/sonar-web/src/main/js/components/issue/Issue.d.ts @@ -17,19 +17,22 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import Template from './templates/source-viewer-line-options-popup.hbs'; -import Popup from '../../common/popup'; +import * as React from 'react'; +import { Issue as IssueType } from '../../app/types'; -export default Popup.extend({ - template: Template, +interface IssueProps { + branch?: string; + checked?: boolean; + displayLocationsCount?: boolean; + displayLocationsLink?: boolean; + issue: IssueType; + onChange: (issue: IssueType) => void; + onCheck?: (issueKey: string) => void; + onClick: (issueKey: string) => void; + onFilter?: (property: string, issue: IssueType) => void; + onPopupToggle: (issue: string, popupName: string, open?: boolean) => void; + openPopup?: string; + selected: boolean; +} - serializeData() { - const { component, line, branch } = this.options; - let permalink = - window.baseUrl + `/component?id=${encodeURIComponent(component.key)}&line=${line}`; - if (branch) { - permalink += `&branch=${encodeURIComponent(branch)}`; - } - return { permalink }; - } -}); +export default class Issue extends React.PureComponent {} diff --git a/server/sonar-web/src/main/js/helpers/issues.ts b/server/sonar-web/src/main/js/helpers/issues.ts index e7910c837e3..a1ccedd665c 100644 --- a/server/sonar-web/src/main/js/helpers/issues.ts +++ b/server/sonar-web/src/main/js/helpers/issues.ts @@ -19,6 +19,7 @@ */ import { flatten, sortBy } from 'lodash'; import { SEVERITIES } from './constants'; +import { Issue } from '../app/types'; interface TextRange { startLine: number; @@ -67,8 +68,6 @@ export interface RawIssue extends IssueBase { textRange?: TextRange; } -interface Issue extends IssueBase {} - export function sortBySeverity(issues: Issue[]): Issue[] { return sortBy(issues, issue => SEVERITIES.indexOf(issue.severity)); } @@ -173,5 +172,5 @@ export function parseIssueFromResponse( ...ensureTextRange(issue), secondaryLocations, flows - }; + } as Issue; } diff --git a/server/sonar-web/src/main/js/store/favorites/duck.js b/server/sonar-web/src/main/js/store/favorites/duck.ts similarity index 50% rename from server/sonar-web/src/main/js/store/favorites/duck.js rename to server/sonar-web/src/main/js/store/favorites/duck.ts index 3015e256953..710151e4eb2 100644 --- a/server/sonar-web/src/main/js/store/favorites/duck.js +++ b/server/sonar-web/src/main/js/store/favorites/duck.ts @@ -17,92 +17,65 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow import { uniq, without } from 'lodash'; -/*:: -type Favorite = { key: string }; -*/ +interface Favorite { + key: string; +} -/*:: -type ReceiveFavoritesAction = { - type: 'RECEIVE_FAVORITES', - favorites: Array, - notFavorites: Array -}; -*/ +interface ReceiveFavoritesAction { + type: 'RECEIVE_FAVORITES'; + favorites: Array; + notFavorites: Array; +} -/*:: -type AddFavoriteAction = { - type: 'ADD_FAVORITE', - componentKey: string -}; -*/ +interface AddFavoriteAction { + type: 'ADD_FAVORITE'; + componentKey: string; +} -/*:: -type RemoveFavoriteAction = { - type: 'REMOVE_FAVORITE', - componentKey: string -}; -*/ +interface RemoveFavoriteAction { + type: 'REMOVE_FAVORITE'; + componentKey: string; +} -/*:: type Action = ReceiveFavoritesAction | AddFavoriteAction | RemoveFavoriteAction; -*/ - -/*:: -type State = Array; -*/ -export const actions = { - RECEIVE_FAVORITES: 'RECEIVE_FAVORITES', - ADD_FAVORITE: 'ADD_FAVORITE', - REMOVE_FAVORITE: 'REMOVE_FAVORITE' -}; +type State = string[]; export function receiveFavorites( - favorites /*: Array */, - notFavorites /*: Array */ = [] -) /*: ReceiveFavoritesAction */ { - return { - type: actions.RECEIVE_FAVORITES, - favorites, - notFavorites - }; + favorites: Favorite[], + notFavorites: Favorite[] = [] +): ReceiveFavoritesAction { + return { type: 'RECEIVE_FAVORITES', favorites, notFavorites }; } -export function addFavorite(componentKey /*: string */) /*: AddFavoriteAction */ { - return { - type: actions.ADD_FAVORITE, - componentKey - }; +export function addFavorite(componentKey: string): AddFavoriteAction { + return { type: 'ADD_FAVORITE', componentKey }; } -export function removeFavorite(componentKey /*: string */) /*: RemoveFavoriteAction */ { - return { - type: actions.REMOVE_FAVORITE, - componentKey - }; +export function removeFavorite(componentKey: string): RemoveFavoriteAction { + return { type: 'REMOVE_FAVORITE', componentKey }; } -export default function(state /*: State */ = [], action /*: Action */) /*: State */ { - if (action.type === actions.RECEIVE_FAVORITES) { +export default function(state: State = [], action: Action): State { + if (action.type === 'RECEIVE_FAVORITES') { const toAdd = action.favorites.map(f => f.key); const toRemove = action.notFavorites.map(f => f.key); return without(uniq([...state, ...toAdd]), ...toRemove); } - if (action.type === actions.ADD_FAVORITE) { + if (action.type === 'ADD_FAVORITE') { return uniq([...state, action.componentKey]); } - if (action.type === actions.REMOVE_FAVORITE) { + if (action.type === 'REMOVE_FAVORITE') { return without(state, action.componentKey); } return state; } -export function isFavorite(state /*: State */, componentKey /*: string */) { +export function isFavorite(state: State, componentKey: string) { return state.includes(componentKey); } -- 2.39.5