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