@@ -17,34 +17,43 @@ | |||
* 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 PropTypes from 'prop-types'; | |||
import * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import * as PropTypes from 'prop-types'; | |||
import GlobalLoading from './GlobalLoading'; | |||
import { fetchCurrentUser } from '../../store/users/actions'; | |||
import { fetchLanguages, fetchAppState } from '../../store/rootActions'; | |||
class App extends React.PureComponent { | |||
/*:: mounted: boolean; */ | |||
interface Props { | |||
children: JSX.Element; | |||
fetchAppState: () => Promise<any>; | |||
fetchCurrentUser: () => Promise<void>; | |||
fetchLanguages: () => Promise<void>; | |||
} | |||
static propTypes = { | |||
fetchAppState: PropTypes.func.isRequired, | |||
fetchCurrentUser: PropTypes.func.isRequired, | |||
fetchLanguages: PropTypes.func.isRequired, | |||
children: PropTypes.element.isRequired | |||
}; | |||
interface State { | |||
branchesEnabled: boolean; | |||
loading: boolean; | |||
} | |||
class App extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
state: State = { branchesEnabled: false, loading: true }; | |||
state = { | |||
loading: true | |||
static childContextTypes = { | |||
branchesEnabled: PropTypes.bool.isRequired | |||
}; | |||
getChildContext() { | |||
return { branchesEnabled: this.state.branchesEnabled }; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.props | |||
.fetchCurrentUser() | |||
.then(() => Promise.all([this.props.fetchAppState(), this.props.fetchLanguages()])) | |||
.then(() => Promise.all([this.fetchAppState(), this.props.fetchLanguages()])) | |||
.then(this.finishLoading, this.finishLoading); | |||
} | |||
@@ -52,6 +61,14 @@ class App extends React.PureComponent { | |||
this.mounted = false; | |||
} | |||
fetchAppState = () => { | |||
return this.props.fetchAppState().then(appState => { | |||
if (this.mounted) { | |||
this.setState({ branchesEnabled: appState.branchesEnabled }); | |||
} | |||
}); | |||
}; | |||
finishLoading = () => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
@@ -66,8 +83,4 @@ class App extends React.PureComponent { | |||
} | |||
} | |||
export default connect(null, { | |||
fetchAppState, | |||
fetchCurrentUser, | |||
fetchLanguages | |||
})(App); | |||
export default connect(null, { fetchAppState, fetchCurrentUser, fetchLanguages })(App as any); |
@@ -17,8 +17,7 @@ | |||
* 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'; | |||
export default function GlobalLoading() { | |||
return ( |
@@ -19,11 +19,16 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import * as PropTypes from 'prop-types'; | |||
import ComponentNavBranchesMenu from './ComponentNavBranchesMenu'; | |||
import SingleBranchHelperPopup from './SingleBranchHelperPopup'; | |||
import NoBranchSupportPopup from './NoBranchSupportPopup'; | |||
import { Branch, Component } from '../../../types'; | |||
import BranchIcon from '../../../../components/icons-components/BranchIcon'; | |||
import { isShortLivingBranch } from '../../../../helpers/branches'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import HelpIcon from '../../../../components/icons-components/HelpIcon'; | |||
import BubblePopupHelper from '../../../../components/common/BubblePopupHelper'; | |||
interface Props { | |||
branches: Branch[]; | |||
@@ -32,12 +37,22 @@ interface Props { | |||
} | |||
interface State { | |||
open: boolean; | |||
dropdownOpen: boolean; | |||
noBranchSupportPopupOpen: boolean; | |||
singleBranchPopupOpen: boolean; | |||
} | |||
export default class ComponentNavBranch extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
state: State = { open: false }; | |||
state: State = { | |||
dropdownOpen: false, | |||
noBranchSupportPopupOpen: false, | |||
singleBranchPopupOpen: false | |||
}; | |||
static contextTypes = { | |||
branchesEnabled: PropTypes.bool.isRequired | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
@@ -48,7 +63,7 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State | |||
nextProps.project !== this.props.project || | |||
nextProps.currentBranch !== this.props.currentBranch | |||
) { | |||
this.setState({ open: false }); | |||
this.setState({ dropdownOpen: false, singleBranchPopupOpen: false }); | |||
} | |||
} | |||
@@ -60,37 +75,128 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
event.currentTarget.blur(); | |||
this.setState({ open: true }); | |||
this.setState({ dropdownOpen: true }); | |||
}; | |||
closeDropdown = () => { | |||
if (this.mounted) { | |||
this.setState({ open: false }); | |||
this.setState({ dropdownOpen: false }); | |||
} | |||
}; | |||
render() { | |||
toggleSingleBranchPopup = (show?: boolean) => { | |||
if (show != undefined) { | |||
this.setState({ singleBranchPopupOpen: show }); | |||
} else { | |||
this.setState(state => ({ singleBranchPopupOpen: !state.singleBranchPopupOpen })); | |||
} | |||
}; | |||
toggleNoBranchSupportPopup = (show?: boolean) => { | |||
if (show != undefined) { | |||
this.setState({ noBranchSupportPopupOpen: show }); | |||
} else { | |||
this.setState(state => ({ noBranchSupportPopupOpen: !state.noBranchSupportPopupOpen })); | |||
} | |||
}; | |||
handleSingleBranchClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
this.toggleSingleBranchPopup(); | |||
}; | |||
handleNoBranchSupportClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
this.toggleNoBranchSupportPopup(); | |||
}; | |||
renderDropdown = () => { | |||
return this.state.dropdownOpen | |||
? <ComponentNavBranchesMenu | |||
branches={this.props.branches} | |||
currentBranch={this.props.currentBranch} | |||
onClose={this.closeDropdown} | |||
project={this.props.project} | |||
/> | |||
: null; | |||
}; | |||
renderMergeBranch = () => { | |||
const { currentBranch } = this.props; | |||
return isShortLivingBranch(currentBranch) && !currentBranch.isOrphan | |||
? <span className="note big-spacer-left text-lowercase"> | |||
{translate('from')} <strong>{currentBranch.mergeBranch}</strong> | |||
</span> | |||
: null; | |||
}; | |||
renderSingleBranchPopup = () => | |||
<div className="display-inline-block spacer-left"> | |||
<a className="link-no-underline" href="#" onClick={this.handleSingleBranchClick}> | |||
<HelpIcon className="" fill="#cdcdcd" /> | |||
</a> | |||
<BubblePopupHelper | |||
isOpen={this.state.singleBranchPopupOpen} | |||
position="bottomleft" | |||
popup={<SingleBranchHelperPopup />} | |||
togglePopup={this.toggleSingleBranchPopup} | |||
/> | |||
</div>; | |||
renderNoBranchSupportPopup = () => | |||
<div className="display-inline-block spacer-left"> | |||
<a className="link-no-underline" href="#" onClick={this.handleNoBranchSupportClick}> | |||
<HelpIcon className="" fill="#cdcdcd" /> | |||
</a> | |||
<BubblePopupHelper | |||
isOpen={this.state.noBranchSupportPopupOpen} | |||
position="bottomleft" | |||
popup={<NoBranchSupportPopup />} | |||
togglePopup={this.toggleNoBranchSupportPopup} | |||
/> | |||
</div>; | |||
render() { | |||
const { branches, currentBranch } = this.props; | |||
if (!this.context.branchesEnabled) { | |||
return ( | |||
<div className="navbar-context-branches"> | |||
<BranchIcon branch={currentBranch} className="little-spacer-right" color="#cdcdcd" /> | |||
<span className="note"> | |||
{currentBranch.name} | |||
</span> | |||
{this.renderNoBranchSupportPopup()} | |||
</div> | |||
); | |||
} | |||
if (branches.length < 2) { | |||
return ( | |||
<div className="navbar-context-branches"> | |||
<BranchIcon branch={currentBranch} className="little-spacer-right" color="#cdcdcd" /> | |||
<span className="note"> | |||
{currentBranch.name} | |||
</span> | |||
{this.renderSingleBranchPopup()} | |||
</div> | |||
); | |||
} | |||
return ( | |||
<div className={classNames('navbar-context-branches', 'dropdown', { open: this.state.open })}> | |||
<div | |||
className={classNames('navbar-context-branches', 'dropdown', { | |||
open: this.state.dropdownOpen | |||
})}> | |||
<a className="link-base-color link-no-underline" href="#" onClick={this.handleClick}> | |||
<BranchIcon branch={currentBranch} className="little-spacer-right" /> | |||
{currentBranch.name} | |||
<i className="icon-dropdown little-spacer-left" /> | |||
</a> | |||
{this.state.open && | |||
<ComponentNavBranchesMenu | |||
branches={this.props.branches} | |||
currentBranch={currentBranch} | |||
onClose={this.closeDropdown} | |||
project={this.props.project} | |||
/>} | |||
{isShortLivingBranch(currentBranch) && | |||
!currentBranch.isOrphan && | |||
<span className="note big-spacer-left text-lowercase"> | |||
{translate('from')} <strong>{currentBranch.mergeBranch}</strong> | |||
</span>} | |||
{this.renderDropdown()} | |||
{this.renderMergeBranch()} | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,52 @@ | |||
/* | |||
* 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 BubblePopup from '../../../../components/common/BubblePopup'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
interface Props { | |||
popupPosition?: any; | |||
} | |||
export default function NoBranchSupportPopup(props: Props) { | |||
return ( | |||
<BubblePopup position={props.popupPosition} customClass="bubble-popup-bottom"> | |||
<div className="abs-width-400"> | |||
<h6 className="spacer-bottom"> | |||
{translate('branches.no_support.header')} | |||
</h6> | |||
<p className="big-spacer-bottom markdown"> | |||
{translate('branches.no_support.header.text')} | |||
</p> | |||
<p> | |||
<a href="https://redirect.sonarsource.com/doc/branches.html" target="_blank"> | |||
{translate('learn_more')} | |||
</a> | |||
<a | |||
className="button spacer-left" | |||
href="https://www.sonarsource.com/company/contact/" | |||
target="_blank"> | |||
{translate('buy_developer_pack')} | |||
</a> | |||
</p> | |||
</div> | |||
</BubblePopup> | |||
); | |||
} |
@@ -0,0 +1,47 @@ | |||
/* | |||
* 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 BubblePopup from '../../../../components/common/BubblePopup'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
interface Props { | |||
popupPosition?: any; | |||
} | |||
export default function SingleBranchHelperPopup(props: Props) { | |||
return ( | |||
<BubblePopup position={props.popupPosition} customClass="bubble-popup-bottom"> | |||
<div className="abs-width-400"> | |||
<h6 className="spacer-bottom"> | |||
{translate('branches.learn_how_to_analyze')} | |||
</h6> | |||
<p className="big-spacer-bottom markdown"> | |||
{translate('branches.learn_how_to_analyze.text')} | |||
</p> | |||
<a | |||
className="button" | |||
href="https://redirect.sonarsource.com/doc/branches.html" | |||
target="_blank"> | |||
{translate('about_page.read_documentation')} | |||
</a> | |||
</div> | |||
</BubblePopup> | |||
); | |||
} |
@@ -20,14 +20,29 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ComponentNavBranch from '../ComponentNavBranch'; | |||
import { BranchType, ShortLivingBranch, MainBranch, Component } from '../../../../types'; | |||
import { | |||
BranchType, | |||
ShortLivingBranch, | |||
MainBranch, | |||
Component, | |||
LongLivingBranch | |||
} from '../../../../types'; | |||
import { click } from '../../../../../helpers/testUtils'; | |||
const fooBranch: LongLivingBranch = { isMain: false, name: 'foo', type: BranchType.LONG }; | |||
it('renders main branch', () => { | |||
const branch: MainBranch = { isMain: true, name: 'master' }; | |||
const component = {} as Component; | |||
expect( | |||
shallow(<ComponentNavBranch branches={[]} currentBranch={branch} project={component} />) | |||
shallow( | |||
<ComponentNavBranch | |||
branches={[branch, fooBranch]} | |||
currentBranch={branch} | |||
project={component} | |||
/>, | |||
{ context: { branchesEnabled: true } } | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
@@ -41,7 +56,14 @@ it('renders short-living branch', () => { | |||
}; | |||
const component = {} as Component; | |||
expect( | |||
shallow(<ComponentNavBranch branches={[]} currentBranch={branch} project={component} />) | |||
shallow( | |||
<ComponentNavBranch | |||
branches={[branch, fooBranch]} | |||
currentBranch={branch} | |||
project={component} | |||
/>, | |||
{ context: { branchesEnabled: true } } | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
@@ -49,9 +71,44 @@ it('opens menu', () => { | |||
const branch: MainBranch = { isMain: true, name: 'master' }; | |||
const component = {} as Component; | |||
const wrapper = shallow( | |||
<ComponentNavBranch branches={[]} currentBranch={branch} project={component} /> | |||
<ComponentNavBranch | |||
branches={[branch, fooBranch]} | |||
currentBranch={branch} | |||
project={component} | |||
/>, | |||
{ context: { branchesEnabled: true } } | |||
); | |||
expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(0); | |||
click(wrapper.find('a')); | |||
expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(1); | |||
}); | |||
it('renders single branch popup', () => { | |||
const branch: MainBranch = { isMain: true, name: 'master' }; | |||
const component = {} as Component; | |||
const wrapper = shallow( | |||
<ComponentNavBranch branches={[branch]} currentBranch={branch} project={component} />, | |||
{ context: { branchesEnabled: true } } | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(wrapper.find('BubblePopupHelper').prop('isOpen')).toBe(false); | |||
click(wrapper.find('a')); | |||
expect(wrapper.find('BubblePopupHelper').prop('isOpen')).toBe(true); | |||
}); | |||
it('renders no branch support popup', () => { | |||
const branch: MainBranch = { isMain: true, name: 'master' }; | |||
const component = {} as Component; | |||
const wrapper = shallow( | |||
<ComponentNavBranch | |||
branches={[branch, fooBranch]} | |||
currentBranch={branch} | |||
project={component} | |||
/>, | |||
{ context: { branchesEnabled: false } } | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(wrapper.find('BubblePopupHelper').prop('isOpen')).toBe(false); | |||
click(wrapper.find('a')); | |||
expect(wrapper.find('BubblePopupHelper').prop('isOpen')).toBe(true); | |||
}); |
@@ -0,0 +1,26 @@ | |||
/* | |||
* 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 NoBranchSupportPopup from '../NoBranchSupportPopup'; | |||
it('renders', () => { | |||
expect(shallow(<NoBranchSupportPopup />)).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,26 @@ | |||
/* | |||
* 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 SingleBranchHelperPopup from '../SingleBranchHelperPopup'; | |||
it('renders', () => { | |||
expect(shallow(<SingleBranchHelperPopup />)).toMatchSnapshot(); | |||
}); |
@@ -26,6 +26,48 @@ exports[`renders main branch 1`] = ` | |||
</div> | |||
`; | |||
exports[`renders no branch support popup 1`] = ` | |||
<div | |||
className="navbar-context-branches" | |||
> | |||
<BranchIcon | |||
branch={ | |||
Object { | |||
"isMain": true, | |||
"name": "master", | |||
} | |||
} | |||
className="little-spacer-right" | |||
color="#cdcdcd" | |||
/> | |||
<span | |||
className="note" | |||
> | |||
master | |||
</span> | |||
<div | |||
className="display-inline-block spacer-left" | |||
> | |||
<a | |||
className="link-no-underline" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<HelpIcon | |||
className="" | |||
fill="#cdcdcd" | |||
/> | |||
</a> | |||
<BubblePopupHelper | |||
isOpen={false} | |||
popup={<NoBranchSupportPopup />} | |||
position="bottomleft" | |||
togglePopup={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
`; | |||
exports[`renders short-living branch 1`] = ` | |||
<div | |||
className="navbar-context-branches dropdown" | |||
@@ -67,3 +109,45 @@ exports[`renders short-living branch 1`] = ` | |||
</span> | |||
</div> | |||
`; | |||
exports[`renders single branch popup 1`] = ` | |||
<div | |||
className="navbar-context-branches" | |||
> | |||
<BranchIcon | |||
branch={ | |||
Object { | |||
"isMain": true, | |||
"name": "master", | |||
} | |||
} | |||
className="little-spacer-right" | |||
color="#cdcdcd" | |||
/> | |||
<span | |||
className="note" | |||
> | |||
master | |||
</span> | |||
<div | |||
className="display-inline-block spacer-left" | |||
> | |||
<a | |||
className="link-no-underline" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<HelpIcon | |||
className="" | |||
fill="#cdcdcd" | |||
/> | |||
</a> | |||
<BubblePopupHelper | |||
isOpen={false} | |||
popup={<SingleBranchHelperPopup />} | |||
position="bottomleft" | |||
togglePopup={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
`; |
@@ -0,0 +1,37 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<BubblePopup | |||
customClass="bubble-popup-bottom" | |||
> | |||
<div | |||
className="abs-width-400" | |||
> | |||
<h6 | |||
className="spacer-bottom" | |||
> | |||
branches.no_support.header | |||
</h6> | |||
<p | |||
className="big-spacer-bottom markdown" | |||
> | |||
branches.no_support.header.text | |||
</p> | |||
<p> | |||
<a | |||
href="https://redirect.sonarsource.com/doc/branches.html" | |||
target="_blank" | |||
> | |||
learn_more | |||
</a> | |||
<a | |||
className="button spacer-left" | |||
href="https://www.sonarsource.com/company/contact/" | |||
target="_blank" | |||
> | |||
buy_developer_pack | |||
</a> | |||
</p> | |||
</div> | |||
</BubblePopup> | |||
`; |
@@ -0,0 +1,29 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<BubblePopup | |||
customClass="bubble-popup-bottom" | |||
> | |||
<div | |||
className="abs-width-400" | |||
> | |||
<h6 | |||
className="spacer-bottom" | |||
> | |||
branches.learn_how_to_analyze | |||
</h6> | |||
<p | |||
className="big-spacer-bottom markdown" | |||
> | |||
branches.learn_how_to_analyze.text | |||
</p> | |||
<a | |||
className="button" | |||
href="https://redirect.sonarsource.com/doc/branches.html" | |||
target="_blank" | |||
> | |||
about_page.read_documentation | |||
</a> | |||
</div> | |||
</BubblePopup> | |||
`; |
@@ -17,30 +17,23 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
export default class BubblePopup extends React.PureComponent { | |||
static propsType = { | |||
children: PropTypes.object.isRequired, | |||
position: PropTypes.object.isRequired, | |||
customClass: PropTypes.string | |||
}; | |||
static defaultProps = { | |||
customClass: '' | |||
}; | |||
interface Props { | |||
customClass?: string; | |||
children: React.ReactNode; | |||
position: { top: number; right: number }; | |||
} | |||
render() { | |||
const popupClass = classNames('bubble-popup', this.props.customClass); | |||
const popupStyle = { ...this.props.position }; | |||
export default function BubblePopup(props: Props) { | |||
const popupClass = classNames('bubble-popup', props.customClass); | |||
const popupStyle = { ...props.position }; | |||
return ( | |||
<div className={popupClass} style={popupStyle}> | |||
{this.props.children} | |||
<div className="bubble-popup-arrow" /> | |||
</div> | |||
); | |||
} | |||
return ( | |||
<div className={popupClass} style={popupStyle}> | |||
{props.children} | |||
<div className="bubble-popup-arrow" /> | |||
</div> | |||
); | |||
} |
@@ -17,44 +17,35 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
/*:: | |||
type Props = { | |||
className?: string, | |||
children?: React.Element<*>, | |||
isOpen: boolean, | |||
offset?: { | |||
vertical: number, | |||
horizontal: number | |||
}, | |||
popup: Object, | |||
position: 'bottomleft' | 'bottomright', | |||
togglePopup: (?boolean) => void | |||
}; | |||
*/ | |||
interface Props { | |||
className?: string; | |||
children?: React.ReactNode; | |||
isOpen: boolean; | |||
offset?: { vertical: number; horizontal: number }; | |||
popup: React.ReactElement<any>; | |||
position: 'bottomleft' | 'bottomright'; | |||
togglePopup: (show: boolean) => void; | |||
} | |||
/*:: | |||
type State = { | |||
position: { top: number, right: number } | |||
}; | |||
*/ | |||
interface State { | |||
position: { top: number; left?: number; right?: number }; | |||
} | |||
export default class BubblePopupHelper extends React.PureComponent { | |||
/*:: props: Props; */ | |||
state /*: State */ = { | |||
position: { | |||
top: 0, | |||
right: 0 | |||
} | |||
export default class BubblePopupHelper extends React.PureComponent<Props, State> { | |||
container: HTMLElement; | |||
popupContainer: HTMLElement | null; | |||
state: State = { | |||
position: { top: 0, right: 0 } | |||
}; | |||
componentDidMount() { | |||
this.setState({ position: this.getPosition(this.props) }); | |||
} | |||
componentWillReceiveProps(nextProps /*: Props */) { | |||
componentWillReceiveProps(nextProps: Props) { | |||
if (!this.props.isOpen && nextProps.isOpen) { | |||
window.addEventListener('keydown', this.handleKey, false); | |||
window.addEventListener('click', this.handleOutsideClick, false); | |||
@@ -64,30 +55,31 @@ export default class BubblePopupHelper extends React.PureComponent { | |||
} | |||
} | |||
handleKey = (evt /*: KeyboardEvent */) => { | |||
handleKey = (event: KeyboardEvent) => { | |||
// Escape key | |||
if (evt.keyCode === 27) { | |||
if (event.keyCode === 27) { | |||
this.props.togglePopup(false); | |||
} | |||
}; | |||
handleOutsideClick = (evt /*: SyntheticInputEvent */) => { | |||
if (!this.popupContainer || !this.popupContainer.contains(evt.target)) { | |||
handleOutsideClick = (event: MouseEvent) => { | |||
if (!this.popupContainer || !this.popupContainer.contains(event.target as Node)) { | |||
this.props.togglePopup(false); | |||
} | |||
}; | |||
handleClick(evt /*: SyntheticInputEvent */) { | |||
evt.stopPropagation(); | |||
handleClick(event: React.SyntheticEvent<HTMLElement>) { | |||
event.stopPropagation(); | |||
} | |||
getPosition(props /*: Props */) { | |||
getPosition(props: Props) { | |||
const containerPos = this.container.getBoundingClientRect(); | |||
const { position } = props; | |||
const offset = props.offset || { vertical: 0, horizontal: 0 }; | |||
if (position === 'bottomleft') { | |||
return { top: containerPos.height + offset.vertical, left: offset.horizontal }; | |||
} else if (position === 'bottomright') { | |||
} else { | |||
// if (position === 'bottomright') | |||
return { top: containerPos.height + offset.vertical, right: offset.horizontal }; | |||
} | |||
} | |||
@@ -96,7 +88,7 @@ export default class BubblePopupHelper extends React.PureComponent { | |||
return ( | |||
<div | |||
className={classNames(this.props.className, 'bubble-popup-helper')} | |||
ref={container => (this.container = container)} | |||
ref={container => (this.container = container as HTMLElement)} | |||
onClick={this.handleClick} | |||
tabIndex={0} | |||
role="tooltip"> |
@@ -29,7 +29,6 @@ exports[`should correctly handle clicks on the button 2`] = ` | |||
</button> | |||
<div> | |||
<BubblePopup | |||
customClass="" | |||
popupPosition={ | |||
Object { | |||
"right": 0, | |||
@@ -47,7 +46,6 @@ exports[`should correctly handle clicks on the button 2`] = ` | |||
exports[`should render an open popup on the left 1`] = ` | |||
<BubblePopup | |||
customClass="" | |||
popupPosition={ | |||
Object { | |||
"left": 2, | |||
@@ -83,7 +81,6 @@ exports[`should render an open popup on the right 1`] = ` | |||
</button> | |||
<div> | |||
<BubblePopup | |||
customClass="" | |||
popupPosition={ | |||
Object { | |||
"right": 0, | |||
@@ -116,7 +113,6 @@ exports[`should render the popup helper with a closed popup 1`] = ` | |||
exports[`should render the popup with offset 1`] = ` | |||
<BubblePopup | |||
customClass="" | |||
popupPosition={ | |||
Object { | |||
"right": 2, |
@@ -21,16 +21,16 @@ | |||
import React from 'react'; | |||
/*:: | |||
type Props = { className?: string, size?: number }; | |||
type Props = { className?: string, fill?: string, size?: number }; | |||
*/ | |||
export default function HelpIcon({ className, size = 16 } /*: Props */) { | |||
export default function HelpIcon({ className, fill = 'currentColor', size = 16 } /*: Props */) { | |||
/* eslint-disable max-len */ | |||
return ( | |||
<svg className={className} viewBox="0 0 16 16" width={size} height={size}> | |||
<g transform="matrix(0.0364583,0,0,0.0364583,1,-0.166667)"> | |||
<path | |||
fill="currentColor" | |||
fill={fill} | |||
d="M224,344L224,296C224,293.667 223.25,291.75 221.75,290.25C220.25,288.75 218.333,288 216,288L168,288C165.667,288 163.75,288.75 162.25,290.25C160.75,291.75 160,293.667 160,296L160,344C160,346.333 160.75,348.25 162.25,349.75C163.75,351.25 165.667,352 168,352L216,352C218.333,352 220.25,351.25 221.75,349.75C223.25,348.25 224,346.333 224,344ZM288,176C288,161.333 283.375,147.75 274.125,135.25C264.875,122.75 253.333,113.083 239.5,106.25C225.667,99.417 211.5,96 197,96C156.5,96 125.583,113.75 104.25,149.25C101.75,153.25 102.417,156.75 106.25,159.75L139.25,184.75C140.417,185.75 142,186.25 144,186.25C146.667,186.25 148.75,185.25 150.25,183.25C159.083,171.917 166.25,164.25 171.75,160.25C177.417,156.25 184.583,154.25 193.25,154.25C201.25,154.25 208.375,156.417 214.625,160.75C220.875,165.083 224,170 224,175.5C224,181.833 222.333,186.917 219,190.75C215.667,194.583 210,198.333 202,202C191.5,206.667 181.875,213.875 173.125,223.625C164.375,233.375 160,243.833 160,255L160,264C160,266.333 160.75,268.25 162.25,269.75C163.75,271.25 165.667,272 168,272L216,272C218.333,272 220.25,271.25 221.75,269.75C223.25,268.25 224,266.333 224,264C224,260.833 225.792,256.708 229.375,251.625C232.958,246.542 237.5,242.417 243,239.25C248.333,236.25 252.417,233.875 255.25,232.125C258.083,230.375 261.917,227.458 266.75,223.375C271.583,219.292 275.292,215.292 277.875,211.375C280.458,207.458 282.792,202.417 284.875,196.25C286.958,190.083 288,183.333 288,176ZM384,224C384,258.833 375.417,290.958 358.25,320.375C341.083,349.792 317.792,373.083 288.375,390.25C258.958,407.417 226.833,416 192,416C157.167,416 125.042,407.417 95.625,390.25C66.208,373.083 42.917,349.792 25.75,320.375C8.583,290.958 0,258.833 0,224C0,189.167 8.583,157.042 25.75,127.625C42.917,98.208 66.208,74.917 95.625,57.75C125.042,40.583 157.167,32 192,32C226.833,32 258.958,40.583 288.375,57.75C317.792,74.917 341.083,98.208 358.25,127.625C375.417,157.042 384,189.167 384,224Z" | |||
/> | |||
</g> |
@@ -17,62 +17,52 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
/*:: | |||
type AppState = { | |||
adminPages?: Array<*>, | |||
authenticationError: boolean, | |||
authorizationError: boolean, | |||
organizationsEnabled: boolean, | |||
qualifiers: ?Array<string> | |||
}; | |||
*/ | |||
interface AppState { | |||
adminPages?: any[]; | |||
authenticationError: boolean; | |||
authorizationError: boolean; | |||
organizationsEnabled: boolean; | |||
qualifiers?: string[]; | |||
} | |||
/*:: | |||
type SetAppStateAction = { | |||
type: 'SET_APP_STATE', | |||
appState: AppState | |||
}; | |||
*/ | |||
interface SetAppStateAction { | |||
type: 'SET_APP_STATE'; | |||
appState: AppState; | |||
} | |||
/*:: | |||
type SetAdminPagesAction = { | |||
type: 'SET_ADMIN_PAGES', | |||
adminPages: Array<*> | |||
}; | |||
*/ | |||
interface SetAdminPagesAction { | |||
type: 'SET_ADMIN_PAGES'; | |||
adminPages: any[]; | |||
} | |||
/*:: | |||
export type Action = SetAppStateAction | SetAdminPagesAction; */ | |||
interface RequireAuthorizationAction { | |||
type: 'REQUIRE_AUTHORIZATION'; | |||
} | |||
export type Action = SetAppStateAction | SetAdminPagesAction | RequireAuthorizationAction; | |||
export function setAppState(appState /*: AppState */) /*: SetAppStateAction */ { | |||
export function setAppState(appState: AppState): SetAppStateAction { | |||
return { | |||
type: 'SET_APP_STATE', | |||
appState | |||
}; | |||
} | |||
export function setAdminPages(adminPages /*: Array<*> */) /*: SetAdminPagesAction */ { | |||
return { | |||
type: 'SET_ADMIN_PAGES', | |||
adminPages | |||
}; | |||
export function setAdminPages(adminPages: any[]): SetAdminPagesAction { | |||
return { type: 'SET_ADMIN_PAGES', adminPages }; | |||
} | |||
export function requireAuthorization() { | |||
return { | |||
type: 'REQUIRE_AUTHORIZATION' | |||
}; | |||
export function requireAuthorization(): RequireAuthorizationAction { | |||
return { type: 'REQUIRE_AUTHORIZATION' }; | |||
} | |||
const defaultValue = { | |||
const defaultValue: AppState = { | |||
authenticationError: false, | |||
authorizationError: false, | |||
organizationsEnabled: false, | |||
qualifiers: null | |||
organizationsEnabled: false | |||
}; | |||
export default function(state /*: AppState */ = defaultValue, action /*: Action */) { | |||
export default function(state: AppState = defaultValue, action: Action): AppState { | |||
if (action.type === 'SET_APP_STATE') { | |||
return { ...state, ...action.appState }; | |||
} | |||
@@ -88,6 +78,6 @@ export default function(state /*: AppState */ = defaultValue, action /*: Action | |||
return state; | |||
} | |||
export function getRootQualifiers(state /*: AppState */) { | |||
export function getRootQualifiers(state: AppState): string[] | undefined { | |||
return state.qualifiers; | |||
} |
@@ -33,7 +33,10 @@ export const onFail = dispatch => error => | |||
parseError(error).then(message => dispatch(addGlobalErrorMessage(message))); | |||
export const fetchAppState = () => dispatch => | |||
getGlobalNavigation().then(appState => dispatch(setAppState(appState)), onFail(dispatch)); | |||
getGlobalNavigation().then(appState => { | |||
dispatch(setAppState(appState)); | |||
return appState; | |||
}, onFail(dispatch)); | |||
export const fetchLanguages = () => dispatch => | |||
getLanguages().then(languages => dispatch(receiveLanguages(languages)), onFail(dispatch)); |
@@ -230,6 +230,7 @@ are_you_sure=Are you sure? | |||
assigned_to=Assigned to | |||
bulk_change=Bulk Change | |||
bulleted_point=Bulleted point | |||
buy_developer_pack=Buy Developer Pack now | |||
check_project=Check project | |||
coding_rules=Rules | |||
clear=Clear | |||
@@ -253,6 +254,7 @@ greater_or_equals=Greater or equals | |||
greater_than=Greater than | |||
help_tips=Help tips | |||
last_analysis_before=Last analysis before | |||
learn_more=Learn more | |||
less_or_equals=Less or equals | |||
less_than=Less than | |||
logging_out=You're logging out, please wait... | |||
@@ -3151,3 +3153,14 @@ onboarding.project_watcher.not_started=Once your project is analyzed, this page | |||
onboarding.project_watcher.in_progress=Analysis is in progress, please wait... | |||
onboarding.project_watcher.finished=Analysis is finished, redirecting... | |||
onboarding.project_watcher.failed=Something went wrong, please check the analysis logs. | |||
#------------------------------------------------------------------------------ | |||
# | |||
# BRANCHES | |||
# | |||
#------------------------------------------------------------------------------ | |||
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. |