aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2018-08-22 13:49:25 +0200
committerSonarTech <sonartech@sonarsource.com>2018-08-22 20:21:22 +0200
commitfacddc67e65ce303e40e494d921a53446277cd43 (patch)
tree3a387503f001ae5be20f20c8c2a8f144f574ba3f
parent238191f069e82f40e0e3a89d7914481975bcb200 (diff)
downloadsonarqube-facddc67e65ce303e40e494d921a53446277cd43.tar.gz
sonarqube-facddc67e65ce303e40e494d921a53446277cd43.zip
drop organization members from redux store (#637)
-rw-r--r--server/sonar-web/src/main/js/api/organizations.ts17
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/AddMemberForm.tsx (renamed from server/sonar-web/src/main/js/apps/organizations/components/forms/AddMemberForm.tsx)27
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/ManageMemberGroupsForm.tsx (renamed from server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.tsx)12
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/MembersList.tsx (renamed from server/sonar-web/src/main/js/apps/organizations/components/MembersList.tsx)13
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx (renamed from server/sonar-web/src/main/js/apps/organizations/components/MembersListHeader.tsx)8
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/MembersListItem.tsx (renamed from server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.tsx)14
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx (renamed from server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.tsx)4
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx232
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembersContainer.tsx (renamed from server/sonar-web/src/main/js/store/organizations/utils.ts)30
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/RemoveMemberForm.tsx (renamed from server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.tsx)8
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/AddMemberForm-test.tsx (renamed from server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/AddMemberForm-test.tsx)54
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/ManageMemberGroupsForm-test.tsx (renamed from server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.tsx)2
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersList-test.tsx (renamed from server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersList-test.tsx)13
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListHeader-test.tsx (renamed from server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersListHeader-test.tsx)0
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListItem-test.tsx (renamed from server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersListItem-test.tsx)39
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx (renamed from server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.tsx)0
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx151
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/RemoveMemberForm-test.tsx (renamed from server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.tsx)2
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/AddMemberForm-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/AddMemberForm-test.tsx.snap)24
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/ManageMemberGroupsForm-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.tsx.snap)0
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersList-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersList-test.tsx.snap)8
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListHeader-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListHeader-test.tsx.snap)0
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListItem-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.tsx.snap)0
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap)0
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap176
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/RemoveMemberForm-test.tsx.snap (renamed from server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.tsx.snap)0
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/actions.ts107
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.tsx119
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembersContainer.tsx89
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationMembers-test.tsx67
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap132
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/routes.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js3
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js8
-rw-r--r--server/sonar-web/src/main/js/store/appState/duck.ts4
-rw-r--r--server/sonar-web/src/main/js/store/globalMessages/duck.js4
-rw-r--r--server/sonar-web/src/main/js/store/metrics/reducer.js1
-rw-r--r--server/sonar-web/src/main/js/store/organizationsMembers/actions.js86
-rw-r--r--server/sonar-web/src/main/js/store/organizationsMembers/reducer.js91
-rw-r--r--server/sonar-web/src/main/js/store/rootActions.js6
-rw-r--r--server/sonar-web/src/main/js/store/rootReducer.js30
-rw-r--r--server/sonar-web/src/main/js/store/users/actions.ts6
-rw-r--r--server/sonar-web/src/main/js/store/users/reducer.ts38
-rw-r--r--server/sonar-web/src/main/js/store/utils/generalReducers.js107
44 files changed, 775 insertions, 959 deletions
diff --git a/server/sonar-web/src/main/js/api/organizations.ts b/server/sonar-web/src/main/js/api/organizations.ts
index ffa4cb24fb2..78d8f63ac1c 100644
--- a/server/sonar-web/src/main/js/api/organizations.ts
+++ b/server/sonar-web/src/main/js/api/organizations.ts
@@ -19,7 +19,7 @@
*/
import { getJSON, post, postJSON } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
-import { Organization, OrganizationBase, Paging } from '../app/types';
+import { Organization, OrganizationBase, Paging, OrganizationMember } from '../app/types';
export function getOrganizations(data: {
organizations?: string;
@@ -69,16 +69,19 @@ export function searchMembers(data: {
ps?: number;
q?: string;
selected?: string;
-}): Promise<{ paging: Paging; users: Array<{ avatar?: string; login: string; name: string }> }> {
- return getJSON('/api/organizations/search_members', data);
+}): Promise<{ paging: Paging; users: OrganizationMember[] }> {
+ return getJSON('/api/organizations/search_members', data).catch(throwGlobalError);
}
-export function addMember(data: { login: string; organization: string }): Promise<any> {
- return postJSON('/api/organizations/add_member', data).then(r => r.user);
+export function addMember(data: {
+ login: string;
+ organization: string;
+}): Promise<OrganizationMember> {
+ return postJSON('/api/organizations/add_member', data).then(r => r.user, throwGlobalError);
}
-export function removeMember(data: { login: string; organization: string }): Promise<void> {
- return post('/api/organizations/remove_member', data);
+export function removeMember(data: { login: string; organization: string }) {
+ return post('/api/organizations/remove_member', data).catch(throwGlobalError);
}
export interface OrganizationBilling {
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/AddMemberForm.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/AddMemberForm.tsx
index 00408077132..2617234955e 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/AddMemberForm.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/AddMemberForm.tsx
@@ -18,12 +18,12 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import UsersSelectSearch from '../../../users/components/UsersSelectSearch';
-import { searchMembers } from '../../../../api/organizations';
-import Modal from '../../../../components/controls/Modal';
-import { translate } from '../../../../helpers/l10n';
-import { SubmitButton, ResetButtonLink, Button } from '../../../../components/ui/buttons';
-import { Organization, OrganizationMember } from '../../../../app/types';
+import UsersSelectSearch from '../users/components/UsersSelectSearch';
+import { searchMembers } from '../../api/organizations';
+import Modal from '../../components/controls/Modal';
+import { translate } from '../../helpers/l10n';
+import { SubmitButton, ResetButtonLink, Button } from '../../components/ui/buttons';
+import { Organization, OrganizationMember } from '../../app/types';
interface Props {
addMember: (member: OrganizationMember) => void;
@@ -103,14 +103,13 @@ export default class AddMemberForm extends React.PureComponent<Props, State> {
}
render() {
- const buttonComponent = (
- <Button key="add-member-button" onClick={this.openForm}>
- {translate('organization.members.add')}
- </Button>
+ return (
+ <>
+ <Button key="add-member-button" onClick={this.openForm}>
+ {translate('organization.members.add')}
+ </Button>
+ {this.state.open && this.renderModal()}
+ </>
);
- if (this.state.open) {
- return [buttonComponent, this.renderModal()];
- }
- return buttonComponent;
}
}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/ManageMemberGroupsForm.tsx
index c9c963b3c1b..6d92e7fce8b 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/ManageMemberGroupsForm.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/ManageMemberGroupsForm.tsx
@@ -19,12 +19,12 @@
*/
import * as React from 'react';
import { keyBy, pickBy } from 'lodash';
-import { getUserGroups, UserGroup } from '../../../../api/users';
-import Modal from '../../../../components/controls/Modal';
-import { translate, translateWithParameters } from '../../../../helpers/l10n';
-import OrganizationGroupCheckbox from '../OrganizationGroupCheckbox';
-import { SubmitButton, ResetButtonLink } from '../../../../components/ui/buttons';
-import { Organization, OrganizationMember, Group } from '../../../../app/types';
+import { getUserGroups, UserGroup } from '../../api/users';
+import Modal from '../../components/controls/Modal';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import OrganizationGroupCheckbox from '../organizations/components/OrganizationGroupCheckbox';
+import { SubmitButton, ResetButtonLink } from '../../components/ui/buttons';
+import { Organization, OrganizationMember, Group } from '../../app/types';
interface Props {
onClose: () => void;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersList.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/MembersList.tsx
index 641486606b5..676478bbbff 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/MembersList.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/MembersList.tsx
@@ -18,8 +18,10 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { sortBy } from 'lodash';
import MembersListItem from './MembersListItem';
-import { Group, Organization, OrganizationMember } from '../../../app/types';
+import { Group, Organization, OrganizationMember } from '../../app/types';
+import { translate } from '../../helpers/l10n';
interface Props {
members: OrganizationMember[];
@@ -35,11 +37,18 @@ interface Props {
export default class MembersList extends React.PureComponent<Props> {
render() {
+ const { members } = this.props;
+
+ if (!members.length) {
+ return <div className="note">{translate('no_results')}</div>;
+ }
+
+ const sortedMembers = sortBy(members, member => member.name, member => member.login);
return (
<div className="boxed-group boxed-group-inner">
<table className="data zebra">
<tbody>
- {this.props.members.map(member => (
+ {sortedMembers.map(member => (
<MembersListItem
key={member.login}
member={member}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersListHeader.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx
index b703131b34a..e0af17ca664 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/MembersListHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx
@@ -18,9 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import SearchBox from '../../../components/controls/SearchBox';
-import { formatMeasure } from '../../../helpers/measures';
-import { translate } from '../../../helpers/l10n';
+import SearchBox from '../../components/controls/SearchBox';
+import { formatMeasure } from '../../helpers/measures';
+import { translate } from '../../helpers/l10n';
interface Props {
handleSearch: (query?: string) => void;
@@ -35,7 +35,7 @@ export default function MembersListHeader({ handleSearch, total }: Props) {
onChange={handleSearch}
placeholder={translate('search.search_for_users')}
/>
- {total != null && (
+ {total !== undefined && (
<span className="pull-right little-spacer-top">
<strong>{formatMeasure(total, 'INT')}</strong> {translate('organization.members.members')}
</span>
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/MembersListItem.tsx
index 99d71d9a4fa..43b2535d68f 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/MembersListItem.tsx
@@ -18,16 +18,16 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import RemoveMemberForm from './forms/RemoveMemberForm';
-import ManageMemberGroupsForm from './forms/ManageMemberGroupsForm';
-import Avatar from '../../../components/ui/Avatar';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
+import RemoveMemberForm from './RemoveMemberForm';
+import ManageMemberGroupsForm from './ManageMemberGroupsForm';
+import Avatar from '../../components/ui/Avatar';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { formatMeasure } from '../../helpers/measures';
import ActionsDropdown, {
ActionsDropdownDivider,
ActionsDropdownItem
-} from '../../../components/controls/ActionsDropdown';
-import { Group, Organization, OrganizationMember } from '../../../app/types';
+} from '../../components/controls/ActionsDropdown';
+import { Group, Organization, OrganizationMember } from '../../app/types';
interface Props {
member: OrganizationMember;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx
index 9481d010480..171dfc48aa8 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/MembersPageHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx
@@ -20,8 +20,8 @@
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
-import { translate } from '../../../helpers/l10n';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { translate } from '../../helpers/l10n';
+import DeferredSpinner from '../../components/common/DeferredSpinner';
interface Props {
children?: React.ReactNode;
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx
new file mode 100644
index 00000000000..5acb076d053
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx
@@ -0,0 +1,232 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import Helmet from 'react-helmet';
+import MembersPageHeader from './MembersPageHeader';
+import MembersListHeader from './MembersListHeader';
+import MembersList from './MembersList';
+import AddMemberForm from './AddMemberForm';
+import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
+import ListFooter from '../../components/controls/ListFooter';
+import DocTooltip from '../../components/docs/DocTooltip';
+import { translate } from '../../helpers/l10n';
+import { Group, Organization, OrganizationMember, Paging } from '../../app/types';
+import { searchMembers, addMember, removeMember } from '../../api/organizations';
+import { searchUsersGroups, addUserToGroup, removeUserFromGroup } from '../../api/user_groups';
+
+interface Props {
+ organization: Organization;
+}
+
+interface State {
+ groups: Group[];
+ loading: boolean;
+ members?: OrganizationMember[];
+ paging?: Paging;
+ query: string;
+}
+
+const PAGE_SIZE = 50;
+
+export default class OrganizationMembers extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = {
+ groups: [],
+ loading: true,
+ query: ''
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchMembers();
+ if (this.props.organization.canAdmin) {
+ this.fetchGroups();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ stopLoading = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
+
+ fetchMembers = (query?: string) => {
+ this.setState({ loading: true });
+ searchMembers({
+ organization: this.props.organization.key,
+ ps: PAGE_SIZE,
+ q: query
+ }).then(({ paging, users }) => {
+ if (this.mounted) {
+ this.setState({ loading: false, members: users, paging });
+ }
+ }, this.stopLoading);
+ };
+
+ fetchGroups = () => {
+ searchUsersGroups({ organization: this.props.organization.key }).then(
+ ({ groups }) => {
+ if (this.mounted) {
+ this.setState({ groups });
+ }
+ },
+ () => {}
+ );
+ };
+
+ handleSearchMembers = (query: string) => {
+ this.setState({ query });
+ this.fetchMembers(query || undefined); // empty string -> undefined
+ };
+
+ handleLoadMoreMembers = () => {
+ const { paging, query } = this.state;
+ if (!paging) {
+ return;
+ }
+
+ this.setState({ loading: true });
+ searchMembers({
+ organization: this.props.organization.key,
+ p: paging.pageIndex + 1,
+ ps: PAGE_SIZE,
+ q: query || undefined // empty string -> undefined
+ }).then(({ paging, users }) => {
+ if (this.mounted) {
+ this.setState(({ members = [] }) => ({
+ loading: false,
+ members: [...members, ...users],
+ paging
+ }));
+ }
+ }, this.stopLoading);
+ };
+
+ handleAddMember = ({ login }: OrganizationMember) => {
+ // TODO optimistic update
+ addMember({ login, organization: this.props.organization.key }).then(
+ member => {
+ if (this.mounted) {
+ this.setState(({ members, paging }) => ({
+ members: members && [...members, member],
+ paging: paging && { ...paging, total: paging.total + 1 }
+ }));
+ }
+ },
+ () => {}
+ );
+ };
+
+ handleRemoveMember = ({ login }: OrganizationMember) => {
+ // TODO optimistic update
+ removeMember({ login, organization: this.props.organization.key }).then(
+ () => {
+ if (this.mounted) {
+ this.setState(({ members, paging }) => ({
+ members: members && members.filter(member => member.login !== login),
+ paging: paging && { ...paging, total: paging.total - 1 }
+ }));
+ }
+ },
+ () => {}
+ );
+ };
+
+ updateGroup = (login: string, updater: (member: OrganizationMember) => OrganizationMember) => {
+ this.setState(({ members }) => ({
+ members: members && members.map(member => (member.login === login ? updater(member) : member))
+ }));
+ };
+
+ updateMemberGroups = ({ login }: OrganizationMember, add: string[], remove: string[]) => {
+ // TODO optimistic update
+ const promises = [
+ ...add.map(name =>
+ addUserToGroup({ name, login, organization: this.props.organization.key })
+ ),
+ ...remove.map(name =>
+ removeUserFromGroup({ name, login, organization: this.props.organization.key })
+ )
+ ];
+ Promise.all(promises).then(
+ () => {
+ if (this.mounted) {
+ this.updateGroup(login, member => ({
+ ...member,
+ groupCount: (member.groupCount || 0) + add.length - remove.length
+ }));
+ }
+ },
+ () => {}
+ );
+ };
+
+ render() {
+ const { organization } = this.props;
+ const { groups, loading, members, paging } = this.state;
+ const memberLogins = members ? members.map(member => member.login) : [];
+ return (
+ <div className="page page-limited">
+ <Helmet title={translate('organization.members.page')} />
+ <Suggestions suggestions="organization_members" />
+ <MembersPageHeader loading={loading}>
+ {organization.canAdmin && (
+ <div className="page-actions">
+ <AddMemberForm
+ addMember={this.handleAddMember}
+ memberLogins={memberLogins}
+ organization={organization}
+ />
+ <DocTooltip
+ className="spacer-left"
+ doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/organizations/add-organization-member.md')}
+ />
+ </div>
+ )}
+ </MembersPageHeader>
+ {members !== undefined &&
+ paging !== undefined && (
+ <>
+ <MembersListHeader handleSearch={this.handleSearchMembers} total={paging.total} />
+ <MembersList
+ members={members}
+ organization={organization}
+ organizationGroups={groups}
+ removeMember={this.handleRemoveMember}
+ updateMemberGroups={this.updateMemberGroups}
+ />
+ {paging.total !== 0 && (
+ <ListFooter
+ count={members.length}
+ loadMore={this.handleLoadMoreMembers}
+ ready={!loading}
+ total={paging.total}
+ />
+ )}
+ </>
+ )}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/store/organizations/utils.ts b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembersContainer.tsx
index 5b3957e1f6c..6f13da8a3c2 100644
--- a/server/sonar-web/src/main/js/store/organizations/utils.ts
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembersContainer.tsx
@@ -17,20 +17,24 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import getStore from '../../app/utils/getStore';
-import {
- getOrganizationByKey,
- areThereCustomOrganizations as customOrganizations
-} from '../rootReducer';
+import { connect } from 'react-redux';
+import OrganizationMembers from './OrganizationMembers';
+import { Organization } from '../../app/types';
+import { getOrganizationByKey } from '../../store/rootReducer';
-export function getOrganization(key: string) {
- const store = getStore();
- const state = store.getState();
- return getOrganizationByKey(state, key);
+interface OwnProps {
+ params: { organizationKey: string };
}
-export function areThereCustomOrganizations() {
- const store = getStore();
- const state = store.getState();
- return customOrganizations(state);
+interface StateProps {
+ organization: Organization;
}
+
+const mapStateToProps = (state: any, ownProps: OwnProps): StateProps => {
+ const { organizationKey } = ownProps.params;
+ return {
+ organization: getOrganizationByKey(state, organizationKey)!
+ };
+};
+
+export default connect<StateProps, {}, OwnProps>(mapStateToProps)(OrganizationMembers);
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/RemoveMemberForm.tsx
index c6e0d603dd2..636ae892a31 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/RemoveMemberForm.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/RemoveMemberForm.tsx
@@ -18,10 +18,10 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import Modal from '../../../../components/controls/Modal';
-import { translate, translateWithParameters } from '../../../../helpers/l10n';
-import { SubmitButton, ResetButtonLink } from '../../../../components/ui/buttons';
-import { Organization, OrganizationMember } from '../../../../app/types';
+import Modal from '../../components/controls/Modal';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { SubmitButton, ResetButtonLink } from '../../components/ui/buttons';
+import { Organization, OrganizationMember } from '../../app/types';
interface Props {
onClose: () => void;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/AddMemberForm-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/AddMemberForm-test.tsx
index 3406cd7361d..2908edeb1c9 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/AddMemberForm-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/AddMemberForm-test.tsx
@@ -19,8 +19,13 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import { click } from '../../../../../helpers/testUtils';
+import { click, submit } from '../../../helpers/testUtils';
import AddMemberForm from '../AddMemberForm';
+import { searchMembers } from '../../../api/organizations';
+
+jest.mock('../../../api/organizations', () => ({
+ searchMembers: jest.fn().mockResolvedValue({ paging: {}, users: [] })
+}));
const memberLogins = ['admin'];
@@ -34,9 +39,7 @@ it('should render and open the modal', () => {
);
expect(wrapper).toMatchSnapshot();
wrapper.setState({ open: true });
-
- // FIXME Can probably be removed when https://github.com/airbnb/enzyme/issues/1149 is resolved
- expect(wrapper.first().getElements()).toMatchSnapshot();
+ expect(wrapper).toMatchSnapshot();
});
it('should correctly handle user interactions', () => {
@@ -52,3 +55,46 @@ it('should correctly handle user interactions', () => {
(wrapper.instance() as AddMemberForm).closeForm();
expect(wrapper.state('open')).toBeFalsy();
});
+
+it('should search users', () => {
+ const wrapper = shallow(
+ <AddMemberForm
+ addMember={jest.fn()}
+ memberLogins={memberLogins}
+ organization={{ key: 'foo', name: 'Foo' }}
+ />
+ );
+ click(wrapper.find('Button'));
+
+ wrapper.find('UsersSelectSearch').prop<Function>('searchUsers')('foo', 100);
+ expect(searchMembers).lastCalledWith({
+ organization: 'foo',
+ ps: 100,
+ q: 'foo',
+ selected: 'deselected'
+ });
+
+ wrapper.find('UsersSelectSearch').prop<Function>('searchUsers')('', 100);
+ expect(searchMembers).lastCalledWith({
+ organization: 'foo',
+ ps: 100,
+ selected: 'deselected'
+ });
+});
+
+it('should select user', () => {
+ const addMember = jest.fn();
+ const user = { login: 'luke', name: 'Luke' };
+ const wrapper = shallow(
+ <AddMemberForm
+ addMember={addMember}
+ memberLogins={memberLogins}
+ organization={{ key: 'foo', name: 'Foo' }}
+ />
+ );
+ click(wrapper.find('Button'));
+
+ wrapper.find('UsersSelectSearch').prop<Function>('handleValueChange')(user);
+ submit(wrapper.find('form'));
+ expect(addMember).toBeCalledWith(user);
+});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/ManageMemberGroupsForm-test.tsx
index 32b0839c275..8a011e697f7 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/ManageMemberGroupsForm-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/ManageMemberGroupsForm-test.tsx
@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import { mockEvent } from '../../../../../helpers/testUtils';
+import { mockEvent } from '../../../helpers/testUtils';
import ManageMemberGroupsForm from '../ManageMemberGroupsForm';
const member = { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 };
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersList-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersList-test.tsx
index 0910f62995c..514ac814a16 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersList-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersList-test.tsx
@@ -39,3 +39,16 @@ it('should render a list of members of an organization', () => {
);
expect(wrapper).toMatchSnapshot();
});
+
+it('should render "no results"', () => {
+ const wrapper = shallow(
+ <MembersList
+ members={[]}
+ organization={organization}
+ organizationGroups={[]}
+ removeMember={jest.fn()}
+ updateMemberGroups={jest.fn()}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersListHeader-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListHeader-test.tsx
index ffc4241d145..ffc4241d145 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersListHeader-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListHeader-test.tsx
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersListItem-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListItem-test.tsx
index 9838194849f..18c91ecec61 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersListItem-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListItem-test.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import MembersListItem from '../MembersListItem';
+import { click } from '../../../helpers/testUtils';
const organization = { key: 'foo', name: 'Foo' };
const admin = { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 };
@@ -63,3 +64,41 @@ it('should groups at 0 if the groupCount field is not defined (just added user)'
);
expect(wrapper).toMatchSnapshot();
});
+
+it('should open groups form', () => {
+ const wrapper = shallow(
+ <MembersListItem
+ member={admin}
+ organization={{ ...organization, canAdmin: true }}
+ organizationGroups={[]}
+ removeMember={jest.fn()}
+ updateMemberGroups={jest.fn()}
+ />
+ );
+
+ click(wrapper.find('ActionsDropdownItem').first());
+ expect(wrapper.find('ManageMemberGroupsForm').exists()).toBe(true);
+
+ wrapper.find('ManageMemberGroupsForm').prop<Function>('onClose')();
+ wrapper.update();
+ expect(wrapper.find('ManageMemberGroupsForm').exists()).toBe(false);
+});
+
+it('should open remove member form', () => {
+ const wrapper = shallow(
+ <MembersListItem
+ member={admin}
+ organization={{ ...organization, canAdmin: true }}
+ organizationGroups={[]}
+ removeMember={jest.fn()}
+ updateMemberGroups={jest.fn()}
+ />
+ );
+
+ click(wrapper.find('ActionsDropdownItem').last());
+ expect(wrapper.find('RemoveMemberForm').exists()).toBe(true);
+
+ wrapper.find('RemoveMemberForm').prop<Function>('onClose')();
+ wrapper.update();
+ expect(wrapper.find('RemoveMemberForm').exists()).toBe(false);
+});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx
index eb2dbe02f4d..eb2dbe02f4d 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/MembersPageHeader-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx
new file mode 100644
index 00000000000..d60b0e9c3c0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx
@@ -0,0 +1,151 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import OrganizationMembers from '../OrganizationMembers';
+import { waitAndUpdate } from '../../../helpers/testUtils';
+import { searchMembers, addMember, removeMember } from '../../../api/organizations';
+import { searchUsersGroups, addUserToGroup, removeUserFromGroup } from '../../../api/user_groups';
+import { OrganizationMember } from '../../../app/types';
+
+jest.mock('../../../api/organizations', () => ({
+ addMember: jest.fn().mockResolvedValue({ login: 'bar', name: 'Bar', groupCount: 1 }),
+ removeMember: jest.fn().mockResolvedValue(undefined),
+ searchMembers: jest.fn().mockResolvedValue({
+ paging: { pageIndex: 1, pageSize: 2, total: 3 },
+ users: [
+ { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 },
+ { login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af', groupCount: 1 }
+ ]
+ })
+}));
+
+jest.mock('../../../api/user_groups', () => ({
+ addUserToGroup: jest.fn().mockResolvedValue(undefined),
+ removeUserFromGroup: jest.fn().mockResolvedValue(undefined),
+ searchUsersGroups: jest.fn().mockResolvedValue({
+ paging: { pageIndex: 1, pageSize: 100, total: 2 },
+ groups: [
+ { id: 1, name: 'Members', description: '', membersCount: 2, default: true },
+ { id: 2, name: 'Watchers', description: '', membersCount: 0, default: false }
+ ]
+ })
+}));
+
+const organization = { key: 'foo', name: 'Foo' };
+
+beforeEach(() => {
+ (searchMembers as jest.Mock).mockClear();
+ (searchUsersGroups as jest.Mock).mockClear();
+});
+
+it('should fetch members and render for non-admin', async () => {
+ const wrapper = shallow(<OrganizationMembers organization={organization} />);
+ expect(wrapper).toMatchSnapshot();
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+ expect(searchMembers).toBeCalledWith({ organization: 'foo', ps: 50, q: undefined });
+});
+
+it('should fetch members and groups and render for admin', async () => {
+ const wrapper = shallow(
+ <OrganizationMembers organization={{ ...organization, canAdmin: true }} />
+ );
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+ expect(searchMembers).toBeCalledWith({ organization: 'foo', ps: 50, q: undefined });
+ expect(searchUsersGroups).toBeCalledWith({ organization: 'foo' });
+});
+
+it('should search users', async () => {
+ const wrapper = shallow(
+ <OrganizationMembers organization={{ ...organization, canAdmin: true }} />
+ );
+ await waitAndUpdate(wrapper);
+ wrapper.find('MembersListHeader').prop<Function>('handleSearch')('user');
+ expect(searchMembers).lastCalledWith({ organization: 'foo', ps: 50, q: 'user' });
+});
+
+it('should load more members', async () => {
+ const wrapper = shallow(
+ <OrganizationMembers organization={{ ...organization, canAdmin: true }} />
+ );
+ await waitAndUpdate(wrapper);
+ wrapper.find('ListFooter').prop<Function>('loadMore')();
+ expect(searchMembers).lastCalledWith({ organization: 'foo', p: 2, ps: 50, q: undefined });
+});
+
+it('should add new member', async () => {
+ const wrapper = shallow(
+ <OrganizationMembers organization={{ ...organization, canAdmin: true }} />
+ );
+ await waitAndUpdate(wrapper);
+ wrapper.find('AddMemberForm').prop<Function>('addMember')({ login: 'bar' });
+ await waitAndUpdate(wrapper);
+ expect(
+ wrapper
+ .find('MembersList')
+ .prop<OrganizationMember[]>('members')
+ .find(m => m.login === 'bar')
+ ).toBeDefined();
+ expect(wrapper.find('ListFooter').prop('total')).toEqual(4);
+ expect(addMember).toBeCalledWith({ login: 'bar', organization: 'foo' });
+});
+
+it('should remove member', async () => {
+ const wrapper = shallow(
+ <OrganizationMembers organization={{ ...organization, canAdmin: true }} />
+ );
+ await waitAndUpdate(wrapper);
+ wrapper.find('MembersList').prop<Function>('removeMember')({ login: 'john' });
+ await waitAndUpdate(wrapper);
+ expect(
+ wrapper
+ .find('MembersList')
+ .prop<OrganizationMember[]>('members')
+ .find(m => m.login === 'john')
+ ).toBeUndefined();
+ expect(wrapper.find('ListFooter').prop('total')).toEqual(2);
+ expect(removeMember).toBeCalledWith({ login: 'john', organization: 'foo' });
+});
+
+it('should update groups', async () => {
+ const wrapper = shallow(
+ <OrganizationMembers organization={{ ...organization, canAdmin: true }} />
+ );
+ await waitAndUpdate(wrapper);
+ wrapper.find('MembersList').prop<Function>('updateMemberGroups')(
+ { login: 'john' },
+ ['cats', 'dogs'], // add to
+ ['birds'] // remove from
+ );
+ await waitAndUpdate(wrapper);
+ const john = wrapper
+ .find('MembersList')
+ .prop<OrganizationMember[]>('members')
+ .find(m => m.login === 'john');
+ expect(john && john.groupCount).toBe(2);
+ expect(addUserToGroup).toHaveBeenCalledTimes(2);
+ expect(addUserToGroup).toBeCalledWith({ login: 'john', name: 'cats', organization: 'foo' });
+ expect(addUserToGroup).toBeCalledWith({ login: 'john', name: 'dogs', organization: 'foo' });
+ expect(removeUserFromGroup).toHaveBeenCalledTimes(1);
+ expect(removeUserFromGroup).toBeCalledWith({ login: 'john', name: 'birds', organization: 'foo' });
+});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/RemoveMemberForm-test.tsx
index 92fbdb9723d..48c45996da9 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/RemoveMemberForm-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/RemoveMemberForm-test.tsx
@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import { mockEvent } from '../../../../../helpers/testUtils';
+import { mockEvent } from '../../../helpers/testUtils';
import RemoveMemberForm from '../RemoveMemberForm';
const member = { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 };
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/AddMemberForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/AddMemberForm-test.tsx.snap
index d480a393f5b..ca0444d7328 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/AddMemberForm-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/AddMemberForm-test.tsx.snap
@@ -1,23 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render and open the modal 1`] = `
-<Button
- key="add-member-button"
- onClick={[Function]}
->
- organization.members.add
-</Button>
+<React.Fragment>
+ <Button
+ key="add-member-button"
+ onClick={[Function]}
+ >
+ organization.members.add
+ </Button>
+</React.Fragment>
`;
exports[`should render and open the modal 2`] = `
-Array [
+<React.Fragment>
<Button
+ key="add-member-button"
onClick={[Function]}
>
organization.members.add
- </Button>,
+ </Button>
<Modal
contentLabel="users.add"
+ key="add-member-modal"
onRequestClose={[Function]}
>
<header
@@ -68,6 +72,6 @@ Array [
</div>
</footer>
</form>
- </Modal>,
-]
+ </Modal>
+</React.Fragment>
`;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/ManageMemberGroupsForm-test.tsx.snap
index 660a194e06c..660a194e06c 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/ManageMemberGroupsForm-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/ManageMemberGroupsForm-test.tsx.snap
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersList-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersList-test.tsx.snap
index 57ff0c52ea6..9635b1e2685 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersList-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersList-test.tsx.snap
@@ -1,5 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`should render "no results" 1`] = `
+<div
+ className="note"
+>
+ no_results
+</div>
+`;
+
exports[`should render a list of members of an organization 1`] = `
<div
className="boxed-group boxed-group-inner"
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListHeader-test.tsx.snap
index 7bfe2725e60..7bfe2725e60 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListHeader-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListHeader-test.tsx.snap
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListItem-test.tsx.snap
index 2f04b608477..2f04b608477 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListItem-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListItem-test.tsx.snap
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
index 86a16038cda..86a16038cda 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap
new file mode 100644
index 00000000000..336f1e66691
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap
@@ -0,0 +1,176 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should fetch members and groups and render for admin 1`] = `
+<div
+ className="page page-limited"
+>
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="organization.members.page"
+ />
+ <Suggestions
+ suggestions="organization_members"
+ />
+ <MembersPageHeader
+ loading={false}
+ >
+ <div
+ className="page-actions"
+ >
+ <AddMemberForm
+ addMember={[Function]}
+ memberLogins={
+ Array [
+ "admin",
+ "john",
+ ]
+ }
+ organization={
+ Object {
+ "canAdmin": true,
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ />
+ <DocTooltip
+ className="spacer-left"
+ doc={Promise {}}
+ />
+ </div>
+ </MembersPageHeader>
+ <React.Fragment>
+ <MembersListHeader
+ handleSearch={[Function]}
+ total={3}
+ />
+ <MembersList
+ members={
+ Array [
+ Object {
+ "avatar": "",
+ "groupCount": 3,
+ "login": "admin",
+ "name": "Admin Istrator",
+ },
+ Object {
+ "avatar": "7daf6c79d4802916d83f6266e24850af",
+ "groupCount": 1,
+ "login": "john",
+ "name": "John Doe",
+ },
+ ]
+ }
+ organization={
+ Object {
+ "canAdmin": true,
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ organizationGroups={
+ Array [
+ Object {
+ "default": true,
+ "description": "",
+ "id": 1,
+ "membersCount": 2,
+ "name": "Members",
+ },
+ Object {
+ "default": false,
+ "description": "",
+ "id": 2,
+ "membersCount": 0,
+ "name": "Watchers",
+ },
+ ]
+ }
+ removeMember={[Function]}
+ updateMemberGroups={[Function]}
+ />
+ <ListFooter
+ count={2}
+ loadMore={[Function]}
+ ready={true}
+ total={3}
+ />
+ </React.Fragment>
+</div>
+`;
+
+exports[`should fetch members and render for non-admin 1`] = `
+<div
+ className="page page-limited"
+>
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="organization.members.page"
+ />
+ <Suggestions
+ suggestions="organization_members"
+ />
+ <MembersPageHeader
+ loading={true}
+ />
+</div>
+`;
+
+exports[`should fetch members and render for non-admin 2`] = `
+<div
+ className="page page-limited"
+>
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="organization.members.page"
+ />
+ <Suggestions
+ suggestions="organization_members"
+ />
+ <MembersPageHeader
+ loading={false}
+ />
+ <React.Fragment>
+ <MembersListHeader
+ handleSearch={[Function]}
+ total={3}
+ />
+ <MembersList
+ members={
+ Array [
+ Object {
+ "avatar": "",
+ "groupCount": 3,
+ "login": "admin",
+ "name": "Admin Istrator",
+ },
+ Object {
+ "avatar": "7daf6c79d4802916d83f6266e24850af",
+ "groupCount": 1,
+ "login": "john",
+ "name": "John Doe",
+ },
+ ]
+ }
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ organizationGroups={Array []}
+ removeMember={[Function]}
+ updateMemberGroups={[Function]}
+ />
+ <ListFooter
+ count={2}
+ loadMore={[Function]}
+ ready={true}
+ total={3}
+ />
+ </React.Fragment>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/RemoveMemberForm-test.tsx.snap
index c4a8afef3ef..c4a8afef3ef 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/RemoveMemberForm-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/RemoveMemberForm-test.tsx.snap
diff --git a/server/sonar-web/src/main/js/apps/organizations/actions.ts b/server/sonar-web/src/main/js/apps/organizations/actions.ts
index 7f5d8897a97..ee00b6b3147 100644
--- a/server/sonar-web/src/main/js/apps/organizations/actions.ts
+++ b/server/sonar-web/src/main/js/apps/organizations/actions.ts
@@ -20,16 +20,10 @@
import { Dispatch } from 'redux';
import * as api from '../../api/organizations';
import * as actions from '../../store/organizations/duck';
-import * as membersActions from '../../store/organizationsMembers/actions';
-import { searchUsersGroups, addUserToGroup, removeUserFromGroup } from '../../api/user_groups';
-import { receiveUser } from '../../store/users/actions';
import { onFail } from '../../store/rootActions';
-import { getOrganizationMembersState } from '../../store/rootReducer';
import { addGlobalSuccessMessage } from '../../store/globalMessages/duck';
import { translate, translateWithParameters } from '../../helpers/l10n';
-import { Organization, OrganizationMember, OrganizationBase } from '../../app/types';
-
-const PAGE_SIZE = 50;
+import { Organization, OrganizationBase } from '../../app/types';
const onRejected = (dispatch: Dispatch<any>) => (error: any) => {
onFail(dispatch)(error);
@@ -48,12 +42,6 @@ export const fetchOrganization = (key: string) => (dispatch: Dispatch<any>) => {
);
};
-export const fetchOrganizationGroups = (organization: string) => (dispatch: Dispatch<any>) => {
- return searchUsersGroups({ organization }).then(response => {
- dispatch(actions.receiveOrganizationGroups(organization, response.groups));
- }, onFail(dispatch));
-};
-
export const createOrganization = (organization: OrganizationBase) => (dispatch: Dispatch<any>) => {
return api.createOrganization(organization).then((organization: Organization) => {
dispatch(actions.createOrganization(organization));
@@ -79,96 +67,3 @@ export const deleteOrganization = (key: string) => (dispatch: Dispatch<any>) =>
dispatch(addGlobalSuccessMessage(translate('organization.deleted')));
}, onFail(dispatch));
};
-
-const fetchMembers = (
- data: {
- organization: string;
- p?: number;
- ps?: number;
- q?: string;
- },
- dispatch: Dispatch<any>,
- receiveAction: Function
-) => {
- dispatch(membersActions.updateState(data.organization, { loading: true }));
- if (data.ps === undefined) {
- data.ps = PAGE_SIZE;
- }
- if (!data.q) {
- data.q = undefined;
- }
- return api.searchMembers(data).then(
- response => {
- dispatch(
- receiveAction(data.organization, response.users, {
- loading: false,
- total: response.paging.total,
- pageIndex: response.paging.pageIndex,
- query: data.q || null
- })
- );
- },
- (error: any) => {
- onFail(dispatch)(error);
- dispatch(membersActions.updateState(data.organization, { loading: false }));
- }
- );
-};
-
-export const fetchOrganizationMembers = (key: string, query?: string) => (
- dispatch: Dispatch<any>
-) => fetchMembers({ organization: key, q: query }, dispatch, membersActions.receiveMembers);
-
-export const fetchMoreOrganizationMembers = (key: string, query?: string) => (
- dispatch: Dispatch<any>,
- getState: () => any
-) =>
- fetchMembers(
- { organization: key, p: getOrganizationMembersState(getState(), key).pageIndex + 1, q: query },
- dispatch,
- membersActions.receiveMoreMembers
- );
-
-export const addOrganizationMember = (key: string, member: OrganizationMember) => (
- dispatch: Dispatch<any>
-) => {
- return api
- .addMember({ login: member.login, organization: key })
- .then(user => dispatch(membersActions.addMember(key, user)), onFail(dispatch));
-};
-
-export const removeOrganizationMember = (key: string, member: OrganizationMember) => (
- dispatch: Dispatch<any>
-) => {
- dispatch(membersActions.removeMember(key, member));
- return api.removeMember({ login: member.login, organization: key }).catch((error: any) => {
- onFail(dispatch)(error);
- dispatch(membersActions.addMember(key, member));
- });
-};
-
-export const updateOrganizationMemberGroups = (
- organization: Organization,
- member: OrganizationMember,
- add: string[],
- remove: string[]
-) => (dispatch: Dispatch<any>) => {
- dispatch(
- receiveUser({
- ...member,
- groupCount: (member.groupCount || 0) + add.length - remove.length
- })
- );
- const promises = [
- ...add.map(name =>
- addUserToGroup({ name, login: member.login, organization: organization.key })
- ),
- ...remove.map(name =>
- removeUserFromGroup({ name, login: member.login, organization: organization.key })
- )
- ];
- return Promise.all(promises).catch(error => {
- dispatch(receiveUser(member));
- onFail(dispatch)(error);
- });
-};
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.tsx
deleted file mode 100644
index 86e505b33b3..00000000000
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembers.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 * as React from 'react';
-import Helmet from 'react-helmet';
-import MembersPageHeader from './MembersPageHeader';
-import MembersListHeader from './MembersListHeader';
-import MembersList from './MembersList';
-import AddMemberForm from './forms/AddMemberForm';
-import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
-import ListFooter from '../../../components/controls/ListFooter';
-import DocTooltip from '../../../components/docs/DocTooltip';
-import { translate } from '../../../helpers/l10n';
-import { Group, Organization, OrganizationMember } from '../../../app/types';
-
-interface Props {
- addOrganizationMember: (organizationKey: string, member: OrganizationMember) => void;
- fetchMoreOrganizationMembers: (organizationKey: string, query?: string) => void;
- fetchOrganizationGroups: (organizationKey: string) => void;
- fetchOrganizationMembers: (organizationKey: string, query?: string) => void;
- members: OrganizationMember[];
- memberLogins: string[];
- organization: Organization;
- organizationGroups: Group[];
- removeOrganizationMember: (organizationKey: string, member: OrganizationMember) => void;
- status: { loading?: boolean; total?: number; pageIndex?: number; query?: string };
- updateOrganizationMemberGroups: (
- organization: Organization,
- member: OrganizationMember,
- add: string[],
- remove: string[]
- ) => void;
-}
-
-export default class OrganizationMembers extends React.PureComponent<Props> {
- componentDidMount() {
- this.handleSearchMembers();
- if (this.props.organization.canAdmin) {
- this.props.fetchOrganizationGroups(this.props.organization.key);
- }
- }
-
- handleSearchMembers = (query?: string) => {
- this.props.fetchOrganizationMembers(this.props.organization.key, query);
- };
-
- handleLoadMoreMembers = () => {
- this.props.fetchMoreOrganizationMembers(this.props.organization.key, this.props.status.query);
- };
-
- addMember = (member: OrganizationMember) => {
- this.props.addOrganizationMember(this.props.organization.key, member);
- };
-
- removeMember = (member: OrganizationMember) => {
- this.props.removeOrganizationMember(this.props.organization.key, member);
- };
-
- updateMemberGroups = (member: OrganizationMember, add: string[], remove: string[]) => {
- this.props.updateOrganizationMemberGroups(this.props.organization, member, add, remove);
- };
-
- render() {
- const { organization, status, members } = this.props;
- return (
- <div className="page page-limited">
- <Helmet title={translate('organization.members.page')} />
- <Suggestions suggestions="organization_members" />
- <MembersPageHeader loading={Boolean(status.loading)}>
- {organization.canAdmin && (
- <div className="page-actions">
- <AddMemberForm
- addMember={this.addMember}
- memberLogins={this.props.memberLogins}
- organization={organization}
- />
- <DocTooltip
- className="spacer-left"
- doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/organizations/add-organization-member.md')}
- />
- </div>
- )}
- </MembersPageHeader>
- <MembersListHeader handleSearch={this.handleSearchMembers} total={status.total} />
- <MembersList
- members={members}
- organization={organization}
- organizationGroups={this.props.organizationGroups}
- removeMember={this.removeMember}
- updateMemberGroups={this.updateMemberGroups}
- />
- {status.total != null && (
- <ListFooter
- count={members.length}
- loadMore={this.handleLoadMoreMembers}
- ready={!status.loading}
- total={status.total}
- />
- )}
- </div>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembersContainer.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembersContainer.tsx
deleted file mode 100644
index 01226a51009..00000000000
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationMembersContainer.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { connect } from 'react-redux';
-import OrganizationMembers from './OrganizationMembers';
-import {
- getOrganizationByKey,
- getOrganizationGroupsByKey,
- getOrganizationMembersLogins,
- getUsersByLogins,
- getOrganizationMembersState
-} from '../../../store/rootReducer';
-import {
- fetchOrganizationMembers,
- fetchMoreOrganizationMembers,
- fetchOrganizationGroups,
- addOrganizationMember,
- removeOrganizationMember,
- updateOrganizationMemberGroups
-} from '../actions';
-import { Organization, OrganizationMember, Group } from '../../../app/types';
-
-interface OwnProps {
- params: { organizationKey: string };
-}
-
-interface StateProps {
- memberLogins: string[];
- members: OrganizationMember[];
- organization: Organization;
- organizationGroups: Group[];
- status: { loading?: boolean; total?: number; pageIndex?: number; query?: string };
-}
-
-interface DispatchProps {
- addOrganizationMember: (organizationKey: string, member: OrganizationMember) => void;
- fetchMoreOrganizationMembers: (organizationKey: string, query?: string) => void;
- fetchOrganizationGroups: (organizationKey: string) => void;
- fetchOrganizationMembers: (organizationKey: string, query?: string) => void;
- removeOrganizationMember: (organizationKey: string, member: OrganizationMember) => void;
- updateOrganizationMemberGroups: (
- organization: Organization,
- member: OrganizationMember,
- add: string[],
- remove: string[]
- ) => void;
-}
-
-const mapStateToProps = (state: any, ownProps: OwnProps): StateProps => {
- const { organizationKey } = ownProps.params;
- const memberLogins = getOrganizationMembersLogins(state, organizationKey);
- return {
- memberLogins,
- members: getUsersByLogins(state, memberLogins),
- organization: getOrganizationByKey(state, organizationKey)!,
- organizationGroups: getOrganizationGroupsByKey(state, organizationKey),
- status: getOrganizationMembersState(state, organizationKey)
- };
-};
-
-const mapDispatchToProps = {
- addOrganizationMember,
- fetchMoreOrganizationMembers,
- fetchOrganizationGroups,
- fetchOrganizationMembers,
- removeOrganizationMember,
- updateOrganizationMemberGroups
-};
-
-export default connect<StateProps, DispatchProps, OwnProps>(
- mapStateToProps,
- mapDispatchToProps
-)(OrganizationMembers);
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationMembers-test.tsx b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationMembers-test.tsx
deleted file mode 100644
index de18ce76e3b..00000000000
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationMembers-test.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 * as React from 'react';
-import { shallow } from 'enzyme';
-import OrganizationMembers from '../OrganizationMembers';
-
-const organization = { key: 'foo', name: 'Foo' };
-const members = [
- { login: 'admin', name: 'Admin Istrator', avatar: '', groupCount: 3 },
- { login: 'john', name: 'John Doe', avatar: '7daf6c79d4802916d83f6266e24850af', groupCount: 1 }
-];
-const status = { total: members.length };
-
-it('should not render actions for non admin', () => {
- const wrapper = shallow(
- <OrganizationMembers
- addOrganizationMember={jest.fn()}
- fetchMoreOrganizationMembers={jest.fn()}
- fetchOrganizationGroups={jest.fn()}
- fetchOrganizationMembers={jest.fn()}
- memberLogins={[]}
- members={members}
- organization={organization}
- organizationGroups={[]}
- removeOrganizationMember={jest.fn()}
- status={status}
- updateOrganizationMemberGroups={jest.fn()}
- />
- );
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should render actions for admin', () => {
- const wrapper = shallow(
- <OrganizationMembers
- addOrganizationMember={jest.fn()}
- fetchMoreOrganizationMembers={jest.fn()}
- fetchOrganizationGroups={jest.fn()}
- fetchOrganizationMembers={jest.fn()}
- memberLogins={[]}
- members={members}
- organization={{ ...organization, canAdmin: true }}
- organizationGroups={[]}
- removeOrganizationMember={jest.fn()}
- status={{ ...status, loading: true }}
- updateOrganizationMemberGroups={jest.fn()}
- />
- );
- expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap
deleted file mode 100644
index 094f4735c81..00000000000
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap
+++ /dev/null
@@ -1,132 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should not render actions for non admin 1`] = `
-<div
- className="page page-limited"
->
- <HelmetWrapper
- defer={true}
- encodeSpecialCharacters={true}
- title="organization.members.page"
- />
- <Suggestions
- suggestions="organization_members"
- />
- <MembersPageHeader
- loading={false}
- />
- <MembersListHeader
- handleSearch={[Function]}
- total={2}
- />
- <MembersList
- members={
- Array [
- Object {
- "avatar": "",
- "groupCount": 3,
- "login": "admin",
- "name": "Admin Istrator",
- },
- Object {
- "avatar": "7daf6c79d4802916d83f6266e24850af",
- "groupCount": 1,
- "login": "john",
- "name": "John Doe",
- },
- ]
- }
- organization={
- Object {
- "key": "foo",
- "name": "Foo",
- }
- }
- organizationGroups={Array []}
- removeMember={[Function]}
- updateMemberGroups={[Function]}
- />
- <ListFooter
- count={2}
- loadMore={[Function]}
- ready={true}
- total={2}
- />
-</div>
-`;
-
-exports[`should render actions for admin 1`] = `
-<div
- className="page page-limited"
->
- <HelmetWrapper
- defer={true}
- encodeSpecialCharacters={true}
- title="organization.members.page"
- />
- <Suggestions
- suggestions="organization_members"
- />
- <MembersPageHeader
- loading={true}
- >
- <div
- className="page-actions"
- >
- <AddMemberForm
- addMember={[Function]}
- memberLogins={Array []}
- organization={
- Object {
- "canAdmin": true,
- "key": "foo",
- "name": "Foo",
- }
- }
- />
- <DocTooltip
- className="spacer-left"
- doc={Promise {}}
- />
- </div>
- </MembersPageHeader>
- <MembersListHeader
- handleSearch={[Function]}
- total={2}
- />
- <MembersList
- members={
- Array [
- Object {
- "avatar": "",
- "groupCount": 3,
- "login": "admin",
- "name": "Admin Istrator",
- },
- Object {
- "avatar": "7daf6c79d4802916d83f6266e24850af",
- "groupCount": 1,
- "login": "john",
- "name": "John Doe",
- },
- ]
- }
- organization={
- Object {
- "canAdmin": true,
- "key": "foo",
- "name": "Foo",
- }
- }
- organizationGroups={Array []}
- removeMember={[Function]}
- updateMemberGroups={[Function]}
- />
- <ListFooter
- count={2}
- loadMore={[Function]}
- ready={false}
- total={2}
- />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/organizations/routes.ts b/server/sonar-web/src/main/js/apps/organizations/routes.ts
index b6fa1ef378e..d78634091ec 100644
--- a/server/sonar-web/src/main/js/apps/organizations/routes.ts
+++ b/server/sonar-web/src/main/js/apps/organizations/routes.ts
@@ -60,7 +60,7 @@ const routes = [
},
{
path: 'members',
- component: lazyLoad(() => import('./components/OrganizationMembersContainer'))
+ component: lazyLoad(() => import('../organizationMembers/OrganizationMembersContainer'))
},
{
path: 'quality_profiles',
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js b/server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js
index 6a8777641c8..0b45197d190 100644
--- a/server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js
+++ b/server/sonar-web/src/main/js/apps/permission-templates/components/AppContainer.js
@@ -21,11 +21,10 @@ import { connect } from 'react-redux';
import App from './App';
import forSingleOrganization from '../../organizations/forSingleOrganization';
import { getAppState } from '../../../store/rootReducer';
-import { getRootQualifiers } from '../../../store/appState/duck';
const mapStateToProps = state => ({
// treat applications as portfolios
- topQualifiers: getRootQualifiers(getAppState(state)).filter(q => q !== 'APP')
+ topQualifiers: getAppState(state).qualifiers.filter(q => q !== 'APP')
});
export default forSingleOrganization(connect(mapStateToProps)(App));
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
index c00df0c9e03..ee9db3cf5ff 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
@@ -20,14 +20,10 @@
import { combineReducers } from 'redux';
import components, { getComponentByKey as nextGetComponentByKey } from './components';
import modulesByProject, { getProjectModules as nextGetProjectModules } from './modulesByProject';
-import globalMessages, {
- getGlobalMessages as nextGetGlobalMessages
-} from '../../../store/globalMessages/duck';
const rootReducer = combineReducers({
components,
- modulesByProject,
- globalMessages
+ modulesByProject
});
export default rootReducer;
@@ -42,5 +38,3 @@ export const getProjectModules = (state, projectKey) => {
}
return moduleKeys.map(moduleKey => getComponentByKey(state, moduleKey));
};
-
-export const getGlobalMessages = state => nextGetGlobalMessages(state.globalMessages);
diff --git a/server/sonar-web/src/main/js/store/appState/duck.ts b/server/sonar-web/src/main/js/store/appState/duck.ts
index 89f3613fd0f..79ada585879 100644
--- a/server/sonar-web/src/main/js/store/appState/duck.ts
+++ b/server/sonar-web/src/main/js/store/appState/duck.ts
@@ -72,7 +72,3 @@ export default function(state: AppState = defaultValue, action: Action): AppStat
return state;
}
-
-export function getRootQualifiers(state: AppState): string[] | undefined {
- return state.qualifiers;
-}
diff --git a/server/sonar-web/src/main/js/store/globalMessages/duck.js b/server/sonar-web/src/main/js/store/globalMessages/duck.js
index e5ee4d3a7e9..81c4cba0ff6 100644
--- a/server/sonar-web/src/main/js/store/globalMessages/duck.js
+++ b/server/sonar-web/src/main/js/store/globalMessages/duck.js
@@ -40,8 +40,8 @@ export type State = Array<Message>;
type Action = Object;
*/
-export const ERROR = 'ERROR';
-export const SUCCESS = 'SUCCESS';
+const ERROR = 'ERROR';
+const SUCCESS = 'SUCCESS';
/* Actions */
const ADD_GLOBAL_MESSAGE = 'ADD_GLOBAL_MESSAGE';
diff --git a/server/sonar-web/src/main/js/store/metrics/reducer.js b/server/sonar-web/src/main/js/store/metrics/reducer.js
index c93aa635941..d2d5284cc3a 100644
--- a/server/sonar-web/src/main/js/store/metrics/reducer.js
+++ b/server/sonar-web/src/main/js/store/metrics/reducer.js
@@ -47,5 +47,4 @@ const keys = (state /*: StateKeys */ = [], action = {}) => {
export default combineReducers({ byKey, keys });
export const getMetrics = (state /*: State */) => state.byKey;
-export const getMetricByKey = (state /*: State */, key /*: string */) => state.byKey[key];
export const getMetricsKey = (state /*: State */) => state.keys;
diff --git a/server/sonar-web/src/main/js/store/organizationsMembers/actions.js b/server/sonar-web/src/main/js/store/organizationsMembers/actions.js
deleted file mode 100644
index a56abe68722..00000000000
--- a/server/sonar-web/src/main/js/store/organizationsMembers/actions.js
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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,
- email?: 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',
- ADD_MEMBER: 'organizations/ADD_MEMBER',
- REMOVE_MEMBER: 'organizations/REMOVE_MEMBER'
-};
-
-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 addMember = (organizationKey /*: string */, member /*: Member */) => ({
- type: actions.ADD_MEMBER,
- organization: organizationKey,
- member
-});
-
-export const removeMember = (organizationKey /*: string */, member /*: Member */) => ({
- type: actions.REMOVE_MEMBER,
- organization: organizationKey,
- member
-});
-
-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
deleted file mode 100644
index 6e89f760491..00000000000
--- a/server/sonar-web/src/main/js/store/organizationsMembers/reducer.js
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 = {}) => {
- const members = state.members || [];
- 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(members.concat(action.members.map(member => member.login)))
- };
- case actions.ADD_MEMBER: {
- const withNew = [...members, action.member.login].sort();
- return {
- ...state,
- total: state.total + 1,
- members: withNew
- };
- }
- case actions.REMOVE_MEMBER: {
- const withoutDeleted = state.members.filter(login => login !== action.member.login);
- if (withoutDeleted.length === state.members.length) {
- return state;
- }
- return {
- ...state,
- total: state.total - 1,
- members: withoutDeleted
- };
- }
- 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:
- case actions.ADD_MEMBER:
- case actions.REMOVE_MEMBER:
- return {
- ...state,
- [action.organization]: organizationMembers(organization, action)
- };
- default:
- return state;
- }
-};
-
-export default organizationsMembers;
diff --git a/server/sonar-web/src/main/js/store/rootActions.js b/server/sonar-web/src/main/js/store/rootActions.js
index 52289a5ccd1..c49e3921c6c 100644
--- a/server/sonar-web/src/main/js/store/rootActions.js
+++ b/server/sonar-web/src/main/js/store/rootActions.js
@@ -32,12 +32,6 @@ import { parseError } from '../helpers/request';
export const onFail = dispatch => error =>
parseError(error).then(message => dispatch(addGlobalErrorMessage(message)));
-export const fetchAppState = () => dispatch =>
- getGlobalNavigation().then(appState => {
- dispatch(setAppState(appState));
- return appState;
- }, onFail(dispatch));
-
export const fetchLanguages = () => dispatch =>
getLanguages().then(languages => dispatch(receiveLanguages(languages)), onFail(dispatch));
diff --git a/server/sonar-web/src/main/js/store/rootReducer.js b/server/sonar-web/src/main/js/store/rootReducer.js
index 8bec4555692..16d038d1e89 100644
--- a/server/sonar-web/src/main/js/store/rootReducer.js
+++ b/server/sonar-web/src/main/js/store/rootReducer.js
@@ -23,7 +23,6 @@ import users, * as fromUsers from './users/reducer';
import languages, * as fromLanguages from './languages/reducer';
import metrics, * as fromMetrics from './metrics/reducer';
import organizations, * as fromOrganizations from './organizations/duck';
-import organizationsMembers, * as fromOrganizationsMembers from './organizationsMembers/reducer';
import globalMessages, * as fromGlobalMessages from './globalMessages/duck';
import permissionsApp, * as fromPermissionsApp from '../apps/permissions/shared/store/rootReducer';
import projectAdminApp, * as fromProjectAdminApp from '../apps/project-admin/store/rootReducer';
@@ -35,7 +34,6 @@ export default combineReducers({
languages,
metrics,
organizations,
- organizationsMembers,
users,
// apps
@@ -51,25 +49,12 @@ export const getGlobalMessages = state =>
export const getLanguages = state => fromLanguages.getLanguages(state.languages);
-export const getLanguageByKey = (state, key) =>
- fromLanguages.getLanguageByKey(state.languages, 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 getMarketplaceState = state => state.marketplace;
-
export const getMetrics = state => fromMetrics.getMetrics(state.metrics);
-export const getMetricByKey = (state, key) => fromMetrics.getMetricByKey(state.metrics, key);
-
export const getMetricsKey = state => fromMetrics.getMetricsKey(state.metrics);
export const getOrganizationByKey = (state, key) =>
@@ -83,12 +68,6 @@ 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 getPermissionsAppUsers = state => fromPermissionsApp.getUsers(state.permissionsApp);
export const getPermissionsAppGroups = state => fromPermissionsApp.getGroups(state.permissionsApp);
@@ -131,14 +110,5 @@ export const getSettingsAppValidationMessage = (state, key) =>
export const getSettingsAppEncryptionState = state =>
fromSettingsApp.getEncryptionState(state.settingsApp);
-export const getSettingsAppGlobalMessages = state =>
- fromSettingsApp.getGlobalMessages(state.settingsApp);
-
-export const getProjectAdminComponentByKey = (state, componentKey) =>
- fromProjectAdminApp.getComponentByKey(state.projectAdminApp, componentKey);
-
export const getProjectAdminProjectModules = (state, projectKey) =>
fromProjectAdminApp.getProjectModules(state.projectAdminApp, projectKey);
-
-export const getProjectAdminGlobalMessages = state =>
- fromProjectAdminApp.getGlobalMessages(state.projectAdminApp);
diff --git a/server/sonar-web/src/main/js/store/users/actions.ts b/server/sonar-web/src/main/js/store/users/actions.ts
index 46c31b7c472..9c9a146e2a4 100644
--- a/server/sonar-web/src/main/js/store/users/actions.ts
+++ b/server/sonar-web/src/main/js/store/users/actions.ts
@@ -22,7 +22,6 @@ import * as api from '../../api/users';
import { CurrentUser, HomePage } from '../../app/types';
export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER';
-export const RECEIVE_USER = 'RECEIVE_USER';
export const SKIP_ONBOARDING = 'SKIP_ONBOARDING';
export const SET_HOMEPAGE = 'SET_HOMEPAGE';
@@ -31,11 +30,6 @@ export const receiveCurrentUser = (user: CurrentUser) => ({
user
});
-export const receiveUser = (user: any) => ({
- type: RECEIVE_USER,
- user
-});
-
export const skipOnboarding = () => ({ type: SKIP_ONBOARDING });
export const fetchCurrentUser = () => (dispatch: Dispatch<any>) => {
diff --git a/server/sonar-web/src/main/js/store/users/reducer.ts b/server/sonar-web/src/main/js/store/users/reducer.ts
index 5f4fddfba20..a6e79db6d97 100644
--- a/server/sonar-web/src/main/js/store/users/reducer.ts
+++ b/server/sonar-web/src/main/js/store/users/reducer.ts
@@ -18,9 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { combineReducers } from 'redux';
-import { uniq, keyBy } from 'lodash';
-import { RECEIVE_CURRENT_USER, RECEIVE_USER, SKIP_ONBOARDING, SET_HOMEPAGE } from './actions';
-import { actions as membersActions } from '../organizationsMembers/actions';
+import { uniq } from 'lodash';
+import { RECEIVE_CURRENT_USER, SKIP_ONBOARDING, SET_HOMEPAGE } from './actions';
import { CurrentUser } from '../../app/types';
interface UsersByLogin {
@@ -28,35 +27,20 @@ interface UsersByLogin {
}
const usersByLogin = (state: UsersByLogin = {}, action: any = {}) => {
- switch (action.type) {
- case RECEIVE_CURRENT_USER:
- case RECEIVE_USER:
- return { ...state, [action.user.login]: action.user };
- case membersActions.RECEIVE_MEMBERS:
- case membersActions.RECEIVE_MORE_MEMBERS:
- return { ...state, ...keyBy(action.members, 'login') };
- case membersActions.ADD_MEMBER:
- return { ...state, [action.member.login]: action.member };
- default:
- return state;
+ if (action.type === RECEIVE_CURRENT_USER) {
+ return { ...state, [action.user.login]: action.user };
+ } else {
+ return state;
}
};
type UserLogins = string[];
const userLogins = (state: UserLogins = [], action: any = {}) => {
- switch (action.type) {
- case RECEIVE_CURRENT_USER:
- case RECEIVE_USER:
- return uniq([...state, action.user.login]);
- case membersActions.RECEIVE_MEMBERS:
- case membersActions.RECEIVE_MORE_MEMBERS:
- return uniq([...state, action.members.map((member: any) => member.login)]);
- case membersActions.ADD_MEMBER: {
- return uniq([...state, action.member.login]).sort();
- }
- default:
- return state;
+ if (action.type === RECEIVE_CURRENT_USER) {
+ return uniq([...state, action.user.login]);
+ } else {
+ return state;
}
};
@@ -82,8 +66,6 @@ interface State {
export default combineReducers({ usersByLogin, userLogins, currentUser });
export const getCurrentUser = (state: State) => state.currentUser!;
-export const getUserLogins = (state: State) => state.userLogins;
export const getUserByLogin = (state: State, login: string) => state.usersByLogin[login];
export const getUsersByLogins = (state: State, logins: string[]) =>
logins.map(login => getUserByLogin(state, login));
-export const getUsers = (state: State) => getUsersByLogins(state, getUserLogins(state));
diff --git a/server/sonar-web/src/main/js/store/utils/generalReducers.js b/server/sonar-web/src/main/js/store/utils/generalReducers.js
deleted file mode 100644
index 037761bfddf..00000000000
--- a/server/sonar-web/src/main/js/store/utils/generalReducers.js
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-// Author: Christoffer Niska <christofferniska@gmail.com>
-// https://gist.github.com/crisu83/42ecffccad9d04c74605fbc75c9dc9d1
-import { uniq } from 'lodash';
-
-/**
- * Creates a reducer that manages a single value.
- *
- * @param {function(state, action)} shouldUpdate
- * @param {function(state, action)} shouldReset
- * @param {function(state, action)} getValue
- * @param {*} defaultValue
- * @returns {function(state, action)}
- */
-export const createValue = (
- shouldUpdate = () => true,
- shouldReset = () => false,
- getValue = (state, action) => action.payload,
- defaultValue = null
-) => (state = defaultValue, action = {}) => {
- if (shouldReset(state, action)) {
- return defaultValue;
- }
- if (shouldUpdate(state, action)) {
- return getValue(state, action);
- }
- return state;
-};
-
-/**
- * Creates a reducer that manages a map.
- *
- * @param {function(state, action)} shouldUpdate
- * @param {function(state, action)} shouldReset
- * @param {function(state, action)} getValues
- * @returns {function(state, action)}
- */
-export const createMap = (
- shouldUpdate = () => true,
- shouldReset = () => false,
- getValues = (state, action) => action.payload
-) =>
- createValue(
- shouldUpdate,
- shouldReset,
- (state, action) => ({ ...state, ...getValues(state, action) }),
- {}
- );
-
-/**
- * Creates a reducer that manages a set.
- *
- * @param {function(state, action)} shouldUpdate
- * @param {function(state, action)} shouldReset
- * @param {function(state, action)} getValues
- * @returns {function(state, action)}
- */
-export const createSet = (
- shouldUpdate = () => true,
- shouldReset = () => false,
- getValues = (state, action) => action.payload
-) =>
- createValue(
- shouldUpdate,
- shouldReset,
- (state, action) => uniq([...state, ...getValues(state, action)]),
- []
- );
-
-/**
- * Creates a reducer that manages a flag.
- *
- * @param {function(state, action)} shouldTurnOn
- * @param {function(state, action)} shouldTurnOff
- * @param {bool} defaultValue
- * @returns {function(state, action)}
- */
-export const createFlag = (shouldTurnOn, shouldTurnOff, defaultValue = false) => (
- state = defaultValue,
- action = {}
-) => {
- if (shouldTurnOn(state, action)) {
- return true;
- }
- if (shouldTurnOff(state, action)) {
- return false;
- }
- return state;
-};