Browse Source

SONAR-12634 Reorganize the branches & pull requests selection menu

tags/8.1.0.31237
Philippe Perrin 4 years ago
parent
commit
956001c58e
54 changed files with 2974 additions and 2089 deletions
  1. 2
    2
      server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts
  2. 72
    0
      server/sonar-web/src/main/js/app/components/nav/component/ComponentBreadcrumb.tsx
  3. 0
    35
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
  4. 0
    2
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
  5. 0
    231
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
  6. 0
    263
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
  7. 0
    77
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx
  8. 24
    97
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx
  9. 52
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentBreadcrumb-test.tsx
  10. 0
    143
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
  11. 0
    101
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx
  12. 18
    60
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx
  13. 48
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentBreadcrumb-test.tsx.snap
  14. 1
    2
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
  15. 0
    286
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
  16. 0
    291
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
  17. 0
    181
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap
  18. 149
    126
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap
  19. 59
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.css
  20. 93
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
  21. 114
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx
  22. 51
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx
  23. 202
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx
  24. 67
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx
  25. 112
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx
  26. 76
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx
  27. 106
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLike-test.tsx
  28. 43
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLikeMergeInformation-test.tsx
  29. 122
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx
  30. 25
    24
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItem-test.tsx
  31. 50
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItemList-test.tsx
  32. 201
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap
  33. 185
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap
  34. 22
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLikeMergeInformation-test.tsx.snap
  35. 335
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/Menu-test.tsx.snap
  36. 102
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap
  37. 428
    0
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItemList-test.tsx.snap
  38. 3
    3
      server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchList-test.tsx.snap
  39. 2
    2
      server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx
  40. 2
    2
      server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRowRenderer.tsx
  41. 1
    1
      server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTableRenderer.tsx
  42. 4
    4
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeRowRenderer-test.tsx.snap
  43. 1
    1
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeTableRenderer-test.tsx.snap
  44. 1
    1
      server/sonar-web/src/main/js/components/hoc/withAppState.tsx
  45. 2
    2
      server/sonar-web/src/main/js/components/icons/BranchLikeIcon.tsx
  46. 3
    3
      server/sonar-web/src/main/js/components/icons/__tests__/BranchLikeIcon-test.tsx
  47. 0
    0
      server/sonar-web/src/main/js/components/icons/__tests__/__snapshots__/BranchLikeIcon-test.tsx.snap
  48. 50
    32
      server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts
  49. 22
    47
      server/sonar-web/src/main/js/helpers/branches.ts
  50. 2
    9
      server/sonar-web/src/main/js/helpers/urls.ts
  51. 78
    0
      server/sonar-web/src/main/js/types/branch-like.d.ts
  52. 31
    0
      server/sonar-web/src/main/js/types/component.ts
  53. 0
    45
      server/sonar-web/src/main/js/types/types.d.ts
  54. 13
    16
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 2
- 2
server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts View File

@@ -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,

+ 72
- 0
server/sonar-web/src/main/js/app/components/nav/component/ComponentBreadcrumb.tsx View File

@@ -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);

+ 0
- 35
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css View File

@@ -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;
}

+ 0
- 2
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx View File

@@ -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}

+ 0
- 231
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx View File

@@ -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);

+ 0
- 263
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx View File

@@ -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);

+ 0
- 77
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx View File

@@ -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>
);
}

+ 24
- 97
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavHeader.tsx View File

@@ -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);

+ 52
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentBreadcrumb-test.tsx View File

@@ -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()}
/>
);
}

+ 0
- 143
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx View File

@@ -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();
});

+ 0
- 101
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx View File

@@ -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' } });
}

+ 18
- 60
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavHeader-test.tsx View File

@@ -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}
/>
);
}

+ 48
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentBreadcrumb-test.tsx.snap View File

@@ -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>
`;

+ 1
- 2
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap View File

@@ -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={

+ 0
- 286
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap View File

@@ -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>
`;

+ 0
- 291
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap View File

@@ -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>
`;

+ 0
- 181
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap View File

@@ -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>
`;

+ 149
- 126
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavHeader-test.tsx.snap View File

@@ -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>
`;

+ 59
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.css View File

@@ -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);
}

+ 93
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx View File

@@ -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));

+ 114
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLike.tsx View File

@@ -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);

+ 51
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/CurrentBranchLikeMergeInformation.tsx View File

@@ -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);

+ 202
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx View File

@@ -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);

+ 67
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItem.tsx View File

@@ -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);

+ 112
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/MenuItemList.tsx View File

@@ -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);

+ 76
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx View File

@@ -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}
/>
);
}

+ 106
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLike-test.tsx View File

@@ -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}
/>
);
}

+ 43
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/CurrentBranchLikeMergeInformation-test.tsx View File

@@ -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} />
);
}

+ 122
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/Menu-test.tsx View File

@@ -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}
/>
);
}

server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx → server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItem-test.tsx View File

@@ -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}
/>
);

+ 50
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/MenuItemList-test.tsx View File

@@ -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}
/>
);
}

+ 201
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap View File

@@ -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>
`;

+ 185
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLike-test.tsx.snap View File

@@ -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>
`;

+ 22
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/CurrentBranchLikeMergeInformation-test.tsx.snap View File

@@ -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>
`;

+ 335
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/Menu-test.tsx.snap View File

@@ -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>
`;

+ 102
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItem-test.tsx.snap View File

@@ -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>
`;

+ 428
- 0
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/MenuItemList-test.tsx.snap View File

@@ -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>
`;

+ 3
- 3
server/sonar-web/src/main/js/apps/projectBaseline/__tests__/__snapshots__/BranchList-test.tsx.snap View File

@@ -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",

+ 2
- 2
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx View File

@@ -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>

+ 2
- 2
server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRowRenderer.tsx View File

@@ -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>

+ 1
- 1
server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTableRenderer.tsx View File

@@ -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>

+ 4
- 4
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeRowRenderer-test.tsx.snap View File

@@ -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",

+ 1
- 1
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeTableRenderer-test.tsx.snap View File

@@ -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"

+ 1
- 1
server/sonar-web/src/main/js/components/hoc/withAppState.tsx View File

@@ -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');

server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx → server/sonar-web/src/main/js/components/icons/BranchLikeIcon.tsx View File

@@ -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 {

server/sonar-web/src/main/js/components/icons-components/__tests__/BranchIcon-test.tsx → server/sonar-web/src/main/js/components/icons/__tests__/BranchLikeIcon-test.tsx View File

@@ -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} />);
}

server/sonar-web/src/main/js/components/icons-components/__tests__/__snapshots__/BranchIcon-test.tsx.snap → server/sonar-web/src/main/js/components/icons/__tests__/__snapshots__/BranchLikeIcon-test.tsx.snap View File


+ 50
- 32
server/sonar-web/src/main/js/helpers/__tests__/branches-test.ts View File

@@ -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]
});
});
});


+ 22
- 47
server/sonar-web/src/main/js/helpers/branches.ts View File

@@ -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);
}
}


+ 2
- 9
server/sonar-web/src/main/js/helpers/urls.ts View File

@@ -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);
}

+ 78
- 0
server/sonar-web/src/main/js/types/branch-like.d.ts View File

@@ -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;
}
}

+ 31
- 0
server/sonar-web/src/main/js/types/component.ts View File

@@ -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'
}

+ 0
- 45
server/sonar-web/src/main/js/types/types.d.ts View File

@@ -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;

+ 13
- 16
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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}

#------------------------------------------------------------------------------
#

Loading…
Cancel
Save