Browse Source

SONARCLOUD-380 Trigger member sync when importing ALM org

 - Remove team button and refactor modals
 - Sync members after importing alm organization
 - Update AlmOrganization type and introduce mock function
 - Add info box when importing or binding organization
 - Remove manage team button in bound org empty state if member sync
tags/7.7
Jeremy Davis 5 years ago
parent
commit
980cb9cb68
58 changed files with 747 additions and 638 deletions
  1. 1
    13
      server/sonar-web/src/main/js/app/components/StartupModal.tsx
  2. 37
    40
      server/sonar-web/src/main/js/app/styles/components/modals.css
  3. 4
    0
      server/sonar-web/src/main/js/app/styles/init/misc.css
  4. 2
    7
      server/sonar-web/src/main/js/app/types.d.ts
  5. 1
    1
      server/sonar-web/src/main/js/apps/create/components/BillingFormShim.tsx
  6. 6
    7
      server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx
  7. 1
    2
      server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationModal-test.tsx.snap
  8. 3
    3
      server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx
  9. 17
    2
      server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationBind.tsx
  10. 23
    2
      server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
  11. 12
    39
      server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
  12. 4
    1
      server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx
  13. 4
    4
      server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx
  14. 3
    9
      server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationBind-test.tsx
  15. 4
    11
      server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx
  16. 2
    10
      server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx
  17. 8
    23
      server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
  18. 2
    9
      server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx
  19. 3
    2
      server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx
  20. 3
    9
      server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx
  21. 2
    9
      server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx
  22. 23
    6
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationBind-test.tsx.snap
  23. 29
    8
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap
  24. 4
    3
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap
  25. 14
    8
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
  26. 2
    5
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap
  27. 5
    2
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap
  28. 2
    2
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap
  29. 85
    0
      server/sonar-web/src/main/js/apps/create/organization/__tests__/actions-test.ts
  30. 51
    0
      server/sonar-web/src/main/js/apps/create/organization/actions.ts
  31. 3
    3
      server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx
  32. 4
    1
      server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx
  33. 16
    8
      server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx
  34. 1
    1
      server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap
  35. 63
    45
      server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/SyncMemberForm-test.tsx.snap
  36. 56
    0
      server/sonar-web/src/main/js/apps/organizations/__tests__/actions-test.ts
  37. 16
    26
      server/sonar-web/src/main/js/apps/organizations/actions.ts
  38. 11
    6
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.tsx
  39. 26
    23
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEmpty-test.tsx
  40. 29
    0
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEmpty-test.tsx.snap
  41. 12
    21
      server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx
  42. 15
    28
      server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx
  43. 0
    4
      server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx
  44. 13
    33
      server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap
  45. 0
    1
      server/sonar-web/src/main/js/apps/tutorials/styles.css
  46. 0
    69
      server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/TeamOnboardingModal.tsx
  47. 0
    61
      server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/__tests__/__snapshots__/TeamOnboardingModal-test.tsx.snap
  48. 13
    2
      server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
  49. 13
    4
      server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx
  50. 11
    6
      server/sonar-web/src/main/js/components/controls/Modal.tsx
  51. 23
    3
      server/sonar-web/src/main/js/components/controls/__tests__/ConfirmButton-test.tsx
  52. 20
    0
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ConfirmButton-test.tsx.snap
  53. 2
    1
      server/sonar-web/src/main/js/components/icons-components/OnboardingProjectIcon.tsx
  54. 0
    33
      server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx
  55. 16
    12
      server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts
  56. 2
    2
      server/sonar-web/src/main/js/helpers/almIntegrations.ts
  57. 15
    0
      server/sonar-web/src/main/js/helpers/testMocks.ts
  58. 10
    8
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 13
server/sonar-web/src/main/js/app/components/StartupModal.tsx View File

@@ -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>
);
}

+ 37
- 40
server/sonar-web/src/main/js/app/styles/components/modals.css View File

@@ -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,

+ 4
- 0
server/sonar-web/src/main/js/app/styles/init/misc.css View File

@@ -257,6 +257,10 @@ td.big-spacer-top {
width: 400px !important;
}

.abs-width-600 {
width: 600px !important;
}

.justify {
margin-bottom: -1em;
text-align: justify;

+ 2
- 7
server/sonar-web/src/main/js/app/types.d.ts View File

@@ -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;

+ 1
- 1
server/sonar-web/src/main/js/apps/create/components/BillingFormShim.tsx View File

@@ -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;

+ 6
- 7
server/sonar-web/src/main/js/apps/create/components/UpgradeOrganizationModal.tsx View File

@@ -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')}

+ 1
- 2
server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/UpgradeOrganizationModal-test.tsx.snap View File

@@ -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

+ 3
- 3
server/sonar-web/src/main/js/apps/create/organization/AlmApplicationInstalling.tsx View File

@@ -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>

+ 17
- 2
server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationBind.tsx View File

@@ -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')}

+ 23
- 2
server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx View File

@@ -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}
/>

