aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/projectBranches
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2017-09-25 11:43:24 +0200
committerStas Vilchik <stas.vilchik@sonarsource.com>2017-09-25 13:40:46 +0200
commit3fe5d569ca87b58e1cfbb9253abe6a8c5c912230 (patch)
tree23b8f180e2cb136326e67c1864de758ab659fe7c /server/sonar-web/src/main/js/apps/projectBranches
parent3882435f10d70ef47f4b189b9d032119335e5130 (diff)
downloadsonarqube-3fe5d569ca87b58e1cfbb9253abe6a8c5c912230.tar.gz
sonarqube-3fe5d569ca87b58e1cfbb9253abe6a8c5c912230.zip
revert changes of branches administration
Diffstat (limited to 'server/sonar-web/src/main/js/apps/projectBranches')
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx60
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx139
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx103
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx129
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx34
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx65
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx98
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx95
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap73
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap100
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap104
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap223
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/routes.ts30
13 files changed, 1253 insertions, 0 deletions
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 (
+ <div className="page page-limited">
+ <header className="page-header">
+ <h1 className="page-title">{translate('project_branches.page')}</h1>
+ </header>
+
+ <table className="data zebra zebra-hover">
+ <thead>
+ <tr>
+ <th>{translate('branch')}</th>
+ <th className="text-right">{translate('status')}</th>
+ <th className="text-right">{translate('actions')}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {sortBranchesAsTree(branches).map(branch => (
+ <BranchRow
+ branch={branch}
+ component={component.key}
+ key={branch.name}
+ onChange={onBranchesChange}
+ />
+ ))}
+ </tbody>
+ </table>
+ </div>
+ );
+}
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<Props, State> {
+ mounted: boolean;
+ state: State = { deleting: false, renaming: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleDeleteClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.setState({ deleting: true });
+ };
+
+ handleDeletingStop = () => {
+ this.setState({ deleting: false });
+ };
+
+ handleRenameClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ 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 (
+ <tr>
+ <td>
+ <BranchIcon
+ branch={branch}
+ className={classNames('little-spacer-right', {
+ 'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan
+ })}
+ />
+ {branch.name}
+ {branch.isMain && (
+ <div className="outline-badge spacer-left">{translate('branches.main_branch')}</div>
+ )}
+ </td>
+ <td className="thin nowrap text-right">
+ <BranchStatus branch={branch} />
+ </td>
+ <td className="thin nowrap text-right">
+ {branch.isMain ? (
+ <Tooltip overlay={translate('branches.rename')}>
+ <a className="js-rename link-no-underline" href="#" onClick={this.handleRenameClick}>
+ <ChangeIcon />
+ </a>
+ </Tooltip>
+ ) : (
+ <Tooltip overlay={translate('branches.delete')}>
+ <a className="js-delete link-no-underline" href="#" onClick={this.handleDeleteClick}>
+ <DeleteIcon />
+ </a>
+ </Tooltip>
+ )}
+ </td>
+
+ {this.state.deleting && (
+ <DeleteBranchModal
+ branch={branch}
+ component={component}
+ onClose={this.handleDeletingStop}
+ onDelete={this.handleChange}
+ />
+ )}
+
+ {this.state.renaming && (
+ <RenameBranchModal
+ branch={branch}
+ component={component}
+ onClose={this.handleRenamingStop}
+ onRename={this.handleChange}
+ />
+ )}
+ </tr>
+ );
+ }
+}
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<Props, State> {
+ mounted: boolean;
+ state: State = { loading: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ 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<HTMLAnchorElement>) => {
+ event.preventDefault();
+ this.props.onClose();
+ };
+
+ render() {
+ const { branch } = this.props;
+ const header = translate('branches.delete');
+
+ return (
+ <Modal
+ isOpen={true}
+ contentLabel={header}
+ className="modal"
+ overlayClassName="modal-overlay"
+ onRequestClose={this.props.onClose}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+ <form onSubmit={this.handleSubmit}>
+ <div className="modal-body">
+ {translateWithParameters('branches.delete.are_you_sure', branch.name)}
+ </div>
+ <footer className="modal-foot">
+ {this.state.loading && <i className="spinner spacer-right" />}
+ <button className="button-red" disabled={this.state.loading} type="submit">
+ {translate('delete')}
+ </button>
+ <a href="#" onClick={this.handleCancelClick}>
+ {translate('cancel')}
+ </a>
+ </footer>
+ </form>
+ </Modal>
+ );
+ }
+}
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<Props, State> {
+ mounted: boolean;
+ state: State = { loading: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ 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<HTMLAnchorElement>) => {
+ event.preventDefault();
+ this.props.onClose();
+ };
+
+ handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
+ 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 (
+ <Modal
+ isOpen={true}
+ contentLabel={header}
+ className="modal"
+ overlayClassName="modal-overlay"
+ onRequestClose={this.props.onClose}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+ <form onSubmit={this.handleSubmit}>
+ <div className="modal-body">
+ <div className="modal-field">
+ <label htmlFor="rename-branch-name">
+ {translate('new_name')}
+ <em className="mandatory">*</em>
+ </label>
+ <input
+ autoFocus={true}
+ id="rename-branch-name"
+ maxLength={100}
+ name="name"
+ onChange={this.handleNameChange}
+ required={true}
+ size={50}
+ type="text"
+ value={this.state.name != undefined ? this.state.name : branch.name}
+ />
+ </div>
+ </div>
+ <footer className="modal-foot">
+ {this.state.loading && <i className="spinner spacer-right" />}
+ <button disabled={submitDisabled} type="submit">
+ {translate('rename')}
+ </button>
+ <a href="#" onClick={this.handleCancelClick}>
+ {translate('cancel')}
+ </a>
+ </footer>
+ </form>
+ </Modal>
+ );
+ }
+}
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(<App branches={branches} component={{ key: 'foo' }} onBranchesChange={jest.fn()} />)
+ ).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(<BranchRow branch={branch} component="foo" onChange={onChange} />);
+ (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<any>).mockClear();
+});
+
+it('renders', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({ loading: true });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('deletes branch', () => {
+ (deleteBranch as jest.Mock<any>).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<any>).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(
+ <DeleteBranchModal branch={branch} component="foo" onClose={onClose} onDelete={onDelete} />
+ );
+ (wrapper.instance() as any).mounted = true;
+ return wrapper;
+}
+
+function submitForm(wrapper: ShallowWrapper<any, any>) {
+ 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<any>).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<any>).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<any>).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(
+ <RenameBranchModal branch={branch} component="foo" onClose={onClose} onRename={onRename} />
+ );
+ (wrapper.instance() as any).mounted = true;
+ return wrapper;
+}
+
+function fillAndSubmit(wrapper: ShallowWrapper<any, any>) {
+ 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`] = `
+<div
+ className="page page-limited"
+>
+ <header
+ className="page-header"
+ >
+ <h1
+ className="page-title"
+ >
+ project_branches.page
+ </h1>
+ </header>
+ <table
+ className="data zebra zebra-hover"
+ >
+ <thead>
+ <tr>
+ <th>
+ branch
+ </th>
+ <th
+ className="text-right"
+ >
+ status
+ </th>
+ <th
+ className="text-right"
+ >
+ actions
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <BranchRow
+ branch={
+ Object {
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ component="foo"
+ onChange={[Function]}
+ />
+ <BranchRow
+ branch={
+ Object {
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "branch-1.0",
+ "type": "SHORT",
+ }
+ }
+ component="foo"
+ onChange={[Function]}
+ />
+ <BranchRow
+ branch={
+ Object {
+ "isMain": false,
+ "name": "branch-1.0",
+ "type": "LONG",
+ }
+ }
+ component="foo"
+ onChange={[Function]}
+ />
+ </tbody>
+ </table>
+</div>
+`;
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`] = `
+<tr>
+ <td>
+ <BranchIcon
+ branch={
+ Object {
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ className="little-spacer-right"
+ />
+ master
+ <div
+ className="outline-badge spacer-left"
+ >
+ branches.main_branch
+ </div>
+ </td>
+ <td
+ className="thin nowrap text-right"
+ >
+ <BranchStatus
+ branch={
+ Object {
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ />
+ </td>
+ <td
+ className="thin nowrap text-right"
+ >
+ <Tooltip
+ overlay="branches.rename"
+ placement="bottom"
+ >
+ <a
+ className="js-rename link-no-underline"
+ href="#"
+ onClick={[Function]}
+ >
+ <ChangeIcon />
+ </a>
+ </Tooltip>
+ </td>
+</tr>
+`;
+
+exports[`renders short-living branch 1`] = `
+<tr>
+ <td>
+ <BranchIcon
+ branch={
+ Object {
+ "isMain": false,
+ "mergeBranch": "foo",
+ "name": "feature",
+ "type": "SHORT",
+ }
+ }
+ className="little-spacer-right big-spacer-left"
+ />
+ feature
+ </td>
+ <td
+ className="thin nowrap text-right"
+ >
+ <BranchStatus
+ branch={
+ Object {
+ "isMain": false,
+ "mergeBranch": "foo",
+ "name": "feature",
+ "type": "SHORT",
+ }
+ }
+ />
+ </td>
+ <td
+ className="thin nowrap text-right"
+ >
+ <Tooltip
+ overlay="branches.delete"
+ placement="bottom"
+ >
+ <a
+ className="js-delete link-no-underline"
+ href="#"
+ onClick={[Function]}
+ >
+ <DeleteIcon />
+ </a>
+ </Tooltip>
+ </td>
+</tr>
+`;
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`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel="branches.delete"
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ branches.delete
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ branches.delete.are_you_sure.feature
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <button
+ className="button-red"
+ disabled={false}
+ type="submit"
+ >
+ delete
+ </button>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ cancel
+ </a>
+ </footer>
+ </form>
+</Modal>
+`;
+
+exports[`renders 2`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel="branches.delete"
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ branches.delete
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ branches.delete.are_you_sure.feature
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <i
+ className="spinner spacer-right"
+ />
+ <button
+ className="button-red"
+ disabled={true}
+ type="submit"
+ >
+ delete
+ </button>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ cancel
+ </a>
+ </footer>
+ </form>
+</Modal>
+`;
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`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel="branches.rename"
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ branches.rename
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-field"
+ >
+ <label
+ htmlFor="rename-branch-name"
+ >
+ new_name
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ autoFocus={true}
+ id="rename-branch-name"
+ maxLength={100}
+ name="name"
+ onChange={[Function]}
+ required={true}
+ size={50}
+ type="text"
+ value="master"
+ />
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <button
+ disabled={true}
+ type="submit"
+ >
+ rename
+ </button>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ cancel
+ </a>
+ </footer>
+ </form>
+</Modal>
+`;
+
+exports[`renders 2`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel="branches.rename"
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ branches.rename
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-field"
+ >
+ <label
+ htmlFor="rename-branch-name"
+ >
+ new_name
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ autoFocus={true}
+ id="rename-branch-name"
+ maxLength={100}
+ name="name"
+ onChange={[Function]}
+ required={true}
+ size={50}
+ type="text"
+ value="dev"
+ />
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <button
+ disabled={false}
+ type="submit"
+ >
+ rename
+ </button>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ cancel
+ </a>
+ </footer>
+ </form>
+</Modal>
+`;
+
+exports[`renders 3`] = `
+<Modal
+ ariaHideApp={true}
+ bodyOpenClassName="ReactModal__Body--open"
+ className="modal"
+ closeTimeoutMS={0}
+ contentLabel="branches.rename"
+ isOpen={true}
+ onRequestClose={[Function]}
+ overlayClassName="modal-overlay"
+ parentSelector={[Function]}
+ portalClassName="ReactModalPortal"
+ shouldCloseOnOverlayClick={true}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ branches.rename
+ </h2>
+ </header>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-field"
+ >
+ <label
+ htmlFor="rename-branch-name"
+ >
+ new_name
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ autoFocus={true}
+ id="rename-branch-name"
+ maxLength={100}
+ name="name"
+ onChange={[Function]}
+ required={true}
+ size={50}
+ type="text"
+ value="dev"
+ />
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <i
+ className="spinner spacer-right"
+ />
+ <button
+ disabled={true}
+ type="submit"
+ >
+ rename
+ </button>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ cancel
+ </a>
+ </footer>
+ </form>
+</Modal>
+`;
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;