From 7260d343ea6a7289695a8c509860cbcf726e433c Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Thu, 7 Dec 2017 14:27:05 +0100 Subject: [PATCH] SONAR-9554 Make "Analyze a project" and "Create an org" more discoverable --- .../app/components/nav/global/GlobalNav.css | 11 +- .../js/app/components/nav/global/GlobalNav.js | 6 + .../components/nav/global/GlobalNavPlus.tsx | 101 ++++++++++++++ .../global/__tests__/GlobalNavPlus-test.tsx | 38 ++++++ .../__snapshots__/GlobalNavPlus-test.tsx.snap | 40 ++++++ ...tionForm.js => CreateOrganizationForm.tsx} | 128 +++++++++--------- .../organizations/UserOrganizations.tsx | 34 +++-- .../src/main/js/apps/account/routes.ts | 10 +- .../src/main/js/apps/organizations/actions.js | 1 + .../main/js/components/controls/Dropdown.tsx | 92 +++++++++++++ .../controls/__tests__/Dropdown-test.tsx | 44 ++++++ .../components/icons-components/PlusIcon.tsx | 36 +++++ .../resources/org/sonar/l10n/core.properties | 2 + 13 files changed, 460 insertions(+), 83 deletions(-) create mode 100644 server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx create mode 100644 server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavPlus-test.tsx create mode 100644 server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap rename server/sonar-web/src/main/js/apps/account/organizations/{CreateOrganizationForm.js => CreateOrganizationForm.tsx} (71%) create mode 100644 server/sonar-web/src/main/js/components/controls/Dropdown.tsx create mode 100644 server/sonar-web/src/main/js/components/controls/__tests__/Dropdown-test.tsx create mode 100644 server/sonar-web/src/main/js/components/icons-components/PlusIcon.tsx 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 { + {isLoggedIn(this.props.currentUser) && + this.props.onSonarCloud && ( + + )} 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 { + static contextTypes = { + router: PropTypes.object + }; + + constructor(props: Props) { + super(props); + this.state = { createOrganization: false }; + } + + handleNewProjectClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + this.props.openOnboardingTutorial(); + }; + + openCreateOrganizationForm = () => this.setState({ createOrganization: true }); + + closeCreateOrganizationForm = () => this.setState({ createOrganization: false }); + + handleNewOrganizationClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + this.openCreateOrganizationForm(); + }; + + handleCreateOrganization = ({ key }: { key: string }) => { + this.closeCreateOrganizationForm(); + this.context.router.push(`/organizations/${key}`); + }; + + render() { + return ( + + {({ onToggleClick, open }) => ( +
  • + + + + + {this.state.createOrganization && ( + + )} +
  • + )} +
    + ); + } +} 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(); + expect(wrapper.is('Dropdown')).toBe(true); + expect(wrapper.find('Dropdown').shallow()).toMatchSnapshot(); +}); + +it('opens onboarding', () => { + const openOnboardingTutorial = jest.fn(); + const wrapper = shallow() + .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`] = ` +
  • + + + + +
  • +`; 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 similarity index 71% rename from server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.js rename to 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) => 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 { + 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) => { + 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) => + this.setState({ name: event.currentTarget.value }); + + handleKeyChange = (event: React.SyntheticEvent) => + this.setState({ key: event.currentTarget.value }); + + handleDescriptionChange = (event: React.SyntheticEvent) => + this.setState({ description: event.currentTarget.value }); + + handleUrlChange = (event: React.SyntheticEvent) => + this.setState({ url: event.currentTarget.value }); + + handleSubmit = (event: React.SyntheticEvent) => { + 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 ( - +

    {translate('my_account.create_organization')}

    @@ -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} />
    {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} />
    {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 {