]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10374 Support pull request in the web app
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Mon, 12 Mar 2018 11:06:11 +0000 (12:06 +0100)
committerTeryk Bellahsene <teryk@users.noreply.github.com>
Tue, 13 Mar 2018 13:05:36 +0000 (14:05 +0100)
175 files changed:
server/sonar-web/src/main/js/api/branches.ts
server/sonar-web/src/main/js/api/components.ts
server/sonar-web/src/main/js/api/measures.ts
server/sonar-web/src/main/js/api/nav.ts
server/sonar-web/src/main/js/api/projectActivity.ts
server/sonar-web/src/main/js/api/settings.ts
server/sonar-web/src/main/js/api/tests.ts
server/sonar-web/src/main/js/api/time-machine.ts
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/app/components/ProjectAdminContainer.tsx
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap
server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/about/actions.js
server/sonar-web/src/main/js/apps/account/notifications/__tests__/__snapshots__/ProjectNotifications-test.js.snap
server/sonar-web/src/main/js/apps/account/organizations/actions.ts
server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx
server/sonar-web/src/main/js/apps/background-tasks/components/__tests__/__snapshots__/TaskComponent-test.tsx.snap
server/sonar-web/src/main/js/apps/background-tasks/types.ts
server/sonar-web/src/main/js/apps/code/components/App.tsx
server/sonar-web/src/main/js/apps/code/components/Breadcrumbs.tsx
server/sonar-web/src/main/js/apps/code/components/Component.tsx
server/sonar-web/src/main/js/apps/code/components/ComponentLink.tsx
server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx
server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx
server/sonar-web/src/main/js/apps/code/components/Components.tsx
server/sonar-web/src/main/js/apps/code/components/Search.tsx
server/sonar-web/src/main/js/apps/code/utils.ts
server/sonar-web/src/main/js/apps/component-measures/components/App.js
server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js
server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js
server/sonar-web/src/main/js/apps/component-measures/components/MeasureFavoriteContainer.js
server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.js
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverviewContainer.js
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap
server/sonar-web/src/main/js/apps/component-measures/drilldown/CodeView.js
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsList.js
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js
server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js
server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.js
server/sonar-web/src/main/js/apps/component/components/App.tsx
server/sonar-web/src/main/js/apps/component/components/__tests__/__snapshots__/App-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/components/App.d.ts
server/sonar-web/src/main/js/apps/issues/components/App.js
server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx
server/sonar-web/src/main/js/apps/issues/components/IssuesList.js
server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js
server/sonar-web/src/main/js/apps/issues/components/ListItem.js
server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap
server/sonar-web/src/main/js/apps/overview/badges/BadgesModal.tsx
server/sonar-web/src/main/js/apps/overview/badges/__tests__/BadgesModal-test.tsx
server/sonar-web/src/main/js/apps/overview/badges/utils.ts
server/sonar-web/src/main/js/apps/overview/components/App.tsx
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx
server/sonar-web/src/main/js/apps/overview/events/__tests__/AnalysesList-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/AnalysesList-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.tsx
server/sonar-web/src/main/js/apps/overview/main/CodeSmells.tsx
server/sonar-web/src/main/js/apps/overview/main/Coverage.tsx
server/sonar-web/src/main/js/apps/overview/main/Duplications.tsx
server/sonar-web/src/main/js/apps/overview/main/enhance.tsx
server/sonar-web/src/main/js/apps/overview/meta/Meta.tsx
server/sonar-web/src/main/js/apps/overview/meta/MetaSize.tsx
server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGate.js
server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.js
server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateConditions.js
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/ApplicationQualityGateProject-test.tsx.snap
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap
server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx
server/sonar-web/src/main/js/apps/portfolio/components/App.tsx
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/Activity-test.tsx
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Effort-test.tsx.snap
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/HistoryButtonLink-test.tsx.snap
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MainRating-test.tsx.snap
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/MeasuresButtonLink-test.tsx.snap
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/ReleasabilityBox-test.tsx.snap
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/Summary-test.tsx.snap
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/LeakPeriodForm.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/LongBranchesPattern.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/SettingForm.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/LongBranchesPattern-test.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/SettingForm-test.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/store/actions.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlay.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/MeasuresOverlayTestCases.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineOptionsPopup-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlay-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/MeasuresOverlayTestCases-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlay-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/MeasuresOverlayCoveredFiles-test.tsx.snap
server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx
server/sonar-web/src/main/js/components/common/BranchStatus.css
server/sonar-web/src/main/js/components/common/BranchStatus.tsx
server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx
server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx
server/sonar-web/src/main/js/components/icons-components/PullRequestIcon.tsx
server/sonar-web/src/main/js/components/issue/Issue.d.ts
server/sonar-web/src/main/js/components/issue/Issue.js
server/sonar-web/src/main/js/components/issue/IssueView.js
server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap
server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts
server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js
server/sonar-web/src/main/js/components/shared/DrilldownLink.tsx
server/sonar-web/src/main/js/components/workspace/views/viewer-view.js
server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts
server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts
server/sonar-web/src/main/js/helpers/branches.ts
server/sonar-web/src/main/js/helpers/urls.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 02ce5fe693ef460951797fde8ee96c7c0e4c1d71..9cb5dbaec27a15dfa808e6dfe127ff2f2f8ba041 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { getJSON, post } from '../helpers/request';
+import { Branch, PullRequest } from '../app/types';
 import throwGlobalError from '../app/utils/throwGlobalError';
 
-export function getBranches(project: string): Promise<any> {
+export function getBranches(project: string): Promise<Branch[]> {
   return getJSON('/api/project_branches/list', { project }).then(r => r.branches, throwGlobalError);
 }
 
-export function deleteBranch(project: string, branch: string): Promise<void | Response> {
-  return post('/api/project_branches/delete', { project, branch }).catch(throwGlobalError);
+export function getPullRequests(project: string): Promise<PullRequest[]> {
+  return getJSON('/api/project_pull_requests/list', { project }).then(
+    r => r.pullRequests,
+    throwGlobalError
+  );
 }
 
-export function renameBranch(project: string, name: string): Promise<void | Response> {
+export function deleteBranch(data: { branch: string; project: string }) {
+  return post('/api/project_branches/delete', data).catch(throwGlobalError);
+}
+
+export function deletePullRequest(data: { project: string; pullRequest: string }) {
+  return post('/api/project_pull_requests/delete', data).catch(throwGlobalError);
+}
+
+export function renameBranch(project: string, name: string) {
   return post('/api/project_branches/rename', { project, name }).catch(throwGlobalError);
 }
index db1e32cc64335b4b630c0c33f7c83ab6d8f75d2f..60f7af8b14b6e5e3ba6ae9cf48b4d82accfe8519 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { getJSON, postJSON, post, RequestData } from '../helpers/request';
 import throwGlobalError from '../app/utils/throwGlobalError';
-import { Paging, Visibility } from '../app/types';
+import { Paging, Visibility, BranchParameters } from '../app/types';
 
 export interface BaseSearchProjectsParameters {
   analyzedBefore?: string;
@@ -118,11 +118,8 @@ export function getComponentLeaves(
 }
 
 export function getComponent(
-  componentKey: string,
-  metrics: string[] = [],
-  branch?: string
+  data: { componentKey: string; metricKeys: string } & BranchParameters
 ): Promise<any> {
-  const data = { branch, componentKey, metricKeys: metrics.join(',') };
   return getJSON('/api/measures/component', data).then(r => r.component);
 }
 
@@ -130,23 +127,23 @@ export function getTree(component: string, options: RequestData = {}): Promise<a
   return getJSON('/api/components/tree', { ...options, component });
 }
 
-export function getComponentShow(component: string, branch?: string): Promise<any> {
-  return getJSON('/api/components/show', { component, branch });
+export function getComponentShow(data: { component: string } & BranchParameters): Promise<any> {
+  return getJSON('/api/components/show', data);
 }
 
 export function getParents(component: string): Promise<any> {
-  return getComponentShow(component).then(r => r.ancestors);
+  return getComponentShow({ component }).then(r => r.ancestors);
 }
 
-export function getBreadcrumbs(component: string, branch?: string): Promise<any> {
-  return getComponentShow(component, branch).then(r => {
+export function getBreadcrumbs(data: { component: string } & BranchParameters): Promise<any> {
+  return getComponentShow(data).then(r => {
     const reversedAncestors = [...r.ancestors].reverse();
     return [...reversedAncestors, r.component];
   });
 }
 
-export function getComponentData(component: string, branch?: string): Promise<any> {
-  return getComponentShow(component, branch).then(r => r.component);
+export function getComponentData(data: { component: string } & BranchParameters): Promise<any> {
+  return getComponentShow(data).then(r => r.component);
 }
 
 export function getMyProjects(data: RequestData): Promise<any> {
@@ -246,31 +243,24 @@ export function getSuggestions(
   return getJSON('/api/components/suggestions', data);
 }
 
-export function getComponentForSourceViewer(component: string, branch?: string): Promise<any> {
-  return getJSON('/api/components/app', { component, branch });
+export function getComponentForSourceViewer(
+  data: { component: string } & BranchParameters
+): Promise<any> {
+  return getJSON('/api/components/app', data);
 }
 
 export function getSources(
-  component: string,
-  from?: number,
-  to?: number,
-  branch?: string
+  data: { key: string; from?: number; to?: number } & BranchParameters
 ): Promise<any> {
-  const data: RequestData = { key: component, branch };
-  if (from) {
-    Object.assign(data, { from });
-  }
-  if (to) {
-    Object.assign(data, { to });
-  }
   return getJSON('/api/sources/lines', data).then(r => r.sources);
 }
 
-export function getDuplications(component: string, branch?: string): Promise<any> {
-  return getJSON('/api/duplications/show', { key: component, branch });
+export function getDuplications(data: { key: string } & BranchParameters): Promise<any> {
+  return getJSON('/api/duplications/show', data);
 }
 
-export function getTests(component: string, line: number | string, branch?: string): Promise<any> {
-  const data = { sourceFileKey: component, sourceFileLineNumber: line, branch };
+export function getTests(
+  data: { sourceFileKey: string; sourceFileLineNumber: number | string } & BranchParameters
+): Promise<any> {
   return getJSON('/api/tests/list', data).then(r => r.tests);
 }
index bf6f8712090be0206a1360d76f8ae083e35939e8..40d219fcfa30caf1af1d2d97daf9e1f4e98e66f5 100644 (file)
 import { getJSON, RequestData, postJSON, post } from '../helpers/request';
 import throwGlobalError from '../app/utils/throwGlobalError';
 import { Measure, MeasurePeriod } from '../helpers/measures';
-import { Metric, CustomMeasure, Paging } from '../app/types';
+import { Metric, CustomMeasure, Paging, BranchParameters } from '../app/types';
 import { Period } from '../helpers/periods';
 
 export function getMeasures(
-  componentKey: string,
-  metrics: string[],
-  branch?: string
+  data: { componentKey: string; metricKeys: string } & BranchParameters
 ): Promise<{ metric: string; value?: string }[]> {
-  const url = '/api/measures/component';
-  const data = { componentKey, metricKeys: metrics.join(','), branch };
-  return getJSON(url, data).then(r => r.component.measures, throwGlobalError);
+  return getJSON('/api/measures/component', data).then(r => r.component.measures, throwGlobalError);
 }
 
 interface MeasureComponent {
index 43a2340cabb893323c71e1416dda8ba8fd2faa07..7dcf404ad711b53b6ede5a9cf4f237e71488eaa0 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { getJSON, parseJSON, request } from '../helpers/request';
+import { BranchParameters } from '../app/types';
 import throwGlobalError from '../app/utils/throwGlobalError';
 
 export function getGlobalNavigation(): Promise<any> {
   return getJSON('/api/navigation/global');
 }
 
-export function getComponentNavigation(componentKey: string, branch?: string): Promise<any> {
-  return getJSON('/api/navigation/component', { componentKey, branch }).catch(throwGlobalError);
+export function getComponentNavigation(
+  data: { componentKey: string } & BranchParameters
+): Promise<any> {
+  return getJSON('/api/navigation/component', data).catch(throwGlobalError);
 }
 
 export function getSettingsNavigation(): Promise<any> {
index b931ea5a6f61daf7ec53b73c0a1e7805eaab39f3..fad7562500f58739fbe5927af5f42ea5cd40b3ea 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { getJSON, postJSON, post, RequestData } from '../helpers/request';
 import throwGlobalError from '../app/utils/throwGlobalError';
-import { Paging } from '../app/types';
+import { Paging, BranchParameters } from '../app/types';
 
 export interface Event {
   key: string;
@@ -34,13 +34,9 @@ export interface Analysis {
   events: Event[];
 }
 
-export function getProjectActivity(data: {
-  branch?: string;
-  project: string;
-  category?: string;
-  p?: number;
-  ps?: number;
-}): Promise<{ analyses: Analysis[]; paging: Paging }> {
+export function getProjectActivity(
+  data: { project: string; category?: string; p?: number; ps?: number } & BranchParameters
+): Promise<{ analyses: Analysis[]; paging: Paging }> {
   return getJSON('/api/project_analyses/search', data).catch(throwGlobalError);
 }
 
index 880d37d65e46cb257933cbda7ec7d1248c998966..46c0ec67cf81d91afd4bbf86efdd8458de9b6b25 100644 (file)
 import { omitBy } from 'lodash';
 import { getJSON, RequestData, post, postJSON } from '../helpers/request';
 import { TYPE_PROPERTY_SET } from '../apps/settings/constants';
+import { BranchParameters } from '../app/types';
 import throwGlobalError from '../app/utils/throwGlobalError';
 
-export function getDefinitions(component: string | null, branch?: string): Promise<any> {
-  return getJSON('/api/settings/list_definitions', { branch, component }).then(r => r.definitions);
+export function getDefinitions(component?: string): Promise<any> {
+  return getJSON('/api/settings/list_definitions', { component }).then(r => r.definitions);
 }
 
 export interface SettingValue {
@@ -36,21 +37,14 @@ export interface SettingValue {
 }
 
 export function getValues(
-  keys: string,
-  component?: string,
-  branch?: string
+  data: { keys: string; component?: string } & BranchParameters
 ): Promise<SettingValue[]> {
-  return getJSON('/api/settings/values', { keys, component, branch }).then(r => r.settings);
+  return getJSON('/api/settings/values', data).then(r => r.settings);
 }
 
-export function setSettingValue(
-  definition: any,
-  value: any,
-  component?: string,
-  branch?: string
-): Promise<void> {
+export function setSettingValue(definition: any, value: any, component?: string): Promise<void> {
   const { key } = definition;
-  const data: RequestData = { key, component, branch };
+  const data: RequestData = { key, component };
 
   if (definition.multiValues) {
     data.values = value;
@@ -65,17 +59,16 @@ export function setSettingValue(
   return post('/api/settings/set', data);
 }
 
-export function setSimpleSettingValue(parameters: {
-  branch?: string;
-  component?: string;
-  value: string;
-  key: string;
-}): Promise<void | Response> {
-  return post('/api/settings/set', parameters).catch(throwGlobalError);
+export function setSimpleSettingValue(
+  data: { component?: string; value: string; key: string } & BranchParameters
+): Promise<void | Response> {
+  return post('/api/settings/set', data).catch(throwGlobalError);
 }
 
-export function resetSettingValue(key: string, component?: string, branch?: string): Promise<void> {
-  return post('/api/settings/reset', { keys: key, component, branch });
+export function resetSettingValue(
+  data: { keys: string; component?: string } & BranchParameters
+): Promise<void> {
+  return post('/api/settings/reset', data);
 }
 
 export function sendTestEmail(to: string, subject: string, message: string): Promise<void> {
index 1ac562186307c5fe544c8922fd32b5f992040334..5b649d1d99b201cbd6ac990bd4e2cd476a64c812 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import throwGlobalError from '../app/utils/throwGlobalError';
-import { Paging, TestCase, CoveredFile } from '../app/types';
+import { Paging, TestCase, CoveredFile, BranchParameters } from '../app/types';
 import { getJSON } from '../helpers/request';
 
-export function getTests(parameters: {
-  branch?: string;
-  p?: number;
-  ps?: number;
-  sourceFileKey?: string;
-  sourceFileLineNumber?: number;
-  testFileKey: string;
-  testId?: string;
-}): Promise<{ paging: Paging; tests: TestCase[] }> {
+export function getTests(
+  parameters: {
+    p?: number;
+    ps?: number;
+    sourceFileKey?: string;
+    sourceFileLineNumber?: number;
+    testFileKey: string;
+    testId?: string;
+  } & BranchParameters
+): Promise<{ paging: Paging; tests: TestCase[] }> {
   return getJSON('/api/tests/list', parameters).catch(throwGlobalError);
 }
 
index 2f25912b191f94b0a4d7d3c7291f0ba93ce500f5..ac77c7c8b4170d72043d6a75bb8e6c3e549af7df 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { getJSON } from '../helpers/request';
-import { Paging } from '../app/types';
+import { Paging, BranchParameters } from '../app/types';
 import throwGlobalError from '../app/utils/throwGlobalError';
 
 export interface HistoryItem {
@@ -39,25 +39,29 @@ interface TimeMachineResponse {
 }
 
 export function getTimeMachineData(
-  component: string,
-  metrics: string[],
-  other?: { branch?: string; p?: number; ps?: number; from?: string; to?: string }
+  data: {
+    component: string;
+    from?: string;
+    metrics: string;
+    p?: number;
+    ps?: number;
+    to?: string;
+  } & BranchParameters
 ): Promise<TimeMachineResponse> {
-  return getJSON('/api/measures/search_history', {
-    component,
-    metrics: metrics.join(),
-    ps: 1000,
-    ...other
-  }).catch(throwGlobalError);
+  return getJSON('/api/measures/search_history', data).catch(throwGlobalError);
 }
 
 export function getAllTimeMachineData(
-  component: string,
-  metrics: Array<string>,
-  other?: { branch?: string; p?: number; from?: string; to?: string },
+  data: {
+    component: string;
+    metrics: string;
+    from?: string;
+    p?: number;
+    to?: string;
+  } & BranchParameters,
   prev?: TimeMachineResponse
 ): Promise<TimeMachineResponse> {
-  return getTimeMachineData(component, metrics, { ...other, ps: 1000 }).then(r => {
+  return getTimeMachineData({ ...data, ps: 1000 }).then(r => {
     const result = prev
       ? {
           measures: prev.measures.map((measure, idx) => ({
@@ -71,11 +75,6 @@ export function getAllTimeMachineData(
     if (result.paging.pageIndex * result.paging.pageSize >= result.paging.total) {
       return result;
     }
-    return getAllTimeMachineData(
-      component,
-      metrics,
-      { ...other, p: result.paging.pageIndex + 1 },
-      result
-    );
+    return getAllTimeMachineData({ ...data, p: result.paging.pageIndex + 1 }, result);
   });
 }
index af04b9229fbfcbd1cf2507ac2bab90bc57932836..530bb5f63d4278a91dd85dfd291eb02ed2ce1487 100644 (file)
@@ -22,25 +22,26 @@ import * as PropTypes from 'prop-types';
 import { connect } from 'react-redux';
 import ComponentContainerNotFound from './ComponentContainerNotFound';
 import ComponentNav from './nav/component/ComponentNav';
-import { Branch, Component } from '../types';
+import { Component, BranchLike } from '../types';
 import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
-import { getBranches } from '../../api/branches';
+import { getBranches, getPullRequests } from '../../api/branches';
 import { Task, getTasksForComponent } from '../../api/ce';
 import { getComponentData } from '../../api/components';
 import { getComponentNavigation } from '../../api/nav';
 import { fetchOrganizations } from '../../store/rootActions';
 import { STATUSES } from '../../apps/background-tasks/constants';
+import { isPullRequest, isBranch } from '../../helpers/branches';
 
 interface Props {
   children: any;
   fetchOrganizations: (organizations: string[]) => void;
   location: {
-    query: { branch?: string; id: string };
+    query: { branch?: string; id: string; pullRequest?: string };
   };
 }
 
 interface State {
-  branches: Branch[];
+  branchLikes: BranchLike[];
   loading: boolean;
   component?: Component;
   currentTask?: Task;
@@ -57,7 +58,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
 
   constructor(props: Props) {
     super(props);
-    this.state = { branches: [], loading: true };
+    this.state = { branchLikes: [], loading: true };
   }
 
   componentDidMount() {
@@ -68,7 +69,8 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
   componentWillReceiveProps(nextProps: Props) {
     if (
       nextProps.location.query.id !== this.props.location.query.id ||
-      nextProps.location.query.branch !== this.props.location.query.branch
+      nextProps.location.query.branch !== this.props.location.query.branch ||
+      nextProps.location.query.pullRequest !== this.props.location.query.pullRequest
     ) {
       this.fetchComponent(nextProps);
     }
@@ -84,7 +86,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
   });
 
   fetchComponent(props: Props) {
-    const { branch, id } = props.location.query;
+    const { branch, id: key, pullRequest } = props.location.query;
     this.setState({ loading: true });
 
     const onError = (error: any) => {
@@ -97,29 +99,33 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
       }
     };
 
-    Promise.all([getComponentNavigation(id, branch), getComponentData(id, branch)]).then(
-      ([nav, data]) => {
-        const component = this.addQualifier({ ...nav, ...data });
+    Promise.all([
+      getComponentNavigation({ componentKey: key, branch, pullRequest }),
+      getComponentData({ component: key, branch, pullRequest })
+    ]).then(([nav, data]) => {
+      const component = this.addQualifier({ ...nav, ...data });
 
-        if (this.context.organizationsEnabled) {
-          this.props.fetchOrganizations([component.organization]);
-        }
+      if (this.context.organizationsEnabled) {
+        this.props.fetchOrganizations([component.organization]);
+      }
 
-        this.fetchBranches(component).then(branches => {
-          if (this.mounted) {
-            this.setState({ loading: false, branches, component });
-          }
-        }, onError);
+      this.fetchBranches(component).then(branchLikes => {
+        if (this.mounted) {
+          this.setState({ loading: false, branchLikes, component });
+        }
+      }, onError);
 
-        this.fetchStatus(component);
-      },
-      onError
-    );
+      this.fetchStatus(component);
+    }, onError);
   }
 
-  fetchBranches = (component: Component) => {
+  fetchBranches = (component: Component): Promise<BranchLike[]> => {
     const project = component.breadcrumbs.find(({ qualifier }) => qualifier === 'TRK');
-    return project ? getBranches(project.key) : Promise.resolve([]);
+    return project
+      ? Promise.all([getBranches(project.key), getPullRequests(project.key)]).then(
+          ([branches, pullRequests]) => [...branches, ...pullRequests]
+        )
+      : Promise.resolve([]);
   };
 
   fetchStatus = (component: Component) => {
@@ -146,9 +152,9 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
   handleBranchesChange = () => {
     if (this.mounted && this.state.component) {
       this.fetchBranches(this.state.component).then(
-        branches => {
+        branchLikes => {
           if (this.mounted) {
-            this.setState({ branches });
+            this.setState({ branchLikes });
           }
         },
         () => {}
@@ -158,22 +164,24 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
 
   render() {
     const { query } = this.props.location;
-    const { branches, component, loading } = this.state;
+    const { branchLikes, component, loading } = this.state;
 
     if (!loading && !component) {
       return <ComponentContainerNotFound />;
     }
 
-    const branch = branches.find(b => (query.branch ? b.name === query.branch : b.isMain));
+    const branchLike = query.pullRequest
+      ? branchLikes.find(b => isPullRequest(b) && b.key === query.pullRequest)
+      : branchLikes.find(b => isBranch(b) && (query.branch ? b.name === query.branch : b.isMain));
 
     return (
       <div>
         {component &&
           !['FIL', 'UTS'].includes(component.qualifier) && (
             <ComponentNav
-              branches={branches}
-              currentBranch={branch}
+              branchLikes={branchLikes}
               component={component}
+              currentBranchLike={branchLike}
               currentTask={this.state.currentTask}
               isInProgress={this.state.isInProgress}
               isPending={this.state.isPending}
@@ -186,8 +194,8 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
           </div>
         ) : (
           React.cloneElement(this.props.children, {
-            branch,
-            branches,
+            branchLike,
+            branchLikes,
             component,
             isInProgress: this.state.isInProgress,
             isPending: this.state.isPending,
index 612f4bb043f50fa18c64714e83c7ddf2aac3a280..e2293316ea9dbb5492e62abde4cd3d96c73e321f 100644 (file)
  */
 import * as React from 'react';
 import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
-import { Component, Branch } from '../types';
+import { BranchLike, Component } from '../types';
 
 interface Props {
   children: JSX.Element;
-  branch?: Branch;
-  branches: Branch[];
+  branchLike?: BranchLike;
+  branchLikes: BranchLike[];
   component: Component;
   isInProgress?: boolean;
   isPending?: boolean;
index b66a2aff8da6d9502545c50f4e4617501f438d43..b0917c9d1c72eccd829bd0285003fc57e9936d62 100644 (file)
 import * as React from 'react';
 import { shallow, mount } from 'enzyme';
 import { ComponentContainer } from '../ComponentContainer';
-import { getBranches } from '../../../api/branches';
+import { getBranches, getPullRequests } from '../../../api/branches';
 import { getTasksForComponent } from '../../../api/ce';
 import { getComponentData } from '../../../api/components';
 import { getComponentNavigation } from '../../../api/nav';
 
-jest.mock('../../../api/branches', () => ({ getBranches: jest.fn(() => Promise.resolve([])) }));
+jest.mock('../../../api/branches', () => ({
+  getBranches: jest.fn(() => Promise.resolve([])),
+  getPullRequests: jest.fn(() => Promise.resolve([]))
+}));
+
 jest.mock('../../../api/ce', () => ({
   getTasksForComponent: jest.fn(() => Promise.resolve({ queue: [] }))
 }));
+
 jest.mock('../../../api/components', () => ({
   getComponentData: jest.fn(() => Promise.resolve({}))
 }));
+
 jest.mock('../../../api/nav', () => ({
   getComponentNavigation: jest.fn(() =>
     Promise.resolve({
@@ -49,10 +55,11 @@ jest.mock('../nav/component/ComponentNav', () => ({
 const Inner = () => <div />;
 
 beforeEach(() => {
-  (getBranches as jest.Mock<any>).mockClear();
-  (getComponentData as jest.Mock<any>).mockClear();
-  (getComponentNavigation as jest.Mock<any>).mockClear();
-  (getTasksForComponent as jest.Mock<any>).mockClear();
+  (getBranches as jest.Mock).mockClear();
+  (getPullRequests as jest.Mock).mockClear();
+  (getComponentData as jest.Mock).mockClear();
+  (getComponentNavigation as jest.Mock).mockClear();
+  (getTasksForComponent as jest.Mock).mockClear();
 });
 
 it('changes component', () => {
@@ -90,8 +97,9 @@ it("loads branches for module's project", async () => {
 
   await new Promise(setImmediate);
   expect(getBranches).toBeCalledWith('projectKey');
-  expect(getComponentData).toBeCalledWith('moduleKey', undefined);
-  expect(getComponentNavigation).toBeCalledWith('moduleKey', undefined);
+  expect(getPullRequests).toBeCalledWith('projectKey');
+  expect(getComponentData).toBeCalledWith({ component: 'moduleKey', branch: undefined });
+  expect(getComponentNavigation).toBeCalledWith({ componentKey: 'moduleKey', branch: undefined });
 });
 
 it("doesn't load branches portfolio", async () => {
@@ -103,8 +111,12 @@ it("doesn't load branches portfolio", async () => {
 
   await new Promise(setImmediate);
   expect(getBranches).not.toBeCalled();
-  expect(getComponentData).toBeCalledWith('portfolioKey', undefined);
-  expect(getComponentNavigation).toBeCalledWith('portfolioKey', undefined);
+  expect(getPullRequests).not.toBeCalled();
+  expect(getComponentData).toBeCalledWith({ component: 'portfolioKey', branch: undefined });
+  expect(getComponentNavigation).toBeCalledWith({
+    componentKey: 'portfolioKey',
+    branch: undefined
+  });
   wrapper.update();
   expect(wrapper.find(Inner).exists()).toBeTruthy();
 });
@@ -123,6 +135,7 @@ it('updates branches on change', () => {
   });
   (wrapper.find(Inner).prop('onBranchesChange') as Function)();
   expect(getBranches).toBeCalledWith('projectKey');
+  expect(getPullRequests).toBeCalledWith('projectKey');
 });
 
 it('loads organization', async () => {
index 8b13a6ef075e556134c88ac4c3fb838cacb3f275..ac7ce95e3f3a0af33adfa4f3b8a89519f34d550d 100644 (file)
   font-size: var(--baseFontSize);
 }
 
+.navbar-context-meta-branch-menu-title {
+  padding-left: calc(3 * var(--gridSize));
+}
+
 .navbar-context-meta-branch-menu-item {
   display: flex !important;
   justify-content: space-between;
index 71380876edf214f884113b40d4ecadb8893f8214..38fbd68f6443d1fb10a21e3e6ca4a9d778def024 100644 (file)
@@ -24,15 +24,15 @@ import ComponentNavMenu from './ComponentNavMenu';
 import ComponentNavBgTaskNotif from './ComponentNavBgTaskNotif';
 import RecentHistory from '../../RecentHistory';
 import * as theme from '../../../theme';
-import { Branch, Component } from '../../../types';
+import { BranchLike, Component } from '../../../types';
 import ContextNavBar from '../../../../components/nav/ContextNavBar';
 import { Task } from '../../../../api/ce';
 import { STATUSES } from '../../../../apps/background-tasks/constants';
 import './ComponentNav.css';
 
 interface Props {
-  branches: Branch[];
-  currentBranch?: Branch;
+  branchLikes: BranchLike[];
+  currentBranchLike: BranchLike | undefined;
   component: Component;
   currentTask?: Task;
   isInProgress?: boolean;
@@ -85,15 +85,18 @@ export default class ComponentNav extends React.PureComponent<Props> {
         height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw}
         notif={notifComponent}>
         <ComponentNavHeader
-          branches={this.props.branches}
+          branchLikes={this.props.branchLikes}
           component={this.props.component}
-          currentBranch={this.props.currentBranch}
+          currentBranchLike={this.props.currentBranchLike}
           // to close dropdown on any location change
           location={this.props.location}
         />
-        <ComponentNavMeta branch={this.props.currentBranch} component={this.props.component} />
+        <ComponentNavMeta
+          branchLike={this.props.currentBranchLike}
+          component={this.props.component}
+        />
         <ComponentNavMenu
-          branch={this.props.currentBranch}
+          branchLike={this.props.currentBranchLike}
           component={this.props.component}
           // to re-render selected menu item
           location={this.props.location}
index 8c600143589f6743240d8d6e5d645e9ef99e0089..d51dadbd73dfb8fb2d5b7966dcf61cd6d2516c36 100644 (file)
 import * as React from 'react';
 import * as classNames from 'classnames';
 import * as PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
 import ComponentNavBranchesMenu from './ComponentNavBranchesMenu';
 import SingleBranchHelperPopup from './SingleBranchHelperPopup';
 import NoBranchSupportPopup from './NoBranchSupportPopup';
-import { Branch, Component } from '../../../types';
+import { BranchLike, Component } from '../../../types';
 import * as theme from '../../../theme';
 import BranchIcon from '../../../../components/icons-components/BranchIcon';
-import { isShortLivingBranch } from '../../../../helpers/branches';
+import {
+  isShortLivingBranch,
+  isSameBranchLike,
+  getBranchLikeDisplayName,
+  isPullRequest
+} from '../../../../helpers/branches';
 import { translate } from '../../../../helpers/l10n';
 import HelpIcon from '../../../../components/icons-components/HelpIcon';
 import BubblePopupHelper from '../../../../components/common/BubblePopupHelper';
 import Tooltip from '../../../../components/controls/Tooltip';
 
 interface Props {
-  branches: Branch[];
+  branchLikes: BranchLike[];
   component: Component;
-  currentBranch: Branch;
+  currentBranchLike: BranchLike;
   location?: any;
 }
 
@@ -69,7 +75,7 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
   componentWillReceiveProps(nextProps: Props) {
     if (
       nextProps.component !== this.props.component ||
-      this.differentBranches(nextProps.currentBranch, this.props.currentBranch) ||
+      !isSameBranchLike(nextProps.currentBranchLike, this.props.currentBranchLike) ||
       nextProps.location !== this.props.location
     ) {
       this.setState({ dropdownOpen: false, singleBranchPopupOpen: false });
@@ -80,11 +86,6 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
     this.mounted = false;
   }
 
-  differentBranches(a: Branch, b: Branch) {
-    // if main branch changes name, we should not close the dropdown
-    return a.isMain && b.isMain ? false : a.name !== b.name;
-  }
-
   handleClick = (event: React.SyntheticEvent<HTMLElement>) => {
     event.preventDefault();
     event.stopPropagation();
@@ -130,32 +131,46 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
     const { configuration } = this.props.component;
     return this.state.dropdownOpen ? (
       <ComponentNavBranchesMenu
-        branches={this.props.branches}
+        branchLikes={this.props.branchLikes}
         canAdmin={configuration && configuration.showSettings}
         component={this.props.component}
-        currentBranch={this.props.currentBranch}
+        currentBranchLike={this.props.currentBranchLike}
         onClose={this.closeDropdown}
       />
     ) : null;
   };
 
   renderMergeBranch = () => {
-    const { currentBranch } = this.props;
-    if (!isShortLivingBranch(currentBranch)) {
+    const { currentBranchLike } = this.props;
+    if (isShortLivingBranch(currentBranchLike)) {
+      return currentBranchLike.isOrphan ? (
+        <span className="note big-spacer-left text-lowercase">
+          {translate('branches.orphan_branch')}
+          <Tooltip overlay={translate('branches.orphan_branches.tooltip')}>
+            <i className="icon-help spacer-left" />
+          </Tooltip>
+        </span>
+      ) : (
+        <span className="note big-spacer-left text-lowercase">
+          {translate('from')} <strong>{currentBranchLike.mergeBranch}</strong>
+        </span>
+      );
+    } else if (isPullRequest(currentBranchLike)) {
+      return (
+        <span className="note big-spacer-left text-lowercase">
+          <FormattedMessage
+            defaultMessage={translate('branches.pull_request.for_merge_into_x_from_y')}
+            id="branches.pull_request.for_merge_into_x_from_y"
+            values={{
+              base: <strong>{currentBranchLike.base}</strong>,
+              branch: <strong>{currentBranchLike.branch}</strong>
+            }}
+          />
+        </span>
+      );
+    } else {
       return null;
     }
-    return currentBranch.isOrphan ? (
-      <span className="note big-spacer-left text-lowercase">
-        {translate('branches.orphan_branch')}
-        <Tooltip overlay={translate('branches.orphan_branches.tooltip')}>
-          <i className="icon-help spacer-left" />
-        </Tooltip>
-      </span>
-    ) : (
-      <span className="note big-spacer-left text-lowercase">
-        {translate('from')} <strong>{currentBranch.mergeBranch}</strong>
-      </span>
-    );
   };
 
   renderSingleBranchPopup = () => (
@@ -187,27 +202,33 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
   );
 
   render() {
-    const { branches, currentBranch } = this.props;
+    const { branchLikes, currentBranchLike } = this.props;
 
     if (this.context.onSonarCloud && !this.context.branchesEnabled) {
       return null;
     }
 
+    const displayName = getBranchLikeDisplayName(currentBranchLike);
+
     if (!this.context.branchesEnabled) {
       return (
         <div className="navbar-context-branches">
-          <BranchIcon branch={currentBranch} className="little-spacer-right" fill={theme.gray80} />
-          <span className="note">{currentBranch.name}</span>
+          <BranchIcon
+            branchLike={currentBranchLike}
+            className="little-spacer-right"
+            fill={theme.gray80}
+          />
+          <span className="note">{displayName}</span>
           {this.renderNoBranchSupportPopup()}
         </div>
       );
     }
 
-    if (branches.length < 2) {
+    if (branchLikes.length < 2) {
       return (
         <div className="navbar-context-branches">
-          <BranchIcon branch={currentBranch} className="little-spacer-right" />
-          <span className="note">{currentBranch.name}</span>
+          <BranchIcon branchLike={currentBranchLike} className="little-spacer-right" />
+          <span className="note">{displayName}</span>
           {this.renderSingleBranchPopup()}
         </div>
       );
@@ -219,9 +240,9 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
           open: this.state.dropdownOpen
         })}>
         <a className="link-base-color link-no-underline" href="#" onClick={this.handleClick}>
-          <BranchIcon branch={currentBranch} className="little-spacer-right" />
-          <Tooltip overlay={currentBranch.name} mouseEnterDelay={1}>
-            <span className="text-limited text-top">{currentBranch.name}</span>
+          <BranchIcon branchLike={currentBranchLike} className="little-spacer-right" />
+          <Tooltip overlay={displayName} mouseEnterDelay={1}>
+            <span className="text-limited text-top">{displayName}</span>
           </Tooltip>
           <i className="icon-dropdown little-spacer-left" />
         </a>
index b46da2bf13db3e9daa4a1ccb44b84e0c4d0a4247..4f2849bdfd8c6a89270f64f0828ef31262096fa8 100644 (file)
@@ -21,28 +21,32 @@ import * as React from 'react';
 import * as PropTypes from 'prop-types';
 import { Link } from 'react-router';
 import ComponentNavBranchesMenuItem from './ComponentNavBranchesMenuItem';
-import { Branch, Component } from '../../../types';
+import { BranchLike, Component } from '../../../types';
 import {
   sortBranchesAsTree,
   isLongLivingBranch,
-  isShortLivingBranch
+  isShortLivingBranch,
+  isSameBranchLike,
+  getBranchLikeKey,
+  isPullRequest,
+  isBranch
 } from '../../../../helpers/branches';
 import { translate } from '../../../../helpers/l10n';
-import { getProjectBranchUrl } from '../../../../helpers/urls';
+import { getBranchLikeUrl } from '../../../../helpers/urls';
 import SearchBox from '../../../../components/controls/SearchBox';
 import Tooltip from '../../../../components/controls/Tooltip';
 
 interface Props {
-  branches: Branch[];
+  branchLikes: BranchLike[];
   canAdmin?: boolean;
   component: Component;
-  currentBranch: Branch;
+  currentBranchLike: BranchLike;
   onClose: () => void;
 }
 
 interface State {
   query: string;
-  selected: string | null;
+  selected: BranchLike | undefined;
 }
 
 export default class ComponentNavBranchesMenu extends React.PureComponent<Props, State> {
@@ -54,7 +58,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
 
   constructor(props: Props) {
     super(props);
-    this.state = { query: '', selected: null };
+    this.state = { query: '', selected: undefined };
   }
 
   componentDidMount() {
@@ -65,10 +69,16 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
     window.removeEventListener('click', this.handleClickOutside);
   }
 
-  getFilteredBranches = () =>
-    sortBranchesAsTree(this.props.branches).filter(branch =>
-      branch.name.toLowerCase().includes(this.state.query.toLowerCase())
-    );
+  getFilteredBranchLikes = () => {
+    const query = this.state.query.toLowerCase();
+    return sortBranchesAsTree(this.props.branchLikes).filter(branchLike => {
+      const matchBranchName = isBranch(branchLike) && branchLike.name.toLowerCase().includes(query);
+      const matchPullRequestTitleOrId =
+        isPullRequest(branchLike) &&
+        (branchLike.title.includes(query) || branchLike.key.includes(query));
+      return matchBranchName || matchPullRequestTitleOrId;
+    });
+  };
 
   handleClickOutside = (event: Event) => {
     if (!this.node || !this.node.contains(event.target as HTMLElement)) {
@@ -76,7 +86,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
     }
   };
 
-  handleSearchChange = (query: string) => this.setState({ query, selected: null });
+  handleSearchChange = (query: string) => this.setState({ query, selected: undefined });
 
   handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
     switch (event.keyCode) {
@@ -99,32 +109,31 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
 
   openSelected = () => {
     const selected = this.getSelected();
-    const branch = this.getFilteredBranches().find(branch => branch.name === selected);
-    if (branch) {
-      this.context.router.push(this.getProjectBranchUrl(branch));
+    if (selected) {
+      this.context.router.push(this.getProjectBranchUrl(selected));
     }
   };
 
   selectPrevious = () => {
     const selected = this.getSelected();
-    const branches = this.getFilteredBranches();
-    const index = branches.findIndex(branch => branch.name === selected);
+    const branchLikes = this.getFilteredBranchLikes();
+    const index = branchLikes.findIndex(b => isSameBranchLike(b, selected));
     if (index > 0) {
-      this.setState({ selected: branches[index - 1].name });
+      this.setState({ selected: branchLikes[index - 1] });
     }
   };
 
   selectNext = () => {
     const selected = this.getSelected();
-    const branches = this.getFilteredBranches();
-    const index = branches.findIndex(branch => branch.name === selected);
+    const branches = this.getFilteredBranchLikes();
+    const index = branches.findIndex(b => isSameBranchLike(b, selected));
     if (index >= 0 && index < branches.length - 1) {
-      this.setState({ selected: branches[index + 1].name });
+      this.setState({ selected: branches[index + 1] });
     }
   };
 
-  handleSelect = (branch: Branch) => {
-    this.setState({ selected: branch.name });
+  handleSelect = (branchLike: BranchLike) => {
+    this.setState({ selected: branchLike });
   };
 
   getSelected = () => {
@@ -132,21 +141,24 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
       return this.state.selected;
     }
 
-    const branches = this.getFilteredBranches();
-    if (branches.find(b => b.name === this.props.currentBranch.name)) {
-      return this.props.currentBranch.name;
+    const branchLikes = this.getFilteredBranchLikes();
+    if (branchLikes.find(b => isSameBranchLike(b, this.props.currentBranchLike))) {
+      return this.props.currentBranchLike;
     }
 
-    if (branches.length > 0) {
-      return branches[0].name;
+    if (branchLikes.length > 0) {
+      return branchLikes[0];
     }
 
     return undefined;
   };
 
-  getProjectBranchUrl = (branch: Branch) => getProjectBranchUrl(this.props.component.key, branch);
+  getProjectBranchUrl = (branchLike: BranchLike) =>
+    getBranchLikeUrl(this.props.component.key, branchLike);
 
-  isSelected = (branch: Branch) => branch.name === this.getSelected();
+  isOrphan = (branchLike: BranchLike) => {
+    return (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) && branchLike.isOrphan;
+  };
 
   renderSearch = () => (
     <div className="menu-search">
@@ -161,21 +173,26 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
   );
 
   renderBranchesList = () => {
-    const branches = this.getFilteredBranches();
+    const branchLikes = this.getFilteredBranchLikes();
     const selected = this.getSelected();
 
-    if (branches.length === 0) {
+    if (branchLikes.length === 0) {
       return <div className="menu-message note">{translate('no_results')}</div>;
     }
 
-    const items = branches.map((branch, index) => {
-      const isOrphan = isShortLivingBranch(branch) && branch.isOrphan;
-      const previous = index > 0 ? branches[index - 1] : undefined;
-      const isPreviousOrphan = isShortLivingBranch(previous) ? previous.isOrphan : false;
-      const showDivider = isLongLivingBranch(branch) || (isOrphan && !isPreviousOrphan);
+    const items = branchLikes.map((branchLike, index) => {
+      const isOrphan = this.isOrphan(branchLike);
+      const previous = index > 0 ? branchLikes[index - 1] : undefined;
+      const isPreviousOrphan = previous !== undefined && this.isOrphan(previous);
+      const showDivider = isLongLivingBranch(branchLike) || (isOrphan && !isPreviousOrphan);
       const showOrphanHeader = isOrphan && !isPreviousOrphan;
+      const showPullRequestHeader =
+        !showOrphanHeader && isPullRequest(branchLike) && !isPullRequest(previous);
+      const showShortLivingBranchHeader =
+        !showOrphanHeader && isShortLivingBranch(branchLike) && !isShortLivingBranch(previous);
+
       return (
-        <React.Fragment key={branch.name}>
+        <React.Fragment key={getBranchLikeKey(branchLike)}>
           {showDivider && <li className="divider" />}
           {showOrphanHeader && (
             <li className="dropdown-header">
@@ -185,12 +202,22 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
               </Tooltip>
             </li>
           )}
+          {showPullRequestHeader && (
+            <li className="dropdown-header navbar-context-meta-branch-menu-title">
+              {translate('branches.pull_requests')}
+            </li>
+          )}
+          {showShortLivingBranchHeader && (
+            <li className="dropdown-header navbar-context-meta-branch-menu-title">
+              {translate('branches.short_lived_branches')}
+            </li>
+          )}
           <ComponentNavBranchesMenuItem
-            branch={branch}
+            branchLike={branchLike}
             component={this.props.component}
-            key={branch.name}
+            key={getBranchLikeKey(branchLike)}
             onSelect={this.handleSelect}
-            selected={branch.name === selected}
+            selected={isSameBranchLike(branchLike, selected)}
           />
         </React.Fragment>
       );
index b98de6bba7ba2c6d80488bae701b81847fa2f129..9375bbf356c78d51780c26604b5489454c72aba5 100644 (file)
@@ -21,47 +21,55 @@ import * as React from 'react';
 import { Link } from 'react-router';
 import * as classNames from 'classnames';
 import BranchStatus from '../../../../components/common/BranchStatus';
-import { Branch, Component } from '../../../types';
+import { BranchLike, Component } from '../../../types';
 import BranchIcon from '../../../../components/icons-components/BranchIcon';
-import { isShortLivingBranch } from '../../../../helpers/branches';
+import {
+  isShortLivingBranch,
+  getBranchLikeDisplayName,
+  getBranchLikeKey,
+  isMainBranch,
+  isPullRequest
+} from '../../../../helpers/branches';
 import { translate } from '../../../../helpers/l10n';
-import { getProjectBranchUrl } from '../../../../helpers/urls';
+import { getBranchLikeUrl } from '../../../../helpers/urls';
 import Tooltip from '../../../../components/controls/Tooltip';
 
 export interface Props {
-  branch: Branch;
+  branchLike: BranchLike;
   component: Component;
-  onSelect: (branch: Branch) => void;
+  onSelect: (branchLike: BranchLike) => void;
   selected: boolean;
 }
 
-export default function ComponentNavBranchesMenuItem({ branch, ...props }: Props) {
+export default function ComponentNavBranchesMenuItem({ branchLike, ...props }: Props) {
   const handleMouseEnter = () => {
-    props.onSelect(branch);
+    props.onSelect(branchLike);
   };
 
+  const displayName = getBranchLikeDisplayName(branchLike);
+  const shouldBeIndented =
+    (isShortLivingBranch(branchLike) && !branchLike.isOrphan) || isPullRequest(branchLike);
+
   return (
-    <li key={branch.name} onMouseEnter={handleMouseEnter}>
-      <Tooltip mouseEnterDelay={0.5} overlay={branch.name} placement="right">
+    <li key={getBranchLikeKey(branchLike)} onMouseEnter={handleMouseEnter}>
+      <Tooltip mouseEnterDelay={0.5} overlay={displayName} placement="right">
         <Link
           className={classNames('navbar-context-meta-branch-menu-item', {
             active: props.selected
           })}
-          to={getProjectBranchUrl(props.component.key, branch)}>
+          to={getBranchLikeUrl(props.component.key, branchLike)}>
           <div className="navbar-context-meta-branch-menu-item-name text-ellipsis">
             <BranchIcon
-              branch={branch}
-              className={classNames('little-spacer-right', {
-                'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan
-              })}
+              branchLike={branchLike}
+              className={classNames('little-spacer-right', { 'big-spacer-left': shouldBeIndented })}
             />
-            {branch.name}
-            {branch.isMain && (
+            {displayName}
+            {isMainBranch(branchLike) && (
               <div className="outline-badge spacer-left">{translate('branches.main_branch')}</div>
             )}
           </div>
           <div className="big-spacer-left note">
-            <BranchStatus branch={branch} concise={true} />
+            <BranchStatus branchLike={branchLike} concise={true} />
           </div>
         </Link>
       </Tooltip>
index ede0242991a61d7604ed07e9bb2124624e6d15a0..b0d387e4fa7fbccb09312822d0537277ebc2e0b2 100644 (file)
@@ -21,7 +21,7 @@ import * as React from 'react';
 import { connect } from 'react-redux';
 import { Link } from 'react-router';
 import ComponentNavBranch from './ComponentNavBranch';
-import { Component, Organization, Branch, Breadcrumb } from '../../../types';
+import { Component, Organization, BranchLike, Breadcrumb } from '../../../types';
 import QualifierIcon from '../../../../components/shared/QualifierIcon';
 import { getOrganizationByKey, areThereCustomOrganizations } from '../../../../store/rootReducer';
 import OrganizationAvatar from '../../../../components/common/OrganizationAvatar';
@@ -37,9 +37,9 @@ interface StateProps {
 }
 
 interface OwnProps {
-  branches: Branch[];
+  branchLikes: BranchLike[];
   component: Component;
-  currentBranch?: Branch;
+  currentBranchLike: BranchLike | undefined;
   location?: any;
 }
 
@@ -70,11 +70,11 @@ export function ComponentNavHeader(props: Props) {
       {component.visibility === 'private' && (
         <PrivateBadge className="spacer-left" qualifier={component.qualifier} />
       )}
-      {props.currentBranch && (
+      {props.currentBranchLike && (
         <ComponentNavBranch
-          branches={props.branches}
+          branchLikes={props.branchLikes}
           component={component}
-          currentBranch={props.currentBranch}
+          currentBranchLike={props.currentBranchLike}
           // to close dropdown on any location change
           location={props.location}
         />
index a52d9a31a51251a19c5ba4e463a87684fc34d83d..80f9ebb31def7a11a11f0dbf5d8438bd3bf52456 100644 (file)
@@ -21,12 +21,13 @@ import * as React from 'react';
 import { Link } from 'react-router';
 import * as classNames from 'classnames';
 import * as PropTypes from 'prop-types';
-import { Branch, Component, Extension } from '../../../types';
+import { BranchLike, Component, Extension } from '../../../types';
 import NavBarTabs from '../../../../components/nav/NavBarTabs';
 import {
   isShortLivingBranch,
-  getBranchName,
-  isLongLivingBranch
+  isPullRequest,
+  isMainBranch,
+  getBranchLikeQuery
 } from '../../../../helpers/branches';
 import { translate } from '../../../../helpers/l10n';
 
@@ -47,7 +48,7 @@ const SETTINGS_URLS = [
 ];
 
 interface Props {
-  branch?: Branch;
+  branchLike: BranchLike | undefined;
   component: Component;
   location?: any;
 }
@@ -78,23 +79,21 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
     return this.props.component.configuration || {};
   }
 
+  getQuery = () => {
+    return { id: this.props.component.key, ...getBranchLikeQuery(this.props.branchLike) };
+  };
+
   renderDashboardLink() {
-    if (isShortLivingBranch(this.props.branch)) {
+    const { branchLike } = this.props;
+
+    if (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) {
       return null;
     }
 
     const pathname = this.isPortfolio() ? '/portfolio' : '/dashboard';
     return (
       <li>
-        <Link
-          to={{
-            pathname,
-            query: {
-              branch: getBranchName(this.props.branch),
-              id: this.props.component.key
-            }
-          }}
-          activeClassName="active">
+        <Link activeClassName="active" to={{ pathname, query: this.getQuery() }}>
           {translate('overview.page')}
         </Link>
       </li>
@@ -108,15 +107,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
 
     return (
       <li>
-        <Link
-          to={{
-            pathname: '/code',
-            query: {
-              branch: getBranchName(this.props.branch),
-              id: this.props.component.key
-            }
-          }}
-          activeClassName="active">
+        <Link to={{ pathname: '/code', query: this.getQuery() }} activeClassName="active">
           {this.isPortfolio() || this.isApplication()
             ? translate('view_projects.page')
             : translate('code.page')}
@@ -126,20 +117,16 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
   }
 
   renderActivityLink() {
-    if (isShortLivingBranch(this.props.branch)) {
+    const { branchLike } = this.props;
+
+    if (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) {
       return null;
     }
 
     return (
       <li>
         <Link
-          to={{
-            pathname: '/project/activity',
-            query: {
-              branch: getBranchName(this.props.branch),
-              id: this.props.component.key
-            }
-          }}
+          to={{ pathname: '/project/activity', query: this.getQuery() }}
           activeClassName="active">
           {translate('project_activity.page')}
         </Link>
@@ -153,16 +140,9 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
     return (
       <li>
         <Link
-          to={{
-            pathname: '/project/issues',
-            query: {
-              branch: getBranchName(this.props.branch),
-              id: this.props.component.key,
-              resolved: 'false'
-            }
-          }}
+          activeClassName="active"
           className={classNames({ active: isIssuesActive })}
-          activeClassName="active">
+          to={{ pathname: '/project/issues', query: { ...this.getQuery(), resolved: 'false' } }}>
           {translate('issues.page')}
         </Link>
       </li>
@@ -170,20 +150,16 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
   }
 
   renderComponentMeasuresLink() {
-    if (isShortLivingBranch(this.props.branch)) {
+    const { branchLike } = this.props;
+
+    if (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) {
       return null;
     }
 
     return (
       <li>
         <Link
-          to={{
-            pathname: '/component_measures',
-            query: {
-              branch: getBranchName(this.props.branch),
-              id: this.props.component.key
-            }
-          }}
+          to={{ pathname: '/component_measures', query: this.getQuery() }}
           activeClassName="active">
           {translate('layout.measures')}
         </Link>
@@ -192,30 +168,14 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
   }
 
   renderAdministration() {
-    const { branch } = this.props;
+    const { branchLike } = this.props;
 
-    if (!this.getConfiguration().showSettings || (branch && !branch.isMain)) {
+    if (!this.getConfiguration().showSettings || (branchLike && !isMainBranch(branchLike))) {
       return null;
     }
 
     const isSettingsActive = SETTINGS_URLS.some(url => window.location.href.indexOf(url) !== -1);
 
-    if (isLongLivingBranch(branch)) {
-      return (
-        <li>
-          <Link
-            className={classNames({ active: isSettingsActive })}
-            id="component-navigation-admin"
-            to={{
-              pathname: '/project/settings',
-              query: { branch: getBranchName(branch), id: this.props.component.key }
-            }}>
-            {translate('branches.branch_settings')}
-          </Link>
-        </li>
-      );
-    }
-
     const adminLinks = this.renderAdministrationLinks();
     if (!adminLinks.some(link => link != null)) {
       return null;
@@ -260,13 +220,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
     return (
       <li key="settings">
         <Link
-          to={{
-            pathname: '/project/settings',
-            query: {
-              branch: getBranchName(this.props.branch),
-              id: this.props.component.key
-            }
-          }}
+          to={{ pathname: '/project/settings', query: this.getQuery() }}
           activeClassName="active">
           {translate('project_settings.page')}
         </Link>
@@ -448,7 +402,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
   };
 
   renderAdminExtensions() {
-    if (this.props.branch && !this.props.branch.isMain) {
+    if (this.props.branchLike && !isMainBranch(this.props.branchLike)) {
       return [];
     }
     const extensions = this.getConfiguration().extensions || [];
@@ -457,7 +411,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
 
   renderExtensions() {
     const extensions = this.props.component.extensions || [];
-    if (!extensions.length || (this.props.branch && !this.props.branch.isMain)) {
+    if (!extensions.length || (this.props.branchLike && !isMainBranch(this.props.branchLike))) {
       return null;
     }
 
index 9bee1a224fed9c2416dfe78d593a969dfc18b265..57f41ffeb3d596be0a9b24146c51d798883bb0eb 100644 (file)
  */
 import * as React from 'react';
 import { connect } from 'react-redux';
-import { Branch, Component, CurrentUser, isLoggedIn, HomePageType, HomePage } from '../../../types';
+import {
+  BranchLike,
+  Component,
+  CurrentUser,
+  isLoggedIn,
+  HomePageType,
+  HomePage
+} from '../../../types';
 import BranchStatus from '../../../../components/common/BranchStatus';
 import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter';
 import Favorite from '../../../../components/controls/Favorite';
@@ -28,7 +35,8 @@ import Tooltip from '../../../../components/controls/Tooltip';
 import {
   isShortLivingBranch,
   isLongLivingBranch,
-  getBranchName
+  isMainBranch,
+  isPullRequest
 } from '../../../../helpers/branches';
 import { translate } from '../../../../helpers/l10n';
 import { getCurrentUser } from '../../../../store/rootReducer';
@@ -38,27 +46,15 @@ interface StateProps {
 }
 
 interface Props extends StateProps {
-  branch?: Branch;
+  branchLike?: BranchLike;
   component: Component;
 }
 
-export function ComponentNavMeta({ branch, component, currentUser }: Props) {
-  const shortBranch = isShortLivingBranch(branch);
-  const mainBranch = !branch || branch.isMain;
-  const longBranch = isLongLivingBranch(branch);
-
-  let currentPage: HomePage | undefined;
-  if (component.qualifier === 'VW' || component.qualifier === 'SVW') {
-    currentPage = { type: HomePageType.Portfolio, component: component.key };
-  } else if (component.qualifier === 'APP') {
-    currentPage = { type: HomePageType.Application, component: component.key };
-  } else if (component.qualifier === 'TRK') {
-    currentPage = {
-      type: HomePageType.Project,
-      component: component.key,
-      branch: getBranchName(branch)
-    };
-  }
+export function ComponentNavMeta({ branchLike, component, currentUser }: Props) {
+  const mainBranch = !branchLike || isMainBranch(branchLike);
+  const longBranch = isLongLivingBranch(branchLike);
+  const currentPage = getCurrentPage(component, branchLike);
+  const displayVersion = component.version !== undefined && (mainBranch || longBranch);
 
   return (
     <div className="navbar-context-meta">
@@ -67,14 +63,13 @@ export function ComponentNavMeta({ branch, component, currentUser }: Props) {
           <DateTimeFormatter date={component.analysisDate} />
         </div>
       )}
-      {component.version &&
-        !shortBranch && (
-          <Tooltip mouseEnterDelay={0.5} overlay={`${translate('version')} ${component.version}`}>
-            <div className="spacer-left text-limited">
-              {translate('version')} {component.version}
-            </div>
-          </Tooltip>
-        )}
+      {displayVersion && (
+        <Tooltip mouseEnterDelay={0.5} overlay={`${translate('version')} ${component.version}`}>
+          <div className="spacer-left text-limited">
+            {translate('version')} {component.version}
+          </div>
+        </Tooltip>
+      )}
       {isLoggedIn(currentUser) && (
         <div className="navbar-context-meta-secondary">
           {mainBranch && (
@@ -90,15 +85,36 @@ export function ComponentNavMeta({ branch, component, currentUser }: Props) {
             )}
         </div>
       )}
-      {shortBranch && (
+      {(isShortLivingBranch(branchLike) || isPullRequest(branchLike)) && (
         <div className="navbar-context-meta-secondary">
-          <BranchStatus branch={branch!} />
+          {isPullRequest(branchLike) &&
+            branchLike.url !== undefined && (
+              <a className="big-spacer-right" href={branchLike.url} rel="nofollow" target="_blank">
+                {translate('branches.see_the_pr')}
+                <i className="icon-detach little-spacer-left" />
+              </a>
+            )}
+          <BranchStatus branchLike={branchLike} />
         </div>
       )}
     </div>
   );
 }
 
+function getCurrentPage(component: Component, branchLike: BranchLike | undefined) {
+  let currentPage: HomePage | undefined;
+  if (component.qualifier === 'VW' || component.qualifier === 'SVW') {
+    currentPage = { type: HomePageType.Portfolio, component: component.key };
+  } else if (component.qualifier === 'APP') {
+    currentPage = { type: HomePageType.Application, component: component.key };
+  } else if (component.qualifier === 'TRK') {
+    const branch =
+      isMainBranch(branchLike) || isLongLivingBranch(branchLike) ? branchLike.name : undefined;
+    currentPage = { type: HomePageType.Project, component: component.key, branch };
+  }
+  return currentPage;
+}
+
 const mapStateToProps = (state: any): StateProps => ({
   currentUser: getCurrentUser(state)
 });
index e4b72cb3564b762417acd8c632158e06cae127c4..b0f633badb5acf1e49f5f373c9f58ae936e6b1af 100644 (file)
@@ -30,7 +30,14 @@ const component = {
 };
 
 it('renders', () => {
-  const wrapper = shallow(<ComponentNav branches={[]} component={component} location={{}} />);
+  const wrapper = shallow(
+    <ComponentNav
+      branchLikes={[]}
+      component={component}
+      currentBranchLike={undefined}
+      location={{}}
+    />
+  );
   wrapper.setState({ isInProgress: true, isPending: true });
   expect(wrapper).toMatchSnapshot();
 });
index dee0d5b79ba45aa813e72f5f0d03e40bc429a67e..7eac067d8dc2bb40ae78e5d1acc7849866a7f678 100644 (file)
@@ -25,21 +25,22 @@ import {
   ShortLivingBranch,
   MainBranch,
   Component,
-  LongLivingBranch
+  LongLivingBranch,
+  PullRequest
 } from '../../../../types';
 import { click } from '../../../../../helpers/testUtils';
 
+const mainBranch: MainBranch = { isMain: true, name: 'master' };
 const fooBranch: LongLivingBranch = { isMain: false, name: 'foo', type: BranchType.LONG };
 
 it('renders main branch', () => {
-  const branch: MainBranch = { isMain: true, name: 'master' };
   const component = {} as Component;
   expect(
     shallow(
       <ComponentNavBranch
-        branches={[branch, fooBranch]}
+        branchLikes={[mainBranch, fooBranch]}
         component={component}
-        currentBranch={branch}
+        currentBranchLike={mainBranch}
       />,
       { context: { branchesEnabled: true } }
     )
@@ -58,9 +59,30 @@ it('renders short-living branch', () => {
   expect(
     shallow(
       <ComponentNavBranch
-        branches={[branch, fooBranch]}
+        branchLikes={[branch, fooBranch]}
         component={component}
-        currentBranch={branch}
+        currentBranchLike={branch}
+      />,
+      { context: { branchesEnabled: true } }
+    )
+  ).toMatchSnapshot();
+});
+
+it('renders pull request', () => {
+  const pullRequest: PullRequest = {
+    base: 'master',
+    branch: 'feature',
+    key: '1234',
+    title: 'Feature PR',
+    url: 'https://example.com/pull/1234'
+  };
+  const component = {} as Component;
+  expect(
+    shallow(
+      <ComponentNavBranch
+        branchLikes={[pullRequest, fooBranch]}
+        component={component}
+        currentBranchLike={pullRequest}
       />,
       { context: { branchesEnabled: true } }
     )
@@ -68,13 +90,12 @@ it('renders short-living branch', () => {
 });
 
 it('opens menu', () => {
-  const branch: MainBranch = { isMain: true, name: 'master' };
   const component = {} as Component;
   const wrapper = shallow(
     <ComponentNavBranch
-      branches={[branch, fooBranch]}
+      branchLikes={[mainBranch, fooBranch]}
       component={component}
-      currentBranch={branch}
+      currentBranchLike={mainBranch}
     />,
     { context: { branchesEnabled: true } }
   );
@@ -84,10 +105,13 @@ it('opens menu', () => {
 });
 
 it('renders single branch popup', () => {
-  const branch: MainBranch = { isMain: true, name: 'master' };
   const component = {} as Component;
   const wrapper = shallow(
-    <ComponentNavBranch branches={[branch]} component={component} currentBranch={branch} />,
+    <ComponentNavBranch
+      branchLikes={[mainBranch]}
+      component={component}
+      currentBranchLike={mainBranch}
+    />,
     { context: { branchesEnabled: true } }
   );
   expect(wrapper).toMatchSnapshot();
@@ -97,13 +121,12 @@ it('renders single branch popup', () => {
 });
 
 it('renders no branch support popup', () => {
-  const branch: MainBranch = { isMain: true, name: 'master' };
   const component = {} as Component;
   const wrapper = shallow(
     <ComponentNavBranch
-      branches={[branch, fooBranch]}
+      branchLikes={[mainBranch, fooBranch]}
       component={component}
-      currentBranch={branch}
+      currentBranchLike={mainBranch}
     />,
     { context: { branchesEnabled: false } }
   );
@@ -114,10 +137,13 @@ it('renders no branch support popup', () => {
 });
 
 it('renders nothing on SonarCloud without branch support', () => {
-  const branch: MainBranch = { isMain: true, name: 'master' };
   const component = {} as Component;
   const wrapper = shallow(
-    <ComponentNavBranch branches={[branch]} component={component} currentBranch={branch} />,
+    <ComponentNavBranch
+      branchLikes={[mainBranch]}
+      component={component}
+      currentBranchLike={mainBranch}
+    />,
     { context: { branchesEnabled: false, onSonarCloud: true } }
   );
   expect(wrapper.type()).toBeNull();
index ec4843e138f3113ec3016f88b3d8d4a252c1c452..bb43ec60c63fb7bddd9d036270bd6166bb8b424e 100644 (file)
@@ -25,7 +25,8 @@ import {
   MainBranch,
   ShortLivingBranch,
   LongLivingBranch,
-  Component
+  Component,
+  PullRequest
 } from '../../../../types';
 import { elementKeydown } from '../../../../../helpers/testUtils';
 
@@ -35,9 +36,15 @@ it('renders list', () => {
   expect(
     shallow(
       <ComponentNavBranchesMenu
-        branches={[mainBranch(), shortBranch('foo'), longBranch('bar'), shortBranch('baz', true)]}
+        branchLikes={[
+          mainBranch(),
+          shortBranch('foo'),
+          longBranch('bar'),
+          shortBranch('baz', true),
+          pullRequest('qux')
+        ]}
         component={component}
-        currentBranch={mainBranch()}
+        currentBranchLike={mainBranch()}
         onClose={jest.fn()}
       />
     )
@@ -47,9 +54,9 @@ it('renders list', () => {
 it('searches', () => {
   const wrapper = shallow(
     <ComponentNavBranchesMenu
-      branches={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]}
+      branchLikes={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]}
       component={component}
-      currentBranch={mainBranch()}
+      currentBranchLike={mainBranch()}
       onClose={jest.fn()}
     />
   );
@@ -60,21 +67,21 @@ it('searches', () => {
 it('selects next & previous', () => {
   const wrapper = shallow(
     <ComponentNavBranchesMenu
-      branches={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]}
+      branchLikes={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]}
       component={component}
-      currentBranch={mainBranch()}
+      currentBranchLike={mainBranch()}
       onClose={jest.fn()}
     />
   );
   elementKeydown(wrapper.find('SearchBox'), 40);
   wrapper.update();
-  expect(wrapper.state().selected).toBe('foo');
+  expect(wrapper.state().selected).toEqual(shortBranch('foo'));
   elementKeydown(wrapper.find('SearchBox'), 40);
   wrapper.update();
-  expect(wrapper.state().selected).toBe('foobar');
+  expect(wrapper.state().selected).toEqual(shortBranch('foobar'));
   elementKeydown(wrapper.find('SearchBox'), 38);
   wrapper.update();
-  expect(wrapper.state().selected).toBe('foo');
+  expect(wrapper.state().selected).toEqual(shortBranch('foo'));
 });
 
 function mainBranch(): MainBranch {
@@ -95,3 +102,13 @@ function shortBranch(name: string, isOrphan?: true): ShortLivingBranch {
 function longBranch(name: string): LongLivingBranch {
   return { isMain: false, name, type: BranchType.LONG };
 }
+
+function pullRequest(title: string): PullRequest {
+  return {
+    base: 'master',
+    branch: 'feature',
+    key: '1234',
+    status: { bugs: 0, codeSmells: 0, vulnerabilities: 0 },
+    title
+  };
+}
index b411df4578ae4d5c0348a39d50199687d5b7a4d7..0635bea82a8cc16c4b6b8d5a9aa38cc4bcb02e94 100644 (file)
@@ -35,7 +35,7 @@ const shortBranch: ShortLivingBranch = {
 const mainBranch: MainBranch = { isMain: true, name: 'master' };
 
 it('renders main branch', () => {
-  expect(shallowRender({ branch: mainBranch })).toMatchSnapshot();
+  expect(shallowRender({ branchLike: mainBranch })).toMatchSnapshot();
 });
 
 it('renders short-living branch', () => {
@@ -43,13 +43,14 @@ it('renders short-living branch', () => {
 });
 
 it('renders short-living orhpan branch', () => {
-  expect(shallowRender({ branch: { ...shortBranch, isOrphan: true } })).toMatchSnapshot();
+  const orhpan: ShortLivingBranch = { ...shortBranch, isOrphan: true };
+  expect(shallowRender({ branchLike: orhpan })).toMatchSnapshot();
 });
 
 function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {
   return shallow(
     <ComponentNavBranchesMenuItem
-      branch={shortBranch}
+      branchLike={shortBranch}
       component={component}
       onSelect={jest.fn()}
       selected={false}
index 912973b455852a567579371be29335653702136c..676a38a577b428a7cd8c7c72af2e28fcf078133f 100644 (file)
@@ -32,7 +32,12 @@ it('should not render breadcrumbs with one element', () => {
     visibility: 'public'
   };
   const result = shallow(
-    <ComponentNavHeader branches={[]} component={component} shouldOrganizationBeDisplayed={false} />
+    <ComponentNavHeader
+      branchLikes={[]}
+      component={component}
+      currentBranchLike={undefined}
+      shouldOrganizationBeDisplayed={false}
+    />
   );
   expect(result).toMatchSnapshot();
 });
@@ -53,8 +58,9 @@ it('should render organization', () => {
   };
   const result = shallow(
     <ComponentNavHeader
-      branches={[]}
+      branchLikes={[]}
       component={component}
+      currentBranchLike={undefined}
       organization={organization}
       shouldOrganizationBeDisplayed={true}
     />
@@ -72,7 +78,12 @@ it('renders private badge', () => {
     visibility: 'private'
   };
   const result = shallow(
-    <ComponentNavHeader branches={[]} component={component} shouldOrganizationBeDisplayed={false} />
+    <ComponentNavHeader
+      branchLikes={[]}
+      component={component}
+      currentBranchLike={undefined}
+      shouldOrganizationBeDisplayed={false}
+    />
   );
   expect(result.find('PrivateBadge')).toHaveLength(1);
 });
index eb400536ed1bd86eee415e48dd868502cff38715..ef502e33aad34f238902bbfa8e75606698532e24 100644 (file)
@@ -39,7 +39,7 @@ it('should work with extensions', () => {
     extensions: [{ key: 'component-foo', name: 'ComponentFoo' }]
   };
   expect(
-    shallow(<ComponentNavMenu branch={mainBranch} component={component} />, {
+    shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, {
       context: { branchesEnabled: true }
     })
   ).toMatchSnapshot();
@@ -58,7 +58,7 @@ it('should work with multiple extensions', () => {
     ]
   };
   expect(
-    shallow(<ComponentNavMenu branch={mainBranch} component={component} />, {
+    shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, {
       context: { branchesEnabled: true }
     })
   ).toMatchSnapshot();
@@ -77,7 +77,7 @@ it('should work for short-living branches', () => {
     extensions: [{ key: 'component-foo', name: 'ComponentFoo' }]
   };
   expect(
-    shallow(<ComponentNavMenu branch={branch} component={component} />, {
+    shallow(<ComponentNavMenu branchLike={branch} component={component} />, {
       context: { branchesEnabled: true }
     })
   ).toMatchSnapshot();
@@ -89,7 +89,7 @@ it('should work for long-living branches', () => {
     expect(
       shallow(
         <ComponentNavMenu
-          branch={branch}
+          branchLike={branch}
           component={{
             ...baseComponent,
             configuration: { showSettings },
@@ -109,7 +109,7 @@ it('should work for all qualifiers', () => {
   function checkWithQualifier(qualifier: string) {
     const component = { ...baseComponent, configuration: { showSettings: true }, qualifier };
     expect(
-      shallow(<ComponentNavMenu branch={mainBranch} component={component} />, {
+      shallow(<ComponentNavMenu branchLike={mainBranch} component={component} />, {
         context: { branchesEnabled: true }
       })
     ).toMatchSnapshot();
index c964e23552a6f25d0d59669357765172606cd683..62bdbdbb71506a5c82e987ab9599a124415df05a 100644 (file)
@@ -20,7 +20,7 @@
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import { ComponentNavMeta } from '../ComponentNavMeta';
-import { BranchType, ShortLivingBranch, LongLivingBranch } from '../../../../types';
+import { BranchType, ShortLivingBranch, LongLivingBranch, PullRequest } from '../../../../types';
 
 const component = {
   analysisDate: '2017-01-02T00:00:00.000Z',
@@ -42,7 +42,11 @@ it('renders status of short-living branch', () => {
   };
   expect(
     shallow(
-      <ComponentNavMeta branch={branch} component={component} currentUser={{ isLoggedIn: false }} />
+      <ComponentNavMeta
+        branchLike={branch}
+        component={component}
+        currentUser={{ isLoggedIn: false }}
+      />
     )
   ).toMatchSnapshot();
 });
@@ -56,7 +60,31 @@ it('renders meta for long-living branch', () => {
   };
   expect(
     shallow(
-      <ComponentNavMeta branch={branch} component={component} currentUser={{ isLoggedIn: false }} />
+      <ComponentNavMeta
+        branchLike={branch}
+        component={component}
+        currentUser={{ isLoggedIn: false }}
+      />
+    )
+  ).toMatchSnapshot();
+});
+
+it('renders meta for pull request', () => {
+  const pullRequest: PullRequest = {
+    base: 'master',
+    branch: 'feature',
+    key: '1234',
+    status: { bugs: 0, codeSmells: 2, vulnerabilities: 3 },
+    title: 'Feature PR',
+    url: 'https://example.com/pull/1234'
+  };
+  expect(
+    shallow(
+      <ComponentNavMeta
+        branchLike={pullRequest}
+        component={component}
+        currentUser={{ isLoggedIn: false }}
+      />
     )
   ).toMatchSnapshot();
 });
index e52606979cfb2781bd7e8b3726700290cd6c4dd0..2d5fe354989884e3c7a7d2b364173b23064f5e05 100644 (file)
@@ -6,7 +6,7 @@ exports[`renders 1`] = `
   id="context-navigation"
 >
   <Connect(ComponentNavHeader)
-    branches={Array []}
+    branchLikes={Array []}
     component={
       Object {
         "breadcrumbs": Array [
index 3571893cc962c330b9de8a16198cb3263f207dcc..6e45772bb56d8efed86f290fca7ff7c093817a27 100644 (file)
@@ -10,7 +10,7 @@ exports[`renders main branch 1`] = `
     onClick={[Function]}
   >
     <BranchIcon
-      branch={
+      branchLike={
         Object {
           "isMain": true,
           "name": "master",
@@ -41,7 +41,7 @@ exports[`renders no branch support popup 1`] = `
   className="navbar-context-branches"
 >
   <BranchIcon
-    branch={
+    branchLike={
       Object {
         "isMain": true,
         "name": "master",
@@ -77,6 +77,63 @@ exports[`renders no branch support popup 1`] = `
 </div>
 `;
 
+exports[`renders pull request 1`] = `
+<div
+  className="navbar-context-branches dropdown"
+>
+  <a
+    className="link-base-color link-no-underline"
+    href="#"
+    onClick={[Function]}
+  >
+    <BranchIcon
+      branchLike={
+        Object {
+          "base": "master",
+          "branch": "feature",
+          "key": "1234",
+          "title": "Feature PR",
+          "url": "https://example.com/pull/1234",
+        }
+      }
+      className="little-spacer-right"
+    />
+    <Tooltip
+      mouseEnterDelay={1}
+      overlay="1234 – Feature PR"
+      placement="bottom"
+    >
+      <span
+        className="text-limited text-top"
+      >
+        1234 – Feature PR
+      </span>
+    </Tooltip>
+    <i
+      className="icon-dropdown little-spacer-left"
+    />
+  </a>
+  <span
+    className="note big-spacer-left text-lowercase"
+  >
+    <FormattedMessage
+      defaultMessage="branches.pull_request.for_merge_into_x_from_y"
+      id="branches.pull_request.for_merge_into_x_from_y"
+      values={
+        Object {
+          "base": <strong>
+            master
+          </strong>,
+          "branch": <strong>
+            feature
+          </strong>,
+        }
+      }
+    />
+  </span>
+</div>
+`;
+
 exports[`renders short-living branch 1`] = `
 <div
   className="navbar-context-branches dropdown"
@@ -87,7 +144,7 @@ exports[`renders short-living branch 1`] = `
     onClick={[Function]}
   >
     <BranchIcon
-      branch={
+      branchLike={
         Object {
           "isMain": false,
           "mergeBranch": "master",
@@ -134,7 +191,7 @@ exports[`renders single branch popup 1`] = `
   className="navbar-context-branches"
 >
   <BranchIcon
-    branch={
+    branchLike={
       Object {
         "isMain": true,
         "name": "master",
index 6946e02b328f2b4c0c40631509c95a3e47dddeb7..cc51efd1976e7fc35b31ad4bcab97f374b9102fe 100644 (file)
@@ -19,10 +19,10 @@ exports[`renders list 1`] = `
     className="menu menu-vertically-limited"
   >
     <React.Fragment
-      key="master"
+      key="branch-master"
     >
       <ComponentNavBranchesMenuItem
-        branch={
+        branchLike={
           Object {
             "isMain": true,
             "name": "master",
@@ -33,13 +33,45 @@ exports[`renders list 1`] = `
             "key": "component",
           }
         }
-        key="master"
+        key="branch-master"
         onSelect={[Function]}
         selected={true}
       />
     </React.Fragment>
     <React.Fragment
-      key="baz"
+      key="pull-request-1234"
+    >
+      <li
+        className="dropdown-header navbar-context-meta-branch-menu-title"
+      >
+        branches.pull_requests
+      </li>
+      <ComponentNavBranchesMenuItem
+        branchLike={
+          Object {
+            "base": "master",
+            "branch": "feature",
+            "key": "1234",
+            "status": Object {
+              "bugs": 0,
+              "codeSmells": 0,
+              "vulnerabilities": 0,
+            },
+            "title": "qux",
+          }
+        }
+        component={
+          Object {
+            "key": "component",
+          }
+        }
+        key="pull-request-1234"
+        onSelect={[Function]}
+        selected={false}
+      />
+    </React.Fragment>
+    <React.Fragment
+      key="branch-baz"
     >
       <li
         className="divider"
@@ -58,7 +90,7 @@ exports[`renders list 1`] = `
         </Tooltip>
       </li>
       <ComponentNavBranchesMenuItem
-        branch={
+        branchLike={
           Object {
             "isMain": false,
             "isOrphan": true,
@@ -77,16 +109,16 @@ exports[`renders list 1`] = `
             "key": "component",
           }
         }
-        key="baz"
+        key="branch-baz"
         onSelect={[Function]}
         selected={false}
       />
     </React.Fragment>
     <React.Fragment
-      key="foo"
+      key="branch-foo"
     >
       <ComponentNavBranchesMenuItem
-        branch={
+        branchLike={
           Object {
             "isMain": false,
             "isOrphan": undefined,
@@ -105,19 +137,19 @@ exports[`renders list 1`] = `
             "key": "component",
           }
         }
-        key="foo"
+        key="branch-foo"
         onSelect={[Function]}
         selected={false}
       />
     </React.Fragment>
     <React.Fragment
-      key="bar"
+      key="branch-bar"
     >
       <li
         className="divider"
       />
       <ComponentNavBranchesMenuItem
-        branch={
+        branchLike={
           Object {
             "isMain": false,
             "name": "bar",
@@ -129,13 +161,13 @@ exports[`renders list 1`] = `
             "key": "component",
           }
         }
-        key="bar"
+        key="branch-bar"
         onSelect={[Function]}
         selected={false}
       />
     </React.Fragment>
     <React.Fragment
-      key="baz"
+      key="branch-baz"
     >
       <li
         className="divider"
@@ -154,7 +186,7 @@ exports[`renders list 1`] = `
         </Tooltip>
       </li>
       <ComponentNavBranchesMenuItem
-        branch={
+        branchLike={
           Object {
             "isMain": false,
             "isOrphan": true,
@@ -173,7 +205,7 @@ exports[`renders list 1`] = `
             "key": "component",
           }
         }
-        key="baz"
+        key="branch-baz"
         onSelect={[Function]}
         selected={false}
       />
@@ -201,10 +233,15 @@ exports[`searches 1`] = `
     className="menu menu-vertically-limited"
   >
     <React.Fragment
-      key="foobar"
+      key="branch-foobar"
     >
+      <li
+        className="dropdown-header navbar-context-meta-branch-menu-title"
+      >
+        branches.short_lived_branches
+      </li>
       <ComponentNavBranchesMenuItem
-        branch={
+        branchLike={
           Object {
             "isMain": false,
             "isOrphan": undefined,
@@ -223,19 +260,19 @@ exports[`searches 1`] = `
             "key": "component",
           }
         }
-        key="foobar"
+        key="branch-foobar"
         onSelect={[Function]}
         selected={true}
       />
     </React.Fragment>
     <React.Fragment
-      key="bar"
+      key="branch-bar"
     >
       <li
         className="divider"
       />
       <ComponentNavBranchesMenuItem
-        branch={
+        branchLike={
           Object {
             "isMain": false,
             "name": "bar",
@@ -247,7 +284,7 @@ exports[`searches 1`] = `
             "key": "component",
           }
         }
-        key="bar"
+        key="branch-bar"
         onSelect={[Function]}
         selected={false}
       />
index ceba812e99c423bfd335658abac5c6ab9cbcf675..781da6de591e9721d9497fc38128bac15e0313b0 100644 (file)
@@ -2,7 +2,7 @@
 
 exports[`renders main branch 1`] = `
 <li
-  key="master"
+  key="branch-master"
   onMouseEnter={[Function]}
 >
   <Tooltip
@@ -27,7 +27,7 @@ exports[`renders main branch 1`] = `
         className="navbar-context-meta-branch-menu-item-name text-ellipsis"
       >
         <BranchIcon
-          branch={
+          branchLike={
             Object {
               "isMain": true,
               "name": "master",
@@ -46,7 +46,7 @@ exports[`renders main branch 1`] = `
         className="big-spacer-left note"
       >
         <BranchStatus
-          branch={
+          branchLike={
             Object {
               "isMain": true,
               "name": "master",
@@ -62,7 +62,7 @@ exports[`renders main branch 1`] = `
 
 exports[`renders short-living branch 1`] = `
 <li
-  key="foo"
+  key="branch-foo"
   onMouseEnter={[Function]}
 >
   <Tooltip
@@ -89,7 +89,7 @@ exports[`renders short-living branch 1`] = `
         className="navbar-context-meta-branch-menu-item-name text-ellipsis"
       >
         <BranchIcon
-          branch={
+          branchLike={
             Object {
               "isMain": false,
               "mergeBranch": "master",
@@ -110,7 +110,7 @@ exports[`renders short-living branch 1`] = `
         className="big-spacer-left note"
       >
         <BranchStatus
-          branch={
+          branchLike={
             Object {
               "isMain": false,
               "mergeBranch": "master",
@@ -133,7 +133,7 @@ exports[`renders short-living branch 1`] = `
 
 exports[`renders short-living orhpan branch 1`] = `
 <li
-  key="foo"
+  key="branch-foo"
   onMouseEnter={[Function]}
 >
   <Tooltip
@@ -160,7 +160,7 @@ exports[`renders short-living orhpan branch 1`] = `
         className="navbar-context-meta-branch-menu-item-name text-ellipsis"
       >
         <BranchIcon
-          branch={
+          branchLike={
             Object {
               "isMain": false,
               "isOrphan": true,
@@ -182,7 +182,7 @@ exports[`renders short-living orhpan branch 1`] = `
         className="big-spacer-left note"
       >
         <BranchStatus
-          branch={
+          branchLike={
             Object {
               "isMain": false,
               "isOrphan": true,
index eb990ac64011a7ffa1e4edfd9ef73f3b3901c6af..dd13db8a75c181c14fc0c367f1821b69dd30b3e7 100644 (file)
@@ -23,7 +23,6 @@ exports[`should not render breadcrumbs with one element 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "my-project",
           },
         }
@@ -91,7 +90,6 @@ exports[`should render organization 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "my-project",
           },
         }
index 22c56c679300d32b09472e2ebf64ab470d5aacb6..5665e5ab9b40e11316718eb900879befce1bc8c5 100644 (file)
@@ -11,7 +11,6 @@ exports[`should work for all qualifiers 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -30,7 +29,6 @@ exports[`should work for all qualifiers 1`] = `
         Object {
           "pathname": "/project/issues",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
             "resolved": "false",
           },
@@ -49,7 +47,6 @@ exports[`should work for all qualifiers 1`] = `
         Object {
           "pathname": "/component_measures",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -67,7 +64,6 @@ exports[`should work for all qualifiers 1`] = `
         Object {
           "pathname": "/code",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -85,7 +81,6 @@ exports[`should work for all qualifiers 1`] = `
         Object {
           "pathname": "/project/activity",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -123,7 +118,6 @@ exports[`should work for all qualifiers 1`] = `
             Object {
               "pathname": "/project/settings",
               "query": Object {
-                "branch": undefined,
                 "id": "foo",
               },
             }
@@ -205,7 +199,6 @@ exports[`should work for all qualifiers 2`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -224,7 +217,6 @@ exports[`should work for all qualifiers 2`] = `
         Object {
           "pathname": "/project/issues",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
             "resolved": "false",
           },
@@ -243,7 +235,6 @@ exports[`should work for all qualifiers 2`] = `
         Object {
           "pathname": "/component_measures",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -261,7 +252,6 @@ exports[`should work for all qualifiers 2`] = `
         Object {
           "pathname": "/code",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -279,7 +269,6 @@ exports[`should work for all qualifiers 2`] = `
         Object {
           "pathname": "/project/activity",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -317,7 +306,6 @@ exports[`should work for all qualifiers 2`] = `
             Object {
               "pathname": "/project/settings",
               "query": Object {
-                "branch": undefined,
                 "id": "foo",
               },
             }
@@ -342,7 +330,6 @@ exports[`should work for all qualifiers 3`] = `
         Object {
           "pathname": "/portfolio",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -361,7 +348,6 @@ exports[`should work for all qualifiers 3`] = `
         Object {
           "pathname": "/project/issues",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
             "resolved": "false",
           },
@@ -380,7 +366,6 @@ exports[`should work for all qualifiers 3`] = `
         Object {
           "pathname": "/component_measures",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -398,7 +383,6 @@ exports[`should work for all qualifiers 3`] = `
         Object {
           "pathname": "/code",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -416,7 +400,6 @@ exports[`should work for all qualifiers 3`] = `
         Object {
           "pathname": "/project/activity",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -478,7 +461,6 @@ exports[`should work for all qualifiers 4`] = `
         Object {
           "pathname": "/portfolio",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -497,7 +479,6 @@ exports[`should work for all qualifiers 4`] = `
         Object {
           "pathname": "/project/issues",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
             "resolved": "false",
           },
@@ -516,7 +497,6 @@ exports[`should work for all qualifiers 4`] = `
         Object {
           "pathname": "/component_measures",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -534,7 +514,6 @@ exports[`should work for all qualifiers 4`] = `
         Object {
           "pathname": "/code",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -552,7 +531,6 @@ exports[`should work for all qualifiers 4`] = `
         Object {
           "pathname": "/project/activity",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -575,7 +553,6 @@ exports[`should work for all qualifiers 5`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -594,7 +571,6 @@ exports[`should work for all qualifiers 5`] = `
         Object {
           "pathname": "/project/issues",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
             "resolved": "false",
           },
@@ -613,7 +589,6 @@ exports[`should work for all qualifiers 5`] = `
         Object {
           "pathname": "/component_measures",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -631,7 +606,6 @@ exports[`should work for all qualifiers 5`] = `
         Object {
           "pathname": "/code",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -649,7 +623,6 @@ exports[`should work for all qualifiers 5`] = `
         Object {
           "pathname": "/project/activity",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -948,7 +921,6 @@ exports[`should work with extensions 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -967,7 +939,6 @@ exports[`should work with extensions 1`] = `
         Object {
           "pathname": "/project/issues",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
             "resolved": "false",
           },
@@ -986,7 +957,6 @@ exports[`should work with extensions 1`] = `
         Object {
           "pathname": "/component_measures",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -1004,7 +974,6 @@ exports[`should work with extensions 1`] = `
         Object {
           "pathname": "/code",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -1022,7 +991,6 @@ exports[`should work with extensions 1`] = `
         Object {
           "pathname": "/project/activity",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -1060,7 +1028,6 @@ exports[`should work with extensions 1`] = `
             Object {
               "pathname": "/project/settings",
               "query": Object {
-                "branch": undefined,
                 "id": "foo",
               },
             }
@@ -1200,7 +1167,6 @@ exports[`should work with multiple extensions 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -1219,7 +1185,6 @@ exports[`should work with multiple extensions 1`] = `
         Object {
           "pathname": "/project/issues",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
             "resolved": "false",
           },
@@ -1238,7 +1203,6 @@ exports[`should work with multiple extensions 1`] = `
         Object {
           "pathname": "/component_measures",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -1256,7 +1220,6 @@ exports[`should work with multiple extensions 1`] = `
         Object {
           "pathname": "/code",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -1274,7 +1237,6 @@ exports[`should work with multiple extensions 1`] = `
         Object {
           "pathname": "/project/activity",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -1312,7 +1274,6 @@ exports[`should work with multiple extensions 1`] = `
             Object {
               "pathname": "/project/settings",
               "query": Object {
-                "branch": undefined,
                 "id": "foo",
               },
             }
index 437ab7a3477263c2e9574237b7ca818e9184ba51..46e1c4becd7f15b3c951d235953bfd7ca8196225 100644 (file)
@@ -27,6 +27,51 @@ exports[`renders meta for long-living branch 1`] = `
 </div>
 `;
 
+exports[`renders meta for pull request 1`] = `
+<div
+  className="navbar-context-meta"
+>
+  <div
+    className="spacer-left"
+  >
+    <DateTimeFormatter
+      date="2017-01-02T00:00:00.000Z"
+    />
+  </div>
+  <div
+    className="navbar-context-meta-secondary"
+  >
+    <a
+      className="big-spacer-right"
+      href="https://example.com/pull/1234"
+      rel="nofollow"
+      target="_blank"
+    >
+      branches.see_the_pr
+      <i
+        className="icon-detach little-spacer-left"
+      />
+    </a>
+    <BranchStatus
+      branchLike={
+        Object {
+          "base": "master",
+          "branch": "feature",
+          "key": "1234",
+          "status": Object {
+            "bugs": 0,
+            "codeSmells": 2,
+            "vulnerabilities": 3,
+          },
+          "title": "Feature PR",
+          "url": "https://example.com/pull/1234",
+        }
+      }
+    />
+  </div>
+</div>
+`;
+
 exports[`renders status of short-living branch 1`] = `
 <div
   className="navbar-context-meta"
@@ -42,7 +87,7 @@ exports[`renders status of short-living branch 1`] = `
     className="navbar-context-meta-secondary"
   >
     <BranchStatus
-      branch={
+      branchLike={
         Object {
           "isMain": false,
           "mergeBranch": "master",
index 0a9d6781bf0eecc8738c884e7edc5bf6bcfd3705..4ad1c1e2e2dc668d59336f7b91a8065681e65ba0 100644 (file)
@@ -21,7 +21,6 @@ exports[`renders favorite 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -70,7 +69,6 @@ exports[`renders match 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -118,7 +116,6 @@ exports[`renders organizations 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -171,7 +168,6 @@ exports[`renders organizations 2`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -219,7 +215,6 @@ exports[`renders projects 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "qwe",
           },
         }
@@ -272,7 +267,6 @@ exports[`renders recently browsed 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -320,7 +314,6 @@ exports[`renders selected 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -366,7 +359,6 @@ exports[`renders selected 2`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
index 92136630ff4f583c2a356eb287d8ca8115407fe2..106d76e679f200f9f670efc05c9b4b7f783ab7c6 100644 (file)
@@ -36,7 +36,15 @@ export interface AppState {
   qualifiers: string[];
 }
 
-export type Branch = MainBranch | LongLivingBranch | ShortLivingBranch;
+export interface Branch {
+  analysisDate?: string;
+  isMain: boolean;
+  name: string;
+}
+
+export type BranchLike = Branch | PullRequest;
+
+export type BranchParameters = { branch?: string } | { pullRequest?: string };
 
 export enum BranchType {
   LONG = 'LONG',
@@ -273,23 +281,14 @@ export interface LoggedInUser extends CurrentUser {
   name: string;
 }
 
-export interface LongLivingBranch {
-  analysisDate?: string;
+export interface LongLivingBranch extends Branch {
   isMain: false;
-  name: string;
-  status?: {
-    qualityGateStatus: string;
-  };
+  status?: { qualityGateStatus: string };
   type: BranchType.LONG;
 }
 
-export interface MainBranch {
-  analysisDate?: string;
+export interface MainBranch extends Branch {
   isMain: true;
-  name: string;
-  status?: {
-    qualityGateStatus: string;
-  };
 }
 
 export interface Metric {
@@ -352,6 +351,21 @@ export interface ProjectLink {
   url: string;
 }
 
+export interface PullRequest {
+  analysisDate?: string;
+  base: string;
+  branch: string;
+  key: string;
+  isOrphan?: true;
+  status?: {
+    bugs: number;
+    codeSmells: number;
+    vulnerabilities: number;
+  };
+  title: string;
+  url?: string;
+}
+
 export interface Rule {
   isTemplate?: boolean;
   key: string;
@@ -419,12 +433,10 @@ export enum RuleScope {
   All = 'ALL'
 }
 
-export interface ShortLivingBranch {
-  analysisDate?: string;
+export interface ShortLivingBranch extends Branch {
   isMain: false;
   isOrphan?: true;
   mergeBranch: string;
-  name: string;
   status?: {
     bugs: number;
     codeSmells: number;
index 2c63a8553b9398987d822b3e3717ca15ea8bd4f9..a60d730c30a7ae92cc582662aeb468fa558ddd95 100644 (file)
@@ -23,7 +23,7 @@ import { receiveValues } from '../settings/store/values/actions';
 export const fetchAboutPageSettings = () => dispatch => {
   const keys = ['sonar.lf.aboutText'];
 
-  return getValues(keys.join()).then(values => {
+  return getValues({ keys: keys.join() }).then(values => {
     dispatch(receiveValues(values));
   });
 };
index 2fb0f7c15d1cb3e52ee377ec2f896c9bfc76dcfb..33c554244f1999ea4af81d7517b62fe4c521646a 100644 (file)
@@ -23,7 +23,6 @@ exports[`should match snapshot 1`] = `
               Object {
                 "pathname": "/dashboard",
                 "query": Object {
-                  "branch": undefined,
                   "id": "foo",
                 },
               }
index 89dbf7c970550a56c05ec38881f63ba2c800e7c4..4361e0d0849dd698ed6133a3702c5e61c7fb34f7 100644 (file)
@@ -30,7 +30,7 @@ export const fetchMyOrganizations = () => (dispatch: Dispatch<any>) => {
 };
 
 export const fetchIfAnyoneCanCreateOrganizations = () => (dispatch: Dispatch<any>) => {
-  return getValues('sonar.organizations.anyoneCanCreate').then(values => {
+  return getValues({ keys: 'sonar.organizations.anyoneCanCreate' }).then(values => {
     dispatch(receiveValues(values, undefined));
   });
 };
index 61fba1730da605c11c7d620c4817cc526c621de6..a81919215b15abf09ad837924caf80db04e32802 100644 (file)
@@ -23,9 +23,16 @@ import TaskType from './TaskType';
 import { Task } from '../types';
 import QualifierIcon from '../../../components/shared/QualifierIcon';
 import Organization from '../../../components/shared/Organization';
-import { getProjectUrl } from '../../../helpers/urls';
+import {
+  getProjectUrl,
+  getShortLivingBranchUrl,
+  getLongLivingBranchUrl,
+  getPullRequestUrl
+} from '../../../helpers/urls';
 import ShortLivingBranchIcon from '../../../components/icons-components/ShortLivingBranchIcon';
 import LongLivingBranchIcon from '../../../components/icons-components/LongLivingBranchIcon';
+import PullRequestIcon from '../../../components/icons-components/PullRequestIcon';
+import Tooltip from '../../../components/controls/Tooltip';
 
 interface Props {
   task: Task;
@@ -45,8 +52,10 @@ export default function TaskComponent({ task }: Props) {
     <td>
       {task.branchType === 'SHORT' && <ShortLivingBranchIcon className="little-spacer-right" />}
       {task.branchType === 'LONG' && <LongLivingBranchIcon className="little-spacer-right" />}
+      {task.pullRequest !== undefined && <PullRequestIcon className="little-spacer-right" />}
 
       {!task.branchType &&
+        !task.pullRequest &&
         task.componentQualifier && (
           <span className="little-spacer-right">
             <QualifierIcon qualifier={task.componentQualifier} />
@@ -56,7 +65,7 @@ export default function TaskComponent({ task }: Props) {
       {task.organization && <Organization organizationKey={task.organization} />}
 
       {task.componentName && (
-        <Link className="spacer-right" to={getProjectUrl(task.componentKey, task.branch)}>
+        <Link className="spacer-right" to={getTaskComponentUrl(task.componentKey, task)}>
           {task.componentName}
 
           {task.branch && (
@@ -65,6 +74,15 @@ export default function TaskComponent({ task }: Props) {
               {task.branch}
             </span>
           )}
+
+          {task.pullRequest && (
+            <Tooltip overlay={task.pullRequestTitle}>
+              <span className="text-limited text-text-top">
+                <span style={{ marginLeft: 5, marginRight: 5 }}>/</span>
+                {task.pullRequest}
+              </span>
+            </Tooltip>
+          )}
         </Link>
       )}
 
@@ -72,3 +90,15 @@ export default function TaskComponent({ task }: Props) {
     </td>
   );
 }
+
+function getTaskComponentUrl(componentKey: string, task: Task) {
+  if (task.branch && task.branchType === 'SHORT') {
+    return getShortLivingBranchUrl(componentKey, task.branchType);
+  } else if (task.branchType && task.branchType === 'LONG') {
+    return getLongLivingBranchUrl(componentKey, task.branchType);
+  } else if (task.pullRequest) {
+    return getPullRequestUrl(componentKey, task.pullRequest);
+  } else {
+    return getProjectUrl(componentKey);
+  }
+}
index 93a63cdafd4bafd02975c5d3a85d55aa802efca8..f71a484187b6c58d4a599dba61e08a4b8424ad73 100644 (file)
@@ -20,7 +20,6 @@ exports[`renders 1`] = `
       Object {
         "pathname": "/dashboard",
         "query": Object {
-          "branch": undefined,
           "id": "foo",
         },
       }
@@ -67,7 +66,6 @@ exports[`renders 3`] = `
       Object {
         "pathname": "/dashboard",
         "query": Object {
-          "branch": "feature",
           "id": "foo",
         },
       }
index b52783299ce9157078fe9e1dfe71fc7a47652b89..239c4581d61a188327bebc86f16c22deb42c6c20 100644 (file)
@@ -29,6 +29,8 @@ export interface Task {
   hasScannerContext?: boolean;
   id: string;
   organization?: string;
+  pullRequest?: string;
+  pullRequestTitle?: string;
   startedAt?: string;
   status: string;
   submittedAt: string;
index b6941e5638863529ec50104f326af4be50a81ede..937a66ee75189c1291eab31382e3c3c9536e755b 100644 (file)
@@ -26,16 +26,16 @@ import Search from './Search';
 import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket';
 import { Component as CodeComponent } from '../types';
 import { retrieveComponentChildren, retrieveComponent, loadMoreChildren } from '../utils';
+import { Component, BranchLike } from '../../../app/types';
 import ListFooter from '../../../components/controls/ListFooter';
 import SourceViewer from '../../../components/SourceViewer/SourceViewer';
-import { parseError } from '../../../helpers/request';
-import { getBranchName } from '../../../helpers/branches';
+import { isSameBranchLike } from '../../../helpers/branches';
 import { translate } from '../../../helpers/l10n';
-import { Component, Branch } from '../../../app/types';
+import { parseError } from '../../../helpers/request';
 import '../code.css';
 
 interface Props {
-  branch?: Branch;
+  branchLike?: BranchLike;
   component: Component;
   location: { query: { [x: string]: string } };
 }
@@ -67,7 +67,10 @@ export default class App extends React.PureComponent<Props, State> {
   }
 
   componentDidUpdate(prevProps: Props) {
-    if (prevProps.component !== this.props.component || prevProps.branch !== this.props.branch) {
+    if (
+      prevProps.component !== this.props.component ||
+      !isSameBranchLike(prevProps.branchLike, this.props.branchLike)
+    ) {
       this.handleComponentChange();
     } else if (prevProps.location !== this.props.location) {
       this.handleUpdate();
@@ -80,14 +83,14 @@ export default class App extends React.PureComponent<Props, State> {
   }
 
   handleComponentChange() {
-    const { branch, component } = this.props;
+    const { branchLike, component } = this.props;
 
     // we already know component's breadcrumbs,
     addComponentBreadcrumbs(component.key, component.breadcrumbs);
 
     this.setState({ loading: true });
     const isPortfolio = ['VW', 'SVW'].includes(component.qualifier);
-    retrieveComponentChildren(component.key, isPortfolio, getBranchName(branch))
+    retrieveComponentChildren(component.key, isPortfolio, branchLike)
       .then(() => {
         addComponent(component);
         if (this.mounted) {
@@ -106,7 +109,7 @@ export default class App extends React.PureComponent<Props, State> {
     this.setState({ loading: true });
 
     const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier);
-    retrieveComponent(componentKey, isPortfolio, getBranchName(this.props.branch))
+    retrieveComponent(componentKey, isPortfolio, this.props.branchLike)
       .then(r => {
         if (this.mounted) {
           if (['FIL', 'UTS'].includes(r.component.qualifier)) {
@@ -152,7 +155,7 @@ export default class App extends React.PureComponent<Props, State> {
       return;
     }
     const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier);
-    loadMoreChildren(baseComponent.key, page + 1, isPortfolio, getBranchName(this.props.branch))
+    loadMoreChildren(baseComponent.key, page + 1, isPortfolio, this.props.branchLike)
       .then(r => {
         if (this.mounted) {
           this.setState({
@@ -177,7 +180,7 @@ export default class App extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { branch, component, location } = this.props;
+    const { branchLike, component, location } = this.props;
     const {
       loading,
       error,
@@ -187,8 +190,6 @@ export default class App extends React.PureComponent<Props, State> {
       total,
       sourceViewer
     } = this.state;
-    const branchName = getBranchName(branch);
-
     const shouldShowBreadcrumbs = breadcrumbs.length > 1;
 
     const componentsClassName = classNames('boxed-group', 'boxed-group-inner', 'spacer-top', {
@@ -202,7 +203,7 @@ export default class App extends React.PureComponent<Props, State> {
         {error && <div className="alert alert-danger">{error}</div>}
 
         <Search
-          branch={branchName}
+          branchLike={branchLike}
           component={component}
           location={location}
           onError={this.handleError}
@@ -210,7 +211,11 @@ export default class App extends React.PureComponent<Props, State> {
 
         <div className="code-components">
           {shouldShowBreadcrumbs && (
-            <Breadcrumbs branch={branchName} breadcrumbs={breadcrumbs} rootComponent={component} />
+            <Breadcrumbs
+              branchLike={branchLike}
+              breadcrumbs={breadcrumbs}
+              rootComponent={component}
+            />
           )}
 
           {sourceViewer === undefined &&
@@ -218,7 +223,7 @@ export default class App extends React.PureComponent<Props, State> {
               <div className={componentsClassName}>
                 <Components
                   baseComponent={baseComponent}
-                  branch={branchName}
+                  branchLike={branchLike}
                   components={components}
                   rootComponent={component}
                 />
@@ -232,7 +237,7 @@ export default class App extends React.PureComponent<Props, State> {
 
           {sourceViewer !== undefined && (
             <div className="spacer-top">
-              <SourceViewer branch={branchName} component={sourceViewer.key} />
+              <SourceViewer branchLike={branchLike} component={sourceViewer.key} />
             </div>
           )}
         </div>
index 783b7126fa5a1b540bfe5aa5dc0a52db5ae4a83a..7ea212cd7ee23d325c414ec32061fe6df247a529 100644 (file)
 import * as React from 'react';
 import ComponentName from './ComponentName';
 import { Component } from '../types';
+import { BranchLike } from '../../../app/types';
 
 interface Props {
-  branch?: string;
+  branchLike?: BranchLike;
   breadcrumbs: Component[];
   rootComponent: Component;
 }
 
-export default function Breadcrumbs({ branch, breadcrumbs, rootComponent }: Props) {
+export default function Breadcrumbs({ branchLike, breadcrumbs, rootComponent }: Props) {
   return (
     <ul className="code-breadcrumbs">
       {breadcrumbs.map((component, index) => (
         <li key={component.key}>
           <ComponentName
-            branch={branch}
+            branchLike={branchLike}
             canBrowse={index < breadcrumbs.length - 1}
             component={component}
             rootComponent={rootComponent}
index 841d96baee72ed94add813975c091b0272de3ec8..df87343950637561fe237e01a54a9b05e7d25c48 100644 (file)
@@ -24,12 +24,13 @@ import ComponentMeasure from './ComponentMeasure';
 import ComponentLink from './ComponentLink';
 import ComponentPin from './ComponentPin';
 import { Component as IComponent } from '../types';
+import { BranchLike } from '../../../app/types';
 
 const TOP_OFFSET = 200;
 const BOTTOM_OFFSET = 10;
 
 interface Props {
-  branch?: string;
+  branchLike?: BranchLike;
   canBrowse?: boolean;
   component: IComponent;
   previous?: IComponent;
@@ -73,7 +74,7 @@ export default class Component extends React.PureComponent<Props> {
 
   render() {
     const {
-      branch,
+      branchLike,
       component,
       rootComponent,
       selected = false,
@@ -89,10 +90,10 @@ export default class Component extends React.PureComponent<Props> {
       switch (component.qualifier) {
         case 'FIL':
         case 'UTS':
-          componentAction = <ComponentPin branch={branch} component={component} />;
+          componentAction = <ComponentPin branchLike={branchLike} component={component} />;
           break;
         default:
-          componentAction = <ComponentLink branch={branch} component={component} />;
+          componentAction = <ComponentLink branchLike={branchLike} component={component} />;
       }
     }
 
@@ -121,7 +122,7 @@ export default class Component extends React.PureComponent<Props> {
         </td>
         <td className="code-name-cell">
           <ComponentName
-            branch={branch}
+            branchLike={branchLike}
             component={component}
             rootComponent={rootComponent}
             previous={previous}
index 599dabdc412f03dc95c1995ac98ab866fc2e7c30..60927947472bc5051b1f66b702a0e233d8dbeeec 100644 (file)
 import * as React from 'react';
 import { Link } from 'react-router';
 import { Component } from '../types';
+import { BranchLike } from '../../../app/types';
 import LinkIcon from '../../../components/icons-components/LinkIcon';
 import { translate } from '../../../helpers/l10n';
-import { getProjectUrl } from '../../../helpers/urls';
+import { getBranchLikeUrl } from '../../../helpers/urls';
 
 interface Props {
-  branch?: string;
+  branchLike?: BranchLike;
   component: Component;
 }
 
-export default function ComponentLink({ component, branch }: Props) {
+export default function ComponentLink({ component, branchLike }: Props) {
   return (
     <Link
       className="link-no-underline"
       title={translate('code.open_component_page')}
-      to={getProjectUrl(component.refKey || component.key, branch)}>
+      to={getBranchLikeUrl(component.refKey || component.key, branchLike)}>
       <LinkIcon />
     </Link>
   );
index 7abb5e51a2d718662f485a842f9e6dcfae96b16e..0bd1290d6625ad06862bc7d7397af880894869e1 100644 (file)
 import * as React from 'react';
 import { Link } from 'react-router';
 import Truncated from './Truncated';
+import { Component } from '../types';
 import * as theme from '../../../app/theme';
+import { BranchLike } from '../../../app/types';
 import QualifierIcon from '../../../components/shared/QualifierIcon';
-import { Component } from '../types';
+import { getBranchLikeQuery } from '../../../helpers/branches';
 
 function getTooltip(component: Component) {
   const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS';
@@ -49,7 +51,7 @@ function mostCommitPrefix(strings: string[]) {
 }
 
 interface Props {
-  branch?: string;
+  branchLike?: BranchLike;
   canBrowse?: boolean;
   component: Component;
   previous?: Component;
@@ -57,7 +59,7 @@ interface Props {
 }
 
 export default function ComponentName(props: Props) {
-  const { branch, component, rootComponent, previous, canBrowse = false } = props;
+  const { branchLike, component, rootComponent, previous, canBrowse = false } = props;
   const areBothDirs = component.qualifier === 'DIR' && previous && previous.qualifier === 'DIR';
   const prefix =
     areBothDirs && previous !== undefined
@@ -83,7 +85,7 @@ export default function ComponentName(props: Props) {
       </Link>
     );
   } else if (canBrowse) {
-    const query = { id: rootComponent.key, branch };
+    const query = { id: rootComponent.key, ...getBranchLikeQuery(branchLike) };
     if (component.key !== rootComponent.key) {
       Object.assign(query, { selected: component.key });
     }
index eb61f75da930bdd396e0a546d4d778158e3017f1..867f4bd476f1efb826e1bf19f5a6c060eb673de5 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import Workspace from '../../../components/workspace/main';
+import { Component } from '../types';
+import { BranchLike } from '../../../app/types';
 import PinIcon from '../../../components/shared/pin-icon';
+import Workspace from '../../../components/workspace/main';
 import { translate } from '../../../helpers/l10n';
-import { Component } from '../types';
 
 interface Props {
-  branch?: string;
+  branchLike?: BranchLike;
   component: Component;
 }
 
-export default function ComponentPin({ branch, component }: Props) {
+export default function ComponentPin({ branchLike, component }: Props) {
   const handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
     event.preventDefault();
-    Workspace.openComponent({ branch, key: component.key });
+    Workspace.openComponent({ branchLike, key: component.key });
   };
 
   return (
index 25e990519947a57bc1bb764665d4c96d3dbb52bc..98d3b569e71360ccf3455a7c7f6935fc02b37dc1 100644 (file)
@@ -22,24 +22,25 @@ import Component from './Component';
 import ComponentsEmpty from './ComponentsEmpty';
 import ComponentsHeader from './ComponentsHeader';
 import { Component as IComponent } from '../types';
+import { BranchLike } from '../../../app/types';
 
 interface Props {
   baseComponent?: IComponent;
-  branch?: string;
+  branchLike?: BranchLike;
   components: IComponent[];
   rootComponent: IComponent;
   selected?: IComponent;
 }
 
 export default function Components(props: Props) {
-  const { baseComponent, branch, components, rootComponent, selected } = props;
+  const { baseComponent, branchLike, components, rootComponent, selected } = props;
   return (
     <table className="data zebra">
       <ComponentsHeader baseComponent={baseComponent} rootComponent={rootComponent} />
       {baseComponent && (
         <tbody>
           <Component
-            branch={branch}
+            branchLike={branchLike}
             component={baseComponent}
             key={baseComponent.key}
             rootComponent={rootComponent}
@@ -53,7 +54,7 @@ export default function Components(props: Props) {
         {components.length ? (
           components.map((component, index, list) => (
             <Component
-              branch={branch}
+              branchLike={branchLike}
               canBrowse={true}
               component={component}
               key={component.key}
index 4a5536ed3de0434507576fa33994f59f61ff8fdd..abeeacc511a07b5454812e64e73956c6179fa05d 100644 (file)
@@ -21,15 +21,17 @@ import * as React from 'react';
 import * as PropTypes from 'prop-types';
 import * as classNames from 'classnames';
 import Components from './Components';
-import { getTree } from '../../../api/components';
-import { parseError } from '../../../helpers/request';
-import { getProjectUrl } from '../../../helpers/urls';
 import { Component } from '../types';
+import { getTree } from '../../../api/components';
+import { BranchLike } from '../../../app/types';
 import SearchBox from '../../../components/controls/SearchBox';
+import { getBranchLikeQuery } from '../../../helpers/branches';
 import { translate } from '../../../helpers/l10n';
+import { parseError } from '../../../helpers/request';
+import { getProjectUrl } from '../../../helpers/urls';
 
 interface Props {
-  branch?: string;
+  branchLike?: BranchLike;
   component: Component;
   location: {};
   onError: (error: string) => void;
@@ -89,7 +91,7 @@ export default class Search extends React.PureComponent<Props, State> {
   }
 
   handleSelectCurrent() {
-    const { branch, component } = this.props;
+    const { branchLike, component } = this.props;
     const { results, selectedIndex } = this.state;
     if (results != null && selectedIndex != null) {
       const selected = results[selectedIndex];
@@ -99,7 +101,7 @@ export default class Search extends React.PureComponent<Props, State> {
       } else {
         this.context.router.push({
           pathname: '/code',
-          query: { branch, id: component.key, selected: selected.key }
+          query: { id: component.key, selected: selected.key, ...getBranchLikeQuery(branchLike) }
         });
       }
     }
@@ -125,13 +127,18 @@ export default class Search extends React.PureComponent<Props, State> {
 
   handleSearch = (query: string) => {
     if (this.mounted) {
-      const { branch, component, onError } = this.props;
+      const { branchLike, component, onError } = this.props;
       this.setState({ loading: true });
 
       const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier);
       const qualifiers = isPortfolio ? 'SVW,TRK' : 'BRC,UTS,FIL';
 
-      getTree(component.key, { branch, q: query, s: 'qualifier,name', qualifiers })
+      getTree(component.key, {
+        q: query,
+        s: 'qualifier,name',
+        qualifiers,
+        ...getBranchLikeQuery(branchLike)
+      })
         .then(r => {
           if (this.mounted) {
             this.setState({
@@ -184,7 +191,7 @@ export default class Search extends React.PureComponent<Props, State> {
         {results != null && (
           <div className="boxed-group boxed-group-inner spacer-top">
             <Components
-              branch={this.props.branch}
+              branchLike={this.props.branchLike}
               components={results}
               rootComponent={component}
               selected={selected}
index 80fdbadea8ca2f03db0519af1a2e43e5f064e90a..75ef812c9774f342eb5991deba649c4a59f128a8 100644 (file)
@@ -28,6 +28,8 @@ import {
 } from './bucket';
 import { Breadcrumb, Component } from './types';
 import { getChildren, getComponent, getBreadcrumbs } from '../../api/components';
+import { BranchLike } from '../../app/types';
+import { getBranchLikeQuery } from '../../helpers/branches';
 
 const METRICS = [
   'ncloc',
@@ -54,11 +56,15 @@ function requestChildren(
   componentKey: string,
   metrics: string[],
   page: number,
-  branch?: string
+  branchLike?: BranchLike
 ): Promise<Component[]> {
-  return getChildren(componentKey, metrics, { branch, p: page, ps: PAGE_SIZE }).then(r => {
+  return getChildren(componentKey, metrics, {
+    p: page,
+    ps: PAGE_SIZE,
+    ...getBranchLikeQuery(branchLike)
+  }).then(r => {
     if (r.paging.total > r.paging.pageSize * r.paging.pageIndex) {
-      return requestChildren(componentKey, metrics, page + 1, branch).then(moreComponents => {
+      return requestChildren(componentKey, metrics, page + 1, branchLike).then(moreComponents => {
         return [...r.components, ...moreComponents];
       });
     }
@@ -69,9 +75,9 @@ function requestChildren(
 function requestAllChildren(
   componentKey: string,
   metrics: string[],
-  branch?: string
+  branchLike?: BranchLike
 ): Promise<Component[]> {
-  return requestChildren(componentKey, metrics, 1, branch);
+  return requestChildren(componentKey, metrics, 1, branchLike);
 }
 
 interface Children {
@@ -84,13 +90,13 @@ interface ExpandRootDirFunc {
   (children: Children): Promise<Children>;
 }
 
-function expandRootDir(metrics: string[], branch?: string): ExpandRootDirFunc {
+function expandRootDir(metrics: string[], branchLike?: BranchLike): ExpandRootDirFunc {
   return function({ components, total, ...other }) {
     const rootDir = components.find(
       (component: Component) => component.qualifier === 'DIR' && component.name === '/'
     );
     if (rootDir) {
-      return requestAllChildren(rootDir.key, metrics, branch).then(rootDirComponents => {
+      return requestAllChildren(rootDir.key, metrics, branchLike).then(rootDirComponents => {
         const nextComponents = without([...rootDirComponents, ...components], rootDir);
         const nextTotal = total + rootDirComponents.length - /* root dir */ 1;
         return { components: nextComponents, total: nextTotal, ...other };
@@ -133,7 +139,11 @@ function getMetrics(isPortfolio: boolean) {
   return isPortfolio ? PORTFOLIO_METRICS : METRICS;
 }
 
-function retrieveComponentBase(componentKey: string, isPortfolio: boolean, branch?: string) {
+function retrieveComponentBase(
+  componentKey: string,
+  isPortfolio: boolean,
+  branchLike?: BranchLike
+) {
   const existing = getComponentFromBucket(componentKey);
   if (existing) {
     return Promise.resolve(existing);
@@ -141,7 +151,11 @@ function retrieveComponentBase(componentKey: string, isPortfolio: boolean, branc
 
   const metrics = getMetrics(isPortfolio);
 
-  return getComponent(componentKey, metrics, branch).then(component => {
+  return getComponent({
+    componentKey,
+    metricKeys: metrics.join(),
+    ...getBranchLikeQuery(branchLike)
+  }).then(component => {
     addComponent(component);
     return component;
   });
@@ -150,7 +164,7 @@ function retrieveComponentBase(componentKey: string, isPortfolio: boolean, branc
 export function retrieveComponentChildren(
   componentKey: string,
   isPortfolio: boolean,
-  branch?: string
+  branchLike?: BranchLike
 ): Promise<{ components: Component[]; page: number; total: number }> {
   const existing = getComponentChildren(componentKey);
   if (existing) {
@@ -163,9 +177,13 @@ export function retrieveComponentChildren(
 
   const metrics = getMetrics(isPortfolio);
 
-  return getChildren(componentKey, metrics, { branch, ps: PAGE_SIZE, s: 'qualifier,name' })
+  return getChildren(componentKey, metrics, {
+    ps: PAGE_SIZE,
+    s: 'qualifier,name',
+    ...getBranchLikeQuery(branchLike)
+  })
     .then(prepareChildren)
-    .then(expandRootDir(metrics, branch))
+    .then(expandRootDir(metrics, branchLike))
     .then(r => {
       addComponentChildren(componentKey, r.components, r.total, r.page);
       storeChildrenBase(r.components);
@@ -175,18 +193,18 @@ export function retrieveComponentChildren(
 }
 
 function retrieveComponentBreadcrumbs(
-  componentKey: string,
-  branch?: string
+  component: string,
+  branchLike?: BranchLike
 ): Promise<Breadcrumb[]> {
-  const existing = getComponentBreadcrumbs(componentKey);
+  const existing = getComponentBreadcrumbs(component);
   if (existing) {
     return Promise.resolve(existing);
   }
 
-  return getBreadcrumbs(componentKey, branch)
+  return getBreadcrumbs({ component, ...getBranchLikeQuery(branchLike) })
     .then(skipRootDir)
     .then(breadcrumbs => {
-      addComponentBreadcrumbs(componentKey, breadcrumbs);
+      addComponentBreadcrumbs(component, breadcrumbs);
       return breadcrumbs;
     });
 }
@@ -194,7 +212,7 @@ function retrieveComponentBreadcrumbs(
 export function retrieveComponent(
   componentKey: string,
   isPortfolio: boolean,
-  branch?: string
+  branchLike?: BranchLike
 ): Promise<{
   breadcrumbs: Component[];
   component: Component;
@@ -203,9 +221,9 @@ export function retrieveComponent(
   total: number;
 }> {
   return Promise.all([
-    retrieveComponentBase(componentKey, isPortfolio, branch),
-    retrieveComponentChildren(componentKey, isPortfolio, branch),
-    retrieveComponentBreadcrumbs(componentKey, branch)
+    retrieveComponentBase(componentKey, isPortfolio, branchLike),
+    retrieveComponentChildren(componentKey, isPortfolio, branchLike),
+    retrieveComponentBreadcrumbs(componentKey, branchLike)
   ]).then(r => {
     return {
       component: r[0],
@@ -221,13 +239,17 @@ export function loadMoreChildren(
   componentKey: string,
   page: number,
   isPortfolio: boolean,
-  branch?: string
+  branchLike?: BranchLike
 ): Promise<Children> {
   const metrics = getMetrics(isPortfolio);
 
-  return getChildren(componentKey, metrics, { branch, ps: PAGE_SIZE, p: page })
+  return getChildren(componentKey, metrics, {
+    ps: PAGE_SIZE,
+    p: page,
+    ...getBranchLikeQuery(branchLike)
+  })
     .then(prepareChildren)
-    .then(expandRootDir(metrics, branch))
+    .then(expandRootDir(metrics, branchLike))
     .then(r => {
       addComponentChildren(componentKey, r.components, r.total, r.page);
       storeChildrenBase(r.components);
index 4198c6689fd798ad26d047883afacf7c6af478a9..d357ff4c15a88790d709ee4f22286cc82cac5acc 100644 (file)
@@ -26,7 +26,7 @@ import MeasureOverviewContainer from './MeasureOverviewContainer';
 import Sidebar from '../sidebar/Sidebar';
 import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
 import { hasBubbleChart, parseQuery, serializeQuery } from '../utils';
-import { getBranchName } from '../../../helpers/branches';
+import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches';
 import { translate } from '../../../helpers/l10n';
 import { getDisplayMetrics } from '../../../helpers/measures';
 /*:: import type { Component, Query, Period } from '../types'; */
@@ -36,14 +36,14 @@ import { getDisplayMetrics } from '../../../helpers/measures';
 import '../style.css';
 
 /*:: type Props = {|
-  branch?: {},
+  branchLike?: { id?: string; name: string },
   component: Component,
   currentUser: { isLoggedIn: boolean },
   location: { pathname: string, query: RawQuery },
   fetchMeasures: (
     component: string,
     metricsKey: Array<string>,
-    branch?: string
+    branchLike?: { id?: string; name: string }
   ) => Promise<{ component: Component, measures: Array<MeasureEnhanced>, leakPeriod: ?Period }>,
   fetchMetrics: () => void,
   metrics: { [string]: Metric },
@@ -88,7 +88,7 @@ export default class App extends React.PureComponent {
 
   componentWillReceiveProps(nextProps /*: Props */) {
     if (
-      nextProps.branch !== this.props.branch ||
+      !isSameBranchLike(nextProps.branchLike, this.props.branchLike) ||
       nextProps.component.key !== this.props.component.key ||
       nextProps.metrics !== this.props.metrics
     ) {
@@ -107,10 +107,10 @@ export default class App extends React.PureComponent {
     }
   }
 
-  fetchMeasures = ({ branch, component, fetchMeasures, metrics } /*: Props */) => {
+  fetchMeasures = ({ branchLike, component, fetchMeasures, metrics } /*: Props */) => {
     this.setState({ loading: true });
     const filteredKeys = getDisplayMetrics(Object.values(metrics)).map(metric => metric.key);
-    fetchMeasures(component.key, filteredKeys, getBranchName(branch)).then(
+    fetchMeasures(component.key, filteredKeys, branchLike).then(
       ({ measures, leakPeriod }) => {
         if (this.mounted) {
           this.setState({
@@ -137,7 +137,7 @@ export default class App extends React.PureComponent {
       pathname: this.props.location.pathname,
       query: {
         ...query,
-        branch: getBranchName(this.props.branch),
+        ...getBranchLikeQuery(this.props.branchLike),
         id: this.props.component.key
       }
     });
@@ -148,7 +148,7 @@ export default class App extends React.PureComponent {
     if (isLoading) {
       return <i className="spinner spinner-margin" />;
     }
-    const { branch, component, fetchMeasures, metrics } = this.props;
+    const { branchLike, component, fetchMeasures, metrics } = this.props;
     const { leakPeriod } = this.state;
     const query = parseQuery(this.props.location.query);
     const metric = metrics[query.metric];
@@ -174,7 +174,7 @@ export default class App extends React.PureComponent {
 
         {metric != null && (
           <MeasureContentContainer
-            branch={getBranchName(branch)}
+            branchLike={branchLike}
             className="layout-page-main"
             currentUser={this.props.currentUser}
             rootComponent={component}
@@ -191,7 +191,7 @@ export default class App extends React.PureComponent {
         {metric == null &&
           hasBubbleChart(query.metric) && (
             <MeasureOverviewContainer
-              branch={getBranchName(branch)}
+              branchLike={branchLike}
               className="layout-page-main"
               rootComponent={component}
               currentUser={this.props.currentUser}
index a9122702894c60d36054564ad45952e75202003d..54d63a4dddd0b9984dd479fd3beb47da8df9669e 100644 (file)
@@ -27,6 +27,7 @@ import { fetchMetrics } from '../../../store/rootActions';
 import { getMeasuresAndMeta } from '../../../api/measures';
 import { getLeakPeriod } from '../../../helpers/periods';
 import { enhanceMeasure } from '../../../components/measure/utils';
+import { getBranchLikeQuery } from '../../../helpers/branches';
 /*:: import type { Component, Period } from '../types'; */
 /*:: import type { Measure, MeasureEnhanced } from '../../../components/measure/types'; */
 
@@ -50,7 +51,7 @@ function banQualityGate(component /*: Component */) /*: Array<Measure> */ {
 const fetchMeasures = (
   component /*: string */,
   metricsKey /*: Array<string> */,
-  branch /*: string | void */
+  branchLike /*: { id?: string; name: string } | void */
 ) => (dispatch, getState) => {
   if (metricsKey.length <= 0) {
     return Promise.resolve({ component: {}, measures: [], leakPeriod: null });
@@ -58,7 +59,7 @@ const fetchMeasures = (
 
   return getMeasuresAndMeta(component, metricsKey, {
     additionalFields: 'periods',
-    branch
+    ...getBranchLikeQuery(branchLike)
   }).then(r => {
     const measures = banQualityGate(r.component).map(measure =>
       enhanceMeasure(measure, getMetrics(getState()))
index fedd4d52dfed6928a1fd47635eea3fc9f77f4f26..7709a31d257ead4c8a88e77d7581ee60d5fabc5e 100644 (file)
@@ -22,11 +22,12 @@ import React from 'react';
 import key from 'keymaster';
 import Breadcrumb from './Breadcrumb';
 import { getBreadcrumbs } from '../../../api/components';
+import { getBranchLikeQuery } from '../../../helpers/branches';
 /*:: import type { Component } from '../types'; */
 
 /*:: type Props = {|
   backToFirst: boolean,
-  branch?: string,
+  branchLike?: { id?: string, name: string },
   className?: string,
   component: Component,
   handleSelect: string => void,
@@ -76,7 +77,7 @@ export default class Breadcrumbs extends React.PureComponent {
     key.unbind('left', 'measures-files');
   }
 
-  fetchBreadcrumbs = ({ branch, component, rootComponent } /*: Props */) => {
+  fetchBreadcrumbs = ({ branchLike, component, rootComponent } /*: Props */) => {
     const isRoot = component.key === rootComponent.key;
     if (isRoot) {
       if (this.mounted) {
@@ -84,11 +85,13 @@ export default class Breadcrumbs extends React.PureComponent {
       }
       return;
     }
-    getBreadcrumbs(component.key, branch).then(breadcrumbs => {
-      if (this.mounted) {
-        this.setState({ breadcrumbs });
+    getBreadcrumbs({ component: component.key, ...getBranchLikeQuery(branchLike) }).then(
+      breadcrumbs => {
+        if (this.mounted) {
+          this.setState({ breadcrumbs });
+        }
       }
-    });
+    );
   };
 
   render() {
index a9d9f81c35dd01c028411f2a9011d983dd5d8d99..8feb83e1167ac074082377918566ca1346b191b5 100644 (file)
@@ -34,6 +34,7 @@ import { complementary } from '../config/complementary';
 import { enhanceComponent, isFileType, isViewType } from '../utils';
 import { getProjectUrl } from '../../../helpers/urls';
 import { isDiffMetric } from '../../../helpers/measures';
+import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches';
 /*:: import type { Component, ComponentEnhanced, Paging, Period } from '../types'; */
 /*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
 /*:: import type { Metric } from '../../../store/metrics/actions'; */
@@ -42,7 +43,7 @@ import { isDiffMetric } from '../../../helpers/measures';
 // https://github.com/facebook/flow/issues/3147
 // router: { push: ({ pathname: string, query?: RawQuery }) => void }
 /*:: type Props = {|
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   className?: string,
   component: Component,
   currentUser: { isLoggedIn: boolean },
@@ -87,7 +88,7 @@ export default class MeasureContent extends React.PureComponent {
 
   componentWillReceiveProps(nextProps /*: Props */) {
     if (
-      nextProps.branch !== this.props.branch ||
+      !isSameBranchLike(nextProps.branchLike, this.props.branchLike) ||
       nextProps.component !== this.props.component ||
       nextProps.metric !== this.props.metric
     ) {
@@ -115,7 +116,7 @@ export default class MeasureContent extends React.PureComponent {
     const strategy = view === 'list' ? 'leaves' : 'children';
     const metricKeys = [metric.key];
     const opts /*: Object */ = {
-      branch: this.props.branch,
+      ...getBranchLikeQuery(this.props.branchLike),
       metricSortFilter: 'withMeasuresOnly'
     };
     const isDiff = isDiffMetric(metric.key);
@@ -225,7 +226,7 @@ export default class MeasureContent extends React.PureComponent {
     return (
       <div className="measure-details-viewer">
         <CodeView
-          branch={this.props.branch}
+          branchLike={this.props.branchLike}
           component={this.props.component}
           components={this.state.components}
           leakPeriod={this.props.leakPeriod}
@@ -244,7 +245,7 @@ export default class MeasureContent extends React.PureComponent {
         const selectedIdx = this.getSelectedIndex();
         return (
           <FilesView
-            branch={this.props.branch}
+            branchLike={this.props.branchLike}
             components={this.state.components}
             fetchMore={this.fetchMoreComponents}
             handleOpen={this.onOpenComponent}
@@ -261,7 +262,7 @@ export default class MeasureContent extends React.PureComponent {
       if (view === 'treemap') {
         return (
           <TreeMapView
-            branch={this.props.branch}
+            branchLike={this.props.branchLike}
             components={this.state.components}
             handleSelect={this.onOpenComponent}
             metric={metric}
@@ -274,7 +275,7 @@ export default class MeasureContent extends React.PureComponent {
   }
 
   render() {
-    const { branch, component, currentUser, measure, metric, rootComponent, view } = this.props;
+    const { branchLike, component, currentUser, measure, metric, rootComponent, view } = this.props;
     const isLoggedIn = currentUser && currentUser.isLoggedIn;
     const isFile = isFileType(component);
     const selectedIdx = this.getSelectedIndex();
@@ -288,7 +289,7 @@ export default class MeasureContent extends React.PureComponent {
             <div className="layout-page-main-inner">
               <Breadcrumbs
                 backToFirst={view === 'list'}
-                branch={branch}
+                branchLike={branchLike}
                 className="measure-breadcrumbs spacer-right text-ellipsis"
                 component={component}
                 handleSelect={this.onOpenComponent}
@@ -327,7 +328,7 @@ export default class MeasureContent extends React.PureComponent {
           measure != null && (
             <div className="layout-page-main-inner measure-details-content">
               <MeasureHeader
-                branch={branch}
+                branchLike={branchLike}
                 component={component}
                 components={this.state.components}
                 leakPeriod={this.props.leakPeriod}
index beaca1bd6e139967263cfcea17b9d878cfe44962..1998460996ea99363feb7f07e8062bcac3400b21 100644 (file)
@@ -26,14 +26,14 @@ import MeasureContent from './MeasureContent';
 /*:: import type { RawQuery } from '../../../helpers/query'; */
 
 /*:: type Props = {|
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   className?: string,
   currentUser: { isLoggedIn: boolean },
   rootComponent: Component,
   fetchMeasures: (
     component: string,
     metricsKey: Array<string>,
-    branch?: string
+    branchLike?: { id?: string; name: string }
   ) => Promise<{ component: Component, measures: Array<MeasureEnhanced> }>,
   leakPeriod?: Period,
   metric: Metric,
@@ -89,7 +89,7 @@ export default class MeasureContentContainer extends React.PureComponent {
     this.mounted = false;
   }
 
-  fetchMeasure = ({ branch, rootComponent, fetchMeasures, metric, selected } /*: Props */) => {
+  fetchMeasure = ({ branchLike, rootComponent, fetchMeasures, metric, selected } /*: Props */) => {
     this.updateLoading({ measure: true });
 
     const metricKeys = [metric.key];
@@ -101,7 +101,7 @@ export default class MeasureContentContainer extends React.PureComponent {
       metricKeys.push('file_complexity_distribution');
     }
 
-    fetchMeasures(selected || rootComponent.key, metricKeys, branch).then(
+    fetchMeasures(selected || rootComponent.key, metricKeys, branchLike).then(
       ({ component, measures }) => {
         if (this.mounted) {
           const measure = measures.find(measure => measure.metric.key === metric.key);
@@ -134,7 +134,7 @@ export default class MeasureContentContainer extends React.PureComponent {
 
     return (
       <MeasureContent
-        branch={this.props.branch}
+        branchLike={this.props.branchLike}
         className={this.props.className}
         component={this.state.component}
         currentUser={this.props.currentUser}
index 1690866ffdec08df6e47ddffeb9b049c166a0426..f1aac5309b1a4cca7d14573b68bd8cbc8601cea9 100644 (file)
@@ -57,7 +57,7 @@ class MeasureFavoriteContainer extends React.PureComponent {
   }
 
   fetchComponentFavorite({ component, onReceiveComponent } /*: Props */) {
-    getComponentForSourceViewer(component).then(component => {
+    getComponentForSourceViewer({ component }).then(component => {
       this.setState({ component });
       onReceiveComponent(component);
     });
index 87c543aaa76966fe7120fc0cac9a50b1028dbb77..5c809374492d82a25a3b2fc4c4cb3d09e3f01f7c 100644 (file)
@@ -34,7 +34,7 @@ import { isDiffMetric } from '../../../helpers/measures';
 /*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
 
 /*:: type Props = {|
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   component: Component,
   components: Array<Component>,
   leakPeriod?: Period,
@@ -43,7 +43,7 @@ import { isDiffMetric } from '../../../helpers/measures';
 |}; */
 
 export default function MeasureHeader(props /*: Props*/) {
-  const { branch, component, leakPeriod, measure, secondaryMeasure } = props;
+  const { branchLike, component, leakPeriod, measure, secondaryMeasure } = props;
   const { metric } = measure;
   const isDiff = isDiffMetric(metric.key);
   return (
@@ -72,7 +72,7 @@ export default function MeasureHeader(props /*: Props*/) {
               overlay={translate('component_measures.show_metric_history')}>
               <Link
                 className="js-show-history spacer-left button button-small"
-                to={getMeasureHistoryUrl(component.key, metric.key, branch)}>
+                to={getMeasureHistoryUrl(component.key, metric.key, branchLike)}>
                 <HistoryIcon />
               </Link>
             </Tooltip>
index a5e79b7a1340cb0181c9deb95432b10f2763dd7b..0128f7b8b1f0ea3c5ec95786a4403a3923a4f062 100644 (file)
@@ -27,11 +27,12 @@ import BubbleChart from '../drilldown/BubbleChart';
 import SourceViewer from '../../../components/SourceViewer/SourceViewer';
 import { getComponentLeaves } from '../../../api/components';
 import { enhanceComponent, getBubbleMetrics, isFileType } from '../utils';
+import { getBranchLikeQuery } from '../../../helpers/branches';
 /*:: import type { Component, ComponentEnhanced, Paging, Period } from '../types'; */
 /*:: import type { Metric } from '../../../store/metrics/actions'; */
 
 /*:: type Props = {|
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   className?: string,
   component: Component,
   currentUser: { isLoggedIn: boolean },
@@ -79,9 +80,10 @@ export default class MeasureOverview extends React.PureComponent {
   }
 
   fetchComponents = (props /*: Props */) => {
-    const { branch, component, domain, metrics } = props;
+    const { branchLike, component, domain, metrics } = props;
     if (isFileType(component)) {
-      return this.setState({ components: [], paging: null });
+      this.setState({ components: [], paging: null });
+      return;
     }
     const { x, y, size, colors } = getBubbleMetrics(domain, metrics);
     const metricsKey = [x.key, y.key, size.key];
@@ -89,7 +91,7 @@ export default class MeasureOverview extends React.PureComponent {
       metricsKey.push(colors.map(metric => metric.key));
     }
     const options = {
-      branch,
+      ...getBranchLikeQuery(branchLike),
       s: 'metric',
       metricSort: size.key,
       asc: false,
@@ -114,11 +116,11 @@ export default class MeasureOverview extends React.PureComponent {
   };
 
   renderContent() {
-    const { branch, component } = this.props;
+    const { branchLike, component } = this.props;
     if (isFileType(component)) {
       return (
         <div className="measure-details-viewer">
-          <SourceViewer branch={branch} component={component.key} />
+          <SourceViewer branchLike={branchLike} component={component.key} />
         </div>
       );
     }
@@ -135,7 +137,7 @@ export default class MeasureOverview extends React.PureComponent {
   }
 
   render() {
-    const { branch, component, currentUser, leakPeriod, rootComponent } = this.props;
+    const { branchLike, component, currentUser, leakPeriod, rootComponent } = this.props;
     const isLoggedIn = currentUser && currentUser.isLoggedIn;
     const isFile = isFileType(component);
     return (
@@ -145,7 +147,7 @@ export default class MeasureOverview extends React.PureComponent {
             <div className="layout-page-main-inner">
               <Breadcrumbs
                 backToFirst={true}
-                branch={branch}
+                branchLike={branchLike}
                 className="measure-breadcrumbs spacer-right text-ellipsis"
                 component={component}
                 handleSelect={this.props.updateSelected}
index 7268b08f30867fc452ff2571c73783d5c9b986d7..b69a9a8378fb070dc531b71f417bff6fd3fbc278 100644 (file)
@@ -23,12 +23,13 @@ import MeasureOverview from './MeasureOverview';
 import { getComponentShow } from '../../../api/components';
 import { getProjectUrl } from '../../../helpers/urls';
 import { isViewType } from '../utils';
+import { getBranchLikeQuery } from '../../../helpers/branches';
 /*:: import type { Component, Period, Query } from '../types'; */
 /*:: import type { RawQuery } from '../../../helpers/query'; */
 /*:: import type { Metric } from '../../../store/metrics/actions'; */
 
 /*:: type Props = {|
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   className?: string,
   rootComponent: Component,
   currentUser: { isLoggedIn: boolean },
@@ -81,14 +82,14 @@ export default class MeasureOverviewContainer extends React.PureComponent {
     this.mounted = false;
   }
 
-  fetchComponent = ({ branch, rootComponent, selected } /*: Props */) => {
+  fetchComponent = ({ branchLike, rootComponent, selected } /*: Props */) => {
     if (!selected || rootComponent.key === selected) {
       this.setState({ component: rootComponent });
       this.updateLoading({ component: false });
       return;
     }
     this.updateLoading({ component: true });
-    getComponentShow(selected, branch).then(
+    getComponentShow({ component: selected, ...getBranchLikeQuery(branchLike) }).then(
       ({ component }) => {
         if (this.mounted) {
           this.setState({ component });
@@ -122,7 +123,7 @@ export default class MeasureOverviewContainer extends React.PureComponent {
 
     return (
       <MeasureOverview
-        branch={this.props.branch}
+        branchLike={this.props.branchLike}
         className={this.props.className}
         component={this.state.component}
         currentUser={this.props.currentUser}
index dfa9a644f24299acbafbb45d4946b89d6aadead6..5b02e3d87fb12f9e0f9770666146a086a1617573 100644 (file)
@@ -77,7 +77,10 @@ it('should render correctly for leak', () => {
 });
 
 it('should render with branch', () => {
-  expect(shallow(<MeasureHeader branch="feature" {...PROPS} />).find('Link')).toMatchSnapshot();
+  const shortBranch = { isMain: false, name: 'feature', mergeBranch: '', type: 'SHORT' };
+  expect(
+    shallow(<MeasureHeader branchLike={shortBranch} {...PROPS} />).find('Link')
+  ).toMatchSnapshot();
 });
 
 it('should display secondary measure too', () => {
index d6c9bceb55af2ca0be704c3ab364031265061a31..8da497107c3d159b6546de6a68d83910bb9f7a80 100644 (file)
@@ -86,7 +86,6 @@ exports[`should render correctly 1`] = `
             Object {
               "pathname": "/project/activity",
               "query": Object {
-                "branch": undefined,
                 "custom_metrics": "reliability_rating",
                 "graph": "custom",
                 "id": "foo",
index 5270c221842bec0fb5b6b272804b9c31897877db..1c0747851f17aaefb8c1ee5870e27acb6e54bdf3 100644 (file)
@@ -25,7 +25,7 @@ import SourceViewer from '../../../components/SourceViewer/SourceViewer';
 /*:: import type { Metric } from '../../../store/metrics/actions'; */
 
 /*:: type Props = {|
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   component: ComponentEnhanced,
   components: Array<ComponentEnhanced>,
   leakPeriod?: Period,
@@ -81,7 +81,7 @@ export default class CodeView extends React.PureComponent {
   };
 
   render() {
-    const { branch, component } = this.props;
-    return <SourceViewer branch={branch} component={component.key} />;
+    const { branchLike, component } = this.props;
+    return <SourceViewer branchLike={branchLike} component={component.key} />;
   }
 }
index 033a8bcf66b70749a162b135b16dda8af413effb..248b6469e2bbf3500aa6558c2622019b7d8e8dfc 100644 (file)
@@ -23,11 +23,11 @@ import { Link } from 'react-router';
 import LinkIcon from '../../../components/icons-components/LinkIcon';
 import QualifierIcon from '../../../components/icons-components/QualifierIcon';
 import { splitPath } from '../../../helpers/path';
-import { getPathUrlAsString, getProjectUrl } from '../../../helpers/urls';
+import { getPathUrlAsString, getBranchLikeUrl } from '../../../helpers/urls';
 /*:: import type { ComponentEnhanced } from '../types'; */
 
 /*:: type Props = {
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   component: ComponentEnhanced,
   onClick: string => void
 }; */
@@ -65,15 +65,16 @@ export default class ComponentCell extends React.PureComponent {
   }
 
   render() {
-    const { branch, component } = this.props;
+    const { branchLike, component } = this.props;
     return (
       <td className="measure-details-component-cell">
         <div className="text-ellipsis">
+          {/* TODO make this <a> link a react-router <Link /> */}
           {component.refKey == null ? (
             <a
               id={'component-measures-component-link-' + component.key}
               className="link-no-underline"
-              href={getPathUrlAsString(getProjectUrl(component.key, branch))}
+              href={getPathUrlAsString(getBranchLikeUrl(component.key, branchLike))}
               onClick={this.handleClick}>
               {this.renderInner()}
             </a>
@@ -81,7 +82,7 @@ export default class ComponentCell extends React.PureComponent {
             <Link
               className="link-no-underline"
               id={'component-measures-component-link-' + component.key}
-              to={getProjectUrl(component.refKey, branch)}>
+              to={getBranchLikeUrl(component.refKey, branchLike)}>
               <span className="big-spacer-right">
                 <LinkIcon />
               </span>
index f2b45b73878b2fb91c9d16e5f6542ae3f20ba381..0e29a704f0c4a37de9d2550ec95f3245b4c0c2df 100644 (file)
@@ -27,7 +27,7 @@ import { getLocalizedMetricName } from '../../../helpers/l10n';
 /*:: import type { Metric } from '../../../store/metrics/actions'; */
 
 /*:: type Props = {|
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   components: Array<ComponentEnhanced>,
   onClick: string => void,
   metric: Metric,
@@ -36,7 +36,7 @@ import { getLocalizedMetricName } from '../../../helpers/l10n';
 |}; */
 
 export default function ComponentsList(
-  { branch, components, onClick, metrics, metric, selectedComponent } /*: Props */
+  { branchLike, components, onClick, metrics, metric, selectedComponent } /*: Props */
 ) {
   if (!components.length) {
     return <EmptyResult />;
@@ -65,7 +65,7 @@ export default function ComponentsList(
         {components.map(component => (
           <ComponentsListRow
             key={component.id}
-            branch={branch}
+            branchLike={branchLike}
             component={component}
             otherMetrics={otherMetrics}
             isSelected={component.key === selectedComponent}
index e87b224967f374436ad8c55577dedc1b61c69e5a..47a5d2bf29a85a9d254508cafed6eecb5361508b 100644 (file)
@@ -26,7 +26,7 @@ import MeasureCell from './MeasureCell';
 /*:: import type { Metric } from '../../../store/metrics/actions'; */
 
 /*:: type Props = {|
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   component: ComponentEnhanced,
   isSelected: boolean,
   onClick: string => void,
@@ -35,7 +35,7 @@ import MeasureCell from './MeasureCell';
 |}; */
 
 export default function ComponentsListRow(props /*: Props */) {
-  const { branch, component } = props;
+  const { branchLike, component } = props;
   const otherMeasures = props.otherMetrics.map(metric => {
     const measure = component.measures.find(measure => measure.metric.key === metric.key);
     return { ...measure, metric };
@@ -45,7 +45,7 @@ export default function ComponentsListRow(props /*: Props */) {
   });
   return (
     <tr className={rowClass}>
-      <ComponentCell branch={branch} component={component} onClick={props.onClick} />
+      <ComponentCell branchLike={branchLike} component={component} onClick={props.onClick} />
 
       <MeasureCell component={component} metric={props.metric} />
 
index 9a0ffde8c05b19af8a849dd3d35f03d5f640e786..2fd8d7feb27560911b41b2a5ca77d7fd93ec6019 100644 (file)
@@ -28,7 +28,7 @@ import { scrollToElement } from '../../../helpers/scrolling';
 /*:: import type { Metric } from '../../../store/metrics/actions'; */
 
 /*:: type Props = {|
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   components: Array<ComponentEnhanced>,
   fetchMore: () => void,
   handleSelect: string => void,
@@ -123,7 +123,7 @@ export default class ListView extends React.PureComponent {
     return (
       <div ref={elem => (this.listContainer = elem)}>
         <ComponentsList
-          branch={this.props.branch}
+          branchLike={this.props.branchLike}
           components={this.props.components}
           metrics={this.props.metrics}
           metric={this.props.metric}
index 3892abf26894456374ce529acbd62cfb7f635faa..1dd3fd68f26cc1fe0bb49015798053a89f6edc5e 100644 (file)
@@ -29,13 +29,13 @@ import QualifierIcon from '../../../components/icons-components/QualifierIcon';
 import TreeMap from '../../../components/charts/TreeMap';
 import { translate, translateWithParameters, getLocalizedMetricName } from '../../../helpers/l10n';
 import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
-import { getProjectUrl } from '../../../helpers/urls';
+import { getBranchLikeUrl } from '../../../helpers/urls';
 /*:: import type { Metric } from '../../../store/metrics/actions'; */
 /*:: import type { ComponentEnhanced } from '../types'; */
 /*:: import type { TreeMapItem } from '../../../components/charts/TreeMap'; */
 
 /*:: type Props = {|
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   components: Array<ComponentEnhanced>,
   handleSelect: string => void,
   metric: Metric
@@ -64,7 +64,7 @@ export default class TreeMapView extends React.PureComponent {
     }
   }
 
-  getTreemapComponents = ({ branch, components, metric } /*: Props */) => {
+  getTreemapComponents = ({ branchLike, components, metric } /*: Props */) => {
     const colorScale = this.getColorScale(metric);
     return components
       .map(component => {
@@ -95,7 +95,7 @@ export default class TreeMapView extends React.PureComponent {
             sizeValue
           ),
           label: component.name,
-          link: getProjectUrl(component.refKey || component.key, branch)
+          link: getBranchLikeUrl(component.refKey || component.key, branchLike)
         };
       })
       .filter(Boolean);
index 96ff380b9063a8060fec5aaba73bd1544aec8de0..71fc859ea2f7d9e7831c0d0d91c665d56750f1ef 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { PullRequest, BranchType, ShortLivingBranch } from '../../../app/types';
 import SourceViewer from '../../../components/SourceViewer/SourceViewer';
 
 interface Props {
@@ -26,6 +27,7 @@ interface Props {
       branch?: string;
       id: string;
       line?: string;
+      pullRequest?: string;
     };
   };
 }
@@ -45,15 +47,30 @@ export default class App extends React.PureComponent<Props> {
   };
 
   render() {
-    const { branch, id, line } = this.props.location.query;
+    const { branch, id, line, pullRequest } = this.props.location.query;
 
     const finalLine = line ? Number(line) : undefined;
 
+    // TODO find a way to avoid creating this fakeBranchLike
+    // probably the best way would be to drop this page completely
+    // and redirect to the Code page
+    let fakeBranchLike: ShortLivingBranch | PullRequest | undefined = undefined;
+    if (branch) {
+      fakeBranchLike = {
+        isMain: false,
+        mergeBranch: '',
+        name: branch,
+        type: BranchType.SHORT
+      } as ShortLivingBranch;
+    } else if (pullRequest) {
+      fakeBranchLike = { base: '', branch: '', key: pullRequest, title: '' } as PullRequest;
+    }
+
     return (
       <div className="page page-limited">
         <SourceViewer
           aroundLine={finalLine}
-          branch={branch}
+          branchLike={fakeBranchLike}
           component={id}
           highlightedLine={finalLine}
           onLoaded={this.scrollToLine}
index 1cb5c653533a183393e2bb477c03c122ff477393..f5dd7e1a6ceb86e9938b41d1c3a5d4d2a2822005 100644 (file)
@@ -6,7 +6,14 @@ exports[`renders 1`] = `
 >
   <Connect(SourceViewerBase)
     aroundLine={7}
-    branch="b"
+    branchLike={
+      Object {
+        "isMain": false,
+        "mergeBranch": "",
+        "name": "b",
+        "type": "SHORT",
+      }
+    }
     component="foo"
     highlightedLine={7}
     onLoaded={[Function]}
index 9bafb84524263eb7b6a93f27d401d2723eb64d55..211061e58bdba9fd0a42e3357ff22804009cc61e 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { Component, CurrentUser } from '../../../app/types';
+import { Component, CurrentUser, BranchLike } from '../../../app/types';
 import { RawQuery } from '../../../helpers/query';
 
 interface Props {
-  branch?: { name: string };
+  branchLike?: BranchLike;
   component?: Component;
   currentUser: CurrentUser;
   fetchIssues: (query: RawQuery, requestOrganizations?: boolean) => Promise<any>;
index bc6cf9826ffb61138fe14bb2a7143a104ec5ef90..85bf0fa458ee4ff13475f601b022ddc7f7826a64 100644 (file)
@@ -59,7 +59,12 @@ import ListFooter from '../../../components/controls/ListFooter';
 import EmptySearch from '../../../components/common/EmptySearch';
 import FiltersHeader from '../../../components/common/FiltersHeader';
 import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
-import { getBranchName, isShortLivingBranch } from '../../../helpers/branches';
+import {
+  isShortLivingBranch,
+  isSameBranchLike,
+  getBranchLikeQuery,
+  isPullRequest
+} from '../../../helpers/branches';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { scrollToElement } from '../../../helpers/scrolling';
 import Checkbox from '../../../components/controls/Checkbox';
@@ -69,7 +74,7 @@ import '../styles.css';
 
 /*::
 export type Props = {
-  branch?: { name: string },
+  branchLike?: { id?: string; name: string },
   component?: Component,
   currentUser: CurrentUser,
   fetchIssues: (query: RawQuery, requestOrganizations?: boolean) => Promise<*>,
@@ -193,7 +198,7 @@ export default class App extends React.PureComponent {
     const { query: prevQuery } = prevProps.location;
     if (
       prevProps.component !== this.props.component ||
-      prevProps.branch !== this.props.branch ||
+      !isSameBranchLike(prevProps.branchLike, this.props.branchLike) ||
       !areQueriesEqual(prevQuery, query) ||
       areMyIssuesSelected(prevQuery) !== areMyIssuesSelected(query)
     ) {
@@ -337,7 +342,7 @@ export default class App extends React.PureComponent {
       pathname: this.props.location.pathname,
       query: {
         ...serializeQuery(this.state.query),
-        branch: getBranchName(this.props.branch),
+        ...getBranchLikeQuery(this.props.branchLike),
         id: this.props.component && this.props.component.key,
         myIssues: this.state.myIssues ? 'true' : undefined,
         open: issue
@@ -356,7 +361,7 @@ export default class App extends React.PureComponent {
         pathname: this.props.location.pathname,
         query: {
           ...serializeQuery(this.state.query),
-          branch: getBranchName(this.props.branch),
+          ...getBranchLikeQuery(this.props.branchLike),
           id: this.props.component && this.props.component.key,
           myIssues: this.state.myIssues ? 'true' : undefined,
           open: undefined
@@ -399,7 +404,7 @@ export default class App extends React.PureComponent {
       : undefined;
 
     const parameters = {
-      branch: getBranchName(this.props.branch),
+      ...getBranchLikeQuery(this.props.branchLike),
       componentKeys: component && component.key,
       s: 'FILE_LINE',
       ...serializeQuery(query),
@@ -594,7 +599,7 @@ export default class App extends React.PureComponent {
       pathname: this.props.location.pathname,
       query: {
         ...serializeQuery({ ...this.state.query, ...changes }),
-        branch: getBranchName(this.props.branch),
+        ...getBranchLikeQuery(this.props.branchLike),
         id: this.props.component && this.props.component.key,
         myIssues: this.state.myIssues ? 'true' : undefined
       }
@@ -610,7 +615,7 @@ export default class App extends React.PureComponent {
       pathname: this.props.location.pathname,
       query: {
         ...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }),
-        branch: getBranchName(this.props.branch),
+        ...getBranchLikeQuery(this.props.branchLike),
         id: this.props.component && this.props.component.key,
         myIssues: myIssues ? 'true' : undefined
       }
@@ -637,7 +642,7 @@ export default class App extends React.PureComponent {
       pathname: this.props.location.pathname,
       query: {
         ...DEFAULT_QUERY,
-        branch: getBranchName(this.props.branch),
+        ...getBranchLikeQuery(this.props.branchLike),
         id: this.props.component && this.props.component.key,
         myIssues: this.state.myIssues ? 'true' : undefined
       }
@@ -724,7 +729,7 @@ export default class App extends React.PureComponent {
 
   handleReload = () => {
     this.fetchFirstIssues();
-    if (isShortLivingBranch(this.props.branch)) {
+    if (isShortLivingBranch(this.props.branchLike) || isPullRequest(this.props.branchLike)) {
       this.props.onBranchesChange();
     }
   };
@@ -892,7 +897,7 @@ export default class App extends React.PureComponent {
   }
 
   renderList() {
-    const { branch, component, currentUser, organization } = this.props;
+    const { branchLike, component, currentUser, organization } = this.props;
     const { issues, openIssue, paging } = this.state;
     const selectedIndex = this.getSelectedIndex();
     const selectedIssue = selectedIndex != null ? issues[selectedIndex] : null;
@@ -905,7 +910,7 @@ export default class App extends React.PureComponent {
       <div>
         {paging.total > 0 && (
           <IssuesList
-            branch={getBranchName(branch)}
+            branchLike={branchLike}
             checked={this.state.checked}
             component={component}
             issues={issues}
@@ -971,7 +976,7 @@ export default class App extends React.PureComponent {
                 {openIssue != null ? (
                   <div className="pull-left width-60">
                     <ComponentBreadcrumbs
-                      branch={getBranchName(this.props.branch)}
+                      branchLike={this.props.branchLike}
                       component={component}
                       issue={openIssue}
                       organization={this.props.organization}
@@ -1000,7 +1005,7 @@ export default class App extends React.PureComponent {
             <div>
               {openIssue ? (
                 <IssuesSourceViewer
-                  branch={getBranchName(this.props.branch)}
+                  branchLike={this.props.branchLike}
                   component={component}
                   openIssue={openIssue}
                   loadIssues={this.fetchIssuesForComponent}
index 9da37298c41527f9789c914da24fefe1aac950f7..c86ec19b057bef3e6d847204416a18dbb13e677e 100644 (file)
@@ -21,11 +21,11 @@ import * as React from 'react';
 import { Link } from 'react-router';
 import Organization from '../../../components/shared/Organization';
 import { collapsePath, limitComponentName } from '../../../helpers/path';
-import { getProjectUrl } from '../../../helpers/urls';
-import { Component } from '../../../app/types';
+import { getBranchLikeUrl, getCodeUrl } from '../../../helpers/urls';
+import { Component, BranchLike } from '../../../app/types';
 
 interface Props {
-  branch?: string;
+  branchLike?: BranchLike;
   component?: Component;
   issue: {
     component: string;
@@ -39,7 +39,12 @@ interface Props {
   organization?: { key: string };
 }
 
-export default function ComponentBreadcrumbs({ branch, component, issue, organization }: Props) {
+export default function ComponentBreadcrumbs({
+  branchLike,
+  component,
+  issue,
+  organization
+}: Props) {
   const displayOrganization =
     !organization && (component == null || ['VW', 'SVW'].includes(component.qualifier));
   const displayProject = component == null || !['TRK', 'BRC', 'DIR'].includes(component.qualifier);
@@ -53,7 +58,7 @@ export default function ComponentBreadcrumbs({ branch, component, issue, organiz
 
       {displayProject && (
         <span title={issue.projectName}>
-          <Link to={getProjectUrl(issue.project, branch)} className="link-no-underline">
+          <Link to={getBranchLikeUrl(issue.project, branchLike)} className="link-no-underline">
             {limitComponentName(issue.projectName)}
           </Link>
           <span className="slash-separator" />
@@ -64,14 +69,16 @@ export default function ComponentBreadcrumbs({ branch, component, issue, organiz
         issue.subProject !== undefined &&
         issue.subProjectName !== undefined && (
           <span title={issue.subProjectName}>
-            <Link to={getProjectUrl(issue.subProject, branch)} className="link-no-underline">
+            <Link to={getBranchLikeUrl(issue.subProject, branchLike)} className="link-no-underline">
               {limitComponentName(issue.subProjectName)}
             </Link>
             <span className="slash-separator" />
           </span>
         )}
 
-      <Link to={getProjectUrl(issue.component, branch)} className="link-no-underline">
+      <Link
+        to={getCodeUrl(issue.project, branchLike, issue.component)}
+        className="link-no-underline">
         <span title={issue.componentLongName}>{collapsePath(issue.componentLongName)}</span>
       </Link>
     </div>
index 30506292162f83b54320a6b49d9977da6edc884c..c7d6bc2e65061c566cc49b62110c3c8946810e46 100644 (file)
@@ -25,7 +25,7 @@ import ListItem from './ListItem';
 
 /*::
 type Props = {|
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   checked: Array<string>,
   component?: Component,
   issues: Array<Issue>,
@@ -44,13 +44,13 @@ export default class IssuesList extends React.PureComponent {
   /*:: props: Props; */
 
   render() {
-    const { branch, checked, component, issues, openPopup, selectedIssue } = this.props;
+    const { branchLike, checked, component, issues, openPopup, selectedIssue } = this.props;
 
     return (
       <div>
         {issues.map((issue, index) => (
           <ListItem
-            branch={branch}
+            branchLike={branchLike}
             checked={checked.includes(issue.key)}
             component={component}
             key={issue.key}
index 6f7422c2290124e681c7e99cfca6df0d861149ab..fb72e2eca3b29cb6011668027b0502dc98c011e6 100644 (file)
@@ -26,7 +26,7 @@ import { scrollToElement } from '../../../helpers/scrolling';
 
 /*::
 type Props = {|
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   component: Component,
   loadIssues: (string, number, number) => Promise<*>,
   onIssueChange: Issue => void,
@@ -107,7 +107,7 @@ export default class IssuesSourceViewer extends React.PureComponent {
       <div ref={node => (this.node = node)}>
         <SourceViewer
           aroundLine={aroundLine}
-          branch={this.props.branch}
+          branchLike={this.props.branchLike}
           component={openIssue.component}
           displayAllIssues={true}
           displayIssueLocationsCount={false}
index 28c6ae6976091bb88bbe0b2479f4a3cad480f3c0..e4802fc64a3edf71981cb2dfa66f7a199ae1d538 100644 (file)
@@ -26,7 +26,7 @@ import Issue from '../../../components/issue/Issue';
 
 /*::
 type Props = {|
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   checked: boolean,
   component?: Component,
   issue: IssueType,
@@ -89,7 +89,7 @@ export default class ListItem extends React.PureComponent {
   };
 
   render() {
-    const { branch, component, issue, previousIssue } = this.props;
+    const { branchLike, component, issue, previousIssue } = this.props;
 
     const displayComponent = previousIssue == null || previousIssue.component !== issue.component;
 
@@ -98,7 +98,7 @@ export default class ListItem extends React.PureComponent {
         {displayComponent && (
           <div className="issues-workspace-list-component">
             <ComponentBreadcrumbs
-              branch={branch}
+              branchLike={branchLike}
               component={component}
               issue={this.props.issue}
               organization={this.props.organization}
@@ -106,7 +106,7 @@ export default class ListItem extends React.PureComponent {
           </div>
         )}
         <Issue
-          branch={branch}
+          branchLike={branchLike}
           checked={this.props.checked}
           displayLocationsLink={false}
           issue={issue}
index 3bee431c81c42306bbb8f0754c7c2ce0f9cd03ff..00c83d10dcd3a93587f6c24169cc877d38df56af 100644 (file)
@@ -20,6 +20,7 @@
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import ComponentBreadcrumbs from '../ComponentBreadcrumbs';
+import { ShortLivingBranch, BranchType } from '../../../../app/types';
 
 const baseIssue = {
   component: 'comp',
@@ -40,5 +41,13 @@ it('renders with sub-project', () => {
 
 it('renders with branch', () => {
   const issue = { ...baseIssue, subProject: 'sub-proj', subProjectName: 'sub-proj-name' };
-  expect(shallow(<ComponentBreadcrumbs branch="feature" issue={issue} />)).toMatchSnapshot();
+  const shortBranch: ShortLivingBranch = {
+    isMain: false,
+    mergeBranch: '',
+    name: 'feature',
+    type: BranchType.SHORT
+  };
+  expect(
+    shallow(<ComponentBreadcrumbs branchLike={shortBranch} issue={issue} />)
+  ).toMatchSnapshot();
 });
index 3861f4a07809efac36a2e5626a9680b5e0fb00b2..ac510c728244ad4048203d73d82deae03a5f759e 100644 (file)
@@ -19,7 +19,6 @@ exports[`renders 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "proj",
           },
         }
@@ -37,10 +36,10 @@ exports[`renders 1`] = `
     style={Object {}}
     to={
       Object {
-        "pathname": "/dashboard",
+        "pathname": "/code",
         "query": Object {
-          "branch": undefined,
-          "id": "comp",
+          "id": "proj",
+          "selected": "comp",
         },
       }
     }
@@ -71,10 +70,11 @@ exports[`renders with branch 1`] = `
       style={Object {}}
       to={
         Object {
-          "pathname": "/dashboard",
+          "pathname": "/project/issues",
           "query": Object {
             "branch": "feature",
             "id": "proj",
+            "resolved": "false",
           },
         }
       }
@@ -94,10 +94,11 @@ exports[`renders with branch 1`] = `
       style={Object {}}
       to={
         Object {
-          "pathname": "/dashboard",
+          "pathname": "/project/issues",
           "query": Object {
             "branch": "feature",
             "id": "sub-proj",
+            "resolved": "false",
           },
         }
       }
@@ -114,10 +115,11 @@ exports[`renders with branch 1`] = `
     style={Object {}}
     to={
       Object {
-        "pathname": "/dashboard",
+        "pathname": "/code",
         "query": Object {
           "branch": "feature",
-          "id": "comp",
+          "id": "proj",
+          "selected": "comp",
         },
       }
     }
@@ -150,7 +152,6 @@ exports[`renders with sub-project 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "proj",
           },
         }
@@ -173,7 +174,6 @@ exports[`renders with sub-project 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "sub-proj",
           },
         }
@@ -191,10 +191,10 @@ exports[`renders with sub-project 1`] = `
     style={Object {}}
     to={
       Object {
-        "pathname": "/dashboard",
+        "pathname": "/code",
         "query": Object {
-          "branch": undefined,
-          "id": "comp",
+          "id": "proj",
+          "selected": "comp",
         },
       }
     }
index 3592c4daea3cf1dce8fb2eef30cd19dc49b42b19..ebea71b75a605ef8385322718d7b286b1f0fec4e 100644 (file)
@@ -21,15 +21,16 @@ import * as React from 'react';
 import BadgeButton from './BadgeButton';
 import BadgeParams from './BadgeParams';
 import { BadgeType, BadgeOptions, getBadgeUrl } from './utils';
-import { Metric } from '../../../app/types';
+import { Metric, BranchLike } from '../../../app/types';
 import CodeSnippet from '../../../components/common/CodeSnippet';
 import Modal from '../../../components/controls/Modal';
+import { getBranchLikeQuery } from '../../../helpers/branches';
 import { translate } from '../../../helpers/l10n';
 import './styles.css';
 import { Button, ResetButtonLink } from '../../../components/ui/buttons';
 
 interface Props {
-  branch?: string;
+  branchLike?: BranchLike;
   metrics: { [key: string]: Metric };
   project: string;
 }
@@ -64,10 +65,10 @@ export default class BadgesModal extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { branch, project } = this.props;
+    const { branchLike, project } = this.props;
     const { selectedType, badgeOptions } = this.state;
     const header = translate('overview.badges.title');
-    const fullBadgeOptions = { branch, project, ...badgeOptions };
+    const fullBadgeOptions = { project, ...badgeOptions, ...getBranchLikeQuery(branchLike) };
     return (
       <div className="overview-meta-card">
         <Button className="js-project-badges" onClick={this.handleOpen}>
index cea857f2e90bd3c511f04b1705153d26fe472ca2..d77c077d6d90c6a6820cfc21c676b48dc20d26fc 100644 (file)
@@ -21,13 +21,20 @@ import * as React from 'react';
 import { shallow } from 'enzyme';
 import BadgesModal from '../BadgesModal';
 import { click } from '../../../../helpers/testUtils';
+import { ShortLivingBranch, BranchType } from '../../../../app/types';
 
 jest.mock('../../../../helpers/urls', () => ({
   getHostUrl: () => 'host'
 }));
 
 it('should display the modal after click', () => {
-  const wrapper = shallow(<BadgesModal branch="branch-6.6" metrics={{}} project="foo" />);
+  const shortBranch: ShortLivingBranch = {
+    isMain: false,
+    mergeBranch: '',
+    name: 'branch-6.6',
+    type: BranchType.SHORT
+  };
+  const wrapper = shallow(<BadgesModal branchLike={shortBranch} metrics={{}} project="foo" />);
   expect(wrapper).toMatchSnapshot();
   click(wrapper.find('Button'));
   expect(wrapper.find('Modal')).toMatchSnapshot();
index 39113f8894535d64f0ef0e1c6fd80c30eda5b555..5f300830c1bf1977678957d6a7353011b381b3a5 100644 (file)
@@ -28,6 +28,7 @@ export interface BadgeOptions {
   color?: BadgeColors;
   project?: string;
   metric?: string;
+  pullRequest?: string;
 }
 
 export enum BadgeType {
@@ -38,19 +39,19 @@ export enum BadgeType {
 
 export function getBadgeUrl(
   type: BadgeType,
-  { branch, project, color = 'white', metric = 'alert_status' }: BadgeOptions
+  { branch, project, color = 'white', metric = 'alert_status', pullRequest }: BadgeOptions
 ) {
   switch (type) {
     case BadgeType.marketing:
       return `${getHostUrl()}/images/project_badges/sonarcloud-${color}.svg`;
     case BadgeType.qualityGate:
       return `${getHostUrl()}/api/project_badges/quality_gate?${stringify(
-        omitNil({ branch, project })
+        omitNil({ branch, project, pullRequest })
       )}`;
     case BadgeType.measure:
     default:
       return `${getHostUrl()}/api/project_badges/measure?${stringify(
-        omitNil({ branch, project, metric })
+        omitNil({ branch, project, metric, pullRequest })
       )}`;
   }
 }
index 03d898d4e9ebe18623b029b5e941f12f83d0f3a3..0d7caeb5d3e565fc09ba1da5f95747f694aff9e5 100644 (file)
@@ -21,12 +21,12 @@ import * as React from 'react';
 import * as PropTypes from 'prop-types';
 import OverviewApp from './OverviewApp';
 import EmptyOverview from './EmptyOverview';
-import { getBranchName, isShortLivingBranch } from '../../../helpers/branches';
-import { getProjectBranchUrl, getCodeUrl } from '../../../helpers/urls';
-import { Branch, Component } from '../../../app/types';
+import { Component, BranchLike } from '../../../app/types';
+import { isShortLivingBranch } from '../../../helpers/branches';
+import { getShortLivingBranchUrl, getCodeUrl } from '../../../helpers/urls';
 
 interface Props {
-  branch?: Branch;
+  branchLike?: BranchLike;
   component: Component;
   isInProgress?: boolean;
   isPending?: boolean;
@@ -39,7 +39,7 @@ export default class App extends React.PureComponent<Props> {
   };
 
   componentDidMount() {
-    const { branch, component } = this.props;
+    const { branchLike, component } = this.props;
 
     if (this.isPortfolio()) {
       this.context.router.replace({
@@ -48,10 +48,10 @@ export default class App extends React.PureComponent<Props> {
       });
     } else if (this.isFile()) {
       this.context.router.replace(
-        getCodeUrl(component.breadcrumbs[0].key, getBranchName(branch), component.key)
+        getCodeUrl(component.breadcrumbs[0].key, branchLike, component.key)
       );
-    } else if (isShortLivingBranch(branch)) {
-      this.context.router.replace(getProjectBranchUrl(component.key, branch));
+    } else if (isShortLivingBranch(branchLike)) {
+      this.context.router.replace(getShortLivingBranchUrl(component.key, branchLike.name));
     }
   }
 
@@ -60,9 +60,9 @@ export default class App extends React.PureComponent<Props> {
   isFile = () => ['FIL', 'UTS'].includes(this.props.component.qualifier);
 
   render() {
-    const { branch, component } = this.props;
+    const { branchLike, component } = this.props;
 
-    if (this.isPortfolio() || this.isFile() || isShortLivingBranch(branch)) {
+    if (this.isPortfolio() || this.isFile() || isShortLivingBranch(branchLike)) {
       return null;
     }
 
@@ -77,7 +77,7 @@ export default class App extends React.PureComponent<Props> {
 
     return (
       <OverviewApp
-        branch={branch}
+        branchLike={branchLike}
         component={component}
         onComponentChange={this.props.onComponentChange}
       />
index 5bc3679f9dfae6604b51c956d8941c1af5df510f..091826a44a6c2079bc7758f7aef147608a4eb735 100644 (file)
@@ -36,14 +36,14 @@ import { getLeakPeriod, Period } from '../../../helpers/periods';
 import { getCustomGraph, getGraph } from '../../../helpers/storage';
 import { METRICS, HISTORY_METRICS_LIST } from '../utils';
 import { DEFAULT_GRAPH, getDisplayedHistoryMetrics } from '../../projectActivity/utils';
-import { getBranchName } from '../../../helpers/branches';
+import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches';
 import { fetchMetrics } from '../../../store/rootActions';
 import { getMetrics } from '../../../store/rootReducer';
-import { Branch, Component, Metric } from '../../../app/types';
+import { BranchLike, Component, Metric } from '../../../app/types';
 import '../styles.css';
 
 interface OwnProps {
-  branch?: Branch;
+  branchLike?: BranchLike;
   component: Component;
   onComponentChange: (changes: {}) => void;
 }
@@ -79,7 +79,7 @@ export class OverviewApp extends React.PureComponent<Props, State> {
   componentDidUpdate(prevProps: Props) {
     if (
       this.props.component.key !== prevProps.component.key ||
-      this.props.branch !== prevProps.branch
+      !isSameBranchLike(this.props.branchLike, prevProps.branchLike)
     ) {
       this.loadMeasures().then(this.loadHistory, () => {});
     }
@@ -90,12 +90,12 @@ export class OverviewApp extends React.PureComponent<Props, State> {
   }
 
   loadMeasures() {
-    const { branch, component } = this.props;
+    const { branchLike, component } = this.props;
     this.setState({ loading: true });
 
     return getMeasuresAndMeta(component.key, METRICS, {
       additionalFields: 'metrics,periods',
-      branch: getBranchName(branch)
+      ...getBranchLikeQuery(branchLike)
     }).then(
       r => {
         if (this.mounted && r.metrics) {
@@ -116,7 +116,7 @@ export class OverviewApp extends React.PureComponent<Props, State> {
   }
 
   loadHistory = () => {
-    const { branch, component } = this.props;
+    const { branchLike, component } = this.props;
 
     let graphMetrics = getDisplayedHistoryMetrics(getGraph(), getCustomGraph());
     if (!graphMetrics || graphMetrics.length <= 0) {
@@ -124,22 +124,24 @@ export class OverviewApp extends React.PureComponent<Props, State> {
     }
 
     const metrics = uniq(HISTORY_METRICS_LIST.concat(graphMetrics));
-    return getAllTimeMachineData(component.key, metrics, { branch: getBranchName(branch) }).then(
-      r => {
-        if (this.mounted) {
-          const history: History = {};
-          r.measures.forEach(measure => {
-            const measureHistory = measure.history.map(analysis => ({
-              date: parseDate(analysis.date),
-              value: analysis.value
-            }));
-            history[measure.metric] = measureHistory;
-          });
-          const historyStartDate = history[HISTORY_METRICS_LIST[0]][0].date;
-          this.setState({ history, historyStartDate });
-        }
+    return getAllTimeMachineData({
+      ...getBranchLikeQuery(branchLike),
+      component: component.key,
+      metrics: metrics.join()
+    }).then(r => {
+      if (this.mounted) {
+        const history: History = {};
+        r.measures.forEach(measure => {
+          const measureHistory = measure.history.map(analysis => ({
+            date: parseDate(analysis.date),
+            value: analysis.value
+          }));
+          history[measure.metric] = measureHistory;
+        });
+        const historyStartDate = history[HISTORY_METRICS_LIST[0]][0].date;
+        this.setState({ history, historyStartDate });
       }
-    );
+    });
   };
 
   getApplicationLeakPeriod = () =>
@@ -156,7 +158,7 @@ export class OverviewApp extends React.PureComponent<Props, State> {
   }
 
   render() {
-    const { branch, component } = this.props;
+    const { branchLike, component } = this.props;
     const { loading, measures, periods, history, historyStartDate } = this.state;
 
     if (loading) {
@@ -165,9 +167,8 @@ export class OverviewApp extends React.PureComponent<Props, State> {
 
     const leakPeriod =
       component.qualifier === 'APP' ? this.getApplicationLeakPeriod() : getLeakPeriod(periods);
-    const branchName = getBranchName(branch);
     const domainProps = {
-      branch: branchName,
+      branchLike,
       component,
       measures,
       leakPeriod,
@@ -182,7 +183,7 @@ export class OverviewApp extends React.PureComponent<Props, State> {
             {component.qualifier === 'APP' ? (
               <ApplicationQualityGate component={component} />
             ) : (
-              <QualityGate branch={branchName} component={component} measures={measures} />
+              <QualityGate branchLike={branchLike} component={component} measures={measures} />
             )}
 
             <div className="overview-domains-list">
@@ -195,7 +196,7 @@ export class OverviewApp extends React.PureComponent<Props, State> {
 
           <div className="overview-sidebar page-sidebar-fixed">
             <Meta
-              branch={branchName}
+              branchLike={branchLike}
               component={component}
               history={history}
               measures={measures}
index 4edd63ffba84e8bd1579b56651031af4cce18834..f3eb8d43cba2966460bb9ae147471da2266872ee 100644 (file)
@@ -54,7 +54,7 @@ it('redirects on Code page for files', () => {
     qualifier: 'FIL'
   };
   const replace = jest.fn();
-  mount(<App branch={branch} component={newComponent} onComponentChange={jest.fn()} />, {
+  mount(<App branchLike={branch} component={newComponent} onComponentChange={jest.fn()} />, {
     context: { router: { replace } }
   });
   expect(replace).toBeCalledWith({
index 8380a53b0a5dbbbdb3bc58534159c6ff14fd3bd4..d405662ace4fa963aa00bf6735d5f63d2fa2fe7d 100644 (file)
@@ -23,11 +23,13 @@ import Analysis from './Analysis';
 import { getProjectActivity, Analysis as IAnalysis } from '../../../api/projectActivity';
 import PreviewGraph from '../../../components/preview-graph/PreviewGraph';
 import { translate } from '../../../helpers/l10n';
-import { Metric, Component } from '../../../app/types';
+import { Metric, Component, BranchLike } from '../../../app/types';
 import { History } from '../../../api/time-machine';
+import { getBranchLikeQuery } from '../../../helpers/branches';
+import { getActivityUrl } from '../../../helpers/urls';
 
 interface Props {
-  branch?: string;
+  branchLike?: BranchLike;
   component: Component;
   history?: History;
   metrics: { [key: string]: Metric };
@@ -76,7 +78,7 @@ export default class AnalysesList extends React.PureComponent<Props, State> {
     this.setState({ loading: true });
 
     getProjectActivity({
-      branch: this.props.branch,
+      ...getBranchLikeQuery(this.props.branchLike),
       project: this.getTopLevelComponent(),
       ps: PAGE_SIZE
     }).then(
@@ -101,7 +103,7 @@ export default class AnalysesList extends React.PureComponent<Props, State> {
     return (
       <ul className="spacer-top">
         {analyses.map(analysis => (
-          <Analysis key={analysis.key} analysis={analysis} qualifier={this.props.qualifier} />
+          <Analysis analysis={analysis} key={analysis.key} qualifier={this.props.qualifier} />
         ))}
       </ul>
     );
@@ -121,20 +123,16 @@ export default class AnalysesList extends React.PureComponent<Props, State> {
         </h4>
 
         <PreviewGraph
-          branch={this.props.branch}
+          branchLike={this.props.branchLike}
           history={this.props.history}
-          project={this.props.component.key}
           metrics={this.props.metrics}
+          project={this.props.component.key}
         />
 
         {this.renderList(analyses)}
 
         <div className="spacer-top small">
-          <Link
-            to={{
-              pathname: '/project/activity',
-              query: { id: this.props.component.key, branch: this.props.branch }
-            }}>
+          <Link to={getActivityUrl(this.props.component.key, this.props.branchLike)}>
             {translate('show_more')}
           </Link>
         </div>
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/AnalysesList-test.tsx b/server/sonar-web/src/main/js/apps/overview/events/__tests__/AnalysesList-test.tsx
new file mode 100644 (file)
index 0000000..710f260
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * 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 AnalysesList from '../AnalysesList';
+
+it('should render show more link', () => {
+  const branchLike = { analysisDate: '2018-03-08T09:49:22+0100', isMain: true, name: 'master' };
+  const component = {
+    breadcrumbs: [{ key: 'foo', name: 'foo', qualifier: 'TRK' }],
+    key: 'foo',
+    name: 'foo',
+    organization: 'org',
+    qualifier: 'TRK'
+  };
+  const wrapper = shallow(
+    <AnalysesList branchLike={branchLike} component={component} metrics={{}} qualifier="TRK" />
+  );
+  wrapper.setState({ loading: false });
+  expect(wrapper.find('Link')).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/AnalysesList-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/AnalysesList-test.tsx.snap
new file mode 100644 (file)
index 0000000..2320d0d
--- /dev/null
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render show more link 1`] = `
+<Link
+  onlyActiveOnIndex={false}
+  style={Object {}}
+  to={
+    Object {
+      "pathname": "/project/activity",
+      "query": Object {
+        "id": "foo",
+      },
+    }
+  }
+>
+  show_more
+</Link>
+`;
index d6433a10e08701355f704061e7796c67ad8a0307..52fd2d5253b152e6465aa1c46c0857ef48b96a93 100644 (file)
@@ -31,7 +31,7 @@ import { translate } from '../../../helpers/l10n';
 
 export class BugsAndVulnerabilities extends React.PureComponent<ComposedProps> {
   renderHeader() {
-    const { branch, component } = this.props;
+    const { branchLike, component } = this.props;
 
     return (
       <div className="overview-card-header">
@@ -39,13 +39,13 @@ export class BugsAndVulnerabilities extends React.PureComponent<ComposedProps> {
           <span>{translate('metric.bugs.name')}</span>
           <Link
             className="button button-small spacer-left text-text-bottom"
-            to={getComponentDrilldownUrl(component.key, 'Reliability', branch)}>
+            to={getComponentDrilldownUrl(component.key, 'Reliability', branchLike)}>
             <BubblesIcon size={14} />
           </Link>
           <span className="big-spacer-left">{translate('metric.vulnerabilities.name')}</span>
           <Link
             className="button button-small spacer-left text-text-bottom"
-            to={getComponentDrilldownUrl(component.key, 'Security', branch)}>
+            to={getComponentDrilldownUrl(component.key, 'Security', branchLike)}>
             <BubblesIcon size={14} />
           </Link>
         </div>
index ac7fcd0c94c5e002e13788a781a6efca943d5fa6..bc632fae96a16f6d592d5f90cfb395bd8caff34e 100644 (file)
@@ -26,6 +26,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
 import { getComponentIssuesUrl } from '../../../helpers/urls';
 import CodeSmellIcon from '../../../components/icons-components/CodeSmellIcon';
+import { getBranchLikeQuery } from '../../../helpers/branches';
 
 export class CodeSmells extends React.PureComponent<ComposedProps> {
   renderHeader() {
@@ -33,10 +34,15 @@ export class CodeSmells extends React.PureComponent<ComposedProps> {
   }
 
   renderDebt(metric: string, type: string) {
-    const { branch, measures, component } = this.props;
+    const { branchLike, measures, component } = this.props;
     const measure = measures.find(measure => measure.metric.key === metric);
     const value = measure ? this.props.getValue(measure) : undefined;
-    const params = { branch, resolved: 'false', facetMode: 'effort', types: type };
+    const params = {
+      ...getBranchLikeQuery(branchLike),
+      resolved: 'false',
+      facetMode: 'effort',
+      types: type
+    };
 
     if (isDiffMetric(metric)) {
       Object.assign(params, { sinceLeakPeriod: 'true' });
index cd1abc807eee64fccbf42b5f6ae8f9156153b138..28e5a141ef786aabe810b5cca44da280a4e37bfc 100644 (file)
@@ -44,7 +44,7 @@ export class Coverage extends React.PureComponent<ComposedProps> {
   }
 
   renderCoverage() {
-    const { branch, component } = this.props;
+    const { branchLike, component } = this.props;
     const metric = 'coverage';
     const coverage = this.getCoverage();
 
@@ -56,7 +56,7 @@ export class Coverage extends React.PureComponent<ComposedProps> {
 
         <div className="display-inline-block text-middle">
           <div className="overview-domain-measure-value">
-            <DrilldownLink branch={branch} component={component.key} metric={metric}>
+            <DrilldownLink branchLike={branchLike} component={component.key} metric={metric}>
               <span className="js-overview-main-coverage">
                 {formatMeasure(coverage, 'PERCENT')}
               </span>
@@ -73,7 +73,7 @@ export class Coverage extends React.PureComponent<ComposedProps> {
   }
 
   renderNewCoverage() {
-    const { branch, component, leakPeriod, measures } = this.props;
+    const { branchLike, component, leakPeriod, measures } = this.props;
     if (!leakPeriod) {
       return null;
     }
@@ -85,7 +85,7 @@ export class Coverage extends React.PureComponent<ComposedProps> {
       newCoverageMeasure && newCoverageValue !== undefined ? (
         <div>
           <DrilldownLink
-            branch={branch}
+            branchLike={branchLike}
             component={component.key}
             metric={newCoverageMeasure.metric.key}>
             <span className="js-overview-main-new-coverage">
@@ -106,7 +106,7 @@ export class Coverage extends React.PureComponent<ComposedProps> {
           {translate('overview.coverage_on')}
           <br />
           <DrilldownLink
-            branch={branch}
+            branchLike={branchLike}
             className="spacer-right overview-domain-secondary-measure-value"
             component={component.key}
             metric={newLinesToCover.metric.key}>
index d8a09caee4064649edeab1cdb43a2d01b5a8d4ab..49db5f74c36cb0742b8ede00cd94b4a07cbb6bae 100644 (file)
@@ -39,8 +39,7 @@ export class Duplications extends React.PureComponent<ComposedProps> {
   }
 
   renderDuplications() {
-    const { branch, component, measures } = this.props;
-
+    const { branchLike, component, measures } = this.props;
     const measure = measures.find(measure => measure.metric.key === 'duplicated_lines_density');
     if (!measure) {
       return null;
@@ -57,7 +56,7 @@ export class Duplications extends React.PureComponent<ComposedProps> {
         <div className="display-inline-block text-middle">
           <div className="overview-domain-measure-value">
             <DrilldownLink
-              branch={branch}
+              branchLike={branchLike}
               component={component.key}
               metric="duplicated_lines_density">
               {formatMeasure(duplications, 'PERCENT')}
@@ -74,11 +73,10 @@ export class Duplications extends React.PureComponent<ComposedProps> {
   }
 
   renderNewDuplications() {
-    const { branch, component, measures, leakPeriod } = this.props;
+    const { branchLike, component, measures, leakPeriod } = this.props;
     if (!leakPeriod) {
       return null;
     }
-
     const newDuplicationsMeasure = measures.find(
       measure => measure.metric.key === 'new_duplicated_lines_density'
     );
@@ -88,7 +86,7 @@ export class Duplications extends React.PureComponent<ComposedProps> {
       newDuplicationsMeasure && newDuplicationsValue ? (
         <div>
           <DrilldownLink
-            branch={branch}
+            branchLike={branchLike}
             component={component.key}
             metric={newDuplicationsMeasure.metric.key}>
             <span className="js-overview-main-new-duplications">
@@ -108,7 +106,7 @@ export class Duplications extends React.PureComponent<ComposedProps> {
           {translate('overview.duplications_on')}
           <br />
           <DrilldownLink
-            branch={branch}
+            branchLike={branchLike}
             className="spacer-right overview-domain-secondary-measure-value"
             component={component.key}
             metric={newLinesMeasure.metric.key}>
index 2b07c3c92393a7ab21db9bf4fcdb8f90ea655db6..2251612e2e3e8aec321c90c6b36ce67d0ccf57ed 100644 (file)
@@ -40,11 +40,12 @@ import {
   getComponentIssuesUrl,
   getMeasureHistoryUrl
 } from '../../../helpers/urls';
-import { Component } from '../../../app/types';
+import { Component, BranchLike } from '../../../app/types';
 import { History } from '../../../api/time-machine';
+import { getBranchLikeQuery } from '../../../helpers/branches';
 
 export interface EnhanceProps {
-  branch?: string;
+  branchLike?: BranchLike;
   component: Component;
   measures: MeasureEnhanced[];
   leakPeriod?: { index: number; date?: string };
@@ -77,14 +78,14 @@ export default function enhance(ComposedComponent: React.ComponentType<ComposedP
     };
 
     renderHeader = (domain: string, label: string) => {
-      const { branch, component } = this.props;
+      const { branchLike, component } = this.props;
       return (
         <div className="overview-card-header">
           <div className="overview-title">
             <span>{label}</span>
             <Link
               className="button button-small spacer-left text-text-bottom"
-              to={getComponentDrilldownUrl(component.key, domain, branch)}>
+              to={getComponentDrilldownUrl(component.key, domain, branchLike)}>
               <BubblesIcon size={14} />
             </Link>
           </div>
@@ -93,7 +94,7 @@ export default function enhance(ComposedComponent: React.ComponentType<ComposedP
     };
 
     renderMeasure = (metricKey: string) => {
-      const { branch, measures, component } = this.props;
+      const { branchLike, measures, component } = this.props;
       const measure = measures.find(measure => measure.metric.key === metricKey);
       if (!measure) {
         return null;
@@ -102,7 +103,7 @@ export default function enhance(ComposedComponent: React.ComponentType<ComposedP
       return (
         <div className="overview-domain-measure">
           <div className="overview-domain-measure-value">
-            <DrilldownLink branch={branch} component={component.key} metric={metricKey}>
+            <DrilldownLink branchLike={branchLike} component={component.key} metric={metricKey}>
               <span className="js-overview-main-tests">
                 {formatMeasure(measure.value, getShortType(measure.metric.type))}
               </span>
@@ -118,7 +119,7 @@ export default function enhance(ComposedComponent: React.ComponentType<ComposedP
     };
 
     renderRating = (metricKey: string) => {
-      const { branch, component, measures } = this.props;
+      const { branchLike, component, measures } = this.props;
       const measure = measures.find(measure => measure.metric.key === metricKey);
       if (!measure) {
         return null;
@@ -130,7 +131,7 @@ export default function enhance(ComposedComponent: React.ComponentType<ComposedP
         <Tooltip overlay={title} placement="top">
           <div className="overview-domain-measure-sup">
             <DrilldownLink
-              branch={branch}
+              branchLike={branchLike}
               className="link-no-underline"
               component={component.key}
               metric={metricKey}>
@@ -142,14 +143,14 @@ export default function enhance(ComposedComponent: React.ComponentType<ComposedP
     };
 
     renderIssues = (metric: string, type: string) => {
-      const { branch, measures, component } = this.props;
+      const { branchLike, measures, component } = this.props;
       const measure = measures.find(measure => measure.metric.key === metric);
       if (!measure) {
         return null;
       }
 
       const value = this.getValue(measure);
-      const params = { branch, resolved: 'false', types: type };
+      const params = { ...getBranchLikeQuery(branchLike), resolved: 'false', types: type };
       if (isDiffMetric(metric)) {
         Object.assign(params, { sinceLeakPeriod: 'true' });
       }
@@ -166,7 +167,7 @@ export default function enhance(ComposedComponent: React.ComponentType<ComposedP
       return (
         <Link
           className={linkClass}
-          to={getMeasureHistoryUrl(this.props.component.key, metricKey, this.props.branch)}>
+          to={getMeasureHistoryUrl(this.props.component.key, metricKey, this.props.branchLike)}>
           <HistoryIcon />
         </Link>
       );
index 4a55b5e29a44a91be5dd99a91283e8fa7839264f..edca9b4c4cbe26d137221396cb027e8fc017f3a7 100644 (file)
@@ -28,13 +28,13 @@ import MetaSize from './MetaSize';
 import MetaTags from './MetaTags';
 import BadgesModal from '../badges/BadgesModal';
 import AnalysesList from '../events/AnalysesList';
-import { Visibility, Component, Metric } from '../../../app/types';
+import { Visibility, Component, Metric, BranchLike } from '../../../app/types';
 import { History } from '../../../api/time-machine';
 import { translate } from '../../../helpers/l10n';
 import { MeasureEnhanced } from '../../../helpers/measures';
 
 interface Props {
-  branch?: string;
+  branchLike?: BranchLike;
   component: Component;
   history?: History;
   measures: MeasureEnhanced[];
@@ -50,7 +50,7 @@ export default class Meta extends React.PureComponent<Props> {
 
   render() {
     const { onSonarCloud, organizationsEnabled } = this.context;
-    const { branch, component, metrics } = this.props;
+    const { branchLike, component, metrics } = this.props;
     const { qualifier, description, qualityProfiles, qualityGate, visibility } = component;
 
     const isProject = qualifier === 'TRK';
@@ -66,11 +66,11 @@ export default class Meta extends React.PureComponent<Props> {
           {isProject && (
             <MetaTags component={component} onComponentChange={this.props.onComponentChange} />
           )}
-          <MetaSize branch={branch} component={component} measures={this.props.measures} />
+          <MetaSize branchLike={branchLike} component={component} measures={this.props.measures} />
         </div>
 
         <AnalysesList
-          branch={branch}
+          branchLike={branchLike}
           component={component}
           history={this.props.history}
           metrics={metrics}
@@ -106,7 +106,9 @@ export default class Meta extends React.PureComponent<Props> {
 
         {onSonarCloud &&
           isProject &&
-          !isPrivate && <BadgesModal branch={branch} metrics={metrics} project={component.key} />}
+          !isPrivate && (
+            <BadgesModal branchLike={branchLike} metrics={metrics} project={component.key} />
+          )}
       </div>
     );
   }
index 335d0b487956bbcab44897c87f7d8f83072bd38a..0fe760979f5d080c203736e5ae0589f071314d8a 100644 (file)
@@ -25,10 +25,10 @@ import SizeRating from '../../../components/ui/SizeRating';
 import { formatMeasure, MeasureEnhanced } from '../../../helpers/measures';
 import { getMetricName } from '../helpers/metrics';
 import { translate } from '../../../helpers/l10n';
-import { LightComponent } from '../../../app/types';
+import { LightComponent, BranchLike } from '../../../app/types';
 
 interface Props {
-  branch?: string;
+  branchLike?: BranchLike;
   component: LightComponent;
   measures: MeasureEnhanced[];
 }
@@ -43,7 +43,10 @@ export default class MetaSize extends React.PureComponent<Props> {
       <span className="spacer-right">
         <SizeRating value={Number(ncloc.value)} />
       </span>
-      <DrilldownLink branch={this.props.branch} component={this.props.component.key} metric="ncloc">
+      <DrilldownLink
+        branchLike={this.props.branchLike}
+        component={this.props.component.key}
+        metric="ncloc">
         {formatMeasure(ncloc.value, 'SHORT_INT')}
       </DrilldownLink>
       <div className="spacer-top text-muted">{getMetricName('ncloc')}</div>
@@ -71,7 +74,7 @@ export default class MetaSize extends React.PureComponent<Props> {
     return projects ? (
       <div id="overview-projects" className="overview-meta-size-ncloc is-half-width">
         <DrilldownLink
-          branch={this.props.branch}
+          branchLike={this.props.branchLike}
           component={this.props.component.key}
           metric="projects">
           {formatMeasure(projects.value, 'SHORT_INT')}
index 5c3125aedeedca75c8fa6a5824e4887623865017..20ab78851469ac7efa294a23ceeea7bbf979ee7c 100644 (file)
@@ -38,13 +38,13 @@ function isProject(component /*: Component */) {
 
 /*::
 type Props = {
-  branch?: string,
+  branchLike?: {id?: string; name: string },
   component: Component,
   measures: MeasuresList
 };
 */
 
-export default function QualityGate({ branch, component, measures } /*: Props */) {
+export default function QualityGate({ branchLike, component, measures } /*: Props */) {
   const statusMeasure = measures.find(measure => measure.metric.key === 'alert_status');
   const detailsMeasure = measures.find(measure => measure.metric.key === 'quality_gate_details');
 
@@ -81,7 +81,11 @@ export default function QualityGate({ branch, component, measures } /*: Props */
       )}
 
       {conditions.length > 0 && (
-        <QualityGateConditions branch={branch} component={component} conditions={conditions} />
+        <QualityGateConditions
+          branchLike={branchLike}
+          component={component}
+          conditions={conditions}
+        />
       )}
     </div>
   );
index 3301c33ef6fb96d616bb329103cae1fc0a7cefc9..ba6d12fa35d05c796c8ab4b4dbd0f6291f2b61c2 100644 (file)
@@ -27,12 +27,13 @@ import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
 import { getPeriodValue, isDiffMetric, formatMeasure } from '../../../helpers/measures';
 import { translate } from '../../../helpers/l10n';
 import { getComponentIssuesUrl } from '../../../helpers/urls';
+import { getBranchLikeQuery } from '../../../helpers/branches';
 /*:: import type { Component } from '../types'; */
 /*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
 
 export default class QualityGateCondition extends React.PureComponent {
   /*:: props: {
-    branch?: string,
+    branchLike?: { id?: string; name: string },
     component: Component,
     condition: {
       level: string,
@@ -54,7 +55,11 @@ export default class QualityGateCondition extends React.PureComponent {
   }
 
   getIssuesUrl = (sinceLeakPeriod /*: boolean */, customQuery /*: {} */) => {
-    const query /*: Object */ = { resolved: 'false', branch: this.props.branch, ...customQuery };
+    const query /*: Object */ = {
+      resolved: 'false',
+      ...getBranchLikeQuery(this.props.branchLike),
+      ...customQuery
+    };
     if (sinceLeakPeriod) {
       Object.assign(query, { sinceLeakPeriod: 'true' });
     }
@@ -89,7 +94,7 @@ export default class QualityGateCondition extends React.PureComponent {
   }
 
   wrapWithLink(children /*: React.Element<*> */) {
-    const { branch, component, condition } = this.props;
+    const { branchLike, component, condition } = this.props;
 
     const className = classNames(
       'overview-quality-gate-condition',
@@ -114,7 +119,7 @@ export default class QualityGateCondition extends React.PureComponent {
       </Link>
     ) : (
       <DrilldownLink
-        branch={branch}
+        branchLike={branchLike}
         className={className}
         component={component.key}
         metric={condition.measure.metric.key}
index 8abd92619a140f2d7b07cc9b7867cc5fc0c41952..57dc3002b4d7b8fb19c9ca2724c91ca472a65651 100644 (file)
  */
 import React from 'react';
 import { sortBy } from 'lodash';
+import PropTypes from 'prop-types';
 import QualityGateCondition from './QualityGateCondition';
 import { ComponentType, ConditionsListType } from '../propTypes';
 import { getMeasuresAndMeta } from '../../../api/measures';
 import { enhanceMeasuresWithMetrics } from '../../../helpers/measures';
+import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches';
 
 const LEVEL_ORDER = ['ERROR', 'WARN'];
 
@@ -35,7 +37,7 @@ function enhanceConditions(conditions, measures) {
 
 export default class QualityGateConditions extends React.PureComponent {
   static propTypes = {
-    // branch
+    branchLike: PropTypes.object,
     component: ComponentType.isRequired,
     conditions: ConditionsListType.isRequired
   };
@@ -51,7 +53,7 @@ export default class QualityGateConditions extends React.PureComponent {
 
   componentDidUpdate(prevProps) {
     if (
-      prevProps.branch !== this.props.branch ||
+      !isSameBranchLike(prevProps.branchLike, this.props.branchLike) ||
       prevProps.conditions !== this.props.conditions ||
       prevProps.component !== this.props.component
     ) {
@@ -64,13 +66,13 @@ export default class QualityGateConditions extends React.PureComponent {
   }
 
   loadFailedMeasures() {
-    const { branch, component, conditions } = this.props;
+    const { branchLike, component, conditions } = this.props;
     const failedConditions = conditions.filter(c => c.level !== 'OK');
     if (failedConditions.length > 0) {
       const metrics = failedConditions.map(condition => condition.metric);
       getMeasuresAndMeta(component.key, metrics, {
         additionalFields: 'metrics',
-        branch
+        ...getBranchLikeQuery(branchLike)
       }).then(r => {
         if (this.mounted) {
           const measures = enhanceMeasuresWithMetrics(r.component.measures, r.metrics);
index 64d2bf86cfb68d35e4d9b027f42c8f1bd4d99a63..4c5e4ee1a87b7c0b4e72e345df43b753db81e2e1 100644 (file)
@@ -9,7 +9,6 @@ exports[`renders 1`] = `
     Object {
       "pathname": "/dashboard",
       "query": Object {
-        "branch": undefined,
         "id": "foo",
       },
     }
index e286b3da99d7346414e4305077ec517ec97938e1..900cc34dce2a6b332ef163f54c6a675f2ec4d5c1 100644 (file)
@@ -9,7 +9,6 @@ exports[`new_maintainability_rating 1`] = `
     Object {
       "pathname": "/project/issues",
       "query": Object {
-        "branch": undefined,
         "id": "abcd-key",
         "resolved": "false",
         "sinceLeakPeriod": "true",
@@ -104,7 +103,6 @@ exports[`new_reliability_rating 1`] = `
     Object {
       "pathname": "/project/issues",
       "query": Object {
-        "branch": undefined,
         "id": "abcd-key",
         "resolved": "false",
         "severities": "BLOCKER,CRITICAL,MAJOR,MINOR",
@@ -158,7 +156,6 @@ exports[`new_security_rating 1`] = `
     Object {
       "pathname": "/project/issues",
       "query": Object {
-        "branch": undefined,
         "id": "abcd-key",
         "resolved": "false",
         "severities": "BLOCKER,CRITICAL,MAJOR,MINOR",
@@ -254,7 +251,6 @@ exports[`reliability_rating 1`] = `
     Object {
       "pathname": "/project/issues",
       "query": Object {
-        "branch": undefined,
         "id": "abcd-key",
         "resolved": "false",
         "severities": "BLOCKER,CRITICAL,MAJOR,MINOR",
@@ -307,7 +303,6 @@ exports[`security_rating 1`] = `
     Object {
       "pathname": "/project/issues",
       "query": Object {
-        "branch": undefined,
         "id": "abcd-key",
         "resolved": "false",
         "severities": "BLOCKER,CRITICAL,MAJOR,MINOR",
@@ -360,7 +355,6 @@ exports[`should work with branch 1`] = `
     Object {
       "pathname": "/project/issues",
       "query": Object {
-        "branch": "feature",
         "id": "abcd-key",
         "resolved": "false",
         "sinceLeakPeriod": "true",
@@ -413,7 +407,6 @@ exports[`sqale_rating 1`] = `
     Object {
       "pathname": "/project/issues",
       "query": Object {
-        "branch": undefined,
         "id": "abcd-key",
         "resolved": "false",
         "types": "CODE_SMELL",
index ddba951e31320022c63ea2626a7b9fb7659ef5a0..01d14bb18078bd29974e6d3ec7731c8204f18784 100644 (file)
@@ -60,13 +60,13 @@ export default class Activity extends React.PureComponent<Props> {
   fetchHistory = () => {
     const { component } = this.props;
 
-    let graphMetrics = getDisplayedHistoryMetrics(getGraph(), getCustomGraph());
+    let graphMetrics: string[] = getDisplayedHistoryMetrics(getGraph(), getCustomGraph());
     if (!graphMetrics || graphMetrics.length <= 0) {
       graphMetrics = getDisplayedHistoryMetrics(DEFAULT_GRAPH, []);
     }
 
     this.setState({ loading: true });
-    return getAllTimeMachineData(component, graphMetrics).then(
+    return getAllTimeMachineData({ component, metrics: graphMetrics.join() }).then(
       timeMachine => {
         if (this.mounted) {
           const history: History = {};
index 27d4e2d957b38e9dae7d52358012178316806751..aa0fd5f0a169d78dec8e3ee06aad741c6f5a9ad0 100644 (file)
@@ -81,7 +81,7 @@ export class App extends React.PureComponent<Props, State> {
   fetchData() {
     this.setState({ loading: true });
     Promise.all([
-      getMeasures(this.props.component.key, PORTFOLIO_METRICS),
+      getMeasures({ componentKey: this.props.component.key, metricKeys: PORTFOLIO_METRICS.join() }),
       getChildren(this.props.component.key, SUB_COMPONENTS_METRICS, { ps: 20, s: 'qualifier' })
     ]).then(
       ([measures, subComponents]) => {
index c8c2e81f295b497c788f61b6ecf7c3e0841aae52..0b5fb1f158482e6b5658132090d21757d0671548 100644 (file)
@@ -67,5 +67,5 @@ it('renders', () => {
 
 it('fetches history', () => {
   mount(<Activity component="foo" metrics={{}} />);
-  expect(getAllTimeMachineData).toBeCalledWith('foo', ['coverage']);
+  expect(getAllTimeMachineData).toBeCalledWith({ component: 'foo', metrics: 'coverage' });
 });
index 6cfe8d6890be29617f450c68d3b325ff478b726c..4abdc4e056b745f61e1740fd50be412747f22797 100644 (file)
@@ -77,23 +77,11 @@ it('fetches measures and children components', () => {
   getMeasures.mockClear();
   getChildren.mockClear();
   mount(<App component={component} fetchMetrics={jest.fn()} metrics={{}} />);
-  expect(getMeasures).toBeCalledWith('foo', [
-    'projects',
-    'ncloc',
-    'ncloc_language_distribution',
-    'releasability_rating',
-    'releasability_effort',
-    'sqale_rating',
-    'maintainability_rating_effort',
-    'reliability_rating',
-    'reliability_rating_effort',
-    'security_rating',
-    'security_rating_effort',
-    'last_change_on_releasability_rating',
-    'last_change_on_maintainability_rating',
-    'last_change_on_security_rating',
-    'last_change_on_reliability_rating'
-  ]);
+  expect(getMeasures).toBeCalledWith({
+    componentKey: 'foo',
+    metricKeys:
+      'projects,ncloc,ncloc_language_distribution,releasability_rating,releasability_effort,sqale_rating,maintainability_rating_effort,reliability_rating,reliability_rating_effort,security_rating,security_rating_effort,last_change_on_releasability_rating,last_change_on_maintainability_rating,last_change_on_security_rating,last_change_on_reliability_rating'
+  });
   expect(getChildren).toBeCalledWith(
     'foo',
     [
index 8918c9e4b99d48c365bce9d96c1446e79d9af8d2..b278210c01b9896c162bd5610bfd25f61d2f6b9c 100644 (file)
@@ -16,7 +16,6 @@ exports[`renders 1`] = `
             Object {
               "pathname": "/component_measures",
               "query": Object {
-                "branch": undefined,
                 "id": "foo",
                 "metric": "security_rating",
               },
index e62b2d558c6aa3d3842e47f0d5fafc0a2fd30692..8f25ec024e178b389c73f030d3cff1532a7e183a 100644 (file)
@@ -9,7 +9,6 @@ exports[`renders 1`] = `
     Object {
       "pathname": "/project/activity",
       "query": Object {
-        "branch": undefined,
         "custom_metrics": "security_rating",
         "graph": "custom",
         "id": "foo",
index d8cc0a6fd990749c76cd6172cc3359b8b3e195d7..b90b5afa3135d7855729b2c99b601b11502e0e25 100644 (file)
@@ -9,7 +9,6 @@ exports[`renders 1`] = `
     Object {
       "pathname": "/component_measures",
       "query": Object {
-        "branch": undefined,
         "id": "foo",
         "metric": "security_rating",
         "view": "treemap",
index a97849bb45ed24b746843ab6241bedd7bc4ed705..b4afa69ade1b7e49fbb234c75ca6bd222ba8083a 100644 (file)
@@ -9,7 +9,6 @@ exports[`renders 1`] = `
     Object {
       "pathname": "/component_measures",
       "query": Object {
-        "branch": undefined,
         "id": "foo",
         "metric": "security_rating",
       },
index c2120360b568db2797b83e0866fc43caae0e3f35..2401a4dc09b71481af3808fddd93ca7b2c9e05f6 100644 (file)
@@ -17,7 +17,6 @@ exports[`renders 1`] = `
       Object {
         "pathname": "/component_measures",
         "query": Object {
-          "branch": undefined,
           "id": "foo",
           "metric": "alert_status",
         },
@@ -41,7 +40,6 @@ exports[`renders 1`] = `
         Object {
           "pathname": "/component_measures",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
             "metric": "alert_status",
           },
index 149c818b286ce9f1540d98458e384191809b6fb3..5c8287190179fcda81c769f93c3bae08fb25c5f6 100644 (file)
@@ -24,7 +24,6 @@ exports[`renders 1`] = `
             Object {
               "pathname": "/component_measures",
               "query": Object {
-                "branch": undefined,
                 "id": "foo",
                 "metric": "projects",
               },
@@ -55,7 +54,6 @@ exports[`renders 1`] = `
             Object {
               "pathname": "/component_measures",
               "query": Object {
-                "branch": undefined,
                 "id": "foo",
                 "metric": "ncloc",
               },
index 736ab24806037a3c2bc3c6a2b5acb82176635d18..ceb66108e3c53d62f568a60da497a1ac24979bd4 100644 (file)
@@ -53,7 +53,6 @@ exports[`renders 1`] = `
               Object {
                 "pathname": "/dashboard",
                 "query": Object {
-                  "branch": undefined,
                   "id": "foo",
                 },
               }
@@ -141,7 +140,6 @@ exports[`renders 1`] = `
               Object {
                 "pathname": "/dashboard",
                 "query": Object {
-                  "branch": undefined,
                   "id": "barbar",
                 },
               }
@@ -229,7 +227,6 @@ exports[`renders 1`] = `
               Object {
                 "pathname": "/dashboard",
                 "query": Object {
-                  "branch": undefined,
                   "id": "bazbaz",
                 },
               }
index 51d28646604cabe66787a2cb8d54f03a3f0ffa3b..54aa9929ecc45f5c1f968a92f7738f69d36a42be 100644 (file)
@@ -26,7 +26,7 @@ import { getAllTimeMachineData } from '../../../api/time-machine';
 import { getAllMetrics } from '../../../api/metrics';
 import * as api from '../../../api/projectActivity';
 import * as actions from '../actions';
-import { getBranchName } from '../../../helpers/branches';
+import { getBranchLikeQuery } from '../../../helpers/branches';
 import { parseDate } from '../../../helpers/dates';
 import { getCustomGraph, getGraph } from '../../../helpers/storage';
 import {
@@ -51,7 +51,7 @@ type Component = {
 };
 
 type Props = {
-  branch?: {},
+  branchLike?: { id?: string; name: string },
   location: { pathname: string, query: RawQuery },
   component: Component
 };
@@ -103,7 +103,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
         pathname: this.props.location.pathname,
         query: {
           ...serializeUrlQuery(newQuery),
-          branch: getBranchName(this.props.branch)
+          ...getBranchLikeQuery(this.props.branchLike)
         }
       });
     } else {
@@ -169,12 +169,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
       [string]: string
     } */
   ) => {
-    const parameters = {
-      project,
-      p,
-      ps,
-      branch: getBranchName(this.props.branch)
-    };
+    const parameters = { project, p, ps, ...getBranchLikeQuery(this.props.branchLike) };
     return api
       .getProjectActivity({ ...additional, ...parameters })
       .then(({ analyses, paging }) => ({
@@ -187,8 +182,10 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
     if (metrics.length <= 0) {
       return Promise.resolve([]);
     }
-    return getAllTimeMachineData(this.props.component.key, metrics, {
-      branch: getBranchName(this.props.branch)
+    return getAllTimeMachineData({
+      component: this.props.component.key,
+      metrics: metrics.join(),
+      ...getBranchLikeQuery(this.props.branchLike)
     }).then(
       ({ measures }) =>
         measures.map(measure => ({
@@ -300,7 +297,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
       pathname: this.props.location.pathname,
       query: {
         ...query,
-        branch: getBranchName(this.props.branch),
+        ...getBranchLikeQuery(this.props.branchLike),
         id: this.props.component.key
       }
     });
index e4c886668fd91dc7d9d880c888823904f8f79cba..24dac728e91d43ae6256537d5f7e446138283220 100644 (file)
@@ -22,14 +22,19 @@ import { FormattedMessage } from 'react-intl';
 import { Link } from 'react-router';
 import BranchRow from './BranchRow';
 import LongBranchesPattern from './LongBranchesPattern';
-import { Branch } from '../../../app/types';
-import { sortBranchesAsTree } from '../../../helpers/branches';
+import { BranchLike } from '../../../app/types';
+import {
+  sortBranchesAsTree,
+  getBranchLikeKey,
+  isShortLivingBranch,
+  isPullRequest
+} from '../../../helpers/branches';
 import { translate } from '../../../helpers/l10n';
 import { getValues } from '../../../api/settings';
 import { formatMeasure } from '../../../helpers/measures';
 
 interface Props {
-  branches: Branch[];
+  branchLikes: BranchLike[];
   canAdmin?: boolean;
   component: { key: string };
   onBranchesChange: () => void;
@@ -57,7 +62,7 @@ export default class App extends React.PureComponent<Props, State> {
 
   fetchPurgeSetting() {
     this.setState({ loading: true });
-    getValues(BRANCH_LIFETIME_SETTING).then(
+    getValues({ keys: BRANCH_LIFETIME_SETTING }).then(
       settings => {
         if (this.mounted) {
           this.setState({
@@ -78,26 +83,29 @@ export default class App extends React.PureComponent<Props, State> {
       return null;
     }
 
-    const messageKey = this.props.canAdmin
-      ? 'project_branches.page.life_time.admin'
-      : 'project_branches.page.life_time';
-
     return (
       <p className="page-description">
         <FormattedMessage
-          defaultMessage={translate(messageKey)}
-          id={messageKey}
-          values={{
-            days: formatMeasure(this.state.branchLifeTime, 'INT'),
-            settings: <Link to="/admin/settings">{translate('settings.page')}</Link>
-          }}
+          defaultMessage={translate('project_branches.page.life_time')}
+          id="project_branches.page.life_time"
+          values={{ days: formatMeasure(this.state.branchLifeTime, 'INT') }}
         />
+        {this.props.canAdmin && (
+          <>
+            <br />
+            <FormattedMessage
+              defaultMessage={translate('project_branches.page.life_time.admin')}
+              id="project_branches.page.life_time.admin"
+              values={{ settings: <Link to="/admin/settings">{translate('settings.page')}</Link> }}
+            />
+          </>
+        )}
       </p>
     );
   }
 
   render() {
-    const { branches, component, onBranchesChange } = this.props;
+    const { branchLikes, component, onBranchesChange } = this.props;
 
     if (this.state.loading) {
       return (
@@ -132,11 +140,15 @@ export default class App extends React.PureComponent<Props, State> {
               </tr>
             </thead>
             <tbody>
-              {sortBranchesAsTree(branches).map(branch => (
+              {sortBranchesAsTree(branchLikes).map(branchLike => (
                 <BranchRow
-                  branch={branch}
+                  branchLike={branchLike}
                   component={component.key}
-                  key={branch.name}
+                  isOrphan={
+                    (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) &&
+                    branchLike.isOrphan
+                  }
+                  key={getBranchLikeKey(branchLike)}
                   onChange={onBranchesChange}
                 />
               ))}
index 723998c70493b1e98d16905cfcc9453dd97a8d5d..0d5e440a37216ddca91d227264c1a98b85cad07d 100644 (file)
@@ -22,10 +22,16 @@ import * as classNames from 'classnames';
 import DeleteBranchModal from './DeleteBranchModal';
 import LeakPeriodForm from './LeakPeriodForm';
 import RenameBranchModal from './RenameBranchModal';
-import { Branch } from '../../../app/types';
+import { BranchLike } from '../../../app/types';
 import BranchStatus from '../../../components/common/BranchStatus';
 import BranchIcon from '../../../components/icons-components/BranchIcon';
-import { isShortLivingBranch, isLongLivingBranch } from '../../../helpers/branches';
+import {
+  isShortLivingBranch,
+  isLongLivingBranch,
+  isMainBranch,
+  getBranchLikeDisplayName,
+  isPullRequest
+} from '../../../helpers/branches';
 import { translate } from '../../../helpers/l10n';
 import DateFromNow from '../../../components/intl/DateFromNow';
 import ActionsDropdown, {
@@ -34,8 +40,9 @@ import ActionsDropdown, {
 } from '../../../components/controls/ActionsDropdown';
 
 interface Props {
-  branch: Branch;
+  branchLike: BranchLike;
   component: string;
+  isOrphan?: boolean;
   onChange: () => void;
 }
 
@@ -91,19 +98,19 @@ export default class BranchRow extends React.PureComponent<Props, State> {
   };
 
   renderActions() {
-    const { branch, component } = this.props;
+    const { branchLike, component } = this.props;
     return (
       <td className="thin nowrap text-right">
         <ActionsDropdown className="ig-spacer-left">
-          {isLongLivingBranch(branch) && (
+          {isLongLivingBranch(branchLike) && (
             <ActionsDropdownItem
               className="js-change-leak-period"
               onClick={this.handleChangeLeakClick}>
               {translate('branches.set_leak_period')}
             </ActionsDropdownItem>
           )}
-          {isLongLivingBranch(branch) && !branch.isMain && <ActionsDropdownDivider />}
-          {branch.isMain ? (
+          {isLongLivingBranch(branchLike) && <ActionsDropdownDivider />}
+          {isMainBranch(branchLike) ? (
             <ActionsDropdownItem className="js-rename" onClick={this.handleRenameClick}>
               {translate('branches.rename')}
             </ActionsDropdownItem>
@@ -112,62 +119,65 @@ export default class BranchRow extends React.PureComponent<Props, State> {
               className="js-delete"
               destructive={true}
               onClick={this.handleDeleteClick}>
-              {translate('branches.delete')}
+              {translate(
+                isPullRequest(branchLike) ? 'branches.pull_request.delete' : 'branches.delete'
+              )}
             </ActionsDropdownItem>
           )}
         </ActionsDropdown>
 
         {this.state.deleting && (
           <DeleteBranchModal
-            branch={branch}
+            branchLike={branchLike}
             component={component}
             onClose={this.handleDeletingStop}
             onDelete={this.handleChange}
           />
         )}
 
-        {this.state.renaming && (
-          <RenameBranchModal
-            branch={branch}
-            component={component}
-            onClose={this.handleRenamingStop}
-            onRename={this.handleChange}
-          />
-        )}
+        {this.state.renaming &&
+          isMainBranch(branchLike) && (
+            <RenameBranchModal
+              branch={branchLike}
+              component={component}
+              onClose={this.handleRenamingStop}
+              onRename={this.handleChange}
+            />
+          )}
 
-        {this.state.changingLeak && (
-          <LeakPeriodForm
-            branch={branch.name}
-            onClose={this.handleChangingLeakStop}
-            project={component}
-          />
-        )}
+        {this.state.changingLeak &&
+          isLongLivingBranch(branchLike) && (
+            <LeakPeriodForm
+              branch={branchLike.name}
+              onClose={this.handleChangingLeakStop}
+              project={component}
+            />
+          )}
       </td>
     );
   }
 
   render() {
-    const { branch } = this.props;
+    const { branchLike, isOrphan } = this.props;
+    const indented = (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) && !isOrphan;
 
     return (
       <tr>
         <td>
           <BranchIcon
-            branch={branch}
-            className={classNames('little-spacer-right', {
-              'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan
-            })}
+            branchLike={branchLike}
+            className={classNames('little-spacer-right', { 'big-spacer-left': indented })}
           />
-          {branch.name}
-          {branch.isMain && (
+          {getBranchLikeDisplayName(branchLike)}
+          {isMainBranch(branchLike) && (
             <div className="outline-badge spacer-left">{translate('branches.main_branch')}</div>
           )}
         </td>
         <td className="thin nowrap text-right">
-          <BranchStatus branch={branch} />
+          <BranchStatus branchLike={branchLike} />
         </td>
         <td className="thin nowrap text-right">
-          {branch.analysisDate && <DateFromNow date={branch.analysisDate} />}
+          {branchLike.analysisDate && <DateFromNow date={branchLike.analysisDate} />}
         </td>
         {this.renderActions()}
       </tr>
index 127b3781189348957d86ea7ae2dab1da520e2830..c98bb190f65f11dbc9b7f0c06c130540f371f342 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { deleteBranch } from '../../../api/branches';
-import { Branch } from '../../../app/types';
+import { deleteBranch, deletePullRequest } from '../../../api/branches';
+import { BranchLike } from '../../../app/types';
 import Modal from '../../../components/controls/Modal';
 import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { isPullRequest, getBranchLikeDisplayName } from '../../../helpers/branches';
 
 interface Props {
-  branch: Branch;
+  branchLike: BranchLike;
   component: string;
   onClose: () => void;
   onDelete: () => void;
@@ -50,7 +51,16 @@ export default class DeleteBranchModal extends React.PureComponent<Props, State>
   handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
     event.preventDefault();
     this.setState({ loading: true });
-    deleteBranch(this.props.component, this.props.branch.name).then(
+    const request = isPullRequest(this.props.branchLike)
+      ? deletePullRequest({
+          project: this.props.component,
+          pullRequest: this.props.branchLike.key
+        })
+      : deleteBranch({
+          branch: this.props.branchLike.name,
+          project: this.props.component
+        });
+    request.then(
       () => {
         if (this.mounted) {
           this.setState({ loading: false });
@@ -66,8 +76,10 @@ export default class DeleteBranchModal extends React.PureComponent<Props, State>
   };
 
   render() {
-    const { branch } = this.props;
-    const header = translate('branches.delete');
+    const { branchLike } = this.props;
+    const header = translate(
+      isPullRequest(branchLike) ? 'branches.pull_request.delete' : 'branches.delete'
+    );
 
     return (
       <Modal contentLabel={header} onRequestClose={this.props.onClose}>
@@ -76,7 +88,12 @@ export default class DeleteBranchModal extends React.PureComponent<Props, State>
         </header>
         <form onSubmit={this.handleSubmit}>
           <div className="modal-body">
-            {translateWithParameters('branches.delete.are_you_sure', branch.name)}
+            {translateWithParameters(
+              isPullRequest(branchLike)
+                ? 'branches.pull_request.delete.are_you_sure'
+                : 'branches.delete.are_you_sure',
+              getBranchLikeDisplayName(branchLike)
+            )}
           </div>
           <footer className="modal-foot">
             {this.state.loading && <i className="spinner spacer-right" />}
index 71d4deaa53f0a5778e8143b74994e9a52b2072a0..3bb22c48aed6a88a1389ca20c33f62fa7b50f655 100644 (file)
@@ -53,7 +53,7 @@ export default class LeakPeriodForm extends React.PureComponent<Props, State> {
 
   fetchSetting() {
     this.setState({ loading: true });
-    getValues(LEAK_PERIOD, this.props.project, this.props.branch).then(
+    getValues({ keys: LEAK_PERIOD, component: this.props.project, branch: this.props.branch }).then(
       settings => {
         if (this.mounted) {
           this.setState({ loading: false, setting: settings[0] });
index e6d8b31d0f23328af0aad431348d4dee046f19e4..61c9e133c4c47ecac76ee63feabec41dd0a0037d 100644 (file)
@@ -48,7 +48,7 @@ export default class LongBranchesPattern extends React.PureComponent<Props, Stat
   }
 
   fetchSetting() {
-    return getValues(LONG_BRANCH_PATTERN, this.props.project).then(
+    return getValues({ keys: LONG_BRANCH_PATTERN, component: this.props.project }).then(
       settings => {
         if (this.mounted) {
           this.setState({ setting: settings[0] });
index bc78f0395cbc0b3f320990944ee6e5fa4301d0f0..2e8ee656d441f343d229d714cafcf09aff49e094 100644 (file)
  */
 import * as React from 'react';
 import { renameBranch } from '../../../api/branches';
-import { Branch } from '../../../app/types';
+import { MainBranch } from '../../../app/types';
 import Modal from '../../../components/controls/Modal';
 import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
 import { translate } from '../../../helpers/l10n';
 
 interface Props {
-  branch: Branch;
+  branch: MainBranch;
   component: string;
   onClose: () => void;
   onRename: () => void;
index 6c3b737dd4cdfafb97a645fe70bb3dc7f0cb2907..45533ab1da6368b9a557d5a1adaf4a38db01bbcb 100644 (file)
@@ -51,6 +51,12 @@ export default class SettingForm extends React.PureComponent<Props, State> {
     this.mounted = false;
   }
 
+  stopLoading = () => {
+    if (this.mounted) {
+      this.setState({ submitting: false });
+    }
+  };
+
   handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
     event.preventDefault();
 
@@ -65,11 +71,7 @@ export default class SettingForm extends React.PureComponent<Props, State> {
       component: this.props.project,
       key: this.props.setting.key,
       value
-    }).then(this.props.onChange, () => {
-      if (this.mounted) {
-        this.setState({ submitting: false });
-      }
-    });
+    }).then(this.props.onChange, this.stopLoading);
   };
 
   handleValueChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
@@ -78,14 +80,11 @@ export default class SettingForm extends React.PureComponent<Props, State> {
 
   handleResetClick = () => {
     this.setState({ submitting: true });
-    resetSettingValue(this.props.setting.key, this.props.project, this.props.branch).then(
-      this.props.onChange,
-      () => {
-        if (this.mounted) {
-          this.setState({ submitting: false });
-        }
-      }
-    );
+    resetSettingValue({
+      keys: this.props.setting.key,
+      component: this.props.project,
+      branch: this.props.branch
+    }).then(this.props.onChange, this.stopLoading);
   };
 
   render() {
index e22b327bbc1582002a1c3c7560be7ac92e4d8806..ee90b112e98ea612e94dcce17d52abb8e0575b53 100644 (file)
@@ -25,7 +25,13 @@ jest.mock('../../../../api/settings', () => ({
 import * as React from 'react';
 import { mount, shallow } from 'enzyme';
 import App from '../App';
-import { BranchType, MainBranch, LongLivingBranch, ShortLivingBranch } from '../../../../app/types';
+import {
+  BranchType,
+  LongLivingBranch,
+  ShortLivingBranch,
+  MainBranch,
+  PullRequest
+} from '../../../../app/types';
 
 const getValues = require('../../../../api/settings').getValues as jest.Mock<any>;
 
@@ -34,19 +40,27 @@ beforeEach(() => {
 });
 
 it('renders sorted list of branches', () => {
-  const branches: [MainBranch, LongLivingBranch, ShortLivingBranch] = [
+  const branchLikes: [MainBranch, LongLivingBranch, ShortLivingBranch, PullRequest] = [
     { isMain: true, name: 'master' },
     { isMain: false, name: 'branch-1.0', type: BranchType.LONG },
-    { isMain: false, name: 'branch-1.0', mergeBranch: 'master', type: BranchType.SHORT }
+    { isMain: false, mergeBranch: 'master', name: 'feature', type: BranchType.SHORT },
+    { base: 'master', branch: 'feature', key: '1234', title: 'Feature PR' }
   ];
   const wrapper = shallow(
-    <App branches={branches} component={{ key: 'foo' }} onBranchesChange={jest.fn()} />
+    <App
+      branchLikes={branchLikes}
+      canAdmin={true}
+      component={{ key: 'foo' }}
+      onBranchesChange={jest.fn()}
+    />
   );
   wrapper.setState({ branchLifeTime: '100', loading: false });
   expect(wrapper).toMatchSnapshot();
 });
 
 it('fetches branch life time setting on mount', () => {
-  mount(<App branches={[]} component={{ key: 'foo' }} onBranchesChange={jest.fn()} />);
-  expect(getValues).toBeCalledWith('sonar.dbcleaner.daysBeforeDeletingInactiveShortLivingBranches');
+  mount(<App branchLikes={[]} component={{ key: 'foo' }} onBranchesChange={jest.fn()} />);
+  expect(getValues).toBeCalledWith({
+    keys: 'sonar.dbcleaner.daysBeforeDeletingInactiveShortLivingBranches'
+  });
 });
index 8d7532033ccb8b3cddf36a6cd1c74d49e885a192..eac799c7f6fe79d51ca80d181736cb940a008e5d 100644 (file)
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import BranchRow from '../BranchRow';
-import { MainBranch, ShortLivingBranch, BranchType } from '../../../../app/types';
+import {
+  MainBranch,
+  ShortLivingBranch,
+  BranchType,
+  PullRequest,
+  BranchLike
+} from '../../../../app/types';
 import { click } from '../../../../helpers/testUtils';
 
 const mainBranch: MainBranch = { isMain: true, name: 'master' };
@@ -33,6 +39,13 @@ const shortBranch: ShortLivingBranch = {
   type: BranchType.SHORT
 };
 
+const pullRequest: PullRequest = {
+  base: 'master',
+  branch: 'feature',
+  key: '1234',
+  title: 'Feature PR'
+};
+
 it('renders main branch', () => {
   expect(shallowRender(mainBranch)).toMatchSnapshot();
 });
@@ -41,6 +54,10 @@ it('renders short-living branch', () => {
   expect(shallowRender(shortBranch)).toMatchSnapshot();
 });
 
+it('renders pull request', () => {
+  expect(shallowRender(pullRequest)).toMatchSnapshot();
+});
+
 it('renames main branch', () => {
   const onChange = jest.fn();
   const wrapper = shallowRender(mainBranch, onChange);
@@ -59,8 +76,19 @@ it('deletes short-living branch', () => {
   expect(onChange).toBeCalled();
 });
 
-function shallowRender(branch: MainBranch | ShortLivingBranch, onChange: () => void = jest.fn()) {
-  const wrapper = shallow(<BranchRow branch={branch} component="foo" onChange={onChange} />);
+it('deletes pull request', () => {
+  const onChange = jest.fn();
+  const wrapper = shallowRender(pullRequest, onChange);
+
+  click(wrapper.find('.js-delete'));
+  (wrapper.find('DeleteBranchModal').prop('onDelete') as Function)();
+  expect(onChange).toBeCalled();
+});
+
+function shallowRender(branchLike: BranchLike, onChange: () => void = jest.fn()) {
+  const wrapper = shallow(
+    <BranchRow branchLike={branchLike} component="foo" isOrphan={false} onChange={onChange} />
+  );
   (wrapper.instance() as any).mounted = true;
   return wrapper;
 }
index 88fd8dffca7ce73ec21c2a35da750a1fcf846cde..16d7dfaf5cf25b94bb014312cdfd7bde602a9c5a 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 /* eslint-disable import/first */
-jest.mock('../../../../api/branches', () => ({ deleteBranch: jest.fn() }));
+jest.mock('../../../../api/branches', () => ({
+  deleteBranch: jest.fn(),
+  deletePullRequest: jest.fn()
+}));
 
 import * as React from 'react';
 import { shallow, ShallowWrapper } from 'enzyme';
 import DeleteBranchModal from '../DeleteBranchModal';
-import { ShortLivingBranch, BranchType } from '../../../../app/types';
+import { ShortLivingBranch, BranchType, BranchLike, PullRequest } from '../../../../app/types';
 import { submit, doAsync, click, waitAndUpdate } from '../../../../helpers/testUtils';
-import { deleteBranch } from '../../../../api/branches';
+import { deleteBranch, deletePullRequest } from '../../../../api/branches';
+
+const branch: ShortLivingBranch = {
+  isMain: false,
+  name: 'feature',
+  mergeBranch: 'master',
+  type: BranchType.SHORT
+};
 
 beforeEach(() => {
-  (deleteBranch as jest.Mock<any>).mockClear();
+  (deleteBranch as jest.Mock).mockClear();
+  (deletePullRequest as jest.Mock).mockClear();
 });
 
 it('renders', () => {
-  const wrapper = shallowRender();
+  const wrapper = shallowRender(branch);
   expect(wrapper).toMatchSnapshot();
   wrapper.setState({ loading: true });
   expect(wrapper).toMatchSnapshot();
 });
 
 it('deletes branch', async () => {
-  (deleteBranch as jest.Mock<any>).mockImplementation(() => Promise.resolve());
+  (deleteBranch as jest.Mock).mockImplementationOnce(() => Promise.resolve());
+  const onDelete = jest.fn();
+  const wrapper = shallowRender(branch, onDelete);
+
+  submitForm(wrapper);
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().loading).toBe(false);
+  expect(onDelete).toBeCalled();
+  expect(deleteBranch).toBeCalledWith({ branch: 'feature', project: 'foo' });
+});
+
+it('deletes pull request', async () => {
+  (deletePullRequest as jest.Mock).mockImplementationOnce(() => Promise.resolve());
+  const pullRequest: PullRequest = {
+    base: 'master',
+    branch: 'feature',
+    key: '1234',
+    title: 'Feature PR'
+  };
   const onDelete = jest.fn();
-  const wrapper = shallowRender(onDelete);
+  const wrapper = shallowRender(pullRequest, onDelete);
 
   submitForm(wrapper);
 
   await waitAndUpdate(wrapper);
   expect(wrapper.state().loading).toBe(false);
   expect(onDelete).toBeCalled();
-  expect(deleteBranch).toBeCalledWith('foo', 'feature');
+  expect(deletePullRequest).toBeCalledWith({ project: 'foo', pullRequest: '1234' });
 });
 
 it('cancels', () => {
   const onClose = jest.fn();
-  const wrapper = shallowRender(jest.fn(), onClose);
+  const wrapper = shallowRender(branch, jest.fn(), onClose);
 
   click(wrapper.find('ResetButtonLink'));
 
@@ -63,27 +93,30 @@ it('cancels', () => {
 });
 
 it('stops loading on WS error', async () => {
-  (deleteBranch as jest.Mock<any>).mockImplementation(() => Promise.reject(null));
+  (deleteBranch as jest.Mock).mockImplementationOnce(() => Promise.reject(null));
   const onDelete = jest.fn();
-  const wrapper = shallowRender(onDelete);
+  const wrapper = shallowRender(branch, onDelete);
 
   submitForm(wrapper);
 
   await waitAndUpdate(wrapper);
   expect(wrapper.state().loading).toBe(false);
   expect(onDelete).not.toBeCalled();
-  expect(deleteBranch).toBeCalledWith('foo', 'feature');
+  expect(deleteBranch).toBeCalledWith({ branch: 'feature', project: 'foo' });
 });
 
-function shallowRender(onDelete: () => void = jest.fn(), onClose: () => void = jest.fn()) {
-  const branch: ShortLivingBranch = {
-    isMain: false,
-    name: 'feature',
-    mergeBranch: 'master',
-    type: BranchType.SHORT
-  };
+function shallowRender(
+  branchLike: BranchLike,
+  onDelete: () => void = jest.fn(),
+  onClose: () => void = jest.fn()
+) {
   const wrapper = shallow(
-    <DeleteBranchModal branch={branch} component="foo" onClose={onClose} onDelete={onDelete} />
+    <DeleteBranchModal
+      branchLike={branchLike}
+      component="foo"
+      onClose={onClose}
+      onDelete={onDelete}
+    />
   );
   (wrapper.instance() as any).mounted = true;
   return wrapper;
index 0e2a0bf65a7e59648f4d20a28d9522a84f79e035..5c19f9f989f0bbf863522b7f147c8c75588defd5 100644 (file)
@@ -53,7 +53,10 @@ it('opens form', () => {
 
 it('fetches setting value on mount', () => {
   shallow(<LongBranchesPattern project="project" />);
-  expect(getValues).lastCalledWith('sonar.branch.longLivedBranches.regex', 'project');
+  expect(getValues).lastCalledWith({
+    keys: 'sonar.branch.longLivedBranches.regex',
+    component: 'project'
+  });
 });
 
 it('fetches new setting value after change', () => {
index 6a107645fc0d4fc92ccf5dcf21bae72e0c1ca0a3..4989daa203db1a79f6bb1eb974534f66977c5332 100644 (file)
@@ -76,7 +76,11 @@ it('resets value', async () => {
   expect(wrapper).toMatchSnapshot();
 
   click(wrapper.find('Button'));
-  expect(resetSettingValue).toBeCalledWith('foo', 'project', undefined);
+  expect(resetSettingValue).toBeCalledWith({
+    keys: 'foo',
+    component: 'project',
+    branch: undefined
+  });
 
   await new Promise(setImmediate);
   expect(onChange).toBeCalled();
index 1c14e27cdbe005024a1cd4dac45a2a617ed4c9e5..a9d71179c0c2810db559fac66606579ac82477d5 100644 (file)
@@ -29,16 +29,27 @@ exports[`renders sorted list of branches 1`] = `
         values={
           Object {
             "days": "100",
-            "settings": <Link
-              onlyActiveOnIndex={false}
-              style={Object {}}
-              to="/admin/settings"
-            >
-              settings.page
-            </Link>,
           }
         }
       />
+      <React.Fragment>
+        <br />
+        <FormattedMessage
+          defaultMessage="project_branches.page.life_time.admin"
+          id="project_branches.page.life_time.admin"
+          values={
+            Object {
+              "settings": <Link
+                onlyActiveOnIndex={false}
+                style={Object {}}
+                to="/admin/settings"
+              >
+                settings.page
+              </Link>,
+            }
+          }
+        />
+      </React.Fragment>
     </p>
   </header>
   <div
@@ -71,31 +82,45 @@ exports[`renders sorted list of branches 1`] = `
       </thead>
       <tbody>
         <BranchRow
-          branch={
+          branchLike={
             Object {
               "isMain": true,
               "name": "master",
             }
           }
           component="foo"
-          key="master"
+          isOrphan={false}
+          key="branch-master"
+          onChange={[MockFunction]}
+        />
+        <BranchRow
+          branchLike={
+            Object {
+              "base": "master",
+              "branch": "feature",
+              "key": "1234",
+              "title": "Feature PR",
+            }
+          }
+          component="foo"
+          key="pull-request-1234"
           onChange={[MockFunction]}
         />
         <BranchRow
-          branch={
+          branchLike={
             Object {
               "isMain": false,
               "mergeBranch": "master",
-              "name": "branch-1.0",
+              "name": "feature",
               "type": "SHORT",
             }
           }
           component="foo"
-          key="branch-1.0"
+          key="branch-feature"
           onChange={[MockFunction]}
         />
         <BranchRow
-          branch={
+          branchLike={
             Object {
               "isMain": false,
               "name": "branch-1.0",
@@ -103,7 +128,8 @@ exports[`renders sorted list of branches 1`] = `
             }
           }
           component="foo"
-          key="branch-1.0"
+          isOrphan={false}
+          key="branch-branch-1.0"
           onChange={[MockFunction]}
         />
       </tbody>
index 5a28f352855c32a9e7886aea2e80cae39704e7cd..d7e94ca3b2ecc02232107a4a58a2df5b0645a0e1 100644 (file)
@@ -4,7 +4,7 @@ exports[`renders main branch 1`] = `
 <tr>
   <td>
     <BranchIcon
-      branch={
+      branchLike={
         Object {
           "isMain": true,
           "name": "master",
@@ -23,7 +23,7 @@ exports[`renders main branch 1`] = `
     className="thin nowrap text-right"
   >
     <BranchStatus
-      branch={
+      branchLike={
         Object {
           "isMain": true,
           "name": "master",
@@ -51,11 +51,62 @@ exports[`renders main branch 1`] = `
 </tr>
 `;
 
+exports[`renders pull request 1`] = `
+<tr>
+  <td>
+    <BranchIcon
+      branchLike={
+        Object {
+          "base": "master",
+          "branch": "feature",
+          "key": "1234",
+          "title": "Feature PR",
+        }
+      }
+      className="little-spacer-right big-spacer-left"
+    />
+    1234 – Feature PR
+  </td>
+  <td
+    className="thin nowrap text-right"
+  >
+    <BranchStatus
+      branchLike={
+        Object {
+          "base": "master",
+          "branch": "feature",
+          "key": "1234",
+          "title": "Feature PR",
+        }
+      }
+    />
+  </td>
+  <td
+    className="thin nowrap text-right"
+  />
+  <td
+    className="thin nowrap text-right"
+  >
+    <ActionsDropdown
+      className="ig-spacer-left"
+    >
+      <ActionsDropdownItem
+        className="js-delete"
+        destructive={true}
+        onClick={[Function]}
+      >
+        branches.pull_request.delete
+      </ActionsDropdownItem>
+    </ActionsDropdown>
+  </td>
+</tr>
+`;
+
 exports[`renders short-living branch 1`] = `
 <tr>
   <td>
     <BranchIcon
-      branch={
+      branchLike={
         Object {
           "analysisDate": "2017-09-27T00:05:19+0000",
           "isMain": false,
@@ -72,7 +123,7 @@ exports[`renders short-living branch 1`] = `
     className="thin nowrap text-right"
   >
     <BranchStatus
-      branch={
+      branchLike={
         Object {
           "analysisDate": "2017-09-27T00:05:19+0000",
           "isMain": false,
index 505f5276139d09ecf23aea4b65306a73d2da5630..733d95af43f2c595e3cdf1787c0e24a0de084a89 100644 (file)
@@ -18,7 +18,6 @@ exports[`renders 1`] = `
           "link": Object {
             "pathname": "/dashboard",
             "query": Object {
-              "branch": undefined,
               "id": "foo",
             },
           },
index afb4e501021a3791759eaeda6045d3405501ce81..b6e9c87e4446b837644e072b2f9541d05740f72a 100644 (file)
@@ -18,7 +18,6 @@ exports[`renders 1`] = `
           "link": Object {
             "pathname": "/dashboard",
             "query": Object {
-              "branch": undefined,
               "id": "foo",
             },
           },
index b43fd2d96fa6b89f33b22932063dfa4fb533cb23..2c58557ac4fcac23d3ab573267db14cf27736c25 100644 (file)
@@ -57,8 +57,8 @@ export default class ProjectRowActions extends React.PureComponent<Props, State>
     // call `getComponentNavigation` to check if user has the "Administer" permission
     // call `getComponentShow` to check if user has the "Browse" permission
     Promise.all([
-      getComponentNavigation(this.props.project.key),
-      getComponentShow(this.props.project.key)
+      getComponentNavigation({ componentKey: this.props.project.key }),
+      getComponentShow({ component: this.props.project.key })
     ]).then(
       ([navResponse]) => {
         if (this.mounted) {
index deec1e2e7fda72839b6585133813dc2a0aa54d80..b0b662adaaedfb57072b7a80fb86fcb9a42135f6 100644 (file)
@@ -385,7 +385,6 @@ exports[`creates project 4`] = `
                   Object {
                     "pathname": "/dashboard",
                     "query": Object {
-                      "branch": undefined,
                       "id": "name",
                     },
                   }
index 7865fd563139ad7614cb08bb6428f4c885d4029e..dcbf19f63dd2707949aa39240dedac25a4ac6038 100644 (file)
@@ -50,10 +50,10 @@ export const fetchSettings = componentKey => dispatch => {
   );
 };
 
-export const fetchValues = (keys, componentKey) => dispatch =>
-  getValues(keys, componentKey).then(
+export const fetchValues = (keys, component) => dispatch =>
+  getValues({ keys, component }).then(
     settings => {
-      dispatch(receiveValues(settings, componentKey));
+      dispatch(receiveValues(settings, component));
       dispatch(closeAllGlobalMessages());
     },
     () => {}
@@ -73,7 +73,7 @@ export const saveValue = (key, componentKey) => (dispatch, getState) => {
   }
 
   return setSettingValue(definition, value, componentKey)
-    .then(() => getValues(key, componentKey))
+    .then(() => getValues({ keys: key, component: componentKey }))
     .then(values => {
       dispatch(receiveValues(values, componentKey));
       dispatch(cancelChange(key));
@@ -90,8 +90,8 @@ export const saveValue = (key, componentKey) => (dispatch, getState) => {
 export const resetValue = (key, componentKey) => dispatch => {
   dispatch(startLoading(key));
 
-  return resetSettingValue(key, componentKey)
-    .then(() => getValues(key, componentKey))
+  return resetSettingValue({ keys: key, component: componentKey })
+    .then(() => getValues({ keys: key, component: componentKey }))
     .then(values => {
       if (values.length > 0) {
         dispatch(receiveValues(values, componentKey));
index 6e487e2558aef96adaee67272e2163a0e823fdf3..101bc211a1a0de953bc47f052b42c2e0d7a2307c 100644 (file)
@@ -38,14 +38,16 @@ import {
   getSources
 } from '../../api/components';
 import {
+  BranchLike,
+  DuplicatedFile,
   Duplication,
   FlowLocation,
   Issue,
   LinearIssueLocation,
   SourceLine,
-  SourceViewerFile,
-  DuplicatedFile
+  SourceViewerFile
 } from '../../app/types';
+import { isSameBranchLike, getBranchLikeQuery } from '../../helpers/branches';
 import { parseDate } from '../../helpers/dates';
 import { translate } from '../../helpers/l10n';
 import './styles.css';
@@ -54,7 +56,7 @@ import './styles.css';
 
 interface Props {
   aroundLine?: number;
-  branch: string | undefined;
+  branchLike: BranchLike | undefined;
   component: string;
   displayAllIssues?: boolean;
   displayIssueLocationsCount?: boolean;
@@ -63,18 +65,21 @@ interface Props {
   highlightedLine?: number;
   highlightedLocations?: FlowLocation[];
   highlightedLocationMessage?: { index: number; text: string };
-  loadComponent?: (component: string, branch: string | undefined) => Promise<SourceViewerFile>;
+  loadComponent?: (
+    component: string,
+    branchLike: BranchLike | undefined
+  ) => Promise<SourceViewerFile>;
   loadIssues?: (
     component: string,
     from: number,
     to: number,
-    branch: string | undefined
+    branchLike: BranchLike | undefined
   ) => Promise<Issue[]>;
   loadSources?: (
     component: string,
     from: number,
     to: number,
-    branch: string | undefined
+    branchLike: BranchLike | undefined
   ) => Promise<SourceLine[]>;
   onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void;
   onLocationSelect?: (index: number) => void;
@@ -162,7 +167,10 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
   }
 
   componentDidUpdate(prevProps: Props) {
-    if (prevProps.component !== this.props.component || prevProps.branch !== this.props.branch) {
+    if (
+      prevProps.component !== this.props.component ||
+      !isSameBranchLike(prevProps.branchLike, this.props.branchLike)
+    ) {
       this.fetchComponent();
     } else if (
       this.props.aroundLine !== undefined &&
@@ -220,7 +228,7 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
   fetchComponent() {
     this.setState({ loading: true });
     const loadIssues = (component: SourceViewerFile, sources: SourceLine[]) => {
-      this.safeLoadIssues(this.props.component, 1, LINES, this.props.branch).then(
+      this.safeLoadIssues(this.props.component, 1, LINES, this.props.branchLike).then(
         issues => {
           if (this.mounted) {
             const finalSources = sources.slice(0, LINES);
@@ -284,7 +292,7 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
       );
     };
 
-    this.safeLoadComponent(this.props.component, this.props.branch).then(
+    this.safeLoadComponent(this.props.component, this.props.branchLike).then(
       onResolve,
       onFailLoadComponent
     );
@@ -324,7 +332,7 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
       this.props.component,
       firstSourceLine && firstSourceLine.line,
       lastSourceLine && lastSourceLine.line,
-      this.props.branch
+      this.props.branchLike
     ).then(
       issues => {
         if (this.mounted) {
@@ -364,7 +372,7 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
       // request one additional line to define `hasSourcesAfter`
       to++;
 
-      return this.safeLoadSources(this.props.component, from, to, this.props.branch).then(
+      return this.safeLoadSources(this.props.component, from, to, this.props.branchLike).then(
         sources => resolve(sources),
         onFailLoadSources
       );
@@ -382,14 +390,14 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
       this.props.component,
       from,
       firstSourceLine.line - 1,
-      this.props.branch
+      this.props.branchLike
     ).then(
       sources => {
         this.safeLoadIssues(
           this.props.component,
           from,
           firstSourceLine.line - 1,
-          this.props.branch
+          this.props.branchLike
         ).then(
           issues => {
             if (this.mounted) {
@@ -429,9 +437,9 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
     const fromLine = lastSourceLine.line + 1;
     // request one additional line to define `hasSourcesAfter`
     const toLine = lastSourceLine.line + LINES + 1;
-    this.safeLoadSources(this.props.component, fromLine, toLine, this.props.branch).then(
+    this.safeLoadSources(this.props.component, fromLine, toLine, this.props.branchLike).then(
       sources => {
-        this.safeLoadIssues(this.props.component, fromLine, toLine, this.props.branch).then(
+        this.safeLoadIssues(this.props.component, fromLine, toLine, this.props.branchLike).then(
           issues => {
             if (this.mounted) {
               this.setState(prevState => {
@@ -469,7 +477,10 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
   };
 
   loadDuplications = (line: SourceLine) => {
-    getDuplications(this.props.component, this.props.branch).then(
+    getDuplications({
+      key: this.props.component,
+      ...getBranchLikeQuery(this.props.branchLike)
+    }).then(
       r => {
         if (this.mounted) {
           this.setState(() => {
@@ -615,7 +626,7 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
     return (
       <DuplicationPopup
         blocks={blocks}
-        branch={this.props.branch}
+        branchLike={this.props.branchLike}
         duplicatedFiles={duplicatedFiles}
         inRemovedComponent={inRemovedComponent}
         onClose={this.closeLinePopup}
@@ -628,7 +639,7 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
     const hasSourcesBefore = sources.length > 0 && sources[0].line > 1;
     return (
       <SourceViewerCode
-        branch={this.props.branch}
+        branchLike={this.props.branchLike}
         componentKey={this.props.component}
         displayAllIssues={this.props.displayAllIssues}
         displayIssueLocationsCount={this.props.displayIssueLocationsCount}
@@ -706,7 +717,10 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
     return (
       <div className={className} ref={node => (this.node = node)}>
         {this.state.component && (
-          <SourceViewerHeader branch={this.props.branch} sourceViewerFile={this.state.component} />
+          <SourceViewerHeader
+            branchLike={this.props.branchLike}
+            sourceViewerFile={this.state.component}
+          />
         )}
         {sourceRemoved && (
           <div className="alert alert-warning spacer-top">
@@ -719,10 +733,10 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
   }
 }
 
-function defaultLoadComponent(key: string, branch: string | undefined) {
+function defaultLoadComponent(key: string, branchLike: BranchLike | undefined) {
   return Promise.all([
-    getComponentForSourceViewer(key, branch),
-    getComponentData(key, branch)
+    getComponentForSourceViewer({ component: key, ...getBranchLikeQuery(branchLike) }),
+    getComponentData({ component: key, ...getBranchLikeQuery(branchLike) })
   ]).then(([component, data]) => ({
     ...component,
     leakPeriodDate: data.leakPeriodDate && parseDate(data.leakPeriodDate)
@@ -733,7 +747,7 @@ function defaultLoadSources(
   key: string,
   from: number | undefined,
   to: number | undefined,
-  branch: string | undefined
+  branchLike: BranchLike | undefined
 ) {
-  return getSources(key, from, to, branch);
+  return getSources({ key, from, to, ...getBranchLikeQuery(branchLike) });
 }
index 15db6dd09617251c6bdd8891c74400435bbdff47..96bd3512e3962735b3b6e1f9c744c0caa4ce4890 100644 (file)
@@ -21,7 +21,14 @@ 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 {
+  BranchLike,
+  Duplication,
+  FlowLocation,
+  Issue,
+  LinearIssueLocation,
+  SourceLine
+} from '../../app/types';
 import { translate } from '../../helpers/l10n';
 import { Button } from '../ui/buttons';
 
@@ -34,7 +41,7 @@ const ZERO_LINE = {
 };
 
 interface Props {
-  branch: string | undefined;
+  branchLike: BranchLike | undefined;
   componentKey: string;
   displayAllIssues?: boolean;
   displayIssueLocationsCount?: boolean;
@@ -153,7 +160,7 @@ export default class SourceViewerCode extends React.PureComponent<Props> {
 
     return (
       <Line
-        branch={this.props.branch}
+        branchLike={this.props.branchLike}
         componentKey={this.props.componentKey}
         displayAllIssues={this.props.displayAllIssues}
         displayCoverage={displayCoverage}
index a29348b9312b1271333d20958b26a1945d5b1bac..0ba3cd6ec40dc105e327ede8b1acc3afa67062eb 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { stringify } from 'querystring';
 import * as React from 'react';
 import { Link } from 'react-router';
 import MeasuresOverlay from './components/MeasuresOverlay';
-import { SourceViewerFile } from '../../app/types';
+import { SourceViewerFile, BranchLike } from '../../app/types';
 import QualifierIcon from '../shared/QualifierIcon';
 import FavoriteContainer from '../controls/FavoriteContainer';
 import {
   getPathUrlAsString,
-  getProjectUrl,
+  getBranchLikeUrl,
   getComponentIssuesUrl,
   getBaseUrl
 } from '../../helpers/urls';
 import { collapsedDirFromPath, fileFromPath } from '../../helpers/path';
 import { translate } from '../../helpers/l10n';
+import { getBranchLikeQuery } from '../../helpers/branches';
 import { formatMeasure } from '../../helpers/measures';
+import { omitNil } from '../../helpers/request';
 
 interface Props {
-  branch: string | undefined;
+  branchLike: BranchLike | undefined;
   sourceViewerFile: SourceViewerFile;
 }
 
@@ -58,7 +61,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State
     event.preventDefault();
     const { key } = this.props.sourceViewerFile;
     const Workspace = require('../workspace/main').default;
-    Workspace.openComponent({ key, branch: this.props.branch });
+    Workspace.openComponent({ key, branchLike: this.props.branchLike });
   };
 
   render() {
@@ -75,11 +78,10 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State
     } = this.props.sourceViewerFile;
     const isUnitTest = q === 'UTS';
     const workspace = false;
-    let rawSourcesLink =
-      getBaseUrl() + `/api/sources/raw?key=${encodeURIComponent(this.props.sourceViewerFile.key)}`;
-    if (this.props.branch) {
-      rawSourcesLink += `&branch=${encodeURIComponent(this.props.branch)}`;
-    }
+    const rawSourcesLink =
+      getBaseUrl() +
+      '/api/sources/raw?' +
+      stringify(omitNil({ key, ...getBranchLikeQuery(this.props.branchLike) }));
 
     // TODO favorite
     return (
@@ -89,7 +91,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State
             <div className="component-name-parent">
               <a
                 className="link-with-icon"
-                href={getPathUrlAsString(getProjectUrl(project, this.props.branch))}>
+                href={getPathUrlAsString(getBranchLikeUrl(project, this.props.branchLike))}>
                 <QualifierIcon qualifier="TRK" /> <span>{projectName}</span>
               </a>
             </div>
@@ -98,7 +100,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State
               <div className="component-name-parent">
                 <a
                   className="link-with-icon"
-                  href={getPathUrlAsString(getProjectUrl(subProject, this.props.branch))}>
+                  href={getPathUrlAsString(getBranchLikeUrl(subProject, this.props.branchLike))}>
                   <QualifierIcon qualifier="BRC" /> <span>{subProjectName}</span>
                 </a>
               </div>
@@ -127,7 +129,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State
               </a>
               {this.state.measuresOverlay && (
                 <MeasuresOverlay
-                  branch={this.props.branch}
+                  branchLike={this.props.branchLike}
                   onClose={this.handleMeasuresOverlayClose}
                   sourceViewerFile={this.props.sourceViewerFile}
                 />
@@ -138,7 +140,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State
                 className="js-new-window"
                 href={getPathUrlAsString({
                   pathname: '/component',
-                  query: { branch: this.props.branch, id: this.props.sourceViewerFile.key }
+                  query: { id: key, ...getBranchLikeQuery(this.props.branchLike) }
                 })}
                 target="_blank">
                 {translate('component_viewer.new_window')}
@@ -188,7 +190,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State
                 to={getComponentIssuesUrl(project, {
                   resolved: 'false',
                   fileUuids: uuid,
-                  branch: this.props.branch
+                  ...getBranchLikeQuery(this.props.branchLike)
                 })}>
                 {measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0}
               </Link>
index 2edff7259674397a30bccd48c0c9346bacbea9bb..ef06788347e367521808ea8604469e8266faa19c 100644 (file)
 import * as React from 'react';
 import { groupBy } from 'lodash';
 import { getTests } from '../../../api/components';
-import { SourceLine, TestCase } from '../../../app/types';
+import { BranchLike, SourceLine, TestCase } from '../../../app/types';
 import BubblePopup from '../../common/BubblePopup';
 import TestStatusIcon from '../../shared/TestStatusIcon';
+import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches';
 import { translate } from '../../../helpers/l10n';
 import { collapsePath } from '../../../helpers/path';
 
 interface Props {
-  branch: string | undefined;
+  branchLike: BranchLike | undefined;
   componentKey: string;
   line: SourceLine;
   onClose: () => void;
@@ -49,9 +50,8 @@ export default class CoveragePopup extends React.PureComponent<Props, State> {
   }
 
   componentDidUpdate(prevProps: Props) {
-    // TODO use branchLike
     if (
-      prevProps.branch !== this.props.branch ||
+      !isSameBranchLike(prevProps.branchLike, this.props.branchLike) ||
       prevProps.componentKey !== this.props.componentKey ||
       prevProps.line.line !== this.props.line.line
     ) {
@@ -65,7 +65,11 @@ export default class CoveragePopup extends React.PureComponent<Props, State> {
 
   fetchTests = () => {
     this.setState({ loading: true });
-    getTests(this.props.componentKey, this.props.line.line, this.props.branch).then(
+    getTests({
+      sourceFileKey: this.props.componentKey,
+      sourceFileLineNumber: this.props.line.line,
+      ...getBranchLikeQuery(this.props.branchLike)
+    }).then(
       testCases => {
         if (this.mounted) {
           this.setState({ loading: false, testCases });
@@ -84,7 +88,7 @@ export default class CoveragePopup extends React.PureComponent<Props, State> {
     event.currentTarget.blur();
     const { key } = event.currentTarget.dataset;
     const Workspace = require('../../workspace/main').default;
-    Workspace.openComponent({ key, branch: this.props.branch });
+    Workspace.openComponent({ key, branchLike: this.props.branchLike });
     this.props.onClose();
   };
 
index 9c58a3e01ad7c92351a744b653af5c414e737655..d4b75a93b286f51a461abfa2a0008f70dac5cd39 100644 (file)
@@ -20,7 +20,7 @@
 import * as React from 'react';
 import { Link } from 'react-router';
 import { groupBy, sortBy } from 'lodash';
-import { SourceViewerFile, DuplicationBlock, DuplicatedFile } from '../../../app/types';
+import { BranchLike, DuplicatedFile, DuplicationBlock, SourceViewerFile } from '../../../app/types';
 import BubblePopup from '../../common/BubblePopup';
 import QualifierIcon from '../../shared/QualifierIcon';
 import { translate } from '../../../helpers/l10n';
@@ -29,8 +29,7 @@ import { getProjectUrl } from '../../../helpers/urls';
 
 interface Props {
   blocks: DuplicationBlock[];
-  // TODO use branchLike
-  branch: string | undefined;
+  branchLike: BranchLike | undefined;
   duplicatedFiles?: { [ref: string]: DuplicatedFile };
   inRemovedComponent: boolean;
   onClose: () => void;
@@ -51,7 +50,7 @@ export default class DuplicationPopup extends React.PureComponent<Props> {
     event.currentTarget.blur();
     const Workspace = require('../../workspace/main').default;
     const { key, line } = event.currentTarget.dataset;
-    Workspace.openComponent({ key, line, branch: this.props.branch });
+    Workspace.openComponent({ key, line, branchLike: this.props.branchLike });
     this.props.onClose();
   };
 
index a29255557dddb9d0e4846777b9f3387d3a7a7972..c6fef8178b315a4e6bf254fb011e95993589fd59 100644 (file)
@@ -27,10 +27,10 @@ import LineDuplications from './LineDuplications';
 import LineDuplicationBlock from './LineDuplicationBlock';
 import LineIssuesIndicator from './LineIssuesIndicator';
 import LineCode from './LineCode';
-import { Issue, LinearIssueLocation, SourceLine } from '../../../app/types';
+import { BranchLike, Issue, LinearIssueLocation, SourceLine } from '../../../app/types';
 
 interface Props {
-  branch: string | undefined;
+  branchLike: BranchLike | undefined;
   componentKey: string;
   displayAllIssues?: boolean;
   displayCoverage: boolean;
@@ -121,7 +121,7 @@ export default class Line extends React.PureComponent<Props> {
     return (
       <tr className={className} data-line-number={line.line}>
         <LineNumber
-          branch={this.props.branch}
+          branchLike={this.props.branchLike}
           componentKey={this.props.componentKey}
           line={line}
           onPopupToggle={this.props.onLinePopupToggle}
@@ -137,7 +137,7 @@ export default class Line extends React.PureComponent<Props> {
 
         {this.props.displayCoverage && (
           <LineCoverage
-            branch={this.props.branch}
+            branchLike={this.props.branchLike}
             componentKey={this.props.componentKey}
             line={line}
             onPopupToggle={this.props.onLinePopupToggle}
@@ -171,7 +171,7 @@ export default class Line extends React.PureComponent<Props> {
           )}
 
         <LineCode
-          branch={this.props.branch}
+          branchLike={this.props.branchLike}
           displayIssueLocationsCount={this.props.displayIssueLocationsCount}
           displayIssueLocationsLink={this.props.displayIssueLocationsLink}
           displayLocationMarkers={this.props.displayLocationMarkers}
index c91f58c43c2493124d38ce7e8d28a0d5164da607..9becbdeb876498db40de600e8fa47d6946a48a5a 100644 (file)
@@ -20,7 +20,7 @@
 import * as React from 'react';
 import * as classNames from 'classnames';
 import LineIssuesList from './LineIssuesList';
-import { Issue, LinearIssueLocation, SourceLine } from '../../../app/types';
+import { BranchLike, Issue, LinearIssueLocation, SourceLine } from '../../../app/types';
 import LocationIndex from '../../common/LocationIndex';
 import LocationMessage from '../../common/LocationMessage';
 import {
@@ -31,7 +31,7 @@ import {
 } from '../helpers/highlight';
 
 interface Props {
-  branch: string | undefined;
+  branchLike: BranchLike | undefined;
   displayIssueLocationsCount?: boolean;
   displayIssueLocationsLink?: boolean;
   displayLocationMarkers?: boolean;
@@ -231,7 +231,7 @@ export default class LineCode extends React.PureComponent<Props, State> {
         {showIssues &&
           issues.length > 0 && (
             <LineIssuesList
-              branch={this.props.branch}
+              branchLike={this.props.branchLike}
               displayIssueLocationsCount={this.props.displayIssueLocationsCount}
               displayIssueLocationsLink={this.props.displayIssueLocationsLink}
               issuePopup={this.props.issuePopup}
index c6b9efb895d513d8520675406c87179667a76776..17d98a03ecaa571b522dbc1a8413ac50f7855be8 100644 (file)
  */
 import * as React from 'react';
 import CoveragePopup from './CoveragePopup';
-import { SourceLine } from '../../../app/types';
+import { BranchLike, SourceLine } from '../../../app/types';
 import Tooltip from '../../controls/Tooltip';
 import { translate } from '../../../helpers/l10n';
 import BubblePopupHelper from '../../common/BubblePopupHelper';
 
 interface Props {
-  branch: string | undefined;
+  branchLike: BranchLike | undefined;
   componentKey: string;
   line: SourceLine;
   onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void;
@@ -49,7 +49,7 @@ export default class LineCoverage extends React.PureComponent<Props> {
   };
 
   render() {
-    const { branch, componentKey, line, popupOpen } = this.props;
+    const { branchLike, componentKey, line, popupOpen } = this.props;
 
     const className =
       'source-meta source-line-coverage' +
@@ -80,7 +80,7 @@ export default class LineCoverage extends React.PureComponent<Props> {
             isOpen={popupOpen}
             popup={
               <CoveragePopup
-                branch={branch}
+                branchLike={branchLike}
                 componentKey={componentKey}
                 line={line}
                 onClose={this.closePopup}
index 231cc6391716699c2e84cbb7d72b9c43e68e9cf5..6e8cfca77326ff011586a27c774105f6e5781ded 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { Issue as IssueType } from '../../../app/types';
+import { BranchLike, Issue as IssueType } from '../../../app/types';
 import Issue from '../../issue/Issue';
 
 interface Props {
-  branch: string | undefined;
+  branchLike: BranchLike | undefined;
   displayIssueLocationsCount?: boolean;
   displayIssueLocationsLink?: boolean;
   issuePopup: { issue: string; name: string } | undefined;
@@ -40,7 +40,7 @@ export default function LineIssuesList(props: Props) {
     <div className="issue-list">
       {props.issues.map(issue => (
         <Issue
-          branch={props.branch}
+          branchLike={props.branchLike}
           displayLocationsCount={props.displayIssueLocationsCount}
           displayLocationsLink={props.displayIssueLocationsLink}
           issue={issue}
index f6126f5e7ec71ca6aad40e9b164ca26590f8a207..6d1f4940141c3f0cbc1e6ad2821930dae9d87d2d 100644 (file)
  */
 import * as React from 'react';
 import LineOptionsPopup from './LineOptionsPopup';
-import { SourceLine } from '../../../app/types';
+import { BranchLike, SourceLine } from '../../../app/types';
 import BubblePopupHelper from '../../common/BubblePopupHelper';
 
 interface Props {
-  // TODO use branchLike
-  branch: string | undefined;
+  branchLike: BranchLike | undefined;
   componentKey: string;
   line: SourceLine;
   onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void;
@@ -44,7 +43,7 @@ export default class LineNumber extends React.PureComponent<Props> {
   };
 
   render() {
-    const { branch, componentKey, line, popupOpen } = this.props;
+    const { branchLike, componentKey, line, popupOpen } = this.props;
     const { line: lineNumber } = line;
     const hasLineNumber = !!lineNumber;
     return hasLineNumber ? (
@@ -57,7 +56,9 @@ export default class LineNumber extends React.PureComponent<Props> {
         tabIndex={0}>
         <BubblePopupHelper
           isOpen={popupOpen}
-          popup={<LineOptionsPopup branch={branch} componentKey={componentKey} line={line} />}
+          popup={
+            <LineOptionsPopup branchLike={branchLike} componentKey={componentKey} line={line} />
+          }
           position="bottomright"
           togglePopup={this.handleTogglePopup}
         />
index db4634b2c28791218aff643191b9a459e162fb0e..340304bb280557f423582845d3172fb9336e10a7 100644 (file)
  */
 import * as React from 'react';
 import { Link } from 'react-router';
-import { SourceLine } from '../../../app/types';
+import { BranchLike, SourceLine } from '../../../app/types';
 import BubblePopup from '../../common/BubblePopup';
 import { translate } from '../../../helpers/l10n';
+import { getBranchLikeQuery } from '../../../helpers/branches';
 
 interface Props {
-  // TODO use branchLike
-  branch: string | undefined;
+  branchLike: BranchLike | undefined;
   componentKey: string;
   line: SourceLine;
   popupPosition?: any;
 }
 
-export default function LineOptionsPopup({ branch, componentKey, line, popupPosition }: Props) {
+export default function LineOptionsPopup({ branchLike, componentKey, line, popupPosition }: Props) {
   const permalink = {
     pathname: '/component',
-    query: { branch, id: componentKey, line: line.line }
+    query: { id: componentKey, line: line.line, ...getBranchLikeQuery(branchLike) }
   };
   return (
     <BubblePopup customClass="source-viewer-bubble-popup" position={popupPosition}>
index 532e14ff7580aa2b115ea3bc404d3b242c448b14..1180729829958cdfa83421b18cbbd7c494b38aff 100644 (file)
@@ -26,7 +26,7 @@ import { Button } from '../../../components/ui/buttons';
 import { getFacets } from '../../../api/issues';
 import { getMeasures } from '../../../api/measures';
 import { getAllMetrics } from '../../../api/metrics';
-import { FacetValue, SourceViewerFile } from '../../../app/types';
+import { FacetValue, SourceViewerFile, BranchLike } from '../../../app/types';
 import Modal from '../../controls/Modal';
 import Measure from '../../measure/Measure';
 import QualifierIcon from '../../shared/QualifierIcon';
@@ -42,10 +42,11 @@ import {
   getDisplayMetrics,
   enhanceMeasuresWithMetrics
 } from '../../../helpers/measures';
-import { getProjectUrl } from '../../../helpers/urls';
+import { getBranchLikeUrl } from '../../../helpers/urls';
+import { getBranchLikeQuery } from '../../../helpers/branches';
 
 interface Props {
-  branch: string | undefined;
+  branchLike: BranchLike | undefined;
   onClose: () => void;
   sourceViewerFile: SourceViewerFile;
 }
@@ -96,23 +97,25 @@ export default class MeasuresOverlay extends React.PureComponent<Props, State> {
       const metricKeys = getDisplayMetrics(metrics).map(metric => metric.key);
 
       // eslint-disable-next-line promise/no-nesting
-      return getMeasures(this.props.sourceViewerFile.key, metricKeys, this.props.branch).then(
-        measures => {
-          const withMetrics = enhanceMeasuresWithMetrics(measures, metrics).filter(
-            measure => measure.metric
-          );
-          return keyBy(withMetrics, measure => measure.metric.key);
-        }
-      );
+      return getMeasures({
+        componentKey: this.props.sourceViewerFile.key,
+        metricKeys: metricKeys.join(),
+        ...getBranchLikeQuery(this.props.branchLike)
+      }).then(measures => {
+        const withMetrics = enhanceMeasuresWithMetrics(measures, metrics).filter(
+          measure => measure.metric
+        );
+        return keyBy(withMetrics, measure => measure.metric.key);
+      });
     });
   };
 
   fetchIssues = () => {
     return getFacets(
       {
-        branch: this.props.branch,
         componentKeys: this.props.sourceViewerFile.key,
-        resolved: 'false'
+        resolved: 'false',
+        ...getBranchLikeQuery(this.props.branchLike)
       },
       ['types', 'severities', 'tags']
     ).then(({ facets }) => {
@@ -383,7 +386,7 @@ export default class MeasuresOverlay extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { branch, sourceViewerFile } = this.props;
+    const { branchLike, sourceViewerFile } = this.props;
     const { loading } = this.state;
 
     return (
@@ -392,14 +395,14 @@ export default class MeasuresOverlay extends React.PureComponent<Props, State> {
           <div className="source-viewer-header-component source-viewer-measures-component">
             <div className="source-viewer-header-component-project">
               <QualifierIcon className="little-spacer-right" qualifier="TRK" />
-              <Link to={getProjectUrl(sourceViewerFile.project, branch)}>
+              <Link to={getBranchLikeUrl(sourceViewerFile.project, branchLike)}>
                 {sourceViewerFile.projectName}
               </Link>
 
               {sourceViewerFile.subProject && (
                 <>
                   <QualifierIcon className="big-spacer-left little-spacer-right" qualifier="BRC" />
-                  <Link to={getProjectUrl(sourceViewerFile.subProject, branch)}>
+                  <Link to={getBranchLikeUrl(sourceViewerFile.subProject, branchLike)}>
                     {sourceViewerFile.subProjectName}
                   </Link>
                 </>
@@ -419,7 +422,10 @@ export default class MeasuresOverlay extends React.PureComponent<Props, State> {
               {sourceViewerFile.q === 'UTS' ? (
                 <>
                   {this.renderTests()}
-                  <MeasuresOverlayTestCases branch={branch} componentKey={sourceViewerFile.key} />
+                  <MeasuresOverlayTestCases
+                    branchLike={branchLike}
+                    componentKey={sourceViewerFile.key}
+                  />
                 </>
               ) : (
                 <div className="source-viewer-measures">
index 64dca78addc010fd83b82fce1ffb688124b11f88..8b2f880be51d1d9ed12e8d1ac17e52522cb35ce6 100644 (file)
@@ -23,11 +23,12 @@ import { orderBy } from 'lodash';
 import MeasuresOverlayCoveredFiles from './MeasuresOverlayCoveredFiles';
 import MeasuresOverlayTestCase from './MeasuresOverlayTestCase';
 import { getTests } from '../../../api/tests';
-import { TestCase } from '../../../app/types';
+import { TestCase, BranchLike } from '../../../app/types';
 import { translate } from '../../../helpers/l10n';
+import { getBranchLikeQuery } from '../../../helpers/branches';
 
 interface Props {
-  branch: string | undefined;
+  branchLike: BranchLike | undefined;
   componentKey: string;
 }
 
@@ -50,7 +51,7 @@ export default class MeasuresOverlayTestCases extends React.PureComponent<Props,
 
   componentDidUpdate(prevProps: Props) {
     if (
-      prevProps.branch !== this.props.branch ||
+      prevProps.branchLike !== this.props.branchLike ||
       prevProps.componentKey !== this.props.componentKey
     ) {
       this.fetchTests();
@@ -64,7 +65,11 @@ export default class MeasuresOverlayTestCases extends React.PureComponent<Props,
   fetchTests = () => {
     // TODO implement pagination one day...
     this.setState({ loading: true });
-    getTests({ branch: this.props.branch, ps: 500, testFileKey: this.props.componentKey }).then(
+    getTests({
+      ps: 500,
+      testFileKey: this.props.componentKey,
+      ...getBranchLikeQuery(this.props.branchLike)
+    }).then(
       ({ tests: testCases }) => {
         if (this.mounted) {
           this.setState({ loading: false, testCases });
index a9127eae0cedd889afa05236fe7d109ea0dffbd2..c9dc513d817c9aecaa4e5eae6832022e7319ff8f 100644 (file)
@@ -20,7 +20,7 @@
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import LineCode from '../LineCode';
-import { Issue } from '../../../../app/types';
+import { BranchType, Issue, ShortLivingBranch } from '../../../../app/types';
 
 const issueBase: Issue = {
   component: '',
@@ -50,9 +50,15 @@ it('render code', () => {
     code: '<span class="k">class</span> <span class="sym sym-1">Foo</span> {'
   };
   const issueLocations = [{ from: 0, to: 5, line: 3 }];
+  const branch: ShortLivingBranch = {
+    isMain: false,
+    mergeBranch: 'master',
+    name: 'feature',
+    type: BranchType.SHORT
+  };
   const wrapper = shallow(
     <LineCode
-      branch="feature"
+      branchLike={branch}
       highlightedLocationMessage={undefined}
       highlightedSymbols={['sym1']}
       issueLocations={issueLocations}
index a737504b3bd55a3b3d093d17e94cf9d9edd4ff16..efbe7ecfe67d70a47352a92adc42e8a1440341db 100644 (file)
@@ -27,7 +27,7 @@ it('render covered line', () => {
   const line: SourceLine = { line: 3, coverageStatus: 'covered' };
   const wrapper = shallow(
     <LineCoverage
-      branch={undefined}
+      branchLike={undefined}
       componentKey="foo"
       line={line}
       onPopupToggle={jest.fn()}
@@ -42,7 +42,7 @@ it('render uncovered line', () => {
   const line: SourceLine = { line: 3, coverageStatus: 'uncovered' };
   const wrapper = shallow(
     <LineCoverage
-      branch={undefined}
+      branchLike={undefined}
       componentKey="foo"
       line={line}
       onPopupToggle={jest.fn()}
@@ -56,7 +56,7 @@ it('render line with unknown coverage', () => {
   const line: SourceLine = { line: 3 };
   const wrapper = shallow(
     <LineCoverage
-      branch={undefined}
+      branchLike={undefined}
       componentKey="foo"
       line={line}
       onPopupToggle={jest.fn()}
@@ -71,7 +71,7 @@ it('should open coverage popup', () => {
   const onPopupToggle = jest.fn();
   const wrapper = shallow(
     <LineCoverage
-      branch={undefined}
+      branchLike={undefined}
       componentKey="foo"
       line={line}
       onPopupToggle={onPopupToggle}
index 8ab04fcda21104822882cbe609115fb429dbed11..7e9c9ec5bbe9b2095ded7dbc19fc4a37a4e3f6a1 100644 (file)
@@ -49,7 +49,7 @@ it('render issues list', () => {
   const onIssueClick = jest.fn();
   const wrapper = shallow(
     <LineIssuesList
-      branch={undefined}
+      branchLike={undefined}
       issuePopup={undefined}
       issues={issues}
       onIssueChange={jest.fn()}
index bf40ca900c8a0b145dd244fc89c5760ada206040..d027d93875656ce2667e44c4c8a53916dec1ad57 100644 (file)
@@ -26,7 +26,7 @@ it('render line 3', () => {
   const line = { line: 3 };
   const wrapper = shallow(
     <LineNumber
-      branch={undefined}
+      branchLike={undefined}
       componentKey="foo"
       line={line}
       onPopupToggle={jest.fn()}
@@ -41,7 +41,7 @@ it('render line 0', () => {
   const line = { line: 0 };
   const wrapper = shallow(
     <LineNumber
-      branch={undefined}
+      branchLike={undefined}
       componentKey="foo"
       line={line}
       onPopupToggle={jest.fn()}
index a5de2ec635e4a68f8b85210bf373ac394a1cc566..5ada553fbd6f29366cb0caf565fa86cd171b9a15 100644 (file)
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import LineOptionsPopup from '../LineOptionsPopup';
+import { BranchType, ShortLivingBranch } from '../../../../app/types';
 
 it('should render', () => {
   const line = { line: 3 };
-  const wrapper = shallow(<LineOptionsPopup branch="feature" componentKey="foo" line={line} />);
+  const branch: ShortLivingBranch = {
+    isMain: false,
+    mergeBranch: 'master',
+    name: 'feature',
+    type: BranchType.SHORT
+  };
+  const wrapper = shallow(<LineOptionsPopup branchLike={branch} componentKey="foo" line={line} />);
   expect(wrapper).toMatchSnapshot();
 });
index 1f94f1375d34a51222ee24b6a69b6ed670ece4ba..3c4a20dd0e2e83aff796bdf44dc5630c5e404118 100644 (file)
@@ -20,7 +20,7 @@
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import MeasuresOverlay from '../MeasuresOverlay';
-import { SourceViewerFile } from '../../../../app/types';
+import { SourceViewerFile, ShortLivingBranch, BranchType } from '../../../../app/types';
 import { waitAndUpdate, click } from '../../../../helpers/testUtils';
 
 jest.mock('../../../../api/issues', () => ({
@@ -148,9 +148,20 @@ const sourceViewerFile: SourceViewerFile = {
   uuid: 'abcd123'
 };
 
+const branchLike: ShortLivingBranch = {
+  isMain: false,
+  mergeBranch: 'master',
+  name: 'feature',
+  type: BranchType.SHORT
+};
+
 it('should render source file', async () => {
   const wrapper = shallow(
-    <MeasuresOverlay branch="branch" onClose={jest.fn()} sourceViewerFile={sourceViewerFile} />
+    <MeasuresOverlay
+      branchLike={branchLike}
+      onClose={jest.fn()}
+      sourceViewerFile={sourceViewerFile}
+    />
   );
   await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
@@ -162,7 +173,7 @@ it('should render source file', async () => {
 it('should render test file', async () => {
   const wrapper = shallow(
     <MeasuresOverlay
-      branch="branch"
+      branchLike={branchLike}
       onClose={jest.fn()}
       sourceViewerFile={{ ...sourceViewerFile, q: 'UTS' }}
     />
index 72226708c6f5d39139b4f94d3995749c90376103..1eacb341dc0bf6ac975176bd7fb605eb9da10885 100644 (file)
@@ -21,6 +21,7 @@ import * as React from 'react';
 import { shallow } from 'enzyme';
 import MeasuresOverlayTestCases from '../MeasuresOverlayTestCases';
 import { waitAndUpdate, click } from '../../../../helpers/testUtils';
+import { ShortLivingBranch, BranchType } from '../../../../app/types';
 
 jest.mock('../../../../api/tests', () => ({
   getTests: () =>
@@ -60,9 +61,16 @@ jest.mock('../../../../api/tests', () => ({
     })
 }));
 
+const branchLike: ShortLivingBranch = {
+  isMain: false,
+  mergeBranch: 'master',
+  name: 'feature',
+  type: BranchType.SHORT
+};
+
 it('should render', async () => {
   const wrapper = shallow(
-    <MeasuresOverlayTestCases branch="branch" componentKey="component-key" />
+    <MeasuresOverlayTestCases branchLike={branchLike} componentKey="component-key" />
   );
   await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
index f5e24cd2736787c4789bc402e50f208fb351e991..dc0f5c922005558a06238ebbcffd93df43604a51 100644 (file)
@@ -36,7 +36,14 @@ exports[`render code 1`] = `
     </pre>
   </div>
   <LineIssuesList
-    branch="feature"
+    branchLike={
+      Object {
+        "isMain": false,
+        "mergeBranch": "master",
+        "name": "feature",
+        "type": "SHORT",
+      }
+    }
     issues={
       Array [
         Object {
index 664a60ddf64c124f8187e6c1226c3917dc74c7f7..4e01802b36384ce3d4075d932620105d0ea79d26 100644 (file)
@@ -20,7 +20,7 @@ exports[`render covered line 1`] = `
     isOpen={false}
     popup={
       <CoveragePopup
-        branch={undefined}
+        branchLike={undefined}
         componentKey="foo"
         line={
           Object {
index 477d94d1c3f59b49e5089b41fcab4bbad20924d1..2633afb6d2c89a8b76ca4321802d0b73e70c49f5 100644 (file)
@@ -18,7 +18,7 @@ exports[`render line 3 1`] = `
     isOpen={false}
     popup={
       <LineOptionsPopup
-        branch={undefined}
+        branchLike={undefined}
         componentKey="foo"
         line={
           Object {
index 7378602ea6b2d7d3fe487d268c7739f4d00bd63f..50bdfbabfcd44a336cec325a7bd1d70b3320c54b 100644 (file)
@@ -24,10 +24,11 @@ exports[`should render source file 1`] = `
           style={Object {}}
           to={
             Object {
-              "pathname": "/dashboard",
+              "pathname": "/project/issues",
               "query": Object {
-                "branch": "branch",
+                "branch": "feature",
                 "id": "project-key",
+                "resolved": "false",
               },
             }
           }
@@ -44,10 +45,11 @@ exports[`should render source file 1`] = `
             style={Object {}}
             to={
               Object {
-                "pathname": "/dashboard",
+                "pathname": "/project/issues",
                 "query": Object {
-                  "branch": "branch",
+                  "branch": "feature",
                   "id": "sub-project-key",
+                  "resolved": "false",
                 },
               }
             }
@@ -401,10 +403,11 @@ exports[`should render source file 2`] = `
           style={Object {}}
           to={
             Object {
-              "pathname": "/dashboard",
+              "pathname": "/project/issues",
               "query": Object {
-                "branch": "branch",
+                "branch": "feature",
                 "id": "project-key",
+                "resolved": "false",
               },
             }
           }
@@ -421,10 +424,11 @@ exports[`should render source file 2`] = `
             style={Object {}}
             to={
               Object {
-                "pathname": "/dashboard",
+                "pathname": "/project/issues",
                 "query": Object {
-                  "branch": "branch",
+                  "branch": "feature",
                   "id": "sub-project-key",
+                  "resolved": "false",
                 },
               }
             }
@@ -1384,10 +1388,11 @@ exports[`should render test file 1`] = `
           style={Object {}}
           to={
             Object {
-              "pathname": "/dashboard",
+              "pathname": "/project/issues",
               "query": Object {
-                "branch": "branch",
+                "branch": "feature",
                 "id": "project-key",
+                "resolved": "false",
               },
             }
           }
@@ -1404,10 +1409,11 @@ exports[`should render test file 1`] = `
             style={Object {}}
             to={
               Object {
-                "pathname": "/dashboard",
+                "pathname": "/project/issues",
                 "query": Object {
-                  "branch": "branch",
+                  "branch": "feature",
                   "id": "sub-project-key",
+                  "resolved": "false",
                 },
               }
             }
@@ -1501,7 +1507,14 @@ exports[`should render test file 1`] = `
           </div>
         </div>
         <MeasuresOverlayTestCases
-          branch="branch"
+          branchLike={
+            Object {
+              "isMain": false,
+              "mergeBranch": "master",
+              "name": "feature",
+              "type": "SHORT",
+            }
+          }
           componentKey="component-key"
         />
       </React.Fragment>
index c578435451a0f1ca434ecfb05aa62f387cfbf3a3..43ac2de8f8ea2d0932e46cb5ffc297377a5768b5 100644 (file)
@@ -55,7 +55,6 @@ exports[`should render OK test 1`] = `
               Object {
                 "pathname": "/dashboard",
                 "query": Object {
-                  "branch": undefined,
                   "id": "project:src/file.js",
                 },
               }
index 8354099e6f30bd35d40086cfcb86289c89ad32b6..e23ce75a528a5a051f7cae3a375a317d74130ed3 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { searchIssues } from '../../../api/issues';
-import { Issue } from '../../../app/types';
+import { BranchLike, Issue } from '../../../app/types';
+import { getBranchLikeQuery } from '../../../helpers/branches';
 import { parseIssueFromResponse } from '../../../helpers/issues';
 import { RawQuery } from '../../../helpers/query';
 
 // maximum possible value
 const PAGE_SIZE = 500;
 
-function buildQuery(component: string, branch: string | undefined) {
+function buildQuery(component: string, branchLike: BranchLike | undefined) {
   return {
     additionalFields: '_all',
     resolved: 'false',
     componentKeys: component,
-    branch,
-    s: 'FILE_LINE'
+    s: 'FILE_LINE',
+    ...getBranchLikeQuery(branchLike)
   };
 }
 
@@ -75,9 +76,9 @@ export default function loadIssues(
   component: string,
   _fromLine: number,
   toLine: number,
-  branch: string | undefined
+  branchLike: BranchLike | undefined
 ): Promise<Issue[]> {
-  const query = buildQuery(component, branch);
+  const query = buildQuery(component, branchLike);
   return new Promise(resolve => {
     loadPageAndNext(query, toLine, 1).then(issues => {
       resolve(issues);
index dcdb4bfb32dc4d01e780a8e59efbdaf3236f0224..492b5b861b6543903f7eaf8c187323a3ddf7a7c0 100644 (file)
@@ -18,7 +18,8 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 .branch-status {
-  display: flex;
+  display: inline-flex;
+  justify-content: flex-end;
   align-items: center;
   min-width: 64px;
   line-height: calc(2 * var(--gridSize));
index 45b271f617935852730fdc98e49dbd7fffa354dc..d2e3f9929738dc4c0dcbd61bc0c98c0136ad631c 100644 (file)
  */
 import * as React from 'react';
 import StatusIndicator from './StatusIndicator';
-import { Branch } from '../../app/types';
 import Level from '../ui/Level';
 import BugIcon from '../icons-components/BugIcon';
 import CodeSmellIcon from '../icons-components/CodeSmellIcon';
 import VulnerabilityIcon from '../icons-components/VulnerabilityIcon';
-import { isShortLivingBranch } from '../../helpers/branches';
+import { BranchLike } from '../../app/types';
+import { isShortLivingBranch, isPullRequest, isLongLivingBranch } from '../../helpers/branches';
 import './BranchStatus.css';
 
 interface Props {
-  branch: Branch;
+  branchLike: BranchLike;
   concise?: boolean;
 }
 
-export default function BranchStatus({ branch, concise = false }: Props) {
-  if (isShortLivingBranch(branch)) {
-    if (!branch.status) {
+export default function BranchStatus({ branchLike, concise = false }: Props) {
+  if (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) {
+    if (!branchLike.status) {
       return null;
     }
 
     const totalIssues =
-      branch.status.bugs + branch.status.vulnerabilities + branch.status.codeSmells;
+      branchLike.status.bugs + branchLike.status.vulnerabilities + branchLike.status.codeSmells;
 
     const indicatorColor = totalIssues > 0 ? 'red' : 'green';
 
@@ -56,24 +56,26 @@ export default function BranchStatus({ branch, concise = false }: Props) {
           <StatusIndicator color={indicatorColor} size="small" />
         </li>
         <li className="spacer-left">
-          {branch.status.bugs}
+          {branchLike.status.bugs}
           <BugIcon />
         </li>
         <li className="spacer-left">
-          {branch.status.vulnerabilities}
+          {branchLike.status.vulnerabilities}
           <VulnerabilityIcon />
         </li>
         <li className="spacer-left">
-          {branch.status.codeSmells}
+          {branchLike.status.codeSmells}
           <CodeSmellIcon />
         </li>
       </ul>
     );
-  } else {
-    if (!branch.status) {
+  } else if (isLongLivingBranch(branchLike)) {
+    if (!branchLike.status) {
       return null;
     }
 
-    return <Level level={branch.status.qualityGateStatus} small={true} />;
+    return <Level level={branchLike.status.qualityGateStatus} small={true} />;
+  } else {
+    return null;
   }
 }
index 3db595c0a2d3767cb488703262c8d031b1641937..12aef45991925b617ab150cbbe72567b20e2adb3 100644 (file)
@@ -28,14 +28,14 @@ it('renders status of short-living branches', () => {
   checkShort(7, 3, 6);
 
   function checkShort(bugs: number, codeSmells: number, vulnerabilities: number) {
-    const branch: ShortLivingBranch = {
+    const shortBranch: ShortLivingBranch = {
       isMain: false,
       mergeBranch: 'master',
       name: 'foo',
       status: { bugs, codeSmells, vulnerabilities },
       type: BranchType.SHORT
     };
-    expect(shallow(<BranchStatus branch={branch} />)).toMatchSnapshot();
+    expect(shallow(<BranchStatus branchLike={shortBranch} />)).toMatchSnapshot();
   }
 });
 
@@ -53,6 +53,6 @@ it('renders status of long-living branches', () => {
     if (qualityGateStatus) {
       branch.status = { qualityGateStatus };
     }
-    return shallow(<BranchStatus branch={branch} />);
+    return shallow(<BranchStatus branchLike={branch} />);
   }
 });
index 915b6a0fbf588423ab08d7ad19645c09f87857c2..9ff03913a0d571074fa9f0735568a0677b373529 100644 (file)
 import * as React from 'react';
 import ShortLivingBranchIcon from './ShortLivingBranchIcon';
 import LongLivingBranchIcon from './LongLivingBranchIcon';
+import PullRequestIcon from './PullRequestIcon';
 import { IconProps } from './types';
-// import PullRequestIcon from './PullRequestIcon';
-import { Branch } from '../../app/types';
-import { isShortLivingBranch } from '../../helpers/branches';
+import { BranchLike } from '../../app/types';
+import { isShortLivingBranch, isPullRequest } from '../../helpers/branches';
 
 interface Props extends IconProps {
-  branch: Branch;
+  branchLike: BranchLike;
 }
 
-export default function BranchIcon({ branch, ...props }: Props) {
-  return isShortLivingBranch(branch) ? (
-    <ShortLivingBranchIcon {...props} />
-  ) : (
-    <LongLivingBranchIcon {...props} />
-  );
+export default function BranchIcon({ branchLike, ...props }: Props) {
+  if (isPullRequest(branchLike)) {
+    return <PullRequestIcon {...props} />;
+  } else if (isShortLivingBranch(branchLike)) {
+    return <ShortLivingBranchIcon {...props} />;
+  } else {
+    return <LongLivingBranchIcon {...props} />;
+  }
 }
index f0e94ac2fbf92430362641f4f413a51f67a03ba4..0e7571a54dc58a2500009b6d02395ef090de252d 100644 (file)
@@ -30,10 +30,11 @@ export default function PullRequestIcon({ className, fill = theme.blue, size = 1
       viewBox="0 0 16 16"
       version="1.1"
       xmlnsXlink="http://www.w3.org/1999/xlink"
-      xmlSpace="preserve">
+      xmlSpace="preserve"
+      style={{ fillRule: 'evenodd' }}>
       <path
         style={{ fill }}
-        d="M3 11.9V4.1c.9-.2 1.5-1 1.5-1.9 0-1.1-.9-2-2-2s-2 .9-2 2c0 .9.6 1.7 1.5 1.9v7.8c-.9.2-1.5 1-1.5 1.9 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.6-1.6-1.5-1.9zM1.5 2.2c0-.6.4-1 1-1s1 .4 1 1-.4 1-1 1-1-.4-1-1zm1 12.7c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1zM14 11.9V5.5c0-.1-.2-3.1-5.1-3.5L10.1.8 9.5.1 6.9 2.6l2.6 2.5.7-.7L8.8 3c4 .2 4.2 2.4 4.2 2.5v6.4c-.9.2-1.5 1-1.5 1.9 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.6-1.6-1.5-1.9zm-.5 3c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1z"
+        d="M13,11.9L13,5.5C13,5.4 13.232,1.996 7.9,2L9.1,0.8L8.5,0.1L5.9,2.6L8.5,5.1L9.2,4.4L7.905,3.008C12.256,2.99 12,5.4 12,5.5L12,11.9C11.1,12.1 10.5,12.9 10.5,13.8C10.5,14.9 11.4,15.8 12.5,15.8C13.6,15.8 14.5,14.9 14.5,13.8C14.5,12.9 13.9,12.2 13,11.9ZM4,11.9C4.9,12.2 5.5,12.9 5.5,13.8C5.5,14.9 4.6,15.8 3.5,15.8C2.4,15.8 1.5,14.9 1.5,13.8C1.5,12.9 2.1,12.1 3,11.9L3,4.1C2.1,3.9 1.5,3.1 1.5,2.2C1.5,1.1 2.4,0.2 3.5,0.2C4.6,0.2 5.5,1.1 5.5,2.2C5.5,3.1 4.9,3.9 4,4.1L4,11.9ZM12.5,14.9C11.9,14.9 11.5,14.5 11.5,13.9C11.5,13.3 11.9,12.9 12.5,12.9C13.1,12.9 13.5,13.3 13.5,13.9C13.5,14.5 13.1,14.9 12.5,14.9ZM3.5,14.9C2.9,14.9 2.5,14.5 2.5,13.9C2.5,13.3 2.9,12.9 3.5,12.9C4.1,12.9 4.5,13.3 4.5,13.9C4.5,14.5 4.1,14.9 3.5,14.9ZM2.5,2.2C2.5,1.6 2.9,1.2 3.5,1.2C4.1,1.2 4.5,1.6 4.5,2.2C4.5,2.8 4.1,3.2 3.5,3.2C2.9,3.2 2.5,2.8 2.5,2.2Z"
       />
     </svg>
   );
index 6930914f170dc412b6e119eec29a566b04589ad7..389a01a09a2cdd4039f0f4b2fd3f93deb12e4d78 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { Issue as IssueType } from '../../app/types';
+import { BranchLike, Issue as IssueType } from '../../app/types';
 
 interface IssueProps {
-  branch?: string;
+  branchLike?: BranchLike;
   checked?: boolean;
   displayLocationsCount?: boolean;
   displayLocationsLink?: boolean;
index 47bf09dab76d2e0b68b0cefd799bc46a3e6c4121..715d4a2d725ff1d8e52400ed764ddbd4d4f56a48 100644 (file)
@@ -29,7 +29,7 @@ import { setIssueAssignee } from '../../api/issues';
 
 /*::
 type Props = {|
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   checked?: boolean,
   displayLocationsCount?: boolean;
   displayLocationsLink?: boolean;
@@ -150,7 +150,7 @@ export default class Issue extends React.PureComponent {
   render() {
     return (
       <IssueView
-        branch={this.props.branch}
+        branchLike={this.props.branchLike}
         checked={this.props.checked}
         currentPopup={this.props.openPopup}
         displayLocationsCount={this.props.displayLocationsCount}
index b7a658e77ba0f4fae1af1c4054425ff6cd286677..5f814a5347e3a4f5d920f3d4ae46634960ba8ae8 100644 (file)
@@ -29,7 +29,7 @@ import { deleteIssueComment, editIssueComment } from '../../api/issues';
 
 /*::
 type Props = {|
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   checked?: boolean,
   currentPopup: ?string,
   displayLocationsCount?: boolean;
@@ -90,7 +90,7 @@ export default class IssueView extends React.PureComponent {
         role="listitem"
         tabIndex={0}>
         <IssueTitleBar
-          branch={this.props.branch}
+          branchLike={this.props.branchLike}
           currentPopup={this.props.currentPopup}
           displayLocationsCount={this.props.displayLocationsCount}
           displayLocationsLink={this.props.displayLocationsLink}
index 7a1cf1d5d6718a41ba6be5bcc4139854fc9d95e1..13de79da2039f263b917e7cedb22875743a485b0 100644 (file)
@@ -26,6 +26,7 @@ import SimilarIssuesFilter from './SimilarIssuesFilter';
 import LinkIcon from '../../../components/icons-components/LinkIcon';
 import LocationIndex from '../../common/LocationIndex';
 import Tooltip from '../../controls/Tooltip';
+import { getBranchLikeQuery } from '../../../helpers/branches';
 import { getComponentIssuesUrl } from '../../../helpers/urls';
 import { formatMeasure } from '../../../helpers/measures';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
@@ -33,7 +34,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n';
 
 /*::
 type Props = {|
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   currentPopup: ?string,
   displayLocationsCount?: boolean;
   displayLocationsLink?: boolean;
@@ -68,7 +69,7 @@ export default function IssueTitleBar(props /*: Props */) {
   const displayLocations = props.displayLocationsCount && locationsCount > 0;
 
   const issueUrl = getComponentIssuesUrl(issue.project, {
-    branch: props.branch,
+    ...getBranchLikeQuery(props.branchLike),
     issues: issue.key,
     open: issue.key
   });
index f3efe9d2904480775279f2e0ebfb7752399adc1a..9134a0c1f79a64ca3952680ef0ce0c5ae2373c50 100644 (file)
@@ -48,7 +48,7 @@ const issueWithLocations = {
 it('should render the titlebar correctly', () => {
   const element = shallow(
     <IssueTitleBar
-      branch="feature-1.0"
+      branchLike={{ isMain: false, name: 'feature-1.0', type: 'SHORT' }}
       issue={issue}
       currentPopup={null}
       onFail={jest.fn()}
index d9f6c77f6b9e256e2675a55d9ca1c8c047b3f412..30fc9e84c333b708e8f8ed4bde9d59e5f098ab35 100644 (file)
@@ -160,7 +160,6 @@ exports[`should render the titlebar with the filter 1`] = `
             Object {
               "pathname": "/project/issues",
               "query": Object {
-                "branch": undefined,
                 "id": "myproject",
                 "issues": "AVsae-CQS-9G3txfbFN2",
                 "open": "AVsae-CQS-9G3txfbFN2",
index 61de9d4f695c97ec33e2f2b4fd0be37116328379..a903ad15af99a3eec996ece34370bcb7b3749b30 100644 (file)
  */
 import * as React from 'react';
 import { History } from '../../api/time-machine';
-import { Metric } from '../../app/types';
+import { Metric, BranchLike } from '../../app/types';
 
 interface Props {
-  branch?: string;
+  branchLike?: BranchLike;
   history?: History;
   metrics: { [key: string]: Metric };
   project: string;
index 124b818a2677921289d634046a04841ed43d559b..ff32a9df3c69f44137dfd9d775db69c591070a40 100644 (file)
@@ -34,12 +34,13 @@ import {
 } from '../../apps/projectActivity/utils';
 import { getCustomGraph, getGraph } from '../../helpers/storage';
 import { formatMeasure, getShortType } from '../../helpers/measures';
+import { getBranchLikeQuery } from '../../helpers/branches';
 /*:: import type { Serie } from '../charts/AdvancedTimeline'; */
 /*:: import type { History, Metric } from '../../apps/overview/types'; */
 
 /*::
 type Props = {
-  branch?: string,
+  branchLike?: { id?: string; name: string },
   history: ?History,
   metrics: { [string]: Metric },
   project: string,
@@ -144,7 +145,7 @@ export default class PreviewGraph extends React.PureComponent {
   handleClick = () => {
     this.context.router.push({
       pathname: '/project/activity',
-      query: { id: this.props.project, branch: this.props.branch }
+      query: { id: this.props.project, ...getBranchLikeQuery(this.props.branchLike) }
     });
   };
 
index 1065e91e9efd7dfa38cf128a14fa5ec83f44b667..ff5587d95b4cb9bdc7786408033ce905d948d77f 100644 (file)
@@ -20,6 +20,8 @@
 import * as React from 'react';
 import { Link } from 'react-router';
 import { getComponentDrilldownUrl, getComponentIssuesUrl } from '../../helpers/urls';
+import { BranchLike } from '../../app/types';
+import { getBranchLikeQuery } from '../../helpers/branches';
 
 const ISSUE_MEASURES = [
   'violations',
@@ -47,7 +49,7 @@ const ISSUE_MEASURES = [
 ];
 
 interface Props {
-  branch?: string;
+  branchLike?: BranchLike;
   children?: React.ReactNode;
   className?: string;
   component: string;
@@ -121,7 +123,7 @@ export default class DrilldownLink extends React.PureComponent<Props> {
   renderIssuesLink = () => {
     const url = getComponentIssuesUrl(this.props.component, {
       ...this.propsToIssueParams(),
-      branch: this.props.branch
+      ...getBranchLikeQuery(this.props.branchLike)
     });
 
     return (
@@ -139,7 +141,7 @@ export default class DrilldownLink extends React.PureComponent<Props> {
     const url = getComponentDrilldownUrl(
       this.props.component,
       this.props.metric,
-      this.props.branch
+      this.props.branchLike
     );
     return (
       <Link to={url} className={this.props.className}>
index 531db332616b1d4df2ce85987e987ee52fda145a..eafcac4460eb2955560057d7b243522a3de8d519 100644 (file)
@@ -49,7 +49,7 @@ export default BaseView.extend({
   },
 
   showViewer() {
-    const { branch, key, line } = this.model.toJSON();
+    const { branchLike, key, line } = this.model.toJSON();
 
     const el = document.querySelector(this.viewerRegion.el);
 
@@ -57,7 +57,7 @@ export default BaseView.extend({
       <WithStore>
         <SourceViewer
           aroundLine={line}
-          branch={branch}
+          branchLike={branchLike}
           component={key}
           fromWorkspace={true}
           highlightedLine={line}
index 501ff66ed76d65b7a399d2033355aae1df3346f1..abf8bd874383c998ed8cdce9f1baf6215f598275 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { sortBranchesAsTree } from '../branches';
-import { MainBranch, BranchType, ShortLivingBranch, LongLivingBranch } from '../../app/types';
+import { sortBranchesAsTree, isSameBranchLike } from '../branches';
+import {
+  MainBranch,
+  BranchType,
+  ShortLivingBranch,
+  LongLivingBranch,
+  PullRequest
+} from '../../app/types';
 
 describe('#sortBranchesAsTree', () => {
   it('sorts main branch and short-living branches', () => {
     const main = mainBranch();
-    const foo = shortLivingBranch('foo', 'master');
-    const bar = shortLivingBranch('bar', 'master');
+    const foo = shortLivingBranch({ name: 'foo' });
+    const bar = shortLivingBranch({ name: 'bar' });
     expect(sortBranchesAsTree([main, foo, bar])).toEqual([main, bar, foo]);
   });
 
   it('sorts main branch and long-living branches', () => {
     const main = mainBranch();
-    const foo = longLivingBranch('foo');
-    const bar = longLivingBranch('bar');
+    const foo = longLivingBranch({ name: 'foo' });
+    const bar = longLivingBranch({ name: 'bar' });
     expect(sortBranchesAsTree([main, foo, bar])).toEqual([main, bar, foo]);
   });
 
   it('sorts all types of branches', () => {
     const main = mainBranch();
-    const shortFoo = shortLivingBranch('shortFoo', 'master');
-    const shortBar = shortLivingBranch('shortBar', 'longBaz');
-    const shortPre = shortLivingBranch('shortPre', 'shortFoo');
-    const longBaz = longLivingBranch('longBaz');
-    const longQux = longLivingBranch('longQux');
-    const longQwe = longLivingBranch('longQwe');
+    const shortFoo = shortLivingBranch({ name: 'shortFoo', mergeBranch: 'master' });
+    const shortBar = shortLivingBranch({ name: 'shortBar', mergeBranch: 'longBaz' });
+    const shortPre = shortLivingBranch({ name: 'shortPre', mergeBranch: 'shortFoo' });
+    const longBaz = longLivingBranch({ name: 'longBaz' });
+    const longQux = longLivingBranch({ name: 'longQux' });
+    const longQwe = longLivingBranch({ name: 'longQwe' });
+    const pr = pullRequest({ base: 'master' });
     // - main                     - main
     //    - shortFoo                - shortFoo
     //      - shortPre              - shortPre
@@ -51,8 +58,43 @@ describe('#sortBranchesAsTree', () => {
     //       - longQwe            - longQwe
     //    - longQux               - longQux
     expect(
-      sortBranchesAsTree([main, shortFoo, shortBar, shortPre, longBaz, longQux, longQwe])
-    ).toEqual([main, shortFoo, shortPre, longBaz, shortBar, longQux, longQwe]);
+      sortBranchesAsTree([main, shortFoo, shortBar, shortPre, longBaz, longQux, longQwe, pr])
+    ).toEqual([main, pr, shortFoo, shortPre, longBaz, shortBar, longQux, longQwe]);
+  });
+});
+
+describe('#isSameBranchLike', () => {
+  it('compares different kinds', () => {
+    const main = mainBranch();
+    const short = shortLivingBranch({ name: 'foo' });
+    const long = longLivingBranch({ name: 'foo' });
+    const pr = pullRequest();
+    expect(isSameBranchLike(main, pr)).toBeFalsy();
+    expect(isSameBranchLike(main, short)).toBeFalsy();
+    expect(isSameBranchLike(main, long)).toBeFalsy();
+    expect(isSameBranchLike(pr, short)).toBeFalsy();
+    expect(isSameBranchLike(pr, long)).toBeFalsy();
+    expect(isSameBranchLike(short, long)).toBeFalsy();
+  });
+
+  it('compares pull requests', () => {
+    expect(isSameBranchLike(pullRequest({ key: '1234' }), pullRequest({ key: '1234' }))).toBeTruthy();
+    expect(isSameBranchLike(pullRequest({ key: '1234' }), pullRequest({ key: '5678' }))).toBeFalsy();
+  });
+
+  it('compares branches', () => {
+    expect(
+      isSameBranchLike(longLivingBranch({ name: 'foo' }), longLivingBranch({ name: 'foo' }))
+    ).toBeTruthy();
+    expect(
+      isSameBranchLike(shortLivingBranch({ name: 'foo' }), shortLivingBranch({ name: 'foo' }))
+    ).toBeTruthy();
+    expect(
+      isSameBranchLike(longLivingBranch({ name: 'foo' }), longLivingBranch({ name: 'bar' }))
+    ).toBeFalsy();
+    expect(
+      isSameBranchLike(shortLivingBranch({ name: 'foo' }), shortLivingBranch({ name: 'bar' }))
+    ).toBeFalsy();
   });
 });
 
@@ -60,12 +102,31 @@ function mainBranch(): MainBranch {
   return { isMain: true, name: 'master' };
 }
 
-function shortLivingBranch(name: string, mergeBranch: string): ShortLivingBranch {
+function shortLivingBranch(overrides?: Partial<ShortLivingBranch>): ShortLivingBranch {
   const status = { bugs: 0, codeSmells: 0, vulnerabilities: 0 };
-  return { isMain: false, mergeBranch, name, status, type: BranchType.SHORT };
+  return {
+    isMain: false,
+    mergeBranch: 'master',
+    name: 'foo',
+    status,
+    type: BranchType.SHORT,
+    ...overrides
+  };
 }
 
-function longLivingBranch(name: string): LongLivingBranch {
+function longLivingBranch(overrides?: Partial<LongLivingBranch>): LongLivingBranch {
   const status = { qualityGateStatus: 'OK' };
-  return { isMain: false, name, status, type: BranchType.LONG };
+  return { isMain: false, name: 'foo', status, type: BranchType.LONG, ...overrides };
+}
+
+function pullRequest(overrides?: Partial<PullRequest>): PullRequest {
+  const status = { bugs: 0, codeSmells: 0, vulnerabilities: 0 };
+  return {
+    base: 'master',
+    branch: 'feature',
+    key: '1234',
+    status,
+    title: 'Random Name',
+    ...overrides
+  };
 }
index ecd54ab1adbe0a509867cecc1c271cb4ee3e7b3d..75382073d329a0385d065d7d2b509280f3cb0fb1 100644 (file)
@@ -44,8 +44,8 @@ afterEach(() => {
 
 describe('#getPathUrlAsString', () => {
   it('should return component url', () => {
-    expect(getPathUrlAsString(getProjectUrl(SIMPLE_COMPONENT_KEY, 'branch:7.0'))).toBe(
-      '/dashboard?id=' + SIMPLE_COMPONENT_KEY + '&branch=branch%3A7.0'
+    expect(getPathUrlAsString(getProjectUrl(SIMPLE_COMPONENT_KEY))).toBe(
+      '/dashboard?id=' + SIMPLE_COMPONENT_KEY
     );
   });
 
index 195b5a95a0e30096d41624b6c6d3fdbba58e6fcf..510ed8c3f85481723729dce80b0e55f00739443d 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { sortBy } from 'lodash';
-import { Branch, BranchType, ShortLivingBranch, LongLivingBranch } from '../app/types';
+import {
+  BranchLike,
+  Branch,
+  BranchType,
+  ShortLivingBranch,
+  LongLivingBranch,
+  PullRequest,
+  MainBranch,
+  BranchParameters
+} from '../app/types';
 
-export function isShortLivingBranch(branch?: Branch): branch is ShortLivingBranch {
-  return branch !== undefined && !branch.isMain && branch.type === BranchType.SHORT;
+export function isBranch(branchLike?: BranchLike): branchLike is Branch {
+  return branchLike !== undefined && (branchLike as Branch).isMain !== undefined;
 }
 
-export function isLongLivingBranch(branch?: Branch): branch is LongLivingBranch {
-  return branch !== undefined && !branch.isMain && branch.type === BranchType.LONG;
+export function isShortLivingBranch(branchLike?: BranchLike): branchLike is ShortLivingBranch {
+  return (
+    isBranch(branchLike) &&
+    !branchLike.isMain &&
+    (branchLike as ShortLivingBranch).type === BranchType.SHORT
+  );
 }
 
-export function getBranchName(branch?: Branch): string | undefined {
-  return !branch || branch.isMain ? undefined : branch.name;
+export function isLongLivingBranch(branchLike?: BranchLike): branchLike is LongLivingBranch {
+  return (
+    isBranch(branchLike) &&
+    !branchLike.isMain &&
+    (branchLike as LongLivingBranch).type === BranchType.LONG
+  );
 }
 
-export function sortBranchesAsTree(branches: Branch[]): Branch[] {
-  const result: Branch[] = [];
+export function isMainBranch(branchLike?: BranchLike): branchLike is MainBranch {
+  return isBranch(branchLike) && branchLike.isMain;
+}
+
+export function isPullRequest(branchLike?: BranchLike): branchLike is PullRequest {
+  return branchLike !== undefined && (branchLike as PullRequest).key !== undefined;
+}
 
-  const shortLivingBranches = branches.filter(isShortLivingBranch);
+export function getPullRequestDisplayName(pullRequest: PullRequest) {
+  return `${pullRequest.key} – ${pullRequest.title}`;
+}
+
+export function getBranchLikeDisplayName(branchLike: BranchLike) {
+  return isPullRequest(branchLike) ? getPullRequestDisplayName(branchLike) : branchLike.name;
+}
+
+export function getBranchLikeKey(branchLike: BranchLike) {
+  return isPullRequest(branchLike) ? `pull-request-${branchLike.key}` : `branch-${branchLike.name}`;
+}
+
+export function isSameBranchLike(a: BranchLike | undefined, b: BranchLike | undefined) {
+  // main branches are always equal
+  if (isMainBranch(a) && isMainBranch(b)) {
+    return true;
+  }
+
+  // short- and long-living branches are compared by type and name
+  if (
+    (isLongLivingBranch(a) && isLongLivingBranch(b)) ||
+    (isShortLivingBranch(a) && isShortLivingBranch(b))
+  ) {
+    return a.type === b.type && a.name === b.name;
+  }
+
+  // pull requests are compared by id
+  if (isPullRequest(a) && isPullRequest(b)) {
+    return a.key === b.key;
+  }
+
+  // finally if both parameters are `undefined`, consider them equal
+  return a === b;
+}
+
+export function sortBranchesAsTree(branchLikes: BranchLike[]) {
+  const result: BranchLike[] = [];
+
+  const mainBranch = branchLikes.find(isMainBranch);
+  const longLivingBranches = branchLikes.filter(isLongLivingBranch);
+  const shortLivingBranches = branchLikes.filter(isShortLivingBranch);
+  const pullRequests = branchLikes.filter(isPullRequest);
 
   // main branch is always first
-  const mainBranch = branches.find(branch => branch.isMain);
   if (mainBranch) {
-    result.push(mainBranch, ...getNestedShortLivingBranches(mainBranch.name));
+    result.push(
+      mainBranch,
+      ...getPullRequests(mainBranch.name),
+      ...getNestedShortLivingBranches(mainBranch.name)
+    );
   }
 
-  // the all long-living branches
-  sortBy(branches.filter(isLongLivingBranch), 'name').forEach(longLivingBranch => {
-    result.push(longLivingBranch, ...getNestedShortLivingBranches(longLivingBranch.name));
+  // then all long-living branches
+  sortBy(longLivingBranches, 'name').forEach(longLivingBranch => {
+    result.push(
+      longLivingBranch,
+      ...getPullRequests(longLivingBranch.name),
+      ...getNestedShortLivingBranches(longLivingBranch.name)
+    );
   });
 
-  // finally all orhpan branches
-  result.push(...shortLivingBranches.filter(branch => branch.isOrphan));
+  // finally all orhpan pull requests and branches
+  result.push(
+    ...pullRequests.filter(pr => pr.isOrphan),
+    ...shortLivingBranches.filter(branch => branch.isOrphan)
+  );
 
   return result;
 
   /** Get all short-living branches (possibly nested) which should be merged to a given branch */
-  function getNestedShortLivingBranches(mergeBranch: string): ShortLivingBranch[] {
+  function getNestedShortLivingBranches(mergeBranch: string) {
     const found: ShortLivingBranch[] = shortLivingBranches.filter(
       branch => branch.mergeBranch === mergeBranch
     );
@@ -68,4 +141,18 @@ export function sortBranchesAsTree(branches: Branch[]): Branch[] {
 
     return sortBy(found, 'name');
   }
+
+  function getPullRequests(base: string) {
+    return pullRequests.filter(pr => pr.base === base);
+  }
+}
+
+export function getBranchLikeQuery(branchLike?: BranchLike): BranchParameters {
+  if (isShortLivingBranch(branchLike) || isLongLivingBranch(branchLike)) {
+    return { branch: branchLike.name };
+  } else if (isPullRequest(branchLike)) {
+    return { pullRequest: branchLike.key };
+  } else {
+    return {};
+  }
 }
index ae1257247613774e8c7ca29f8f1024c4cadf6a38..2f6b31408409c94d919a86499f1863479016fbd6 100644 (file)
  */
 import { stringify } from 'querystring';
 import { omitBy, isNil } from 'lodash';
-import { isShortLivingBranch } from './branches';
+import {
+  isShortLivingBranch,
+  isPullRequest,
+  isLongLivingBranch,
+  getBranchLikeQuery
+} from './branches';
 import { getProfilePath } from '../apps/quality-profiles/utils';
-import { Branch, HomePage, HomePageType } from '../app/types';
+import { BranchLike, HomePage, HomePageType } from '../app/types';
 
 interface Query {
   [x: string]: string | undefined;
@@ -44,8 +49,8 @@ export function getPathUrlAsString(path: Location): string {
   return `${getBaseUrl()}${path.pathname}?${stringify(omitBy(path.query, isNil))}`;
 }
 
-export function getProjectUrl(key: string, branch?: string): Location {
-  return { pathname: '/dashboard', query: { id: key, branch } };
+export function getProjectUrl(project: string): Location {
+  return { pathname: '/dashboard', query: { id: project } };
 }
 
 export function getPortfolioUrl(key: string): Location {
@@ -56,19 +61,30 @@ export function getComponentBackgroundTaskUrl(componentKey: string, status?: str
   return { pathname: '/project/background_tasks', query: { id: componentKey, status } };
 }
 
-export function getProjectBranchUrl(key: string, branch: Branch): Location {
-  if (isShortLivingBranch(branch)) {
-    return {
-      pathname: '/project/issues',
-      query: { branch: branch.name, id: key, resolved: 'false' }
-    };
-  } else if (!branch.isMain) {
-    return { pathname: '/dashboard', query: { branch: branch.name, id: key } };
+export function getBranchLikeUrl(project: string, branchLike?: BranchLike): Location {
+  if (isPullRequest(branchLike)) {
+    return getPullRequestUrl(project, branchLike.key);
+  } else if (isShortLivingBranch(branchLike)) {
+    return getShortLivingBranchUrl(project, branchLike.name);
+  } else if (isLongLivingBranch(branchLike)) {
+    return getLongLivingBranchUrl(project, branchLike.name);
   } else {
-    return { pathname: '/dashboard', query: { id: key } };
+    return getProjectUrl(project);
   }
 }
 
+export function getLongLivingBranchUrl(project: string, branch: string): Location {
+  return { pathname: '/dashboard', query: { branch, id: project } };
+}
+
+export function getShortLivingBranchUrl(project: string, branch: string): Location {
+  return { pathname: '/project/issues', query: { branch, id: project, resolved: 'false' } };
+}
+
+export function getPullRequestUrl(project: string, pullRequest: string): Location {
+  return { pathname: '/project/issues', query: { id: project, pullRequest, resolved: 'false' } };
+}
+
 /**
  * Generate URL for a global issues page
  */
@@ -90,25 +106,40 @@ export function getComponentIssuesUrl(componentKey: string, query?: Query): Loca
 export function getComponentDrilldownUrl(
   componentKey: string,
   metric: string,
-  branch?: string
+  branchLike?: BranchLike
 ): Location {
-  return { pathname: '/component_measures', query: { id: componentKey, metric, branch } };
+  return {
+    pathname: '/component_measures',
+    query: { id: componentKey, metric, ...getBranchLikeQuery(branchLike) }
+  };
 }
 
-export function getMeasureTreemapUrl(component: string, metric: string, branch?: string) {
+export function getMeasureTreemapUrl(component: string, metric: string) {
   return {
     pathname: '/component_measures',
-    query: { id: component, metric, branch, view: 'treemap' }
+    query: { id: component, metric, view: 'treemap' }
+  };
+}
+
+export function getActivityUrl(component: string, branchLike?: BranchLike) {
+  return {
+    pathname: '/project/activity',
+    query: { id: component, ...getBranchLikeQuery(branchLike) }
   };
 }
 
 /**
  * Generate URL for a component's measure history
  */
-export function getMeasureHistoryUrl(component: string, metric: string, branch?: string) {
+export function getMeasureHistoryUrl(component: string, metric: string, branchLike?: BranchLike) {
   return {
     pathname: '/project/activity',
-    query: { id: component, graph: 'custom', custom_metrics: metric, branch }
+    query: {
+      id: component,
+      graph: 'custom',
+      custom_metrics: metric,
+      ...getBranchLikeQuery(branchLike)
+    }
   };
 }
 
@@ -172,8 +203,8 @@ export function getMarkdownHelpUrl(): string {
   return getBaseUrl() + '/markdown/help';
 }
 
-export function getCodeUrl(project: string, branch?: string, selected?: string) {
-  return { pathname: '/code', query: { id: project, branch, selected } };
+export function getCodeUrl(project: string, branchLike?: BranchLike, selected?: string) {
+  return { pathname: '/code', query: { id: project, ...getBranchLikeQuery(branchLike), selected } };
 }
 
 export function getOrganizationUrl(organization: string) {
@@ -185,7 +216,9 @@ export function getHomePageUrl(homepage: HomePage) {
     case HomePageType.Application:
       return getProjectUrl(homepage.component);
     case HomePageType.Project:
-      return getProjectUrl(homepage.component, homepage.branch);
+      return homepage.branch
+        ? getLongLivingBranchUrl(homepage.component, homepage.branch)
+        : getProjectUrl(homepage.component);
     case HomePageType.Organization:
       return getOrganizationUrl(homepage.organization);
     case HomePageType.Portfolio:
index 43d19d28015efc81f90f692fd8874ded2209b3c9..f5b5fb14f0217de1322a103d9ee71fcf42fc08ab 100644 (file)
@@ -489,10 +489,10 @@ deletion.page=Deletion
 project_deletion.page.description=Delete this project. The operation cannot be undone.
 portfolio_deletion.page.description=Delete this portfolio. Component projects and Local Reference Portfolios will not be deleted, but component Standard Portfolios will be deleted. This operation cannot be undone.
 application_deletion.page.description=Delete this application. Application projects will not be deleted. This operation cannot be undone.
-project_branches.page=Branches
-project_branches.page.description=Use this page to manage project branches.
-project_branches.page.life_time=Short-lived branches are permanently deleted after {days} days without analysis.
-project_branches.page.life_time.admin=Short-lived branches are permanently deleted after {days} days without analysis. You can adjust this value globally in {settings}.
+project_branches.page=Branches & Pull Requests
+project_branches.page.description=Use this page to manage project branches and pull requests.
+project_branches.page.life_time=Short-lived branches and pull requests are permanently deleted after {days} days without analysis.
+project_branches.page.life_time.admin=You can adjust this value globally in {settings}.
 
 #------------------------------------------------------------------------------
 #
@@ -2632,11 +2632,13 @@ branches.learn_how_to_analyze=Learn how to analyze branches in SonarQube
 branches.learn_how_to_analyze.text=Quickly setup branch analysis and get separate insights for each of your branches and pull requests.
 branches.delete=Delete Branch
 branches.delete.are_you_sure=Are you sure you want to delete branch "{0}"?
+branches.pull_request.delete=Delete Pull Request
+branches.pull_request.delete.are_you_sure=Are you sure you want to delete pull request "{0}"?
 branches.rename=Rename Branch
 branches.manage=Manage branches
 branches.orphan_branch=Orphan Branch
-branches.orphan_branches=Orphan Branches
-branches.orphan_branches.tooltip=When a target branch of a short-living branch was deleted, this short-living branch becomes orphan.
+branches.orphan_branches=Orphan Branches & Pull Requests
+branches.orphan_branches.tooltip=When a target branch of a short-living branch or a base of a pull request was deleted, this short-living branch or pull request becomes orphan.
 branches.main_branch=Main Branch
 branches.branch_settings=Branch Settings
 branches.long_living_branches_pattern=Long living branches pattern
@@ -2647,6 +2649,10 @@ branches.last_analysis_date=Last Analysis Date
 branches.no_support.header=Get the most out of SonarQube with branches analysis
 branches.no_support.header.text=Analyze each branch of your project separately with the Developer Edition.
 branches.search_for_branches=Search for branches...
+branches.pull_requests=Pull Requests
+branches.short_lived_branches=Short-lived branches
+branches.pull_request.for_merge_into_x_from_y=for merge into {base} from {branch}
+branches.see_the_pr=See the PR
 
 
 #------------------------------------------------------------------------------