diff options
6 files changed, 232 insertions, 11 deletions
diff --git a/server/sonar-web/src/main/js/api/organizations.js b/server/sonar-web/src/main/js/api/organizations.js index 95de53bb6d9..0723fb0c258 100644 --- a/server/sonar-web/src/main/js/api/organizations.js +++ b/server/sonar-web/src/main/js/api/organizations.js @@ -54,3 +54,13 @@ export const updateOrganization = (key: string, changes: {}) => post('/api/organizations/update', { key, ...changes }); export const deleteOrganization = (key: string) => post('/api/organizations/delete', { key }); + +export const searchMembers = ( + data: { organizations?: string, p?: number, ps?: number, q?: string, selected?: string } +) => getJSON('/api/organizations/search_members', data); + +export const addMember = (data: { login: string, organization: string }) => + post('/api/organizations/add_member', data); + +export const removeMember = (data: { login: string, organization: string }) => + post('/api/organizations/remove_member', data); 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 61f0bd8ac0b..916a9d7e17a 100644 --- a/server/sonar-web/src/main/js/apps/organizations/actions.js +++ b/server/sonar-web/src/main/js/apps/organizations/actions.js @@ -19,18 +19,28 @@ */ // @flow import * as api from '../../api/organizations'; -import { onFail } from '../../store/rootActions'; import * as actions from '../../store/organizations/duck'; +import * as membersActions from '../../store/organizationsMembers/actions'; +import { onFail } from '../../store/rootActions'; +import { getOrganizationMembersState } from '../../store/rootReducer'; import { addGlobalSuccessMessage } from '../../store/globalMessages/duck'; import { translate, translateWithParameters } from '../../helpers/l10n'; import type { Organization } from '../../store/organizations/duck'; +const PAGE_SIZE = 50; + const onRejected = (dispatch: Function) => (error: Object) => { onFail(dispatch)(error); return Promise.reject(); }; +const onMembersFail = (organization: string, dispatch: Function) => + (error: Object) => { + onFail(dispatch)(error); + dispatch(membersActions.updateState(organization, { loading: false })); + }; + export const fetchOrganization = (key: string): Function => (dispatch: Function): Promise<*> => { const onFulfilled = ([organization, navigation]) => { @@ -77,3 +87,47 @@ export const deleteOrganization = (key: string): Function => return api.deleteOrganization(key).then(onFulfilled, onFail(dispatch)); }; + +const fetchMembers = ( + dispatch: Function, + receiveAction: Function, + key: string, + query?: string, + page?: number +) => { + dispatch(membersActions.updateState(key, { loading: true })); + const data: Object = { + organizations: key, + ps: PAGE_SIZE + }; + if (page != null) { + data.p = page; + } + if (query) { + data.q = query; + } + return api.searchMembers(data).then( + response => { + dispatch(receiveAction(key, response.users, { + loading: false, + total: response.paging.total, + pageIndex: response.paging.pageIndex, + query: query || null + })); + }, + onMembersFail(key, dispatch) + ); +}; + +export const fetchOrganizationMembers = (key: string, query?: string) => + (dispatch: Function) => fetchMembers(dispatch, membersActions.receiveMembers, key, query); + +export const fetchMoreOrganizationMembers = (key: string, query?: string) => + (dispatch: Function, getState: Function) => + fetchMembers( + dispatch, + membersActions.receiveMoreMembers, + key, + query, + getOrganizationMembersState(getState(), key).pageIndex + 1 + ); diff --git a/server/sonar-web/src/main/js/store/organizationsMembers/actions.js b/server/sonar-web/src/main/js/store/organizationsMembers/actions.js new file mode 100644 index 00000000000..44de107c3f8 --- /dev/null +++ b/server/sonar-web/src/main/js/store/organizationsMembers/actions.js @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info 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 +export type Member = { + login: string, + name: string, + avatar: string, + groupCount: number +}; + +type MembersState = { + paging?: number, + total?: number, + loading?: boolean, + query?: string | null +}; + +export const actions = { + UPDATE_STATE: 'organizations/UPDATE_STATE', + RECEIVE_MEMBERS: 'organizations/RECEIVE_MEMBERS', + RECEIVE_MORE_MEMBERS: 'organizations/RECEIVE_MORE_MEMBERS' +}; + +export const receiveMembers = (organizationKey: string, members: Array<Member>, stateChanges: MembersState) => ({ + type: actions.RECEIVE_MEMBERS, + organization: organizationKey, + members, + stateChanges +}); + +export const receiveMoreMembers = (organizationKey: string, members: Array<Member>, stateChanges: MembersState) => ({ + type: actions.RECEIVE_MORE_MEMBERS, + organization: organizationKey, + members, + stateChanges +}); + +export const updateState = ( + organizationKey: string, + stateChanges: MembersState +) => ({ + type: actions.UPDATE_STATE, + organization: organizationKey, + stateChanges +}); diff --git a/server/sonar-web/src/main/js/store/organizationsMembers/reducer.js b/server/sonar-web/src/main/js/store/organizationsMembers/reducer.js new file mode 100644 index 00000000000..1e7e5094945 --- /dev/null +++ b/server/sonar-web/src/main/js/store/organizationsMembers/reducer.js @@ -0,0 +1,65 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info 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. + */ +import { uniq } from 'lodash'; +import { actions } from './actions'; + +export const getOrganizationMembersLogins = (state, organization) => { + if (organization && state[organization]) { + return state[organization].members || []; + } + return []; +}; + +export const getOrganizationMembersState = (state, organization) => + organization && state[organization] ? state[organization] : {}; + +const organizationMembers = (state = {}, action = {}) => { + switch (action.type) { + case actions.UPDATE_STATE: + return { ...state, ...action.stateChanges }; + case actions.RECEIVE_MEMBERS: + return { ...state, ...action.stateChanges, members: action.members.map(member => member.login) }; + case actions.RECEIVE_MORE_MEMBERS: + return { + ...state, + ...action.stateChanges, + members: uniq((state.members || []).concat(action.members.map(member => member.login))) + }; + default: + return state; + } +}; + +const organizationsMembers = (state = {}, action = {}) => { + const organization = state[action.organization] || {}; + switch (action.type) { + case actions.UPDATE_STATE: + case actions.RECEIVE_MEMBERS: + case actions.RECEIVE_MORE_MEMBERS: + return { + ...state, + [action.organization]: organizationMembers(organization, action) + }; + default: + return state; + } +}; + +export default organizationsMembers; diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js index 91c0a2a9ba6..cac6ee072ea 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.js +++ b/server/sonar-web/src/main/js/store/rootReducer.js @@ -27,6 +27,8 @@ import languages, * as fromLanguages from './languages/reducer'; import measures, * as fromMeasures from './measures/reducer'; import notifications, * as fromNotifications from './notifications/duck'; import organizations, * as fromOrganizations from './organizations/duck'; +import organizationsMembers, * as fromOrganizationsMembers + from './organizationsMembers/reducer'; import globalMessages, * as fromGlobalMessages from './globalMessages/duck'; import projectActivity from './projectActivity/duck'; import measuresApp, * as fromMeasuresApp from '../apps/component-measures/store/rootReducer'; @@ -46,6 +48,7 @@ export default combineReducers({ measures, notifications, organizations, + organizationsMembers, projectActivity, users, @@ -72,6 +75,14 @@ export const getLanguageByKey = (state, key) => export const getCurrentUser = state => fromUsers.getCurrentUser(state.users); +export const getUserLogins = state => fromUsers.getUserLogins(state.users); + +export const getUserByLogin = (state, login) => fromUsers.getUserByLogin(state.users, login); + +export const getUsersByLogins = (state, logins) => fromUsers.getUsersByLogins(state.users, logins); + +export const getUsers = state => fromUsers.getUsers(state.users); + export const isFavorite = (state, componentKey) => fromFavorites.isFavorite(state.favorites, componentKey); @@ -107,6 +118,12 @@ export const getMyOrganizations = state => export const areThereCustomOrganizations = state => getAppState(state).organizationsEnabled; +export const getOrganizationMembersLogins = (state, organization) => + fromOrganizationsMembers.getOrganizationMembersLogins(state.organizationsMembers, organization); + +export const getOrganizationMembersState = (state, organization) => + fromOrganizationsMembers.getOrganizationMembersState(state.organizationsMembers, organization); + export const getProjectActivity = state => state.projectActivity; export const getProjects = state => fromProjectsApp.getProjects(state.projectsApp); diff --git a/server/sonar-web/src/main/js/store/users/reducer.js b/server/sonar-web/src/main/js/store/users/reducer.js index f0ec0163898..a6206ffca58 100644 --- a/server/sonar-web/src/main/js/store/users/reducer.js +++ b/server/sonar-web/src/main/js/store/users/reducer.js @@ -18,33 +18,46 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { combineReducers } from 'redux'; -import { uniq } from 'lodash'; +import { uniq, keyBy } from 'lodash'; import { RECEIVE_CURRENT_USER } from './actions'; +import { actions as membersActions } from '../organizationsMembers/actions'; const usersByLogin = (state = {}, action = {}) => { - if (action.type === RECEIVE_CURRENT_USER) { - return { ...state, [action.user.login]: action.user }; + switch (action.type) { + case RECEIVE_CURRENT_USER: + return { ...state, [action.user.login]: action.user }; + case membersActions.RECEIVE_MEMBERS: + case membersActions.RECEIVE_MORE_MEMBERS: + return { ...state, ...keyBy(action.members, 'login') }; + default: + return state; } - - return state; }; const userLogins = (state = [], action = {}) => { - if (action.type === RECEIVE_CURRENT_USER) { - return uniq([...state, action.user.login]); + switch (action.type) { + case RECEIVE_CURRENT_USER: + return uniq([...state, action.user.login]); + case membersActions.RECEIVE_MEMBERS: + case membersActions.RECEIVE_MORE_MEMBERS: + return uniq([...state, action.members.map(member => member.login)]); + default: + return state; } - - return state; }; const currentUser = (state = null, action = {}) => { if (action.type === RECEIVE_CURRENT_USER) { return action.user; } - return state; }; export default combineReducers({ usersByLogin, userLogins, currentUser }); export const getCurrentUser = state => state.currentUser; +export const getUserLogins = state => state.userLogins; +export const getUserByLogin = (state, login) => state.usersByLogin[login]; +export const getUsersByLogins = (state, logins) => + logins.map(login => getUserByLogin(state, login)); +export const getUsers = state => getUsersByLogins(state, getUserLogins(state)); |