+ 12
- 39
server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx View File

@@ -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,

+ 4
- 1
server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx View File

@@ -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}

+ 4
- 4
server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx View File

@@ -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

+ 3
- 9
server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationBind-test.tsx View File

@@ -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}
/>
);

+ 4
- 11
server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx View File

@@ -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' });

+ 2
- 10
server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx View File

@@ -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 });

+ 8
- 23
server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx View File

@@ -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' };


+ 2
- 9
server/sonar-web/src/main/js/apps/create/organization/__tests__/ManualOrganizationCreate-test.tsx View File

@@ -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 });

+ 3
- 2
server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx View File

@@ -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);

+ 3
- 9
server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx View File

@@ -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()}

+ 2
- 9
server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx View File

@@ -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();

+ 23
- 6
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationBind-test.tsx.snap View File

@@ -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"
>

+ 29
- 8
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap View File

@@ -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,

+ 4
- 3
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap View File

@@ -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,

+ 14
- 8
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap View File

@@ -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",

+ 2
- 5
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap View File

@@ -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",
},
],
],

+ 5
- 2
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap View File

@@ -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]}

+ 2
- 2
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap View File

@@ -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"

+ 85
- 0
server/sonar-web/src/main/js/apps/create/organization/__tests__/actions-test.ts View File

@@ -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;
});
});

+ 51
- 0
server/sonar-web/src/main/js/apps/create/organization/actions.ts View File

@@ -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;
});
};
}

+ 3
- 3
server/sonar-web/src/main/js/apps/organizationMembers/MembersListHeader.tsx View File

@@ -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>

+ 4
- 1
server/sonar-web/src/main/js/apps/organizationMembers/MembersPageHeader.tsx View File

@@ -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',

+ 16
- 8
server/sonar-web/src/main/js/apps/organizationMembers/SyncMemberForm.tsx View File

@@ -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>

+ 1
- 1
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/MembersPageHeader-test.tsx.snap View File

@@ -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"
>

+ 63
- 45
server/sonar-web/src/main/js/apps/organizationMembers/__tests__/__snapshots__/SyncMemberForm-test.tsx.snap View File

@@ -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 />

+ 56
- 0
server/sonar-web/src/main/js/apps/organizations/__tests__/actions-test.ts View File

@@ -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' });
});
});

+ 16
- 26
server/sonar-web/src/main/js/apps/organizations/actions.ts View File

@@ -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));
});
};
}

+ 11
- 6
server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.tsx View File

@@ -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>
);

+ 26
- 23
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEmpty-test.tsx View File

@@ -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');
});
}

+ 29
- 0
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEmpty-test.tsx.snap View File

@@ -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"

+ 12
- 21
server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.tsx View File

@@ -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>
);

+ 15
- 28
server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx View File

@@ -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>
);
}
}

+ 0
- 4
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OnboardingModal-test.tsx View File

@@ -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();
});

+ 13
- 33
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/OnboardingModal-test.tsx.snap View File

@@ -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>
`;

+ 0
- 1
server/sonar-web/src/main/js/apps/tutorials/styles.css View File

@@ -85,7 +85,6 @@
}

.onboarding-choice-name {
padding-top: var(--gridSize);
color: inherit;
font-size: var(--mediumFontSize);
}

+ 0
- 69
server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/TeamOnboardingModal.tsx View File

@@ -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>
);
}
}

+ 0
- 61
server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/__tests__/__snapshots__/TeamOnboardingModal-test.tsx.snap View File

@@ -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>
`;

+ 13
- 2
server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx View File

@@ -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>
);

+ 13
- 4
server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx View File

@@ -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}

+ 11
- 6
server/sonar-web/src/main/js/components/controls/Modal.tsx View File

@@ -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}

server/sonar-web/src/main/js/apps/tutorials/teamOnboarding/__tests__/TeamOnboardingModal-test.tsx → server/sonar-web/src/main/js/components/controls/__tests__/ConfirmButton-test.tsx View File

@@ -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>
);
}

+ 20
- 0
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ConfirmButton-test.tsx.snap View File

@@ -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>
`;

+ 2
- 1
server/sonar-web/src/main/js/components/icons-components/OnboardingProjectIcon.tsx View File

@@ -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 (

+ 0
- 33
server/sonar-web/src/main/js/components/icons-components/OnboardingTeamIcon.tsx View File

@@ -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>
);
}

+ 16
- 12
server/sonar-web/src/main/js/helpers/__tests__/almIntegrations-test.ts View File

@@ -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', () => {

+ 2
- 2
server/sonar-web/src/main/js/helpers/almIntegrations.ts View File

@@ -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';
}

+ 15
- 0
server/sonar-web/src/main/js/helpers/testMocks.ts View File

@@ -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',

+ 10
- 8
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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}


Loading…
Cancel
Save