From c003387eb63a644d9e887dcb7799d962ec27310c Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Mon, 10 Sep 2018 15:40:46 +0200 Subject: [PATCH] SONARCLOUD-120 Add new "Create Organization" page (#691) --- server/sonar-web/package.json | 2 +- .../main/js/app/components/StartupModal.tsx | 24 +- .../components/nav/global/GlobalNavPlus.tsx | 63 +---- .../__snapshots__/GlobalNavPlus-test.tsx.snap | 19 +- .../main/js/app/styles/components/modals.css | 14 +- .../src/main/js/app/styles/init/forms.css | 29 +- .../src/main/js/app/utils/startReactApp.js | 9 + .../organizations/CreateOrganizationForm.tsx | 252 ------------------ .../organizations/UserOrganizations.tsx | 25 +- .../organization/CreateOrganization.tsx | 107 ++++++++ .../organization/OrganizationDetailsInput.tsx | 75 ++++++ .../organization/OrganizationDetailsStep.tsx | 216 +++++++++++++++ .../__tests__/CreateOrganization-test.tsx | 45 ++++ .../OrganizationDetailsInput-test.tsx | 54 ++++ .../OrganizationDetailsStep-test.tsx | 101 +++++++ .../CreateOrganization-test.tsx.snap | 60 +++++ .../OrganizationDetailsInput-test.tsx.snap | 31 +++ .../OrganizationDetailsStep-test.tsx.snap | 155 +++++++++++ .../__tests__/whenLoggedIn-test.tsx | 53 ++++ .../apps/create/organization/whenLoggedIn.tsx | 54 ++++ .../projects/create/ManualProjectCreate.tsx | 38 +-- .../__tests__/ManualProjectCreate-test.tsx | 16 +- .../ManualProjectCreate-test.tsx.snap | 10 +- .../tutorials/onboarding/OnboardingPage.tsx | 10 +- .../webhooks/components/CreateWebhookForm.tsx | 4 +- .../controls/InputValidationField.tsx | 2 +- .../controls/ModalValidationField.tsx | 4 +- .../js/components/controls/ValidationForm.tsx | 65 +++++ .../components/controls/ValidationModal.tsx | 95 +++---- .../__tests__/ValidationForm-test.tsx | 47 ++++ .../__tests__/ValidationModal-test.tsx | 47 +--- .../ModalValidationField-test.tsx.snap | 2 +- .../ValidationForm-test.tsx.snap | 17 ++ .../ValidationModal-test.tsx.snap | 54 +--- .../src/main/js/helpers/testUtils.ts | 15 ++ server/sonar-web/yarn.lock | 59 +++- .../resources/org/sonar/l10n/core.properties | 19 ++ 37 files changed, 1301 insertions(+), 591 deletions(-) delete mode 100644 server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsInput.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsInput-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsInput-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/whenLoggedIn-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx create mode 100644 server/sonar-web/src/main/js/components/controls/ValidationForm.tsx create mode 100644 server/sonar-web/src/main/js/components/controls/__tests__/ValidationForm-test.tsx create mode 100644 server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationForm-test.tsx.snap diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index f8bcb27794b..2f7a6c21da2 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -16,7 +16,7 @@ "d3-shape": "1.2.0", "d3-zoom": "1.7.1", "date-fns": "1.29.0", - "formik": "0.11.11", + "formik": "1.2.0", "history": "3.3.0", "intl-relativeformat": "2.1.0", "keymaster": "1.6.2", diff --git a/server/sonar-web/src/main/js/app/components/StartupModal.tsx b/server/sonar-web/src/main/js/app/components/StartupModal.tsx index ee49e969d49..8481e4dc86f 100644 --- a/server/sonar-web/src/main/js/app/components/StartupModal.tsx +++ b/server/sonar-web/src/main/js/app/components/StartupModal.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { CurrentUser, isLoggedIn, Organization } from '../types'; +import { CurrentUser, isLoggedIn } from '../types'; import { differenceInDays, parseDate, toShortNotSoISOString } from '../../helpers/dates'; import { EditionKey } from '../../apps/marketplace/utils'; import { getCurrentUser, getAppState, Store } from '../../store/rootReducer'; @@ -32,9 +32,6 @@ import { isSonarCloud } from '../../helpers/system'; import { skipOnboarding } from '../../api/users'; import { lazyLoad } from '../../components/lazyLoad'; -const CreateOrganizationForm = lazyLoad(() => - import('../../apps/account/organizations/CreateOrganizationForm') -); const OnboardingModal = lazyLoad(() => import('../../apps/tutorials/onboarding/OnboardingModal')); const LicensePromptModal = lazyLoad( () => import('../../apps/marketplace/components/LicensePromptModal'), @@ -68,7 +65,6 @@ type Props = StateProps & DispatchProps & OwnProps; enum ModalKey { license, onboarding, - organizationOnboarding, projectOnboarding, teamOnboarding } @@ -119,17 +115,13 @@ export class StartupModal extends React.PureComponent { }); }; - closeOrganizationOnboarding = ({ key }: Pick) => { - this.closeOnboarding(); - this.context.router.push(`/organizations/${key}`); - }; - openOnboarding = () => { this.setState({ modal: ModalKey.onboarding }); }; openOrganizationOnboarding = () => { - this.setState({ modal: ModalKey.organizationOnboarding }); + this.closeOnboarding(); + this.context.router.push('/create-organization'); }; openProjectOnboarding = () => { @@ -160,11 +152,11 @@ export class StartupModal extends React.PureComponent { this.setState({ automatic: true, modal: ModalKey.license }); return Promise.resolve(); } - return Promise.reject('License exists'); + return Promise.reject(); }); } } - return Promise.reject('No license prompt'); + return Promise.reject(); }; tryAutoOpenOnboarding = () => { @@ -201,12 +193,6 @@ export class StartupModal extends React.PureComponent { {modal === ModalKey.projectOnboarding && ( )} - {modal === ModalKey.organizationOnboarding && ( - - )} {modal === ModalKey.teamOnboarding && ( )} 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 index a191797ca53..57f21824533 100644 --- 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 @@ -18,8 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import * as PropTypes from 'prop-types'; -import CreateOrganizationForm from '../../../../apps/account/organizations/CreateOrganizationForm'; +import { Link } from 'react-router'; import PlusIcon from '../../../../components/icons-components/PlusIcon'; import Dropdown from '../../../../components/controls/Dropdown'; import { translate } from '../../../../helpers/l10n'; @@ -28,40 +27,12 @@ interface Props { openProjectOnboarding: () => 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 }; - } - +export default class GlobalNavPlus extends React.PureComponent { handleNewProjectClick = (event: React.SyntheticEvent) => { event.preventDefault(); this.props.openProjectOnboarding(); }; - 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 ( {
  • - + {translate('my_account.create_new_organization')} - +
  • } tagName="li"> - {({ onToggleClick, open }) => ( - <> - - - - - {this.state.createOrganization && ( - - )} - - )} + + +
    ); } 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 index 56a23c2dc5e..a051a721d21 100644 --- 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 @@ -19,16 +19,25 @@ exports[`render 1`] = ` className="divider" />
  • - my_account.create_new_organization - +
  • } tagName="li" -/> +> + + + + `; diff --git a/server/sonar-web/src/main/js/app/styles/components/modals.css b/server/sonar-web/src/main/js/app/styles/components/modals.css index dd8be688094..1a4274b1451 100644 --- a/server/sonar-web/src/main/js/app/styles/components/modals.css +++ b/server/sonar-web/src/main/js/app/styles/components/modals.css @@ -246,21 +246,11 @@ min-height: var(--controlHeight); } -.modal-validation-field input:not(.has-error), -.modal-validation-field .Select:not(.has-error) { +.modal-validation-field input:not(.is-invalid), +.modal-validation-field .Select:not(.is-invalid) { margin-bottom: 18px; } -.modal-validation-field .has-error, -.modal-validation-field .has-error > .Select-control { - border-color: var(--red); -} - -.modal-validation-field .is-valid, -.modal-validation-field .is-valid > .Select-control { - border-color: var(--green); -} - .modal-field-description { padding-bottom: 4px; line-height: 1.4; diff --git a/server/sonar-web/src/main/js/app/styles/init/forms.css b/server/sonar-web/src/main/js/app/styles/init/forms.css index 93a7d5c7a15..4d9d83329fd 100644 --- a/server/sonar-web/src/main/js/app/styles/init/forms.css +++ b/server/sonar-web/src/main/js/app/styles/init/forms.css @@ -69,14 +69,27 @@ select:invalid { outline: none; } -input[type='text'].invalid, -input[type='password'].invalid, -input[type='email'].invalid, -input[type='search'].invalid, -input[type='date'].invalid, -input[type='number'].invalid, -textarea.invalid, -select.invalid { +input[type='text'].is-valid, +input[type='password'].is-valid, +input[type='email'].is-valid, +input[type='search'].is-valid, +input[type='date'].is-valid, +input[type='number'].is-valid, +textarea.is-valid, +select.is-valid, +.is-valid > .Select-control { + border-color: var(--green); +} + +input[type='text'].is-invalid, +input[type='password'].is-invalid, +input[type='email'].is-invalid, +input[type='search'].is-invalid, +input[type='date'].is-invalid, +input[type='number'].is-invalid, +textarea.is-invalid, +select.is-invalid, +.is-invalid > .Select-control { border-color: var(--red); } diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.js b/server/sonar-web/src/main/js/app/utils/startReactApp.js index d521a298da7..474e9f56083 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.js +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.js @@ -67,6 +67,7 @@ import webhooksRoutes from '../../apps/webhooks/routes'; import { maintenanceRoutes, setupRoutes } from '../../apps/maintenance/routes'; import { globalPermissionsRoutes, projectPermissionsRoutes } from '../../apps/permissions/routes'; import { lazyLoad } from '../../components/lazyLoad'; +import { isSonarCloud } from '../../helpers/system'; function handleUpdate() { const { action } = this.state.location; @@ -171,6 +172,14 @@ const startReactApp = (lang, currentUser, appState) => { /> + {isSonarCloud() && ( + + import('../../apps/create/organization/CreateOrganization') + )} + /> + )} diff --git a/server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.tsx b/server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.tsx deleted file mode 100644 index 25636965522..00000000000 --- a/server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.tsx +++ /dev/null @@ -1,252 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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, OrganizationBase } from '../../../app/types'; -import Modal from '../../../components/controls/Modal'; -import DocTooltip from '../../../components/docs/DocTooltip'; -import { translate } from '../../../helpers/l10n'; -import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; - -interface DispatchProps { - createOrganization: (fields: OrganizationBase) => Promise; -} - -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 = false; - - 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) => { - const { value } = event.currentTarget; - this.setState({ avatar: value }); - this.changeAvatarImage(value); - }; - - changeAvatarImage = (value: string) => { - this.setState({ avatarImage: value }); - }; - - 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 }); - } - 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 ( - -
    -

    - {translate('my_account.create_organization')} - -

    -
    - -
    -
    -
    - - -
    - {translate('organization.name.description')} -
    -
    -
    - - -
    - {translate('organization.key.description')} -
    -
    -
    - - -
    - {translate('organization.avatar.description')} -
    - {!!this.state.avatarImage && ( -
    -
    - {translate('organization.avatar.preview')} - {':'} -
    - -
    - )} -
    -
    - -