aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/app/components/StartupModal.tsx14
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/modals.css77
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/misc.css4
-rw-r--r--server/sonar-web/src/main/js/app/types.d.ts9
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/BillingFormShim.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationModal-test.tsx.snap3
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationBind.tsx19
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx25
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx51
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationBind-test.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx31
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationBind-test.tsx.snap29
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap37
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap7
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap22
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap7
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap7
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/actions-test.ts85
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/actions.ts51
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx24
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/SyncMemberForm-test.tsx.snap108
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/__tests__/actions-test.ts56
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/actions.ts42
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEmpty-test.tsx49
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEmpty-test.tsx.snap29
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx33
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx43
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap46
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/styles.css1
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/TeamOnboardingModal.tsx69
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/__tests__/__snapshots__/TeamOnboardingModal-test.tsx.snap61
-rw-r--r--server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx15
-rw-r--r--server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx17
-rw-r--r--server/sonar-web/src/main/js/components/controls/Modal.tsx17
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/ConfirmButton-test.tsx (renamed from server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/__tests__/TeamOnboardingModal-test.tsx)26
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ConfirmButton-test.tsx.snap20
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/OnboardingProjectIcon.tsx3
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx33
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts28
-rw-r--r--server/sonar-web/src/main/js/helpers/almIntegrations.ts4
-rw-r--r--server/sonar-web/src/main/js/helpers/testMocks.ts15
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties18
58 files changed, 747 insertions, 638 deletions
diff --git a/server/sonar-web/src/main/js/app/components/StartupModal.tsx b/server/sonar-web/src/main/js/app/components/StartupModal.tsx
index 45cf0a3d7b4..c5d84e6c8eb 100644
--- a/server/sonar-web/src/main/js/app/components/StartupModal.tsx
+++ b/server/sonar-web/src/main/js/app/components/StartupModal.tsx
@@ -36,9 +36,6 @@ const LicensePromptModal = lazyLoad(
() => import('../../apps/marketplace/components/LicensePromptModal'),
'LicensePromptModal'
);
-const TeamOnboardingModal = lazyLoad(() =>
- import('../../apps/tutorials/teamOnboarding/TeamOnboardingModal')
-);
interface StateProps {
canAdmin?: boolean;
@@ -63,8 +60,7 @@ type Props = StateProps & DispatchProps & OwnProps & WithRouterProps;
enum ModalKey {
license,
- onboarding,
- teamOnboarding
+ onboarding
}
interface State {
@@ -113,10 +109,6 @@ export class StartupModal extends React.PureComponent<Props, State> {
this.props.router.push({ pathname: `/projects/create`, state });
};
- openTeamOnboarding = () => {
- this.setState({ modal: ModalKey.teamOnboarding });
- };
-
tryAutoOpenLicense = () => {
const { canAdmin, currentEdition, currentUser } = this.props;
const hasLicenseManager = hasMessage('license.prompt.title');
@@ -161,12 +153,8 @@ export class StartupModal extends React.PureComponent<Props, State> {
<OnboardingModal
onClose={this.closeOnboarding}
onOpenProjectOnboarding={this.openProjectOnboarding}
- onOpenTeamOnboarding={this.openTeamOnboarding}
/>
)}
- {modal === ModalKey.teamOnboarding && (
- <TeamOnboardingModal onFinish={this.closeOnboarding} />
- )}
</OnboardingContext.Provider>
);
}
diff --git a/server/sonar-web/src/main/js/app/styles/components/modals.css b/server/sonar-web/src/main/js/app/styles/components/modals.css
index 7a1b8a4e72b..370030f2a61 100644
--- a/server/sonar-web/src/main/js/app/styles/components/modals.css
+++ b/server/sonar-web/src/main/js/app/styles/components/modals.css
@@ -30,6 +30,10 @@
transition: all 0.2s ease;
}
+.modal.sonarcloud {
+ border-radius: 3px;
+}
+
.modal:focus,
.ReactModal__Content:focus {
outline: none;
@@ -83,67 +87,53 @@
}
.modal-container {
- max-height: 70vh;
+ max-height: 60vh;
padding: 10px;
box-sizing: border-box;
overflow: auto;
}
-.modal-head {
- padding: 0 10px;
- background-color: var(--gray94);
- border-bottom: 1px solid #ddd;
-}
-
-.modal-head h1,
-.modal-head h2 {
- line-height: 30px;
- min-height: 30px;
+.modal.sonarcloud .modal-container {
+ border-top: 1px solid var(--barBorderColor);
+ margin-top: var(--pagePadding);
}
-.modal-body {
- padding: 10px;
+.modal.sonarcloud .modal-container > :last-child {
+ margin-bottom: var(--pagePadding);
}
-.modal-simple {
- border-radius: 3px;
+.modal-head {
+ padding: 0 10px;
+ background-color: var(--gray94);
+ border-bottom: 1px solid var(--disableGrayBorder);
}
-.modal-simple-head {
- padding: var(--pagePadding) calc(2 * var(--pagePadding));
+.modal.sonarcloud .modal-head {
+ background-color: transparent;
+ border-bottom: none;
+ padding: var(--pagePadding) calc(2 * var(--pagePadding)) 0;
}
-.modal-simple-head h1 {
- margin-top: var(--pagePadding);
- font-size: var(--hugeFontSize);
- font-weight: bold;
+.modal-head h1,
+.modal-head h2 {
line-height: 30px;
+ min-height: 30px;
}
-.modal-simple-head h2 {
+.modal.sonarcloud .modal-head h1,
+.modal.sonarcloud .modal-head h2 {
+ margin-top: var(--gridSize);
font-size: var(--bigFontSize);
font-weight: bold;
- line-height: 24px;
-}
-
-.modal-simple-body {
- padding: 0 calc(2 * var(--pagePadding)) var(--pagePadding);
+ line-height: 30px;
}
-.modal-simple-foot {
- padding: calc(2 * var(--pagePadding)) calc(2 * var(--pagePadding));
- border-radius: 3px;
+.modal-body {
+ padding: 10px;
}
-.modal-simple-foot-action {
- display: flex;
- justify-content: space-between;
- align-items: center;
+.modal.sonarcloud .modal-body {
padding: var(--pagePadding) calc(2 * var(--pagePadding));
- border-top: 1px solid var(--barBorderColor);
- background-color: var(--barBackgroundColor);
- text-align: right;
- border-radius: 3px;
}
.modal-field,
@@ -293,16 +283,23 @@
.modal-foot {
padding: 10px;
- border-top: 1px solid #ccc;
+ border-top: 1px solid var(--disableGrayBorder);
background-color: var(--gray94);
text-align: right;
}
+.modal.sonarcloud .modal-foot {
+ padding: var(--pagePadding);
+ border-top: 1px solid var(--barBorderColor);
+ background-color: var(--barBackgroundColor);
+ border-radius: 3px;
+}
+
.modal-foot button,
.modal-foot .button,
.modal-foot input[type='submit'],
.modal-foot input[type='button'] {
- margin-right: 10px;
+ margin-left: var(--gridSize);
}
.modal-error,
diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css
index 71ac4dc098c..06637e62f6d 100644
--- a/server/sonar-web/src/main/js/app/styles/init/misc.css
+++ b/server/sonar-web/src/main/js/app/styles/init/misc.css
@@ -257,6 +257,10 @@ td.big-spacer-top {
width: 400px !important;
}
+.abs-width-600 {
+ width: 600px !important;
+}
+
.justify {
margin-bottom: -1em;
text-align: justify;
diff --git a/server/sonar-web/src/main/js/app/types.d.ts b/server/sonar-web/src/main/js/app/types.d.ts
index 9f70f1e0f97..45d8d3e0dc2 100644
--- a/server/sonar-web/src/main/js/app/types.d.ts
+++ b/server/sonar-web/src/main/js/app/types.d.ts
@@ -27,6 +27,7 @@ declare namespace T {
}
export interface AlmOrganization extends OrganizationBase {
+ almUrl: string;
key: string;
personal: boolean;
privateRepos: number;
@@ -502,7 +503,7 @@ declare namespace T {
export interface Organization extends OrganizationBase {
actions?: OrganizationActions;
- alm?: OrganizationAlm;
+ alm?: { key: string; membersSync: boolean; url: string };
adminPages?: Extension[];
canUpdateProjectsVisibilityToPrivate?: boolean;
guarded?: boolean;
@@ -513,12 +514,6 @@ declare namespace T {
subscription?: OrganizationSubscription;
}
- export interface OrganizationAlm {
- key: string;
- membersSync: boolean;
- url: string;
- }
-
export interface OrganizationBase {
avatar?: string;
description?: string;
diff --git a/server/sonar-web/src/main/js/apps/create/components/BillingFormShim.tsx b/server/sonar-web/src/main/js/apps/create/components/BillingFormShim.tsx
index 1eb726cf2ad..97327f7b95b 100644
--- a/server/sonar-web/src/main/js/apps/create/components/BillingFormShim.tsx
+++ b/server/sonar-web/src/main/js/apps/create/components/BillingFormShim.tsx
@@ -23,7 +23,7 @@ interface ChildrenProps {
onSubmit: React.FormEventHandler;
processingUpgrade: boolean;
renderFormFields: () => React.ReactNode;
- renderNextCharge: () => React.ReactNode;
+ renderNextCharge: (className?: string) => React.ReactNode;
renderRecap: () => React.ReactNode;
renderSubmitButton: (submitText?: string) => React.ReactNode;
renderSubmitGroup: (submitText?: string) => React.ReactNode;
diff --git a/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx b/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx
index 269f5be6e00..dfaca7bc0f0 100644
--- a/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx
+++ b/server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx
@@ -75,9 +75,8 @@ export default class UpgradeOrganizationModal extends React.PureComponent<Props,
medium={true}
noBackdrop={this.props.insideModal}
onRequestClose={this.props.onClose}
- shouldCloseOnOverlayClick={false}
- simple={true}>
- <div className="modal-simple-head">
+ shouldCloseOnOverlayClick={false}>
+ <div className="modal-head">
<h2>{header}</h2>
</div>
<BillingForm
@@ -93,7 +92,7 @@ export default class UpgradeOrganizationModal extends React.PureComponent<Props,
renderSubmitButton
}) => (
<form id="organization-paid-plan-form" onSubmit={onSubmit}>
- <div className="modal-simple-body modal-container">
+ <div className="modal-body modal-container">
<div className="huge-spacer-bottom">
<p className="spacer-bottom">
<FormattedMessage
@@ -109,10 +108,10 @@ export default class UpgradeOrganizationModal extends React.PureComponent<Props,
{renderFormFields()}
<div className="big-spacer-top">{renderRecap()}</div>
</div>
- <footer className="modal-simple-foot-action">
- <span className="note">{renderNextCharge()}</span>
+ <footer className="modal-foot display-flex-center display-flex-space-between">
+ {renderNextCharge() || <span />}
<div>
- <DeferredSpinner className="spacer-right" loading={processingUpgrade} />
+ <DeferredSpinner loading={processingUpgrade} />
{renderSubmitButton()}
<ResetButtonLink onClick={this.props.onClose}>
{translate('cancel')}
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationModal-test.tsx.snap
index f826be7df3e..3043cc08b5f 100644
--- a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationModal-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationModal-test.tsx.snap
@@ -6,10 +6,9 @@ exports[`should render correctly 1`] = `
medium={true}
onRequestClose={[MockFunction]}
shouldCloseOnOverlayClick={false}
- simple={true}
>
<div
- className="modal-simple-head"
+ className="modal-head"
>
<h2>
billing.upgrade_box.upgrade_to_paid_plan
diff --git a/server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx b/server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx
index 007b7b23d2b..307641af43d 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx
@@ -18,9 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { translate } from '../../../helpers/l10n';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import { sanitizeAlmId } from '../../../helpers/almIntegrations';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
export default function AlmApplicationInstalling({ almKey }: { almKey?: string }) {
return (
@@ -30,9 +30,9 @@ export default function AlmApplicationInstalling({ almKey }: { almKey?: string }
<div className="huge-spacer-top text-center">
<i className="spinner" />
<p className="big-spacer-top">
- {translate(
+ {translateWithParameters(
'onboarding.import_organization.installing',
- sanitizeAlmId(almKey) || 'ALM'
+ almKey ? translate(sanitizeAlmId(almKey)) : 'ALM'
)}
</p>
</div>
diff --git a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationBind.tsx b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationBind.tsx
index 493ec1c92e1..77a7d627ff1 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationBind.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationBind.tsx
@@ -18,12 +18,15 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { Link } from 'react-router';
import OrganizationSelect from '../components/OrganizationSelect';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { Alert } from '../../../components/ui/Alert';
import { SubmitButton } from '../../../components/ui/buttons';
-import { translate } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
interface Props {
+ almKey: string;
onBindOrganization: (organization: string) => Promise<void>;
unboundOrganizations: T.Organization[];
}
@@ -84,6 +87,18 @@ export default class AutoOrganizationBind extends React.PureComponent<Props, Sta
organization={organization}
organizations={this.props.unboundOrganizations}
/>
+ <Alert className="abs-width-400 big-spacer-top" display="block" variant="info">
+ {translateWithParameters(
+ 'onboarding.import_organization.bind_members_not_sync_info_x',
+ translate('organization', this.props.almKey)
+ )}
+ <Link
+ className="spacer-left"
+ target="_blank"
+ to={{ pathname: '/documentation/organizations/manage-team/' }}>
+ {translate('learn_more')}
+ </Link>
+ </Alert>
<div className="display-flex-center big-spacer-top">
<SubmitButton disabled={submitting || !organization}>
{translate('onboarding.import_organization.bind')}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
index 8808ba534ce..bce0bcb2c21 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
@@ -24,11 +24,12 @@ import OrganizationDetailsForm from './OrganizationDetailsForm';
import OrganizationDetailsStep from './OrganizationDetailsStep';
import PlanStep from './PlanStep';
import { Step } from './utils';
+import { Alert } from '../../../components/ui/Alert';
import { DeleteButton } from '../../../components/ui/buttons';
import RadioToggle from '../../../components/controls/RadioToggle';
import { bindAlmOrganization } from '../../../api/alm-integration';
-import { sanitizeAlmId } from '../../../helpers/almIntegrations';
-import { translate } from '../../../helpers/l10n';
+import { sanitizeAlmId, getAlmMembersUrl } from '../../../helpers/almIntegrations';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/urls';
enum Filters {
@@ -102,6 +103,7 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S
} = this.props;
const { filter } = this.state;
const hasUnboundOrgs = unboundOrganizations.length > 0;
+ const almKey = sanitizeAlmId(almApplication.key);
return (
<div className={className}>
<OrganizationDetailsStep
@@ -156,6 +158,24 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S
{filter === Filters.Create && (
<OrganizationDetailsForm
+ infoBlock={
+ <Alert className="abs-width-600 big-spacer-top" display="block" variant="info">
+ <p>
+ {translateWithParameters(
+ 'onboarding.import_organization.members_sync_info_x',
+ translate('organization', almKey),
+ almOrganization.name,
+ translate(almKey)
+ )}
+ </p>
+ <a
+ href={getAlmMembersUrl(almOrganization.key, almOrganization.almUrl)}
+ rel="noopener noreferrer"
+ target="_blank">
+ {translate('onboarding.import_organization.see_who_has_access')}
+ </a>
+ </Alert>
+ }
onContinue={this.props.handleOrgDetailsFinish}
organization={almOrganization}
submitText={translate('continue')}
@@ -163,6 +183,7 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S
)}
{filter === Filters.Bind && (
<AutoOrganizationBind
+ almKey={almKey}
onBindOrganization={this.handleBindOrganization}
unboundOrganizations={unboundOrganizations}
/>
diff --git a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
index b1d1b633812..0e441863c12 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
@@ -22,9 +22,9 @@ import * as classNames from 'classnames';
import { differenceInMinutes } from 'date-fns';
import { times } from 'lodash';
import { connect } from 'react-redux';
-import { Dispatch } from 'redux';
import { Helmet } from 'react-helmet';
import { withRouter, WithRouterProps } from 'react-router';
+import { createOrganization, updateOrganization } from './actions';
import {
ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP,
parseQuery,
@@ -42,8 +42,8 @@ import DeferredSpinner from '../../../components/common/DeferredSpinner';
import Tabs from '../../../components/controls/Tabs';
import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
import { withUserOrganizations } from '../../../components/hoc/withUserOrganizations';
+import { deleteOrganization } from '../../organizations/actions';
import {
- bindAlmOrganization,
getAlmAppInfo,
getAlmOrganization,
GetAlmOrganizationResponse,
@@ -51,14 +51,17 @@ import {
} from '../../../api/alm-integration';
import { getSubscriptionPlans } from '../../../api/billing';
import * as api from '../../../api/organizations';
-import { hasAdvancedALMIntegration, isPersonal } from '../../../helpers/almIntegrations';
-import { translate } from '../../../helpers/l10n';
+import {
+ hasAdvancedALMIntegration,
+ isPersonal,
+ sanitizeAlmId
+} from '../../../helpers/almIntegrations';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import { addWhitePageClass, removeWhitePageClass } from '../../../helpers/pages';
import { get, remove } from '../../../helpers/storage';
import { slugify } from '../../../helpers/strings';
import { getOrganizationUrl } from '../../../helpers/urls';
import { skipOnboarding } from '../../../store/users';
-import * as actions from '../../../store/organizations';
import '../../tutorials/styles.css'; // TODO remove me
interface Props {
@@ -337,7 +340,10 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
tabs={[
{
key: 'auto',
- node: translate('onboarding.import_organization', almApplication.key)
+ node: translateWithParameters(
+ 'onboarding.import_organization.import_from_x',
+ translate(sanitizeAlmId(almApplication.key))
+ )
},
{
key: 'manual',
@@ -426,39 +432,6 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
}
}
-function createOrganization(organization: T.Organization & { installationId?: string }) {
- return (dispatch: Dispatch) => {
- return api
- .createOrganization({ ...organization, name: organization.name || organization.key })
- .then((organization: T.Organization) => {
- dispatch(actions.createOrganization(organization));
- return organization.key;
- });
- };
-}
-
-function updateOrganization(organization: T.Organization & { installationId?: string }) {
- return (dispatch: Dispatch) => {
- const { key, installationId, ...changes } = organization;
- const promises = [api.updateOrganization(key, changes)];
- if (installationId) {
- promises.push(bindAlmOrganization({ organization: key, installationId }));
- }
- return Promise.all(promises).then(() => {
- dispatch(actions.updateOrganization(key, changes));
- return organization.key;
- });
- };
-}
-
-function deleteOrganization(key: string) {
- return (dispatch: Dispatch) => {
- return api.deleteOrganization(key).then(() => {
- dispatch(actions.deleteOrganization(key));
- });
- };
-}
-
const mapDispatchToProps = {
createOrganization: createOrganization as any,
deleteOrganization: deleteOrganization as any,
diff --git a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx
index 678f6f246cf..2a4bac7937c 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx
@@ -31,6 +31,7 @@ import { translate } from '../../../helpers/l10n';
type RequiredOrganization = Required<T.OrganizationBase>;
interface Props {
+ infoBlock?: React.ReactNode;
keyReadOnly?: boolean;
onContinue: (organization: T.Organization) => Promise<void>;
organization?: T.Organization;
@@ -133,7 +134,7 @@ export default class OrganizationDetailsForm extends React.PureComponent<Props,
render() {
const { submitting } = this.state;
- const { keyReadOnly } = this.props;
+ const { infoBlock, keyReadOnly } = this.props;
return (
<form id="organization-form" onSubmit={this.handleSubmit}>
{!keyReadOnly && (
@@ -174,6 +175,8 @@ export default class OrganizationDetailsForm extends React.PureComponent<Props,
</div>
</div>
+ {infoBlock}
+
<div className="display-flex-center big-spacer-top">
<SubmitButton disabled={submitting || !this.canSubmit(this.state)}>
{this.props.submitText}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx b/server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx
index 09abb20d910..4c086a58779 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx
@@ -29,7 +29,7 @@ import Select from '../../../components/controls/Select';
import { Alert } from '../../../components/ui/Alert';
import { SubmitButton } from '../../../components/ui/buttons';
import { sanitizeAlmId } from '../../../helpers/almIntegrations';
-import { translate } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import { save } from '../../../helpers/storage';
import { getBaseUrl } from '../../../helpers/urls';
@@ -169,9 +169,9 @@ export class RemoteOrganizationChoose extends React.PureComponent<Props & WithRo
<form className="big-spacer-top big-spacer-bottom" onSubmit={this.handleSubmit}>
<div className="form-field abs-width-400">
<label htmlFor="select-unbound-installation">
- {translate(
- 'onboarding.import_organization.choose_unbound_installation',
- almApplication.key
+ {translateWithParameters(
+ 'onboarding.import_organization.choose_unbound_installation_x',
+ translate(sanitizeAlmId(almApplication.key))
)}
</label>
<Select
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationBind-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationBind-test.tsx
index 8e88a06454f..729c237a4c1 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationBind-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationBind-test.tsx
@@ -21,14 +21,7 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import AutoOrganizationBind from '../AutoOrganizationBind';
import { submit } from '../../../../helpers/testUtils';
-
-const organization = {
- avatar: 'http://example.com/avatar',
- description: 'description-foo',
- key: 'key-foo',
- name: 'name-foo',
- url: 'http://example.com/foo'
-};
+import { mockOrganization } from '../../../../helpers/testMocks';
it('should render correctly', () => {
const onBindOrganization = jest.fn().mockResolvedValue({});
@@ -42,8 +35,9 @@ it('should render correctly', () => {
function shallowRender(props: Partial<AutoOrganizationBind['props']> = {}) {
return shallow(
<AutoOrganizationBind
+ almKey="github"
onBindOrganization={jest.fn()}
- unboundOrganizations={[organization]}
+ unboundOrganizations={[mockOrganization()]}
{...props}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx
index 555004a3f19..8ac2a9e1a07 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx
@@ -20,23 +20,16 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import AutoOrganizationCreate from '../AutoOrganizationCreate';
-import { waitAndUpdate, click } from '../../../../helpers/testUtils';
-import { bindAlmOrganization } from '../../../../api/alm-integration';
import { Step } from '../utils';
+import { bindAlmOrganization } from '../../../../api/alm-integration';
+import { mockAlmOrganization } from '../../../../helpers/testMocks';
+import { waitAndUpdate, click } from '../../../../helpers/testUtils';
jest.mock('../../../../api/alm-integration', () => ({
bindAlmOrganization: jest.fn().mockResolvedValue({})
}));
-const organization = {
- avatar: 'http://example.com/avatar',
- description: 'description-foo',
- key: 'key-foo',
- name: 'name-foo',
- privateRepos: 0,
- publicRepos: 3,
- url: 'http://example.com/foo'
-};
+const organization = mockAlmOrganization();
it('should render prefilled and create org', async () => {
const createOrganization = jest.fn().mockResolvedValue({ key: 'foo' });
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx
index 6db83b6f126..7aec3fc2661 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx
@@ -22,18 +22,10 @@ import { shallow } from 'enzyme';
import AutoPersonalOrganizationBind from '../AutoPersonalOrganizationBind';
import { waitAndUpdate, click } from '../../../../helpers/testUtils';
import { Step } from '../utils';
+import { mockAlmOrganization } from '../../../../helpers/testMocks';
const personalOrg = { key: 'personalorg', name: 'Personal Org' };
-const almOrganization = {
- avatar: 'http://example.com/avatar',
- description: 'description-foo',
- key: 'key-foo',
- name: 'name-foo',
- personal: true,
- privateRepos: 0,
- publicRepos: 3,
- url: 'http://example.com/foo'
-};
+const almOrganization = mockAlmOrganization({ personal: true });
it('should render correctly', async () => {
const updateOrganization = jest.fn().mockResolvedValue({ key: personalOrg.key });
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
index 9330ea3039c..fc80778c403 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
@@ -33,7 +33,9 @@ import { get, remove } from '../../../../helpers/storage';
import {
mockRouter,
mockOrganizationWithAdminActions,
- mockOrganizationWithAlm
+ mockOrganizationWithAlm,
+ mockAlmOrganization,
+ mockCurrentUser
} from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
@@ -77,31 +79,14 @@ jest.mock('../../../../helpers/storage', () => ({
remove: jest.fn()
}));
-const user: T.LoggedInUser = {
- groups: [],
- isLoggedIn: true,
- login: 'luke',
- name: 'Skywalker',
- scmAccounts: []
-};
-
-const fooAlmOrganization = {
- avatar: 'my-avatar',
- key: 'foo',
- name: 'Foo',
- personal: true,
- privateRepos: 0,
- publicRepos: 3
-};
-
-const fooBarAlmOrganization = {
+const user = mockCurrentUser();
+const fooAlmOrganization = mockAlmOrganization({ personal: true });
+const fooBarAlmOrganization = mockAlmOrganization({
avatar: 'https://avatars3.githubusercontent.com/u/37629810?v=4',
key: 'Foo&Bar',
name: 'Foo & Bar',
- personal: true,
- privateRepos: 0,
- publicRepos: 3
-};
+ personal: true
+});
const boundOrganization = { key: 'foobar', name: 'Foo & Bar' };
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx
index de6048af733..5cc0aed0fc5 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx
@@ -22,14 +22,7 @@ import { shallow } from 'enzyme';
import ManualOrganizationCreate from '../ManualOrganizationCreate';
import { waitAndUpdate } from '../../../../helpers/testUtils';
import { Step } from '../utils';
-
-const organization = {
- avatar: 'http://example.com/avatar',
- description: 'description-foo',
- key: 'key-foo',
- name: 'name-foo',
- url: 'http://example.com/foo'
-};
+import { mockOrganization } from '../../../../helpers/testMocks';
it('should render and create organization', async () => {
const createOrganization = jest.fn().mockResolvedValue({ key: 'foo' });
@@ -40,7 +33,7 @@ it('should render and create organization', async () => {
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
- wrapper.find('OrganizationDetailsForm').prop<Function>('onContinue')(organization);
+ wrapper.find('OrganizationDetailsForm').prop<Function>('onContinue')(mockOrganization());
await waitAndUpdate(wrapper);
expect(handleOrgDetailsFinish).toHaveBeenCalled();
wrapper.setProps({ step: Step.Plan });
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx
index 0867e13eccd..fe0b7bda1f5 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx
@@ -21,6 +21,7 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import PlanSelect, { Plan } from '../PlanSelect';
import { click } from '../../../../helpers/testUtils';
+import { mockAlmOrganization } from '../../../../helpers/testMocks';
it('should render and select', () => {
const onChange = jest.fn();
@@ -35,7 +36,7 @@ it('should render and select', () => {
it('should recommend paid plan', () => {
const wrapper = shallowRender({
- almOrganization: { key: 'foo', name: 'Foo', personal: false, privateRepos: 1, publicRepos: 5 },
+ almOrganization: mockAlmOrganization({ privateRepos: 1, publicRepos: 5 }),
plan: Plan.Paid
});
expect(wrapper.find('PaidCardPlan').prop('isRecommended')).toBe(true);
@@ -48,7 +49,7 @@ it('should recommend paid plan', () => {
it('should recommend paid plan and disable free plan', () => {
const wrapper = shallowRender({
- almOrganization: { key: 'foo', name: 'Foo', personal: false, privateRepos: 1, publicRepos: 0 }
+ almOrganization: mockAlmOrganization({ privateRepos: 1, publicRepos: 0 })
});
expect(wrapper.find('PaidCardPlan').prop('isRecommended')).toBe(true);
expect(wrapper.find('FreeCardPlan').prop('disabled')).toBe(true);
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx
index 5edbb21d108..6baad0adb27 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx
@@ -20,8 +20,9 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import PlanStep from '../PlanStep';
-import { waitAndUpdate, submit } from '../../../../helpers/testUtils';
import { Plan } from '../PlanSelect';
+import { mockAlmOrganization } from '../../../../helpers/testMocks';
+import { waitAndUpdate, submit } from '../../../../helpers/testUtils';
jest.mock('../../../../app/components/extensions/utils', () => ({
getExtensionStart: jest.fn().mockResolvedValue(undefined)
@@ -80,14 +81,7 @@ it('should upgrade', async () => {
it('should preselect paid plan', async () => {
const wrapper = shallow(
<PlanStep
- almOrganization={{
- avatar: 'my-avatar',
- key: 'foo',
- name: 'Foo',
- personal: true,
- privateRepos: 5,
- publicRepos: 0
- }}
+ almOrganization={mockAlmOrganization({ personal: true, privateRepos: 5, publicRepos: 0 })}
createOrganization={jest.fn()}
onDone={jest.fn()}
onUpgradeFail={jest.fn()}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx
index d2e35c6371c..7051a49ab55 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx
@@ -20,7 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import { RemoteOrganizationChoose } from '../RemoteOrganizationChoose';
-import { mockRouter } from '../../../../helpers/testMocks';
+import { mockRouter, mockAlmOrganization } from '../../../../helpers/testMocks';
import { submit } from '../../../../helpers/testUtils';
it('should render', () => {
@@ -52,14 +52,7 @@ it('should display already bound alert message', () => {
expect(
shallowRender({
almInstallId: 'foo',
- almOrganization: {
- avatar: 'foo-avatar',
- key: 'foo',
- name: 'Foo',
- personal: false,
- privateRepos: 0,
- publicRepos: 3
- },
+ almOrganization: mockAlmOrganization(),
boundOrganization: { avatar: 'bound-avatar', key: 'bound', name: 'Bound' }
}).find('Alert')
).toMatchSnapshot();
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationBind-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationBind-test.tsx.snap
index 16cca64c656..7c32193e58c 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationBind-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationBind-test.tsx.snap
@@ -7,19 +7,36 @@ exports[`should render correctly 1`] = `
>
<OrganizationSelect
onChange={[Function]}
- organization="key-foo"
+ organization="foo"
organizations={
Array [
Object {
- "avatar": "http://example.com/avatar",
- "description": "description-foo",
- "key": "key-foo",
- "name": "name-foo",
- "url": "http://example.com/foo",
+ "key": "foo",
+ "name": "Foo",
},
]
}
/>
+ <Alert
+ className="abs-width-400 big-spacer-top"
+ display="block"
+ variant="info"
+ >
+ onboarding.import_organization.bind_members_not_sync_info_x.organization.github
+ <Link
+ className="spacer-left"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to={
+ Object {
+ "pathname": "/documentation/organizations/manage-team/",
+ }
+ }
+ >
+ learn_more
+ </Link>
+ </Alert>
<div
className="display-flex-center big-spacer-top"
>
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap
index f268d88ce0f..d16fbc70263 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap
@@ -26,7 +26,7 @@ exports[`should display choice between import or creation 1`] = `
width={16}
/>,
"name": <strong>
- name-foo
+ foo
</strong>,
}
}
@@ -68,10 +68,11 @@ exports[`should display choice between import or creation 1`] = `
}
almOrganization={
Object {
+ "almUrl": "https://github.com/foo",
"avatar": "http://example.com/avatar",
"description": "description-foo",
- "key": "key-foo",
- "name": "name-foo",
+ "key": "foo",
+ "name": "foo",
"personal": false,
"privateRepos": 0,
"publicRepos": 3,
@@ -124,7 +125,7 @@ exports[`should render prefilled and create org 1`] = `
width={16}
/>,
"name": <strong>
- name-foo
+ foo
</strong>,
}
}
@@ -136,13 +137,32 @@ exports[`should render prefilled and create org 1`] = `
</p>
</div>
<OrganizationDetailsForm
+ infoBlock={
+ <Alert
+ className="abs-width-600 big-spacer-top"
+ display="block"
+ variant="info"
+ >
+ <p>
+ onboarding.import_organization.members_sync_info_x.organization.bitbucket.foo.bitbucket
+ </p>
+ <a
+ href="https://github.com/foo/profile/members"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ onboarding.import_organization.see_who_has_access
+ </a>
+ </Alert>
+ }
onContinue={[MockFunction]}
organization={
Object {
+ "almUrl": "https://github.com/foo",
"avatar": "http://example.com/avatar",
"description": "description-foo",
- "key": "key-foo",
- "name": "name-foo",
+ "key": "foo",
+ "name": "foo",
"personal": false,
"privateRepos": 0,
"publicRepos": 3,
@@ -164,10 +184,11 @@ exports[`should render prefilled and create org 1`] = `
}
almOrganization={
Object {
+ "almUrl": "https://github.com/foo",
"avatar": "http://example.com/avatar",
"description": "description-foo",
- "key": "key-foo",
- "name": "name-foo",
+ "key": "foo",
+ "name": "foo",
"personal": false,
"privateRepos": 0,
"publicRepos": 3,
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap
index c19b4cff019..d465e5f0ac8 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap
@@ -23,7 +23,7 @@ exports[`should render correctly 1`] = `
width={16}
/>,
"name": <strong>
- name-foo
+ foo
</strong>,
"personalAvatar": <OrganizationAvatar
organization={
@@ -69,10 +69,11 @@ exports[`should render correctly 1`] = `
}
almOrganization={
Object {
+ "almUrl": "https://github.com/foo",
"avatar": "http://example.com/avatar",
"description": "description-foo",
- "key": "key-foo",
- "name": "name-foo",
+ "key": "foo",
+ "name": "foo",
"personal": true,
"privateRepos": 0,
"publicRepos": 3,
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
index a5739c3fc7c..488d7bb62f3 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
@@ -41,12 +41,15 @@ exports[`should render with auto personal organization bind page 2`] = `
almInstallId="foo"
almOrganization={
Object {
- "avatar": "my-avatar",
+ "almUrl": "https://github.com/foo",
+ "avatar": "http://example.com/avatar",
+ "description": "description-foo",
"key": "foo",
- "name": "Foo",
+ "name": "foo",
"personal": true,
"privateRepos": 0,
"publicRepos": 3,
+ "url": "http://example.com/foo",
}
}
handleCancelImport={[Function]}
@@ -115,7 +118,7 @@ exports[`should render with auto tab displayed 1`] = `
Array [
Object {
"key": "auto",
- "node": "onboarding.import_organization.github",
+ "node": "onboarding.import_organization.import_from_x.github",
},
Object {
"key": "manual",
@@ -202,7 +205,7 @@ exports[`should render with auto tab selected and manual disabled 2`] = `
Array [
Object {
"key": "auto",
- "node": "onboarding.import_organization.github",
+ "node": "onboarding.import_organization.import_from_x.github",
},
Object {
"key": "manual",
@@ -383,7 +386,7 @@ exports[`should render with organization bind page 2`] = `
Array [
Object {
"key": "auto",
- "node": "onboarding.import_organization.github",
+ "node": "onboarding.import_organization.import_from_x.github",
},
Object {
"key": "manual",
@@ -426,12 +429,15 @@ exports[`should render with organization bind page 2`] = `
almInstallId="foo"
almOrganization={
Object {
- "avatar": "my-avatar",
+ "almUrl": "https://github.com/foo",
+ "avatar": "http://example.com/avatar",
+ "description": "description-foo",
"key": "foo",
- "name": "Foo",
+ "name": "foo",
"personal": false,
"privateRepos": 0,
"publicRepos": 3,
+ "url": "http://example.com/foo",
}
}
className=""
@@ -505,7 +511,7 @@ exports[`should switch tabs 1`] = `
Array [
Object {
"key": "auto",
- "node": "onboarding.import_organization.github",
+ "node": "onboarding.import_organization.import_from_x.github",
},
Object {
"key": "manual",
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap
index c9fab655504..43ce253d98d 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap
@@ -46,11 +46,8 @@ exports[`should render and create organization 2`] = `
"calls": Array [
Array [
Object {
- "avatar": "http://example.com/avatar",
- "description": "description-foo",
- "key": "key-foo",
- "name": "name-foo",
- "url": "http://example.com/foo",
+ "key": "foo",
+ "name": "Foo",
},
],
],
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap
index 451e76df78d..f202fc7d3f9 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap
@@ -25,12 +25,15 @@ exports[`should preselect paid plan 1`] = `
<PlanSelect
almOrganization={
Object {
- "avatar": "my-avatar",
+ "almUrl": "https://github.com/foo",
+ "avatar": "http://example.com/avatar",
+ "description": "description-foo",
"key": "foo",
- "name": "Foo",
+ "name": "foo",
"personal": true,
"privateRepos": 5,
"publicRepos": 0,
+ "url": "http://example.com/foo",
}
}
onChange={[Function]}
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap
index 83c6ba9b429..8cd6afa0fd3 100644
--- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap
@@ -31,7 +31,7 @@ exports[`should display already bound alert message 1`] = `
Bound
</strong>,
"name": <strong>
- Foo
+ foo
</strong>,
}
}
@@ -126,7 +126,7 @@ exports[`should display unbound installations 1`] = `
<label
htmlFor="select-unbound-installation"
>
- onboarding.import_organization.choose_unbound_installation.github
+ onboarding.import_organization.choose_unbound_installation_x.github
</label>
<Select
className="input-super-large"
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/actions-test.ts b/server/sonar-web/src/main/js/apps/create/organization/__tests__/actions-test.ts
new file mode 100644
index 00000000000..26800ae126a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/actions-test.ts
@@ -0,0 +1,85 @@
+/*
+ * 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 actions from '../actions';
+import { mockOrganization, mockOrganizationWithAlm } from '../../../../helpers/testMocks';
+import { createOrganization, syncMembers, updateOrganization } from '../../../../api/organizations';
+import { bindAlmOrganization } from '../../../../api/alm-integration';
+
+jest.mock('../../../../api/alm-integration', () => ({
+ bindAlmOrganization: jest.fn().mockResolvedValue({})
+}));
+
+jest.mock('../../../../api/organizations', () => ({
+ createOrganization: jest.fn().mockResolvedValue({ key: 'foo', name: 'Foo' }),
+ updateOrganization: jest.fn().mockResolvedValue({}),
+ syncMembers: jest.fn()
+}));
+
+const dispatch = jest.fn();
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+describe('#createOrganization', () => {
+ it('should create and return an org key', async () => {
+ const org = mockOrganization();
+ const promise = actions.createOrganization(org)(dispatch);
+
+ expect(createOrganization).toHaveBeenCalledWith(org);
+ const returnValue = await promise;
+ expect(dispatch).toHaveBeenCalledWith({ organization: org, type: 'CREATE_ORGANIZATION' });
+ expect(syncMembers).not.toBeCalled();
+ expect(returnValue).toBe(org.key);
+ });
+
+ it('should create and sync members', async () => {
+ const org = mockOrganizationWithAlm({}, { membersSync: true });
+ (createOrganization as jest.Mock).mockResolvedValueOnce(org);
+ const promise = actions.createOrganization(org)(dispatch);
+
+ expect(createOrganization).toHaveBeenCalledWith(org);
+ await promise;
+ expect(syncMembers).toHaveBeenCalledWith(org.key);
+ });
+});
+
+describe('#updateOrganization', () => {
+ it('should update and dispatch', async () => {
+ const org = mockOrganization();
+ const { key, ...changes } = org;
+ const promise = actions.updateOrganization(org)(dispatch);
+
+ expect(updateOrganization).toHaveBeenCalledWith(key, changes);
+ const returnValue = await promise;
+ expect(dispatch).toHaveBeenCalledWith({ changes, key, type: 'UPDATE_ORGANIZATION' });
+ expect(returnValue).toBe(key);
+ });
+
+ it('should update and bind', () => {
+ const org = { ...mockOrganization(), installationId: '1' };
+ const { key, installationId, ...changes } = org;
+ const promise = actions.updateOrganization(org)(dispatch);
+
+ expect(updateOrganization).toHaveBeenCalledWith(key, changes);
+ expect(bindAlmOrganization).toHaveBeenCalledWith({ organization: key, installationId });
+ return promise;
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/create/organization/actions.ts b/server/sonar-web/src/main/js/apps/create/organization/actions.ts
new file mode 100644
index 00000000000..400de6cdf61
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/create/organization/actions.ts
@@ -0,0 +1,51 @@
+/*
+ * 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 { Dispatch } from 'redux';
+import { bindAlmOrganization } from '../../../api/alm-integration';
+import * as api from '../../../api/organizations';
+import * as actions from '../../../store/organizations';
+
+export function createOrganization(organization: T.Organization & { installationId?: string }) {
+ return (dispatch: Dispatch) => {
+ return api
+ .createOrganization({ ...organization, name: organization.name || organization.key })
+ .then((organization: T.Organization) => {
+ dispatch(actions.createOrganization(organization));
+ if (organization.alm && organization.alm.membersSync) {
+ api.syncMembers(organization.key);
+ }
+ return organization.key;
+ });
+ };
+}
+
+export function updateOrganization(organization: T.Organization & { installationId?: string }) {
+ return (dispatch: Dispatch) => {
+ const { key, installationId, ...changes } = organization;
+ const promises = [api.updateOrganization(key, changes)];
+ if (installationId) {
+ promises.push(bindAlmOrganization({ organization: key, installationId }));
+ }
+ return Promise.all(promises).then(() => {
+ dispatch(actions.updateOrganization(key, changes));
+ return organization.key;
+ });
+ };
+}
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 ba7cabf81a0..11a052e4462 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx
@@ -56,7 +56,7 @@ export default function MembersListHeader({
<p>
{translate(
'organization.members.auto_sync_total_help',
- sanitizeAlmId(organization.alm.key) || ''
+ sanitizeAlmId(organization.alm.key)
)}
</p>
{currentUser.personalOrganization !== organization.key && (
@@ -64,12 +64,12 @@ export default function MembersListHeader({
<hr />
<p>
<a
- href={getAlmMembersUrl(organization.alm)}
+ href={getAlmMembersUrl(organization.alm.key, organization.alm.url)}
rel="noopener noreferrer"
target="_blank">
{translateWithParameters(
'organization.members.see_all_members_on_x',
- translate(sanitizeAlmId(organization.alm.key) || '')
+ translate(sanitizeAlmId(organization.alm.key))
)}
</a>
</p>
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 82c68eba3d6..8871a1933de 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx
@@ -84,7 +84,10 @@ export class MembersPageHeader extends React.PureComponent<Props> {
{almKey &&
showSyncNotif && (
<NewInfoBox
- description={translate('organization.members.auto_sync_members_from_org', almKey)}
+ description={translateWithParameters(
+ 'organization.members.auto_sync_members_from_org_x',
+ translate(almKey)
+ )}
onClose={this.handleDismissSyncNotif}
title={translateWithParameters(
'organization.members.auto_sync_with_x',
diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx
index b04c679563f..1a7a5a8400e 100644
--- a/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx
+++ b/server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx
@@ -69,12 +69,9 @@ export class SyncMemberForm extends React.PureComponent<Props, State> {
this.setState({ membersSync: true });
};
- renderModalBody = () => {
- const { membersSync } = this.state;
- const { organization } = this.props;
- const almKey = organization.alm && sanitizeAlmId(organization.alm.key);
+ renderModalDescription = () => {
return (
- <>
+ <p className="spacer-top">
{translate('organization.members.management.description')}
<Link
className="spacer-left"
@@ -82,6 +79,16 @@ export class SyncMemberForm extends React.PureComponent<Props, State> {
to={{ pathname: '/documentation/organizations/manage-team/' }}>
{translate('learn_more')}
</Link>
+ </p>
+ );
+ };
+
+ renderModalBody = () => {
+ const { membersSync } = this.state;
+ const { organization } = this.props;
+ const almKey = organization.alm && sanitizeAlmId(organization.alm.key);
+ return (
+ <>
<div className="display-flex-stretch big-spacer-top">
<RadioCard
onClick={this.handleManualClick}
@@ -110,9 +117,9 @@ export class SyncMemberForm extends React.PureComponent<Props, State> {
{almKey && (
<>
<li className="spacer-bottom">
- {translate(
- 'organization.members.management.automatic.synchronized_from',
- almKey
+ {translateWithParameters(
+ 'organization.members.management.automatic.synchronized_from_x',
+ translate(almKey)
)}
</li>
<li className="spacer-bottom">
@@ -152,6 +159,7 @@ export class SyncMemberForm extends React.PureComponent<Props, State> {
medium={true}
modalBody={this.renderModalBody()}
modalHeader={translate('organization.members.management.title')}
+ modalHeaderDescription={this.renderModalDescription()}
onConfirm={this.handleConfirm}>
{({ onClick }) => (
<Button onClick={onClick}>{translate('organization.members.config_synchro')}</Button>
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 b0bc7e4a009..93646b3ecc1 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
@@ -138,7 +138,7 @@ exports[`should render for bound organization without sync 1`] = `
/>
</div>
<NewInfoBox
- description="organization.members.auto_sync_members_from_org.github"
+ description="organization.members.auto_sync_members_from_org_x.github"
onClose={[Function]}
title="organization.members.auto_sync_with_x.github"
>
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
index b8744db2ef6..c7c59107ab3 100644
--- 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
@@ -8,20 +8,6 @@ exports[`should allow to switch to automatic mode with bitbucket 1`] = `
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"
>
@@ -62,7 +48,7 @@ exports[`should allow to switch to automatic mode with bitbucket 1`] = `
<li
className="spacer-bottom"
>
- organization.members.management.automatic.synchronized_from.bitbucket
+ organization.members.management.automatic.synchronized_from_x.bitbucket
</li>
<li
className="spacer-bottom"
@@ -86,20 +72,10 @@ exports[`should allow to switch to automatic mode with bitbucket 1`] = `
</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>
+ modalHeaderDescription={
+ <p
+ className="spacer-top"
+ >
organization.members.management.description
<Link
className="spacer-left"
@@ -114,6 +90,22 @@ exports[`should allow to switch to automatic mode with github 1`] = `
>
learn_more
</Link>
+ </p>
+ }
+ 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>
<div
className="display-flex-stretch big-spacer-top"
>
@@ -154,7 +146,7 @@ exports[`should allow to switch to automatic mode with github 1`] = `
<li
className="spacer-bottom"
>
- organization.members.management.automatic.synchronized_from.github
+ organization.members.management.automatic.synchronized_from_x.github
</li>
<li
className="spacer-bottom"
@@ -178,20 +170,10 @@ exports[`should allow to switch to automatic mode with github 1`] = `
</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>
+ modalHeaderDescription={
+ <p
+ className="spacer-top"
+ >
organization.members.management.description
<Link
className="spacer-left"
@@ -206,6 +188,22 @@ exports[`should allow to switch to manual mode 1`] = `
>
learn_more
</Link>
+ </p>
+ }
+ 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>
<div
className="display-flex-stretch big-spacer-top"
>
@@ -246,7 +244,7 @@ exports[`should allow to switch to manual mode 1`] = `
<li
className="spacer-bottom"
>
- organization.members.management.automatic.synchronized_from.github
+ organization.members.management.automatic.synchronized_from_x.github
</li>
<li
className="spacer-bottom"
@@ -264,6 +262,26 @@ exports[`should allow to switch to manual mode 1`] = `
</React.Fragment>
}
modalHeader="organization.members.management.title"
+ modalHeaderDescription={
+ <p
+ className="spacer-top"
+ >
+ organization.members.management.description
+ <Link
+ className="spacer-left"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to={
+ Object {
+ "pathname": "/documentation/organizations/manage-team/",
+ }
+ }
+ >
+ learn_more
+ </Link>
+ </p>
+ }
onConfirm={[Function]}
>
<Component />
diff --git a/server/sonar-web/src/main/js/apps/organizations/__tests__/actions-test.ts b/server/sonar-web/src/main/js/apps/organizations/__tests__/actions-test.ts
new file mode 100644
index 00000000000..dcf0b9d3327
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/organizations/__tests__/actions-test.ts
@@ -0,0 +1,56 @@
+/*
+ * 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 actions from '../actions';
+import { mockOrganization } from '../../../helpers/testMocks';
+import { deleteOrganization, updateOrganization } from '../../../api/organizations';
+
+jest.mock('../../../api/organizations', () => ({
+ deleteOrganization: jest.fn().mockResolvedValue({}),
+ updateOrganization: jest.fn().mockResolvedValue({})
+}));
+
+const dispatch = jest.fn();
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+describe('#updateOrganization', () => {
+ it('should update and dispatch', async () => {
+ const org = mockOrganization();
+ const { key, ...changes } = org;
+ const promise = actions.updateOrganization(key, changes)(dispatch);
+
+ expect(updateOrganization).toHaveBeenCalledWith(key, changes);
+ await promise;
+ expect(dispatch).toHaveBeenCalledWith({ changes, key, type: 'UPDATE_ORGANIZATION' });
+ });
+});
+
+describe('#deleteOrganization', () => {
+ it('should delete and dispatch', async () => {
+ const key = 'foo';
+ const promise = actions.deleteOrganization(key)(dispatch);
+
+ expect(deleteOrganization).toHaveBeenCalledWith(key);
+ await promise;
+ expect(dispatch).toHaveBeenCalledWith({ key, type: 'DELETE_ORGANIZATION' });
+ });
+});
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 cda9b50e9e4..701b632f4cd 100644
--- a/server/sonar-web/src/main/js/apps/organizations/actions.ts
+++ b/server/sonar-web/src/main/js/apps/organizations/actions.ts
@@ -21,31 +21,21 @@ import { Dispatch } from 'redux';
import * as api from '../../api/organizations';
import * as actions from '../../store/organizations';
import { addGlobalSuccessMessage } from '../../store/globalMessages';
-import { translate, translateWithParameters } from '../../helpers/l10n';
+import { translate } from '../../helpers/l10n';
-export const createOrganization = (organization: T.OrganizationBase) => (
- dispatch: Dispatch<any>
-) => {
- return api.createOrganization(organization).then((organization: T.Organization) => {
- dispatch(actions.createOrganization(organization));
- dispatch(
- addGlobalSuccessMessage(translateWithParameters('organization.created', organization.name))
- );
- return organization;
- });
-};
+export function updateOrganization(key: string, changes: T.OrganizationBase) {
+ return (dispatch: Dispatch<any>) => {
+ return api.updateOrganization(key, changes).then(() => {
+ dispatch(actions.updateOrganization(key, changes));
+ dispatch(addGlobalSuccessMessage(translate('organization.updated')));
+ });
+ };
+}
-export const updateOrganization = (key: string, changes: T.OrganizationBase) => (
- dispatch: Dispatch<any>
-) => {
- return api.updateOrganization(key, changes).then(() => {
- dispatch(actions.updateOrganization(key, changes));
- dispatch(addGlobalSuccessMessage(translate('organization.updated')));
- });
-};
-
-export const deleteOrganization = (key: string) => (dispatch: Dispatch<any>) => {
- return api.deleteOrganization(key).then(() => {
- dispatch(actions.deleteOrganization(key));
- });
-};
+export function deleteOrganization(key: string) {
+ return (dispatch: Dispatch<any>) => {
+ return api.deleteOrganization(key).then(() => {
+ dispatch(actions.deleteOrganization(key));
+ });
+ };
+}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.tsx
index 59a80c25ce7..4310bb215a9 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.tsx
@@ -44,6 +44,9 @@ export class OrganizationEmpty extends React.PureComponent<Props> {
};
render() {
+ const { organization } = this.props;
+ const memberSyncActivated = organization.alm && organization.alm.membersSync;
+
return (
<div className="organization-empty">
<h3 className="text-center">{translate('onboarding.create_organization.ready')}</h3>
@@ -54,12 +57,14 @@ export class OrganizationEmpty extends React.PureComponent<Props> {
{translate('provisioning.analyze_new_project')}
</h6>
</Button>
- <Button className="onboarding-choice" onClick={this.handleAddMembersClick}>
- <OnboardingAddMembersIcon />
- <h6 className="onboarding-choice-name">
- {translate('organization.members.add.multiple')}
- </h6>
- </Button>
+ {!memberSyncActivated && (
+ <Button className="onboarding-choice" onClick={this.handleAddMembersClick}>
+ <OnboardingAddMembersIcon />
+ <h6 className="onboarding-choice-name">
+ {translate('organization.members.add.multiple')}
+ </h6>
+ </Button>
+ )}
</div>
</div>
);
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEmpty-test.tsx b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEmpty-test.tsx
index 775ffecb1f0..43a8c00e93f 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEmpty-test.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEmpty-test.tsx
@@ -21,43 +21,46 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import { OrganizationEmpty } from '../OrganizationEmpty';
import { click } from '../../../../helpers/testUtils';
+import {
+ mockRouter,
+ mockOrganization,
+ mockOrganizationWithAlm
+} from '../../../../helpers/testMocks';
-const organization: T.Organization = { key: 'foo', name: 'Foo' };
+const organization: T.Organization = mockOrganization();
it('should render', () => {
- expect(
- shallow(
- <OrganizationEmpty
- openProjectOnboarding={jest.fn()}
- organization={organization}
- router={{ push: jest.fn() }}
- />
- )
- ).toMatchSnapshot();
+ expect(shallowRender()).toMatchSnapshot();
});
it('should create new project', () => {
const openProjectOnboarding = jest.fn();
- const wrapper = shallow(
- <OrganizationEmpty
- openProjectOnboarding={openProjectOnboarding}
- organization={organization}
- router={{ push: jest.fn() }}
- />
- );
+ const wrapper = shallowRender({ openProjectOnboarding });
+
click(wrapper.find('Button').first());
expect(openProjectOnboarding).toBeCalledWith({ key: 'foo', name: 'Foo' });
});
it('should add members', () => {
- const router = { push: jest.fn() };
- const wrapper = shallow(
+ const push = jest.fn();
+ const wrapper = shallowRender({ router: mockRouter({ push }) });
+ click(wrapper.find('Button').last());
+ expect(push).toBeCalledWith('/organizations/foo/members');
+});
+
+it('should hide add members button when member sync activated', () => {
+ expect(
+ shallowRender({ organization: mockOrganizationWithAlm({}, { membersSync: true }) })
+ ).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<OrganizationEmpty['props']> = {}) {
+ return shallow(
<OrganizationEmpty
openProjectOnboarding={jest.fn()}
organization={organization}
- router={router}
+ router={mockRouter()}
+ {...props}
/>
);
- click(wrapper.find('Button').last());
- expect(router.push).toBeCalledWith('/organizations/foo/members');
-});
+}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEmpty-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEmpty-test.tsx.snap
index 32086688e74..d58fec02a6c 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEmpty-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEmpty-test.tsx.snap
@@ -1,5 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`should hide add members button when member sync activated 1`] = `
+<div
+ className="organization-empty"
+>
+ <h3
+ className="text-center"
+ >
+ onboarding.create_organization.ready
+ </h3>
+ <div
+ className="onboarding-choices"
+ >
+ <Button
+ className="onboarding-choice"
+ onClick={[Function]}
+ >
+ <OnboardingProjectIcon
+ className="big-spacer-bottom"
+ />
+ <h6
+ className="onboarding-choice-name"
+ >
+ provisioning.analyze_new_project
+ </h6>
+ </Button>
+ </div>
+</div>
+`;
+
exports[`should render 1`] = `
<div
className="organization-empty"
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx
index bb2d34a08b2..adf205bd0ea 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx
@@ -22,7 +22,6 @@ import { connect } from 'react-redux';
import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
import Modal from '../../../components/controls/Modal';
import OnboardingProjectIcon from '../../../components/icons-components/OnboardingProjectIcon';
-import OnboardingTeamIcon from '../../../components/icons-components/OnboardingTeamIcon';
import { Button, ResetButtonLink } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';
import { getCurrentUser, Store } from '../../../store/rootReducer';
@@ -32,7 +31,6 @@ import '../styles.css';
interface OwnProps {
onClose: () => void;
onOpenProjectOnboarding: () => void;
- onOpenTeamOnboarding: () => void;
}
interface StateProps {
@@ -59,29 +57,22 @@ export class OnboardingModal extends React.PureComponent<Props> {
contentLabel={header}
medium={true}
onRequestClose={this.props.onClose}
- shouldCloseOnOverlayClick={false}
- simple={true}>
- <div className="modal-simple-head text-center">
- <h1>{translate('onboarding.header')}</h1>
+ shouldCloseOnOverlayClick={false}>
+ <div className="modal-head">
+ <h2>{translate('onboarding.header')}</h2>
<p className="spacer-top">{translate('onboarding.header.description')}</p>
</div>
- <div className="modal-simple-body text-center onboarding-choices">
- <Button className="onboarding-choice" onClick={this.props.onOpenProjectOnboarding}>
- <OnboardingProjectIcon className="big-spacer-bottom" />
- <h6 className="onboarding-choice-name">{translate('onboarding.analyze_your_code')}</h6>
- </Button>
- <Button className="onboarding-choice" onClick={this.props.onOpenTeamOnboarding}>
- <OnboardingTeamIcon className="big-spacer-bottom" />
- <h6 className="onboarding-choice-name">
- {translate('onboarding.contribute_existing_project')}
- </h6>
+ <div className="modal-body text-center huge-spacer-top huge-spacer-bottom">
+ <OnboardingProjectIcon className="big-spacer-bottom" />
+ <h6 className="onboarding-choice-name big-spacer-bottom">
+ {translate('onboarding.analyze_your_code')}
+ </h6>
+ <Button onClick={this.props.onOpenProjectOnboarding}>
+ {translate('onboarding.project.create')}
</Button>
</div>
- <div className="modal-simple-foot text-center">
- <ResetButtonLink className="spacer-bottom" onClick={this.props.onClose}>
- {translate('not_now')}
- </ResetButtonLink>
- <p className="note">{translate('onboarding.footer')}</p>
+ <div className="modal-foot text-right">
+ <ResetButtonLink onClick={this.props.onClose}>{translate('not_now')}</ResetButtonLink>
</div>
</Modal>
);
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx
index 49092c14189..9533287403b 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx
@@ -22,7 +22,6 @@ import { connect } from 'react-redux';
import { InjectedRouter } from 'react-router';
import OnboardingModal from './OnboardingModal';
import { skipOnboarding } from '../../../store/users';
-import TeamOnboardingModal from '../teamOnboarding/TeamOnboardingModal';
import { OnboardingContext } from '../../../app/components/OnboardingContext';
interface DispatchProps {
@@ -33,46 +32,34 @@ interface OwnProps {
router: InjectedRouter;
}
-enum ModalKey {
- onboarding,
- teamOnboarding
-}
-
interface State {
- modal?: ModalKey;
+ open: boolean;
}
export class OnboardingPage extends React.PureComponent<OwnProps & DispatchProps, State> {
- state: State = { modal: ModalKey.onboarding };
+ state: State = { open: false };
closeOnboarding = () => {
this.props.skipOnboarding();
this.props.router.replace('/');
};
- openTeamOnboarding = () => {
- this.setState({ modal: ModalKey.teamOnboarding });
- };
-
render() {
- const { modal } = this.state;
+ const { open } = this.state;
+
+ if (!open) {
+ return null;
+ }
+
return (
- <>
- {modal === ModalKey.onboarding && (
- <OnboardingContext.Consumer>
- {openProjectOnboarding => (
- <OnboardingModal
- onClose={this.closeOnboarding}
- onOpenProjectOnboarding={openProjectOnboarding}
- onOpenTeamOnboarding={this.openTeamOnboarding}
- />
- )}
- </OnboardingContext.Consumer>
- )}
- {modal === ModalKey.teamOnboarding && (
- <TeamOnboardingModal onFinish={this.closeOnboarding} />
+ <OnboardingContext.Consumer>
+ {openProjectOnboarding => (
+ <OnboardingModal
+ onClose={this.closeOnboarding}
+ onOpenProjectOnboarding={openProjectOnboarding}
+ />
)}
- </>
+ </OnboardingContext.Consumer>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx
index 48559e4ab7d..7ae1c13dec6 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx
@@ -29,7 +29,6 @@ it('renders correctly', () => {
currentUser={{ isLoggedIn: true }}
onClose={jest.fn()}
onOpenProjectOnboarding={jest.fn()}
- onOpenTeamOnboarding={jest.fn()}
/>
)
).toMatchSnapshot();
@@ -38,13 +37,11 @@ it('renders correctly', () => {
it('should correctly open the different tutorials', () => {
const onClose = jest.fn();
const onOpenProjectOnboarding = jest.fn();
- const onOpenTeamOnboarding = jest.fn();
const wrapper = shallow(
<OnboardingModal
currentUser={{ isLoggedIn: true }}
onClose={onClose}
onOpenProjectOnboarding={onOpenProjectOnboarding}
- onOpenTeamOnboarding={onOpenTeamOnboarding}
/>
);
@@ -53,5 +50,4 @@ it('should correctly open the different tutorials', () => {
wrapper.find('Button').forEach(button => click(button));
expect(onOpenProjectOnboarding).toHaveBeenCalled();
- expect(onOpenTeamOnboarding).toHaveBeenCalled();
});
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap
index 23ded61ecb6..daf332ad6a2 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap
@@ -6,14 +6,13 @@ exports[`renders correctly 1`] = `
medium={true}
onRequestClose={[MockFunction]}
shouldCloseOnOverlayClick={false}
- simple={true}
>
<div
- className="modal-simple-head text-center"
+ className="modal-head"
>
- <h1>
+ <h2>
onboarding.header
- </h1>
+ </h2>
<p
className="spacer-top"
>
@@ -21,49 +20,30 @@ exports[`renders correctly 1`] = `
</p>
</div>
<div
- className="modal-simple-body text-center onboarding-choices"
+ className="modal-body text-center huge-spacer-top huge-spacer-bottom"
>
- <Button
- className="onboarding-choice"
- onClick={[MockFunction]}
+ <OnboardingProjectIcon
+ className="big-spacer-bottom"
+ />
+ <h6
+ className="onboarding-choice-name big-spacer-bottom"
>
- <OnboardingProjectIcon
- className="big-spacer-bottom"
- />
- <h6
- className="onboarding-choice-name"
- >
- onboarding.analyze_your_code
- </h6>
- </Button>
+ onboarding.analyze_your_code
+ </h6>
<Button
- className="onboarding-choice"
onClick={[MockFunction]}
>
- <OnboardingTeamIcon
- className="big-spacer-bottom"
- />
- <h6
- className="onboarding-choice-name"
- >
- onboarding.contribute_existing_project
- </h6>
+ onboarding.project.create
</Button>
</div>
<div
- className="modal-simple-foot text-center"
+ className="modal-foot text-right"
>
<ResetButtonLink
- className="spacer-bottom"
onClick={[MockFunction]}
>
not_now
</ResetButtonLink>
- <p
- className="note"
- >
- onboarding.footer
- </p>
</div>
</Modal>
`;
diff --git a/server/sonar-web/src/main/js/apps/tutorials/styles.css b/server/sonar-web/src/main/js/apps/tutorials/styles.css
index f40e4834b0e..41cf98f083d 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/styles.css
+++ b/server/sonar-web/src/main/js/apps/tutorials/styles.css
@@ -85,7 +85,6 @@
}
.onboarding-choice-name {
- padding-top: var(--gridSize);
color: inherit;
font-size: var(--mediumFontSize);
}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/TeamOnboardingModal.tsx b/server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/TeamOnboardingModal.tsx
deleted file mode 100644
index 36945c53f97..00000000000
--- a/server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/TeamOnboardingModal.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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 { FormattedMessage } from 'react-intl';
-import { Link } from 'react-router';
-import Modal from '../../../components/controls/Modal';
-import { translate } from '../../../helpers/l10n';
-import { ResetButtonLink } from '../../../components/ui/buttons';
-import { Alert } from '../../../components/ui/Alert';
-
-interface Props {
- onFinish: () => void;
-}
-
-export default class TeamOnboardingModal extends React.PureComponent<Props> {
- render() {
- const header = translate('onboarding.team.header');
- return (
- <Modal
- contentLabel={header}
- medium={true}
- onRequestClose={this.props.onFinish}
- shouldCloseOnOverlayClick={false}>
- <header className="modal-head">
- <h2>{header}</h2>
- </header>
- <div className="modal-body">
- <Alert variant="info">{translate('onboarding.team.work_in_progress')}</Alert>
- <p className="spacer-top big-spacer-bottom">{translate('onboarding.team.first_step')}</p>
- <p className="spacer-top big-spacer-bottom">
- <FormattedMessage
- defaultMessage={translate('onboarding.team.how_to_join')}
- id="onboarding.team.how_to_join"
- values={{
- link: (
- <Link
- onClick={this.props.onFinish}
- to="/documentation/organizations/manage-team/">
- {translate('as_explained_here')}
- </Link>
- )
- }}
- />
- </p>
- </div>
- <footer className="modal-foot">
- <ResetButtonLink onClick={this.props.onFinish}>{translate('close')}</ResetButtonLink>
- </footer>
- </Modal>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/__tests__/__snapshots__/TeamOnboardingModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/__tests__/__snapshots__/TeamOnboardingModal-test.tsx.snap
deleted file mode 100644
index b9217d73929..00000000000
--- a/server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/__tests__/__snapshots__/TeamOnboardingModal-test.tsx.snap
+++ /dev/null
@@ -1,61 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders correctly 1`] = `
-<Modal
- contentLabel="onboarding.team.header"
- medium={true}
- onRequestClose={[MockFunction]}
- shouldCloseOnOverlayClick={false}
->
- <header
- className="modal-head"
- >
- <h2>
- onboarding.team.header
- </h2>
- </header>
- <div
- className="modal-body"
- >
- <Alert
- variant="info"
- >
- onboarding.team.work_in_progress
- </Alert>
- <p
- className="spacer-top big-spacer-bottom"
- >
- onboarding.team.first_step
- </p>
- <p
- className="spacer-top big-spacer-bottom"
- >
- <FormattedMessage
- defaultMessage="onboarding.team.how_to_join"
- id="onboarding.team.how_to_join"
- values={
- Object {
- "link": <Link
- onClick={[MockFunction]}
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/documentation/organizations/manage-team/"
- >
- as_explained_here
- </Link>,
- }
- }
- />
- </p>
- </div>
- <footer
- className="modal-foot"
- >
- <ResetButtonLink
- onClick={[MockFunction]}
- >
- close
- </ResetButtonLink>
- </footer>
-</Modal>
-`;
diff --git a/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
index 0718757ff1a..984ff40dc6a 100644
--- a/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
+++ b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
@@ -25,6 +25,7 @@ interface Props<T> extends ConfirmModalProps<T> {
children: (props: ChildrenProps) => React.ReactNode;
modalBody: React.ReactNode;
modalHeader: string;
+ modalHeaderDescription?: React.ReactNode;
}
interface State {
@@ -33,9 +34,19 @@ interface State {
export default class ConfirmButton<T> extends React.PureComponent<Props<T>, State> {
renderConfirmModal = ({ onClose }: ModalProps) => {
- const { children, modalBody, modalHeader, ...confirmModalProps } = this.props;
+ const {
+ children,
+ modalBody,
+ modalHeader,
+ modalHeaderDescription,
+ ...confirmModalProps
+ } = this.props;
return (
- <ConfirmModal header={modalHeader} onClose={onClose} {...confirmModalProps}>
+ <ConfirmModal
+ header={modalHeader}
+ headerDescription={modalHeaderDescription}
+ onClose={onClose}
+ {...confirmModalProps}>
{modalBody}
</ConfirmModal>
);
diff --git a/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx b/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx
index f51f9704aec..a86ac592a73 100644
--- a/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx
+++ b/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx
@@ -35,6 +35,7 @@ export interface ConfirmModalProps<T> extends ModalProps {
interface Props<T> extends ConfirmModalProps<T> {
header: string;
+ headerDescription?: React.ReactNode;
onClose: () => void;
}
@@ -60,12 +61,20 @@ export default class ConfirmModal<T = string> extends React.PureComponent<Props<
};
renderModalContent = ({ onCloseClick, onFormSubmit, submitting }: ChildrenProps) => {
- const { children, confirmButtonText, confirmDisable, header, isDestructive } = this.props;
- const { cancelButtonText = translate('cancel') } = this.props;
+ const {
+ children,
+ confirmButtonText,
+ confirmDisable,
+ header,
+ headerDescription,
+ isDestructive,
+ cancelButtonText = translate('cancel')
+ } = this.props;
return (
<form onSubmit={onFormSubmit}>
<header className="modal-head">
<h2>{header}</h2>
+ {headerDescription}
</header>
<div className="modal-body">{children}</div>
<footer className="modal-foot">
@@ -85,8 +94,8 @@ export default class ConfirmModal<T = string> extends React.PureComponent<Props<
};
render() {
- const { header, onClose, medium, noBackdrop, large, simple } = this.props;
- const modalProps = { header, onClose, medium, noBackdrop, large, simple };
+ const { header, onClose, medium, noBackdrop, large } = this.props;
+ const modalProps = { header, onClose, medium, noBackdrop, large };
return (
<SimpleModal onSubmit={this.handleSubmit} {...modalProps}>
{this.renderModalContent}
diff --git a/server/sonar-web/src/main/js/components/controls/Modal.tsx b/server/sonar-web/src/main/js/components/controls/Modal.tsx
index e7b14c3a1df..e3beac1109b 100644
--- a/server/sonar-web/src/main/js/components/controls/Modal.tsx
+++ b/server/sonar-web/src/main/js/components/controls/Modal.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import * as ReactModal from 'react-modal';
import * as classNames from 'classnames';
+import { isSonarCloud } from '../../helpers/system';
ReactModal.setAppElement('#content');
@@ -28,7 +29,6 @@ export interface ModalProps {
medium?: boolean;
noBackdrop?: boolean;
large?: boolean;
- simple?: true;
}
type MandatoryProps = Pick<ReactModal.Props, 'contentLabel'>;
@@ -38,11 +38,16 @@ type Props = Partial<ReactModal.Props> & MandatoryProps & ModalProps;
export default function Modal(props: Props) {
return (
<ReactModal
- className={classNames('modal', {
- 'modal-medium': props.medium,
- 'modal-large': props.large,
- 'modal-simple': props.simple
- })}
+ className={classNames(
+ 'modal',
+ {
+ sonarcloud: isSonarCloud()
+ },
+ {
+ 'modal-medium': props.medium,
+ 'modal-large': props.large
+ }
+ )}
isOpen={true}
overlayClassName={classNames('modal-overlay', { 'modal-no-backdrop': props.noBackdrop })}
{...props}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/__tests__/TeamOnboardingModal-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/ConfirmButton-test.tsx
index b20c75e1a5b..2bfe5cf21b4 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/__tests__/TeamOnboardingModal-test.tsx
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/ConfirmButton-test.tsx
@@ -19,8 +19,28 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import TeamOnboardingModal from '../TeamOnboardingModal';
+import ConfirmButton from '../ConfirmButton';
-it('renders correctly', () => {
- expect(shallow(<TeamOnboardingModal onFinish={jest.fn()} />)).toMatchSnapshot();
+it('should display a modal button', () => {
+ expect(shallowRender()).toMatchSnapshot();
});
+
+it('should display a confirm modal', () => {
+ expect(
+ shallowRender()
+ .find('ModalButton')
+ .prop<Function>('modal')({ onClose: jest.fn() })
+ ).toMatchSnapshot();
+});
+
+function shallowRender() {
+ return shallow(
+ <ConfirmButton
+ confirmButtonText="submit"
+ modalBody={<div />}
+ modalHeader="title"
+ onConfirm={jest.fn()}>
+ {() => 'Confirm button'}
+ </ConfirmButton>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ConfirmButton-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ConfirmButton-test.tsx.snap
new file mode 100644
index 00000000000..f94c3829503
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ConfirmButton-test.tsx.snap
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display a confirm modal 1`] = `
+<ConfirmModal
+ confirmButtonText="submit"
+ header="title"
+ onClose={[MockFunction]}
+ onConfirm={[MockFunction]}
+>
+ <div />
+</ConfirmModal>
+`;
+
+exports[`should display a modal button 1`] = `
+<ModalButton
+ modal={[Function]}
+>
+ <Component />
+</ModalButton>
+`;
diff --git a/server/sonar-web/src/main/js/components/icons-components/OnboardingProjectIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/OnboardingProjectIcon.tsx
index 8fb1edf014c..31c0a27fbde 100644
--- a/server/sonar-web/src/main/js/components/icons-components/OnboardingProjectIcon.tsx
+++ b/server/sonar-web/src/main/js/components/icons-components/OnboardingProjectIcon.tsx
@@ -19,10 +19,11 @@
*/
import * as React from 'react';
import Icon, { IconProps } from './Icon';
+import * as theme from '../../app/theme';
export default function OnboardingProjectIcon({
className,
- fill = 'currentColor',
+ fill = theme.darkBlue,
size
}: IconProps) {
return (
diff --git a/server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx
deleted file mode 100644
index a362c767ffb..00000000000
--- a/server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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 Icon, { IconProps } from './Icon';
-
-export default function OnboardingTeamIcon({ className, fill = 'currentColor', size }: IconProps) {
- return (
- <Icon className={className} size={size || 64} viewBox="0 0 64 64">
- <g fill="none" fillRule="evenodd" stroke={fill} strokeWidth="2">
- <path d="M32 9v5M11.5195 43.0898l7.48-4.091m33.481-18.0994l-7.48 4.1m-33.481-4.1l7.48 4.1M45 38.999l7.48 4.101M32 50v5m15-23c0 8.284-6.715 15-15 15s-15-6.716-15-15c0-8.285 6.715-15 15-15s15 6.715 15 15z" />
- <path d="M40 38c0 1.656-3.58 2-8 2s-8-.344-8-2m16 0v-3l-5-3-1-1m-10 7v-3l5-3 1-1m6-4c0 2.2-1.8 4-4 4s-4-1.8-4-4v-1c0-2.2 1.8-4 4-4s4 1.8 4 4v1zm-.0098-21.71c7.18 1.069 13.439 4.96 17.609 10.51m-17.609 42.91c7.18-1.07 13.439-4.96 17.609-10.51M6.6299 41.25c-1.06-2.88-1.63-6-1.63-9.25s.57-6.37 1.63-9.25m3.7705-6.9502c4.17-5.55 10.43-9.44 17.609-10.51m-17.609 42.9104c4.17 5.55 10.43 9.439 17.609 10.51M57.3701 22.75c1.06 2.88 1.63 6 1.63 9.25s-.57 6.37-1.63 9.25" />
- <path d="M36 5c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M12 19c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2m51 0c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M12 45c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2m51 0c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M36 59c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2" />
- </g>
- </Icon>
- );
-}
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts
index 877de0ca6c8..b2a09af3ab0 100644
--- a/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts
+++ b/server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts
@@ -27,12 +27,12 @@ import {
} from '../almIntegrations';
it('#getAlmMembersUrl', () => {
- expect(
- getAlmMembersUrl({ key: 'github', membersSync: true, url: 'https://github.com/Foo' })
- ).toBe('https://github.com/orgs/Foo/people');
- expect(
- getAlmMembersUrl({ key: 'bitbucket', membersSync: true, url: 'https://bitbucket.com/Foo/' })
- ).toBe('https://bitbucket.com/Foo/profile/members');
+ expect(getAlmMembersUrl('github', 'https://github.com/Foo')).toBe(
+ 'https://github.com/orgs/Foo/people'
+ );
+ expect(getAlmMembersUrl('bitbucket', 'https://bitbucket.com/Foo/')).toBe(
+ 'https://bitbucket.com/Foo/profile/members'
+ );
});
it('#isBitbucket', () => {
@@ -52,12 +52,16 @@ it('#isVSTS', () => {
});
it('#isPersonal', () => {
- expect(
- isPersonal({ key: 'foo', name: 'Foo', personal: true, privateRepos: 0, publicRepos: 3 })
- ).toBeTruthy();
- expect(
- isPersonal({ key: 'foo', name: 'Foo', personal: false, privateRepos: 0, publicRepos: 3 })
- ).toBeFalsy();
+ const almOrg = {
+ almUrl: '',
+ key: 'foo',
+ name: 'Foo',
+ personal: true,
+ privateRepos: 0,
+ publicRepos: 3
+ };
+ expect(isPersonal(almOrg)).toBeTruthy();
+ expect(isPersonal({ ...almOrg, personal: false })).toBeFalsy();
});
it('#sanitizeAlmId', () => {
diff --git a/server/sonar-web/src/main/js/helpers/almIntegrations.ts b/server/sonar-web/src/main/js/helpers/almIntegrations.ts
index ebeccc800f4..c3cfc0744a3 100644
--- a/server/sonar-web/src/main/js/helpers/almIntegrations.ts
+++ b/server/sonar-web/src/main/js/helpers/almIntegrations.ts
@@ -19,7 +19,7 @@
*/
import { isLoggedIn } from './users';
-export function getAlmMembersUrl({ key, url }: T.OrganizationAlm): string {
+export function getAlmMembersUrl(key: string, url: string): string {
if (!url.endsWith('/')) {
url += '/';
}
@@ -51,7 +51,7 @@ export function isPersonal(organization?: T.AlmOrganization) {
return Boolean(organization && organization.personal);
}
-export function sanitizeAlmId(almKey?: string) {
+export function sanitizeAlmId(almKey: string) {
if (isBitbucket(almKey)) {
return 'bitbucket';
}
diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts
index 9f7ec979805..bd37528c205 100644
--- a/server/sonar-web/src/main/js/helpers/testMocks.ts
+++ b/server/sonar-web/src/main/js/helpers/testMocks.ts
@@ -21,6 +21,21 @@ import { InjectedRouter } from 'react-router';
import { Location } from 'history';
import { Profile } from '../apps/quality-profiles/types';
+export function mockAlmOrganization(overrides: Partial<T.AlmOrganization> = {}): T.AlmOrganization {
+ return {
+ avatar: 'http://example.com/avatar',
+ almUrl: 'https://github.com/foo',
+ description: 'description-foo',
+ key: 'foo',
+ name: 'foo',
+ personal: false,
+ privateRepos: 0,
+ publicRepos: 3,
+ url: 'http://example.com/foo',
+ ...overrides
+ };
+}
+
export function mockAppState(overrides: Partial<T.AppState> = {}): T.AppState {
return {
defaultOrganization: 'foo',
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index cbbb966a3a1..ac74be9ca5b 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -123,6 +123,8 @@ off=Off
on=On
or=Or
organization_key=Organization Key
+organization.bitbucket=Bitbucket team
+organization.github=GitHub organization
open=Open
optional=Optional
order=Order
@@ -2677,8 +2679,7 @@ organization.members.manage_a_team=Manage a team
organization.members.add_to_members=Add to members
organization.members.config_synchro=Configure Synchronization
organization.members.auto_sync_with_x=Automatic sync with {0}
-organization.members.auto_sync_members_from_org.bitbucket=Members can be synchronized automatically from your Bitbucket team
-organization.members.auto_sync_members_from_org.github=Members can be synchronized automatically from your GitHub organization
+organization.members.auto_sync_members_from_org_x=Members can be synchronized automatically from your {0}
organization.members.auto_sync_total_help.bitbucket=You might not see all members from your Bitbucket team yet, as they need to reconnect to SonarCloud to be members of the organization.
organization.members.auto_sync_total_help.github=You might not see all members from your GitHub organization yet, as they need to connect to SonarCloud at least once to appear in this list.
organization.members.see_all_members_on_x=See all members on {0}
@@ -2688,8 +2689,7 @@ organization.members.management.manual=Manual
organization.members.management.manual.add_members_manually=Admin add members manually from Sonarcloud existing users
organization.members.management.manual.choose_members_permissions=Admin chooses each member permissions
organization.members.management.automatic=Automatic sync with {0}
-organization.members.management.automatic.synchronized_from.bitbucket=Members are synchronized automatically from your Bitbucket team
-organization.members.management.automatic.synchronized_from.github=Members are synchronized automatically from your GitHub organization
+organization.members.management.automatic.synchronized_from_x=Members are synchronized automatically from your {0}
organization.members.management.automatic.members_changes_reflected.bitbucket=Your team members must reconnect to SonarCloud to be automatically added to correct SonarCloud organization
organization.members.management.automatic.members_changes_reflected.github=If you add or remove a member on GitHub, SonarCloud immediately reflect the changes
organization.members.management.automatic.still_choose_members_permissions=Admin still manages permissions for each member in SonarCloud
@@ -2750,6 +2750,7 @@ onboarding.footer=Don't worry you can do all of this later. Just click the "+" i
onboarding.project.header=Analyze a project
onboarding.project.header.description=Want to quickly analyze a first project? Follow these {0} easy steps.
+onboarding.project.create=Create a new project
onboarding.project_analysis.header=Analyze your project
onboarding.project_analysis.description=We initialized your project on {instance}, now it's up to you to launch analyses!
@@ -2819,8 +2820,7 @@ onboarding.create_organization.enter_your_coupon=Enter your coupon
onboarding.create_organization.create_and_upgrade=Create Organization and Upgrade
onboarding.create_organization.ready=All set! Your organization is now ready to go
onboarding.import_organization.bind=Bind Organization
-onboarding.import_organization.choose_unbound_installation.bitbucket=Choose one of your Bitbucket teams that already have the SonarCloud application installed:
-onboarding.import_organization.choose_unbound_installation.github=Choose one of your GitHub organizations that already have the SonarCloud application installed:
+onboarding.import_organization.choose_unbound_installation_x=Choose one of your {0} that already have the SonarCloud application installed:
onboarding.import_organization.import=Import Organization
onboarding.import_organization.import_org_details=Import organization details
onboarding.import_organization.org_not_found=We were not able to find the requested organization, here are a few tips to help you troubleshoot the issue:
@@ -2833,11 +2833,13 @@ onboarding.import_organization.installing=Finalize installation of the {0} appli
onboarding.import_organization.personal.page.header=Bind to your personal organization
onboarding.import_organization.personal.import_org_details=Import personal organization details
onboarding.import_organization.private.disabled=Selecting private repository is not available yet and will come soon. Meanwhile, you need to create the project manually.
-onboarding.import_organization.bitbucket=Import from Bitbucket teams
-onboarding.import_organization.github=Import from GitHub organizations
+onboarding.import_organization.import_from_x=Import from {0}
onboarding.import_organization.bind_existing=Bind to an existing SonarCloud organization
onboarding.import_organization.create_new=Create new SonarCloud organization from it
onboarding.import_organization.already_bound_x=Your organization {avatar} {name} is already bound to the SonarCloud organization {boundAvatar} {boundName}. Try again and choose a different organization.
+onboarding.import_organization.members_sync_info_x=All members from your {0} {1} will be added to your SonarCloud organization. As they connect to SonarCloud with their {2} account, members will automatically have access to your SonarCloud organization and its projects.
+onboarding.import_organization.bind_members_not_sync_info_x=We'll keep your members, groups and permissions as they are today on SonarCloud. To sync your members with your {0}, enable members sync in your Members tab.
+onboarding.import_organization.see_who_has_access=See who will have access
onboarding.import_organization_x=Import {avatar} {name} into a SonarCloud organization
onboarding.import_personal_organization_x=Bind {avatar} {name} with your personal SonarCloud organization {personalAvatar} {personalName}