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;
.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;
}
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';
</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>
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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();
+});
--- /dev/null
+// 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>
+`;
+++ /dev/null
-/*
- * 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.
- */
-// @flow
-import React from 'react';
-import { debounce } from 'lodash';
-import { connect } from 'react-redux';
-import { withRouter } from 'react-router';
-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 }
- };
-*/
-
- constructor(props) {
- super(props);
- this.state = {
- loading: false,
- avatar: '',
- avatarImage: '',
- description: '',
- key: '',
- name: '',
- url: ''
- };
- this.changeAvatarImage = debounce(this.changeAvatarImage, 500);
- }
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- 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;
- this.setState({ avatar: value });
- this.changeAvatarImage(value);
- };
-
- changeAvatarImage = (value /*: string */) => {
- this.setState({ avatarImage: value });
- };
-
- handleSubmit = (e /*: Object */) => {
- e.preventDefault();
- const organization /*: Object */ = { name: this.state.name };
- if (this.state.avatar) {
- Object.assign(organization, { avatar: this.state.avatar });
- }
- if (this.state.description) {
- Object.assign(organization, { description: this.state.description });
- }
- if (this.state.key) {
- Object.assign(organization, { key: this.state.key });
- }
- if (this.state.url) {
- Object.assign(organization, { url: this.state.url });
- }
- this.setState({ loading: true });
- this.props
- .createOrganization(organization)
- .then(this.stopProcessingAndClose, this.stopProcessing);
- };
-
- render() {
- return (
- <Modal contentLabel="modal form" onRequestClose={this.closeForm}>
- <header className="modal-head">
- <h2>{translate('my_account.create_organization')}</h2>
- </header>
-
- <form onSubmit={this.handleSubmit}>
- <div className="modal-body">
- <div className="modal-field">
- <label htmlFor="organization-name">
- {translate('organization.name')}
- <em className="mandatory">*</em>
- </label>
- <input
- id="organization-name"
- autoFocus={true}
- name="name"
- required={true}
- type="text"
- minLength="2"
- maxLength="64"
- value={this.state.name}
- disabled={this.state.loading}
- onChange={e => this.setState({ name: e.target.value })}
- />
- <div className="modal-field-description">
- {translate('organization.name.description')}
- </div>
- </div>
- <div className="modal-field">
- <label htmlFor="organization-key">{translate('organization.key')}</label>
- <input
- id="organization-key"
- name="key"
- type="text"
- minLength="2"
- maxLength="64"
- value={this.state.key}
- disabled={this.state.loading}
- onChange={e => this.setState({ key: e.target.value })}
- />
- <div className="modal-field-description">
- {translate('organization.key.description')}
- </div>
- </div>
- <div className="modal-field">
- <label htmlFor="organization-avatar">{translate('organization.avatar')}</label>
- <input
- id="organization-avatar"
- name="avatar"
- type="text"
- maxLength="256"
- value={this.state.avatar}
- disabled={this.state.loading}
- onChange={this.handleAvatarInputChange}
- />
- <div className="modal-field-description">
- {translate('organization.avatar.description')}
- </div>
- {!!this.state.avatarImage && (
- <div className="spacer-top spacer-bottom">
- <div className="little-spacer-bottom">
- {translate('organization.avatar.preview')}
- {':'}
- </div>
- <img src={this.state.avatarImage} alt="" height={30} />
- </div>
- )}
- </div>
- <div className="modal-field">
- <label htmlFor="organization-description">{translate('description')}</label>
- <textarea
- id="organization-description"
- name="description"
- rows="3"
- maxLength="256"
- value={this.state.description}
- disabled={this.state.loading}
- onChange={e => this.setState({ description: e.target.value })}
- />
- <div className="modal-field-description">
- {translate('organization.description.description')}
- </div>
- </div>
- <div className="modal-field">
- <label htmlFor="organization-url">{translate('organization.url')}</label>
- <input
- id="organization-url"
- name="url"
- type="text"
- maxLength="256"
- value={this.state.url}
- disabled={this.state.loading}
- onChange={e => this.setState({ url: e.target.value })}
- />
- <div className="modal-field-description">
- {translate('organization.url.description')}
- </div>
- </div>
- </div>
-
- <footer className="modal-foot">
- <div>
- {this.state.loading && <i className="spinner spacer-right" />}
- <button disabled={this.state.loading} type="submit">
- {translate('create')}
- </button>
- <button type="reset" className="button-link" onClick={this.closeForm}>
- {translate('cancel')}
- </button>
- </div>
- </footer>
- </form>
- </Modal>
- );
- }
-}
-
-const mapStateToProps = null;
-
-const mapDispatchToProps = { createOrganization };
-
-export default connect(mapStateToProps, mapDispatchToProps)(withRouter(CreateOrganizationForm));
--- /dev/null
+/*
+ * 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 { debounce } from 'lodash';
+import { connect } from 'react-redux';
+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';
+
+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: Props) {
+ super(props);
+ this.state = {
+ avatar: '',
+ avatarImage: '',
+ description: '',
+ key: '',
+ loading: false,
+ name: '',
+ url: ''
+ };
+ this.changeAvatarImage = debounce(this.changeAvatarImage, 500);
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ stopProcessing = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
+
+ handleAvatarInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
+ const { value } = event.currentTarget;
+ this.setState({ avatar: value });
+ this.changeAvatarImage(value);
+ };
+
+ changeAvatarImage = (value: string) => {
+ this.setState({ avatarImage: value });
+ };
+
+ 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 });
+ }
+ if (this.state.description) {
+ Object.assign(organization, { description: this.state.description });
+ }
+ if (this.state.key) {
+ Object.assign(organization, { key: this.state.key });
+ }
+ if (this.state.url) {
+ Object.assign(organization, { url: this.state.url });
+ }
+ this.setState({ loading: true });
+ this.props.createOrganization(organization).then(this.props.onCreate, this.stopProcessing);
+ };
+
+ render() {
+ return (
+ <Modal contentLabel="modal form" onRequestClose={this.props.onClose}>
+ <header className="modal-head">
+ <h2>{translate('my_account.create_organization')}</h2>
+ </header>
+
+ <form onSubmit={this.handleSubmit}>
+ <div className="modal-body">
+ <div className="modal-field">
+ <label htmlFor="organization-name">
+ {translate('organization.name')}
+ <em className="mandatory">*</em>
+ </label>
+ <input
+ id="organization-name"
+ autoFocus={true}
+ name="name"
+ required={true}
+ type="text"
+ minLength={2}
+ maxLength={64}
+ value={this.state.name}
+ disabled={this.state.loading}
+ onChange={this.handleNameChange}
+ />
+ <div className="modal-field-description">
+ {translate('organization.name.description')}
+ </div>
+ </div>
+ <div className="modal-field">
+ <label htmlFor="organization-key">{translate('organization.key')}</label>
+ <input
+ id="organization-key"
+ name="key"
+ type="text"
+ minLength={2}
+ maxLength={64}
+ value={this.state.key}
+ disabled={this.state.loading}
+ onChange={this.handleKeyChange}
+ />
+ <div className="modal-field-description">
+ {translate('organization.key.description')}
+ </div>
+ </div>
+ <div className="modal-field">
+ <label htmlFor="organization-avatar">{translate('organization.avatar')}</label>
+ <input
+ id="organization-avatar"
+ name="avatar"
+ type="text"
+ maxLength={256}
+ value={this.state.avatar}
+ disabled={this.state.loading}
+ onChange={this.handleAvatarInputChange}
+ />
+ <div className="modal-field-description">
+ {translate('organization.avatar.description')}
+ </div>
+ {!!this.state.avatarImage && (
+ <div className="spacer-top spacer-bottom">
+ <div className="little-spacer-bottom">
+ {translate('organization.avatar.preview')}
+ {':'}
+ </div>
+ <img src={this.state.avatarImage} alt="" height={30} />
+ </div>
+ )}
+ </div>
+ <div className="modal-field">
+ <label htmlFor="organization-description">{translate('description')}</label>
+ <textarea
+ id="organization-description"
+ name="description"
+ rows={3}
+ maxLength={256}
+ value={this.state.description}
+ disabled={this.state.loading}
+ onChange={this.handleDescriptionChange}
+ />
+ <div className="modal-field-description">
+ {translate('organization.description.description')}
+ </div>
+ </div>
+ <div className="modal-field">
+ <label htmlFor="organization-url">{translate('organization.url')}</label>
+ <input
+ id="organization-url"
+ name="url"
+ type="text"
+ maxLength={256}
+ value={this.state.url}
+ disabled={this.state.loading}
+ onChange={this.handleUrlChange}
+ />
+ <div className="modal-field-description">
+ {translate('organization.url.description')}
+ </div>
+ </div>
+ </div>
+
+ <footer className="modal-foot">
+ <div>
+ {this.state.loading && <i className="spinner spacer-right" />}
+ <button disabled={this.state.loading} type="submit">
+ {translate('create')}
+ </button>
+ <button className="button-link" onClick={this.props.onClose} type="reset">
+ {translate('cancel')}
+ </button>
+ </div>
+ </footer>
+ </form>
+ </Modal>
+ );
+ }
+}
+
+const mapDispatchToProps: DispatchProps = { createOrganization: createOrganization as any };
+
+export default connect(null, mapDispatchToProps)(CreateOrganizationForm);
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';
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;
}
};
+ 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';
<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 ? (
<OrganizationsList organizations={this.props.organizations} />
)}
- {this.props.children}
+ {this.state.createOrganization && (
+ <CreateOrganizationForm
+ onClose={this.closeCreateOrganizationForm}
+ onCreate={this.handleCreate}
+ />
+ )}
</div>
);
}
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));
- }
- }
- ]
+ }
}
]
}
dispatch(
addGlobalSuccessMessage(translateWithParameters('organization.created', organization.name))
);
+ return organization;
};
return api.createOrganization(fields).then(onFulfilled, onRejected(dispatch));
--- /dev/null
+/*
+ * 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
+ });
+ }
+}
--- /dev/null
+/*
+ * 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 });
+});
--- /dev/null
+/*
+ * 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>
+ );
+}
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
#------------------------------------------------------------------------------