Browse Source

SONAR-11321 Apply feedback

* Do not autofocus when a default org is selected
* Do not skip onboarding when opening the organization create page
* Add button to cancel org import
* Fix bug of org created with description in place of avatar
* Redirect to organization projects after multiple projects import
* Correctly select newly create organization when redirected to project creation page
* Remove tutorial steps in auto import organization components
* Update already imported repository link
* Hide key value in the additional info when read only
* Hide org type icons in the organization select of the page to manually create a project
* Update wording to analyze projects instead of create projects
* Display spinner while importing organization
* Disable auto import of org for now when the user must create a paid org
* Add placeholder to avatar input when there is no url specified
* Add missing app installation text in create project page
* Allow to switch between tabs during organization import and keep data
* Remove read-only key when binding personal org
tags/7.5
Grégoire Aubert 5 years ago
parent
commit
4e72416a41
53 changed files with 907 additions and 788 deletions
  1. 1
    1
      server/sonar-web/src/main/js/app/components/StartupModal.tsx
  2. 1
    1
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx
  3. 3
    3
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap
  4. 1
    0
      server/sonar-web/src/main/js/app/styles/init/misc.css
  5. 1
    0
      server/sonar-web/src/main/js/apps/create/components/OrganizationAvatarInput.tsx
  6. 16
    22
      server/sonar-web/src/main/js/apps/create/components/OrganizationKeyInput.tsx
  7. 29
    18
      server/sonar-web/src/main/js/apps/create/components/OrganizationSelect.tsx
  8. 0
    7
      server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationKeyInput-test.tsx
  9. 4
    3
      server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationSelect-test.tsx
  10. 2
    0
      server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationAvatarInput-test.tsx.snap
  11. 0
    21
      server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap
  12. 12
    1
      server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationSelect-test.tsx.snap
  13. 4
    2
      server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationBind.tsx
  14. 100
    88
      server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx
  15. 39
    32
      server/sonar-web/src/main/js/apps/create/organization/AutoPersonalOrganizationBind.tsx
  16. 60
    42
      server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
  17. 4
    3
      server/sonar-web/src/main/js/apps/create/organization/ManualOrganizationCreate.tsx
  18. 21
    18
      server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsForm.tsx
  19. 3
    2
      server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx
  20. 4
    26
      server/sonar-web/src/main/js/apps/create/organization/RemoteOrganizationChoose.tsx
  21. 14
    1
      server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoOrganizationCreate-test.tsx
  22. 16
    2
      server/sonar-web/src/main/js/apps/create/organization/__tests__/AutoPersonalOrganizationBind-test.tsx
  23. 4
    2
      server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
  24. 4
    4
      server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx
  25. 1
    1
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationBind-test.tsx.snap
  26. 127
    110
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoOrganizationCreate-test.tsx.snap
  27. 48
    48
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/AutoPersonalOrganizationBind-test.tsx.snap
  28. 0
    222
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ChooseRemoteOrganizationStep-test.tsx.snap
  29. 62
    25
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
  30. 4
    4
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/ManualOrganizationCreate-test.tsx.snap
  31. 1
    1
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsForm-test.tsx.snap
  32. 182
    0
      server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/RemoteOrganizationChoose-test.tsx.snap
  33. 13
    4
      server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx
  34. 9
    8
      server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx
  35. 6
    4
      server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx
  36. 1
    3
      server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx
  37. 1
    0
      server/sonar-web/src/main/js/apps/create/project/OrganizationInput.tsx
  38. 7
    5
      server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx
  39. 1
    1
      server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx
  40. 20
    12
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap
  41. 5
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap
  42. 2
    2
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPage-test.tsx.snap
  43. 3
    3
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
  44. 1
    0
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationInput-test.tsx.snap
  45. 3
    3
      server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/RemoteRepositories-test.tsx.snap
  46. 11
    4
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationEdit.tsx
  47. 1
    1
      server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.tsx
  48. 35
    8
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEdit-test.tsx.snap
  49. 1
    1
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationJustCreated-test.tsx.snap
  50. 1
    3
      server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
  51. 1
    1
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
  52. 1
    1
      server/sonar-web/src/main/js/components/controls/react-select.css
  53. 16
    14
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

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

@@ -115,7 +115,7 @@ export class StartupModal extends React.PureComponent<Props, State> {
};

openOrganizationOnboarding = () => {
this.closeOnboarding();
this.setState({ automatic: false, modal: undefined });
this.props.router.push({ pathname: '/create-organization', state: { paid: true } });
};


+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx View File

@@ -106,7 +106,7 @@ export class GlobalNavPlus extends React.PureComponent<Props & WithRouterProps,
return (
<li>
<a className="js-new-project" href="#" onClick={this.handleNewProjectClick}>
{translate('provisioning.create_new_project')}
{translate('provisioning.analyze_new_project')}
</a>
</li>
);

+ 3
- 3
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap View File

@@ -12,7 +12,7 @@ exports[`render 1`] = `
href="#"
onClick={[Function]}
>
provisioning.create_new_project
provisioning.analyze_new_project
</a>
</li>
</ul>
@@ -49,7 +49,7 @@ exports[`should display create new organization on SonarCloud only 1`] = `
href="#"
onClick={[Function]}
>
provisioning.create_new_project
provisioning.analyze_new_project
</a>
</li>
<li>
@@ -95,7 +95,7 @@ exports[`should display new organization and new project on SonarCloud 1`] = `
href="#"
onClick={[Function]}
>
provisioning.create_new_project
provisioning.analyze_new_project
</a>
</li>
<li>

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

@@ -368,6 +368,7 @@ td.big-spacer-top {
.vertical-pipe-separator {
display: flex;
flex-direction: column;
margin-left: 60px;
margin-right: 60px;
}


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

@@ -101,6 +101,7 @@ export default class OrganizationAvatarInput extends React.PureComponent<Props,
onBlur={this.handleBlur}
onChange={this.handleChange}
onFocus={this.handleFocus}
placeholder={translate('onboarding.create_organization.avatar.placeholder')}
type="text"
value={this.state.value}
/>

+ 16
- 22
server/sonar-web/src/main/js/apps/create/components/OrganizationKeyInput.tsx View File

@@ -28,7 +28,6 @@ import { getHostUrl } from '../../../helpers/urls';
interface Props {
initialValue?: string;
onChange: (value: string | undefined) => void;
readOnly?: boolean;
}

interface State {
@@ -51,9 +50,7 @@ export default class OrganizationKeyInput extends React.PureComponent<Props, Sta
this.mounted = true;
if (this.props.initialValue !== undefined) {
this.setState({ value: this.props.initialValue });
if (!this.props.readOnly) {
this.validateKey(this.props.initialValue);
}
this.validateKey(this.props.initialValue);
}
}

@@ -123,28 +120,25 @@ export default class OrganizationKeyInput extends React.PureComponent<Props, Sta
isInvalid={isInvalid}
isValid={isValid}
label={translate('onboarding.create_organization.organization_name')}
required={!this.props.readOnly}>
required={true}>
<div className="display-inline-flex-baseline">
<span className="little-spacer-right">
{getHostUrl().replace(/https*:\/\//, '') + '/organizations/'}
{this.props.readOnly && this.state.value}
</span>
{!this.props.readOnly && (
<input
autoFocus={true}
className={classNames('input-super-large', {
'is-invalid': isInvalid,
'is-valid': isValid
})}
id="organization-key"
maxLength={255}
onBlur={this.handleBlur}
onChange={this.handleChange}
onFocus={this.handleFocus}
type="text"
value={this.state.value}
/>
)}
<input
autoFocus={true}
className={classNames('input-super-large', {
'is-invalid': isInvalid,
'is-valid': isValid
})}
id="organization-key"
maxLength={255}
onBlur={this.handleBlur}
onChange={this.handleChange}
onFocus={this.handleFocus}
type="text"
value={this.state.value}
/>
</div>
</ValidationInput>
);

+ 29
- 18
server/sonar-web/src/main/js/apps/create/components/OrganizationSelect.tsx View File

@@ -26,15 +26,22 @@ import { sanitizeAlmId } from '../../../helpers/almIntegrations';
import { getBaseUrl } from '../../../helpers/urls';

interface Props {
hideIcons?: boolean;
onChange: (organization: Organization) => void;
organization: string;
organizations: Organization[];
}

export default function OrganizationSelect({ onChange, organization, organizations }: Props) {
export default function OrganizationSelect({
hideIcons,
onChange,
organization,
organizations
}: Props) {
const optionRenderer = getOptionRenderer(hideIcons);
return (
<Select
autoFocus={true}
autoFocus={!organization}
className="input-super-large"
clearable={false}
id="select-organization"
@@ -51,20 +58,24 @@ export default function OrganizationSelect({ onChange, organization, organizatio
);
}

export function optionRenderer(organization: Organization) {
const icon = organization.alm
? `sonarcloud/${sanitizeAlmId(organization.alm.key)}`
: 'sonarcloud-square-logo';
return (
<span>
<img
alt={organization.alm ? organization.alm.key : 'SonarCloud'}
className="spacer-right"
height={14}
src={`${getBaseUrl()}/images/${icon}.svg`}
/>
{organization.name}
<span className="note little-spacer-left">{organization.key}</span>
</span>
);
export function getOptionRenderer(hideIcons?: boolean) {
return function optionRenderer(organization: Organization) {
const icon = organization.alm
? `sonarcloud/${sanitizeAlmId(organization.alm.key)}`
: 'sonarcloud-square-logo';
return (
<span>
{!hideIcons && (
<img
alt={organization.alm ? organization.alm.key : 'SonarCloud'}
className="spacer-right"
height={14}
src={`${getBaseUrl()}/images/${icon}.svg`}
/>
)}
{organization.name}
<span className="note little-spacer-left">{organization.key}</span>
</span>
);
};
}

+ 0
- 7
server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationKeyInput-test.tsx View File

@@ -38,13 +38,6 @@ it('should render correctly', () => {
expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot();
});

it('should render correctly with readonly mode', () => {
const wrapper = shallow(
<OrganizationKeyInput initialValue="key" onChange={jest.fn()} readOnly={true} />
);
expect(wrapper).toMatchSnapshot();
});

it('should not display any status when the key is not defined', async () => {
const wrapper = shallow(<OrganizationKeyInput onChange={jest.fn()} />);
await waitAndUpdate(wrapper);

+ 4
- 3
server/sonar-web/src/main/js/apps/create/components/__tests__/OrganizationSelect-test.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import OrganizationSelect, { optionRenderer } from '../OrganizationSelect';
import OrganizationSelect, { getOptionRenderer } from '../OrganizationSelect';

const organizations = [
{ key: 'foo', name: 'Foo' },
@@ -35,6 +35,7 @@ it('should render correctly', () => {
});

it('should render options correctly', () => {
expect(shallow(optionRenderer(organizations[0]))).toMatchSnapshot();
expect(shallow(optionRenderer(organizations[1]))).toMatchSnapshot();
expect(shallow(getOptionRenderer()(organizations[0]))).toMatchSnapshot();
expect(shallow(getOptionRenderer()(organizations[1]))).toMatchSnapshot();
expect(shallow(getOptionRenderer(true)(organizations[0]))).toMatchSnapshot();
});

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

@@ -23,6 +23,7 @@ exports[`should display the fallback avatar when there is no url 1`] = `
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="onboarding.create_organization.avatar.placeholder"
type="text"
value=""
/>
@@ -52,6 +53,7 @@ exports[`should render correctly 1`] = `
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="onboarding.create_organization.avatar.placeholder"
type="text"
value="https://my.avatar"
/>

+ 0
- 21
server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/OrganizationKeyInput-test.tsx.snap View File

@@ -32,24 +32,3 @@ exports[`should render correctly 1`] = `
`;

exports[`should render correctly 2`] = `true`;

exports[`should render correctly with readonly mode 1`] = `
<ValidationInput
id="organization-key"
isInvalid={false}
isValid={false}
label="onboarding.create_organization.organization_name"
required={false}
>
<div
className="display-inline-flex-baseline"
>
<span
className="little-spacer-right"
>
localhost/organizations/
key
</span>
</div>
</ValidationInput>
`;

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

@@ -2,7 +2,7 @@

exports[`should render correctly 1`] = `
<Select
autoFocus={true}
autoFocus={false}
className="input-super-large"
clearable={false}
id="select-organization"
@@ -66,3 +66,14 @@ exports[`should render options correctly 2`] = `
</span>
</span>
`;

