diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-09-25 11:43:24 +0200 |
---|---|---|
committer | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-09-25 13:40:46 +0200 |
commit | 3fe5d569ca87b58e1cfbb9253abe6a8c5c912230 (patch) | |
tree | 23b8f180e2cb136326e67c1864de758ab659fe7c /server/sonar-web/src/main/js/apps/projectBranches | |
parent | 3882435f10d70ef47f4b189b9d032119335e5130 (diff) | |
download | sonarqube-3fe5d569ca87b58e1cfbb9253abe6a8c5c912230.tar.gz sonarqube-3fe5d569ca87b58e1cfbb9253abe6a8c5c912230.zip |
revert changes of branches administration
Diffstat (limited to 'server/sonar-web/src/main/js/apps/projectBranches')
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; |