- Remove team button and refactor modals - Sync members after importing alm organization - Update AlmOrganization type and introduce mock function - Add info box when importing or binding organization - Remove manage team button in bound org empty state if member synctags/7.7
@@ -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> | |||
); | |||
} |
@@ -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, |
@@ -257,6 +257,10 @@ td.big-spacer-top { | |||
width: 400px !important; | |||
} | |||
.abs-width-600 { | |||
width: 600px !important; | |||
} | |||
.justify { | |||
margin-bottom: -1em; | |||
text-align: justify; |
@@ -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; |
@@ -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; |
@@ -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')} |
@@ -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 |
@@ -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> |
@@ -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')} |
@@ -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} | |||
/> |
@@ -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, |
@@ -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} |
@@ -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 |
@@ -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} | |||
/> | |||
); |
@@ -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' }); |
@@ -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 }); |
@@ -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' }; | |||
@@ -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 }); |
@@ -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); |
@@ -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()} |
@@ -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(); |
@@ -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" | |||
> |
@@ -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, |
@@ -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, |
@@ -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", |
@@ -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", | |||
}, | |||
], | |||
], |
@@ -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]} |
@@ -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" |
@@ -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; | |||
}); | |||
}); |
@@ -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; | |||
}); | |||
}; | |||
} |
@@ -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> |
@@ -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', |
@@ -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> |
@@ -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" | |||
> |
@@ -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 /> |
@@ -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' }); | |||
}); | |||
}); |
@@ -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)); | |||
}); | |||
}; | |||
} |
@@ -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> | |||
); |
@@ -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'); | |||
}); | |||
} |
@@ -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" |
@@ -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> | |||
); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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(); | |||
}); |
@@ -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> | |||
`; |
@@ -85,7 +85,6 @@ | |||
} | |||
.onboarding-choice-name { | |||
padding-top: var(--gridSize); | |||
color: inherit; | |||
font-size: var(--mediumFontSize); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
`; |
@@ -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> | |||
); |
@@ -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} |
@@ -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} |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -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 ( |
@@ -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> | |||
); | |||
} |
@@ -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', () => { |
@@ -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'; | |||
} |
@@ -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', |
@@ -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} | |||