exports[`should render options correctly 3`] = `
<span>
Foo
<span
className="note little-spacer-left"
>
foo
</span>
</span>
`;

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

@@ -18,8 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { Organization } from '../../../app/types';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import OrganizationSelect from '../components/OrganizationSelect';
import { Organization } from '../../../app/types';
import { SubmitButton } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';

@@ -84,10 +85,11 @@ export default class AutoOrganizationBind extends React.PureComponent<Props, Sta
organization={organization}
organizations={this.props.unboundOrganizations}
/>
<div className="big-spacer-top">
<div className="display-flex-center big-spacer-top">
<SubmitButton disabled={submitting || !organization}>
{translate('onboarding.import_organization.bind')}
</SubmitButton>
{submitting && <DeferredSpinner className="spacer-left" />}
</div>
</form>
);

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

@@ -18,12 +18,14 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import AutoOrganizationBind from './AutoOrganizationBind';
import ChooseRemoteOrganizationStep from './ChooseRemoteOrganizationStep';
import RemoteOrganizationChoose from './RemoteOrganizationChoose';
import OrganizationDetailsForm from './OrganizationDetailsForm';
import OrganizationDetailsStep from './OrganizationDetailsStep';
import { Query } from './utils';
import RadioToggle from '../../../components/controls/RadioToggle';
import { DeleteButton } from '../../../components/ui/buttons';
import {
AlmApplication,
AlmOrganization,
@@ -47,11 +49,13 @@ interface Props {
almOrganization?: AlmOrganization;
almUnboundApplications: AlmUnboundApplication[];
boundOrganization?: OrganizationBase;
className?: string;
createOrganization: (
organization: OrganizationBase & { installationId?: string }
) => Promise<Organization>;
onOrgCreated: (organization: string, justCreated?: boolean) => void;
unboundOrganizations: Organization[];
updateUrlQuery: (query: Partial<Query>) => void;
}

interface State {
@@ -66,8 +70,18 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S
};
}

handleOptionChange = (filter: Filters) => {
this.setState({ filter });
handleBindOrganization = (organization: string) => {
if (this.props.almInstallId) {
return bindAlmOrganization({
organization,
installationId: this.props.almInstallId
}).then(() => this.props.onOrgCreated(organization, false));
}
return Promise.reject();
};

handleCancelImport = () => {
this.props.updateUrlQuery({ almInstallId: undefined, almKey: undefined });
};

handleCreateOrganization = (organization: Required<OrganizationBase>) => {
@@ -83,98 +97,96 @@ export default class AutoOrganizationCreate extends React.PureComponent<Props, S
.then(({ key }) => this.props.onOrgCreated(key));
};

handleBindOrganization = (organization: string) => {
if (this.props.almInstallId) {
return bindAlmOrganization({
organization,
installationId: this.props.almInstallId
}).then(() => this.props.onOrgCreated(organization, false));
}
return Promise.reject();
handleOptionChange = (filter: Filters) => {
this.setState({ filter });
};

render() {
const {
almApplication,
almInstallId,
almOrganization,
boundOrganization,
unboundOrganizations
} = this.props;
if (almInstallId && almOrganization && !boundOrganization) {
const { filter } = this.state;
const hasUnboundOrgs = unboundOrganizations.length > 0;
return (
<OrganizationDetailsStep
finished={false}
onOpen={() => {}}
open={true}
organization={almOrganization}>
<div className="huge-spacer-bottom">
<p className="big-spacer-bottom">
<FormattedMessage
defaultMessage={translate('onboarding.import_organization_x')}
id="onboarding.import_organization_x"
values={{
avatar: (
<img
alt={almApplication.name}
className="little-spacer-left"
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(
almApplication.key
)}.svg`}
width={16}
/>
),
name: <strong>{almOrganization.name}</strong>
}}
/>
</p>

{hasUnboundOrgs && (
<RadioToggle
name="filter"
onCheck={this.handleOptionChange}
options={[
{
label: translate('onboarding.import_organization.create_new'),
value: Filters.Create
},
{
label: translate('onboarding.import_organization.bind_existing'),
value: Filters.Bind
}
]}
value={filter}
/>
)}
</div>
renderContent = (almOrganization: AlmOrganization) => {
const { almApplication, unboundOrganizations } = this.props;

{filter === Filters.Create && (
<OrganizationDetailsForm
onContinue={this.handleCreateOrganization}
organization={almOrganization}
submitText={translate('onboarding.import_organization.import')}
const { filter } = this.state;
const hasUnboundOrgs = unboundOrganizations.length > 0;
return (
<div className="boxed-group-inner">
<div className="huge-spacer-bottom">
<p className="display-flex-center big-spacer-bottom">
<FormattedMessage
defaultMessage={translate('onboarding.import_organization_x')}
id="onboarding.import_organization_x"
values={{
avatar: (
<img
alt={almApplication.name}
className="little-spacer-left"
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(
almApplication.key
)}.svg`}
width={16}
/>
),
name: <strong>{almOrganization.name}</strong>
}}
/>
)}
{filter === Filters.Bind && (
<AutoOrganizationBind
onBindOrganization={this.handleBindOrganization}
unboundOrganizations={unboundOrganizations}
<DeleteButton className="little-spacer-left" onClick={this.handleCancelImport} />
</p>

{hasUnboundOrgs && (
<RadioToggle
name="filter"
onCheck={this.handleOptionChange}
options={[
{
label: translate('onboarding.import_organization.create_new'),
value: Filters.Create
},
{
label: translate('onboarding.import_organization.bind_existing'),
value: Filters.Bind
}
]}
value={filter}
/>
)}
</OrganizationDetailsStep>
);
}
</div>

{filter === Filters.Create && (
<OrganizationDetailsForm
onContinue={this.handleCreateOrganization}
organization={almOrganization}
submitText={translate('onboarding.import_organization.import')}
/>
)}
{filter === Filters.Bind && (
<AutoOrganizationBind
onBindOrganization={this.handleBindOrganization}
unboundOrganizations={unboundOrganizations}
/>
)}
</div>
);
};

render() {
const { almInstallId, almOrganization, boundOrganization, className } = this.props;

return (
<ChooseRemoteOrganizationStep
almApplication={this.props.almApplication}
almInstallId={almInstallId}
almOrganization={almOrganization}
almUnboundApplications={this.props.almUnboundApplications}
boundOrganization={boundOrganization}
/>
<div className={classNames('boxed-group', className)}>
<div className="boxed-group-header">
<h2>{translate('onboarding.import_organization.import_org_details')}</h2>
</div>

{almInstallId && almOrganization && !boundOrganization ? (
this.renderContent(almOrganization)
) : (
<RemoteOrganizationChoose
almApplication={this.props.almApplication}
almInstallId={almInstallId}
almOrganization={almOrganization}
almUnboundApplications={this.props.almUnboundApplications}
boundOrganization={boundOrganization}
/>
)}
</div>
);
}
}

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

