@@ -27,13 +27,19 @@ | |||
border: none !important; | |||
} | |||
.navbar-help { | |||
.navbar-help, | |||
.navbar-plus { | |||
display: inline-block; | |||
height: var(--globalNavHeight); | |||
padding: calc(var(--globalNavHeight) - var(--globalNavContentHeight)) 12px !important; | |||
border-bottom: none !important; | |||
color: #fff !important; | |||
} | |||
.navbar-plus { | |||
margin-right: calc(-1 * var(--gridSize)); | |||
} | |||
.global-navbar-menu { | |||
display: flex; | |||
align-items: center; | |||
@@ -56,7 +62,8 @@ | |||
.navbar-brand:focus, | |||
.global-navbar-menu > li > a.active, | |||
.global-navbar-menu > li > a:hover, | |||
.global-navbar-menu > li > a:focus { | |||
.global-navbar-menu > li > a:focus, | |||
.global-navbar-menu > .dropdown.open > a { | |||
background-color: #020202; | |||
} | |||
@@ -24,9 +24,11 @@ import GlobalNavBranding from './GlobalNavBranding'; | |||
import GlobalNavMenu from './GlobalNavMenu'; | |||
import GlobalNavExplore from './GlobalNavExplore'; | |||
import GlobalNavUserContainer from './GlobalNavUserContainer'; | |||
import GlobalNavPlus from './GlobalNavPlus'; | |||
import Search from '../../search/Search'; | |||
import GlobalHelp from '../../help/GlobalHelp'; | |||
import * as theme from '../../../theme'; | |||
import { isLoggedIn } from '../../../types'; | |||
import NavBar from '../../../../components/nav/NavBar'; | |||
import Tooltip from '../../../../components/controls/Tooltip'; | |||
import HelpIcon from '../../../../components/icons-components/HelpIcon'; | |||
@@ -130,6 +132,10 @@ class GlobalNav extends React.PureComponent { | |||
</a> | |||
</li> | |||
<Search appState={this.props.appState} currentUser={this.props.currentUser} /> | |||
{isLoggedIn(this.props.currentUser) && | |||
this.props.onSonarCloud && ( | |||
<GlobalNavPlus openOnboardingTutorial={this.openOnboardingTutorial} /> | |||
)} | |||
<GlobalNavUserContainer {...this.props} /> | |||
</ul> | |||
@@ -0,0 +1,101 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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 * as classNames from 'classnames'; | |||
import * as PropTypes from 'prop-types'; | |||
import CreateOrganizationForm from '../../../../apps/account/organizations/CreateOrganizationForm'; | |||
import PlusIcon from '../../../../components/icons-components/PlusIcon'; | |||
import Dropdown from '../../../../components/controls/Dropdown'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
interface Props { | |||
openOnboardingTutorial: () => void; | |||
} | |||
interface State { | |||
createOrganization: boolean; | |||
} | |||
export default class GlobalNavPlus extends React.PureComponent<Props, State> { | |||
static contextTypes = { | |||
router: PropTypes.object | |||
}; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { createOrganization: false }; | |||
} | |||
handleNewProjectClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
this.props.openOnboardingTutorial(); | |||
}; | |||
openCreateOrganizationForm = () => this.setState({ createOrganization: true }); | |||
closeCreateOrganizationForm = () => this.setState({ createOrganization: false }); | |||
handleNewOrganizationClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.openCreateOrganizationForm(); | |||
}; | |||
handleCreateOrganization = ({ key }: { key: string }) => { | |||
this.closeCreateOrganizationForm(); | |||
this.context.router.push(`/organizations/${key}`); | |||
}; | |||
render() { | |||
return ( | |||
<Dropdown> | |||
{({ onToggleClick, open }) => ( | |||
<li className={classNames('dropdown', { open })}> | |||
<a className="navbar-plus" href="#" onClick={onToggleClick}> | |||
<PlusIcon /> | |||
</a> | |||
<ul className="dropdown-menu dropdown-menu-right"> | |||
<li> | |||
<a className="js-new-project" href="#" onClick={this.handleNewProjectClick}> | |||
{translate('my_account.analyze_new_project')} | |||
</a> | |||
</li> | |||
<li className="divider" /> | |||
<li> | |||
<a | |||
className="js-new-organization" | |||
href="#" | |||
onClick={this.handleNewOrganizationClick}> | |||
{translate('my_account.create_new_organization')} | |||
</a> | |||
</li> | |||
</ul> | |||
{this.state.createOrganization && ( | |||
<CreateOrganizationForm | |||
onClose={this.closeCreateOrganizationForm} | |||
onCreate={this.handleCreateOrganization} | |||
/> | |||
)} | |||
</li> | |||
)} | |||
</Dropdown> | |||
); | |||
} | |||
} |
@@ -0,0 +1,38 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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 GlobalNavPlus from '../GlobalNavPlus'; | |||
import { click } from '../../../../../helpers/testUtils'; | |||
it('render', () => { | |||
const wrapper = shallow(<GlobalNavPlus openOnboardingTutorial={jest.fn()} />); | |||
expect(wrapper.is('Dropdown')).toBe(true); | |||
expect(wrapper.find('Dropdown').shallow()).toMatchSnapshot(); | |||
}); | |||
it('opens onboarding', () => { | |||
const openOnboardingTutorial = jest.fn(); | |||
const wrapper = shallow(<GlobalNavPlus openOnboardingTutorial={openOnboardingTutorial} />) | |||
.find('Dropdown') | |||
.shallow(); | |||
click(wrapper.find('.js-new-project')); | |||
expect(openOnboardingTutorial).toBeCalled(); | |||
}); |
@@ -0,0 +1,40 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`render 1`] = ` | |||
<li | |||
className="dropdown" | |||
> | |||
<a | |||
className="navbar-plus" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<PlusIcon /> | |||
</a> | |||
<ul | |||
className="dropdown-menu dropdown-menu-right" | |||
> | |||
<li> | |||
<a | |||
className="js-new-project" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
my_account.analyze_new_project | |||
</a> | |||
</li> | |||
<li | |||
className="divider" | |||
/> | |||
<li> | |||
<a | |||
className="js-new-organization" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
my_account.create_new_organization | |||
</a> | |||
</li> | |||
</ul> | |||
</li> | |||
`; |
@@ -17,44 +17,49 @@ | |||
* 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'; | |||
import { debounce } from 'lodash'; | |||
import { connect } from 'react-redux'; | |||
import { withRouter } from 'react-router'; | |||
import * as PropTypes from 'prop-types'; | |||
import { createOrganization } from '../../organizations/actions'; | |||
import { Organization } from '../../../app/types'; | |||
import Modal from '../../../components/controls/Modal'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { createOrganization } from '../../organizations/actions'; | |||
/*:: | |||
type State = { | |||
loading: boolean, | |||
avatar: string, | |||
avatarImage: string, | |||
description: string, | |||
key: string, | |||
name: string, | |||
url: string | |||
}; | |||
*/ | |||
class CreateOrganizationForm extends React.PureComponent { | |||
/*:: mounted: boolean; */ | |||
/*:: state: State; */ | |||
/*:: props: { | |||
createOrganization: (fields: {}) => Promise<*>, | |||
router: { push: string => void } | |||
interface DispatchProps { | |||
createOrganization: (fields: Partial<Organization>) => Promise<{ key: string }>; | |||
} | |||
interface Props extends DispatchProps { | |||
onClose: () => void; | |||
onCreate: (organization: { key: string }) => void; | |||
} | |||
interface State { | |||
avatar: string; | |||
avatarImage: string; | |||
description: string; | |||
key: string; | |||
loading: boolean; | |||
name: string; | |||
url: string; | |||
} | |||
class CreateOrganizationForm extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
static contextTypes = { | |||
router: PropTypes.object | |||
}; | |||
*/ | |||
constructor(props) { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
loading: false, | |||
avatar: '', | |||
avatarImage: '', | |||
description: '', | |||
key: '', | |||
loading: false, | |||
name: '', | |||
url: '' | |||
}; | |||
@@ -69,36 +74,37 @@ class CreateOrganizationForm extends React.PureComponent { | |||
this.mounted = false; | |||
} | |||
closeForm = () => { | |||
this.props.router.push('/account/organizations'); | |||
}; | |||
stopProcessing = () => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}; | |||
stopProcessingAndClose = () => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
this.closeForm(); | |||
}; | |||
handleAvatarInputChange = (e /*: Object */) => { | |||
const { value } = e.target; | |||
handleAvatarInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => { | |||
const { value } = event.currentTarget; | |||
this.setState({ avatar: value }); | |||
this.changeAvatarImage(value); | |||
}; | |||
changeAvatarImage = (value /*: string */) => { | |||
changeAvatarImage = (value: string) => { | |||
this.setState({ avatarImage: value }); | |||
}; | |||
handleSubmit = (e /*: Object */) => { | |||
e.preventDefault(); | |||
const organization /*: Object */ = { name: this.state.name }; | |||
handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => | |||
this.setState({ name: event.currentTarget.value }); | |||
handleKeyChange = (event: React.SyntheticEvent<HTMLInputElement>) => | |||
this.setState({ key: event.currentTarget.value }); | |||
handleDescriptionChange = (event: React.SyntheticEvent<HTMLTextAreaElement>) => | |||
this.setState({ description: event.currentTarget.value }); | |||
handleUrlChange = (event: React.SyntheticEvent<HTMLInputElement>) => | |||
this.setState({ url: event.currentTarget.value }); | |||
handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
const organization = { name: this.state.name }; | |||
if (this.state.avatar) { | |||
Object.assign(organization, { avatar: this.state.avatar }); | |||
} | |||
@@ -112,14 +118,12 @@ class CreateOrganizationForm extends React.PureComponent { | |||
Object.assign(organization, { url: this.state.url }); | |||
} | |||
this.setState({ loading: true }); | |||
this.props | |||
.createOrganization(organization) | |||
.then(this.stopProcessingAndClose, this.stopProcessing); | |||
this.props.createOrganization(organization).then(this.props.onCreate, this.stopProcessing); | |||
}; | |||
render() { | |||
return ( | |||
<Modal contentLabel="modal form" onRequestClose={this.closeForm}> | |||
<Modal contentLabel="modal form" onRequestClose={this.props.onClose}> | |||
<header className="modal-head"> | |||
<h2>{translate('my_account.create_organization')}</h2> | |||
</header> | |||
@@ -137,11 +141,11 @@ class CreateOrganizationForm extends React.PureComponent { | |||
name="name" | |||
required={true} | |||
type="text" | |||
minLength="2" | |||
maxLength="64" | |||
minLength={2} | |||
maxLength={64} | |||
value={this.state.name} | |||
disabled={this.state.loading} | |||
onChange={e => this.setState({ name: e.target.value })} | |||
onChange={this.handleNameChange} | |||
/> | |||
<div className="modal-field-description"> | |||
{translate('organization.name.description')} | |||
@@ -153,11 +157,11 @@ class CreateOrganizationForm extends React.PureComponent { | |||
id="organization-key" | |||
name="key" | |||
type="text" | |||
minLength="2" | |||
maxLength="64" | |||
minLength={2} | |||
maxLength={64} | |||
value={this.state.key} | |||
disabled={this.state.loading} | |||
onChange={e => this.setState({ key: e.target.value })} | |||
onChange={this.handleKeyChange} | |||
/> | |||
<div className="modal-field-description"> | |||
{translate('organization.key.description')} | |||
@@ -169,7 +173,7 @@ class CreateOrganizationForm extends React.PureComponent { | |||
id="organization-avatar" | |||
name="avatar" | |||
type="text" | |||
maxLength="256" | |||
maxLength={256} | |||
value={this.state.avatar} | |||
disabled={this.state.loading} | |||
onChange={this.handleAvatarInputChange} | |||
@@ -192,11 +196,11 @@ class CreateOrganizationForm extends React.PureComponent { | |||
<textarea | |||
id="organization-description" | |||
name="description" | |||
rows="3" | |||
maxLength="256" | |||
rows={3} | |||
maxLength={256} | |||
value={this.state.description} | |||
disabled={this.state.loading} | |||
onChange={e => this.setState({ description: e.target.value })} | |||
onChange={this.handleDescriptionChange} | |||
/> | |||
<div className="modal-field-description"> | |||
{translate('organization.description.description')} | |||
@@ -208,10 +212,10 @@ class CreateOrganizationForm extends React.PureComponent { | |||
id="organization-url" | |||
name="url" | |||
type="text" | |||
maxLength="256" | |||
maxLength={256} | |||
value={this.state.url} | |||
disabled={this.state.loading} | |||
onChange={e => this.setState({ url: e.target.value })} | |||
onChange={this.handleUrlChange} | |||
/> | |||
<div className="modal-field-description"> | |||
{translate('organization.url.description')} | |||
@@ -225,7 +229,7 @@ class CreateOrganizationForm extends React.PureComponent { | |||
<button disabled={this.state.loading} type="submit"> | |||
{translate('create')} | |||
</button> | |||
<button type="reset" className="button-link" onClick={this.closeForm}> | |||
<button className="button-link" onClick={this.props.onClose} type="reset"> | |||
{translate('cancel')} | |||
</button> | |||
</div> | |||
@@ -236,8 +240,6 @@ class CreateOrganizationForm extends React.PureComponent { | |||
} | |||
} | |||
const mapStateToProps = null; | |||
const mapDispatchToProps = { createOrganization }; | |||
const mapDispatchToProps: DispatchProps = { createOrganization: createOrganization as any }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(CreateOrganizationForm)); | |||
export default connect(null, mapDispatchToProps)(CreateOrganizationForm); |
@@ -20,8 +20,8 @@ | |||
import * as React from 'react'; | |||
import Helmet from 'react-helmet'; | |||
import { connect } from 'react-redux'; | |||
import { Link } from 'react-router'; | |||
import OrganizationsList from './OrganizationsList'; | |||
import CreateOrganizationForm from './CreateOrganizationForm'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { fetchIfAnyoneCanCreateOrganizations, fetchMyOrganizations } from './actions'; | |||
import { getAppState, getMyOrganizations, getGlobalSettingValue } from '../../../store/rootReducer'; | |||
@@ -38,17 +38,16 @@ interface DispatchProps { | |||
fetchMyOrganizations: () => Promise<void>; | |||
} | |||
interface Props extends StateProps, DispatchProps { | |||
children?: React.ReactNode; | |||
} | |||
interface Props extends StateProps, DispatchProps {} | |||
interface State { | |||
createOrganization: boolean; | |||
loading: boolean; | |||
} | |||
class UserOrganizations extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
state: State = { loading: true }; | |||
state: State = { createOrganization: false, loading: true }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
@@ -68,6 +67,20 @@ class UserOrganizations extends React.PureComponent<Props, State> { | |||
} | |||
}; | |||
openCreateOrganizationForm = () => this.setState({ createOrganization: true }); | |||
closeCreateOrganizationForm = () => this.setState({ createOrganization: false }); | |||
handleCreateClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.openCreateOrganizationForm(); | |||
}; | |||
handleCreate = () => { | |||
this.closeCreateOrganizationForm(); | |||
}; | |||
render() { | |||
const anyoneCanCreate = | |||
this.props.anyoneCanCreate != null && this.props.anyoneCanCreate.value === 'true'; | |||
@@ -82,9 +95,7 @@ class UserOrganizations extends React.PureComponent<Props, State> { | |||
<h2 className="page-title">{translate('my_account.organizations')}</h2> | |||
{canCreateOrganizations && ( | |||
<div className="page-actions"> | |||
<Link to="/account/organizations/create" className="button"> | |||
{translate('create')} | |||
</Link> | |||
<button onClick={this.handleCreateClick}>{translate('create')}</button> | |||
</div> | |||
)} | |||
{this.props.organizations.length > 0 ? ( | |||
@@ -104,7 +115,12 @@ class UserOrganizations extends React.PureComponent<Props, State> { | |||
<OrganizationsList organizations={this.props.organizations} /> | |||
)} | |||
{this.props.children} | |||
{this.state.createOrganization && ( | |||
<CreateOrganizationForm | |||
onClose={this.closeCreateOrganizationForm} | |||
onCreate={this.handleCreate} | |||
/> | |||
)} | |||
</div> | |||
); | |||
} |
@@ -52,15 +52,7 @@ const routes = [ | |||
path: 'organizations', | |||
getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) { | |||
import('./organizations/UserOrganizations').then(i => callback(null, i.default)); | |||
}, | |||
childRoutes: [ | |||
{ | |||
path: 'create', | |||
getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) { | |||
import('./organizations/CreateOrganizationForm').then(i => callback(null, i.default)); | |||
} | |||
} | |||
] | |||
} | |||
} | |||
] | |||
} |
@@ -72,6 +72,7 @@ export const createOrganization = (fields /*: Object */) => (dispatch /*: Functi | |||
dispatch( | |||
addGlobalSuccessMessage(translateWithParameters('organization.created', organization.name)) | |||
); | |||
return organization; | |||
}; | |||
return api.createOrganization(fields).then(onFulfilled, onRejected(dispatch)); |
@@ -0,0 +1,92 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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'; | |||
interface RenderProps { | |||
closeDropdown: () => void; | |||
onToggleClick: (event: React.SyntheticEvent<HTMLElement>) => void; | |||
open: boolean; | |||
} | |||
interface Props { | |||
children: (renderProps: RenderProps) => JSX.Element; | |||
onOpen?: () => void; | |||
} | |||
interface State { | |||
open: boolean; | |||
} | |||
export default class Dropdown extends React.PureComponent<Props, State> { | |||
toggleNode?: HTMLElement; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { open: false }; | |||
} | |||
componentDidUpdate(_: Props, prevState: State) { | |||
if (!prevState.open && this.state.open) { | |||
this.addClickHandler(); | |||
if (this.props.onOpen) { | |||
this.props.onOpen(); | |||
} | |||
} | |||
if (prevState.open && !this.state.open) { | |||
this.removeClickHandler(); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.removeClickHandler(); | |||
} | |||
addClickHandler = () => { | |||
window.addEventListener('click', this.handleWindowClick); | |||
}; | |||
removeClickHandler = () => { | |||
window.removeEventListener('click', this.handleWindowClick); | |||
}; | |||
handleWindowClick = (event: MouseEvent) => { | |||
if (!this.toggleNode || !this.toggleNode.contains(event.target as Node)) { | |||
this.closeDropdown(); | |||
} | |||
}; | |||
closeDropdown = () => this.setState({ open: false }); | |||
handleToggleClick = (event: React.SyntheticEvent<HTMLElement>) => { | |||
this.toggleNode = event.currentTarget; | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.setState(state => ({ open: !state.open })); | |||
}; | |||
render() { | |||
return this.props.children({ | |||
closeDropdown: this.closeDropdown, | |||
onToggleClick: this.handleToggleClick, | |||
open: this.state.open | |||
}); | |||
} | |||
} |
@@ -0,0 +1,44 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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 Dropdown from '../Dropdown'; | |||
import { click } from '../../../helpers/testUtils'; | |||
it('renders', () => { | |||
expect( | |||
shallow(<Dropdown>{() => <div />}</Dropdown>) | |||
.find('div') | |||
.exists() | |||
).toBeTruthy(); | |||
}); | |||
it('toggles', () => { | |||
const wrapper = shallow( | |||
<Dropdown>{({ onToggleClick }) => <button onClick={onToggleClick} />}</Dropdown> | |||
); | |||
expect(wrapper.state()).toEqual({ open: false }); | |||
click(wrapper.find('button')); | |||
expect(wrapper.state()).toEqual({ open: true }); | |||
click(wrapper.find('button')); | |||
expect(wrapper.state()).toEqual({ open: false }); | |||
}); |
@@ -0,0 +1,36 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info 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 { IconProps } from './types'; | |||
export default function PlusIcon({ className, fill = 'currentColor', size = 16 }: IconProps) { | |||
return ( | |||
<svg | |||
className={className} | |||
width={size} | |||
height={size} | |||
viewBox="0 0 16 16" | |||
version="1.1" | |||
xmlnsXlink="http://www.w3.org/1999/xlink" | |||
xmlSpace="preserve"> | |||
<path style={{ fill }} d="M1,7L7,7L7,1L9,1L9,7L15,7L15,9L9,9L9,15L7,15L7,9L1,9L1,7Z" /> | |||
</svg> | |||
); | |||
} |
@@ -1405,6 +1405,8 @@ my_account.organizations.no_results=You are not a member of any organizations ye | |||
my_account.create_organization=Create Organization | |||
my_account.search_project=Search Project | |||
my_account.set_notifications_for=Set notifications for | |||
my_account.analyze_new_project=Analyze new project | |||
my_account.create_new_organization=Create new organization | |||
#------------------------------------------------------------------------------ |