From 3fe5d569ca87b58e1cfbb9253abe6a8c5c912230 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Mon, 25 Sep 2017 11:43:24 +0200 Subject: revert changes of branches administration --- .../main/js/app/components/ComponentContainer.tsx | 2 +- .../__tests__/ComponentContainer-test.tsx | 17 ++ .../app/components/nav/component/ComponentNav.tsx | 2 - .../nav/component/ComponentNavBranch.tsx | 2 - .../nav/component/ComponentNavBranchesMenu.tsx | 23 ++- .../nav/component/ComponentNavBranchesMenuItem.tsx | 129 +++--------- .../components/nav/component/ComponentNavMenu.tsx | 21 ++ .../components/nav/component/DeleteBranchModal.tsx | 104 ---------- .../components/nav/component/RenameBranchModal.tsx | 130 ------------ .../nav/component/__tests__/ComponentNav-test.tsx | 6 +- .../__tests__/ComponentNavBranch-test.tsx | 11 +- .../__tests__/ComponentNavBranchesMenu-test.tsx | 11 +- .../ComponentNavBranchesMenuItem-test.tsx | 20 -- .../component/__tests__/DeleteBranchModal-test.tsx | 98 --------- .../component/__tests__/RenameBranchModal-test.tsx | 95 --------- .../ComponentNavBranchesMenu-test.tsx.snap | 7 - .../__snapshots__/ComponentNavMenu-test.tsx.snap | 51 +++++ .../__snapshots__/DeleteBranchModal-test.tsx.snap | 104 ---------- .../__snapshots__/RenameBranchModal-test.tsx.snap | 223 --------------------- .../src/main/js/app/utils/startReactApp.js | 2 + .../js/apps/projectBranches/components/App.tsx | 60 ++++++ .../apps/projectBranches/components/BranchRow.tsx | 139 +++++++++++++ .../components/DeleteBranchModal.tsx | 103 ++++++++++ .../components/RenameBranchModal.tsx | 129 ++++++++++++ .../components/__tests__/App-test.tsx | 34 ++++ .../components/__tests__/BranchRow-test.tsx | 65 ++++++ .../__tests__/DeleteBranchModal-test.tsx | 98 +++++++++ .../__tests__/RenameBranchModal-test.tsx | 95 +++++++++ .../__tests__/__snapshots__/App-test.tsx.snap | 73 +++++++ .../__snapshots__/BranchRow-test.tsx.snap | 100 +++++++++ .../__snapshots__/DeleteBranchModal-test.tsx.snap | 104 ++++++++++ .../__snapshots__/RenameBranchModal-test.tsx.snap | 223 +++++++++++++++++++++ .../src/main/js/apps/projectBranches/routes.ts | 30 +++ .../src/main/js/apps/settings/components/App.js | 2 +- .../main/resources/org/sonar/l10n/core.properties | 5 +- 35 files changed, 1399 insertions(+), 919 deletions(-) delete mode 100644 server/sonar-web/src/main/js/app/components/nav/component/DeleteBranchModal.tsx delete mode 100644 server/sonar-web/src/main/js/app/components/nav/component/RenameBranchModal.tsx delete mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/DeleteBranchModal-test.tsx delete mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/RenameBranchModal-test.tsx delete mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/projectBranches/routes.ts diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx index a1d01845343..5c4287ae231 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -137,7 +137,6 @@ export default class ComponentContainer extends React.PureComponent )} {loading ? ( @@ -149,6 +148,7 @@ export default class ComponentContainer extends React.PureComponent { expect(wrapper.find(Inner).exists()).toBeTruthy(); }); }); + +it('updates branches on change', () => { + (getBranches as jest.Mock).mockImplementation(() => Promise.resolve([])); + const wrapper = shallow( + + + + ); + (wrapper.instance() as ComponentContainer).mounted = true; + wrapper.setState({ + branches: [{ isMain: true }], + component: { breadcrumbs: [{ key: 'projectKey', name: 'project', qualifier: 'TRK' }] }, + loading: false + }); + (wrapper.find(Inner).prop('onBranchesChange') as Function)(); + expect(getBranches).toBeCalledWith('projectKey'); +}); 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 5d78833af2d..5b9a509fdd8 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 @@ -35,7 +35,6 @@ interface Props { currentBranch?: Branch; component: Component; location: {}; - onBranchesChange: () => void; } interface State { @@ -106,7 +105,6 @@ export default class ComponentNav extends React.PureComponent { currentBranch={this.props.currentBranch} // to close dropdown on any location change location={this.props.location} - onBranchesChange={this.props.onBranchesChange} /> )} 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 index 21010a1ec69..7215a3ab825 100644 --- 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 @@ -35,7 +35,6 @@ interface Props { component: Component; currentBranch: Branch; location?: any; - onBranchesChange: () => void; } interface State { @@ -128,7 +127,6 @@ export default class ComponentNavBranch extends React.PureComponent ) : null; 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 index f4ea43b0bc6..21d66918b1f 100644 --- 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 @@ -19,6 +19,7 @@ */ import * as React from 'react'; import * as PropTypes from 'prop-types'; +import { Link } from 'react-router'; import ComponentNavBranchesMenuItem from './ComponentNavBranchesMenuItem'; import { Branch, Component } from '../../../types'; import { @@ -35,7 +36,6 @@ interface Props { canAdmin?: boolean; component: Component; currentBranch: Branch; - onBranchesChange: () => void; onClose: () => void; } @@ -66,9 +66,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent { - // do not close when rename or delete branch modal is open - const modal = document.querySelector('.modal'); - if (!modal && (!this.node || !this.node.contains(event.target as HTMLElement))) { + if (!this.node || !this.node.contains(event.target as HTMLElement)) { this.props.onClose(); } }; @@ -194,10 +192,8 @@ export default class ComponentNavBranchesMenu extends React.PureComponent @@ -208,10 +204,25 @@ export default class ComponentNavBranchesMenu extends React.PureComponent (this.node = node)}> {this.renderSearch()} {this.renderBranchesList()} + {showManageLink && ( +
+ + {translate('branches.manage')} + +
+ )} ); } 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 index a1e02a1aa8f..46f9a02957e 100644 --- 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 @@ -20,125 +20,48 @@ import * as React from 'react'; import { Link } from 'react-router'; import * as classNames from 'classnames'; -import DeleteBranchModal from './DeleteBranchModal'; -import RenameBranchModal from './RenameBranchModal'; import BranchStatus from '../../../../components/common/BranchStatus'; import { Branch, Component } from '../../../types'; import BranchIcon from '../../../../components/icons-components/BranchIcon'; -import ChangeIcon from '../../../../components/icons-components/ChangeIcon'; -import DeleteIcon from '../../../../components/icons-components/DeleteIcon'; import { isShortLivingBranch } from '../../../../helpers/branches'; import { translate } from '../../../../helpers/l10n'; import { getProjectBranchUrl } from '../../../../helpers/urls'; export interface Props { branch: Branch; - canAdmin?: boolean; component: Component; - onBranchesChange: () => void; onSelect: (branch: Branch) => void; selected: boolean; } -interface State { - deleteBranchModal: boolean; - renameBranchModal: boolean; -} - -export default class ComponentNavBranchesMenuItem extends React.PureComponent { - state: State = { deleteBranchModal: false, renameBranchModal: false }; - - handleMouseEnter = () => { - this.props.onSelect(this.props.branch); - }; - - handleDeleteBranchClick = (event: React.SyntheticEvent) => { - event.preventDefault(); - event.currentTarget.blur(); - this.setState({ deleteBranchModal: true }); - }; - - handleDeleteBranchClose = () => { - this.setState({ deleteBranchModal: false }); - }; - - handleBranchDelete = () => { - this.props.onBranchesChange(); - this.setState({ deleteBranchModal: false }); - }; - - handleRenameBranchClick = (event: React.SyntheticEvent) => { - event.preventDefault(); - event.currentTarget.blur(); - this.setState({ renameBranchModal: true }); +export default function ComponentNavBranchesMenuItem({ branch, ...props }: Props) { + const handleMouseEnter = () => { + props.onSelect(branch); }; - handleRenameBranchClose = () => { - this.setState({ renameBranchModal: false }); - }; - - handleBranchRename = () => { - this.props.onBranchesChange(); - this.setState({ renameBranchModal: false }); - }; - - render() { - const { branch } = this.props; - return ( -
  • - -
    - - {branch.name} - {branch.isMain && ( -
    {translate('branches.main_branch')}
    - )} -
    -
    - -
    - {this.props.canAdmin && ( -
    - {branch.isMain ? ( - - ) : ( - - )} -
    - )} - - - {this.state.deleteBranchModal && ( - + +
    + - )} - - {this.state.renameBranchModal && ( - - )} -
  • - ); - } + {branch.name} + {branch.isMain && ( +
    {translate('branches.main_branch')}
    + )} + +
    + +
    + + + ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx index 270829acee0..9cc10de252e 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx @@ -241,6 +241,7 @@ export default class ComponentNavMenu extends React.PureComponent { renderAdministrationLinks() { return [ this.renderSettingsLink(), + this.renderBranchesLink(), this.renderProfilesLink(), this.renderQualityGateLink(), this.renderCustomMeasuresLink(), @@ -274,6 +275,26 @@ export default class ComponentNavMenu extends React.PureComponent { ); } + renderBranchesLink() { + if ( + !this.context.branchesEnabled || + !this.isProject() || + !this.getConfiguration().showSettings + ) { + return null; + } + + return ( +
  • + + {translate('project_branches.page')} + +
  • + ); + } + renderProfilesLink() { if (!this.getConfiguration().showQualityProfiles) { return null; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/DeleteBranchModal.tsx b/server/sonar-web/src/main/js/app/components/nav/component/DeleteBranchModal.tsx deleted file mode 100644 index 2273692a9a8..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/DeleteBranchModal.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact 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 Modal from 'react-modal'; -import { deleteBranch } from '../../../../api/branches'; -import { Branch } from '../../../../app/types'; -import { translate, translateWithParameters } from '../../../../helpers/l10n'; - -interface Props { - branch: Branch; - component: string; - onClose: () => void; - onDelete: () => void; -} - -interface State { - loading: boolean; -} - -export default class DeleteBranchModal extends React.PureComponent { - mounted: boolean; - state: State = { loading: false }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - handleSubmit = (event: React.SyntheticEvent) => { - event.preventDefault(); - this.setState({ loading: true }); - deleteBranch(this.props.component, this.props.branch.name).then( - () => { - if (this.mounted) { - this.setState({ loading: false }); - this.props.onDelete(); - } - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); - }; - - handleCancelClick = (event: React.SyntheticEvent) => { - event.preventDefault(); - event.stopPropagation(); - this.props.onClose(); - }; - - render() { - const { branch } = this.props; - const header = translate('branches.delete'); - - return ( - -
    -

    {header}

    -
    -
    -
    - {translateWithParameters('branches.delete.are_you_sure', branch.name)} -
    - - -
    - ); - } -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/RenameBranchModal.tsx b/server/sonar-web/src/main/js/app/components/nav/component/RenameBranchModal.tsx deleted file mode 100644 index b17401c4e08..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/RenameBranchModal.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact 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 Modal from 'react-modal'; -import { renameBranch } from '../../../../api/branches'; -import { Branch } from '../../../../app/types'; -import { translate } from '../../../../helpers/l10n'; - -interface Props { - branch: Branch; - component: string; - onClose: () => void; - onRename: () => void; -} - -interface State { - loading: boolean; - name?: string; -} - -export default class RenameBranchModal extends React.PureComponent { - mounted: boolean; - state: State = { loading: false }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - handleSubmit = (event: React.SyntheticEvent) => { - event.preventDefault(); - if (!this.state.name) { - return; - } - this.setState({ loading: true }); - renameBranch(this.props.component, this.state.name).then( - () => { - if (this.mounted) { - this.setState({ loading: false }); - this.props.onRename(); - } - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); - }; - - handleCancelClick = (event: React.SyntheticEvent) => { - event.preventDefault(); - event.stopPropagation(); - this.props.onClose(); - }; - - handleNameChange = (event: React.SyntheticEvent) => { - this.setState({ name: event.currentTarget.value }); - }; - - render() { - const { branch } = this.props; - const header = translate('branches.rename'); - const submitDisabled = - this.state.loading || !this.state.name || this.state.name === branch.name; - - return ( - -
    -

    {header}

    -
    -
    -
    -
    - - -
    -
    - - -
    - ); - } -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx index 6adc8751910..f63e785ca5a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx @@ -62,15 +62,13 @@ const component = { it('loads status', () => { getTasksForComponent.mockClear(); - mount( - - ); + mount(); expect(getTasksForComponent).toBeCalledWith('component'); }); it('renders', () => { const wrapper = shallow( - + ); wrapper.setState({ incremental: true, 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 index 5a2343bb4c5..8da338c23e1 100644 --- 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 @@ -40,7 +40,6 @@ it('renders main branch', () => { branches={[branch, fooBranch]} component={component} currentBranch={branch} - onBranchesChange={jest.fn()} />, { context: { branchesEnabled: true } } ) @@ -62,7 +61,6 @@ it('renders short-living branch', () => { branches={[branch, fooBranch]} component={component} currentBranch={branch} - onBranchesChange={jest.fn()} />, { context: { branchesEnabled: true } } ) @@ -77,7 +75,6 @@ it('opens menu', () => { branches={[branch, fooBranch]} component={component} currentBranch={branch} - onBranchesChange={jest.fn()} />, { context: { branchesEnabled: true } } ); @@ -90,12 +87,7 @@ it('renders single branch popup', () => { const branch: MainBranch = { isMain: true, name: 'master' }; const component = {} as Component; const wrapper = shallow( - , + , { context: { branchesEnabled: true } } ); expect(wrapper).toMatchSnapshot(); @@ -112,7 +104,6 @@ it('renders nothing when no branch support', () => { branches={[branch, fooBranch]} component={component} currentBranch={branch} - onBranchesChange={jest.fn()} />, { context: { branchesEnabled: false } } ); 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 index 13e251e059a..6232c936b4d 100644 --- 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 @@ -29,16 +29,15 @@ import { } from '../../../../types'; import { elementKeydown } from '../../../../../helpers/testUtils'; -const project = { key: 'component' } as Component; +const component = { key: 'component' } as Component; it('renders list', () => { expect( shallow( ) @@ -49,9 +48,8 @@ it('searches', () => { const wrapper = shallow( ); @@ -63,9 +61,8 @@ it('selects next & previous', () => { const wrapper = shallow( ); 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/__tests__/ComponentNavBranchesMenuItem-test.tsx index c8ff060fa56..75887b7c2f6 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/__tests__/ComponentNavBranchesMenuItem-test.tsx @@ -21,7 +21,6 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import ComponentNavBranchesMenuItem, { Props } from '../ComponentNavBranchesMenuItem'; import { BranchType, MainBranch, ShortLivingBranch, Component } from '../../../../types'; -import { click } from '../../../../../helpers/testUtils'; const component = { key: 'component' } as Component; @@ -47,30 +46,11 @@ it('renders short-living orhpan branch', () => { expect(shallowRender({ branch: { ...shortBranch, isOrphan: true } })).toMatchSnapshot(); }); -it('renames main branch', () => { - const onBranchesChange = jest.fn(); - const wrapper = shallowRender({ branch: mainBranch, canAdmin: true, onBranchesChange }); - - click(wrapper.find('.js-rename')); - (wrapper.find('RenameBranchModal').prop('onRename') as Function)(); - expect(onBranchesChange).toBeCalled(); -}); - -it('deletes short-living branch', () => { - const onBranchesChange = jest.fn(); - const wrapper = shallowRender({ canAdmin: true, onBranchesChange }); - - click(wrapper.find('.js-delete')); - (wrapper.find('DeleteBranchModal').prop('onDelete') as Function)(); - expect(onBranchesChange).toBeCalled(); -}); - function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { return shallow( ({ deleteBranch: jest.fn() })); - -import * as React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; -import DeleteBranchModal from '../DeleteBranchModal'; -import { ShortLivingBranch, BranchType } from '../../../../../app/types'; -import { submit, doAsync, click } from '../../../../../helpers/testUtils'; -import { deleteBranch } from '../../../../../api/branches'; - -beforeEach(() => { - (deleteBranch as jest.Mock).mockClear(); -}); - -it('renders', () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); - wrapper.setState({ loading: true }); - expect(wrapper).toMatchSnapshot(); -}); - -it('deletes branch', () => { - (deleteBranch as jest.Mock).mockImplementation(() => Promise.resolve()); - const onDelete = jest.fn(); - const wrapper = shallowRender(onDelete); - - submitForm(wrapper); - - return doAsync().then(() => { - wrapper.update(); - expect(wrapper.state().loading).toBe(false); - expect(onDelete).toBeCalled(); - expect(deleteBranch).toBeCalledWith('foo', 'feature'); - }); -}); - -it('cancels', () => { - const onClose = jest.fn(); - const wrapper = shallowRender(jest.fn(), onClose); - - click(wrapper.find('a')); - - return doAsync().then(() => { - expect(onClose).toBeCalled(); - }); -}); - -it('stops loading on WS error', () => { - (deleteBranch as jest.Mock).mockImplementation(() => Promise.reject(null)); - const onDelete = jest.fn(); - const wrapper = shallowRender(onDelete); - - submitForm(wrapper); - - return doAsync().then(() => { - wrapper.update(); - expect(wrapper.state().loading).toBe(false); - expect(onDelete).not.toBeCalled(); - expect(deleteBranch).toBeCalledWith('foo', 'feature'); - }); -}); - -function shallowRender(onDelete: () => void = jest.fn(), onClose: () => void = jest.fn()) { - const branch: ShortLivingBranch = { - isMain: false, - name: 'feature', - mergeBranch: 'master', - type: BranchType.SHORT - }; - const wrapper = shallow( - - ); - (wrapper.instance() as any).mounted = true; - return wrapper; -} - -function submitForm(wrapper: ShallowWrapper) { - submit(wrapper.find('form')); - expect(wrapper.state().loading).toBe(true); -} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/RenameBranchModal-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/RenameBranchModal-test.tsx deleted file mode 100644 index c1a0750e5bb..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/RenameBranchModal-test.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact 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. - */ -jest.mock('../../../../../api/branches', () => ({ renameBranch: jest.fn() })); - -import * as React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; -import RenameBranchModal from '../RenameBranchModal'; -import { MainBranch } from '../../../../../app/types'; -import { submit, doAsync, click, change } from '../../../../../helpers/testUtils'; -import { renameBranch } from '../../../../../api/branches'; - -beforeEach(() => { - (renameBranch as jest.Mock).mockClear(); -}); - -it('renders', () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); - wrapper.setState({ name: 'dev' }); - expect(wrapper).toMatchSnapshot(); - wrapper.setState({ loading: true }); - expect(wrapper).toMatchSnapshot(); -}); - -it('renames branch', () => { - (renameBranch as jest.Mock).mockImplementation(() => Promise.resolve()); - const onRename = jest.fn(); - const wrapper = shallowRender(onRename); - - fillAndSubmit(wrapper); - - return doAsync().then(() => { - wrapper.update(); - expect(wrapper.state().loading).toBe(false); - expect(onRename).toBeCalled(); - expect(renameBranch).toBeCalledWith('foo', 'dev'); - }); -}); - -it('cancels', () => { - const onClose = jest.fn(); - const wrapper = shallowRender(jest.fn(), onClose); - - click(wrapper.find('a')); - - return doAsync().then(() => { - expect(onClose).toBeCalled(); - }); -}); - -it('stops loading on WS error', () => { - (renameBranch as jest.Mock).mockImplementation(() => Promise.reject(null)); - const onRename = jest.fn(); - const wrapper = shallowRender(onRename); - - fillAndSubmit(wrapper); - - return doAsync().then(() => { - wrapper.update(); - expect(wrapper.state().loading).toBe(false); - expect(onRename).not.toBeCalled(); - }); -}); - -function shallowRender(onRename: () => void = jest.fn(), onClose: () => void = jest.fn()) { - const branch: MainBranch = { isMain: true, name: 'master' }; - const wrapper = shallow( - - ); - (wrapper.instance() as any).mounted = true; - return wrapper; -} - -function fillAndSubmit(wrapper: ShallowWrapper) { - change(wrapper.find('input'), 'dev'); - submit(wrapper.find('form')); - expect(wrapper.state().loading).toBe(true); -} 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 index eda0f3f1cb5..463fb48d493 100644 --- 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 @@ -39,7 +39,6 @@ exports[`renders list 1`] = ` "key": "component", } } - onBranchesChange={[Function]} onSelect={[Function]} selected={true} /> @@ -79,7 +78,6 @@ exports[`renders list 1`] = ` "key": "component", } } - onBranchesChange={[Function]} onSelect={[Function]} selected={false} /> @@ -103,7 +101,6 @@ exports[`renders list 1`] = ` "key": "component", } } - onBranchesChange={[Function]} onSelect={[Function]} selected={false} /> @@ -123,7 +120,6 @@ exports[`renders list 1`] = ` "key": "component", } } - onBranchesChange={[Function]} onSelect={[Function]} selected={false} /> @@ -163,7 +159,6 @@ exports[`renders list 1`] = ` "key": "component", } } - onBranchesChange={[Function]} onSelect={[Function]} selected={false} /> @@ -218,7 +213,6 @@ exports[`searches 1`] = ` "key": "component", } } - onBranchesChange={[Function]} onSelect={[Function]} selected={true} /> @@ -238,7 +232,6 @@ exports[`searches 1`] = ` "key": "component", } } - onBranchesChange={[Function]} onSelect={[Function]} selected={false} /> diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap index 7b305c2b763..60d527b6a42 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap @@ -129,6 +129,23 @@ exports[`should work for all qualifiers 1`] = ` project_settings.page +
  • + + project_branches.page + +
  • +
  • + + project_branches.page + +
  • +
  • + + project_branches.page + +
  • -
    -

    - branches.delete -

    -
    -
    -
    - branches.delete.are_you_sure.feature -
    - -
    - -`; - -exports[`renders 2`] = ` - -
    -

    - branches.delete -

    -
    -
    -
    - branches.delete.are_you_sure.feature -
    - - -
    -`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap deleted file mode 100644 index 7867fa4785a..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap +++ /dev/null @@ -1,223 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` - -
    -

    - branches.rename -

    -
    -
    -
    -
    - - -
    -
    - -
    -
    -`; - -exports[`renders 2`] = ` - -
    -

    - branches.rename -

    -
    -
    -
    -
    - - -
    -
    - -
    -
    -`; - -exports[`renders 3`] = ` - -
    -

    - branches.rename -

    -
    -
    -
    -
    - - -
    -
    - - -
    -`; diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js index 1b8f7f606c3..b6577565efd 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.js +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js @@ -55,6 +55,7 @@ import permissionTemplatesRoutes from '../../apps/permission-templates/routes'; import portfolioRoutes from '../../apps/portfolio/routes'; import projectActivityRoutes from '../../apps/projectActivity/routes'; import projectAdminRoutes from '../../apps/project-admin/routes'; +import projectBranchesRoutes from '../../apps/projectBranches/routes'; import projectQualityGateRoutes from '../../apps/projectQualityGate/routes'; import projectQualityProfilesRoutes from '../../apps/projectQualityProfiles/routes'; import projectsRoutes from '../../apps/projects/routes'; @@ -206,6 +207,7 @@ const startReactApp = () => { component={ProjectAdminPageExtension} /> + diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx new file mode 100644 index 00000000000..02a8e20365f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 BranchRow from './BranchRow'; +import { Branch } from '../../../app/types'; +import { sortBranchesAsTree } from '../../../helpers/branches'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + branches: Branch[]; + component: { key: string }; + onBranchesChange: () => void; +} + +export default function App({ branches, component, onBranchesChange }: Props) { + return ( +
    +
    +

    {translate('project_branches.page')}

    +
    + + + + + + + + + + + {sortBranchesAsTree(branches).map(branch => ( + + ))} + +
    {translate('branch')}{translate('status')}{translate('actions')}
    +
    + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx new file mode 100644 index 00000000000..e163481d759 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx @@ -0,0 +1,139 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 { Branch } from '../../../app/types'; +import * as classNames from 'classnames'; +import DeleteBranchModal from './DeleteBranchModal'; +import BranchStatus from '../../../components/common/BranchStatus'; +import BranchIcon from '../../../components/icons-components/BranchIcon'; +import { isShortLivingBranch } from '../../../helpers/branches'; +import ChangeIcon from '../../../components/icons-components/ChangeIcon'; +import DeleteIcon from '../../../components/icons-components/DeleteIcon'; +import { translate } from '../../../helpers/l10n'; +import Tooltip from '../../../components/controls/Tooltip'; +import RenameBranchModal from './RenameBranchModal'; + +interface Props { + branch: Branch; + component: string; + onChange: () => void; +} + +interface State { + deleting: boolean; + renaming: boolean; +} + +export default class BranchRow extends React.PureComponent { + mounted: boolean; + state: State = { deleting: false, renaming: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleDeleteClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + this.setState({ deleting: true }); + }; + + handleDeletingStop = () => { + this.setState({ deleting: false }); + }; + + handleRenameClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + this.setState({ renaming: true }); + }; + + handleChange = () => { + if (this.mounted) { + this.setState({ deleting: false, renaming: false }); + this.props.onChange(); + } + }; + + handleRenamingStop = () => { + this.setState({ renaming: false }); + }; + + render() { + const { branch, component } = this.props; + + return ( + + + + {branch.name} + {branch.isMain && ( +
    {translate('branches.main_branch')}
    + )} + + + + + + {branch.isMain ? ( + + + + + + ) : ( + + + + + + )} + + + {this.state.deleting && ( + + )} + + {this.state.renaming && ( + + )} + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx new file mode 100644 index 00000000000..66d14ed260f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx @@ -0,0 +1,103 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 Modal from 'react-modal'; +import { deleteBranch } from '../../../api/branches'; +import { Branch } from '../../../app/types'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +interface Props { + branch: Branch; + component: string; + onClose: () => void; + onDelete: () => void; +} + +interface State { + loading: boolean; +} + +export default class DeleteBranchModal extends React.PureComponent { + mounted: boolean; + state: State = { loading: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleSubmit = (event: React.SyntheticEvent) => { + event.preventDefault(); + this.setState({ loading: true }); + deleteBranch(this.props.component, this.props.branch.name).then( + () => { + if (this.mounted) { + this.setState({ loading: false }); + this.props.onDelete(); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + }; + + handleCancelClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + this.props.onClose(); + }; + + render() { + const { branch } = this.props; + const header = translate('branches.delete'); + + return ( + +
    +

    {header}

    +
    +
    +
    + {translateWithParameters('branches.delete.are_you_sure', branch.name)} +
    + + +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx new file mode 100644 index 00000000000..181fee72365 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx @@ -0,0 +1,129 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 Modal from 'react-modal'; +import { renameBranch } from '../../../api/branches'; +import { Branch } from '../../../app/types'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + branch: Branch; + component: string; + onClose: () => void; + onRename: () => void; +} + +interface State { + loading: boolean; + name?: string; +} + +export default class RenameBranchModal extends React.PureComponent { + mounted: boolean; + state: State = { loading: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleSubmit = (event: React.SyntheticEvent) => { + event.preventDefault(); + if (!this.state.name) { + return; + } + this.setState({ loading: true }); + renameBranch(this.props.component, this.state.name).then( + () => { + if (this.mounted) { + this.setState({ loading: false }); + this.props.onRename(); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + }; + + handleCancelClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + this.props.onClose(); + }; + + handleNameChange = (event: React.SyntheticEvent) => { + this.setState({ name: event.currentTarget.value }); + }; + + render() { + const { branch } = this.props; + const header = translate('branches.rename'); + const submitDisabled = + this.state.loading || !this.state.name || this.state.name === branch.name; + + return ( + +
    +

    {header}

    +
    +
    +
    +
    + + +
    +
    + + +
    + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx new file mode 100644 index 00000000000..4288105f79a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import App from '../App'; +import { Branch, BranchType } from '../../../../app/types'; + +it('renders sorted list of branches', () => { + const branches: Branch[] = [ + { isMain: true, name: 'master' }, + { isMain: false, name: 'branch-1.0', type: BranchType.LONG }, + { isMain: false, name: 'branch-1.0', mergeBranch: 'master', type: BranchType.SHORT } + ]; + expect( + shallow() + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx new file mode 100644 index 00000000000..4edc3ce70d6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx @@ -0,0 +1,65 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import BranchRow from '../BranchRow'; +import { MainBranch, ShortLivingBranch, BranchType } from '../../../../app/types'; +import { click } from '../../../../helpers/testUtils'; + +const mainBranch: MainBranch = { isMain: true, name: 'master' }; + +const shortBranch: ShortLivingBranch = { + isMain: false, + name: 'feature', + mergeBranch: 'foo', + type: BranchType.SHORT +}; + +it('renders main branch', () => { + expect(shallowRender(mainBranch)).toMatchSnapshot(); +}); + +it('renders short-living branch', () => { + expect(shallowRender(shortBranch)).toMatchSnapshot(); +}); + +it('renames main branch', () => { + const onChange = jest.fn(); + const wrapper = shallowRender(mainBranch, onChange); + + click(wrapper.find('.js-rename')); + (wrapper.find('RenameBranchModal').prop('onRename') as Function)(); + expect(onChange).toBeCalled(); +}); + +it('deletes short-living branch', () => { + const onChange = jest.fn(); + const wrapper = shallowRender(shortBranch, onChange); + + click(wrapper.find('.js-delete')); + (wrapper.find('DeleteBranchModal').prop('onDelete') as Function)(); + expect(onChange).toBeCalled(); +}); + +function shallowRender(branch: MainBranch | ShortLivingBranch, onChange: () => void = jest.fn()) { + const wrapper = shallow(); + (wrapper.instance() as any).mounted = true; + return wrapper; +} diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx new file mode 100644 index 00000000000..b2870587114 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx @@ -0,0 +1,98 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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. + */ +jest.mock('../../../../api/branches', () => ({ deleteBranch: jest.fn() })); + +import * as React from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; +import DeleteBranchModal from '../DeleteBranchModal'; +import { ShortLivingBranch, BranchType } from '../../../../app/types'; +import { submit, doAsync, click } from '../../../../helpers/testUtils'; +import { deleteBranch } from '../../../../api/branches'; + +beforeEach(() => { + (deleteBranch as jest.Mock).mockClear(); +}); + +it('renders', () => { + const wrapper = shallowRender(); + expect(wrapper).toMatchSnapshot(); + wrapper.setState({ loading: true }); + expect(wrapper).toMatchSnapshot(); +}); + +it('deletes branch', () => { + (deleteBranch as jest.Mock).mockImplementation(() => Promise.resolve()); + const onDelete = jest.fn(); + const wrapper = shallowRender(onDelete); + + submitForm(wrapper); + + return doAsync().then(() => { + wrapper.update(); + expect(wrapper.state().loading).toBe(false); + expect(onDelete).toBeCalled(); + expect(deleteBranch).toBeCalledWith('foo', 'feature'); + }); +}); + +it('cancels', () => { + const onClose = jest.fn(); + const wrapper = shallowRender(jest.fn(), onClose); + + click(wrapper.find('a')); + + return doAsync().then(() => { + expect(onClose).toBeCalled(); + }); +}); + +it('stops loading on WS error', () => { + (deleteBranch as jest.Mock).mockImplementation(() => Promise.reject(null)); + const onDelete = jest.fn(); + const wrapper = shallowRender(onDelete); + + submitForm(wrapper); + + return doAsync().then(() => { + wrapper.update(); + expect(wrapper.state().loading).toBe(false); + expect(onDelete).not.toBeCalled(); + expect(deleteBranch).toBeCalledWith('foo', 'feature'); + }); +}); + +function shallowRender(onDelete: () => void = jest.fn(), onClose: () => void = jest.fn()) { + const branch: ShortLivingBranch = { + isMain: false, + name: 'feature', + mergeBranch: 'master', + type: BranchType.SHORT + }; + const wrapper = shallow( + + ); + (wrapper.instance() as any).mounted = true; + return wrapper; +} + +function submitForm(wrapper: ShallowWrapper) { + submit(wrapper.find('form')); + expect(wrapper.state().loading).toBe(true); +} diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx new file mode 100644 index 00000000000..3a1c962d68e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx @@ -0,0 +1,95 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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. + */ +jest.mock('../../../../api/branches', () => ({ renameBranch: jest.fn() })); + +import * as React from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; +import RenameBranchModal from '../RenameBranchModal'; +import { MainBranch } from '../../../../app/types'; +import { submit, doAsync, click, change } from '../../../../helpers/testUtils'; +import { renameBranch } from '../../../../api/branches'; + +beforeEach(() => { + (renameBranch as jest.Mock).mockClear(); +}); + +it('renders', () => { + const wrapper = shallowRender(); + expect(wrapper).toMatchSnapshot(); + wrapper.setState({ name: 'dev' }); + expect(wrapper).toMatchSnapshot(); + wrapper.setState({ loading: true }); + expect(wrapper).toMatchSnapshot(); +}); + +it('renames branch', () => { + (renameBranch as jest.Mock).mockImplementation(() => Promise.resolve()); + const onRename = jest.fn(); + const wrapper = shallowRender(onRename); + + fillAndSubmit(wrapper); + + return doAsync().then(() => { + wrapper.update(); + expect(wrapper.state().loading).toBe(false); + expect(onRename).toBeCalled(); + expect(renameBranch).toBeCalledWith('foo', 'dev'); + }); +}); + +it('cancels', () => { + const onClose = jest.fn(); + const wrapper = shallowRender(jest.fn(), onClose); + + click(wrapper.find('a')); + + return doAsync().then(() => { + expect(onClose).toBeCalled(); + }); +}); + +it('stops loading on WS error', () => { + (renameBranch as jest.Mock).mockImplementation(() => Promise.reject(null)); + const onRename = jest.fn(); + const wrapper = shallowRender(onRename); + + fillAndSubmit(wrapper); + + return doAsync().then(() => { + wrapper.update(); + expect(wrapper.state().loading).toBe(false); + expect(onRename).not.toBeCalled(); + }); +}); + +function shallowRender(onRename: () => void = jest.fn(), onClose: () => void = jest.fn()) { + const branch: MainBranch = { isMain: true, name: 'master' }; + const wrapper = shallow( + + ); + (wrapper.instance() as any).mounted = true; + return wrapper; +} + +function fillAndSubmit(wrapper: ShallowWrapper) { + change(wrapper.find('input'), 'dev'); + submit(wrapper.find('form')); + expect(wrapper.state().loading).toBe(true); +} diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap new file mode 100644 index 00000000000..6f983e33df8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders sorted list of branches 1`] = ` +
    +
    +

    + project_branches.page +

    +
    + + + + + + + + + + + + + +
    + branch + + status + + actions +
    +
    +`; diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap new file mode 100644 index 00000000000..ea135765ece --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap @@ -0,0 +1,100 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders main branch 1`] = ` + + + + master +
    + branches.main_branch +
    + + + + + + + + + + + + +`; + +exports[`renders short-living branch 1`] = ` + + + + feature + + + + + + + + + + + + +`; diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap new file mode 100644 index 00000000000..934f8ed2d7d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap @@ -0,0 +1,104 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` + +
    +

    + branches.delete +

    +
    +
    +
    + branches.delete.are_you_sure.feature +
    + +
    +
    +`; + +exports[`renders 2`] = ` + +
    +

    + branches.delete +

    +
    +
    +
    + branches.delete.are_you_sure.feature +
    + + +
    +`; diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap new file mode 100644 index 00000000000..7867fa4785a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap @@ -0,0 +1,223 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` + +
    +

    + branches.rename +

    +
    +
    +
    +
    + + +
    +
    + +
    +
    +`; + +exports[`renders 2`] = ` + +
    +

    + branches.rename +

    +
    +
    +
    +
    + + +
    +
    + +
    +
    +`; + +exports[`renders 3`] = ` + +
    +

    + branches.rename +

    +
    +
    +
    +
    + + +
    +
    + + +
    +`; diff --git a/server/sonar-web/src/main/js/apps/projectBranches/routes.ts b/server/sonar-web/src/main/js/apps/projectBranches/routes.ts new file mode 100644 index 00000000000..520805ebac5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/routes.ts @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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 { RouterState, IndexRouteProps } from 'react-router'; + +const routes = [ + { + getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) { + import('./components/App').then(i => callback(null, { component: (i as any).default })); + } + } +]; + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/settings/components/App.js b/server/sonar-web/src/main/js/apps/settings/components/App.js index dff1d0b74d5..866830fbb69 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/App.js +++ b/server/sonar-web/src/main/js/apps/settings/components/App.js @@ -98,7 +98,7 @@ export default class App extends React.PureComponent { link: ( {translate('branches.settings_hint_tab')} 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 34add05186a..eb6a8c88bfa 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3178,13 +3178,14 @@ branches.no_support.header.text=Analyze each branch of your project separately w branches.delete=Delete Branch branches.delete.are_you_sure=Are you sure you want to delete branch "{0}"? branches.rename=Rename Branch +branches.manage=Manage branches branches.orphan_branch=Orphan Branch branches.orphan_branches=Orphan Branches branches.orphan_branches.tooltip=When a target branch of a short-living branch was deleted, this short-living branch becomes orphan. branches.main_branch=Main Branch branches.branch_settings=Branch Settings -branches.settings_hint=To administrate your project, you have to go to your main branch's {link} tab. -branches.settings_hint_tab=Administration +branches.settings_hint=To administrate your branches, you have to go to your main branch's {link} tab. +branches.settings_hint_tab=Administration > Branches #------------------------------------------------------------------------------ -- cgit v1.2.3