From 1bc6f768f4f77dd6316292d2730a5b1c0137c28b Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Mon, 6 Feb 2017 18:44:08 +0100 Subject: [PATCH] SONAR-8665 Create the "Organizations" page in the "My Account" space --- .../src/main/js/api/organizations.js | 4 + .../account/organizations/OrganizationCard.js | 66 ++++++++++++++++ .../organizations/OrganizationsList.js | 41 ++++++++++ .../organizations/UserOrganizations.js | 79 +++++++++++++++++-- .../js/apps/account/organizations/actions.js | 42 ++++++++++ .../src/main/js/apps/organizations/actions.js | 2 +- .../__tests__/__snapshots__/duck-test.js.snap | 3 + .../src/main/js/store/organizations/duck.js | 61 +++++++++++++- .../src/main/js/store/rootReducer.js | 4 + .../resources/org/sonar/l10n/core.properties | 1 + 10 files changed, 291 insertions(+), 12 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.js create mode 100644 server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.js create mode 100644 server/sonar-web/src/main/js/apps/account/organizations/actions.js diff --git a/server/sonar-web/src/main/js/api/organizations.js b/server/sonar-web/src/main/js/api/organizations.js index ef998da81c3..bc8873b80bf 100644 --- a/server/sonar-web/src/main/js/api/organizations.js +++ b/server/sonar-web/src/main/js/api/organizations.js @@ -29,6 +29,10 @@ export const getOrganizations = (organizations?: Array) => { 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 = { diff --git a/server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.js b/server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.js new file mode 100644 index 00000000000..d1cbea1fb43 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.js @@ -0,0 +1,66 @@ +/* + * 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 ( +
+ + +

+ + {organization.name} + +

+ +
{organization.key}
+ + {!!organization.description && ( +
+ {organization.description} +
+ )} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.js b/server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.js new file mode 100644 index 00000000000..3c0c01906f3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.js @@ -0,0 +1,41 @@ +/* + * 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 + }; + + render () { + return ( +
    + {this.props.organizations.map(organization => ( +
  • + +
  • + ))} +
+ ); + } +} 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 index ca97abba72d..cd8e2b831c0 100644 --- a/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.js +++ b/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.js @@ -20,29 +20,94 @@ // @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, + 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 (

{translate('my_account.organizations')}

-
- {translate('create')} -
-
- {translate('my_account.organizations.description')} -
+ {canCreateOrganizations && ( +
+ {translate('create')} +
+ )} + {this.props.organizations.length > 0 ? ( +
+ {translate('my_account.organizations.description')} +
+ ) : ( +
+ {translate('my_account.organizations.no_results')} +
+ )}
+ {this.state.loading ? ( + + ) : ( + + )} + {this.props.children}
); } } + +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); diff --git a/server/sonar-web/src/main/js/apps/account/organizations/actions.js b/server/sonar-web/src/main/js/apps/account/organizations/actions.js new file mode 100644 index 00000000000..0971b33ca94 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/organizations/actions.js @@ -0,0 +1,42 @@ +/* + * 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)); + }); +}; 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 cd8ee61d9a6..14ccf6a5c56 100644 --- a/server/sonar-web/src/main/js/apps/organizations/actions.js +++ b/server/sonar-web/src/main/js/apps/organizations/actions.js @@ -46,7 +46,7 @@ export const fetchOrganization = (key: string): Function => (dispatch: Function) 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))); }; diff --git a/server/sonar-web/src/main/js/store/organizations/__tests__/__snapshots__/duck-test.js.snap b/server/sonar-web/src/main/js/store/organizations/__tests__/__snapshots__/duck-test.js.snap index 158cccb9dde..7225fb41f88 100644 --- a/server/sonar-web/src/main/js/store/organizations/__tests__/__snapshots__/duck-test.js.snap +++ b/server/sonar-web/src/main/js/store/organizations/__tests__/__snapshots__/duck-test.js.snap @@ -1,6 +1,7 @@ exports[`Reducer should have initial state 1`] = ` Object { "byKey": Object {}, + "my": Array [], } `; @@ -16,6 +17,7 @@ Object { "name": "Foo", }, }, + "my": Array [], } `; @@ -31,5 +33,6 @@ Object { "name": "Qwe", }, }, + "my": Array [], } `; diff --git a/server/sonar-web/src/main/js/store/organizations/duck.js b/server/sonar-web/src/main/js/store/organizations/duck.js index 2d42f78795d..7df169b1dd3 100644 --- a/server/sonar-web/src/main/js/store/organizations/duck.js +++ b/server/sonar-web/src/main/js/store/organizations/duck.js @@ -20,6 +20,8 @@ // @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, @@ -36,6 +38,16 @@ type ReceiveOrganizationsAction = { organizations: Array }; +type ReceiveMyOrganizationsAction = { + type: 'RECEIVE_MY_ORGANIZATIONS', + organizations: Array +}; + +type CreateOrganizationAction = { + type: 'CREATE_ORGANIZATION', + organization: Organization +}; + type UpdateOrganizationAction = { type: 'UPDATE_ORGANIZATION', key: string, @@ -47,14 +59,22 @@ type DeleteOrganizationAction = { key: string }; -type Action = ReceiveOrganizationsAction | UpdateOrganizationAction | DeleteOrganizationAction; +type Action = + ReceiveOrganizationsAction | + ReceiveMyOrganizationsAction | + CreateOrganizationAction | + UpdateOrganizationAction | + DeleteOrganizationAction; type ByKeyState = { [key: string]: Organization }; +type MyState = Array; + type State = { - byKey: ByKeyState + byKey: ByKeyState, + my: MyState }; export const receiveOrganizations = (organizations: Array): ReceiveOrganizationsAction => ({ @@ -62,6 +82,16 @@ export const receiveOrganizations = (organizations: Array): Receiv organizations }); +export const receiveMyOrganizations = (organizations: Array): 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, @@ -73,7 +103,10 @@ export const deleteOrganization = (key: string): DeleteOrganizationAction => ({ 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 }; @@ -84,7 +117,10 @@ const onReceiveOrganizations = (state: ByKeyState, action: ReceiveOrganizationsA 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, @@ -101,12 +137,29 @@ const byKey = (state: ByKeyState = {}, action: Action) => { } }; -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 => ( + state.my.map(key => getOrganizationByKey(state, key)) +); + export const areThereCustomOrganizations = (state: State): boolean => ( Object.keys(state.byKey).length > 1 ); diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js index e3a26024c09..9403962fdeb 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.js +++ b/server/sonar-web/src/main/js/store/rootReducer.js @@ -116,6 +116,10 @@ export const getOrganizationByKey = (state, key) => ( fromOrganizations.getOrganizationByKey(state.organizations, key) ); +export const getMyOrganizations = state => ( + fromOrganizations.getMyOrganizations(state.organizations) +); + export const areThereCustomOrganizations = state => ( fromOrganizations.areThereCustomOrganizations(state.organizations) ); diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 5ea428ad4ac..a963f404ac8 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1786,6 +1786,7 @@ 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.organizations.no_results=You are not administering any organizations yet. my_account.create_organization=Create Organization -- 2.39.5