@@ -20,7 +20,8 @@
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import OrganizationDetailsForm from './OrganizationDetailsForm';
import OrganizationDetailsStep from './OrganizationDetailsStep';
import { Query } from './utils';
import { DeleteButton } from '../../../components/ui/buttons';
import {
AlmApplication,
AlmOrganization,
@@ -41,9 +42,14 @@ interface Props {
updateOrganization: (
organization: OrganizationBase & { installationId?: string }
) => Promise<Organization>;
updateUrlQuery: (query: Partial<Query>) => void;
}

export default class AutoPersonalOrganizationBind extends React.PureComponent<Props> {
handleCancelImport = () => {
this.props.updateUrlQuery({ almInstallId: undefined, almKey: undefined });
};

handleCreateOrganization = (organization: Required<OrganizationBase>) => {
return this.props
.updateOrganization({
@@ -60,39 +66,40 @@ export default class AutoPersonalOrganizationBind extends React.PureComponent<Pr
render() {
const { almApplication, importPersonalOrg } = this.props;
return (
<OrganizationDetailsStep
finished={false}
onOpen={() => {}}
open={true}
organization={importPersonalOrg}>
<div className="huge-spacer-bottom">
<FormattedMessage
defaultMessage={translate('onboarding.import_personal_organization_x')}
id="onboarding.import_personal_organization_x"
values={{
avatar: (
<img
alt={almApplication.name}
className="little-spacer-left"
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(almApplication.key)}.svg`}
width={16}
/>
),
name: <strong>{this.props.almOrganization.name}</strong>,
personalAvatar: importPersonalOrg && (
<OrganizationAvatar organization={importPersonalOrg} small={true} />
),
personalName: importPersonalOrg && <strong>{importPersonalOrg.name}</strong>
}}
<div className="boxed-group">
<div className="boxed-group-inner">
<div className="display-flex-center big-spacer-bottom">
<FormattedMessage
defaultMessage={translate('onboarding.import_personal_organization_x')}
id="onboarding.import_personal_organization_x"
values={{
avatar: (
<img
alt={almApplication.name}
className="little-spacer-left"
src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(
almApplication.key
)}.svg`}
width={16}
/>
),
name: <strong>{this.props.almOrganization.name}</strong>,
personalAvatar: importPersonalOrg && (
<OrganizationAvatar organization={importPersonalOrg} small={true} />
),
personalName: importPersonalOrg && <strong>{importPersonalOrg.name}</strong>
}}
/>
<DeleteButton className="little-spacer-left" onClick={this.handleCancelImport} />
</div>
<OrganizationDetailsForm
keyReadOnly={true}
onContinue={this.handleCreateOrganization}
organization={importPersonalOrg}
submitText={translate('onboarding.import_organization.bind')}
/>
</div>
<OrganizationDetailsForm
keyReadOnly={true}
onContinue={this.handleCreateOrganization}
organization={importPersonalOrg}
submitText={translate('onboarding.import_organization.bind')}
/>
</OrganizationDetailsStep>
</div>
);
}
}

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

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as classNames from 'classnames';
import { differenceInMinutes } from 'date-fns';
import { times } from 'lodash';
import { connect } from 'react-redux';
@@ -27,8 +28,10 @@ import { FormattedMessage } from 'react-intl';
import { Link, withRouter, WithRouterProps } from 'react-router';
import {
formatPrice,
ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP,
parseQuery,
ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP
serializeQuery,
Query
} from './utils';
import AlmApplicationInstalling from './AlmApplicationInstalling';
import AutoOrganizationCreate from './AutoOrganizationCreate';
@@ -90,6 +93,8 @@ interface State {
subscriptionPlans?: SubscriptionPlan[];
}

type StateWithAutoImport = State & Required<Pick<State, 'almApplication'>>;

type TabKeys = 'auto' | 'manual';

interface LocationState {
@@ -158,6 +163,10 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
});
};

hasAutoImport(state: State, paid?: boolean): state is StateWithAutoImport {
return Boolean(state.almApplication && !paid);
}

setValidOrgKey = (almOrganization: AlmOrganization) => {
const key = slugify(almOrganization.key);
const keys = [key, ...times(9, i => `${key}-${i + 1}`)];
@@ -227,7 +236,7 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
};

onTabChange = (tab: TabKeys) => {
this.updateUrl({ tab });
this.updateUrlState({ tab });
};

stopLoading = () => {
@@ -236,7 +245,15 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
}
};

