return getJSON('/api/organizations/search', data);
};
+export const getMyOrganizations = () => (
+ getJSON('/api/organizations/search_my_organizations').then(r => r.organizations)
+);
+
type GetOrganizationType = null | Organization;
type GetOrganizationNavigation = {
--- /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 OrganizationLink from '../../../components/ui/OrganizationLink';
+import type { Organization } from '../../../store/organizations/duck';
+
+export default class OrganizationCard extends React.Component {
+ props: {
+ organization: Organization
+ };
+
+ render () {
+ const { organization } = this.props;
+
+ return (
+ <div className="account-project-card clearfix">
+ <aside className="account-project-side">
+ {!!organization.avatar && (
+ <div className="spacer-bottom">
+ <img src={organization.avatar} height={30} alt={organization.name}/>
+ </div>
+ )}
+ {!!organization.url && (
+ <div className="text-limited text-top spacer-bottom">
+ <a className="small" href={organization.url} title={organization.url} rel="nofollow">
+ {organization.url}
+ </a>
+ </div>
+ )}
+ </aside>
+
+ <h3 className="account-project-name">
+ <OrganizationLink organization={organization}>
+ {organization.name}
+ </OrganizationLink>
+ </h3>
+
+ <div className="account-project-key">{organization.key}</div>
+
+ {!!organization.description && (
+ <div className="account-project-description">
+ {organization.description}
+ </div>
+ )}
+ </div>
+ );
+ }
+}
--- /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 OrganizationCard from './OrganizationCard';
+import type { Organization } from '../../../store/organizations/duck';
+
+export default class OrganizationsList extends React.Component {
+ props: {
+ organizations: Array<Organization>
+ };
+
+ render () {
+ return (
+ <ul className="account-projects-list">
+ {this.props.organizations.map(organization => (
+ <li key={organization.key}>
+ <OrganizationCard organization={organization}/>
+ </li>
+ ))}
+ </ul>
+ );
+ }
+}
// @flow
import React from 'react';
import Helmet from 'react-helmet';
+import { connect } from 'react-redux';
import { Link } from 'react-router';
+import OrganizationsList from './OrganizationsList';
import { translate } from '../../../helpers/l10n';
+import { fetchIfAnyoneCanCreateOrganizations, fetchMyOrganizations } from './actions';
+import { getMyOrganizations, getSettingValue, getCurrentUser } from '../../../store/rootReducer';
+import type { Organization } from '../../../store/organizations/duck';
+import { isUserAdmin } from '../../../helpers/users';
+
+class UserOrganizations extends React.Component {
+ mounted: boolean;
+
+ props: {
+ anyoneCanCreate: boolean,
+ currentUser: Object,
+ children: Object,
+ organizations: Array<Organization>,
+ fetchIfAnyoneCanCreateOrganizations: () => Promise<*>,
+ fetchMyOrganizations: () => Promise<*>
+ };
+
+ state: { loading: boolean } = {
+ loading: true
+ };
+
+ componentDidMount () {
+ this.mounted = true;
+ Promise.all([this.props.fetchMyOrganizations(), this.props.fetchIfAnyoneCanCreateOrganizations()]).then(() => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ });
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
-export default class UserOrganizations extends React.Component {
render () {
const title = translate('my_account.organizations') + ' - ' + translate('my_account.page');
+ const canCreateOrganizations = !this.state.loading &&
+ this.props.anyoneCanCreate ||
+ isUserAdmin(this.props.currentUser);
+
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>
+ {canCreateOrganizations && (
+ <div className="page-actions">
+ <Link to="/account/organizations/create" className="button">{translate('create')}</Link>
+ </div>
+ )}
+ {this.props.organizations.length > 0 ? (
+ <div className="page-description">
+ {translate('my_account.organizations.description')}
+ </div>
+ ) : (
+ <div className="page-description">
+ {translate('my_account.organizations.no_results')}
+ </div>
+ )}
</header>
+ {this.state.loading ? (
+ <i className="spinner"/>
+ ) : (
+ <OrganizationsList organizations={this.props.organizations}/>
+ )}
+
{this.props.children}
</div>
);
}
}
+
+const mapStateToProps = state => ({
+ anyoneCanCreate: getSettingValue(state, 'sonar.organizations.anyoneCanCreate') === 'true',
+ currentUser: getCurrentUser(state),
+ organizations: getMyOrganizations(state)
+});
+
+const mapDispatchToProps = {
+ fetchMyOrganizations,
+ fetchIfAnyoneCanCreateOrganizations
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(UserOrganizations);
--- /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 * as api from '../../../api/organizations';
+import { receiveMyOrganizations } from '../../../store/organizations/duck';
+import { getValues } from '../../../api/settings';
+import { receiveValues } from '../../settings/store/values/actions';
+
+export const fetchMyOrganizations = (): Function => (dispatch: Function): Promise<*> => {
+ return api.getMyOrganizations().then(keys => {
+ if (keys.length > 0) {
+ return api.getOrganizations(keys).then(({ organizations }) => {
+ return dispatch(receiveMyOrganizations(organizations));
+ });
+ } else {
+ return dispatch(receiveMyOrganizations([]));
+ }
+ });
+};
+
+export const fetchIfAnyoneCanCreateOrganizations = (): Function => (dispatch: Function): Promise<*> => {
+ return getValues('sonar.organizations.anyoneCanCreate').then(values => {
+ dispatch(receiveValues(values));
+ });
+};
export const createOrganization = (fields: {}): Function => (dispatch: Function): Promise<*> => {
const onFulfilled = (organization: Organization) => {
- dispatch(actions.receiveOrganizations([organization]));
+ dispatch(actions.createOrganization(organization));
dispatch(addGlobalSuccessMessage(translateWithParameters('organization.created', organization.name)));
};
exports[`Reducer should have initial state 1`] = `
Object {
"byKey": Object {},
+ "my": Array [],
}
`;
"name": "Foo",
},
},
+ "my": Array [],
}
`;
"name": "Qwe",
},
},
+ "my": Array [],
}
`;
// @flow
import { combineReducers } from 'redux';
import omit from 'lodash/omit';
+import uniq from 'lodash/uniq';
+import without from 'lodash/without';
export type Organization = {
avatar?: string,
organizations: Array<Organization>
};
+type ReceiveMyOrganizationsAction = {
+ type: 'RECEIVE_MY_ORGANIZATIONS',
+ organizations: Array<Organization>
+};
+
+type CreateOrganizationAction = {
+ type: 'CREATE_ORGANIZATION',
+ organization: Organization
+};
+
type UpdateOrganizationAction = {
type: 'UPDATE_ORGANIZATION',
key: string,
key: string
};
-type Action = ReceiveOrganizationsAction | UpdateOrganizationAction | DeleteOrganizationAction;
+type Action =
+ ReceiveOrganizationsAction |
+ ReceiveMyOrganizationsAction |
+ CreateOrganizationAction |
+ UpdateOrganizationAction |
+ DeleteOrganizationAction;
type ByKeyState = {
[key: string]: Organization
};
+type MyState = Array<string>;
+
type State = {
- byKey: ByKeyState
+ byKey: ByKeyState,
+ my: MyState
};
export const receiveOrganizations = (organizations: Array<Organization>): ReceiveOrganizationsAction => ({
organizations
});
+export const receiveMyOrganizations = (organizations: Array<Organization>): ReceiveMyOrganizationsAction => ({
+ type: 'RECEIVE_MY_ORGANIZATIONS',
+ organizations
+});
+
+export const createOrganization = (organization: Organization): CreateOrganizationAction => ({
+ type: 'CREATE_ORGANIZATION',
+ organization
+});
+
export const updateOrganization = (key: string, changes: {}): UpdateOrganizationAction => ({
type: 'UPDATE_ORGANIZATION',
key,
key
});
-const onReceiveOrganizations = (state: ByKeyState, action: ReceiveOrganizationsAction): ByKeyState => {
+const onReceiveOrganizations = (
+ state: ByKeyState,
+ action: ReceiveOrganizationsAction | ReceiveMyOrganizationsAction
+): ByKeyState => {
const nextState = { ...state };
action.organizations.forEach(organization => {
nextState[organization.key] = { ...state[organization.key], ...organization };
const byKey = (state: ByKeyState = {}, action: Action) => {
switch (action.type) {
case 'RECEIVE_ORGANIZATIONS':
+ case 'RECEIVE_MY_ORGANIZATIONS':
return onReceiveOrganizations(state, action);
+ case 'CREATE_ORGANIZATION':
+ return { ...state, [action.organization.key]: action.organization };
case 'UPDATE_ORGANIZATION':
return {
...state,
}
};
-export default combineReducers({ byKey });
+const my = (state: MyState = [], action: Action) => {
+ switch (action.type) {
+ case 'RECEIVE_MY_ORGANIZATIONS':
+ return uniq([...state, ...action.organizations.map(o => o.key)]);
+ case 'CREATE_ORGANIZATION':
+ return uniq([...state, action.organization.key]);
+ case 'DELETE_ORGANIZATION':
+ return without(state, action.key);
+ default:
+ return state;
+ }
+};
+
+export default combineReducers({ byKey, my });
export const getOrganizationByKey = (state: State, key: string): Organization => (
state.byKey[key]
);
+export const getMyOrganizations = (state: State): Array<Organization> => (
+ state.my.map(key => getOrganizationByKey(state, key))
+);
+
export const areThereCustomOrganizations = (state: State): boolean => (
Object.keys(state.byKey).length > 1
);
fromOrganizations.getOrganizationByKey(state.organizations, key)
);
+export const getMyOrganizations = state => (
+ fromOrganizations.getMyOrganizations(state.organizations)
+);
+
export const areThereCustomOrganizations = state => (
fromOrganizations.areThereCustomOrganizations(state.organizations)
);
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.organizations.no_results=You are not administering any organizations yet.
my_account.create_organization=Create Organization