aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPhilippe Perrin <philippe.perrin@sonarsource.com>2019-11-11 08:58:07 +0100
committerSonarTech <sonartech@sonarsource.com>2019-12-09 20:46:16 +0100
commit956001c58e66df580b0a3a8b91cb886b84980971 (patch)
treedb9ec5dcdc5c98b93055993b58f6e07930527ef8
parentfe9dd29337def7804fc77d1ad75364a9eaf5476f (diff)
downloadsonarqube-956001c58e66df580b0a3a8b91cb886b84980971.tar.gz
sonarqube-956001c58e66df580b0a3a8b91cb886b84980971.zip
SONAR-12634 Reorganize the branches & pull requests selection menu
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts4
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentBreadcrumb.tsx72
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css35
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx231
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx263
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx77
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx121
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentBreadcrumb-test.tsx52
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx143
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx101
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx78
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentBreadcrumb-test.tsx.snap48
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap3
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap286
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap291
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap181
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap275
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.css59
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx93
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx114
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx51
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx202
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx67
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx112
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx76
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLike-test.tsx106
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLikeMergeInformation-test.tsx43
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx122
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItem-test.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx)49
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItemList-test.tsx50
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap201
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap185
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLikeMergeInformation-test.tsx.snap22
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/Menu-test.tsx.snap335
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap102
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItemList-test.tsx.snap428
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchList-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRowRenderer.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTableRenderer.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeRowRenderer-test.tsx.snap8
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeTableRenderer-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/components/hoc/withAppState.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/icons/BranchLikeIcon.tsx (renamed from server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx)4
-rw-r--r--server/sonar-web/src/main/js/components/icons/__tests__/BranchLikeIcon-test.tsx (renamed from server/sonar-web/src/main/js/components/icons-components/__tests__/BranchIcon-test.tsx)6
-rw-r--r--server/sonar-web/src/main/js/components/icons/__tests__/__snapshots__/BranchLikeIcon-test.tsx.snap (renamed from server/sonar-web/src/main/js/components/icons-components/__tests__/__snapshots__/BranchIcon-test.tsx.snap)0
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts82
-rw-r--r--server/sonar-web/src/main/js/helpers/branches.ts69
-rw-r--r--server/sonar-web/src/main/js/helpers/urls.ts11
-rw-r--r--server/sonar-web/src/main/js/types/branch-like.d.ts78
-rw-r--r--server/sonar-web/src/main/js/types/component.ts31
-rw-r--r--server/sonar-web/src/main/js/types/types.d.ts45
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties29
54 files changed, 2974 insertions, 2089 deletions
diff --git a/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts b/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts
index 680a7f3cfb0..c0231f17d4c 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts
+++ b/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts
@@ -67,7 +67,7 @@ import { formatMeasure } from 'sonar-ui-common/helpers/measures';
import NotFound from '../../../app/components/NotFound';
import Favorite from '../../../components/controls/Favorite';
import HomePageSelect from '../../../components/controls/HomePageSelect';
-import BranchIcon from '../../../components/icons-components/BranchIcon';
+import BranchLikeIcon from '../../../components/icons/BranchLikeIcon';
import DateFormatter from '../../../components/intl/DateFormatter';
import DateFromNow from '../../../components/intl/DateFromNow';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
@@ -125,7 +125,7 @@ const exposeLibraries = () => {
AlertErrorIcon,
AlertSuccessIcon,
AlertWarnIcon,
- BranchIcon,
+ BranchIcon: BranchLikeIcon,
Button,
Checkbox,
CheckIcon,
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentBreadcrumb.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentBreadcrumb.tsx
new file mode 100644
index 00000000000..5e646b5cdb6
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentBreadcrumb.tsx
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { last } from 'lodash';
+import * as React from 'react';
+import { Link } from 'react-router';
+import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon';
+import { isMainBranch } from '../../../../helpers/branches';
+import { getProjectUrl } from '../../../../helpers/urls';
+
+interface Props {
+ component: T.Component;
+ currentBranchLike: T.BranchLike | undefined;
+}
+
+export function ComponentBreadcrumb(props: Props) {
+ const {
+ component: { breadcrumbs },
+ currentBranchLike
+ } = props;
+ const lastBreadcrumbElement = last(breadcrumbs);
+ const isNoMainBranch = currentBranchLike && !isMainBranch(currentBranchLike);
+
+ return (
+ <div className="big flex-shrink display-flex-center">
+ {breadcrumbs.map((breadcrumbElement, i) => {
+ const isFirst = i === 0;
+ const isNotLast = i < breadcrumbs.length - 1;
+
+ return (
+ <span className="flex-shrink display-flex-center" key={breadcrumbElement.key}>
+ {isFirst && lastBreadcrumbElement && (
+ <QualifierIcon className="spacer-right" qualifier={lastBreadcrumbElement.qualifier} />
+ )}
+ {isNoMainBranch || isNotLast ? (
+ <Link
+ className="link-no-underline text-ellipsis"
+ title={breadcrumbElement.name}
+ to={getProjectUrl(breadcrumbElement.key)}>
+ {breadcrumbElement.name}
+ </Link>
+ ) : (
+ <span className="text-ellipsis" title={breadcrumbElement.name}>
+ {breadcrumbElement.name}
+ </span>
+ )}
+ {isNotLast && <span className="slash-separator" />}
+ </span>
+ );
+ })}
+ </div>
+ );
+}
+
+export default React.memo(ComponentBreadcrumb);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
index 089be790d73..70c4db36c6e 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
@@ -17,21 +17,6 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-.navbar-context-branches {
- display: inline-flex;
- justify-content: center;
- align-items: center;
- flex-shrink: 1 !important;
- min-width: 0;
- line-height: calc(2 * var(--gridSize));
- margin-left: calc(2 * var(--gridSize));
- font-size: var(--baseFontSize);
-}
-
-.navbar-context-branches .popup {
- min-width: 430px;
- max-width: 650px;
-}
.navbar-context-meta .alert {
margin-bottom: 0;
@@ -40,23 +25,3 @@
.navbar-context-meta .alert-content {
padding: 6px 8px;
}
-
-.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;
- align-items: center;
-}
-
-.navbar-context-meta-branch-menu-item-name {
- flex: 0 1 550px; /* Workaround for SONAR-10971 */
- min-width: 0;
-}
-
-.navbar-context-meta-branch-menu-item-actions {
- height: 12px;
- margin-left: 32px;
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
index 80214202fc7..6605535550a 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
@@ -91,8 +91,6 @@ export default class ComponentNav extends React.PureComponent<Props> {
branchLikes={this.props.branchLikes}
component={component}
currentBranchLike={currentBranchLike}
- // to close dropdown on any location change
- location={this.props.location}
/>
<ComponentNavMeta
branchLike={currentBranchLike}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
deleted file mode 100644
index f6abb60a0ad..00000000000
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 { FormattedMessage } from 'react-intl';
-import { Link } from 'react-router';
-import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
-import Toggler from 'sonar-ui-common/components/controls/Toggler';
-import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon';
-import PlusCircleIcon from 'sonar-ui-common/components/icons/PlusCircleIcon';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import DocTooltip from '../../../../components/docs/DocTooltip';
-import { withAppState } from '../../../../components/hoc/withAppState';
-import BranchIcon from '../../../../components/icons-components/BranchIcon';
-import {
- getBranchLikeDisplayName,
- isPullRequest,
- isSameBranchLike,
- isShortLivingBranch
-} from '../../../../helpers/branches';
-import { isSonarCloud } from '../../../../helpers/system';
-import { getPortfolioAdminUrl } from '../../../../helpers/urls';
-import { colors } from '../../../theme';
-import ComponentNavBranchesMenu from './ComponentNavBranchesMenu';
-
-interface Props {
- appState: Pick<T.AppState, 'branchesEnabled'>;
- branchLikes: T.BranchLike[];
- component: T.Component;
- currentBranchLike: T.BranchLike;
- location?: any;
-}
-
-interface State {
- dropdownOpen: boolean;
-}
-
-export class ComponentNavBranch extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { dropdownOpen: false };
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillReceiveProps(nextProps: Props) {
- if (
- nextProps.component !== this.props.component ||
- !isSameBranchLike(nextProps.currentBranchLike, this.props.currentBranchLike) ||
- nextProps.location !== this.props.location
- ) {
- this.setState({ dropdownOpen: false });
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- handleClick = (event: React.SyntheticEvent<HTMLElement>) => {
- event.preventDefault();
- event.stopPropagation();
- event.currentTarget.blur();
- this.setState(state => ({ dropdownOpen: !state.dropdownOpen }));
- };
-
- closeDropdown = () => {
- if (this.mounted) {
- this.setState({ dropdownOpen: false });
- }
- };
-
- renderMergeBranch = () => {
- const { currentBranchLike } = this.props;
- if (isShortLivingBranch(currentBranchLike)) {
- return currentBranchLike.isOrphan ? (
- <span className="note big-spacer-left text-ellipsis flex-shrink">
- <span className="text-middle">{translate('branches.orphan_branch')}</span>
- <HelpTooltip
- className="spacer-left"
- overlay={translate('branches.orphan_branches.tooltip')}
- />
- </span>
- ) : (
- <span className="note big-spacer-left">
- {translate('from')} <strong>{currentBranchLike.mergeBranch}</strong>
- </span>
- );
- } else if (isPullRequest(currentBranchLike)) {
- return (
- <span className="note big-spacer-left text-ellipsis flex-shrink">
- <FormattedMessage
- defaultMessage={translate('branches.pull_request.for_merge_into_x_from_y')}
- id="branches.pull_request.for_merge_into_x_from_y"
- values={{
- target: <strong>{currentBranchLike.target}</strong>,
- branch: <strong>{currentBranchLike.branch}</strong>
- }}
- />
- </span>
- );
- } else {
- return null;
- }
- };
-
- renderOverlay = () => {
- return (
- <>
- <p>{translate('application.branches.help')}</p>
- <hr className="spacer-top spacer-bottom" />
- <Link
- className="spacer-left link-no-underline"
- to={getPortfolioAdminUrl(this.props.component.breadcrumbs[0].key, 'APP')}>
- {translate('application.branches.link')}
- </Link>
- </>
- );
- };
-
- render() {
- const { branchLikes, currentBranchLike } = this.props;
- const { configuration, breadcrumbs } = this.props.component;
-
- if (isSonarCloud() && !this.props.appState.branchesEnabled) {
- return null;
- }
-
- const displayName = getBranchLikeDisplayName(currentBranchLike);
- const isApp = breadcrumbs && breadcrumbs[0] && breadcrumbs[0].qualifier === 'APP';
-
- if (isApp && branchLikes.length < 2) {
- return (
- <div className="navbar-context-branches">
- <BranchIcon
- branchLike={currentBranchLike}
- className="little-spacer-right"
- fill={colors.gray80}
- />
- <span className="note">{displayName}</span>
- {configuration && configuration.showSettings && (
- <HelpTooltip className="spacer-left" overlay={this.renderOverlay()}>
- <PlusCircleIcon className="text-middle" fill={colors.blue} size={12} />
- </HelpTooltip>
- )}
- </div>
- );
- } else {
- if (!this.props.appState.branchesEnabled) {
- return (
- <div className="navbar-context-branches">
- <BranchIcon
- branchLike={currentBranchLike}
- className="little-spacer-right"
- fill={colors.gray80}
- />
- <span className="note">{displayName}</span>
- <DocTooltip
- className="spacer-left"
- doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/branches/no-branch-support.md')}>
- <PlusCircleIcon fill={colors.gray71} size={12} />
- </DocTooltip>
- </div>
- );
- }
-
- if (branchLikes.length < 2) {
- return (
- <div className="navbar-context-branches">
- <BranchIcon branchLike={currentBranchLike} className="little-spacer-right" />
- <span className="note">{displayName}</span>
- <DocTooltip
- className="spacer-left"
- doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/branches/single-branch.md')}>
- <PlusCircleIcon fill={colors.blue} size={12} />
- </DocTooltip>
- </div>
- );
- }
- }
-
- return (
- <div className="navbar-context-branches">
- <div className="dropdown">
- <Toggler
- onRequestClose={this.closeDropdown}
- open={this.state.dropdownOpen}
- overlay={
- <ComponentNavBranchesMenu
- branchLikes={this.props.branchLikes}
- canAdmin={configuration && configuration.showSettings}
- component={this.props.component}
- currentBranchLike={this.props.currentBranchLike}
- onClose={this.closeDropdown}
- />
- }>
- <a
- className="link-base-color link-no-underline nowrap"
- href="#"
- onClick={this.handleClick}>
- <BranchIcon branchLike={currentBranchLike} className="little-spacer-right" />
- <span className="text-limited text-top" title={displayName}>
- {displayName}
- </span>
- <DropdownIcon className="little-spacer-left" />
- </a>
- </Toggler>
- </div>
- {this.renderMergeBranch()}
- </div>
- );
- }
-}
-
-export default withAppState(ComponentNavBranch);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
deleted file mode 100644
index 72da26274fa..00000000000
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
+++ /dev/null
@@ -1,263 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { Link } from 'react-router';
-import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
-import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
-import SearchBox from 'sonar-ui-common/components/controls/SearchBox';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import { scrollToElement } from 'sonar-ui-common/helpers/scrolling';
-import { Router, withRouter } from '../../../../components/hoc/withRouter';
-import {
- getBranchLikeKey,
- isBranch,
- isLongLivingBranch,
- isPullRequest,
- isSameBranchLike,
- isShortLivingBranch,
- sortBranchesAsTree
-} from '../../../../helpers/branches';
-import { getBranchLikeUrl } from '../../../../helpers/urls';
-import ComponentNavBranchesMenuItem from './ComponentNavBranchesMenuItem';
-
-interface Props {
- branchLikes: T.BranchLike[];
- canAdmin?: boolean;
- component: T.Component;
- currentBranchLike: T.BranchLike;
- onClose: () => void;
- router: Pick<Router, 'push'>;
-}
-
-interface State {
- query: string;
- selected: T.BranchLike | undefined;
-}
-
-export class ComponentNavBranchesMenu extends React.PureComponent<Props, State> {
- listNode?: HTMLUListElement | null;
- selectedBranchNode?: HTMLLIElement | null;
- state: State = { query: '', selected: undefined };
-
- componentDidMount() {
- this.scrollToSelectedBranch(false);
- }
-
- componentDidUpdate() {
- this.scrollToSelectedBranch(true);
- }
-
- scrollToSelectedBranch(smooth: boolean) {
- if (this.listNode && this.selectedBranchNode) {
- scrollToElement(this.selectedBranchNode, {
- parent: this.listNode,
- smooth
- });
- }
- }
-
- 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.toLowerCase().includes(query) ||
- branchLike.key.toLowerCase().includes(query));
- return matchBranchName || matchPullRequestTitleOrId;
- });
- };
-
- handleSearchChange = (query: string) => this.setState({ query, selected: undefined });
-
- handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
- switch (event.keyCode) {
- case 13:
- event.preventDefault();
- this.openSelected();
- return;
- case 38:
- event.preventDefault();
- this.selectPrevious();
- return;
- case 40:
- event.preventDefault();
- this.selectNext();
- // keep this return to prevent fall-through in case more cases will be adder later
- // eslint-disable-next-line no-useless-return
- return;
- }
- };
-
- openSelected = () => {
- const selected = this.getSelected();
- if (selected) {
- this.props.router.push(this.getProjectBranchUrl(selected));
- }
- };
-
- selectPrevious = () => {
- const selected = this.getSelected();
- const branchLikes = this.getFilteredBranchLikes();
- const index = branchLikes.findIndex(b => isSameBranchLike(b, selected));
- if (index > 0) {
- this.setState({ selected: branchLikes[index - 1] });
- }
- };
-
- selectNext = () => {
- const selected = this.getSelected();
- 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] });
- }
- };
-
- handleSelect = (branchLike: T.BranchLike) => {
- this.setState({ selected: branchLike });
- };
-
- getSelected = () => {
- if (this.state.selected) {
- return this.state.selected;
- }
-
- const branchLikes = this.getFilteredBranchLikes();
- if (branchLikes.find(b => isSameBranchLike(b, this.props.currentBranchLike))) {
- return this.props.currentBranchLike;
- }
-
- if (branchLikes.length > 0) {
- return branchLikes[0];
- }
-
- return undefined;
- };
-
- getProjectBranchUrl = (branchLike: T.BranchLike) =>
- getBranchLikeUrl(this.props.component.key, branchLike);
-
- isOrphan = (branchLike: T.BranchLike) => {
- return (isShortLivingBranch(branchLike) || isPullRequest(branchLike)) && branchLike.isOrphan;
- };
-
- renderSearch = () => (
- <div className="menu-search">
- <SearchBox
- autoFocus={true}
- onChange={this.handleSearchChange}
- onKeyDown={this.handleKeyDown}
- placeholder={translate('branches.search_for_branches')}
- value={this.state.query}
- />
- </div>
- );
-
- renderBranchesList = () => {
- const branchLikes = this.getFilteredBranchLikes();
- const selected = this.getSelected();
-
- if (branchLikes.length === 0) {
- return <div className="menu-message note">{translate('no_results')}</div>;
- }
-
- 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);
- const isSelected = isSameBranchLike(branchLike, selected);
- return (
- <React.Fragment key={getBranchLikeKey(branchLike)}>
- {showDivider && <li className="divider" />}
- {showOrphanHeader && (
- <li className="menu-header">
- <div className="display-inline-block text-middle">
- {translate('branches.orphan_branches')}
- </div>
- <HelpTooltip
- className="spacer-left"
- overlay={translate('branches.orphan_branches.tooltip')}
- />
- </li>
- )}
- {showPullRequestHeader && (
- <li className="menu-header navbar-context-meta-branch-menu-title">
- {translate('branches.pull_requests')}
- </li>
- )}
- {showShortLivingBranchHeader && (
- <li className="menu-header navbar-context-meta-branch-menu-title">
- {translate('branches.short_lived_branches')}
- </li>
- )}
- <ComponentNavBranchesMenuItem
- branchLike={branchLike}
- component={this.props.component}
- innerRef={node =>
- (this.selectedBranchNode = isSelected ? node : this.selectedBranchNode)
- }
- key={getBranchLikeKey(branchLike)}
- onSelect={this.handleSelect}
- selected={isSelected}
- />
- </React.Fragment>
- );
- });
-
- return (
- <ul className="menu menu-vertically-limited" ref={node => (this.listNode = node)}>
- {items}
- </ul>
- );
- };
-
- render() {
- const { component } = this.props;
- const showManageLink =
- component.qualifier === 'TRK' &&
- component.configuration &&
- component.configuration.showSettings;
-
- return (
- <DropdownOverlay noPadding={true}>
- {this.renderSearch()}
- {this.renderBranchesList()}
- {showManageLink && (
- <div className="dropdown-bottom-hint text-right">
- <Link
- className="text-muted"
- to={{ pathname: '/project/branches', query: { id: component.key } }}>
- {translate('branches.manage')}
- </Link>
- </div>
- )}
- </DropdownOverlay>
- );
- }
-}
-
-export default withRouter(ComponentNavBranchesMenu);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx
deleted file mode 100644
index d4f76e2761f..00000000000
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 classNames from 'classnames';
-import * as React from 'react';
-import { Link } from 'react-router';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import BranchStatus from '../../../../components/common/BranchStatus';
-import BranchIcon from '../../../../components/icons-components/BranchIcon';
-import {
- getBranchLikeDisplayName,
- getBranchLikeKey,
- isMainBranch,
- isPullRequest,
- isShortLivingBranch
-} from '../../../../helpers/branches';
-import { getBranchLikeUrl } from '../../../../helpers/urls';
-
-export interface Props {
- branchLike: T.BranchLike;
- component: T.Component;
- onSelect: (branchLike: T.BranchLike) => void;
- selected: boolean;
- innerRef?: (node: HTMLLIElement) => void;
-}
-
-export default function ComponentNavBranchesMenuItem({ branchLike, ...props }: Props) {
- const handleMouseEnter = () => {
- props.onSelect(branchLike);
- };
-
- const displayName = getBranchLikeDisplayName(branchLike);
- const shouldBeIndented =
- (isShortLivingBranch(branchLike) && !branchLike.isOrphan) || isPullRequest(branchLike);
-
- return (
- <li key={getBranchLikeKey(branchLike)} onMouseEnter={handleMouseEnter} ref={props.innerRef}>
- <Link
- className={classNames('navbar-context-meta-branch-menu-item', {
- active: props.selected
- })}
- to={getBranchLikeUrl(props.component.key, branchLike)}>
- <div
- className="navbar-context-meta-branch-menu-item-name text-ellipsis"
- title={displayName}>
- <BranchIcon
- branchLike={branchLike}
- className={classNames('little-spacer-right', { 'big-spacer-left': shouldBeIndented })}
- />
- {displayName}
- {isMainBranch(branchLike) && (
- <div className="badge spacer-left">{translate('branches.main_branch')}</div>
- )}
- </div>
- <div className="big-spacer-left note">
- <BranchStatus branchLike={branchLike} component={props.component.key} />
- </div>
- </Link>
- </li>
- );
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx
index 457bb69ea70..93d15ab572e 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx
@@ -18,111 +18,38 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { connect } from 'react-redux';
-import { Link } from 'react-router';
-import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon';
-import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
-import OrganizationAvatar from '../../../../components/common/OrganizationAvatar';
-import OrganizationHelmet from '../../../../components/common/OrganizationHelmet';
-import OrganizationLink from '../../../../components/ui/OrganizationLink';
-import { sanitizeAlmId } from '../../../../helpers/almIntegrations';
-import { isMainBranch } from '../../../../helpers/branches';
-import { isSonarCloud } from '../../../../helpers/system';
-import { getProjectUrl } from '../../../../helpers/urls';
-import { getOrganizationByKey, Store } from '../../../../store/rootReducer';
-import ComponentNavBranch from './ComponentNavBranch';
+import Helmet from 'react-helmet';
+import BranchLikeNavigation from './branch-like/BranchLikeNavigation';
+import CurrentBranchLikeMergeInformation from './branch-like/CurrentBranchLikeMergeInformation';
+import { ComponentBreadcrumb } from './ComponentBreadcrumb';
-interface StateProps {
- organization?: T.Organization;
-}
-
-interface OwnProps {
+export interface ComponentNavHeaderProps {
branchLikes: T.BranchLike[];
component: T.Component;
currentBranchLike: T.BranchLike | undefined;
- location?: any;
}
-type Props = StateProps & OwnProps;
-
-export function ComponentNavHeader(props: Props) {
- const { component, organization } = props;
+export function ComponentNavHeader(props: ComponentNavHeaderProps) {
+ const { branchLikes, component, currentBranchLike } = props;
return (
- <header className="navbar-context-header">
- <OrganizationHelmet
- organization={organization && isSonarCloud() ? organization : undefined}
- title={component.name}
- />
- {organization && isSonarCloud() && (
- <>
- <OrganizationAvatar organization={organization} />
- <OrganizationLink
- className="navbar-context-header-breadcrumb-link link-base-color link-no-underline spacer-left"
- organization={organization}>
- {organization.name}
- </OrganizationLink>
- <span className="slash-separator" />
- </>
- )}
- {renderBreadcrumbs(
- component.breadcrumbs,
- props.currentBranchLike !== undefined && !isMainBranch(props.currentBranchLike)
- )}
- {isSonarCloud() && component.alm && (
- <a
- className="link-no-underline"
- href={component.alm.url}
- rel="noopener noreferrer"
- target="_blank">
- <img
- alt={sanitizeAlmId(component.alm.key)}
- className="text-text-top spacer-left"
- height={16}
- src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(component.alm.key)}.svg`}
- width={16}
- />
- </a>
- )}
- {props.currentBranchLike && (
- <ComponentNavBranch
- branchLikes={props.branchLikes}
- component={component}
- currentBranchLike={props.currentBranchLike}
- // to close dropdown on any location change
- location={props.location}
- />
- )}
- </header>
- );
-}
-
-function renderBreadcrumbs(breadcrumbs: T.Breadcrumb[], shouldLinkLast: boolean) {
- const lastItem = breadcrumbs[breadcrumbs.length - 1];
- return breadcrumbs.map((item, index) => {
- return (
- <React.Fragment key={item.key}>
- {index === 0 && <QualifierIcon className="spacer-right" qualifier={lastItem.qualifier} />}
- {shouldLinkLast || index < breadcrumbs.length - 1 ? (
- <Link
- className="navbar-context-header-breadcrumb-link link-base-color link-no-underline"
- title={item.name}
- to={getProjectUrl(item.key)}>
- {item.name}
- </Link>
- ) : (
- <span className="navbar-context-header-breadcrumb-link" title={item.name}>
- {item.name}
- </span>
+ <>
+ <Helmet title={component.name} />
+ <header className="display-flex-center flex-shrink">
+ <ComponentBreadcrumb component={component} currentBranchLike={currentBranchLike} />
+ {currentBranchLike && (
+ <>
+ <BranchLikeNavigation
+ branchLikes={branchLikes}
+ component={component}
+ currentBranchLike={currentBranchLike}
+ />
+ <CurrentBranchLikeMergeInformation currentBranchLike={currentBranchLike} />
+ </>
)}
- {index < breadcrumbs.length - 1 && <span className="slash-separator" />}
- </React.Fragment>
- );
- });
+ </header>
+ </>
+ );
}
-const mapStateToProps = (state: Store, ownProps: OwnProps): StateProps => ({
- organization: getOrganizationByKey(state, ownProps.component.organization)
-});
-
-export default connect(mapStateToProps)(ComponentNavHeader);
+export default React.memo(ComponentNavHeader);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentBreadcrumb-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentBreadcrumb-test.tsx
new file mode 100644
index 00000000000..3dde40f6f04
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentBreadcrumb-test.tsx
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { mockComponent, mockMainBranch } from '../../../../../helpers/testMocks';
+import { ComponentQualifier } from '../../../../../types/component';
+import { ComponentBreadcrumb } from '../ComponentBreadcrumb';
+
+it('should render correctly', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+});
+
+function shallowRender() {
+ return shallow(
+ <ComponentBreadcrumb
+ component={mockComponent({
+ breadcrumbs: [
+ {
+ key: 'parent-portfolio',
+ name: 'parent-portfolio',
+ qualifier: ComponentQualifier.Portfolio
+ },
+ {
+ key: 'child-portfolio',
+ name: 'child-portfolio',
+ qualifier: ComponentQualifier.SubPortfolio
+ }
+ ]
+ })}
+ currentBranchLike={mockMainBranch()}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
deleted file mode 100644
index b7761bf3141..00000000000
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { click } from 'sonar-ui-common/helpers/testUtils';
-import { isSonarCloud } from '../../../../../helpers/system';
-import {
- mockLongLivingBranch,
- mockMainBranch,
- mockPullRequest,
- mockShortLivingBranch
-} from '../../../../../helpers/testMocks';
-import { ComponentNavBranch } from '../ComponentNavBranch';
-
-jest.mock('../../../../../helpers/system', () => ({ isSonarCloud: jest.fn() }));
-
-const mainBranch = mockMainBranch();
-const fooBranch = mockLongLivingBranch();
-
-beforeEach(() => {
- (isSonarCloud as jest.Mock).mockImplementation(() => false);
-});
-
-it('renders main branch', () => {
- const component = {} as T.Component;
- expect(
- shallow(
- <ComponentNavBranch
- appState={{ branchesEnabled: true }}
- branchLikes={[mainBranch, fooBranch]}
- component={component}
- currentBranchLike={mainBranch}
- />
- )
- ).toMatchSnapshot();
-});
-
-it('renders short-living branch', () => {
- const branch: T.ShortLivingBranch = mockShortLivingBranch({
- status: { qualityGateStatus: 'OK' }
- });
- const component = {} as T.Component;
- expect(
- shallow(
- <ComponentNavBranch
- appState={{ branchesEnabled: true }}
- branchLikes={[branch, fooBranch]}
- component={component}
- currentBranchLike={branch}
- />
- )
- ).toMatchSnapshot();
-});
-
-it('renders pull request', () => {
- const pullRequest = mockPullRequest({
- target: 'feature/foo',
- url: 'https://example.com/pull/1234'
- });
- const component = {} as T.Component;
- expect(
- shallow(
- <ComponentNavBranch
- appState={{ branchesEnabled: true }}
- branchLikes={[pullRequest, fooBranch]}
- component={component}
- currentBranchLike={pullRequest}
- />
- )
- ).toMatchSnapshot();
-});
-
-it('opens menu', () => {
- const component = {} as T.Component;
- const wrapper = shallow(
- <ComponentNavBranch
- appState={{ branchesEnabled: true }}
- branchLikes={[mainBranch, fooBranch]}
- component={component}
- currentBranchLike={mainBranch}
- />
- );
- expect(wrapper.find('Toggler').prop('open')).toBe(false);
- click(wrapper.find('a'));
- expect(wrapper.find('Toggler').prop('open')).toBe(true);
-});
-
-it('renders single branch popup', () => {
- const component = {} as T.Component;
- const wrapper = shallow(
- <ComponentNavBranch
- appState={{ branchesEnabled: true }}
- branchLikes={[mainBranch]}
- component={component}
- currentBranchLike={mainBranch}
- />
- );
- expect(wrapper.find('DocTooltip')).toMatchSnapshot();
-});
-
-it('renders no branch support popup', () => {
- const component = {} as T.Component;
- const wrapper = shallow(
- <ComponentNavBranch
- appState={{ branchesEnabled: false }}
- branchLikes={[mainBranch, fooBranch]}
- component={component}
- currentBranchLike={mainBranch}
- />
- );
- expect(wrapper.find('DocTooltip')).toMatchSnapshot();
-});
-
-it('renders nothing on SonarCloud without branch support', () => {
- (isSonarCloud as jest.Mock).mockImplementation(() => true);
- const component = {} as T.Component;
- const wrapper = shallow(
- <ComponentNavBranch
- appState={{ branchesEnabled: false }}
- branchLikes={[mainBranch]}
- component={component}
- currentBranchLike={mainBranch}
- />
- );
- expect(wrapper.type()).toBeNull();
-});
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx
deleted file mode 100644
index 54be1a41132..00000000000
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { elementKeydown } from 'sonar-ui-common/helpers/testUtils';
-import {
- mockLongLivingBranch,
- mockMainBranch,
- mockPullRequest,
- mockShortLivingBranch
-} from '../../../../../helpers/testMocks';
-import { ComponentNavBranchesMenu } from '../ComponentNavBranchesMenu';
-
-const component = { key: 'component' } as T.Component;
-
-it('renders list', () => {
- expect(
- shallow(
- <ComponentNavBranchesMenu
- branchLikes={[
- mockMainBranch(),
- shortBranch('foo'),
- mockLongLivingBranch({ name: 'bar' }),
- shortBranch('baz', true),
- mockPullRequest({ status: { qualityGateStatus: 'OK' }, title: 'qux' })
- ]}
- component={component}
- currentBranchLike={mockMainBranch()}
- onClose={jest.fn()}
- router={{ push: jest.fn() }}
- />
- )
- ).toMatchSnapshot();
-});
-
-it('searches', () => {
- const wrapper = shallow(
- <ComponentNavBranchesMenu
- branchLikes={[
- mockMainBranch(),
- shortBranch('foo'),
- shortBranch('foobar'),
- mockLongLivingBranch({ name: 'bar' }),
- mockLongLivingBranch({ name: 'BARBAZ' })
- ]}
- component={component}
- currentBranchLike={mockMainBranch()}
- onClose={jest.fn()}
- router={{ push: jest.fn() }}
- />
- );
- wrapper.setState({ query: 'bar' });
- expect(wrapper).toMatchSnapshot();
-});
-
-it('selects next & previous', () => {
- const wrapper = shallow<ComponentNavBranchesMenu>(
- <ComponentNavBranchesMenu
- branchLikes={[
- mockMainBranch(),
- shortBranch('foo'),
- shortBranch('foobar'),
- mockLongLivingBranch({ name: 'bar' })
- ]}
- component={component}
- currentBranchLike={mockMainBranch()}
- onClose={jest.fn()}
- router={{ push: jest.fn() }}
- />
- );
- elementKeydown(wrapper.find('SearchBox'), 40);
- wrapper.update();
- expect(wrapper.state().selected).toEqual(shortBranch('foo'));
- elementKeydown(wrapper.find('SearchBox'), 40);
- wrapper.update();
- expect(wrapper.state().selected).toEqual(shortBranch('foobar'));
- elementKeydown(wrapper.find('SearchBox'), 38);
- wrapper.update();
- expect(wrapper.state().selected).toEqual(shortBranch('foo'));
-});
-
-function shortBranch(name: string, isOrphan?: true) {
- return mockShortLivingBranch({ name, isOrphan, status: { qualityGateStatus: 'OK' } });
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx
index acd86faf934..285598465d5 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx
@@ -17,69 +17,27 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { shallow } from 'enzyme';
import * as React from 'react';
-import { isSonarCloud } from '../../../../../helpers/system';
-import { ComponentNavHeader } from '../ComponentNavHeader';
-
-jest.mock('../../../../../helpers/system', () => ({
- isSonarCloud: jest.fn().mockReturnValue(false)
-}));
-
-const component: T.Component = {
- breadcrumbs: [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }],
- key: 'my-project',
- name: 'My Project',
- organization: 'foo',
- qualifier: 'TRK',
- visibility: 'public'
-};
-
-const organization: T.Organization = {
- key: 'foo',
- name: 'The Foo Organization',
- projectVisibility: 'public'
-};
+import { mockSetOfBranchAndPullRequest } from '../../../../../helpers/mocks/branch-pull-request';
+import { mockComponent } from '../../../../../helpers/testMocks';
+import { ComponentNavHeader, ComponentNavHeaderProps } from '../ComponentNavHeader';
-beforeEach(() => {
- (isSonarCloud as jest.Mock<any>).mockReturnValue(false);
+it('should render correctly', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
});
-it('should not render breadcrumbs with one element', () => {
- expect(
- shallow(
- <ComponentNavHeader branchLikes={[]} component={component} currentBranchLike={undefined} />
- )
- ).toMatchSnapshot();
-});
-
-it('should render organization', () => {
- (isSonarCloud as jest.Mock<any>).mockReturnValue(true);
- expect(
- shallow(
- <ComponentNavHeader
- branchLikes={[]}
- component={component}
- currentBranchLike={undefined}
- organization={organization}
- />
- )
- ).toMatchSnapshot();
-});
+function shallowRender(props?: Partial<ComponentNavHeaderProps>) {
+ const branchLikes = mockSetOfBranchAndPullRequest();
-it('should render alm links', () => {
- (isSonarCloud as jest.Mock<any>).mockReturnValue(true);
- expect(
- shallow(
- <ComponentNavHeader
- branchLikes={[]}
- component={{
- ...component,
- alm: { key: 'bitbucketcloud', url: 'https://bitbucket.org/foo' }
- }}
- currentBranchLike={undefined}
- organization={organization}
- />
- )
- ).toMatchSnapshot();
-});
+ return shallow(
+ <ComponentNavHeader
+ branchLikes={branchLikes}
+ component={mockComponent()}
+ currentBranchLike={branchLikes[0]}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentBreadcrumb-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentBreadcrumb-test.tsx.snap
new file mode 100644
index 00000000000..314c7e511a6
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentBreadcrumb-test.tsx.snap
@@ -0,0 +1,48 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+ className="big flex-shrink display-flex-center"
+>
+ <span
+ className="flex-shrink display-flex-center"
+ key="parent-portfolio"
+ >
+ <QualifierIcon
+ className="spacer-right"
+ qualifier="SVW"
+ />
+ <Link
+ className="link-no-underline text-ellipsis"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ title="parent-portfolio"
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": undefined,
+ "id": "parent-portfolio",
+ },
+ }
+ }
+ >
+ parent-portfolio
+ </Link>
+ <span
+ className="slash-separator"
+ />
+ </span>
+ <span
+ className="flex-shrink display-flex-center"
+ key="child-portfolio"
+ >
+ <span
+ className="text-ellipsis"
+ title="child-portfolio"
+ >
+ child-portfolio
+ </span>
+ </span>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
index 65d8dd19a62..8052b0bf3bd 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
@@ -8,7 +8,7 @@ exports[`renders 1`] = `
<div
className="navbar-context-justified"
>
- <Connect(ComponentNavHeader)
+ <Memo(ComponentNavHeader)
branchLikes={Array []}
component={
Object {
@@ -25,7 +25,6 @@ exports[`renders 1`] = `
"qualifier": "TRK",
}
}
- location={Object {}}
/>
<Connect(ComponentNavMeta)
component={
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
deleted file mode 100644
index 41d56c5bb58..00000000000
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
+++ /dev/null
@@ -1,286 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders main branch 1`] = `
-<div
- className="navbar-context-branches"
->
- <div
- className="dropdown"
- >
- <Toggler
- onRequestClose={[Function]}
- open={false}
- overlay={
- <withRouter(ComponentNavBranchesMenu)
- branchLikes={
- Array [
- Object {
- "analysisDate": "2018-01-01",
- "isMain": true,
- "name": "master",
- },
- Object {
- "analysisDate": "2018-01-01",
- "isMain": false,
- "name": "branch-6.7",
- "type": "LONG",
- },
- ]
- }
- component={Object {}}
- currentBranchLike={
- Object {
- "analysisDate": "2018-01-01",
- "isMain": true,
- "name": "master",
- }
- }
- onClose={[Function]}
- />
- }
- >
- <a
- className="link-base-color link-no-underline nowrap"
- href="#"
- onClick={[Function]}
- >
- <BranchIcon
- branchLike={
- Object {
- "analysisDate": "2018-01-01",
- "isMain": true,
- "name": "master",
- }
- }
- className="little-spacer-right"
- />
- <span
- className="text-limited text-top"
- title="master"
- >
- master
- </span>
- <DropdownIcon
- className="little-spacer-left"
- />
- </a>
- </Toggler>
- </div>
-</div>
-`;
-
-exports[`renders no branch support popup 1`] = `
-<DocTooltip
- className="spacer-left"
- doc={Promise {}}
->
- <PlusCircleIcon
- fill="#b4b4b4"
- size={12}
- />
-</DocTooltip>
-`;
-
-exports[`renders pull request 1`] = `
-<div
- className="navbar-context-branches"
->
- <div
- className="dropdown"
- >
- <Toggler
- onRequestClose={[Function]}
- open={false}
- overlay={
- <withRouter(ComponentNavBranchesMenu)
- branchLikes={
- Array [
- Object {
- "analysisDate": "2018-01-01",
- "base": "master",
- "branch": "feature/foo/bar",
- "key": "1001",
- "target": "feature/foo",
- "title": "Foo Bar feature",
- "url": "https://example.com/pull/1234",
- },
- Object {
- "analysisDate": "2018-01-01",
- "isMain": false,
- "name": "branch-6.7",
- "type": "LONG",
- },
- ]
- }
- component={Object {}}
- currentBranchLike={
- Object {
- "analysisDate": "2018-01-01",
- "base": "master",
- "branch": "feature/foo/bar",
- "key": "1001",
- "target": "feature/foo",
- "title": "Foo Bar feature",
- "url": "https://example.com/pull/1234",
- }
- }
- onClose={[Function]}
- />
- }
- >
- <a
- className="link-base-color link-no-underline nowrap"
- href="#"
- onClick={[Function]}
- >
- <BranchIcon
- branchLike={
- Object {
- "analysisDate": "2018-01-01",
- "base": "master",
- "branch": "feature/foo/bar",
- "key": "1001",
- "target": "feature/foo",
- "title": "Foo Bar feature",
- "url": "https://example.com/pull/1234",
- }
- }
- className="little-spacer-right"
- />
- <span
- className="text-limited text-top"
- title="1001 – Foo Bar feature"
- >
- 1001 – Foo Bar feature
- </span>
- <DropdownIcon
- className="little-spacer-left"
- />
- </a>
- </Toggler>
- </div>
- <span
- className="note big-spacer-left text-ellipsis flex-shrink"
- >
- <FormattedMessage
- defaultMessage="branches.pull_request.for_merge_into_x_from_y"
- id="branches.pull_request.for_merge_into_x_from_y"
- values={
- Object {
- "branch": <strong>
- feature/foo/bar
- </strong>,
- "target": <strong>
- feature/foo
- </strong>,
- }
- }
- />
- </span>
-</div>
-`;
-
-exports[`renders short-living branch 1`] = `
-<div
- className="navbar-context-branches"
->
- <div
- className="dropdown"
- >
- <Toggler
- onRequestClose={[Function]}
- open={false}
- overlay={
- <withRouter(ComponentNavBranchesMenu)
- branchLikes={
- Array [
- Object {
- "analysisDate": "2018-01-01",
- "isMain": false,
- "mergeBranch": "master",
- "name": "feature/foo",
- "status": Object {
- "qualityGateStatus": "OK",
- },
- "type": "SHORT",
- },
- Object {
- "analysisDate": "2018-01-01",
- "isMain": false,
- "name": "branch-6.7",
- "type": "LONG",
- },
- ]
- }
- component={Object {}}
- currentBranchLike={
- Object {
- "analysisDate": "2018-01-01",
- "isMain": false,
- "mergeBranch": "master",
- "name": "feature/foo",
- "status": Object {
- "qualityGateStatus": "OK",
- },
- "type": "SHORT",
- }
- }
- onClose={[Function]}
- />
- }
- >
- <a
- className="link-base-color link-no-underline nowrap"
- href="#"
- onClick={[Function]}
- >
- <BranchIcon
- branchLike={
- Object {
- "analysisDate": "2018-01-01",
- "isMain": false,
- "mergeBranch": "master",
- "name": "feature/foo",
- "status": Object {
- "qualityGateStatus": "OK",
- },
- "type": "SHORT",
- }
- }
- className="little-spacer-right"
- />
- <span
- className="text-limited text-top"
- title="feature/foo"
- >
- feature/foo
- </span>
- <DropdownIcon
- className="little-spacer-left"
- />
- </a>
- </Toggler>
- </div>
- <span
- className="note big-spacer-left"
- >
- from
-
- <strong>
- master
- </strong>
- </span>
-</div>
-`;
-
-exports[`renders single branch popup 1`] = `
-<DocTooltip
- className="spacer-left"
- doc={Promise {}}
->
- <PlusCircleIcon
- fill="#4b9fd5"
- size={12}
- />
-</DocTooltip>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
deleted file mode 100644
index 561e853fce9..00000000000
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
+++ /dev/null
@@ -1,291 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders list 1`] = `
-<DropdownOverlay
- noPadding={true}
->
- <div
- className="menu-search"
- >
- <SearchBox
- autoFocus={true}
- onChange={[Function]}
- onKeyDown={[Function]}
- placeholder="branches.search_for_branches"
- value=""
- />
- </div>
- <ul
- className="menu menu-vertically-limited"
- >
- <ComponentNavBranchesMenuItem
- branchLike={
- Object {
- "analysisDate": "2018-01-01",
- "isMain": true,
- "name": "master",
- }
- }
- component={
- Object {
- "key": "component",
- }
- }
- innerRef={[Function]}
- key="branch-master"
- onSelect={[Function]}
- selected={true}
- />
- <li
- className="menu-header navbar-context-meta-branch-menu-title"
- >
- branches.pull_requests
- </li>
- <ComponentNavBranchesMenuItem
- branchLike={
- Object {
- "analysisDate": "2018-01-01",
- "base": "master",
- "branch": "feature/foo/bar",
- "key": "1001",
- "status": Object {
- "qualityGateStatus": "OK",
- },
- "target": "master",
- "title": "qux",
- }
- }
- component={
- Object {
- "key": "component",
- }
- }
- innerRef={[Function]}
- key="pull-request-1001"
- onSelect={[Function]}
- selected={false}
- />
- <li
- className="divider"
- />
- <li
- className="menu-header"
- >
- <div
- className="display-inline-block text-middle"
- >
- branches.orphan_branches
- </div>
- <HelpTooltip
- className="spacer-left"
- overlay="branches.orphan_branches.tooltip"
- />
- </li>
- <ComponentNavBranchesMenuItem
- branchLike={
- Object {
- "analysisDate": "2018-01-01",
- "isMain": false,
- "isOrphan": true,
- "mergeBranch": "master",
- "name": "baz",
- "status": Object {
- "qualityGateStatus": "OK",
- },
- "type": "SHORT",
- }
- }
- component={
- Object {
- "key": "component",
- }
- }
- innerRef={[Function]}
- key="branch-baz"
- onSelect={[Function]}
- selected={false}
- />
- <ComponentNavBranchesMenuItem
- branchLike={
- Object {
- "analysisDate": "2018-01-01",
- "isMain": false,
- "isOrphan": undefined,
- "mergeBranch": "master",
- "name": "foo",
- "status": Object {
- "qualityGateStatus": "OK",
- },
- "type": "SHORT",
- }
- }
- component={
- Object {
- "key": "component",
- }
- }
- innerRef={[Function]}
- key="branch-foo"
- onSelect={[Function]}
- selected={false}
- />
- <li
- className="divider"
- />
- <ComponentNavBranchesMenuItem
- branchLike={
- Object {
- "analysisDate": "2018-01-01",
- "isMain": false,
- "name": "bar",
- "type": "LONG",
- }
- }
- component={
- Object {
- "key": "component",
- }
- }
- innerRef={[Function]}
- key="branch-bar"
- onSelect={[Function]}
- selected={false}
- />
- <li
- className="divider"
- />
- <li
- className="menu-header"
- >
- <div
- className="display-inline-block text-middle"
- >
- branches.orphan_branches
- </div>
- <HelpTooltip
- className="spacer-left"
- overlay="branches.orphan_branches.tooltip"
- />
- </li>
- <ComponentNavBranchesMenuItem
- branchLike={
- Object {
- "analysisDate": "2018-01-01",
- "isMain": false,
- "isOrphan": true,
- "mergeBranch": "master",
- "name": "baz",
- "status": Object {
- "qualityGateStatus": "OK",
- },
- "type": "SHORT",
- }
- }
- component={
- Object {
- "key": "component",
- }
- }
- innerRef={[Function]}
- key="branch-baz"
- onSelect={[Function]}
- selected={false}
- />
- </ul>
-</DropdownOverlay>
-`;
-
-exports[`searches 1`] = `
-<DropdownOverlay
- noPadding={true}
->
- <div
- className="menu-search"
- >
- <SearchBox
- autoFocus={true}
- onChange={[Function]}
- onKeyDown={[Function]}
- placeholder="branches.search_for_branches"
- value="bar"
- />
- </div>
- <ul
- className="menu menu-vertically-limited"
- >
- <li
- className="menu-header navbar-context-meta-branch-menu-title"
- >
- branches.short_lived_branches
- </li>
- <ComponentNavBranchesMenuItem
- branchLike={
- Object {
- "analysisDate": "2018-01-01",
- "isMain": false,
- "isOrphan": undefined,
- "mergeBranch": "master",
- "name": "foobar",
- "status": Object {
- "qualityGateStatus": "OK",
- },
- "type": "SHORT",
- }
- }
- component={
- Object {
- "key": "component",
- }
- }
- innerRef={[Function]}
- key="branch-foobar"
- onSelect={[Function]}
- selected={true}
- />
- <li
- className="divider"
- />
- <ComponentNavBranchesMenuItem
- branchLike={
- Object {
- "analysisDate": "2018-01-01",
- "isMain": false,
- "name": "BARBAZ",
- "type": "LONG",
- }
- }
- component={
- Object {
- "key": "component",
- }
- }
- innerRef={[Function]}
- key="branch-BARBAZ"
- onSelect={[Function]}
- selected={false}
- />
- <li
- className="divider"
- />
- <ComponentNavBranchesMenuItem
- branchLike={
- Object {
- "analysisDate": "2018-01-01",
- "isMain": false,
- "name": "bar",
- "type": "LONG",
- }
- }
- component={
- Object {
- "key": "component",
- }
- }
- innerRef={[Function]}
- key="branch-bar"
- onSelect={[Function]}
- selected={false}
- />
- </ul>
-</DropdownOverlay>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap
deleted file mode 100644
index 9f65d7011e0..00000000000
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap
+++ /dev/null
@@ -1,181 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders main branch 1`] = `
-<li
- key="branch-master"
- onMouseEnter={[Function]}
->
- <Link
- className="navbar-context-meta-branch-menu-item"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/dashboard",
- "query": Object {
- "branch": undefined,
- "id": "component",
- },
- }
- }
- >
- <div
- className="navbar-context-meta-branch-menu-item-name text-ellipsis"
- title="master"
- >
- <BranchIcon
- branchLike={
- Object {
- "isMain": true,
- "name": "master",
- }
- }
- className="little-spacer-right"
- />
- master
- <div
- className="badge spacer-left"
- >
- branches.main_branch
- </div>
- </div>
- <div
- className="big-spacer-left note"
- >
- <Connect(BranchStatus)
- branchLike={
- Object {
- "isMain": true,
- "name": "master",
- }
- }
- component="component"
- />
- </div>
- </Link>
-</li>
-`;
-
-exports[`renders short-living branch 1`] = `
-<li
- key="branch-foo"
- onMouseEnter={[Function]}
->
- <Link
- className="navbar-context-meta-branch-menu-item"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/dashboard",
- "query": Object {
- "branch": "foo",
- "id": "component",
- },
- }
- }
- >
- <div
- className="navbar-context-meta-branch-menu-item-name text-ellipsis"
- title="foo"
- >
- <BranchIcon
- branchLike={
- Object {
- "isMain": false,
- "mergeBranch": "master",
- "name": "foo",
- "status": Object {
- "qualityGateStatus": "ERROR",
- },
- "type": "SHORT",
- }
- }
- className="little-spacer-right big-spacer-left"
- />
- foo
- </div>
- <div
- className="big-spacer-left note"
- >
- <Connect(BranchStatus)
- branchLike={
- Object {
- "isMain": false,
- "mergeBranch": "master",
- "name": "foo",
- "status": Object {
- "qualityGateStatus": "ERROR",
- },
- "type": "SHORT",
- }
- }
- component="component"
- />
- </div>
- </Link>
-</li>
-`;
-
-exports[`renders short-living orhpan branch 1`] = `
-<li
- key="branch-foo"
- onMouseEnter={[Function]}
->
- <Link
- className="navbar-context-meta-branch-menu-item"
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/dashboard",
- "query": Object {
- "branch": "foo",
- "id": "component",
- },
- }
- }
- >
- <div
- className="navbar-context-meta-branch-menu-item-name text-ellipsis"
- title="foo"
- >
- <BranchIcon
- branchLike={
- Object {
- "isMain": false,
- "isOrphan": true,
- "mergeBranch": "master",
- "name": "foo",
- "status": Object {
- "qualityGateStatus": "ERROR",
- },
- "type": "SHORT",
- }
- }
- className="little-spacer-right"
- />
- foo
- </div>
- <div
- className="big-spacer-left note"
- >
- <Connect(BranchStatus)
- branchLike={
- Object {
- "isMain": false,
- "isOrphan": true,
- "mergeBranch": "master",
- "name": "foo",
- "status": Object {
- "qualityGateStatus": "ERROR",
- },
- "type": "SHORT",
- }
- }
- component="component"
- />
- </div>
- </Link>
-</li>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap
index eea61301c22..341033a7752 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap
@@ -1,137 +1,160 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should not render breadcrumbs with one element 1`] = `
-<header
- className="navbar-context-header"
->
- <OrganizationHelmet
- title="My Project"
+exports[`should render correctly 1`] = `
+<Fragment>
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="MyProject"
/>
- <QualifierIcon
- className="spacer-right"
- qualifier="TRK"
- />
- <span
- className="navbar-context-header-breadcrumb-link"
- title="My Project"
+ <header
+ className="display-flex-center flex-shrink"
>
- My Project
- </span>
-</header>
-`;
-
-exports[`should render alm links 1`] = `
-<header
- className="navbar-context-header"
->
- <OrganizationHelmet
- organization={
- Object {
- "key": "foo",
- "name": "The Foo Organization",
- "projectVisibility": "public",
+ <ComponentBreadcrumb
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
}
- }
- title="My Project"
- />
- <OrganizationAvatar
- organization={
- Object {
- "key": "foo",
- "name": "The Foo Organization",
- "projectVisibility": "public",
- }
- }
- />
- <OrganizationLink
- className="navbar-context-header-breadcrumb-link link-base-color link-no-underline spacer-left"
- organization={
- Object {
- "key": "foo",
- "name": "The Foo Organization",
- "projectVisibility": "public",
+ currentBranchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "slb-1",
+ "type": "SHORT",
+ }
}
- }
- >
- The Foo Organization
- </OrganizationLink>
- <span
- className="slash-separator"
- />
- <QualifierIcon
- className="spacer-right"
- qualifier="TRK"
- />
- <span
- className="navbar-context-header-breadcrumb-link"
- title="My Project"
- >
- My Project
- </span>
- <a
- className="link-no-underline"
- href="https://bitbucket.org/foo"
- rel="noopener noreferrer"
- target="_blank"
- >
- <img
- alt="bitbucket"
- className="text-text-top spacer-left"
- height={16}
- src="/images/sonarcloud/bitbucket.svg"
- width={16}
/>
- </a>
-</header>
-`;
-
-exports[`should render organization 1`] = `
-<header
- className="navbar-context-header"
->
- <OrganizationHelmet
- organization={
- Object {
- "key": "foo",
- "name": "The Foo Organization",
- "projectVisibility": "public",
+ <Connect(withAppState(Component))
+ branchLikes={
+ Array [
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "slb-1",
+ "type": "SHORT",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "name": "llb-1",
+ "type": "LONG",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "key": "1",
+ "target": "master",
+ "title": "PR-1",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "llb-1",
+ "name": "slb-2",
+ "type": "SHORT",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "key": "2",
+ "target": "master",
+ "title": "PR-2",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "name": "llb-3",
+ "type": "LONG",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "name": "llb-2",
+ "type": "LONG",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "isOrphan": true,
+ "key": "2",
+ "target": "llb-100",
+ "title": "PR-2",
+ },
+ ]
}
- }
- title="My Project"
- />
- <OrganizationAvatar
- organization={
- Object {
- "key": "foo",
- "name": "The Foo Organization",
- "projectVisibility": "public",
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
}
- }
- />
- <OrganizationLink
- className="navbar-context-header-breadcrumb-link link-base-color link-no-underline spacer-left"
- organization={
- Object {
- "key": "foo",
- "name": "The Foo Organization",
- "projectVisibility": "public",
+ currentBranchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "slb-1",
+ "type": "SHORT",
+ }
}
- }
- >
- The Foo Organization
- </OrganizationLink>
- <span
- className="slash-separator"
- />
- <QualifierIcon
- className="spacer-right"
- qualifier="TRK"
- />
- <span
- className="navbar-context-header-breadcrumb-link"
- title="My Project"
- >
- My Project
- </span>
-</header>
+ />
+ <Memo(CurrentBranchLikeMergeInformation)
+ currentBranchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "slb-1",
+ "type": "SHORT",
+ }
+ }
+ />
+ </header>
+</Fragment>
`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.css b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.css
new file mode 100644
index 00000000000..9b71f06942b
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.css
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.
+ */
+
+.branch-like-navigation-toggler-container .popup {
+ min-width: 430px;
+ max-width: 650px;
+}
+
+.branch-like-navigation-menu .search-box-container {
+ padding: var(--gridSize);
+}
+
+.branch-like-navigation-menu .search-box-container .search-box,
+.branch-like-navigation-menu .search-box-container .search-box-input {
+ max-width: initial !important;
+}
+
+.branch-like-navigation-menu .item-list {
+ padding-bottom: var(--gridSize);
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.branch-like-navigation-menu .item {
+ padding: calc(var(--gridSize) / 2) var(--gridSize);
+}
+
+.branch-like-navigation-menu .item.header {
+ color: var(--secondFontColor);
+}
+
+.branch-like-navigation-menu .item:not(.header):hover,
+.branch-like-navigation-menu .item:not(.header).active {
+ background-color: var(--barBackgroundColor);
+ cursor: pointer;
+}
+
+.branch-like-navigation-menu .hint-container {
+ padding: var(--gridSize);
+ background-color: var(--barBackgroundColor);
+ border-top: 1px solid var(--barBorderColor);
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
new file mode 100644
index 00000000000..8b92aa6dbb4
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 classNames from 'classnames';
+import * as React from 'react';
+import Toggler from 'sonar-ui-common/components/controls/Toggler';
+import { withAppState } from '../../../../../components/hoc/withAppState';
+import './BranchLikeNavigation.css';
+import CurrentBranchLike from './CurrentBranchLike';
+import Menu from './Menu';
+
+export interface BranchLikeNavigationProps {
+ appState: Pick<T.AppState, 'branchesEnabled'>;
+ branchLikes: T.BranchLike[];
+ component: T.Component;
+ currentBranchLike: T.BranchLike;
+}
+
+export function BranchLikeNavigation(props: BranchLikeNavigationProps) {
+ const {
+ appState: { branchesEnabled },
+ branchLikes,
+ component,
+ component: { configuration },
+ currentBranchLike
+ } = props;
+
+ const [isMenuOpen, setIsMenuOpen] = React.useState(false);
+
+ const canAdminComponent = configuration && configuration.showSettings;
+ const hasManyBranches = branchLikes.length >= 2;
+ const isMenuEnabled = branchesEnabled && hasManyBranches;
+
+ const currentBranchLikeElement = (
+ <CurrentBranchLike
+ branchesEnabled={Boolean(branchesEnabled)}
+ component={component}
+ currentBranchLike={currentBranchLike}
+ hasManyBranches={hasManyBranches}
+ />
+ );
+
+ return (
+ <span
+ className={classNames(
+ 'big-spacer-left flex-shrink branch-like-navigation-toggler-container',
+ { dropdown: isMenuEnabled }
+ )}>
+ {isMenuEnabled ? (
+ <Toggler
+ onRequestClose={() => setIsMenuOpen(false)}
+ open={isMenuOpen}
+ overlay={
+ <Menu
+ branchLikes={branchLikes}
+ canAdminComponent={canAdminComponent}
+ component={component}
+ currentBranchLike={currentBranchLike}
+ onClose={() => setIsMenuOpen(false)}
+ />
+ }>
+ <a
+ className="link-base-color link-no-underline"
+ href="#"
+ onClick={() => setIsMenuOpen(!isMenuOpen)}>
+ {currentBranchLikeElement}
+ </a>
+ </Toggler>
+ ) : (
+ currentBranchLikeElement
+ )}
+ </span>
+ );
+}
+
+export default withAppState(React.memo(BranchLikeNavigation));
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx
new file mode 100644
index 00000000000..acb2f334efe
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx
@@ -0,0 +1,114 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import * as React from 'react';
+import { Link } from 'react-router';
+import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
+import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon';
+import PlusCircleIcon from 'sonar-ui-common/components/icons/PlusCircleIcon';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import DocTooltip from '../../../../../components/docs/DocTooltip';
+import BranchLikeIcon from '../../../../../components/icons/BranchLikeIcon';
+import { getBranchLikeDisplayName } from '../../../../../helpers/branches';
+import { getPortfolioAdminUrl } from '../../../../../helpers/urls';
+import { ComponentQualifier } from '../../../../../types/component';
+import { colors } from '../../../../theme';
+
+export interface CurrentBranchLikeProps {
+ branchesEnabled: boolean;
+ component: T.Component;
+ currentBranchLike: T.BranchLike;
+ hasManyBranches: boolean;
+}
+
+export function CurrentBranchLike(props: CurrentBranchLikeProps) {
+ const {
+ branchesEnabled,
+ component,
+ component: { configuration },
+ currentBranchLike,
+ hasManyBranches
+ } = props;
+
+ const displayName = getBranchLikeDisplayName(currentBranchLike);
+ const isApplication = component.qualifier === ComponentQualifier.Application;
+ const canAdminComponent = configuration && configuration.showSettings;
+
+ const additionalIcon = () => {
+ const plusIcon = <PlusCircleIcon fill={colors.blue} size={12} />;
+
+ if (branchesEnabled && hasManyBranches) {
+ return <DropdownIcon />;
+ }
+
+ if (isApplication) {
+ if (!hasManyBranches && canAdminComponent) {
+ return (
+ <HelpTooltip
+ overlay={
+ <>
+ <p>{translate('application.branches.help')}</p>
+ <hr className="spacer-top spacer-bottom" />
+ <Link to={getPortfolioAdminUrl(component.key, component.qualifier)}>
+ {translate('application.branches.link')}
+ </Link>
+ </>
+ }>
+ {plusIcon}
+ </HelpTooltip>
+ );
+ }
+ } else {
+ if (!branchesEnabled) {
+ return (
+ <DocTooltip
+ data-test="branches-support-disabled"
+ doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/branches/no-branch-support.md')}>
+ {plusIcon}
+ </DocTooltip>
+ );
+ }
+
+ if (!hasManyBranches) {
+ return (
+ <DocTooltip
+ data-test="only-one-branch-like"
+ doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/branches/single-branch.md')}>
+ {plusIcon}
+ </DocTooltip>
+ );
+ }
+ }
+
+ return null;
+ };
+
+ return (
+ <span className="display-flex-center flex-shrink text-ellipsis">
+ <BranchLikeIcon branchLike={currentBranchLike} />
+ <span className="spacer-left spacer-right flex-shrink text-ellipsis" title={displayName}>
+ {displayName}
+ </span>
+ {additionalIcon()}
+ </span>
+ );
+}
+
+export default React.memo(CurrentBranchLike);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx
new file mode 100644
index 00000000000..7e5e3560f40
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { FormattedMessage } from 'react-intl';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { isPullRequest } from '../../../../../helpers/branches';
+
+export interface CurrentBranchLikeMergeInformationProps {
+ currentBranchLike: T.BranchLike;
+}
+
+export function CurrentBranchLikeMergeInformation(props: CurrentBranchLikeMergeInformationProps) {
+ const { currentBranchLike } = props;
+
+ if (!isPullRequest(currentBranchLike)) {
+ return null;
+ }
+
+ return (
+ <span className="big-spacer-left flex-shrink note text-ellipsis">
+ <FormattedMessage
+ defaultMessage={translate('branch_like_navigation.for_merge_into_x_from_y')}
+ id="branch_like_navigation.for_merge_into_x_from_y"
+ values={{
+ target: <strong>{currentBranchLike.target}</strong>,
+ branch: <strong>{currentBranchLike.branch}</strong>
+ }}
+ />
+ </span>
+ );
+}
+
+export default React.memo(CurrentBranchLikeMergeInformation);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx
new file mode 100644
index 00000000000..abc88308d7e
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx
@@ -0,0 +1,202 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import * as React from 'react';
+import { Link } from 'react-router';
+import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
+import SearchBox from 'sonar-ui-common/components/controls/SearchBox';
+import { KeyCodes } from 'sonar-ui-common/helpers/keycodes';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { Router, withRouter } from '../../../../../components/hoc/withRouter';
+import {
+ getBrancheLikesAsTree,
+ isBranch,
+ isPullRequest,
+ isSameBranchLike
+} from '../../../../../helpers/branches';
+import { getBranchLikeUrl } from '../../../../../helpers/urls';
+import { ComponentQualifier } from '../../../../../types/component';
+import MenuItemList from './MenuItemList';
+
+interface Props {
+ branchLikes: T.BranchLike[];
+ canAdminComponent?: boolean;
+ component: T.Component;
+ currentBranchLike: T.BranchLike;
+ onClose: () => void;
+ router: Pick<Router, 'push'>;
+}
+
+interface State {
+ branchLikesToDisplay: T.BranchLike[];
+ branchLikesToDisplayTree: T.BranchLikeTree;
+ query: string;
+ selectedBranchLike: T.BranchLike | undefined;
+}
+
+export class Menu extends React.PureComponent<Props, State> {
+ constructor(props: Props) {
+ super(props);
+
+ let selectedBranchLike = undefined;
+
+ if (props.branchLikes.some(b => isSameBranchLike(b, props.currentBranchLike))) {
+ selectedBranchLike = props.currentBranchLike;
+ } else if (props.branchLikes.length > 0) {
+ selectedBranchLike = props.branchLikes[0];
+ }
+
+ this.state = {
+ query: '',
+ selectedBranchLike,
+ ...this.processBranchLikes(props.branchLikes)
+ };
+ }
+
+ processBranchLikes = (branchLikes: T.BranchLike[]) => {
+ const tree = getBrancheLikesAsTree(branchLikes);
+ return {
+ branchLikesToDisplay: [
+ ...(tree.mainBranchTree
+ ? [tree.mainBranchTree.branch, ...tree.mainBranchTree.pullRequests]
+ : []),
+ ...tree.branchTree.reduce((prev, t) => [...prev, t.branch, ...t.pullRequests], []),
+ ...tree.parentlessPullRequests,
+ ...tree.orphanPullRequests
+ ],
+ branchLikesToDisplayTree: tree
+ };
+ };
+
+ openHighlightedBranchLike = () => {
+ if (this.state.selectedBranchLike) {
+ this.handleOnSelect(this.state.selectedBranchLike);
+ }
+ };
+
+ highlightSiblingBranchlike = (indexDelta: number) => {
+ const selectBranchLikeIndex = this.state.branchLikesToDisplay.findIndex(b =>
+ isSameBranchLike(b, this.state.selectedBranchLike)
+ );
+ const newIndex = selectBranchLikeIndex + indexDelta;
+
+ if (
+ selectBranchLikeIndex !== -1 &&
+ newIndex >= 0 &&
+ newIndex < this.state.branchLikesToDisplay.length
+ ) {
+ this.setState(({ branchLikesToDisplay }) => ({
+ selectedBranchLike: branchLikesToDisplay[newIndex]
+ }));
+ }
+ };
+
+ handleKeyDown = (event: React.KeyboardEvent) => {
+ switch (event.keyCode) {
+ case KeyCodes.Enter:
+ event.preventDefault();
+ this.openHighlightedBranchLike();
+ break;
+ case KeyCodes.UpArrow:
+ event.preventDefault();
+ this.highlightSiblingBranchlike(-1);
+ break;
+ case KeyCodes.DownArrow:
+ event.preventDefault();
+ this.highlightSiblingBranchlike(+1);
+ break;
+ }
+ };
+
+ handleSearchChange = (query: string) => {
+ const q = query.toLowerCase();
+
+ const filterBranch = (branch: T.BranchLike) =>
+ isBranch(branch) && branch.name.toLowerCase().includes(q);
+ const filterPullRequest = (pr: T.BranchLike) =>
+ isPullRequest(pr) && (pr.title.toLowerCase().includes(q) || pr.key.toLowerCase().includes(q));
+
+ const filteredBranchLikes = this.props.branchLikes.filter(
+ bl => filterBranch(bl) || filterPullRequest(bl)
+ );
+
+ this.setState({
+ query: q,
+ selectedBranchLike: filteredBranchLikes.length > 0 ? filteredBranchLikes[0] : undefined,
+ ...this.processBranchLikes(filteredBranchLikes)
+ });
+ };
+
+ handleOnSelect = (branchLike: T.BranchLike) => {
+ this.setState({ selectedBranchLike: branchLike }, () => {
+ this.props.onClose();
+ this.props.router.push(getBranchLikeUrl(this.props.component.key, branchLike));
+ });
+ };
+
+ render() {
+ const { canAdminComponent, component, onClose } = this.props;
+ const {
+ branchLikesToDisplay,
+ branchLikesToDisplayTree,
+ query,
+ selectedBranchLike
+ } = this.state;
+
+ const showManageLink = component.qualifier === ComponentQualifier.Project && canAdminComponent;
+ const hasResults = branchLikesToDisplay.length > 0;
+
+ return (
+ <DropdownOverlay className="branch-like-navigation-menu" noPadding={true}>
+ <div className="search-box-container">
+ <SearchBox
+ autoFocus={true}
+ onChange={this.handleSearchChange}
+ onKeyDown={this.handleKeyDown}
+ placeholder={translate('branch_like_navigation.search_for_branch_like')}
+ value={query}
+ />
+ </div>
+
+ <div className="item-list-container">
+ <MenuItemList
+ branchLikeTree={branchLikesToDisplayTree}
+ component={component}
+ hasResults={hasResults}
+ onSelect={this.handleOnSelect}
+ selectedBranchLike={selectedBranchLike}
+ />
+ </div>
+
+ {showManageLink && (
+ <div className="hint-container text-right">
+ <Link
+ onClick={() => onClose()}
+ to={{ pathname: '/project/branches', query: { id: component.key } }}>
+ {translate('branch_like_navigation.manage')}
+ </Link>
+ </div>
+ )}
+ </DropdownOverlay>
+ );
+ }
+}
+
+export default withRouter(Menu);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx
new file mode 100644
index 00000000000..c9aa4daddc6
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 classNames from 'classnames';
+import * as React from 'react';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import BranchStatus from '../../../../../components/common/BranchStatus';
+import BranchLikeIcon from '../../../../../components/icons/BranchLikeIcon';
+import { getBranchLikeDisplayName, isMainBranch } from '../../../../../helpers/branches';
+
+export interface MenuItemProps {
+ branchLike: T.BranchLike;
+ component: T.Component;
+ indent?: boolean;
+ onSelect: (branchLike: T.BranchLike) => void;
+ selected: boolean;
+ setSelectedNode?: (node: HTMLLIElement) => void;
+}
+
+export function MenuItem(props: MenuItemProps) {
+ const { branchLike, component, indent, setSelectedNode, onSelect, selected } = props;
+ const displayName = getBranchLikeDisplayName(branchLike);
+
+ return (
+ <li
+ className={classNames('item', {
+ active: selected
+ })}
+ onClick={() => onSelect(branchLike)}
+ ref={selected ? setSelectedNode : undefined}>
+ <div
+ className={classNames('display-flex-center display-flex-space-between', {
+ 'big-spacer-left': indent
+ })}>
+ <div className="item-name text-ellipsis" title={displayName}>
+ <BranchLikeIcon branchLike={branchLike} />
+ <span className="spacer-left">{displayName}</span>
+ {isMainBranch(branchLike) && (
+ <span className="badge spacer-left">{translate('branches.main_branch')}</span>
+ )}
+ </div>
+ <div className="spacer-left">
+ <BranchStatus branchLike={branchLike} component={component.key} />
+ </div>
+ </div>
+ </li>
+ );
+}
+
+export default React.memo(MenuItem);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx
new file mode 100644
index 00000000000..903893912a3
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx
@@ -0,0 +1,112 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { scrollToElement } from 'sonar-ui-common/helpers/scrolling';
+import { isDefined } from 'sonar-ui-common/helpers/types';
+import { getBranchLikeKey, isSameBranchLike } from '../../../../../helpers/branches';
+import MenuItem from './MenuItem';
+
+export interface MenuItemListProps {
+ branchLikeTree: T.BranchLikeTree;
+ component: T.Component;
+ hasResults: boolean;
+ onSelect: (branchLike: T.BranchLike) => void;
+ selectedBranchLike: T.BranchLike | undefined;
+}
+
+export function MenuItemList(props: MenuItemListProps) {
+ let listNode: HTMLUListElement | null = null;
+ let selectedNode: HTMLLIElement | null = null;
+
+ React.useEffect(() => {
+ if (listNode && selectedNode) {
+ scrollToElement(selectedNode, { parent: listNode, smooth: false });
+ }
+ });
+
+ const { branchLikeTree, component, hasResults, onSelect, selectedBranchLike } = props;
+
+ const renderItem = (branchLike: T.BranchLike, indent?: boolean) => (
+ <MenuItem
+ branchLike={branchLike}
+ component={component}
+ indent={indent}
+ key={getBranchLikeKey(branchLike)}
+ onSelect={onSelect}
+ selected={isSameBranchLike(branchLike, selectedBranchLike)}
+ setSelectedNode={node => (selectedNode = node)}
+ />
+ );
+
+ return (
+ <ul className="item-list" ref={node => (listNode = node)}>
+ {!hasResults && (
+ <li className="item">
+ <span className="note">{translate('no_results')}</span>
+ </li>
+ )}
+
+ {/* BRANCHES & PR */}
+ {[branchLikeTree.mainBranchTree, ...branchLikeTree.branchTree].filter(isDefined).map(tree => (
+ <React.Fragment key={getBranchLikeKey(tree.branch)}>
+ {renderItem(tree.branch)}
+ {tree.pullRequests.length > 0 && (
+ <>
+ <li className="item header">
+ <span className="big-spacer-left">
+ {translate('branch_like_navigation.pull_requests')}
+ </span>
+ </li>
+ {tree.pullRequests.map(pr => renderItem(pr, true))}
+ </>
+ )}
+ <hr />
+ </React.Fragment>
+ ))}
+
+ {/* PARENTLESS PR (for display during search) */}
+ {branchLikeTree.parentlessPullRequests.length > 0 && (
+ <>
+ <li className="item header">{translate('branch_like_navigation.pull_requests')}</li>
+ {branchLikeTree.parentlessPullRequests.map(pr => renderItem(pr))}
+ </>
+ )}
+
+ {/* ORPHAN PR */}
+ {branchLikeTree.orphanPullRequests.length > 0 && (
+ <>
+ <li className="item header">
+ {translate('branch_like_navigation.orphan_pull_requests')}
+ <HelpTooltip
+ className="little-spacer-left"
+ overlay={translate('branch_like_navigation.orphan_pull_requests.tooltip')}
+ />
+ </li>
+ {branchLikeTree.orphanPullRequests.map(pr => renderItem(pr))}
+ </>
+ )}
+ </ul>
+ );
+}
+
+export default React.memo(MenuItemList);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx
new file mode 100644
index 00000000000..9ffcbf39fee
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import Toggler from 'sonar-ui-common/components/controls/Toggler';
+import { click } from 'sonar-ui-common/helpers/testUtils';
+import { mockSetOfBranchAndPullRequest } from '../../../../../../helpers/mocks/branch-pull-request';
+import { mockAppState, mockComponent } from '../../../../../../helpers/testMocks';
+import { BranchLikeNavigation, BranchLikeNavigationProps } from '../BranchLikeNavigation';
+
+it('should render correctly', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should render the menu trigger if branches are enabled', () => {
+ const wrapper = shallowRender({ appState: mockAppState({ branchesEnabled: true }) });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should properly toggle menu opening when clicking the anchor', () => {
+ const wrapper = shallowRender({ appState: mockAppState({ branchesEnabled: true }) });
+ expect(wrapper.find(Toggler).props().open).toBe(false);
+
+ click(wrapper.find('a'));
+ expect(wrapper.find(Toggler).props().open).toBe(true);
+
+ click(wrapper.find('a'));
+ expect(wrapper.find(Toggler).props().open).toBe(false);
+});
+
+it('should properly close menu when toggler asks for', () => {
+ const wrapper = shallowRender({ appState: mockAppState({ branchesEnabled: true }) });
+ expect(wrapper.find(Toggler).props().open).toBe(false);
+
+ click(wrapper.find('a'));
+ expect(wrapper.find(Toggler).props().open).toBe(true);
+
+ wrapper
+ .find(Toggler)
+ .props()
+ .onRequestClose();
+ expect(wrapper.find(Toggler).props().open).toBe(false);
+});
+
+function shallowRender(props?: Partial<BranchLikeNavigationProps>) {
+ const branchLikes = mockSetOfBranchAndPullRequest();
+
+ return shallow(
+ <BranchLikeNavigation
+ appState={mockAppState()}
+ branchLikes={branchLikes}
+ component={mockComponent()}
+ currentBranchLike={branchLikes[0]}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLike-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLike-test.tsx
new file mode 100644
index 00000000000..03328f8ff33
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLike-test.tsx
@@ -0,0 +1,106 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { mockComponent, mockMainBranch } from '../../../../../../helpers/testMocks';
+import { ComponentQualifier } from '../../../../../../types/component';
+import { CurrentBranchLike, CurrentBranchLikeProps } from '../CurrentBranchLike';
+
+describe('CurrentBranchLikeRenderer should render correctly for application when', () => {
+ test('there is only one branch and the user can admin the application', () => {
+ const wrapper = shallowRender({
+ component: mockComponent({
+ configuration: { showSettings: true },
+ qualifier: ComponentQualifier.Application
+ }),
+ hasManyBranches: false
+ });
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test("there is only one branch and the user CAN'T admin the application", () => {
+ const wrapper = shallowRender({
+ component: mockComponent({
+ configuration: { showSettings: false },
+ qualifier: ComponentQualifier.Application
+ }),
+ hasManyBranches: false
+ });
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('there are many branchlikes', () => {
+ const wrapper = shallowRender({
+ branchesEnabled: true,
+ component: mockComponent({
+ qualifier: ComponentQualifier.Application
+ }),
+ hasManyBranches: true
+ });
+ expect(wrapper).toMatchSnapshot();
+ });
+});
+
+describe('CurrentBranchLikeRenderer should render correctly for project when', () => {
+ test('branches support is disabled', () => {
+ const wrapper = shallowRender({
+ branchesEnabled: false,
+ component: mockComponent({
+ qualifier: ComponentQualifier.Project
+ })
+ });
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('there is only one branchlike', () => {
+ const wrapper = shallowRender({
+ branchesEnabled: true,
+ component: mockComponent({
+ qualifier: ComponentQualifier.Project
+ }),
+ hasManyBranches: false
+ });
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('there are many branchlikes', () => {
+ const wrapper = shallowRender({
+ branchesEnabled: true,
+ component: mockComponent({
+ qualifier: ComponentQualifier.Project
+ }),
+ hasManyBranches: true
+ });
+ expect(wrapper).toMatchSnapshot();
+ });
+});
+
+function shallowRender(props?: Partial<CurrentBranchLikeProps>) {
+ return shallow(
+ <CurrentBranchLike
+ branchesEnabled={false}
+ component={mockComponent()}
+ currentBranchLike={mockMainBranch()}
+ hasManyBranches={false}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLikeMergeInformation-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLikeMergeInformation-test.tsx
new file mode 100644
index 00000000000..d1c7873aa13
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLikeMergeInformation-test.tsx
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { mockMainBranch, mockPullRequest } from '../../../../../../helpers/testMocks';
+import {
+ CurrentBranchLikeMergeInformation,
+ CurrentBranchLikeMergeInformationProps
+} from '../CurrentBranchLikeMergeInformation';
+
+it('should render correctly', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should not render for non-pull-request branch like', () => {
+ const wrapper = shallowRender({ currentBranchLike: mockMainBranch() });
+ expect(wrapper.type()).toBeNull();
+});
+
+function shallowRender(props?: Partial<CurrentBranchLikeMergeInformationProps>) {
+ return shallow(
+ <CurrentBranchLikeMergeInformation currentBranchLike={mockPullRequest()} {...props} />
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx
new file mode 100644
index 00000000000..7e80ca84ec4
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx
@@ -0,0 +1,122 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { Link } from 'react-router';
+import SearchBox from 'sonar-ui-common/components/controls/SearchBox';
+import { KeyCodes } from 'sonar-ui-common/helpers/keycodes';
+import { click, mockEvent } from 'sonar-ui-common/helpers/testUtils';
+import { mockSetOfBranchAndPullRequest } from '../../../../../../helpers/mocks/branch-pull-request';
+import { mockComponent, mockPullRequest, mockRouter } from '../../../../../../helpers/testMocks';
+import { Menu } from '../Menu';
+import { MenuItemList } from '../MenuItemList';
+
+it('should render correctly', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should render correctly with no current branch like', () => {
+ const wrapper = shallowRender({ currentBranchLike: undefined });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should close the menu when "manage branches" link is clicked', () => {
+ const onClose = jest.fn();
+ const wrapper = shallowRender({ onClose });
+
+ click(wrapper.find(Link));
+ expect(onClose).toHaveBeenCalled();
+});
+
+it('should change url and close menu when an element is selected', () => {
+ const onClose = jest.fn();
+ const push = jest.fn();
+ const router = mockRouter({ push });
+ const component = mockComponent();
+ const pr = mockPullRequest();
+
+ const wrapper = shallowRender({ component, onClose, router });
+
+ wrapper
+ .find(MenuItemList)
+ .props()
+ .onSelect(pr);
+
+ expect(onClose).toHaveBeenCalled();
+ expect(push).toHaveBeenCalledWith(
+ expect.objectContaining({
+ query: {
+ id: component.key,
+ pullRequest: pr.key
+ }
+ })
+ );
+});
+
+it('should filter branchlike list correctly', () => {
+ const wrapper = shallowRender();
+
+ wrapper
+ .find(SearchBox)
+ .props()
+ .onChange('PR');
+
+ expect(wrapper.state().branchLikesToDisplay.length).toBe(3);
+});
+
+it('should handle keyboard shortcut correctly', () => {
+ const push = jest.fn();
+ const router = mockRouter({ push });
+ const wrapper = shallowRender({ currentBranchLike: branchLikes[1], router });
+
+ const { onKeyDown } = wrapper.find(SearchBox).props();
+
+ if (!onKeyDown) {
+ fail('onKeyDown should be defined');
+ } else {
+ onKeyDown(mockEvent({ keyCode: KeyCodes.UpArrow }));
+ expect(wrapper.state().selectedBranchLike).toBe(branchLikes[5]);
+
+ onKeyDown(mockEvent({ keyCode: KeyCodes.DownArrow }));
+ onKeyDown(mockEvent({ keyCode: KeyCodes.DownArrow }));
+ expect(wrapper.state().selectedBranchLike).toBe(branchLikes[7]);
+
+ onKeyDown(mockEvent({ keyCode: KeyCodes.Enter }));
+ expect(push).toHaveBeenCalled();
+ }
+});
+
+const branchLikes = mockSetOfBranchAndPullRequest();
+
+function shallowRender(props?: Partial<Menu['props']>) {
+ return shallow<Menu>(
+ <Menu
+ branchLikes={branchLikes}
+ canAdminComponent={true}
+ component={mockComponent()}
+ currentBranchLike={branchLikes[2]}
+ onClose={jest.fn()}
+ router={mockRouter()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItem-test.tsx
index 33f62432701..cb701325499 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItem-test.tsx
@@ -17,42 +17,43 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { shallow } from 'enzyme';
import * as React from 'react';
-import ComponentNavBranchesMenuItem, { Props } from '../ComponentNavBranchesMenuItem';
-
-const component = { key: 'component' } as T.Component;
-
-const shortBranch: T.ShortLivingBranch = {
- isMain: false,
- mergeBranch: 'master',
- name: 'foo',
- status: { qualityGateStatus: 'ERROR' },
- type: 'SHORT'
-};
+import { click } from 'sonar-ui-common/helpers/testUtils';
+import {
+ mockComponent,
+ mockMainBranch,
+ mockPullRequest
+} from '../../../../../../helpers/testMocks';
+import { MenuItem, MenuItemProps } from '../MenuItem';
-const mainBranch: T.MainBranch = { isMain: true, name: 'master' };
-
-it('renders main branch', () => {
- expect(shallowRender({ branchLike: mainBranch })).toMatchSnapshot();
+it('should render a main branch correctly', () => {
+ const wrapper = shallowRender({ branchLike: mockMainBranch() });
+ expect(wrapper).toMatchSnapshot();
});
-it('renders short-living branch', () => {
- expect(shallowRender()).toMatchSnapshot();
+it('should render a non-main branch, indented and selected item correctly', () => {
+ const wrapper = shallowRender({ branchLike: mockPullRequest(), indent: true, selected: true });
+ expect(wrapper).toMatchSnapshot();
});
-it('renders short-living orhpan branch', () => {
- const orhpan: T.ShortLivingBranch = { ...shortBranch, isOrphan: true };
- expect(shallowRender({ branchLike: orhpan })).toMatchSnapshot();
+it('should propagate click event correctly', () => {
+ const onSelect = jest.fn();
+ const wrapper = shallowRender({ onSelect });
+
+ click(wrapper.find('li'));
+ expect(onSelect).toHaveBeenCalled();
});
-function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {
+function shallowRender(props?: Partial<MenuItemProps>) {
return shallow(
- <ComponentNavBranchesMenuItem
- branchLike={shortBranch}
- component={component}
+ <MenuItem
+ branchLike={mockMainBranch()}
+ component={mockComponent()}
onSelect={jest.fn()}
selected={false}
+ setSelectedNode={jest.fn()}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItemList-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItemList-test.tsx
new file mode 100644
index 00000000000..1cc6404d4e5
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItemList-test.tsx
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { getBrancheLikesAsTree } from '../../../../../../helpers/branches';
+import { mockSetOfBranchAndPullRequest } from '../../../../../../helpers/mocks/branch-pull-request';
+import { mockComponent, mockPullRequest } from '../../../../../../helpers/testMocks';
+import { MenuItemList, MenuItemListProps } from '../MenuItemList';
+
+it('should render correctly', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+});
+
+function shallowRender(props?: Partial<MenuItemListProps>) {
+ const branchLikes = [
+ ...mockSetOfBranchAndPullRequest(),
+ mockPullRequest({ base: 'not-in-the-list' })
+ ];
+ const branchLikeTree = getBrancheLikesAsTree(branchLikes);
+
+ return shallow(
+ <MenuItemList
+ branchLikeTree={branchLikeTree}
+ component={mockComponent()}
+ hasResults={false}
+ onSelect={jest.fn()}
+ selectedBranchLike={branchLikes[0]}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap
new file mode 100644
index 00000000000..b235ccee944
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap
@@ -0,0 +1,201 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<span
+ className="big-spacer-left flex-shrink branch-like-navigation-toggler-container"
+>
+ <Memo(CurrentBranchLike)
+ branchesEnabled={false}
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ currentBranchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "slb-1",
+ "type": "SHORT",
+ }
+ }
+ hasManyBranches={true}
+ />
+</span>
+`;
+
+exports[`should render the menu trigger if branches are enabled 1`] = `
+<span
+ className="big-spacer-left flex-shrink branch-like-navigation-toggler-container dropdown"
+>
+ <Toggler
+ onRequestClose={[Function]}
+ open={false}
+ overlay={
+ <withRouter(Menu)
+ branchLikes={
+ Array [
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "slb-1",
+ "type": "SHORT",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "name": "llb-1",
+ "type": "LONG",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "key": "1",
+ "target": "master",
+ "title": "PR-1",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "llb-1",
+ "name": "slb-2",
+ "type": "SHORT",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "key": "2",
+ "target": "master",
+ "title": "PR-2",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "name": "llb-3",
+ "type": "LONG",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "name": "llb-2",
+ "type": "LONG",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "isOrphan": true,
+ "key": "2",
+ "target": "llb-100",
+ "title": "PR-2",
+ },
+ ]
+ }
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ currentBranchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "slb-1",
+ "type": "SHORT",
+ }
+ }
+ onClose={[Function]}
+ />
+ }
+ >
+ <a
+ className="link-base-color link-no-underline"
+ href="#"
+ onClick={[Function]}
+ >
+ <Memo(CurrentBranchLike)
+ branchesEnabled={true}
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ currentBranchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "slb-1",
+ "type": "SHORT",
+ }
+ }
+ hasManyBranches={true}
+ />
+ </a>
+ </Toggler>
+</span>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap
new file mode 100644
index 00000000000..cd18a3b5b47
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap
@@ -0,0 +1,185 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CurrentBranchLikeRenderer should render correctly for application when there are many branchlikes 1`] = `
+<span
+ className="display-flex-center flex-shrink text-ellipsis"
+>
+ <BranchLikeIcon
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ />
+ <span
+ className="spacer-left spacer-right flex-shrink text-ellipsis"
+ title="master"
+ >
+ master
+ </span>
+ <DropdownIcon />
+</span>
+`;
+
+exports[`CurrentBranchLikeRenderer should render correctly for application when there is only one branch and the user CAN'T admin the application 1`] = `
+<span
+ className="display-flex-center flex-shrink text-ellipsis"
+>
+ <BranchLikeIcon
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ />
+ <span
+ className="spacer-left spacer-right flex-shrink text-ellipsis"
+ title="master"
+ >
+ master
+ </span>
+</span>
+`;
+
+exports[`CurrentBranchLikeRenderer should render correctly for application when there is only one branch and the user can admin the application 1`] = `
+<span
+ className="display-flex-center flex-shrink text-ellipsis"
+>
+ <BranchLikeIcon
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ />
+ <span
+ className="spacer-left spacer-right flex-shrink text-ellipsis"
+ title="master"
+ >
+ master
+ </span>
+ <HelpTooltip
+ overlay={
+ <React.Fragment>
+ <p>
+ application.branches.help
+ </p>
+ <hr
+ className="spacer-top spacer-bottom"
+ />
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/admin/extension/governance/console",
+ "query": Object {
+ "id": "my-project",
+ "qualifier": "APP",
+ },
+ }
+ }
+ >
+ application.branches.link
+ </Link>
+ </React.Fragment>
+ }
+ >
+ <PlusCircleIcon
+ fill="#4b9fd5"
+ size={12}
+ />
+ </HelpTooltip>
+</span>
+`;
+
+exports[`CurrentBranchLikeRenderer should render correctly for project when branches support is disabled 1`] = `
+<span
+ className="display-flex-center flex-shrink text-ellipsis"
+>
+ <BranchLikeIcon
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ />
+ <span
+ className="spacer-left spacer-right flex-shrink text-ellipsis"
+ title="master"
+ >
+ master
+ </span>
+ <DocTooltip
+ data-test="branches-support-disabled"
+ doc={Promise {}}
+ >
+ <PlusCircleIcon
+ fill="#4b9fd5"
+ size={12}
+ />
+ </DocTooltip>
+</span>
+`;
+
+exports[`CurrentBranchLikeRenderer should render correctly for project when there are many branchlikes 1`] = `
+<span
+ className="display-flex-center flex-shrink text-ellipsis"
+>
+ <BranchLikeIcon
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ />
+ <span
+ className="spacer-left spacer-right flex-shrink text-ellipsis"
+ title="master"
+ >
+ master
+ </span>
+ <DropdownIcon />
+</span>
+`;
+
+exports[`CurrentBranchLikeRenderer should render correctly for project when there is only one branchlike 1`] = `
+<span
+ className="display-flex-center flex-shrink text-ellipsis"
+>
+ <BranchLikeIcon
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ />
+ <span
+ className="spacer-left spacer-right flex-shrink text-ellipsis"
+ title="master"
+ >
+ master
+ </span>
+ <DocTooltip
+ data-test="only-one-branch-like"
+ doc={Promise {}}
+ >
+ <PlusCircleIcon
+ fill="#4b9fd5"
+ size={12}
+ />
+ </DocTooltip>
+</span>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLikeMergeInformation-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLikeMergeInformation-test.tsx.snap
new file mode 100644
index 00000000000..bd901ffd0ef
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLikeMergeInformation-test.tsx.snap
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<span
+ className="big-spacer-left flex-shrink note text-ellipsis"
+>
+ <FormattedMessage
+ defaultMessage="branch_like_navigation.for_merge_into_x_from_y"
+ id="branch_like_navigation.for_merge_into_x_from_y"
+ values={
+ Object {
+ "branch": <strong>
+ feature/foo/bar
+ </strong>,
+ "target": <strong>
+ master
+ </strong>,
+ }
+ }
+ />
+</span>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/Menu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/Menu-test.tsx.snap
new file mode 100644
index 00000000000..9455b556ffd
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/Menu-test.tsx.snap
@@ -0,0 +1,335 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<DropdownOverlay
+ className="branch-like-navigation-menu"
+ noPadding={true}
+>
+ <div
+ className="search-box-container"
+ >
+ <SearchBox
+ autoFocus={true}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="branch_like_navigation.search_for_branch_like"
+ value=""
+ />
+ </div>
+ <div
+ className="item-list-container"
+ >
+ <Memo(MenuItemList)
+ branchLikeTree={
+ Object {
+ "branchTree": Array [
+ Object {
+ "branch": Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "name": "llb-1",
+ "type": "LONG",
+ },
+ "pullRequests": Array [],
+ },
+ Object {
+ "branch": Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "name": "llb-2",
+ "type": "LONG",
+ },
+ "pullRequests": Array [],
+ },
+ Object {
+ "branch": Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "name": "llb-3",
+ "type": "LONG",
+ },
+ "pullRequests": Array [],
+ },
+ Object {
+ "branch": Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "slb-1",
+ "type": "SHORT",
+ },
+ "pullRequests": Array [],
+ },
+ Object {
+ "branch": Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "llb-1",
+ "name": "slb-2",
+ "type": "SHORT",
+ },
+ "pullRequests": Array [],
+ },
+ ],
+ "mainBranchTree": Object {
+ "branch": Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ },
+ "pullRequests": Array [
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "key": "1",
+ "target": "master",
+ "title": "PR-1",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "key": "2",
+ "target": "master",
+ "title": "PR-2",
+ },
+ ],
+ },
+ "orphanPullRequests": Array [
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "isOrphan": true,
+ "key": "2",
+ "target": "llb-100",
+ "title": "PR-2",
+ },
+ ],
+ "parentlessPullRequests": Array [],
+ }
+ }
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ hasResults={true}
+ onSelect={[Function]}
+ selectedBranchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ />
+ </div>
+ <div
+ className="hint-container text-right"
+ >
+ <Link
+ onClick={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/branches",
+ "query": Object {
+ "id": "my-project",
+ },
+ }
+ }
+ >
+ branch_like_navigation.manage
+ </Link>
+ </div>
+</DropdownOverlay>
+`;
+
+exports[`should render correctly with no current branch like 1`] = `
+<DropdownOverlay
+ className="branch-like-navigation-menu"
+ noPadding={true}
+>
+ <div
+ className="search-box-container"
+ >
+ <SearchBox
+ autoFocus={true}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="branch_like_navigation.search_for_branch_like"
+ value=""
+ />
+ </div>
+ <div
+ className="item-list-container"
+ >
+ <Memo(MenuItemList)
+ branchLikeTree={
+ Object {
+ "branchTree": Array [
+ Object {
+ "branch": Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "name": "llb-1",
+ "type": "LONG",
+ },
+ "pullRequests": Array [],
+ },
+ Object {
+ "branch": Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "name": "llb-2",
+ "type": "LONG",
+ },
+ "pullRequests": Array [],
+ },
+ Object {
+ "branch": Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "name": "llb-3",
+ "type": "LONG",
+ },
+ "pullRequests": Array [],
+ },
+ Object {
+ "branch": Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "slb-1",
+ "type": "SHORT",
+ },
+ "pullRequests": Array [],
+ },
+ Object {
+ "branch": Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "llb-1",
+ "name": "slb-2",
+ "type": "SHORT",
+ },
+ "pullRequests": Array [],
+ },
+ ],
+ "mainBranchTree": Object {
+ "branch": Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ },
+ "pullRequests": Array [
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "key": "1",
+ "target": "master",
+ "title": "PR-1",
+ },
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "key": "2",
+ "target": "master",
+ "title": "PR-2",
+ },
+ ],
+ },
+ "orphanPullRequests": Array [
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "isOrphan": true,
+ "key": "2",
+ "target": "llb-100",
+ "title": "PR-2",
+ },
+ ],
+ "parentlessPullRequests": Array [],
+ }
+ }
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ hasResults={true}
+ onSelect={[Function]}
+ selectedBranchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "slb-1",
+ "type": "SHORT",
+ }
+ }
+ />
+ </div>
+ <div
+ className="hint-container text-right"
+ >
+ <Link
+ onClick={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/branches",
+ "query": Object {
+ "id": "my-project",
+ },
+ }
+ }
+ >
+ branch_like_navigation.manage
+ </Link>
+ </div>
+</DropdownOverlay>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap
new file mode 100644
index 00000000000..91ae3e4c65f
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap
@@ -0,0 +1,102 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render a main branch correctly 1`] = `
+<li
+ className="item"
+ onClick={[Function]}
+>
+ <div
+ className="display-flex-center display-flex-space-between"
+ >
+ <div
+ className="item-name text-ellipsis"
+ title="master"
+ >
+ <BranchLikeIcon
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ />
+ <span
+ className="spacer-left"
+ >
+ master
+ </span>
+ <span
+ className="badge spacer-left"
+ >
+ branches.main_branch
+ </span>
+ </div>
+ <div
+ className="spacer-left"
+ >
+ <Connect(BranchStatus)
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ component="my-project"
+ />
+ </div>
+ </div>
+</li>
+`;
+
+exports[`should render a non-main branch, indented and selected item correctly 1`] = `
+<li
+ className="item active"
+ onClick={[Function]}
+>
+ <div
+ className="display-flex-center display-flex-space-between big-spacer-left"
+ >
+ <div
+ className="item-name text-ellipsis"
+ title="1001 – Foo Bar feature"
+ >
+ <BranchLikeIcon
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "key": "1001",
+ "target": "master",
+ "title": "Foo Bar feature",
+ }
+ }
+ />
+ <span
+ className="spacer-left"
+ >
+ 1001 – Foo Bar feature
+ </span>
+ </div>
+ <div
+ className="spacer-left"
+ >
+ <Connect(BranchStatus)
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "key": "1001",
+ "target": "master",
+ "title": "Foo Bar feature",
+ }
+ }
+ component="my-project"
+ />
+ </div>
+ </div>
+</li>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItemList-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItemList-test.tsx.snap
new file mode 100644
index 00000000000..891c36c58f6
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItemList-test.tsx.snap
@@ -0,0 +1,428 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<ul
+ className="item-list"
+>
+ <li
+ className="item"
+ >
+ <span
+ className="note"
+ >
+ no_results
+ </span>
+ </li>
+ <Memo(MenuItem)
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ key="branch-master"
+ onSelect={[MockFunction]}
+ selected={false}
+ setSelectedNode={[Function]}
+ />
+ <li
+ className="item header"
+ >
+ <span
+ className="big-spacer-left"
+ >
+ branch_like_navigation.pull_requests
+ </span>
+ </li>
+ <Memo(MenuItem)
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "key": "1",
+ "target": "master",
+ "title": "PR-1",
+ }
+ }
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ indent={true}
+ key="pull-request-1"
+ onSelect={[MockFunction]}
+ selected={false}
+ setSelectedNode={[Function]}
+ />
+ <Memo(MenuItem)
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "key": "2",
+ "target": "master",
+ "title": "PR-2",
+ }
+ }
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ indent={true}
+ key="pull-request-2"
+ onSelect={[MockFunction]}
+ selected={false}
+ setSelectedNode={[Function]}
+ />
+ <hr />
+ <Memo(MenuItem)
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "name": "llb-1",
+ "type": "LONG",
+ }
+ }
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ key="branch-llb-1"
+ onSelect={[MockFunction]}
+ selected={false}
+ setSelectedNode={[Function]}
+ />
+ <hr />
+ <Memo(MenuItem)
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "name": "llb-2",
+ "type": "LONG",
+ }
+ }
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ key="branch-llb-2"
+ onSelect={[MockFunction]}
+ selected={false}
+ setSelectedNode={[Function]}
+ />
+ <hr />
+ <Memo(MenuItem)
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "name": "llb-3",
+ "type": "LONG",
+ }
+ }
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ key="branch-llb-3"
+ onSelect={[MockFunction]}
+ selected={false}
+ setSelectedNode={[Function]}
+ />
+ <hr />
+ <Memo(MenuItem)
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "slb-1",
+ "type": "SHORT",
+ }
+ }
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ key="branch-slb-1"
+ onSelect={[MockFunction]}
+ selected={true}
+ setSelectedNode={[Function]}
+ />
+ <hr />
+ <Memo(MenuItem)
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": false,
+ "mergeBranch": "llb-1",
+ "name": "slb-2",
+ "type": "SHORT",
+ }
+ }
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ key="branch-slb-2"
+ onSelect={[MockFunction]}
+ selected={false}
+ setSelectedNode={[Function]}
+ />
+ <hr />
+ <li
+ className="item header"
+ >
+ branch_like_navigation.pull_requests
+ </li>
+ <Memo(MenuItem)
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "not-in-the-list",
+ "branch": "feature/foo/bar",
+ "key": "1001",
+ "target": "master",
+ "title": "Foo Bar feature",
+ }
+ }
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ key="pull-request-1001"
+ onSelect={[MockFunction]}
+ selected={false}
+ setSelectedNode={[Function]}
+ />
+ <li
+ className="item header"
+ >
+ branch_like_navigation.orphan_pull_requests
+ <HelpTooltip
+ className="little-spacer-left"
+ overlay="branch_like_navigation.orphan_pull_requests.tooltip"
+ />
+ </li>
+ <Memo(MenuItem)
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "base": "master",
+ "branch": "feature/foo/bar",
+ "isOrphan": true,
+ "key": "2",
+ "target": "llb-100",
+ "title": "PR-2",
+ }
+ }
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ key="pull-request-2"
+ onSelect={[MockFunction]}
+ selected={false}
+ setSelectedNode={[Function]}
+ />
+</ul>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchList-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchList-test.tsx.snap
index df73eeeaa98..96c102bf2ec 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchList-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchList-test.tsx.snap
@@ -29,7 +29,7 @@ exports[`should render correctly 1`] = `
<td
className="nowrap"
>
- <BranchIcon
+ <BranchLikeIcon
branchLike={
Object {
"analysisDate": "2018-01-01",
@@ -79,7 +79,7 @@ exports[`should render correctly 1`] = `
<td
className="nowrap"
>
- <BranchIcon
+ <BranchLikeIcon
branchLike={
Object {
"analysisDate": "2018-01-01",
@@ -115,7 +115,7 @@ exports[`should render correctly 1`] = `
<td
className="nowrap"
>
- <BranchIcon
+ <BranchLikeIcon
branchLike={
Object {
"analysisDate": "2018-01-01",
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx
index 9aa7ffdf8b9..94cdebad8f7 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx
@@ -24,7 +24,7 @@ import ActionsDropdown, {
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { listBranchesNewCodePeriod, resetNewCodePeriod } from '../../../api/newCodePeriod';
-import BranchIcon from '../../../components/icons-components/BranchIcon';
+import BranchLikeIcon from '../../../components/icons/BranchLikeIcon';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import { isBranch, sortBranches } from '../../../helpers/branches';
import BranchBaselineSettingModal from './BranchBaselineSettingModal';
@@ -176,7 +176,7 @@ export default class BranchList extends React.PureComponent<Props, State> {
{branches.map(branch => (
<tr key={branch.name}>
<td className="nowrap">
- <BranchIcon branchLike={branch} className="little-spacer-right" />
+ <BranchLikeIcon branchLike={branch} className="little-spacer-right" />
{branch.name}
{branch.isMain && (
<div className="badge spacer-left">{translate('branches.main_branch')}</div>
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRowRenderer.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRowRenderer.tsx
index 33c5be081cd..34439e3e2cb 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRowRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRowRenderer.tsx
@@ -24,7 +24,7 @@ import ActionsDropdown, {
} from 'sonar-ui-common/components/controls/ActionsDropdown';
import { translate } from 'sonar-ui-common/helpers/l10n';
import BranchStatus from '../../../components/common/BranchStatus';
-import BranchIcon from '../../../components/icons-components/BranchIcon';
+import BranchLikeIcon from '../../../components/icons/BranchLikeIcon';
import DateFromNow from '../../../components/intl/DateFromNow';
import { getBranchLikeDisplayName, isMainBranch, isPullRequest } from '../../../helpers/branches';
@@ -41,7 +41,7 @@ export function BranchLikeRowRenderer(props: BranchLikeRowRendererProps) {
return (
<tr>
<td>
- <BranchIcon branchLike={branchLike} className="little-spacer-right" />
+ <BranchLikeIcon branchLike={branchLike} className="little-spacer-right" />
{getBranchLikeDisplayName(branchLike)}
{isMainBranch(branchLike) && (
<div className="badge spacer-left">{translate('branches.main_branch')}</div>
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTableRenderer.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTableRenderer.tsx
index 641ae3f21b6..2d4b6aa5b6f 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTableRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTableRenderer.tsx
@@ -42,7 +42,7 @@ export function BranchLikeTableRenderer(props: BranchLikeTableRendererProps) {
<th>{tableTitle}</th>
<th className="thin nowrap">{translate('status')}</th>
<th className="thin nowrap text-right big-spacer-left">
- {translate('branches.last_analysis_date')}
+ {translate('project_branch_pull_request.last_analysis_date')}
</th>
<th className="thin nowrap text-right">{translate('actions')}</th>
</tr>
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeRowRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeRowRenderer-test.tsx.snap
index 53ca50c0bf8..3cc09481ada 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeRowRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeRowRenderer-test.tsx.snap
@@ -3,7 +3,7 @@
exports[`should render correctly for long lived branch 1`] = `
<tr>
<td>
- <BranchIcon
+ <BranchLikeIcon
branchLike={
Object {
"analysisDate": "2018-01-01",
@@ -59,7 +59,7 @@ exports[`should render correctly for long lived branch 1`] = `
exports[`should render correctly for mai branch 1`] = `
<tr>
<td>
- <BranchIcon
+ <BranchLikeIcon
branchLike={
Object {
"analysisDate": "2018-01-01",
@@ -117,7 +117,7 @@ exports[`should render correctly for mai branch 1`] = `
exports[`should render correctly for pull request 1`] = `
<tr>
<td>
- <BranchIcon
+ <BranchLikeIcon
branchLike={
Object {
"analysisDate": "2018-01-01",
@@ -177,7 +177,7 @@ exports[`should render correctly for pull request 1`] = `
exports[`should render correctly for short lived branch 1`] = `
<tr>
<td>
- <BranchIcon
+ <BranchLikeIcon
branchLike={
Object {
"analysisDate": "2018-01-01",
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeTableRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeTableRenderer-test.tsx.snap
index 372b315da23..c26c41e5225 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeTableRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeTableRenderer-test.tsx.snap
@@ -20,7 +20,7 @@ exports[`should render correctly 1`] = `
<th
className="thin nowrap text-right big-spacer-left"
>
- branches.last_analysis_date
+ project_branch_pull_request.last_analysis_date
</th>
<th
className="thin nowrap text-right"
diff --git a/server/sonar-web/src/main/js/components/hoc/withAppState.tsx b/server/sonar-web/src/main/js/components/hoc/withAppState.tsx
index c81252162d4..912be0ad321 100644
--- a/server/sonar-web/src/main/js/components/hoc/withAppState.tsx
+++ b/server/sonar-web/src/main/js/components/hoc/withAppState.tsx
@@ -23,7 +23,7 @@ import { getAppState, Store } from '../../store/rootReducer';
import { getWrappedDisplayName } from './utils';
export function withAppState<P>(
- WrappedComponent: React.ComponentClass<P & { appState: Partial<T.AppState> }>
+ WrappedComponent: React.ComponentType<P & { appState: Partial<T.AppState> }>
) {
class Wrapper extends React.Component<P & { appState: T.AppState }> {
static displayName = getWrappedDisplayName(WrappedComponent, 'withAppState');
diff --git a/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx b/server/sonar-web/src/main/js/components/icons/BranchLikeIcon.tsx
index 200afa5a85a..0307ad9f699 100644
--- a/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx
+++ b/server/sonar-web/src/main/js/components/icons/BranchLikeIcon.tsx
@@ -23,11 +23,11 @@ import PullRequestIcon from 'sonar-ui-common/components/icons/PullRequestIcon';
import ShortLivingBranchIcon from 'sonar-ui-common/components/icons/ShortLivingBranchIcon';
import { isPullRequest } from '../../helpers/branches';
-export interface BranchIconProps extends IconProps {
+export interface BranchLikeIconProps extends IconProps {
branchLike: T.BranchLike;
}
-export default function BranchIcon({ branchLike, ...props }: BranchIconProps) {
+export default function BranchLikeIcon({ branchLike, ...props }: BranchLikeIconProps) {
if (isPullRequest(branchLike)) {
return <PullRequestIcon {...props} />;
} else {
diff --git a/server/sonar-web/src/main/js/components/icons-components/__tests__/BranchIcon-test.tsx b/server/sonar-web/src/main/js/components/icons/__tests__/BranchLikeIcon-test.tsx
index 9fad6aad693..d4ea4f0bdf4 100644
--- a/server/sonar-web/src/main/js/components/icons-components/__tests__/BranchIcon-test.tsx
+++ b/server/sonar-web/src/main/js/components/icons/__tests__/BranchLikeIcon-test.tsx
@@ -25,7 +25,7 @@ import {
mockPullRequest,
mockShortLivingBranch
} from '../../../helpers/testMocks';
-import BranchIcon, { BranchIconProps } from '../BranchIcon';
+import BranchLikeIcon, { BranchLikeIconProps } from '../BranchLikeIcon';
it('should render short living branch icon for short living branch', () => {
const wrapper = shallowRender({ branchLike: mockShortLivingBranch() });
@@ -42,6 +42,6 @@ it('should render pull request icon correctly', () => {
expect(wrapper).toMatchSnapshot();
});
-function shallowRender(props: BranchIconProps) {
- return shallow(<BranchIcon {...props} />);
+function shallowRender(props: BranchLikeIconProps) {
+ return shallow(<BranchLikeIcon {...props} />);
}
diff --git a/server/sonar-web/src/main/js/components/icons-components/__tests__/__snapshots__/BranchIcon-test.tsx.snap b/server/sonar-web/src/main/js/components/icons/__tests__/__snapshots__/BranchLikeIcon-test.tsx.snap
index abc10820cf4..abc10820cf4 100644
--- a/server/sonar-web/src/main/js/components/icons-components/__tests__/__snapshots__/BranchIcon-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/icons/__tests__/__snapshots__/BranchLikeIcon-test.tsx.snap
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts
index a8186914371..09b9442991c 100644
--- a/server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts
+++ b/server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts
@@ -17,7 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { isSameBranchLike, sortBranches, sortBranchesAsTree } from '../branches';
+
+import { getBrancheLikesAsTree, isSameBranchLike, sortBranches } from '../branches';
import {
mockLongLivingBranch,
mockMainBranch,
@@ -25,40 +26,57 @@ import {
mockShortLivingBranch
} from '../testMocks';
-describe('#sortBranchesAsTree', () => {
- it('sorts main branch and short-living branches', () => {
- const main = mockMainBranch();
- const foo = mockShortLivingBranch({ name: 'foo' });
- const bar = mockShortLivingBranch({ name: 'bar' });
- expect(sortBranchesAsTree([main, foo, bar])).toEqual([main, bar, foo]);
- });
+describe('#getBrancheLikesAsTree', () => {
+ it('should correctly map branches and prs to tree object', () => {
+ const main = mockMainBranch({ name: 'master' });
+ const llb1 = mockLongLivingBranch({ name: 'llb1' });
+ const llb2 = mockLongLivingBranch({ name: 'llb2' });
+ const slb1 = mockShortLivingBranch({ name: 'slb1' });
+ const slb2 = mockShortLivingBranch({ name: 'slb2' });
- it('sorts main branch and long-living branches', () => {
- const main = mockMainBranch();
- const foo = mockLongLivingBranch({ name: 'foo' });
- const bar = mockLongLivingBranch({ name: 'bar' });
- expect(sortBranchesAsTree([main, foo, bar])).toEqual([main, bar, foo]);
- });
+ const mainPr1 = mockPullRequest({ base: main.name, key: 'PR1' });
+ const mainPr2 = mockPullRequest({ base: main.name, key: 'PR2' });
+ const llb1Pr1 = mockPullRequest({ base: llb1.name, key: 'PR1' });
+ const llb1Pr2 = mockPullRequest({ base: llb1.name, key: 'PR2' });
+ const llb2Pr1 = mockPullRequest({ base: llb2.name, key: 'PR1' });
+ const llb2Pr2 = mockPullRequest({ base: llb2.name, key: 'PR1' });
+ const orphanPR1 = mockPullRequest({ isOrphan: true, key: 'PR1' });
+ const orphanPR2 = mockPullRequest({ isOrphan: true, key: 'PR2' });
+ const parentlessPR1 = mockPullRequest({ base: 'not_present_branch_1', key: 'PR1' });
+ const parentlessPR2 = mockPullRequest({ base: 'not_present_branch_2', key: 'PR2' });
- it('sorts all types of branches', () => {
- const main = mockMainBranch();
- const shortFoo = mockShortLivingBranch({ name: 'shortFoo', mergeBranch: 'master' });
- const shortBar = mockShortLivingBranch({ name: 'shortBar', mergeBranch: 'longBaz' });
- const shortPre = mockShortLivingBranch({ name: 'shortPre', mergeBranch: 'shortFoo' });
- const longBaz = mockLongLivingBranch({ name: 'longBaz' });
- const longQux = mockLongLivingBranch({ name: 'longQux' });
- const longQwe = mockLongLivingBranch({ name: 'longQwe' });
- const pr = mockPullRequest({ base: 'master' });
- // - main - main
- // - shortFoo - shortFoo
- // - shortPre - shortPre
- // - longBaz ----> - longBaz
- // - shortBar - shortBar
- // - longQwe - longQwe
- // - longQux - longQux
expect(
- sortBranchesAsTree([main, shortFoo, shortBar, shortPre, longBaz, longQux, longQwe, pr])
- ).toEqual([main, pr, shortFoo, shortPre, longBaz, shortBar, longQux, longQwe]);
+ getBrancheLikesAsTree([
+ llb2,
+ llb1,
+ main,
+ orphanPR2,
+ orphanPR1,
+ slb2,
+ slb1,
+ mainPr2,
+ mainPr1,
+ parentlessPR2,
+ parentlessPR1,
+ llb2Pr2,
+ llb2Pr1,
+ llb1Pr2,
+ llb1Pr1
+ ])
+ ).toEqual({
+ mainBranchTree: {
+ branch: main,
+ pullRequests: [mainPr1, mainPr2]
+ },
+ branchTree: [
+ { branch: llb1, pullRequests: [llb1Pr1, llb1Pr2] },
+ { branch: llb2, pullRequests: [llb2Pr1, llb2Pr1] },
+ { branch: slb1, pullRequests: [] },
+ { branch: slb2, pullRequests: [] }
+ ],
+ parentlessPullRequests: [parentlessPR1, parentlessPR2],
+ orphanPullRequests: [orphanPR1, orphanPR2]
+ });
});
});
diff --git a/server/sonar-web/src/main/js/helpers/branches.ts b/server/sonar-web/src/main/js/helpers/branches.ts
index 3cac030ccba..9e3c705e225 100644
--- a/server/sonar-web/src/main/js/helpers/branches.ts
+++ b/server/sonar-web/src/main/js/helpers/branches.ts
@@ -17,7 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { orderBy, sortBy } from 'lodash';
+
+import { orderBy } from 'lodash';
export function isBranch(branchLike?: T.BranchLike): branchLike is T.Branch {
return branchLike !== undefined && (branchLike as T.Branch).isMain !== undefined;
@@ -96,58 +97,32 @@ export function isSameBranchLike(a: T.BranchLike | undefined, b: T.BranchLike |
return a === b;
}
-export function sortBranchesAsTree(branchLikes: T.BranchLike[]) {
- const result: T.BranchLike[] = [];
-
+export function getBrancheLikesAsTree(branchLikes: T.BranchLike[]): T.BranchLikeTree {
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
- if (mainBranch) {
- result.push(
- mainBranch,
- ...getPullRequests(mainBranch.name),
- ...getNestedShortLivingBranches(mainBranch.name)
- );
- }
-
- // then all long-living branches
- sortBy(longLivingBranches, 'name').forEach(longLivingBranch => {
- result.push(
- longLivingBranch,
- ...getPullRequests(longLivingBranch.name),
- ...getNestedShortLivingBranches(longLivingBranch.name)
- );
- });
-
- // finally all orhpan pull requests and branches
- result.push(
- ...sortBy(pullRequests.filter(pr => pr.isOrphan), pullRequest => pullRequest.key),
- ...sortBy(shortLivingBranches.filter(branch => branch.isOrphan), branch => branch.name)
+ const branches = orderBy(branchLikes.filter(isBranch).filter(b => !isMainBranch(b)), b => b.name);
+ const pullRequests = orderBy(branchLikes.filter(isPullRequest), b => b.key);
+ const parentlessPullRequests = pullRequests.filter(
+ pr => !pr.isOrphan && ![mainBranch, ...branches].find(b => !!b && b.name === pr.base)
);
+ const orphanPullRequests = pullRequests.filter(pr => pr.isOrphan);
- return result;
-
- /** Get all short-living branches (possibly nested) which should be merged to a given branch */
- function getNestedShortLivingBranches(mergeBranch: string) {
- const found: T.ShortLivingBranch[] = shortLivingBranches.filter(
- branch => branch.mergeBranch === mergeBranch
- );
-
- let i = 0;
- while (i < found.length) {
- const current = found[i];
- found.push(...shortLivingBranches.filter(branch => branch.mergeBranch === current.name));
- i++;
- }
+ const tree: T.BranchLikeTree = {
+ branchTree: branches.map(b => ({ branch: b, pullRequests: getPullRequests(b) })),
+ parentlessPullRequests,
+ orphanPullRequests
+ };
- return sortBy(found, branch => branch.name);
+ if (mainBranch) {
+ tree.mainBranchTree = {
+ branch: mainBranch,
+ pullRequests: getPullRequests(mainBranch)
+ };
}
- function getPullRequests(base: string) {
- return sortBy(pullRequests.filter(pr => pr.base === base), pullRequest => pullRequest.key);
+ return tree;
+
+ function getPullRequests(branch: T.Branch) {
+ return pullRequests.filter(pr => !pr.isOrphan && pr.base === branch.name);
}
}
diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts
index 73afb0e5053..11e34a1311c 100644
--- a/server/sonar-web/src/main/js/helpers/urls.ts
+++ b/server/sonar-web/src/main/js/helpers/urls.ts
@@ -19,12 +19,7 @@
*/
import { getBaseUrl, Location } from 'sonar-ui-common/helpers/urls';
import { getProfilePath } from '../apps/quality-profiles/utils';
-import {
- getBranchLikeQuery,
- isLongLivingBranch,
- isPullRequest,
- isShortLivingBranch
-} from './branches';
+import { getBranchLikeQuery, isBranch, isMainBranch, isPullRequest } from './branches';
type Query = Location['query'];
@@ -47,10 +42,8 @@ export function getComponentBackgroundTaskUrl(componentKey: string, status?: str
export function getBranchLikeUrl(project: string, branchLike?: T.BranchLike): Location {
if (isPullRequest(branchLike)) {
return getPullRequestUrl(project, branchLike.key);
- } else if (isShortLivingBranch(branchLike)) {
+ } else if (isBranch(branchLike) && !isMainBranch(branchLike)) {
return getShortLivingBranchUrl(project, branchLike.name);
- } else if (isLongLivingBranch(branchLike)) {
- return getLongLivingBranchUrl(project, branchLike.name);
} else {
return getProjectUrl(project);
}
diff --git a/server/sonar-web/src/main/js/types/branch-like.d.ts b/server/sonar-web/src/main/js/types/branch-like.d.ts
new file mode 100644
index 00000000000..5b83375ca32
--- /dev/null
+++ b/server/sonar-web/src/main/js/types/branch-like.d.ts
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.
+ */
+
+declare namespace T {
+ export type BranchType = 'LONG' | 'SHORT';
+
+ export interface Branch {
+ analysisDate?: string;
+ isMain: boolean;
+ name: string;
+ status?: { qualityGateStatus: Status };
+ }
+
+ export interface MainBranch extends Branch {
+ isMain: true;
+ }
+
+ export interface LongLivingBranch extends Branch {
+ isMain: false;
+ type: 'LONG';
+ }
+
+ export interface ShortLivingBranch extends Branch {
+ isMain: false;
+ isOrphan?: true;
+ mergeBranch: string;
+ type: 'SHORT';
+ }
+
+ export interface PullRequest {
+ analysisDate?: string;
+ base: string;
+ branch: string;
+ key: string;
+ isOrphan?: true;
+ status?: { qualityGateStatus: Status };
+ target: string;
+ title: string;
+ url?: string;
+ }
+
+ export type BranchLike = Branch | PullRequest;
+
+ export interface BranchTree {
+ branch: Branch;
+ pullRequests: PullRequest[];
+ }
+
+ export interface BranchLikeTree {
+ mainBranchTree?: BranchTree;
+ branchTree: BranchTree[];
+ parentlessPullRequests: PullRequest[];
+ orphanPullRequests: PullRequest[];
+ }
+
+ export type BranchParameters = { branch?: string } | { pullRequest?: string };
+
+ export interface BranchWithNewCodePeriod extends Branch {
+ newCodePeriod?: NewCodePeriod;
+ }
+}
diff --git a/server/sonar-web/src/main/js/types/component.ts b/server/sonar-web/src/main/js/types/component.ts
new file mode 100644
index 00000000000..3bd1ff9914d
--- /dev/null
+++ b/server/sonar-web/src/main/js/types/component.ts
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.
+ */
+
+export enum ComponentQualifier {
+ Application = 'APP',
+ Directory = 'DIR',
+ Developper = 'DEV',
+ File = 'FIL',
+ Portfolio = 'VW',
+ Project = 'TRK',
+ SubPortfolio = 'SVW',
+ SubProject = 'BRC',
+ TestFile = 'UTS'
+}
diff --git a/server/sonar-web/src/main/js/types/types.d.ts b/server/sonar-web/src/main/js/types/types.d.ts
index 48bcb09704e..91d2c2b8e4e 100644
--- a/server/sonar-web/src/main/js/types/types.d.ts
+++ b/server/sonar-web/src/main/js/types/types.d.ts
@@ -108,23 +108,6 @@ declare namespace T {
webAnalyticsJsPath?: string;
}
- export interface Branch {
- analysisDate?: string;
- isMain: boolean;
- name: string;
- status?: { qualityGateStatus: Status };
- }
-
- export type BranchLike = Branch | PullRequest;
-
- export type BranchParameters = { branch?: string } | { pullRequest?: string };
-
- export type BranchType = 'LONG' | 'SHORT';
-
- export interface BranchWithNewCodePeriod extends Branch {
- newCodePeriod?: NewCodePeriod;
- }
-
export interface Breadcrumb {
key: string;
name: string;
@@ -455,15 +438,6 @@ declare namespace T {
settings?: CurrentUserSetting[];
}
- export interface LongLivingBranch extends Branch {
- isMain: false;
- type: 'LONG';
- }
-
- export interface MainBranch extends Branch {
- isMain: true;
- }
-
export interface Measure extends MeasureIntern {
metric: string;
}
@@ -669,18 +643,6 @@ declare namespace T {
url: string;
}
- export interface PullRequest {
- analysisDate?: string;
- base: string;
- branch: string;
- key: string;
- isOrphan?: true;
- status?: { qualityGateStatus: Status };
- target: string;
- title: string;
- url?: string;
- }
-
export interface QualityGate {
actions?: {
associateProjects?: boolean;
@@ -834,13 +796,6 @@ declare namespace T {
values?: string[];
}
- export interface ShortLivingBranch extends Branch {
- isMain: false;
- isOrphan?: true;
- mergeBranch: string;
- type: 'SHORT';
- }
-
export interface Snippet {
start: number;
end: number;
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index bcfa835f73c..b97a782d043 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -546,6 +546,8 @@ project_branch_pull_request.tabs.branches=Branches
project_branch_pull_request.tabs.pull_requests=Pull Requests
project_branch_pull_request.table.branch=Branch
project_branch_pull_request.table.pull_request=Pull Request
+project_branch_pull_request.last_analysis_date=Last Analysis Date
+
project_baseline.page=New Code Period
project_baseline.page.description=Use this page to manage the New Code Period of your project. {link}
project_baseline.page.description.link=Learn More
@@ -3188,25 +3190,20 @@ onboarding.tutorial.return_to_tutorial=Return to tutorial
# BRANCHES
#
#------------------------------------------------------------------------------
-branches.manage=Manage branches
-branches.orphan_branch=Orphan Branch
-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.set_new_code_period=Set New Code Period
-branches.last_analysis_date=Last Analysis Date
-branches.search_for_branches=Search for branches...
-branches.pull_requests=Pull Requests
-branches.short_lived.quality_gate.description=The branch status is passed because there are no open issue. The remaining {0} issue(s) have been confirmed.
-branches.short_lived_branches=Short-lived branches
-branches.pull_request.for_merge_into_x_from_y=for merge into {target} from {branch}
branches.see_the_pr=See the PR
-branches.measures.new_coverage.help=Coverage on New Code. See {link} for details.
-branches.measures.new_coverage.missing=No coverage data for new code.
-branches.measures.new_duplicated_lines_density.help=Duplications on New Code. See {link} for details.
-branches.measures.new_duplicated_lines_density.missing=No duplications data for new code.
+#------------------------------------------------------------------------------
+#
+# BRANCH-LIKE NAVIGATION
+#
+#------------------------------------------------------------------------------
+branch_like_navigation.manage=Manage branches and Pull Requests
+branch_like_navigation.search_for_branch_like=Search for branches or Pull Requests...
+branch_like_navigation.pull_requests=Pull Requests
+branch_like_navigation.orphan_pull_requests=Orphan Pull Requests
+branch_like_navigation.orphan_pull_requests.tooltip=When the base of a Pull Request is deleted, this Pull Request becomes orphan.
+branch_like_navigation.for_merge_into_x_from_y=for merge into {target} from {branch}
#------------------------------------------------------------------------------
#