@@ -137,7 +137,6 @@ export default class ComponentContainer extends React.PureComponent<Props, State | |||
currentBranch={branch} | |||
component={component} | |||
location={this.props.location} | |||
onBranchesChange={this.handleBranchesChange} | |||
/> | |||
)} | |||
{loading ? ( | |||
@@ -149,6 +148,7 @@ export default class ComponentContainer extends React.PureComponent<Props, State | |||
branch, | |||
branches, | |||
component, | |||
onBranchesChange: this.handleBranchesChange, | |||
onComponentChange: this.handleComponentChange | |||
}) | |||
)} |
@@ -106,3 +106,20 @@ it("doesn't load branches portfolio", () => { | |||
expect(wrapper.find(Inner).exists()).toBeTruthy(); | |||
}); | |||
}); | |||
it('updates branches on change', () => { | |||
(getBranches as jest.Mock<any>).mockImplementation(() => Promise.resolve([])); | |||
const wrapper = shallow( | |||
<ComponentContainer location={{ query: { id: 'portfolioKey' } }}> | |||
<Inner /> | |||
</ComponentContainer> | |||
); | |||
(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'); | |||
}); |
@@ -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<Props, State> { | |||
currentBranch={this.props.currentBranch} | |||
// to close dropdown on any location change | |||
location={this.props.location} | |||
onBranchesChange={this.props.onBranchesChange} | |||
/> | |||
)} | |||
@@ -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<Props, State | |||
canAdmin={configuration && configuration.showSettings} | |||
component={this.props.component} | |||
currentBranch={this.props.currentBranch} | |||
onBranchesChange={this.props.onBranchesChange} | |||
onClose={this.closeDropdown} | |||
/> | |||
) : null; |
@@ -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<Props, | |||
); | |||
handleClickOutside = (event: Event) => { | |||
// 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<Props, | |||
menu.push( | |||
<ComponentNavBranchesMenuItem | |||
branch={branch} | |||
canAdmin={this.props.canAdmin} | |||
component={this.props.component} | |||
key={branch.name} | |||
onBranchesChange={this.props.onBranchesChange} | |||
onSelect={this.handleSelect} | |||
selected={branch.name === selected} | |||
/> | |||
@@ -208,10 +204,25 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, | |||
}; | |||
render() { | |||
const { component } = this.props; | |||
const showManageLink = | |||
component.qualifier === 'TRK' && | |||
component.configuration && | |||
component.configuration.showSettings; | |||
return ( | |||
<div className="dropdown-menu dropdown-menu-shadow" ref={node => (this.node = node)}> | |||
{this.renderSearch()} | |||
{this.renderBranchesList()} | |||
{showManageLink && ( | |||
<div className="dropdown-bottom-hint text-right"> | |||
<Link | |||
className="text-muted" | |||
to={{ pathname: '/project/branches', query: { id: component.key } }}> | |||
{translate('branches.manage')} | |||
</Link> | |||
</div> | |||
)} | |||
</div> | |||
); | |||
} |
@@ -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<Props, State> { | |||
state: State = { deleteBranchModal: false, renameBranchModal: false }; | |||
handleMouseEnter = () => { | |||
this.props.onSelect(this.props.branch); | |||
}; | |||
handleDeleteBranchClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { | |||
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<HTMLButtonElement>) => { | |||
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 ( | |||
<li key={branch.name} onMouseEnter={this.handleMouseEnter}> | |||
<Link | |||
className={classNames('navbar-context-meta-branch-menu-item', { | |||
active: this.props.selected | |||
})} | |||
to={getProjectBranchUrl(this.props.component.key, branch)}> | |||
<div className="navbar-context-meta-branch-menu-item-name"> | |||
<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> | |||
)} | |||
</div> | |||
<div className="big-spacer-left note"> | |||
<BranchStatus branch={branch} concise={true} /> | |||
</div> | |||
{this.props.canAdmin && ( | |||
<div className="navbar-context-meta-branch-menu-item-actions"> | |||
{branch.isMain ? ( | |||
<button className="js-rename button-link" onClick={this.handleRenameBranchClick}> | |||
<ChangeIcon /> | |||
</button> | |||
) : ( | |||
<button className="js-delete button-link" onClick={this.handleDeleteBranchClick}> | |||
<DeleteIcon /> | |||
</button> | |||
)} | |||
</div> | |||
)} | |||
</Link> | |||
{this.state.deleteBranchModal && ( | |||
<DeleteBranchModal | |||
return ( | |||
<li key={branch.name} onMouseEnter={handleMouseEnter}> | |||
<Link | |||
className={classNames('navbar-context-meta-branch-menu-item', { | |||
active: props.selected | |||
})} | |||
to={getProjectBranchUrl(props.component.key, branch)}> | |||
<div className="navbar-context-meta-branch-menu-item-name"> | |||
<BranchIcon | |||
branch={branch} | |||
component={this.props.component.key} | |||
onClose={this.handleDeleteBranchClose} | |||
onDelete={this.handleBranchDelete} | |||
className={classNames('little-spacer-right', { | |||
'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan | |||
})} | |||
/> | |||
)} | |||
{this.state.renameBranchModal && ( | |||
<RenameBranchModal | |||
branch={branch} | |||
component={this.props.component.key} | |||
onClose={this.handleRenameBranchClose} | |||
onRename={this.handleBranchRename} | |||
/> | |||
)} | |||
</li> | |||
); | |||
} | |||
{branch.name} | |||
{branch.isMain && ( | |||
<div className="outline-badge spacer-left">{translate('branches.main_branch')}</div> | |||
)} | |||
</div> | |||
<div className="big-spacer-left note"> | |||
<BranchStatus branch={branch} concise={true} /> | |||
</div> | |||
</Link> | |||
</li> | |||
); | |||
} |
@@ -241,6 +241,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
renderAdministrationLinks() { | |||
return [ | |||
this.renderSettingsLink(), | |||
this.renderBranchesLink(), | |||
this.renderProfilesLink(), | |||
this.renderQualityGateLink(), | |||
this.renderCustomMeasuresLink(), | |||
@@ -274,6 +275,26 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
); | |||
} | |||
renderBranchesLink() { | |||
if ( | |||
!this.context.branchesEnabled || | |||
!this.isProject() || | |||
!this.getConfiguration().showSettings | |||
) { | |||
return null; | |||
} | |||
return ( | |||
<li key="branches"> | |||
<Link | |||
to={{ pathname: '/project/branches', query: { id: this.props.component.key } }} | |||
activeClassName="active"> | |||
{translate('project_branches.page')} | |||
</Link> | |||
</li> | |||
); | |||
} | |||
renderProfilesLink() { | |||
if (!this.getConfiguration().showQualityProfiles) { | |||
return null; |
@@ -62,15 +62,13 @@ const component = { | |||
it('loads status', () => { | |||
getTasksForComponent.mockClear(); | |||
mount( | |||
<ComponentNav branches={[]} component={component} location={{}} onBranchesChange={jest.fn()} /> | |||
); | |||
mount(<ComponentNav branches={[]} component={component} location={{}} />); | |||
expect(getTasksForComponent).toBeCalledWith('component'); | |||
}); | |||
it('renders', () => { | |||
const wrapper = shallow( | |||
<ComponentNav branches={[]} component={component} location={{}} onBranchesChange={jest.fn()} /> | |||
<ComponentNav branches={[]} component={component} location={{}} /> | |||
); | |||
wrapper.setState({ | |||
incremental: true, |
@@ -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( | |||
<ComponentNavBranch | |||
branches={[branch]} | |||
component={component} | |||
currentBranch={branch} | |||
onBranchesChange={jest.fn()} | |||
/>, | |||
<ComponentNavBranch branches={[branch]} component={component} currentBranch={branch} />, | |||
{ 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 } } | |||
); |
@@ -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( | |||
<ComponentNavBranchesMenu | |||
branches={[mainBranch(), shortBranch('foo'), longBranch('bar'), shortBranch('baz', true)]} | |||
component={project} | |||
component={component} | |||
currentBranch={mainBranch()} | |||
onBranchesChange={jest.fn()} | |||
onClose={jest.fn()} | |||
/> | |||
) | |||
@@ -49,9 +48,8 @@ it('searches', () => { | |||
const wrapper = shallow( | |||
<ComponentNavBranchesMenu | |||
branches={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]} | |||
component={project} | |||
component={component} | |||
currentBranch={mainBranch()} | |||
onBranchesChange={jest.fn()} | |||
onClose={jest.fn()} | |||
/> | |||
); | |||
@@ -63,9 +61,8 @@ it('selects next & previous', () => { | |||
const wrapper = shallow( | |||
<ComponentNavBranchesMenu | |||
branches={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]} | |||
component={project} | |||
component={component} | |||
currentBranch={mainBranch()} | |||
onBranchesChange={jest.fn()} | |||
onClose={jest.fn()} | |||
/> | |||
); |
@@ -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( | |||
<ComponentNavBranchesMenuItem | |||
branch={shortBranch} | |||
component={component} | |||
onBranchesChange={jest.fn()} | |||
onSelect={jest.fn()} | |||
selected={false} | |||
{...props} |
@@ -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} | |||
/> |
@@ -129,6 +129,23 @@ exports[`should work for all qualifiers 1`] = ` | |||
project_settings.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/branches", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_branches.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -1011,6 +1028,23 @@ exports[`should work with extensions 1`] = ` | |||
project_settings.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/branches", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_branches.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -1216,6 +1250,23 @@ exports[`should work with multiple extensions 1`] = ` | |||
project_settings.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/branches", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_branches.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" |
@@ -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} | |||
/> | |||
<Route path="project/background_tasks" childRoutes={backgroundTasksRoutes} /> | |||
<Route path="project/branches" childRoutes={projectBranchesRoutes} /> | |||
<Route path="project/settings" childRoutes={settingsRoutes} /> | |||
<Route path="project_roles" childRoutes={projectPermissionsRoutes} /> | |||
</Route> |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -19,9 +19,9 @@ | |||
*/ | |||
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'; | |||
import { deleteBranch } from '../../../api/branches'; | |||
import { Branch } from '../../../app/types'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
interface Props { | |||
branch: Branch; | |||
@@ -66,7 +66,6 @@ export default class DeleteBranchModal extends React.PureComponent<Props, State> | |||
handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
this.props.onClose(); | |||
}; | |||
@@ -19,9 +19,9 @@ | |||
*/ | |||
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'; | |||
import { renameBranch } from '../../../api/branches'; | |||
import { Branch } from '../../../app/types'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
branch: Branch; | |||
@@ -70,7 +70,6 @@ export default class RenameBranchModal extends React.PureComponent<Props, State> | |||
handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
this.props.onClose(); | |||
}; | |||
@@ -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(); | |||
}); |
@@ -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; | |||
} |
@@ -17,14 +17,14 @@ | |||
* 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() })); | |||
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'; | |||
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(); |
@@ -17,14 +17,14 @@ | |||
* 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() })); | |||
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'; | |||
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(); |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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; |
@@ -98,7 +98,7 @@ export default class App extends React.PureComponent { | |||
link: ( | |||
<Link | |||
to={{ | |||
pathname: '/project/settings', | |||
pathname: '/project/branches', | |||
query: { id: this.props.component && this.props.component.key } | |||
}}> | |||
{translate('branches.settings_hint_tab')} |
@@ -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 | |||
#------------------------------------------------------------------------------ |