diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2017-02-06 17:45:29 +0100 |
---|---|---|
committer | Stas Vilchik <stas-vilchik@users.noreply.github.com> | 2017-02-07 13:13:58 +0100 |
commit | a298f8fe28f83f0b08b7481d85792057f9881033 (patch) | |
tree | b873b47e0a7b0bd2df643c8b062155fa5957005e /server/sonar-web/src | |
parent | c54e107bb8305de177d8c805d0368eaf7df3bc37 (diff) | |
download | sonarqube-a298f8fe28f83f0b08b7481d85792057f9881033.tar.gz sonarqube-a298f8fe28f83f0b08b7481d85792057f9881033.zip |
SONAR-8666 Make it possible to create a new organization
Diffstat (limited to 'server/sonar-web/src')
7 files changed, 344 insertions, 22 deletions
diff --git a/server/sonar-web/src/main/js/api/organizations.js b/server/sonar-web/src/main/js/api/organizations.js index 4685c4ee2c9..ef998da81c3 100644 --- a/server/sonar-web/src/main/js/api/organizations.js +++ b/server/sonar-web/src/main/js/api/organizations.js @@ -18,7 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // @flow -import { getJSON, post } from '../helpers/request'; +import { getJSON, post, postJSON } from '../helpers/request'; +import type { Organization } from '../store/organizations/duck'; export const getOrganizations = (organizations?: Array<string>) => { const data = {}; @@ -28,13 +29,7 @@ export const getOrganizations = (organizations?: Array<string>) => { return getJSON('/api/organizations/search', data); }; -type GetOrganizationType = null | { - avatar?: string, - description?: string, - key: string, - name: string, - url?: string -}; +type GetOrganizationType = null | Organization; type GetOrganizationNavigation = { canAdmin: boolean, @@ -49,6 +44,10 @@ export const getOrganizationNavigation = (key: string): Promise<GetOrganizationN return getJSON('/api/navigation/organization', { organization: key }).then(r => r.organization); }; +export const createOrganization = (fields: {}): Promise<Organization> => ( + postJSON('/api/organizations/create', fields).then(r => r.organization) +); + export const updateOrganization = (key: string, changes: {}) => ( post('/api/organizations/update', { key, ...changes }) ); diff --git a/server/sonar-web/src/main/js/apps/account/components/Account.js b/server/sonar-web/src/main/js/apps/account/components/Account.js index ec2f895d076..e87d7911149 100644 --- a/server/sonar-web/src/main/js/apps/account/components/Account.js +++ b/server/sonar-web/src/main/js/apps/account/components/Account.js @@ -21,7 +21,7 @@ import React from 'react'; import { connect } from 'react-redux'; import Nav from './Nav'; import UserCard from './UserCard'; -import { getCurrentUser } from '../../../store/rootReducer'; +import { getCurrentUser, areThereCustomOrganizations } from '../../../store/rootReducer'; import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication'; import '../account.css'; @@ -44,7 +44,7 @@ class Account extends React.Component { <header className="account-header"> <div className="account-container clearfix"> <UserCard user={currentUser}/> - <Nav user={currentUser}/> + <Nav user={currentUser} customOrganizations={this.props.customOrganizations}/> </div> </header> @@ -54,8 +54,9 @@ class Account extends React.Component { } } -export default connect( - state => ({ - currentUser: getCurrentUser(state) - }) -)(Account); +const mapStateToProps = state => ({ + currentUser: getCurrentUser(state), + customOrganizations: areThereCustomOrganizations(state) +}); + +export default connect(mapStateToProps)(Account); diff --git a/server/sonar-web/src/main/js/apps/account/components/Nav.js b/server/sonar-web/src/main/js/apps/account/components/Nav.js index f02b4f2db89..2889e7c259e 100644 --- a/server/sonar-web/src/main/js/apps/account/components/Nav.js +++ b/server/sonar-web/src/main/js/apps/account/components/Nav.js @@ -17,11 +17,16 @@ * 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 { Link, IndexLink } from 'react-router'; import { translate } from '../../../helpers/l10n'; -const Nav = () => ( +type Props = { + customOrganizations: boolean +}; + +const Nav = ({ customOrganizations }: Props) => ( <nav className="account-nav clearfix"> <ul className="nav navbar-nav nav-tabs"> <li> @@ -39,11 +44,20 @@ const Nav = () => ( {translate('my_account.notifications')} </Link> </li> - <li> - <Link to="/account/projects/" activeClassName="active"> - {translate('my_account.projects')} - </Link> - </li> + {!customOrganizations && ( + <li> + <Link to="/account/projects/" activeClassName="active"> + {translate('my_account.projects')} + </Link> + </li> + )} + {customOrganizations && ( + <li> + <Link to="/account/organizations" activeClassName="active"> + {translate('my_account.organizations')} + </Link> + </li> + )} </ul> </nav> ); 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.js new file mode 100644 index 00000000000..4196449ad0f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.js @@ -0,0 +1,240 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 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. + */ +// @flow +import React from 'react'; +import Modal from 'react-modal'; +import debounce from 'lodash/debounce'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; +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.Component { + mounted: boolean; + + props: { + createOrganization: () => Promise<*>, + router: { push: (string) => void } + }; + + state: State = { + loading: false, + avatar: '', + avatarImage: '', + description: '', + key: '', + name: '', + url: '' + }; + + constructor (props) { + super(props); + 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 isOpen={true} + contentLabel="modal form" + className="modal" + overlayClassName="modal-overlay" + 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-key"> + {translate('organization.key')} + </label> + <input id="organization-key" + autoFocus={true} + name="key" + type="text" + 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-name"> + {translate('organization.name')} + <em className="mandatory">*</em> + </label> + <input id="organization-name" + name="name" + required={true} + type="text" + 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-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"> + {this.state.processing ? ( + <i className="spinner"/> + ) : ( + <div> + <button 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)); diff --git a/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.js b/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.js new file mode 100644 index 00000000000..ca97abba72d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.js @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 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. + */ +// @flow +import React from 'react'; +import Helmet from 'react-helmet'; +import { Link } from 'react-router'; +import { translate } from '../../../helpers/l10n'; + +export default class UserOrganizations extends React.Component { + render () { + const title = translate('my_account.organizations') + ' - ' + translate('my_account.page'); + + return ( + <div className="account-body account-container"> + <Helmet title={title} titleTemplate="%s - SonarQube"/> + + <header className="page-header"> + <h2 className="page-title">{translate('my_account.organizations')}</h2> + <div className="page-actions"> + <Link to="/account/organizations/create" className="button">{translate('create')}</Link> + </div> + <div className="page-description"> + {translate('my_account.organizations.description')} + </div> + </header> + + {this.props.children} + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/account/routes.js b/server/sonar-web/src/main/js/apps/account/routes.js index 2cf1a59c8b4..f5a3590906f 100644 --- a/server/sonar-web/src/main/js/apps/account/routes.js +++ b/server/sonar-web/src/main/js/apps/account/routes.js @@ -24,6 +24,8 @@ import ProjectsContainer from './projects/ProjectsContainer'; import Security from './components/Security'; import Profile from './profile/Profile'; import Notifications from './notifications/Notifications'; +import UserOrganizations from './organizations/UserOrganizations'; +import CreateOrganizationForm from './organizations/CreateOrganizationForm'; export default ( <Route component={Account}> @@ -31,6 +33,9 @@ export default ( <Route path="security" component={Security}/> <Route path="projects" component={ProjectsContainer}/> <Route path="notifications" component={Notifications}/> + <Route path="organizations" component={UserOrganizations}> + <Route path="create" component={CreateOrganizationForm}/> + </Route> <Route path="issues" onEnter={() => { window.location = window.baseUrl + '/issues' + window.location.hash + '|assigned_to_me=true'; 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 51266527fc9..cd8ee61d9a6 100644 --- a/server/sonar-web/src/main/js/apps/organizations/actions.js +++ b/server/sonar-web/src/main/js/apps/organizations/actions.js @@ -22,7 +22,13 @@ import * as api from '../../api/organizations'; import { onFail } from '../../store/rootActions'; import * as actions from '../../store/organizations/duck'; import { addGlobalSuccessMessage } from '../../store/globalMessages/duck'; -import { translate } from '../../helpers/l10n'; +import { translate, translateWithParameters } from '../../helpers/l10n'; +import type { Organization } from '../../store/organizations/duck'; + +const onRejected = (dispatch: Function) => (error: Object) => { + onFail(dispatch)(error); + return Promise.reject(); +}; export const fetchOrganization = (key: string): Function => (dispatch: Function): Promise<*> => { const onFulfilled = ([organization, navigation]) => { @@ -38,6 +44,15 @@ export const fetchOrganization = (key: string): Function => (dispatch: Function) ]).then(onFulfilled, onFail(dispatch)); }; +export const createOrganization = (fields: {}): Function => (dispatch: Function): Promise<*> => { + const onFulfilled = (organization: Organization) => { + dispatch(actions.receiveOrganizations([organization])); + dispatch(addGlobalSuccessMessage(translateWithParameters('organization.created', organization.name))); + }; + + return api.createOrganization(fields).then(onFulfilled, onRejected(dispatch)); +}; + export const updateOrganization = (key: string, changes: {}): Function => (dispatch: Function): Promise<*> => { const onFulfilled = () => { dispatch(actions.updateOrganization(key, changes)); |