aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/organizationMembers
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2019-02-05 09:38:19 +0100
committersonartech <sonartech@sonarsource.com>2019-03-06 11:30:41 +0100
commit9bd42df0365a3c64161ac9283c62b4a9f422402d (patch)
tree0bf09e9422342aa91c949c62481841481aec48db /server/sonar-web/src/main/js/apps/organizationMembers
parent2847ce4e648a167335d675a3ba6e71b7b1b0f248 (diff)
downloadsonarqube-9bd42df0365a3c64161ac9283c62b4a9f422402d.tar.gz
sonarqube-9bd42df0365a3c64161ac9283c62b4a9f422402d.zip
SONARCLOUD-379 Enable users sync on existing ALM bound organizations
* Display org sync advertisement block * Add membersSync prop to organization type and update mock functions * Extract RadioCard from CardPlan * Allow to customize Modal through ConfirmButton * Add user sync configuration modal * Show help tooltip when user sync is activated
Diffstat (limited to 'server/sonar-web/src/main/js/apps/organizationMembers')
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx47
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx121
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx33
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembersContainer.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx169
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListHeader-test.tsx50
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx55
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/OrganizationMembers-test.tsx50
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/SyncMemberForm-test.tsx93
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListHeader-test.tsx.snap69
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap156
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/OrganizationMembers-test.tsx.snap143
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/SyncMemberForm-test.tsx.snap271
13 files changed, 1069 insertions, 191 deletions
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx
index 938e5a47d05..ba7cabf81a0 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx
@@ -18,16 +18,25 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import HelpTooltip from '../../components/controls/HelpTooltip';
import SearchBox from '../../components/controls/SearchBox';
+import { getAlmMembersUrl, sanitizeAlmId } from '../../helpers/almIntegrations';
+import { translate, translateWithParameters } from '../../helpers/l10n';
import { formatMeasure } from '../../helpers/measures';
-import { translate } from '../../helpers/l10n';
-interface Props {
+export interface Props {
+ currentUser: T.LoggedInUser;
handleSearch: (query?: string) => void;
+ organization: T.Organization;
total?: number;
}
-export default function MembersListHeader({ handleSearch, total }: Props) {
+export default function MembersListHeader({
+ currentUser,
+ handleSearch,
+ organization,
+ total
+}: Props) {
return (
<div className="panel panel-vertical bordered-bottom spacer-bottom">
<SearchBox
@@ -38,6 +47,38 @@ export default function MembersListHeader({ handleSearch, total }: Props) {
{total !== undefined && (
<span className="pull-right little-spacer-top">
<strong>{formatMeasure(total, 'INT')}</strong> {translate('organization.members.members')}
+ {organization.alm &&
+ organization.alm.membersSync && (
+ <HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div className="abs-width-300 markdown cut-margins">
+ <p>
+ {translate(
+ 'organization.members.auto_sync_total_help',
+ sanitizeAlmId(organization.alm.key) || ''
+ )}
+ </p>
+ {currentUser.personalOrganization !== organization.key && (
+ <>
+ <hr />
+ <p>
+ <a
+ href={getAlmMembersUrl(organization.alm)}
+ rel="noopener noreferrer"
+ target="_blank">
+ {translateWithParameters(
+ 'organization.members.see_all_members_on_x',
+ translate(sanitizeAlmId(organization.alm.key) || '')
+ )}
+ </a>
+ </p>
+ </>
+ )}
+ </div>
+ }
+ />
+ )}
</span>
)}
</div>
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx
index ec540fed313..82c68eba3d6 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx
@@ -18,35 +18,110 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
-import { translate } from '../../helpers/l10n';
+import AddMemberForm from './AddMemberForm';
+import SyncMemberForm from './SyncMemberForm';
import DeferredSpinner from '../../components/common/DeferredSpinner';
+import DocTooltip from '../../components/docs/DocTooltip';
+import NewInfoBox from '../../components/ui/NewInfoBox';
+import { sanitizeAlmId } from '../../helpers/almIntegrations';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { getCurrentUserSetting, Store } from '../../store/rootReducer';
+import { setCurrentUserSetting } from '../../store/users';
interface Props {
- children?: React.ReactNode;
+ dismissSyncNotifOrg: string[];
+ handleAddMember: (member: T.OrganizationMember) => void;
loading: boolean;
+ members?: T.OrganizationMember[];
+ organization: T.Organization;
+ setCurrentUserSetting: (setting: T.CurrentUserSetting) => void;
}
-export default function MembersPageHeader(props: Props) {
- return (
- <header className="page-header">
- <h1 className="page-title">{translate('organization.members.page')}</h1>
- <DeferredSpinner loading={props.loading} />
- {props.children}
- <p className="page-description">
- <FormattedMessage
- defaultMessage={translate('organization.members.page.description')}
- id="organization.members.page.description"
- values={{
- link: (
- <Link to="/documentation/organizations/manage-team/">
- {translate('organization.members.manage_a_team')}
- </Link>
- )
- }}
- />
- </p>
- </header>
- );
+export class MembersPageHeader extends React.PureComponent<Props> {
+ handleDismissSyncNotif = () => {
+ const { dismissSyncNotifOrg, organization } = this.props;
+ this.props.setCurrentUserSetting({
+ key: 'organizations.members.dismissSyncNotif',
+ value: [...dismissSyncNotifOrg, organization.key].join(',')
+ });
+ };
+
+ render() {
+ const { dismissSyncNotifOrg, members, organization } = this.props;
+ const memberLogins = members ? members.map(member => member.login) : [];
+ const isAdmin = organization.actions && organization.actions.admin;
+ const almKey = organization.alm && sanitizeAlmId(organization.alm.key);
+ const hasMemberSync = organization.alm && organization.alm.membersSync;
+ const showSyncNotif =
+ isAdmin &&
+ organization.alm &&
+ !hasMemberSync &&
+ !dismissSyncNotifOrg.some(orgKey => orgKey === organization.key);
+
+ return (
+ <header className="page-header">
+ <h1 className="page-title">{translate('organization.members.page')}</h1>
+ <DeferredSpinner loading={this.props.loading} />
+ {isAdmin && (
+ <div className="page-actions text-right">
+ {almKey && !showSyncNotif && <SyncMemberForm organization={organization} />}
+ {!hasMemberSync && (
+ <div className="display-inline-block spacer-left spacer-bottom">
+ <AddMemberForm
+ addMember={this.props.handleAddMember}
+ memberLogins={memberLogins}
+ organization={organization}
+ />
+ <DocTooltip
+ className="spacer-left"
+ doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/organizations/add-organization-member.md')}
+ />
+ </div>
+ )}
+ {almKey &&
+ showSyncNotif && (
+ <NewInfoBox
+ description={translate('organization.members.auto_sync_members_from_org', almKey)}
+ onClose={this.handleDismissSyncNotif}
+ title={translateWithParameters(
+ 'organization.members.auto_sync_with_x',
+ translate(almKey)
+ )}>
+ <SyncMemberForm organization={organization} />
+ </NewInfoBox>
+ )}
+ </div>
+ )}
+ <div className="page-description">
+ <FormattedMessage
+ defaultMessage={translate('organization.members.page.description')}
+ id="organization.members.page.description"
+ values={{
+ link: (
+ <Link to="/documentation/organizations/manage-team/">
+ {translate('organization.members.manage_a_team')}
+ </Link>
+ )
+ }}
+ />
+ </div>
+ </header>
+ );
+ }
}
+
+const mapStateToProps = (state: Store) => ({
+ dismissSyncNotifOrg: (
+ getCurrentUserSetting(state, 'organizations.members.dismissSyncNotif') || ''
+ ).split(',')
+});
+
+const mapDispatchToProps = { setCurrentUserSetting };
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(MembersPageHeader);
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx
index 215415f6216..46e1162372b 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembers.tsx
@@ -22,15 +22,14 @@ 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 { searchMembers, addMember, removeMember } from '../../api/organizations';
import { searchUsersGroups, addUserToGroup, removeUserFromGroup } from '../../api/user_groups';
interface Props {
+ currentUser: T.LoggedInUser;
organization: T.Organization;
}
@@ -187,31 +186,25 @@ export default class OrganizationMembers extends React.PureComponent<Props, Stat
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.actions &&
- organization.actions.admin && (
- <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>
+ <MembersPageHeader
+ handleAddMember={this.handleAddMember}
+ loading={loading}
+ members={members}
+ organization={organization}
+ />
{members !== undefined &&
paging !== undefined && (
<>
- <MembersListHeader handleSearch={this.handleSearchMembers} total={paging.total} />
+ <MembersListHeader
+ currentUser={this.props.currentUser}
+ handleSearch={this.handleSearchMembers}
+ organization={organization}
+ total={paging.total}
+ />
<MembersList
members={members}
organization={organization}
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembersContainer.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembersContainer.tsx
index 0390d301ea3..05c709f3920 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembersContainer.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/OrganizationMembersContainer.tsx
@@ -20,6 +20,7 @@
import { connect } from 'react-redux';
import OrganizationMembers from './OrganizationMembers';
import { getOrganizationByKey, Store } from '../../store/rootReducer';
+import { withCurrentUser } from '../../components/hoc/withCurrentUser';
interface OwnProps {
params: { organizationKey: string };
@@ -33,4 +34,4 @@ const mapStateToProps = (state: Store, ownProps: OwnProps): StateProps => {
return { organization: getOrganizationByKey(state, ownProps.params.organizationKey) };
};
-export default connect(mapStateToProps)(OrganizationMembers);
+export default withCurrentUser(connect(mapStateToProps)(OrganizationMembers));
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx
new file mode 100644
index 00000000000..b04c679563f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx
@@ -0,0 +1,169 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { connect } from 'react-redux';
+import { Link } from 'react-router';
+import ConfirmButton from '../../components/controls/ConfirmButton';
+import RadioCard from '../../components/controls/RadioCard';
+import { Alert } from '../../components/ui/Alert';
+import { Button } from '../../components/ui/buttons';
+import { setOrganizationMemberSync, syncMembers } from '../../api/organizations';
+import { sanitizeAlmId, isGithub } from '../../helpers/almIntegrations';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { fetchOrganization } from '../../store/rootActions';
+
+interface Props {
+ fetchOrganization: (key: string) => void;
+ organization: T.Organization;
+}
+
+interface State {
+ membersSync: boolean;
+}
+
+export class SyncMemberForm extends React.PureComponent<Props, State> {
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ membersSync: Boolean(props.organization.alm && props.organization.alm.membersSync)
+ };
+ }
+
+ handleConfirm = () => {
+ const { organization } = this.props;
+ const { membersSync } = this.state;
+ return setOrganizationMemberSync({
+ organization: organization.key,
+ enabled: membersSync
+ }).then(() => {
+ this.props.fetchOrganization(organization.key);
+ if (membersSync && isGithub(organization.alm && organization.alm.key)) {
+ return syncMembers(organization.key);
+ }
+ return Promise.resolve();
+ });
+ };
+
+ handleManualClick = () => {
+ this.setState({ membersSync: false });
+ };
+
+ handleAutoClick = () => {
+ this.setState({ membersSync: true });
+ };
+
+ renderModalBody = () => {
+ const { membersSync } = this.state;
+ const { organization } = this.props;
+ const almKey = organization.alm && sanitizeAlmId(organization.alm.key);
+ return (
+ <>
+ {translate('organization.members.management.description')}
+ <Link
+ className="spacer-left"
+ target="_blank"
+ to={{ pathname: '/documentation/organizations/manage-team/' }}>
+ {translate('learn_more')}
+ </Link>
+ <div className="display-flex-stretch big-spacer-top">
+ <RadioCard
+ onClick={this.handleManualClick}
+ selected={!membersSync}
+ title={translate('organization.members.management.manual')}>
+ <div className="spacer-left">
+ <ul className="big-spacer-left note">
+ <li className="spacer-bottom">
+ {translate('organization.members.management.manual.add_members_manually')}
+ </li>
+ <li>
+ {translate('organization.members.management.manual.choose_members_permissions')}
+ </li>
+ </ul>
+ </div>
+ </RadioCard>
+ <RadioCard
+ onClick={this.handleAutoClick}
+ selected={membersSync}
+ title={translateWithParameters(
+ 'organization.members.management.automatic',
+ translate(almKey || '')
+ )}>
+ <div className="spacer-left">
+ <ul className="big-spacer-left note">
+ {almKey && (
+ <>
+ <li className="spacer-bottom">
+ {translate(
+ 'organization.members.management.automatic.synchronized_from',
+ almKey
+ )}
+ </li>
+ <li className="spacer-bottom">
+ {translate(
+ 'organization.members.management.automatic.members_changes_reflected',
+ almKey
+ )}
+ </li>
+ </>
+ )}
+ <li>
+ {translate(
+ 'organization.members.management.automatic.still_choose_members_permissions'
+ )}
+ </li>
+ </ul>
+ </div>
+ {(!organization.alm || !organization.alm.membersSync) && (
+ <Alert className="big-spacer-top" variant="warning">
+ {translate('organization.members.management.automatic.warning')}
+ </Alert>
+ )}
+ </RadioCard>
+ </div>
+ </>
+ );
+ };
+
+ render() {
+ const { organization } = this.props;
+ const orgMemberSync = Boolean(organization.alm && organization.alm.membersSync);
+ return (
+ <ConfirmButton
+ cancelButtonText={translate('close')}
+ confirmButtonText={translate('save')}
+ confirmDisable={this.state.membersSync === orgMemberSync}
+ medium={true}
+ modalBody={this.renderModalBody()}
+ modalHeader={translate('organization.members.management.title')}
+ onConfirm={this.handleConfirm}>
+ {({ onClick }) => (
+ <Button onClick={onClick}>{translate('organization.members.config_synchro')}</Button>
+ )}
+ </ConfirmButton>
+ );
+ }
+}
+
+const mapDispatchToProps = { fetchOrganization };
+
+export default connect(
+ null,
+ mapDispatchToProps
+)(SyncMemberForm);
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListHeader-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListHeader-test.tsx
index 3105927acfa..7c97806d07e 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListHeader-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersListHeader-test.tsx
@@ -19,14 +19,54 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import MembersListHeader from '../MembersListHeader';
+import MembersListHeader, { Props } from '../MembersListHeader';
+import {
+ mockOrganization,
+ mockCurrentUser,
+ mockOrganizationWithAlm
+} from '../../../helpers/testMocks';
it('should render without the total', () => {
- const wrapper = shallow(<MembersListHeader handleSearch={jest.fn()} />);
- expect(wrapper).toMatchSnapshot();
+ expect(shallowRender({ total: undefined })).toMatchSnapshot();
});
it('should render with the total', () => {
- const wrapper = shallow(<MembersListHeader handleSearch={jest.fn()} total={8} />);
- expect(wrapper).toMatchSnapshot();
+ expect(shallowRender()).toMatchSnapshot();
});
+
+it('should render a help tooltip', () => {
+ expect(
+ shallowRender({ organization: mockOrganizationWithAlm({}, { membersSync: true }) }).find(
+ 'HelpTooltip'
+ )
+ ).toMatchSnapshot();
+ expect(
+ shallowRender({
+ organization: mockOrganizationWithAlm(
+ {},
+ { key: 'bitbucket', membersSync: true, url: 'https://bitbucket.com/foo' }
+ )
+ }).find('HelpTooltip')
+ ).toMatchSnapshot();
+});
+
+it('should not render link in help tooltip', () => {
+ expect(
+ shallowRender({
+ currentUser: mockCurrentUser({ personalOrganization: 'foo' }),
+ organization: mockOrganizationWithAlm({}, { membersSync: true })
+ }).find('HelpTooltip')
+ ).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+ return shallow(
+ <MembersListHeader
+ currentUser={mockCurrentUser()}
+ handleSearch={jest.fn()}
+ organization={mockOrganization()}
+ total={8}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx
index 538d093be88..7f74a8eff3c 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/MembersPageHeader-test.tsx
@@ -19,13 +19,52 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import MembersPageHeader from '../MembersPageHeader';
+import { MembersPageHeader } from '../MembersPageHeader';
+import {
+ mockOrganization,
+ mockOrganizationWithAlm,
+ mockOrganizationWithAdminActions
+} from '../../../helpers/testMocks';
-it('should render', () => {
- const wrapper = shallow(
- <MembersPageHeader loading={true}>
- <span>children test</span>
- </MembersPageHeader>
- );
- expect(wrapper).toMatchSnapshot();
+it('should render correctly', () => {
+ expect(shallowRender({ loading: true })).toMatchSnapshot();
+});
+
+it('should render for admin', () => {
+ expect(
+ shallowRender({ organization: mockOrganization({ actions: { admin: true } }) })
+ ).toMatchSnapshot();
+});
+
+it('should render for bound organization without sync', () => {
+ const organization = mockOrganizationWithAlm(mockOrganizationWithAdminActions());
+ expect(shallowRender({ organization })).toMatchSnapshot();
+
+ const wrapper = shallowRender({ organization, dismissSyncNotifOrg: [organization.key] });
+ expect(wrapper.find('Connect(SyncMemberForm)').exists()).toBe(true);
+ expect(wrapper.find('NewInfoBox').exists()).toBe(false);
+});
+
+it('should render for bound organization with sync', () => {
+ const organization = mockOrganizationWithAlm(mockOrganizationWithAdminActions(), {
+ membersSync: true
+ });
+ const wrapper = shallowRender({ organization });
+ expect(wrapper.find('Connect(SyncMemberForm)').exists()).toBe(true);
+ expect(wrapper.find('AddMemberForm').exists()).toBe(false);
+ expect(wrapper.find('NewInfoBox').exists()).toBe(false);
});
+
+function shallowRender(props: Partial<MembersPageHeader['props']> = {}) {
+ return shallow(
+ <MembersPageHeader
+ dismissSyncNotifOrg={[]}
+ handleAddMember={jest.fn()}
+ loading={false}
+ members={[]}
+ organization={mockOrganization()}
+ setCurrentUserSetting={jest.fn()}
+ {...props}
+ />
+ );
+}
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
index 43c07605766..7861581aa2e 100644
--- 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
@@ -20,9 +20,14 @@
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 {
+ mockOrganization,
+ mockCurrentUser,
+ mockOrganizationWithAdminActions
+} from '../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../helpers/testUtils';
jest.mock('../../../api/organizations', () => ({
addMember: jest.fn().mockResolvedValue({ login: 'bar', name: 'Bar', groupCount: 1 }),
@@ -48,15 +53,13 @@ jest.mock('../../../api/user_groups', () => ({
})
}));
-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} />);
+ const wrapper = shallowRender({ organization: mockOrganization() });
expect(wrapper).toMatchSnapshot();
await waitAndUpdate(wrapper);
@@ -64,40 +67,31 @@ it('should fetch members and render for non-admin', async () => {
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, actions: { admin: true } }} />
- );
+it('should fetch members and groups for admin', async () => {
+ const wrapper = shallowRender();
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, actions: { admin: true } }} />
- );
+ const wrapper = shallowRender();
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, actions: { admin: true } }} />
- );
+ const wrapper = shallowRender();
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, actions: { admin: true } }} />
- );
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
- wrapper.find('AddMemberForm').prop<Function>('addMember')({ login: 'bar' });
+ wrapper.find('Connect(MembersPageHeader)').prop<Function>('handleAddMember')({ login: 'bar' });
await waitAndUpdate(wrapper);
expect(
wrapper
@@ -110,9 +104,7 @@ it('should add new member', async () => {
});
it('should remove member', async () => {
- const wrapper = shallow(
- <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
- );
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.find('MembersList').prop<Function>('removeMember')({ login: 'john' });
await waitAndUpdate(wrapper);
@@ -127,9 +119,7 @@ it('should remove member', async () => {
});
it('should update groups', async () => {
- const wrapper = shallow(
- <OrganizationMembers organization={{ ...organization, actions: { admin: true } }} />
- );
+ const wrapper = shallowRender();
await waitAndUpdate(wrapper);
wrapper.find('MembersList').prop<Function>('updateMemberGroups')(
{ login: 'john' },
@@ -148,3 +138,13 @@ it('should update groups', async () => {
expect(removeUserFromGroup).toHaveBeenCalledTimes(1);
expect(removeUserFromGroup).toBeCalledWith({ login: 'john', name: 'birds', organization: 'foo' });
});
+
+function shallowRender(props: Partial<OrganizationMembers['props']> = {}) {
+ return shallow(
+ <OrganizationMembers
+ currentUser={mockCurrentUser()}
+ organization={mockOrganizationWithAdminActions()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/SyncMemberForm-test.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/SyncMemberForm-test.tsx
new file mode 100644
index 00000000000..4410d45c301
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/SyncMemberForm-test.tsx
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 { SyncMemberForm } from '../SyncMemberForm';
+import { setOrganizationMemberSync, syncMembers } from '../../../api/organizations';
+import { mockOrganizationWithAlm } from '../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../helpers/testUtils';
+
+jest.mock('../../../api/organizations', () => ({
+ setOrganizationMemberSync: jest.fn().mockResolvedValue(undefined),
+ syncMembers: jest.fn().mockResolvedValue(undefined)
+}));
+
+beforeEach(() => {
+ (setOrganizationMemberSync as jest.Mock).mockClear();
+ (syncMembers as jest.Mock).mockClear();
+});
+
+it('should allow to switch to automatic mode with github', async () => {
+ const fetchOrganization = jest.fn();
+ const wrapper = shallowRender({ fetchOrganization });
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setState({ membersSync: true });
+ wrapper.find('ConfirmButton').prop<Function>('onConfirm')();
+ expect(setOrganizationMemberSync).toHaveBeenCalledWith({ organization: 'foo', enabled: true });
+
+ await waitAndUpdate(wrapper);
+ expect(fetchOrganization).toHaveBeenCalledWith('foo');
+ expect(syncMembers).toHaveBeenCalledWith('foo');
+});
+
+it('should allow to switch to automatic mode with bitbucket', async () => {
+ const fetchOrganization = jest.fn();
+ const wrapper = shallowRender({
+ fetchOrganization,
+ organization: mockOrganizationWithAlm({}, { key: 'bitbucket' })
+ });
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setState({ membersSync: true });
+ wrapper.find('ConfirmButton').prop<Function>('onConfirm')();
+ expect(setOrganizationMemberSync).toHaveBeenCalledWith({ organization: 'foo', enabled: true });
+
+ await waitAndUpdate(wrapper);
+ expect(fetchOrganization).toHaveBeenCalledWith('foo');
+ expect(syncMembers).not.toHaveBeenCalled();
+});
+
+it('should allow to switch to manual mode', async () => {
+ const fetchOrganization = jest.fn();
+ const wrapper = shallowRender({
+ fetchOrganization,
+ organization: mockOrganizationWithAlm({}, { membersSync: true })
+ });
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.setState({ membersSync: false });
+ wrapper.find('ConfirmButton').prop<Function>('onConfirm')();
+ expect(setOrganizationMemberSync).toHaveBeenCalledWith({ organization: 'foo', enabled: false });
+
+ await waitAndUpdate(wrapper);
+ expect(fetchOrganization).toHaveBeenCalledWith('foo');
+ expect(syncMembers).not.toHaveBeenCalled();
+});
+
+function shallowRender(props: Partial<SyncMemberForm['props']> = {}) {
+ return shallow<SyncMemberForm>(
+ <SyncMemberForm
+ fetchOrganization={jest.fn()}
+ organization={mockOrganizationWithAlm()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListHeader-test.tsx.snap
index 7bfe2725e60..1d8ecfb92df 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListHeader-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersListHeader-test.tsx.snap
@@ -1,5 +1,74 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`should not render link in help tooltip 1`] = `
+<HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div
+ className="abs-width-300 markdown cut-margins"
+ >
+ <p>
+ organization.members.auto_sync_total_help.github
+ </p>
+ </div>
+ }
+/>
+`;
+
+exports[`should render a help tooltip 1`] = `
+<HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div
+ className="abs-width-300 markdown cut-margins"
+ >
+ <p>
+ organization.members.auto_sync_total_help.github
+ </p>
+ <React.Fragment>
+ <hr />
+ <p>
+ <a
+ href="https://github.com/orgs/foo/people"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ organization.members.see_all_members_on_x.github
+ </a>
+ </p>
+ </React.Fragment>
+ </div>
+ }
+/>
+`;
+
+exports[`should render a help tooltip 2`] = `
+<HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div
+ className="abs-width-300 markdown cut-margins"
+ >
+ <p>
+ organization.members.auto_sync_total_help.bitbucket
+ </p>
+ <React.Fragment>
+ <hr />
+ <p>
+ <a
+ href="https://bitbucket.com/foo/profile/members"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ organization.members.see_all_members_on_x.bitbucket
+ </a>
+ </p>
+ </React.Fragment>
+ </div>
+ }
+/>
+`;
+
exports[`should render with the total 1`] = `
<div
className="panel panel-vertical bordered-bottom spacer-bottom"
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
index 95179079487..b0bc7e4a009 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render 1`] = `
+exports[`should render correctly 1`] = `
<header
className="page-header"
>
@@ -13,10 +13,7 @@ exports[`should render 1`] = `
loading={true}
timeout={100}
/>
- <span>
- children test
- </span>
- <p
+ <div
className="page-description"
>
<FormattedMessage
@@ -34,6 +31,153 @@ exports[`should render 1`] = `
}
}
/>
- </p>
+ </div>
+</header>
+`;
+
+exports[`should render for admin 1`] = `
+<header
+ className="page-header"
+>
+ <h1
+ className="page-title"
+ >
+ organization.members.page
+ </h1>
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ />
+ <div
+ className="page-actions text-right"
+ >
+ <div
+ className="display-inline-block spacer-left spacer-bottom"
+ >
+ <AddMemberForm
+ addMember={[MockFunction]}
+ memberLogins={Array []}
+ organization={
+ Object {
+ "actions": Object {
+ "admin": true,
+ },
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ />
+ <DocTooltip
+ className="spacer-left"
+ doc={Promise {}}
+ />
+ </div>
+ </div>
+ <div
+ className="page-description"
+ >
+ <FormattedMessage
+ defaultMessage="organization.members.page.description"
+ id="organization.members.page.description"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/documentation/organizations/manage-team/"
+ >
+ organization.members.manage_a_team
+ </Link>,
+ }
+ }
+ />
+ </div>
+</header>
+`;
+
+exports[`should render for bound organization without sync 1`] = `
+<header
+ className="page-header"
+>
+ <h1
+ className="page-title"
+ >
+ organization.members.page
+ </h1>
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ />
+ <div
+ className="page-actions text-right"
+ >
+ <div
+ className="display-inline-block spacer-left spacer-bottom"
+ >
+ <AddMemberForm
+ addMember={[MockFunction]}
+ memberLogins={Array []}
+ organization={
+ Object {
+ "actions": Object {
+ "admin": true,
+ },
+ "alm": Object {
+ "key": "github",
+ "membersSync": false,
+ "url": "https://github.com/foo",
+ },
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ />
+ <DocTooltip
+ className="spacer-left"
+ doc={Promise {}}
+ />
+ </div>
+ <NewInfoBox
+ description="organization.members.auto_sync_members_from_org.github"
+ onClose={[Function]}
+ title="organization.members.auto_sync_with_x.github"
+ >
+ <Connect(SyncMemberForm)
+ organization={
+ Object {
+ "actions": Object {
+ "admin": true,
+ },
+ "alm": Object {
+ "key": "github",
+ "membersSync": false,
+ "url": "https://github.com/foo",
+ },
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ />
+ </NewInfoBox>
+ </div>
+ <div
+ className="page-description"
+ >
+ <FormattedMessage
+ defaultMessage="organization.members.page.description"
+ id="organization.members.page.description"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/documentation/organizations/manage-team/"
+ >
+ organization.members.manage_a_team
+ </Link>,
+ }
+ }
+ />
+ </div>
</header>
`;
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
index 90b44168efc..dd26a5928f4 100644
--- 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
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should fetch members and groups and render for admin 1`] = `
+exports[`should fetch members and render for non-admin 1`] = `
<div
className="page page-limited"
>
@@ -12,110 +12,15 @@ exports[`should fetch members and groups and render for admin 1`] = `
<Suggestions
suggestions="organization_members"
/>
- <MembersPageHeader
- loading={false}
- >
- <div
- className="page-actions"
- >
- <AddMemberForm
- addMember={[Function]}
- memberLogins={
- Array [
- "admin",
- "john",
- ]
- }
- organization={
- Object {
- "actions": Object {
- "admin": true,
- },
- "key": "foo",
- "name": "Foo",
- }
- }
- />
- <DocTooltip
- className="spacer-left"
- doc={Promise {}}
- />
- </div>
- </MembersPageHeader>
- <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",
- },
- ]
- }
+ <Connect(MembersPageHeader)
+ handleAddMember={[Function]}
+ loading={true}
organization={
Object {
- "actions": Object {
- "admin": 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}
- />
-</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>
`;
@@ -132,11 +37,49 @@ exports[`should fetch members and render for non-admin 2`] = `
<Suggestions
suggestions="organization_members"
/>
- <MembersPageHeader
+ <Connect(MembersPageHeader)
+ handleAddMember={[Function]}
loading={false}
+ 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",
+ }
+ }
/>
<MembersListHeader
+ currentUser={
+ Object {
+ "groups": Array [],
+ "isLoggedIn": true,
+ "login": "luke",
+ "name": "Skywalker",
+ "scmAccounts": Array [],
+ }
+ }
handleSearch={[Function]}
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
total={3}
/>
<MembersList
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/SyncMemberForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/SyncMemberForm-test.tsx.snap
new file mode 100644
index 00000000000..b8744db2ef6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/SyncMemberForm-test.tsx.snap
@@ -0,0 +1,271 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should allow to switch to automatic mode with bitbucket 1`] = `
+<ConfirmButton
+ cancelButtonText="close"
+ confirmButtonText="save"
+ confirmDisable={true}
+ medium={true}
+ modalBody={
+ <React.Fragment>
+ organization.members.management.description
+ <Link
+ className="spacer-left"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to={
+ Object {
+ "pathname": "/documentation/organizations/manage-team/",
+ }
+ }
+ >
+ learn_more
+ </Link>
+ <div
+ className="display-flex-stretch big-spacer-top"
+ >
+ <RadioCard
+ onClick={[Function]}
+ selected={true}
+ title="organization.members.management.manual"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.manual.add_members_manually
+ </li>
+ <li>
+ organization.members.management.manual.choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ </RadioCard>
+ <RadioCard
+ onClick={[Function]}
+ selected={false}
+ title="organization.members.management.automatic.bitbucket"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <React.Fragment>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.synchronized_from.bitbucket
+ </li>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.members_changes_reflected.bitbucket
+ </li>
+ </React.Fragment>
+ <li>
+ organization.members.management.automatic.still_choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ <Alert
+ className="big-spacer-top"
+ variant="warning"
+ >
+ organization.members.management.automatic.warning
+ </Alert>
+ </RadioCard>
+ </div>
+ </React.Fragment>
+ }
+ modalHeader="organization.members.management.title"
+ onConfirm={[Function]}
+>
+ <Component />
+</ConfirmButton>
+`;
+
+exports[`should allow to switch to automatic mode with github 1`] = `
+<ConfirmButton
+ cancelButtonText="close"
+ confirmButtonText="save"
+ confirmDisable={true}
+ medium={true}
+ modalBody={
+ <React.Fragment>
+ organization.members.management.description
+ <Link
+ className="spacer-left"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to={
+ Object {
+ "pathname": "/documentation/organizations/manage-team/",
+ }
+ }
+ >
+ learn_more
+ </Link>
+ <div
+ className="display-flex-stretch big-spacer-top"
+ >
+ <RadioCard
+ onClick={[Function]}
+ selected={true}
+ title="organization.members.management.manual"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.manual.add_members_manually
+ </li>
+ <li>
+ organization.members.management.manual.choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ </RadioCard>
+ <RadioCard
+ onClick={[Function]}
+ selected={false}
+ title="organization.members.management.automatic.github"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <React.Fragment>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.synchronized_from.github
+ </li>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.members_changes_reflected.github
+ </li>
+ </React.Fragment>
+ <li>
+ organization.members.management.automatic.still_choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ <Alert
+ className="big-spacer-top"
+ variant="warning"
+ >
+ organization.members.management.automatic.warning
+ </Alert>
+ </RadioCard>
+ </div>
+ </React.Fragment>
+ }
+ modalHeader="organization.members.management.title"
+ onConfirm={[Function]}
+>
+ <Component />
+</ConfirmButton>
+`;
+
+exports[`should allow to switch to manual mode 1`] = `
+<ConfirmButton
+ cancelButtonText="close"
+ confirmButtonText="save"
+ confirmDisable={true}
+ medium={true}
+ modalBody={
+ <React.Fragment>
+ organization.members.management.description
+ <Link
+ className="spacer-left"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to={
+ Object {
+ "pathname": "/documentation/organizations/manage-team/",
+ }
+ }
+ >
+ learn_more
+ </Link>
+ <div
+ className="display-flex-stretch big-spacer-top"
+ >
+ <RadioCard
+ onClick={[Function]}
+ selected={false}
+ title="organization.members.management.manual"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.manual.add_members_manually
+ </li>
+ <li>
+ organization.members.management.manual.choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ </RadioCard>
+ <RadioCard
+ onClick={[Function]}
+ selected={true}
+ title="organization.members.management.automatic.github"
+ >
+ <div
+ className="spacer-left"
+ >
+ <ul
+ className="big-spacer-left note"
+ >
+ <React.Fragment>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.synchronized_from.github
+ </li>
+ <li
+ className="spacer-bottom"
+ >
+ organization.members.management.automatic.members_changes_reflected.github
+ </li>
+ </React.Fragment>
+ <li>
+ organization.members.management.automatic.still_choose_members_permissions
+ </li>
+ </ul>
+ </div>
+ </RadioCard>
+ </div>
+ </React.Fragment>
+ }
+ modalHeader="organization.members.management.title"
+ onConfirm={[Function]}
+>
+ <Component />
+</ConfirmButton>
+`;