diff options
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} |