diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-12-07 14:27:05 +0100 |
---|---|---|
committer | Stas Vilchik <stas.vilchik@sonarsource.com> | 2018-01-02 10:38:10 +0100 |
commit | 7260d343ea6a7289695a8c509860cbcf726e433c (patch) | |
tree | 0a635089cf3d2ca61d902af9202d8d467be4328d /server/sonar-web/src/main | |
parent | 49f29073c935c97e52d4f7d2a8e02e79391e3ff2 (diff) | |
download | sonarqube-7260d343ea6a7289695a8c509860cbcf726e433c.tar.gz sonarqube-7260d343ea6a7289695a8c509860cbcf726e433c.zip |
SONAR-9554 Make "Analyze a project" and "Create an org" more discoverable
Diffstat (limited to 'server/sonar-web/src/main')
12 files changed, 458 insertions, 83 deletions
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.css b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.css index ac02282d585..48a78705251 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.css +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.css @@ -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; } diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js index 317077eb1af..564775c0179 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js @@ -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> diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx new file mode 100644 index 00000000000..12e1d701ba2 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx new file mode 100644 index 00000000000..e315be5676f --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap new file mode 100644 index 00000000000..4400a02b250 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.js b/server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.tsx index bd6b0c854fa..66087e08cb4 100644 --- a/server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.js +++ b/server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.tsx @@ -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); diff --git a/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx b/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx index 75a48b2bed9..c33f2be07eb 100644 --- a/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx +++ b/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/account/routes.ts b/server/sonar-web/src/main/js/apps/account/routes.ts index 90e66149504..3346b234050 100644 --- a/server/sonar-web/src/main/js/apps/account/routes.ts +++ b/server/sonar-web/src/main/js/apps/account/routes.ts @@ -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)); - } - } - ] + } } ] } diff --git a/server/sonar-web/src/main/js/apps/organizations/actions.js b/server/sonar-web/src/main/js/apps/organizations/actions.js index 9050442d5e8..b8484243b70 100644 --- a/server/sonar-web/src/main/js/apps/organizations/actions.js +++ b/server/sonar-web/src/main/js/apps/organizations/actions.js @@ -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)); diff --git a/server/sonar-web/src/main/js/components/controls/Dropdown.tsx b/server/sonar-web/src/main/js/components/controls/Dropdown.tsx new file mode 100644 index 00000000000..ba419195739 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/Dropdown.tsx @@ -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 + }); + } +} diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/Dropdown-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/Dropdown-test.tsx new file mode 100644 index 00000000000..82048afd2f7 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/Dropdown-test.tsx @@ -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 }); +}); diff --git a/server/sonar-web/src/main/js/components/icons-components/PlusIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/PlusIcon.tsx new file mode 100644 index 00000000000..1a0738f546b --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/PlusIcon.tsx @@ -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> + ); +} |