@@ -26,6 +26,7 @@ | |||
"keymaster": "1.6.2", | |||
"lodash": "4.17.4", | |||
"numeral": "1.5.3", | |||
"prop-types": "15.5.10", | |||
"rc-tooltip": "3.4.7", | |||
"react": "15.6.1", | |||
"react-dom": "15.6.1", | |||
@@ -96,7 +97,6 @@ | |||
"less-loader": "4.0.4", | |||
"postcss-loader": "2.0.6", | |||
"prettier": "1.5.2", | |||
"prop-types": "15.5.10", | |||
"react-dev-utils": "3.0.0", | |||
"react-error-overlay": "1.0.7", | |||
"react-test-renderer": "15.6.1", | |||
@@ -132,6 +132,7 @@ | |||
"jest": { | |||
"coverageDirectory": "<rootDir>/target/coverage", | |||
"coveragePathIgnorePatterns": ["<rootDir>/node_modules", "<rootDir>/tests"], | |||
"mapCoverage": true, | |||
"moduleFileExtensions": ["ts", "tsx", "js", "json"], | |||
"moduleNameMapper": { | |||
"^.+\\.(hbs|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/config/jest/FileStub.js", |
@@ -17,9 +17,17 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { getJSON } from '../helpers/request'; | |||
import { getJSON, post } from '../helpers/request'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
export function getBranches(project: string): Promise<any> { | |||
return getJSON('/api/project_branches/list', { project }).then(r => r.branches, throwGlobalError); | |||
} | |||
export function deleteBranch(project: string, branch: string): Promise<void | Response> { | |||
return post('/api/project_branches/delete', { project, branch }).catch(throwGlobalError); | |||
} | |||
export function renameBranch(project: string, branch: string): Promise<void | Response> { | |||
return post('/api/project_branches/rename', { project, branch }).catch(throwGlobalError); | |||
} |
@@ -83,9 +83,7 @@ export default class ProjectContainer extends React.PureComponent<Props, State> | |||
Promise.all([getComponentNavigation(id), getComponentData(id, branch)]).then(([nav, data]) => { | |||
const component = this.addQualifier({ ...nav, ...data }); | |||
const project = component.breadcrumbs.find((c: Component) => c.qualifier === 'TRK'); | |||
const branchesRequest = project ? getBranches(project.key) : Promise.resolve([]); | |||
branchesRequest.then(branches => { | |||
this.fetchBranches(component).then(branches => { | |||
if (this.mounted) { | |||
this.setState({ loading: false, branches, component }); | |||
} | |||
@@ -93,12 +91,30 @@ export default class ProjectContainer extends React.PureComponent<Props, State> | |||
}, onError); | |||
} | |||
fetchBranches = (component: Component) => { | |||
const project = component.breadcrumbs.find((c: Component) => c.qualifier === 'TRK'); | |||
return project ? getBranches(project.key) : Promise.resolve([]); | |||
}; | |||
handleProjectChange = (changes: {}) => { | |||
if (this.mounted) { | |||
this.setState(state => ({ component: { ...state.component, ...changes } })); | |||
} | |||
}; | |||
handleBranchesChange = () => { | |||
if (this.mounted && this.state.component) { | |||
this.fetchBranches(this.state.component).then( | |||
branches => { | |||
if (this.mounted) { | |||
this.setState({ branches }); | |||
} | |||
}, | |||
() => {} | |||
); | |||
} | |||
}; | |||
render() { | |||
const { query } = this.props.location; | |||
const { branches, component, loading } = this.state; | |||
@@ -128,7 +144,9 @@ export default class ProjectContainer extends React.PureComponent<Props, State> | |||
/>} | |||
{React.cloneElement(this.props.children, { | |||
branch, | |||
branches, | |||
component: component, | |||
onBranchesChange: this.handleBranchesChange, | |||
onComponentChange: this.handleProjectChange | |||
})} | |||
</div> |
@@ -100,3 +100,21 @@ it("doesn't load branches portfolio", () => { | |||
expect(getComponentNavigation).toBeCalledWith('portfolioKey'); | |||
}); | |||
}); | |||
it('updates branches on change', () => { | |||
(getBranches as jest.Mock<any>).mockImplementation(() => Promise.resolve([])); | |||
const Inner = () => <div />; | |||
const wrapper = shallow( | |||
<ProjectContainer location={{ query: { id: 'portfolioKey' } }}> | |||
<Inner /> | |||
</ProjectContainer> | |||
); | |||
(wrapper.instance() as ProjectContainer).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'); | |||
}); |
@@ -102,6 +102,8 @@ export default class ComponentNav extends React.PureComponent<Props, State> { | |||
<ComponentNavBranch | |||
branches={this.props.branches} | |||
currentBranch={this.props.currentBranch} | |||
// to close dropdown on any location change | |||
location={this.props.location} | |||
project={this.props.component} | |||
/> | |||
@@ -116,6 +118,8 @@ export default class ComponentNav extends React.PureComponent<Props, State> { | |||
branch={this.props.currentBranch} | |||
component={this.props.component} | |||
conf={this.props.conf} | |||
// to re-render selected menu item | |||
location={this.props.location} | |||
/> | |||
</ContextNavBar> | |||
); |
@@ -33,6 +33,7 @@ import BubblePopupHelper from '../../../../components/common/BubblePopupHelper'; | |||
interface Props { | |||
branches: Branch[]; | |||
currentBranch: Branch; | |||
location?: any; | |||
project: Component; | |||
} | |||
@@ -61,7 +62,8 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State | |||
componentWillReceiveProps(nextProps: Props) { | |||
if ( | |||
nextProps.project !== this.props.project || | |||
nextProps.currentBranch !== this.props.currentBranch | |||
nextProps.currentBranch !== this.props.currentBranch || | |||
nextProps.location !== this.props.location | |||
) { | |||
this.setState({ dropdownOpen: false, singleBranchPopupOpen: false }); | |||
} |
@@ -28,6 +28,7 @@ import { | |||
} from '../../../../helpers/branches'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { getProjectBranchUrl } from '../../../../helpers/urls'; | |||
import { Link } from 'react-router'; | |||
interface Props { | |||
branches: Branch[]; | |||
@@ -179,17 +180,29 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, | |||
}); | |||
return ( | |||
<ul className="menu"> | |||
<ul className="menu menu-vertically-limited"> | |||
{menu} | |||
</ul> | |||
); | |||
}; | |||
render() { | |||
const { project } = this.props; | |||
const showManageLink = | |||
project.qualifier === 'TRK' && project.configuration && project.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: project.key } }}> | |||
{translate('branches.manage')} | |||
</Link> | |||
</div>} | |||
</div> | |||
); | |||
} |
@@ -20,7 +20,7 @@ | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import * as classNames from 'classnames'; | |||
import BranchStatus from './BranchStatus'; | |||
import BranchStatus from '../../../../components/common/BranchStatus'; | |||
import { Branch, Component } from '../../../types'; | |||
import BranchIcon from '../../../../components/icons-components/BranchIcon'; | |||
import { isShortLivingBranch } from '../../../../helpers/branches'; |
@@ -20,6 +20,7 @@ | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import * as classNames from 'classnames'; | |||
import * as PropTypes from 'prop-types'; | |||
import { Branch, Component, ComponentExtension, ComponentConfiguration } from '../../../types'; | |||
import NavBarTabs from '../../../../components/nav/NavBarTabs'; | |||
import { isShortLivingBranch, getBranchName } from '../../../../helpers/branches'; | |||
@@ -27,6 +28,7 @@ import { translate } from '../../../../helpers/l10n'; | |||
const SETTINGS_URLS = [ | |||
'/project/admin', | |||
'/project/branches', | |||
'/project/settings', | |||
'/project/quality_profiles', | |||
'/project/quality_gate', | |||
@@ -43,9 +45,14 @@ interface Props { | |||
branch: Branch; | |||
component: Component; | |||
conf: ComponentConfiguration; | |||
location?: any; | |||
} | |||
export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
static contextTypes = { | |||
branchesEnabled: PropTypes.bool.isRequired | |||
}; | |||
isProject() { | |||
return this.props.component.qualifier === 'TRK'; | |||
} | |||
@@ -196,6 +203,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
renderAdministrationLinks() { | |||
return [ | |||
this.renderSettingsLink(), | |||
this.renderBranchesLink(), | |||
this.renderProfilesLink(), | |||
this.renderQualityGateLink(), | |||
this.renderCustomMeasuresLink(), | |||
@@ -223,6 +231,21 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
); | |||
} | |||
renderBranchesLink() { | |||
if (!this.context.branchesEnabled || !this.isProject() || !this.props.conf.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.props.conf.showQualityProfiles) { | |||
return null; |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import IncrementalBadge from './IncrementalBadge'; | |||
import BranchStatus from './BranchStatus'; | |||
import BranchStatus from '../../../../components/common/BranchStatus'; | |||
import { Branch, Component, ComponentConfiguration } from '../../../types'; | |||
import Tooltip from '../../../../components/controls/Tooltip'; | |||
import PendingIcon from '../../../../components/icons-components/PendingIcon'; |
@@ -44,7 +44,10 @@ it('should work with extensions', () => { | |||
extensions: [{ key: 'foo', name: 'Foo' }] | |||
}; | |||
expect( | |||
shallow(<ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />) | |||
shallow( | |||
<ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />, | |||
{ context: { branchesEnabled: true } } | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
@@ -62,7 +65,10 @@ it('should work with multiple extensions', () => { | |||
extensions: [{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }] | |||
}; | |||
expect( | |||
shallow(<ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />) | |||
shallow( | |||
<ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />, | |||
{ context: { branchesEnabled: true } } | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
@@ -77,7 +83,9 @@ it('should work for short-living branches', () => { | |||
const component = { key: 'foo', qualifier: 'TRK' } as Component; | |||
const conf = { showSettings: true }; | |||
expect( | |||
shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />) | |||
shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />, { | |||
context: { branchesEnabled: true } | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
@@ -86,6 +94,8 @@ it('should work for long-living branches', () => { | |||
const component = { key: 'foo', qualifier: 'TRK' } as Component; | |||
const conf = { showSettings: true }; | |||
expect( | |||
shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />) | |||
shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />, { | |||
context: { branchesEnabled: true } | |||
}) | |||
).toMatchSnapshot(); | |||
}); |
@@ -25,7 +25,7 @@ exports[`renders list 1`] = ` | |||
/> | |||
</div> | |||
<ul | |||
className="menu" | |||
className="menu menu-vertically-limited" | |||
> | |||
<ComponentNavBranchesMenuItem | |||
branch={ | |||
@@ -165,7 +165,7 @@ exports[`searches 1`] = ` | |||
/> | |||
</div> | |||
<ul | |||
className="menu" | |||
className="menu menu-vertically-limited" | |||
> | |||
<ComponentNavBranchesMenuItem | |||
branch={ |
@@ -266,6 +266,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" | |||
@@ -470,6 +487,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" |
@@ -72,16 +72,6 @@ | |||
left: -5px; | |||
} | |||
.navbar-search-shortcut-hint { | |||
line-height: 16px; | |||
margin-top: 5px; | |||
padding: 5px 10px; | |||
border-top: 1px solid #e6e6e6; | |||
background-color: #f3f3f3; | |||
color: #777; | |||
font-size: 11px; | |||
} | |||
.navbar-search-no-results { | |||
margin-top: 4px; | |||
padding: 5px 10px; | |||
@@ -94,3 +84,7 @@ | |||
overflow-y: auto; | |||
overflow-x: hidden; | |||
} | |||
.global-navbar-search-dropdown .dropdown-bottom-hint { | |||
margin-bottom: 0; | |||
} |
@@ -367,7 +367,7 @@ export default class Search extends React.PureComponent { | |||
results={this.state.results} | |||
selected={this.state.selected} | |||
/> | |||
<div className="navbar-search-shortcut-hint"> | |||
<div className="dropdown-bottom-hint"> | |||
<div className="pull-right"> | |||
<ClockIcon className="little-spacer-right" size={12} /> | |||
{translate('recently_browsed')} |
@@ -55,6 +55,7 @@ import organizationsRoutes from '../../apps/organizations/routes'; | |||
import permissionTemplatesRoutes from '../../apps/permission-templates/routes'; | |||
import projectActivityRoutes from '../../apps/projectActivity/routes'; | |||
import projectAdminRoutes from '../../apps/project-admin/routes'; | |||
import projectBranchesRoutes from '../../apps/projectBranches/routes'; | |||
import projectsRoutes from '../../apps/projects/routes'; | |||
import projectsManagementRoutes from '../../apps/projectsManagement/routes'; | |||
import qualityGatesRoutes from '../../apps/quality-gates/routes'; | |||
@@ -187,6 +188,7 @@ const startReactApp = () => { | |||
component={ProjectPageExtension} | |||
/> | |||
<Route path="background_tasks" childRoutes={backgroundTasksRoutes} /> | |||
<Route path="branches" childRoutes={projectBranchesRoutes} /> | |||
<Route path="issues" childRoutes={issuesRoutes} /> | |||
<Route path="settings" childRoutes={settingsRoutes} /> | |||
{projectAdminRoutes} |
@@ -0,0 +1,68 @@ | |||
/* | |||
* 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,138 @@ | |||
/* | |||
* 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} | |||
</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> | |||
); | |||
} | |||
} |
@@ -0,0 +1,105 @@ | |||
/* | |||
* 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> | |||
); | |||
} | |||
} |
@@ -0,0 +1,131 @@ | |||
/* | |||
* 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> | |||
); | |||
} | |||
} |
@@ -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; | |||
} |
@@ -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); | |||
} |
@@ -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); | |||
} |
@@ -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,95 @@ | |||
// 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 | |||
</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,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> | |||
`; |
@@ -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> | |||
`; |
@@ -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; |
@@ -19,12 +19,12 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import { Branch } from '../../../types'; | |||
import Level from '../../../../components/ui/Level'; | |||
import BugIcon from '../../../../components/icons-components/BugIcon'; | |||
import CodeSmellIcon from '../../../../components/icons-components/CodeSmellIcon'; | |||
import VulnerabilityIcon from '../../../../components/icons-components/VulnerabilityIcon'; | |||
import { isShortLivingBranch } from '../../../../helpers/branches'; | |||
import { Branch } from '../../app/types'; | |||
import Level from '../ui/Level'; | |||
import BugIcon from '../icons-components/BugIcon'; | |||
import CodeSmellIcon from '../icons-components/CodeSmellIcon'; | |||
import VulnerabilityIcon from '../icons-components/VulnerabilityIcon'; | |||
import { isShortLivingBranch } from '../../helpers/branches'; | |||
import './BranchStatus.css'; | |||
interface Props { |
@@ -20,7 +20,7 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import BranchStatus from '../BranchStatus'; | |||
import { BranchType, LongLivingBranch } from '../../../../types'; | |||
import { BranchType, LongLivingBranch } from '../../../app/types'; | |||
it('renders status of short-living branches', () => { | |||
checkShort(0, 0, 0); |
@@ -17,15 +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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
/*:: | |||
type Props = { className?: string, size?: number }; | |||
*/ | |||
interface Props { | |||
className?: string; | |||
size?: number; | |||
} | |||
export default function ChangeIcon({ className, size = 12 } /*: Props */) { | |||
/* eslint-disable max-len */ | |||
export default function ChangeIcon({ className, size = 12 }: Props) { | |||
return ( | |||
<svg | |||
className={className} |
@@ -17,15 +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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import * as React from 'react'; | |||
/*:: | |||
type Props = { className?: string, size?: number }; | |||
*/ | |||
interface Props { | |||
className?: string; | |||
size?: number; | |||
} | |||
export default function DeleteIcon({ className, size = 12 } /*: Props */) { | |||
/* eslint-disable max-len */ | |||
export default function DeleteIcon({ className, size = 12 }: Props) { | |||
return ( | |||
<svg | |||
className={className} |
@@ -170,10 +170,10 @@ export function postJSON(url: string, data?: RequestData): Promise<any> { | |||
* Shortcut to do a POST request | |||
*/ | |||
export function post(url: string, data?: RequestData): Promise<void> { | |||
return new Promise(resolve => { | |||
return new Promise((resolve, reject) => { | |||
request(url).setMethod('POST').setData(data).submit().then(checkStatus).then(() => { | |||
resolve(); | |||
}); | |||
}, reject); | |||
}); | |||
} | |||
@@ -99,3 +99,14 @@ | |||
top: 0; | |||
z-index: (1000 - 10); | |||
} | |||
.dropdown-bottom-hint { | |||
line-height: 16px; | |||
margin-top: 5px; | |||
margin-bottom: -5px; | |||
padding: 5px 10px; | |||
border-top: 1px solid #e6e6e6; | |||
background-color: #f3f3f3; | |||
color: #777; | |||
font-size: 11px; | |||
} |
@@ -80,6 +80,11 @@ | |||
} | |||
} | |||
.menu-vertically-limited { | |||
max-height: 300px; | |||
overflow-y: auto; | |||
} | |||
.menu-footer > a > span { | |||
border-bottom: 1px solid @darkGrey; | |||
color: @secondFontColor; |
@@ -112,6 +112,7 @@ name=Name | |||
name_too_long_x=Name is too long (maximum is {0} characters) | |||
navigation=Navigation | |||
never=Never | |||
new_name=New name | |||
none=None | |||
no_tags=No tags | |||
off=Off | |||
@@ -587,6 +588,7 @@ portfolio_deletion.page.description=Delete this portfolio from SonarQube. Compon | |||
application_deletion.page.description=Delete this application from SonarQube. Application projects will not be deleted. This operation cannot be undone. | |||
provisioning.page=Provisioning | |||
provisioning.page.description=Use this page to initialize projects if you would like to configure them before the first analysis. Once a project is provisioned, you have access to perform all project configurations on it. | |||
project_branches.page=Branches | |||
#------------------------------------------------------------------------------ | |||
# | |||
@@ -3164,3 +3166,7 @@ branches.learn_how_to_analyze=Learn how to analyze branches in SonarQube | |||
branches.learn_how_to_analyze.text=Quickly setup branch analysis and get separate insights for each of your branches and pull requests. | |||
branches.no_support.header=Get the most out of SonarQube with branches analysis | |||
branches.no_support.header.text=Analyze each branch of your project separately with our Developer Pack. | |||
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 |