aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2017-02-06 17:45:29 +0100
committerStas Vilchik <stas-vilchik@users.noreply.github.com>2017-02-07 13:13:58 +0100
commita298f8fe28f83f0b08b7481d85792057f9881033 (patch)
treeb873b47e0a7b0bd2df643c8b062155fa5957005e /server/sonar-web/src
parentc54e107bb8305de177d8c805d0368eaf7df3bc37 (diff)
downloadsonarqube-a298f8fe28f83f0b08b7481d85792057f9881033.tar.gz
sonarqube-a298f8fe28f83f0b08b7481d85792057f9881033.zip
SONAR-8666 Make it possible to create a new organization
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/api/organizations.js15
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/Account.js15
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/Nav.js26
-rw-r--r--server/sonar-web/src/main/js/apps/account/organizations/CreateOrganizationForm.js240
-rw-r--r--server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.js48
-rw-r--r--server/sonar-web/src/main/js/apps/account/routes.js5
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/actions.js17
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));