* 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 = {};
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,
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 })
);
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';
<header className="account-header">
<div className="account-container clearfix">
<UserCard user={currentUser}/>
- <Nav user={currentUser}/>
+ <Nav user={currentUser} customOrganizations={this.props.customOrganizations}/>
</div>
</header>
}
}
-export default connect(
- state => ({
- currentUser: getCurrentUser(state)
- })
-)(Account);
+const mapStateToProps = state => ({
+ currentUser: getCurrentUser(state),
+ customOrganizations: areThereCustomOrganizations(state)
+});
+
+export default connect(mapStateToProps)(Account);
* 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>
{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>
);
--- /dev/null
+/*
+ * 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));
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
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}>
<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';
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]) => {
]).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));
my_account.projects.analyzed_x=Analyzed {0}
my_account.projects.never_analyzed=Never analyzed
my_account.projects.x_characters_min=({0} characters min)
+my_account.organizations=Organizations
+my_account.organizations.description=Those organizations are the ones you are administering.
+my_account.create_organization=Create Organization
organization.avatar=Avatar
organization.avatar.description=Url of a small image that represents the organization (preferably 30px height).
organization.avatar.preview=Preview
+organization.created=Organization "{0}" has been created.
organization.delete=Delete Organization
organization.delete.description=Delete this organization from SonarQube. All projects belonging to the organization will be deleted as well. The operation cannot be undone.
organization.delete.question=Are you sure you want to delete this organization?
organization.description.description=Description of the organization (256 characters max).
organization.edit=Edit Organization
organization.key=Key
+organization.key.description=Key of the organization (2 to 32 characters). The key is unique to the whole SonarQube. When not specified, the key is computed from the name.
organization.name=Name
organization.name.description=Name of the organization (2 to 64 characters).
organization.updated=Organization details have been updated.