updateUrl = (state: Partial<LocationState> = {}) => {
updateUrlQuery = (query: Partial<Query> = {}) => {
this.props.router.push({
pathname: this.props.location.pathname,
query: serializeQuery({ ...parseQuery(this.props.location.query), ...query }),
state: this.props.location.state
});
};

updateUrlState = (state: Partial<LocationState> = {}) => {
this.props.router.replace({
pathname: this.props.location.pathname,
query: this.props.location.query,
@@ -246,36 +263,36 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr

renderContent = (almInstallId?: string, importPersonalOrg?: Organization) => {
const { currentUser, location } = this.props;
const { almApplication, almOrganization } = this.state;
const state: LocationState = location.state || {};
const { state } = this;
const { almOrganization } = state;
const { paid, tab = 'auto' } = (location.state || {}) as LocationState;

if (importPersonalOrg && almOrganization && almApplication) {
if (importPersonalOrg && almOrganization && state.almApplication) {
return (
<AutoPersonalOrganizationBind
almApplication={almApplication}
almApplication={state.almApplication}
almInstallId={almInstallId}
almOrganization={almOrganization}
importPersonalOrg={importPersonalOrg}
onOrgCreated={this.handleOrgCreated}
updateOrganization={this.props.updateOrganization}
updateUrlQuery={this.updateUrlQuery}
/>
);
}

const showManualTab = state.tab === 'manual' && !almOrganization;
return (
<>
{almApplication && (
{this.hasAutoImport(state, paid) && (
<Tabs<TabKeys>
onChange={this.onTabChange}
selected={showManualTab ? 'manual' : 'auto'}
selected={tab || 'auto'}
tabs={[
{
key: 'auto',
node: translate('onboarding.import_organization', almApplication.key)
node: translate('onboarding.import_organization', state.almApplication.key)
},
{
disabled: Boolean(almOrganization),
key: 'manual',
node: translate('onboarding.create_organization.create_manually')
}
@@ -283,27 +300,30 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
/>
)}

{showManualTab || !almApplication ? (
<ManualOrganizationCreate
createOrganization={this.props.createOrganization}
deleteOrganization={this.props.deleteOrganization}
onOrgCreated={this.handleOrgCreated}
onlyPaid={state.paid}
subscriptionPlans={this.state.subscriptionPlans}
/>
) : (
<ManualOrganizationCreate
className={classNames({ hidden: tab !== 'manual' && this.hasAutoImport(state, paid) })}
createOrganization={this.props.createOrganization}
deleteOrganization={this.props.deleteOrganization}
onOrgCreated={this.handleOrgCreated}
onlyPaid={paid}
subscriptionPlans={this.state.subscriptionPlans}
/>

{this.hasAutoImport(state, paid) && (
<AutoOrganizationCreate
almApplication={almApplication}
almApplication={state.almApplication}
almInstallId={almInstallId}
almOrganization={almOrganization}
almUnboundApplications={this.state.almUnboundApplications}
boundOrganization={this.state.boundOrganization}
className={classNames({ hidden: tab !== 'auto' })}
createOrganization={this.props.createOrganization}
onOrgCreated={this.handleOrgCreated}
unboundOrganizations={this.props.userOrganizations.filter(
({ actions = {}, alm, key }) =>
!alm && key !== currentUser.personalOrganization && actions.admin
)}
updateUrlQuery={this.updateUrlQuery}
/>
)}
</>
@@ -325,9 +345,6 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
const header = importPersonalOrg
? translate('onboarding.import_organization.personal.page.header')
: translate('onboarding.create_organization.page.header');
const description = importPersonalOrg
? translate('onboarding.import_organization.personal.page.description')
: translate('onboarding.create_organization.page.description');
const startedPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price;
const formattedPrice = formatPrice(startedPrice);

@@ -337,23 +354,24 @@ export class CreateOrganization extends React.PureComponent<Props & WithRouterPr
<div className="sonarcloud page page-limited">
<header className="page-header">
<h1 className="page-title big-spacer-bottom">{header}</h1>
{startedPrice !== undefined && (
<p className="page-description">
<FormattedMessage
defaultMessage={description}
id={description}
values={{
break: <br />,
price: formattedPrice,
more: (
<Link target="_blank" to="/documentation/sonarcloud-pricing/">
{translate('learn_more')}
</Link>
)
}}
/>
</p>
)}
{!importPersonalOrg &&
startedPrice !== undefined && (
<p className="page-description">
<FormattedMessage
defaultMessage={translate('onboarding.create_organization.page.description')}
id="onboarding.create_organization.page.description"
values={{
break: <br />,
price: formattedPrice,
more: (
<Link target="_blank" to="/documentation/sonarcloud-pricing/">
{translate('learn_more')}
</Link>
)
}}
/>
</p>
)}
</header>
{this.state.loading ? (
<DeferredSpinner />

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

@@ -27,6 +27,7 @@ import { translate } from '../../../helpers/l10n';

interface Props {
createOrganization: (organization: OrganizationBase) => Promise<Organization>;
className?: string;
deleteOrganization: (key: string) => Promise<void>;
onOrgCreated: (organization: string) => void;
onlyPaid?: boolean;
@@ -101,12 +102,12 @@ export default class ManualOrganizationCreate extends React.PureComponent<Props,
};

render() {
const { subscriptionPlans } = this.props;
const { className, subscriptionPlans } = this.props;
const startedPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price;
const formattedPrice = formatPrice(startedPrice);

return (
<>
<div className={className}>
<OrganizationDetailsStep
finished={this.state.organization !== undefined}
onOpen={this.handleOrganizationDetailsStepOpen}
@@ -131,7 +132,7 @@ export default class ManualOrganizationCreate extends React.PureComponent<Props,
subscriptionPlans={subscriptionPlans}
/>
)}
</>
</div>
);
}
}

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

@@ -18,12 +18,13 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import DropdownIcon from '../../../components/icons-components/DropdownIcon';
import OrganizationAvatarInput from '../components/OrganizationAvatarInput';
import OrganizationDescriptionInput from '../components/OrganizationDescriptionInput';
import OrganizationKeyInput from '../components/OrganizationKeyInput';
import OrganizationNameInput from '../components/OrganizationNameInput';
import OrganizationUrlInput from '../components/OrganizationUrlInput';
import DropdownIcon from '../../../components/icons-components/DropdownIcon';
import { OrganizationBase } from '../../../app/types';
import { ResetButtonLink, SubmitButton } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';
@@ -88,20 +89,20 @@ export default class OrganizationDetailsForm extends React.PureComponent<Props,
this.setState(state => ({ additional: !state.additional }));
};

handleKeyUpdate = (key: string | undefined) => {
this.setState({ key });
};

handleNameUpdate = (name: string | undefined) => {
this.setState({ name });
handleAvatarUpdate = (avatar: string | undefined) => {
this.setState({ avatar });
};

handleDescriptionUpdate = (description: string | undefined) => {
this.setState({ description });
};

handleAvatarUpdate = (avatar: string | undefined) => {
this.setState({ avatar });
handleKeyUpdate = (key: string | undefined) => {
this.setState({ key });
};

handleNameUpdate = (name: string | undefined) => {
this.setState({ name });
};

handleUrlUpdate = (url: string | undefined) => {
@@ -132,13 +133,13 @@ export default class OrganizationDetailsForm extends React.PureComponent<Props,
};

render() {
const { submitting } = this.state;
const { keyReadOnly } = this.props;
return (
<form id="organization-form" onSubmit={this.handleSubmit}>
<OrganizationKeyInput
initialValue={this.state.key}
onChange={this.handleKeyUpdate}
readOnly={this.props.keyReadOnly}
/>
{!keyReadOnly && (
<OrganizationKeyInput initialValue={this.state.key} onChange={this.handleKeyUpdate} />
)}
<div className="big-spacer-top">
<ResetButtonLink onClick={this.handleAdditionalClick}>
{translate(
@@ -160,23 +161,25 @@ export default class OrganizationDetailsForm extends React.PureComponent<Props,
<OrganizationAvatarInput
initialValue={this.state.avatar}
name={this.state.name}
onChange={this.handleDescriptionUpdate}
onChange={this.handleAvatarUpdate}
/>
</div>
<div className="big-spacer-top">
<OrganizationDescriptionInput
initialValue={this.state.description}
onChange={this.handleAvatarUpdate}
onChange={this.handleDescriptionUpdate}
/>
</div>
<div className="big-spacer-top">
<OrganizationUrlInput initialValue={this.state.url} onChange={this.handleUrlUpdate} />
</div>
</div>
<div className="big-spacer-top">
<SubmitButton disabled={this.state.submitting || !this.canSubmit(this.state)}>

<div className="display-flex-center big-spacer-top">
<SubmitButton disabled={submitting || !this.canSubmit(this.state)}>
{this.props.submitText}
</SubmitButton>
{submitting && <DeferredSpinner className="spacer-left" />}
</div>
</form>
);

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

@@ -91,6 +91,7 @@ export default class PlanStep extends React.PureComponent<Props, State> {
};

renderForm = () => {
const { submitting } = this.state;
return (
<div className="boxed-group-inner">
{this.state.ready && (
@@ -122,10 +123,10 @@ export default class PlanStep extends React.PureComponent<Props, State> {
</BillingForm>
) : (
<div className="display-flex-center big-spacer-top">
<SubmitButton disabled={this.state.submitting} onClick={this.handleFreePlanSubmit}>
<SubmitButton disabled={submitting} onClick={this.handleFreePlanSubmit}>
{translate('my_account.create_organization')}
</SubmitButton>
{this.state.submitting && <DeferredSpinner className="spacer-left" />}
{submitting && <DeferredSpinner className="spacer-left" />}
</div>
)}
</>

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

@@ -25,7 +25,6 @@ import { serializeQuery } from './utils';
import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
import Select from '../../../components/controls/Select';
import Step from '../../tutorials/components/Step';
import { Alert } from '../../../components/ui/Alert';
import { SubmitButton } from '../../../components/ui/buttons';
import {
@@ -50,10 +49,7 @@ interface State {
unboundInstallationId: string;
}

export class ChooseRemoteOrganizationStep extends React.PureComponent<
Props & WithRouterProps,
State
> {
export class RemoteOrganizationChoose extends React.PureComponent<Props & WithRouterProps, State> {
state: State = { unboundInstallationId: '' };

handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
@@ -90,7 +86,7 @@ export class ChooseRemoteOrganizationStep extends React.PureComponent<
);
};

renderForm = () => {
render() {
const {
almApplication,
almInstallId,
@@ -144,7 +140,7 @@ export class ChooseRemoteOrganizationStep extends React.PureComponent<
</Alert>
)}
<div className="display-flex-center">
<div className="display-inline-block abs-width-400">
<div className="display-inline-block">
<IdentityProviderLink
className="display-inline-block"
identityProvider={almApplication}
@@ -194,25 +190,7 @@ export class ChooseRemoteOrganizationStep extends React.PureComponent<
</div>
</div>
);
};

renderResult = () => {
return null;
};

render() {
return (
<Step
finished={false}
onOpen={() => {}}
open={true}
renderForm={this.renderForm}
renderResult={this.renderResult}
stepNumber={1}
stepTitle={translate('onboarding.import_organization.import_org_details')}
/>
);
}
}

export default withRouter(ChooseRemoteOrganizationStep);
export default withRouter(RemoteOrganizationChoose);

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

@@ -20,7 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import AutoOrganizationCreate from '../AutoOrganizationCreate';
import { waitAndUpdate } from '../../../../helpers/testUtils';
import { waitAndUpdate, click } from '../../../../helpers/testUtils';
import { bindAlmOrganization } from '../../../../api/alm-integration';

jest.mock('../../../../api/alm-integration', () => ({
@@ -58,6 +58,18 @@ it('should render prefilled and create org', async () => {
expect(onOrgCreated).toBeCalledWith('foo');
});

it('should allow to cancel org import', () => {
const updateUrlQuery = jest.fn().mockResolvedValue({ key: 'foo' });
const wrapper = shallowRender({
almInstallId: 'id-foo',
almOrganization: { ...organization, personal: false },
updateUrlQuery
});

click(wrapper.find('DeleteButton'));
expect(updateUrlQuery).toBeCalledWith({ almInstallId: undefined, almKey: undefined });
});

it('should display choice between import or creation', () => {
const wrapper = shallowRender({
almInstallId: 'id-foo',
@@ -109,6 +121,7 @@ function shallowRender(props: Partial<AutoOrganizationCreate['props']> = {}) {
createOrganization={jest.fn()}
onOrgCreated={jest.fn()}
unboundOrganizations={[]}
updateUrlQuery={jest.fn()}
{...props}
/>
);

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

@@ -20,10 +20,11 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import AutoPersonalOrganizationBind from '../AutoPersonalOrganizationBind';
import { waitAndUpdate } from '../../../../helpers/testUtils';
import { waitAndUpdate, click } from '../../../../helpers/testUtils';

const personalOrg = { key: 'personalorg', name: 'Personal Org' };

it('should render correctly', async () => {
const personalOrg = { key: 'personalorg', name: 'Personal Org' };
const updateOrganization = jest.fn().mockResolvedValue({ key: personalOrg.key });
const onOrgCreated = jest.fn();
const wrapper = shallowRender({
@@ -42,6 +43,18 @@ it('should render correctly', async () => {
expect(onOrgCreated).toBeCalledWith(personalOrg.key);
});

it('should allow to cancel org import', () => {
const updateUrlQuery = jest.fn();
const wrapper = shallowRender({
almInstallId: 'id-foo',
importPersonalOrg: personalOrg,
updateUrlQuery
});

click(wrapper.find('DeleteButton'));
expect(updateUrlQuery).toBeCalledWith({ almInstallId: undefined, almKey: undefined });
});

function shallowRender(props: Partial<AutoPersonalOrganizationBind['props']> = {}) {
return shallow(
<AutoPersonalOrganizationBind
@@ -63,6 +76,7 @@ function shallowRender(props: Partial<AutoPersonalOrganizationBind['props']> = {
importPersonalOrg={{ key: 'personalorg', name: 'Personal Org' }}
onOrgCreated={jest.fn()}
updateOrganization={jest.fn()}
updateUrlQuery={jest.fn()}
{...props}
/>
);

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

@@ -185,9 +185,11 @@ it('should switch tabs', async () => {
expect(wrapper).toMatchSnapshot();

(wrapper.find('Tabs').prop('onChange') as Function)('manual');
expect(wrapper.find('ManualOrganizationCreate').exists()).toBeTruthy();
expect(wrapper.find('ManualOrganizationCreate').hasClass('hidden')).toBeFalsy();
expect(wrapper.find('AutoOrganizationCreate').hasClass('hidden')).toBeTruthy();
(wrapper.find('Tabs').prop('onChange') as Function)('auto');
expect(wrapper.find('AutoOrganizationCreate').exists()).toBeTruthy();
expect(wrapper.find('AutoOrganizationCreate').hasClass('hidden')).toBeFalsy();
expect(wrapper.find('ManualOrganizationCreate').hasClass('hidden')).toBeTruthy();
});

it('should reload the alm organization when the url query changes', async () => {

server/sonar-web/src/main/js/apps/create/organization/__tests__/ChooseRemoteOrganizationStep-test.tsx → server/sonar-web/src/main/js/apps/create/organization/__tests__/RemoteOrganizationChoose-test.tsx View File

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import { ChooseRemoteOrganizationStep } from '../ChooseRemoteOrganizationStep';
import { RemoteOrganizationChoose } from '../RemoteOrganizationChoose';
import { mockRouter, submit } from '../../../../helpers/testUtils';

it('should render', () => {
@@ -57,10 +57,10 @@ it('should display already bound alert message', () => {
).toMatchSnapshot();
});

function shallowRender(props: Partial<ChooseRemoteOrganizationStep['props']> = {}) {
function shallowRender(props: Partial<RemoteOrganizationChoose['props']> = {}) {
return shallow(
// @ts-ignore avoid passing everything from WithRouterProps
<ChooseRemoteOrganizationStep
<RemoteOrganizationChoose
almApplication={{
backgroundColor: 'blue',
iconPath: 'icon/path',
@@ -72,5 +72,5 @@ function shallowRender(props: Partial<ChooseRemoteOrganizationStep['props']> = {
router={mockRouter()}
{...props}
/>
).dive();
);
}

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

@@ -21,7 +21,7 @@ exports[`should render correctly 1`] = `
}
/>
<div
className="big-spacer-top"
className="display-flex-center big-spacer-top"
>
<SubmitButton
disabled={false}

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

@@ -1,136 +1,153 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display choice between import or creation 1`] = `
<OrganizationDetailsStep
finished={false}
onOpen={[Function]}
open={true}
organization={
Object {
"avatar": "http://example.com/avatar",
"description": "description-foo",
"key": "key-foo",
"name": "name-foo",
"personal": false,
"url": "http://example.com/foo",
}
}
<div
className="boxed-group"
>
<div
className="huge-spacer-bottom"
className="boxed-group-header"
>
<h2>
onboarding.import_organization.import_org_details
</h2>
</div>
<div
className="boxed-group-inner"
>
<p
className="big-spacer-bottom"
<div
className="huge-spacer-bottom"
>
<FormattedMessage
defaultMessage="onboarding.import_organization_x"
id="onboarding.import_organization_x"
values={
Object {
"avatar": <img
alt="BitBucket"
className="little-spacer-left"
src="/images/sonarcloud/bitbucket.svg"
width={16}
/>,
"name": <strong>
name-foo
</strong>,
<p
className="display-flex-center big-spacer-bottom"
>
<FormattedMessage
defaultMessage="onboarding.import_organization_x"
id="onboarding.import_organization_x"
values={
Object {
"avatar": <img
alt="BitBucket"
className="little-spacer-left"
src="/images/sonarcloud/bitbucket.svg"
width={16}
/>,
"name": <strong>
name-foo
</strong>,
}
}
/>
<DeleteButton
className="little-spacer-left"
onClick={[Function]}
/>
</p>
<RadioToggle
disabled={false}
name="filter"
onCheck={[Function]}
options={
Array [
Object {
"label": "onboarding.import_organization.create_new",
"value": "create",
},
Object {
"label": "onboarding.import_organization.bind_existing",
"value": "bind",
},
]
}
value={null}
/>
</p>
<RadioToggle
disabled={false}
name="filter"
onCheck={[Function]}
options={
Array [
Object {
"label": "onboarding.import_organization.create_new",
"value": "create",
},
Object {
"label": "onboarding.import_organization.bind_existing",
"value": "bind",
},
]
}
value={null}
/>
</div>
</div>
</OrganizationDetailsStep>
</div>
`;

exports[`should render prefilled and create org 1`] = `
<OrganizationDetailsStep
finished={false}
onOpen={[Function]}
open={true}
organization={
Object {
"avatar": "http://example.com/avatar",
"description": "description-foo",
"key": "key-foo",
"name": "name-foo",
"personal": false,
"url": "http://example.com/foo",
}
}
<div
className="boxed-group"
>
<div
className="huge-spacer-bottom"
className="boxed-group-header"
>
<h2>
onboarding.import_organization.import_org_details
</h2>
</div>
<div
className="boxed-group-inner"
>
<p
className="big-spacer-bottom"
<div
className="huge-spacer-bottom"
>
<FormattedMessage
defaultMessage="onboarding.import_organization_x"
id="onboarding.import_organization_x"
values={
Object {
"avatar": <img
alt="BitBucket"
className="little-spacer-left"
src="/images/sonarcloud/bitbucket.svg"
width={16}
/>,
"name": <strong>
name-foo
</strong>,
<p
className="display-flex-center big-spacer-bottom"
>
<FormattedMessage
defaultMessage="onboarding.import_organization_x"
id="onboarding.import_organization_x"
values={
Object {
"avatar": <img
alt="BitBucket"
className="little-spacer-left"
src="/images/sonarcloud/bitbucket.svg"
width={16}
/>,
"name": <strong>
name-foo
</strong>,
}
}
/>
<DeleteButton
className="little-spacer-left"
onClick={[Function]}
/>
</p>
</div>
<OrganizationDetailsForm
onContinue={[Function]}
organization={
Object {
"avatar": "http://example.com/avatar",
"description": "description-foo",
"key": "key-foo",
"name": "name-foo",
"personal": false,
"url": "http://example.com/foo",
}
/>
</p>
</div>
<OrganizationDetailsForm
onContinue={[Function]}
organization={
Object {
"avatar": "http://example.com/avatar",
"description": "description-foo",
"key": "key-foo",
"name": "name-foo",
"personal": false,
"url": "http://example.com/foo",
}
}
submitText="onboarding.import_organization.import"
/>
</OrganizationDetailsStep>
submitText="onboarding.import_organization.import"
/>
</div>
</div>
`;

exports[`should render with import org button 1`] = `
<withRouter(ChooseRemoteOrganizationStep)
almApplication={
Object {
"backgroundColor": "#0052CC",
"iconPath": "\\"/static/authbitbucket/bitbucket.svg\\"",
"installationUrl": "https://bitbucket.org/install/app",
"key": "bitbucket",
"name": "BitBucket",
<div
className="boxed-group"
>
<div
className="boxed-group-header"
>
<h2>
onboarding.import_organization.import_org_details
</h2>
</div>
<withRouter(RemoteOrganizationChoose)
almApplication={
Object {
"backgroundColor": "#0052CC",
"iconPath": "\\"/static/authbitbucket/bitbucket.svg\\"",
"installationUrl": "https://bitbucket.org/install/app",
"key": "bitbucket",
"name": "BitBucket",
}
}
}
almUnboundApplications={Array []}
/>
almUnboundApplications={Array []}
/>
</div>
`;

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

@@ -1,60 +1,60 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<OrganizationDetailsStep
finished={false}
onOpen={[Function]}
open={true}
organization={
Object {
"key": "personalorg",
"name": "Personal Org",
}
}
<div
className="boxed-group"
>
<div
className="huge-spacer-bottom"
className="boxed-group-inner"
>
<FormattedMessage
defaultMessage="onboarding.import_personal_organization_x"
id="onboarding.import_personal_organization_x"
values={
Object {
"avatar": <img
alt="BitBucket"
className="little-spacer-left"
src="/images/sonarcloud/bitbucket.svg"
width={16}
/>,
"name": <strong>
name-foo
</strong>,
"personalAvatar": <OrganizationAvatar
organization={
Object {
"key": "personalorg",
"name": "Personal Org",
<div
className="display-flex-center big-spacer-bottom"
>
<FormattedMessage
defaultMessage="onboarding.import_personal_organization_x"
id="onboarding.import_personal_organization_x"
values={
Object {
"avatar": <img
alt="BitBucket"
className="little-spacer-left"
src="/images/sonarcloud/bitbucket.svg"
width={16}
/>,
"name": <strong>
name-foo
</strong>,
"personalAvatar": <OrganizationAvatar
organization={
Object {
"key": "personalorg",
"name": "Personal Org",
}
}
}
small={true}
/>,
"personalName": <strong>
Personal Org
</strong>,
small={true}
/>,
"personalName": <strong>
Personal Org
</strong>,
}
}
/>
<DeleteButton
className="little-spacer-left"
onClick={[Function]}
/>
</div>
<OrganizationDetailsForm
keyReadOnly={true}
onContinue={[Function]}
organization={
Object {
"key": "personalorg",
"name": "Personal Org",
}
}
submitText="onboarding.import_organization.bind"
/>
</div>
<OrganizationDetailsForm
keyReadOnly={true}
onContinue={[Function]}
organization={
Object {
"key": "personalorg",
"name": "Personal Org",
}
}
submitText="onboarding.import_organization.bind"
/>
</OrganizationDetailsStep>
</div>
`;

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

@@ -1,222 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display already bound alert message 1`] = `
<Alert
className="big-spacer-bottom width-60"
variant="error"
>
<FormattedMessage
defaultMessage="onboarding.import_organization.already_bound_x"
id="onboarding.import_organization.already_bound_x"
values={
Object {
"avatar": <img
alt="GitHub"
className="little-spacer-left"
src="/images/sonarcloud/github.svg"
width={16}
/>,
"boundAvatar": <OrganizationAvatar
className="little-spacer-left"
organization={
Object {
"avatar": "bound-avatar",
"key": "bound",
"name": "Bound",
}
}
small={true}
/>,
"boundName": <strong>
Bound
</strong>,
"name": <strong>
Foo
</strong>,
}
}
/>
</Alert>
`;

exports[`should display an alert message 1`] = `
<Alert
className="big-spacer-bottom width-60"
variant="error"
>
<div
className="markdown"
>
onboarding.import_organization.org_not_found
<ul>
<li>
onboarding.import_organization.org_not_found.tips_1
</li>
<li>
onboarding.import_organization.org_not_found.tips_2
</li>
</ul>
</div>
</Alert>
`;

exports[`should display unbound installations 1`] = `
<div
className="boxed-group onboarding-step is-open"
>
<div
className="onboarding-step-number"
>
1
</div>
<div
className="boxed-group-header"
>
<h2>
onboarding.import_organization.import_org_details
</h2>
</div>
<div
className=""
>
<div
className="boxed-group-inner"
>
<div
className="display-flex-center"
>
<div
className="display-inline-block abs-width-400"
>
<IdentityProviderLink
className="display-inline-block"
identityProvider={
Object {
"backgroundColor": "blue",
"iconPath": "icon/path",
"installationUrl": "https://alm.application.url",
"key": "github",
"name": "GitHub",
}
}
small={true}
url="https://alm.application.url"
>
onboarding.import_organization.choose_organization_button.github
</IdentityProviderLink>
</div>
<div
className="display-flex-stretch"
>
<div
className="vertical-pipe-separator"
>
<div
className="vertical-separator "
/>
<span
className="note"
>
or
</span>
<div
className="vertical-separator"
/>
</div>
<form
className="big-spacer-top big-spacer-bottom"
onSubmit={[Function]}
>
<div
className="form-field abs-width-400"
>
<label
htmlFor="select-unbound-installation"
>
onboarding.import_organization.choose_unbound_installation.github
</label>
<Select
className="input-super-large"
clearable={false}
id="select-unbound-installation"
labelKey="name"
onChange={[Function]}
optionRenderer={[Function]}
options={
Array [
Object {
"installationId": "12345",
"key": "foo",
"name": "Foo",
},
]
}
placeholder="onboarding.import_organization.choose_organization"
value=""
valueKey="installationId"
valueRenderer={[Function]}
/>
</div>
<SubmitButton
disabled={true}
>
continue
</SubmitButton>
</form>
</div>
</div>
</div>
</div>
</div>
`;

exports[`should render 1`] = `
<div
className="boxed-group onboarding-step is-open"
>
<div
className="onboarding-step-number"
>
1
</div>
<div
className="boxed-group-header"
>
<h2>
onboarding.import_organization.import_org_details
</h2>
</div>
<div
className=""
>
<div
className="boxed-group-inner"
>
<div
className="display-flex-center"
>
<div
className="display-inline-block abs-width-400"
>
<IdentityProviderLink
className="display-inline-block"
identityProvider={
Object {
"backgroundColor": "blue",
"iconPath": "icon/path",
"installationUrl": "https://alm.application.url",
"key": "github",
"name": "GitHub",
}
}
small={true}
url="https://alm.application.url"
>
onboarding.import_organization.choose_organization_button.github
</IdentityProviderLink>
</div>
</div>
</div>
</div>
</div>
`;

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

@@ -25,28 +25,6 @@ exports[`should render with auto personal organization bind page 2`] = `
>
onboarding.import_organization.personal.page.header
</h1>
<p
className="page-description"
>
<FormattedMessage
defaultMessage="onboarding.import_organization.personal.page.description"
id="onboarding.import_organization.personal.page.description"
values={
Object {
"break": <br />,
"more": <Link
onlyActiveOnIndex={false}
style={Object {}}
target="_blank"
to="/documentation/sonarcloud-pricing/"
>
learn_more
</Link>,
"price": "billing.price_format.10",
}
}
/>
</p>
</header>
<AutoPersonalOrganizationBind
almApplication={
@@ -78,6 +56,7 @@ exports[`should render with auto personal organization bind page 2`] = `
}
onOrgCreated={[Function]}
updateOrganization={[MockFunction]}
updateUrlQuery={[Function]}
/>
</div>
</Fragment>
@@ -135,13 +114,30 @@ exports[`should render with auto tab displayed 1`] = `
"node": "onboarding.import_organization.github",
},
Object {
"disabled": false,
"key": "manual",
"node": "onboarding.create_organization.create_manually",
},
]
}
/>
<ManualOrganizationCreate
className="hidden"
createOrganization={[MockFunction]}
deleteOrganization={[MockFunction]}
onOrgCreated={[Function]}
subscriptionPlans={
Array [
Object {
"maxNcloc": 100000,
"price": 10,
},
Object {
"maxNcloc": 250000,
"price": 75,
},
]
}
/>
<AutoOrganizationCreate
almApplication={
Object {
@@ -153,6 +149,7 @@ exports[`should render with auto tab displayed 1`] = `
}
}
almUnboundApplications={Array []}
className=""
createOrganization={[MockFunction]}
onOrgCreated={[Function]}
unboundOrganizations={
@@ -166,6 +163,7 @@ exports[`should render with auto tab displayed 1`] = `
},
]
}
updateUrlQuery={[Function]}
/>
</div>
</Fragment>
@@ -229,13 +227,30 @@ exports[`should render with auto tab selected and manual disabled 2`] = `
"node": "onboarding.import_organization.github",
},
Object {
"disabled": true,
"key": "manual",
"node": "onboarding.create_organization.create_manually",
},
]
}
/>
<ManualOrganizationCreate
className="hidden"
createOrganization={[MockFunction]}
deleteOrganization={[MockFunction]}
onOrgCreated={[Function]}
subscriptionPlans={
Array [
Object {
"maxNcloc": 100000,
"price": 10,
},
Object {
"maxNcloc": 250000,
"price": 75,
},
]
}
/>
<AutoOrganizationCreate
almApplication={
Object {
@@ -258,6 +273,7 @@ exports[`should render with auto tab selected and manual disabled 2`] = `
}
}
almUnboundApplications={Array []}
className=""
createOrganization={[MockFunction]}
onOrgCreated={[Function]}
unboundOrganizations={
@@ -271,6 +287,7 @@ exports[`should render with auto tab selected and manual disabled 2`] = `
},
]
}
updateUrlQuery={[Function]}
/>
</div>
</Fragment>
@@ -319,6 +336,7 @@ exports[`should render with manual tab displayed 1`] = `
</p>
</header>
<ManualOrganizationCreate
className=""
createOrganization={[MockFunction]}
deleteOrganization={[MockFunction]}
onOrgCreated={[Function]}
@@ -391,13 +409,30 @@ exports[`should switch tabs 1`] = `
"node": "onboarding.import_organization.github",
},
Object {
"disabled": false,
"key": "manual",
"node": "onboarding.create_organization.create_manually",
},
]
}
/>
<ManualOrganizationCreate
className="hidden"
createOrganization={[MockFunction]}
deleteOrganization={[MockFunction]}
onOrgCreated={[Function]}
subscriptionPlans={
Array [
Object {
"maxNcloc": 100000,
"price": 10,
},
Object {
"maxNcloc": 250000,
"price": 75,
},
]
}
/>
<AutoOrganizationCreate
almApplication={
Object {
@@ -409,6 +444,7 @@ exports[`should switch tabs 1`] = `
}
}
almUnboundApplications={Array []}
className=""
createOrganization={[MockFunction]}
onOrgCreated={[Function]}
unboundOrganizations={
@@ -422,6 +458,7 @@ exports[`should switch tabs 1`] = `
},
]
}
updateUrlQuery={[Function]}
/>
</div>
</Fragment>

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

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render and create organization 1`] = `
<Fragment>
<div>
<OrganizationDetailsStep
finished={false}
onOpen={[Function]}
@@ -32,11 +32,11 @@ exports[`should render and create organization 1`] = `
]
}
/>
</Fragment>
</div>
`;

exports[`should render and create organization 2`] = `
<Fragment>
<div>
<OrganizationDetailsStep
finished={true}
onOpen={[Function]}
@@ -85,5 +85,5 @@ exports[`should render and create organization 2`] = `
]
}
/>
</Fragment>
</div>
`;

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

@@ -60,7 +60,7 @@ exports[`should render form 1`] = `
</div>
</div>
<div
className="big-spacer-top"
className="display-flex-center big-spacer-top"
>
<SubmitButton
disabled={true}

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

@@ -0,0 +1,182 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display already bound alert message 1`] = `
<Alert
className="big-spacer-bottom width-60"
variant="error"
>
<FormattedMessage
defaultMessage="onboarding.import_organization.already_bound_x"
id="onboarding.import_organization.already_bound_x"
values={
Object {
"avatar": <img
alt="GitHub"
className="little-spacer-left"
src="/images/sonarcloud/github.svg"
width={16}
/>,
"boundAvatar": <OrganizationAvatar
className="little-spacer-left"
organization={
Object {
"avatar": "bound-avatar",
"key": "bound",
"name": "Bound",
}
}
small={true}
/>,
"boundName": <strong>
Bound
</strong>,
"name": <strong>
Foo
</strong>,
}
}
/>
</Alert>
`;

exports[`should display an alert message 1`] = `
<Alert
className="big-spacer-bottom width-60"
variant="error"
>
<div
className="markdown"
>
onboarding.import_organization.org_not_found
<ul>
<li>
onboarding.import_organization.org_not_found.tips_1
</li>
<li>
onboarding.import_organization.org_not_found.tips_2
</li>
</ul>
</div>
</Alert>
`;

exports[`should display unbound installations 1`] = `
<div
className="boxed-group-inner"
>
<div
className="display-flex-center"
>
<div
className="display-inline-block"
>
<IdentityProviderLink
className="display-inline-block"
identityProvider={
Object {
"backgroundColor": "blue",
"iconPath": "icon/path",
"installationUrl": "https://alm.application.url",
"key": "github",
"name": "GitHub",
}
}
small={true}
url="https://alm.application.url"
>
onboarding.import_organization.choose_organization_button.github
</IdentityProviderLink>
</div>
<div
className="display-flex-stretch"
>
<div
className="vertical-pipe-separator"
>
<div
className="vertical-separator "
/>
<span
className="note"
>
or
</span>
<div
className="vertical-separator"
/>
</div>
<form
className="big-spacer-top big-spacer-bottom"
onSubmit={[Function]}
>
<div
className="form-field abs-width-400"
>
<label
htmlFor="select-unbound-installation"
>
onboarding.import_organization.choose_unbound_installation.github
</label>
<Select
className="input-super-large"
clearable={false}
id="select-unbound-installation"
labelKey="name"
onChange={[Function]}
optionRenderer={[Function]}
options={
Array [
Object {
"installationId": "12345",
"key": "foo",
"name": "Foo",
},
]
}
placeholder="onboarding.import_organization.choose_organization"
value=""
valueKey="installationId"
valueRenderer={[Function]}
/>
</div>
<SubmitButton
disabled={true}
>
continue
</SubmitButton>
</form>
</div>
</div>
</div>
`;

exports[`should render 1`] = `
<div
className="boxed-group-inner"
>
<div
className="display-flex-center"
>
<div
className="display-inline-block"
>
<IdentityProviderLink
className="display-inline-block"
identityProvider={
Object {
"backgroundColor": "blue",
"iconPath": "icon/path",
"installationUrl": "https://alm.application.url",
"key": "github",
"name": "GitHub",
}
}
small={true}
url="https://alm.application.url"
>
onboarding.import_organization.choose_organization_button.github
</IdentityProviderLink>
</div>
</div>
</div>
`;

+ 13
- 4
server/sonar-web/src/main/js/apps/create/project/AlmRepositoryItem.tsx View File

@@ -18,14 +18,15 @@
* 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 * as theme from '../../../app/theme';
import Checkbox from '../../../components/controls/Checkbox';
import CheckIcon from '../../../components/icons-components/CheckIcon';
import Tooltip from '../../../components/controls/Tooltip';
import { AlmRepository, IdentityProvider } from '../../../app/types';
import { getBaseUrl, getProjectUrl } from '../../../helpers/urls';
import { translate } from '../../../helpers/l10n';
import Tooltip from '../../../components/controls/Tooltip';

interface Props {
identityProvider: IdentityProvider;
@@ -61,9 +62,17 @@ export default class AlmRepositoryItem extends React.PureComponent<Props> {
{repository.linkedProjectKey && (
<span className="big-spacer-left">
<CheckIcon className="little-spacer-right" fill={theme.green} />
<Link to={getProjectUrl(repository.linkedProjectKey)}>
{translate('onboarding.create_project.already_imported')}
</Link>
<FormattedMessage
defaultMessage={translate('onboarding.create_project.repository_imported')}
id="onboarding.create_project.repository_imported"
values={{
link: (
<Link to={getProjectUrl(repository.linkedProjectKey)}>
{translate('onboarding.create_project.see_project')}
</Link>
)
}}
/>
</span>
)}
{repository.private && (

+ 9
- 8
server/sonar-web/src/main/js/apps/create/project/AutoProjectCreate.tsx View File

@@ -29,7 +29,7 @@ import { save } from '../../../helpers/storage';
interface Props {
almApplication: AlmApplication;
boundOrganizations: Organization[];
onProjectCreate: (projectKeys: string[]) => void;
onProjectCreate: (projectKeys: string[], organization: string) => void;
organization?: string;
}

@@ -44,15 +44,13 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State>
}

getInitialSelectedOrganization(props: Props) {
const organization =
props.organization && props.boundOrganizations.find(o => o.key === props.organization);
if (organization) {
return organization.key;
}
if (props.boundOrganizations.length === 1) {
if (props.organization) {
return props.organization;
} else if (props.boundOrganizations.length === 1) {
return props.boundOrganizations[0].key;
} else {
return '';
}
return '';
}

handleInstallAppClick = () => {
@@ -69,6 +67,9 @@ export default class AutoProjectCreate extends React.PureComponent<Props, State>
if (boundOrganizations.length === 0) {
return (
<>
<p className="spacer-bottom">
{translate('onboarding.create_project.install_app_description', almApplication.key)}
</p>
<IdentityProviderLink
className="display-inline-block"
identityProvider={almApplication}

+ 6
- 4
server/sonar-web/src/main/js/apps/create/project/CreateProjectPage.tsx View File

@@ -32,7 +32,7 @@ import { LoggedInUser, AlmApplication, Organization } from '../../../app/types';
import { getAlmAppInfo } from '../../../api/alm-integration';
import { hasAdvancedALMIntegration } from '../../../helpers/almIntegrations';
import { translate } from '../../../helpers/l10n';
import { getProjectUrl } from '../../../helpers/urls';
import { getProjectUrl, getOrganizationUrl } from '../../../helpers/urls';
import '../../../app/styles/sonarcloud.css';

interface Props {
@@ -78,10 +78,12 @@ export class CreateProjectPage extends React.PureComponent<Props & WithRouterPro
}
}

handleProjectCreate = (projectKeys: string[]) => {
handleProjectCreate = (projectKeys: string[], organization?: string) => {
this.props.skipOnboarding();
if (projectKeys.length > 1) {
this.props.router.push({ pathname: '/projects' });
this.props.router.push({
pathname: (organization ? getOrganizationUrl(organization) : '') + '/projects'
});
} else if (projectKeys.length === 1) {
this.props.router.push(getProjectUrl(projectKeys[0]));
}
@@ -141,7 +143,7 @@ export class CreateProjectPage extends React.PureComponent<Props & WithRouterPro
key: 'auto',
node: translate('onboarding.create_project.select_repositories')
},
{ key: 'manual', node: translate('onboarding.create_project.create_manually') }
{ key: 'manual', node: translate('onboarding.create_project.setup_manually') }
]}
/>
)}

+ 1
- 3
server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx View File

@@ -150,9 +150,7 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
value={this.state.projectKey}
/>
</div>
<SubmitButton disabled={!this.isValid() || submitting}>
{translate('create')}
</SubmitButton>
<SubmitButton disabled={!this.isValid() || submitting}>{translate('setup')}</SubmitButton>
<DeferredSpinner className="spacer-left" loading={submitting} />
</form>
</>

+ 1
- 0
server/sonar-web/src/main/js/apps/create/project/OrganizationInput.tsx View File

@@ -52,6 +52,7 @@ export class OrganizationInput extends React.PureComponent<Props & WithRouterPro
<em className="mandatory">*</em>
</label>
<OrganizationSelect
hideIcons={!autoImport}
onChange={onChange}
organization={organization}
organizations={organizations}

+ 7
- 5
server/sonar-web/src/main/js/apps/create/project/RemoteRepositories.tsx View File

@@ -27,7 +27,7 @@ import { translate } from '../../../helpers/l10n';

interface Props {
almApplication: AlmApplication;
onProjectCreate: (projectKeys: string[]) => void;
onProjectCreate: (projectKeys: string[], organization: string) => void;
organization: string;
}

@@ -90,7 +90,11 @@ export default class RemoteRepositories extends React.PureComponent<Props, State
}),
organization: this.props.organization
}).then(
({ projects }) => this.props.onProjectCreate(projects.map(project => project.projectKey)),
({ projects }) =>
this.props.onProjectCreate(
projects.map(project => project.projectKey),
this.props.organization
),
this.handleProvisionFail
);
}
@@ -150,9 +154,7 @@ export default class RemoteRepositories extends React.PureComponent<Props, State
))}
</ul>
</div>
<SubmitButton disabled={!this.isValid() || submitting}>
{translate('create')}
</SubmitButton>
<SubmitButton disabled={!this.isValid() || submitting}>{translate('setup')}</SubmitButton>
<DeferredSpinner className="spacer-left" loading={submitting} />
</form>
</DeferredSpinner>

+ 1
- 1
server/sonar-web/src/main/js/apps/create/project/__tests__/RemoteRepositories-test.tsx View File

@@ -79,7 +79,7 @@ it('should correctly create a project', async () => {
});

await waitAndUpdate(wrapper);
expect(onProjectCreate).toBeCalledWith(['awesome']);
expect(onProjectCreate).toBeCalledWith(['awesome'], 'sonarsource');
});

function shallowRender(props: Partial<RemoteRepositories['props']> = {}) {

+ 20
- 12
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AlmRepositoryItem-test.tsx.snap View File

@@ -61,21 +61,29 @@ exports[`should render disabled 1`] = `
className="little-spacer-right"
fill="#00aa00"
/>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to={
<FormattedMessage
defaultMessage="onboarding.create_project.repository_imported"
id="onboarding.create_project.repository_imported"
values={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "proj_cool",
},
"link": <Link
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "proj_cool",
},
}
}
>
onboarding.create_project.see_project
</Link>,
}
}
>
onboarding.create_project.already_imported
</Link>
/>
</span>
</Fragment>
`;

+ 5
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/AutoProjectCreate-test.tsx.snap View File

@@ -45,6 +45,11 @@ exports[`should display the bounded organizations dropdown with the list of repo

exports[`should display the provider app install button 1`] = `
<Fragment>
<p
className="spacer-bottom"
>
onboarding.create_project.install_app_description.github
</p>
<IdentityProviderLink
className="display-inline-block"
identityProvider={

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

@@ -58,7 +58,7 @@ exports[`should render correctly 2`] = `
},
Object {
"key": "manual",
"node": "onboarding.create_project.create_manually",
"node": "onboarding.create_project.setup_manually",
},
]
}
@@ -184,7 +184,7 @@ exports[`should switch tabs 1`] = `
},
Object {
"key": "manual",
"node": "onboarding.create_project.create_manually",
"node": "onboarding.create_project.setup_manually",
},
]
}

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

@@ -4,7 +4,7 @@ exports[`should correctly create a project 1`] = `
<SubmitButton
disabled={true}
>
create
setup
</SubmitButton>
`;

@@ -12,7 +12,7 @@ exports[`should correctly create a project 2`] = `
<SubmitButton
disabled={false}
>
create
setup
</SubmitButton>
`;

@@ -88,7 +88,7 @@ exports[`should render correctly 1`] = `
<SubmitButton
disabled={true}
>
create
setup
</SubmitButton>
<DeferredSpinner
className="spacer-left"

+ 1
- 0
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/OrganizationInput-test.tsx.snap View File

@@ -15,6 +15,7 @@ exports[`should render correctly 1`] = `
</em>
</label>
<OrganizationSelect
hideIcons={true}
onChange={[MockFunction]}
organization="bar"
organizations={

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

@@ -4,7 +4,7 @@ exports[`should correctly create a project 1`] = `
<SubmitButton
disabled={false}
>
create
setup
</SubmitButton>
`;

@@ -24,7 +24,7 @@ exports[`should display the list of repositories 1`] = `
<SubmitButton
disabled={true}
>
create
setup
</SubmitButton>
<DeferredSpinner
className="spacer-left"
@@ -102,7 +102,7 @@ exports[`should display the list of repositories 2`] = `
<SubmitButton
disabled={true}
>
create
setup
</SubmitButton>
<DeferredSpinner
className="spacer-left"

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

@@ -21,10 +21,11 @@ import * as React from 'react';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { translate } from '../../../helpers/l10n';
import { updateOrganization } from '../actions';
import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
import { SubmitButton } from '../../../components/ui/buttons';
import { updateOrganization } from '../actions';
import { Organization, OrganizationBase } from '../../../app/types';
import { translate } from '../../../helpers/l10n';

interface DispatchProps {
updateOrganization: (organization: string, changes: OrganizationBase) => Promise<any>;
@@ -138,19 +139,25 @@ export class OrganizationEdit extends React.PureComponent<Props, State> {
maxLength={256}
name="avatar"
onChange={this.handleAvatarInputChange}
placeholder={translate('onboarding.create_organization.avatar.placeholder')}
type="text"
value={this.state.avatar}
/>
<div className="modal-field-description">
{translate('organization.avatar.description')}
</div>
{!!this.state.avatarImage && (
{(this.state.avatarImage || this.state.name) && (
<div className="spacer-top spacer-bottom">
<div className="little-spacer-bottom">
{translate('organization.avatar.preview')}
{':'}
</div>
<img alt="" height={30} src={this.state.avatarImage} />
<OrganizationAvatar
organization={{
avatar: this.state.avatarImage || undefined,
name: this.state.name || ''
}}
/>
</div>
)}
</div>

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

@@ -53,7 +53,7 @@ export class OrganizationJustCreated extends React.PureComponent<Props & WithRou
<Button className="onboarding-choice" onClick={this.handleNewProjectClick}>
<OnboardingProjectIcon className="big-spacer-bottom" />
<h6 className="onboarding-choice-name">
{translate('provisioning.create_new_project')}
{translate('provisioning.analyze_new_project')}
</h6>
</Button>
<Button className="onboarding-choice" onClick={this.handleAddMembersClick}>

+ 35
- 8
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationEdit-test.tsx.snap View File

@@ -67,6 +67,7 @@ exports[`smoke test 1`] = `
maxLength={256}
name="avatar"
onChange={[Function]}
placeholder="onboarding.create_organization.avatar.placeholder"
type="text"
value=""
/>
@@ -75,6 +76,24 @@ exports[`smoke test 1`] = `
>
organization.avatar.description
</div>
<div
className="spacer-top spacer-bottom"
>
<div
className="little-spacer-bottom"
>
organization.avatar.preview
:
</div>
<OrganizationAvatar
organization={
Object {
"avatar": undefined,
"name": "Foo",
}
}
/>
</div>
</div>
<div
className="modal-field"
@@ -203,6 +222,7 @@ exports[`smoke test 2`] = `
maxLength={256}
name="avatar"
onChange={[Function]}
placeholder="onboarding.create_organization.avatar.placeholder"
type="text"
value="foo-avatar"
/>
@@ -220,10 +240,13 @@ exports[`smoke test 2`] = `
organization.avatar.preview
:
</div>
<img
alt=""
height={30}
src="foo-avatar-image"
<OrganizationAvatar
organization={
Object {
"avatar": "foo-avatar-image",
"name": "New Foo",
}
}
/>
</div>
</div>
@@ -354,6 +377,7 @@ exports[`smoke test 3`] = `
maxLength={256}
name="avatar"
onChange={[Function]}
placeholder="onboarding.create_organization.avatar.placeholder"
type="text"
value="foo-avatar"
/>
@@ -371,10 +395,13 @@ exports[`smoke test 3`] = `
organization.avatar.preview
:
</div>
<img
alt=""
height={30}
src="foo-avatar-image"
<OrganizationAvatar
organization={
Object {
"avatar": "foo-avatar-image",
"name": "New Foo",
}
}
/>
</div>
</div>

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

@@ -22,7 +22,7 @@ exports[`should render 1`] = `
<h6
className="onboarding-choice-name"
>
provisioning.create_new_project
provisioning.analyze_new_project
</h6>
</Button>
<Button

+ 1
- 3
server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx View File

@@ -54,9 +54,7 @@ export class NoFavoriteProjects extends React.PureComponent<StateProps> {
<p>{translate('projects.no_favorite_projects.how_to_add_projects')}</p>
<div className="huge-spacer-top">
<Button onClick={this.onAnalyzeProjectClick}>
{isSonarCloud()
? translate('provisioning.create_new_project')
: translate('my_account.analyze_new_project')}
{translate('provisioning.analyze_new_project')}
</Button>

<Dropdown

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

@@ -48,7 +48,7 @@ exports[`renders for SonarCloud 1`] = `
<Button
onClick={[Function]}
>
provisioning.create_new_project
provisioning.analyze_new_project
</Button>
<Dropdown
className="display-inline-block big-spacer-left"

+ 1
- 1
server/sonar-web/src/main/js/components/controls/react-select.css View File

@@ -112,7 +112,7 @@

.Select-value svg,
.Select-value img {
padding-top: 3px;
padding-top: 4px;
}

.Select-option svg,

+ 16
- 14
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -152,6 +152,7 @@ see_all=See All
select_verb=Select
selected=Selected
set=Set
setup=Setup
severity=Severity
shared=Shared
start_date=Start Date
@@ -1499,13 +1500,12 @@ my_account.organizations.no_results=You are not a member of any organizations ye
my_account.create_organization=Create Organization
my_account.search_project=Search Project
my_account.set_notifications_for=Set notifications for
my_account.analyze_new_project=Analyze new project
my_account.create_new_portfolio_application=Create new portfolio / application
my_account.create_new.VW=Create new portfolio
my_account.create_new.APP=Create new application
my_account.create_new_organization=Create new organization
my_account.create_new_project_or_organization=Create new project or organization
my_account.create_new_project_portfolio_or_application=Create new project, portfolio or application
my_account.create_new_project_or_organization=Analyze new project or create new organization
my_account.create_new_project_portfolio_or_application=Analyze new project / Create new portfolio or application


#------------------------------------------------------------------------------
@@ -1513,7 +1513,7 @@ my_account.create_new_project_portfolio_or_application=Create new project, portf
# PROJECT PROVISIONING
#
#------------------------------------------------------------------------------
provisioning.create_new_project=Create new project
provisioning.analyze_new_project=Analyze new project
provisioning.no_analysis=No analysis has been performed since creation. The only available section is the configuration.
provisioning.no_analysis.delete=Either you should retry analysis or simply {link}.
provisioning.no_analysis.delete_project=delete the project
@@ -2717,15 +2717,17 @@ onboarding.project_analysis.simply_link=Simply {link}.
onboarding.project_analysis.suggestions.bitbucket=If you are using Bitbucket Cloud Pipelines, the SonarCloud App makes it easier to run these commands with your CI process.
onboarding.project_analysis.suggestions.github=If you are using Travis CI, the SonarCloud Travis Add-on makes it easier to run these commands with your CI process.

onboarding.create_project.header=Create project(s)
onboarding.create_project.already_imported=Repository already imported
onboarding.create_project.create_manually=Create manually
onboarding.create_project.header=Analyze projects
onboarding.create_project.setup_manually=Setup manually
onboarding.create_project.create_new_org=I want to create another organization
onboarding.create_project.import_new_org=I want to import another organization
onboarding.create_project.install_app_x.button=Install SonarCloud {0} application
onboarding.create_project.install_app_description.bitbucket=We need you to install the SonarCloud Bitbucket application on one of your team in order to select which repositories you want to analyze.
onboarding.create_project.install_app_description.github=We need you to install the SonarCloud GitHub application on one of your organization in order to select which repositories you want to analyze.
onboarding.create_project.organization=Organization
onboarding.create_project.project_key=Project key
onboarding.create_project.project_name=Project name
onboarding.create_project.repository_imported=Already imported: {link}
onboarding.create_project.see_project=See the project
onboarding.create_project.select_repositories=Select repositories

onboarding.create_organization.page.header=Create Organization
@@ -2744,6 +2746,7 @@ onboarding.create_organization.display_name.error=The provided value doesn't mat
onboarding.create_organization.avatar=Avatar
onboarding.create_organization.avatar.description=Url of a small image that represents the organization (preferably 30px height).
onboarding.create_organization.avatar.error=The value must be a valid url.
onboarding.create_organization.avatar.placeholder=Default avatar
onboarding.create_organization.url=URL
onboarding.create_organization.url.error=The value must be a valid url.
onboarding.create_organization.description=Description
@@ -2765,15 +2768,14 @@ onboarding.import_organization.org_not_found.tips_2=Try to uninstall and re-inst
onboarding.import_organization.choose_organization=Choose an organization...
onboarding.import_organization.choose_organization_button.bitbucket=Choose a team on Bitbucket
onboarding.import_organization.choose_organization_button.github=Choose an organization on GitHub
onboarding.import_organization.installing=Installation of the ALM application in progress...
onboarding.import_organization.installing.bitbucket=Installation of the Bitbucket application in progress..
onboarding.import_organization.installing.github=Installation of the GitHub application in progress...
onboarding.import_organization.installing=Finalize installation of the ALM application...
onboarding.import_organization.installing.bitbucket=Finalize installation of the Bitbucket application..
onboarding.import_organization.installing.github=Finalize installation of the GitHub application...
onboarding.import_organization.personal.page.header=Bind to your personal organization
onboarding.import_organization.personal.page.description=An organization is a space where a team or a whole company can collaborate accross many projects.
onboarding.import_organization.private.disabled=It is not possible to automatically import private repositories yet.
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.bind_existing=Bind to an existing sonarcloud organization
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_x=Import {avatar} {name} into SonarCloud organization

Loading…
Cancel
Save