package org.sonar.db.alm;
import java.util.List;
-import java.util.Objects;
import java.util.Optional;
import javax.annotation.Nullable;
import org.sonar.api.utils.System2;
this.uuidFactory = uuidFactory;
}
+ public Optional<AlmAppInstallDto> selectByUuid(DbSession dbSession, String uuid) {
+ AlmAppInstallMapper mapper = getMapper(dbSession);
+ return Optional.ofNullable(mapper.selectByUuid(uuid));
+ }
+
public Optional<AlmAppInstallDto> selectByOwnerId(DbSession dbSession, ALM alm, String ownerId) {
checkAlm(alm);
checkOwnerId(ownerId);
@CheckForNull
AlmAppInstallDto selectByInstallationId(@Param("almId") String almId, @Param("installId") String installId);
+ @CheckForNull
+ AlmAppInstallDto selectByUuid(@Param("uuid") String uuid);
+
List<AlmAppInstallDto> selectAllWithNoOwnerType();
void insert(@Param("uuid") String uuid, @Param("almId") String almId, @Param("ownerId") String ownerId,
</select>
<select id="selectByInstallationId" parameterType="Map" resultType="org.sonar.db.alm.AlmAppInstallDto">
- select
- <include refid="sqlColumns"/>
+ select <include refid="sqlColumns"/>
from
alm_app_installs
where
and install_id = #{installId, jdbcType=VARCHAR}
</select>
+ <select id="selectByUuid" parameterType="Map" resultType="org.sonar.db.alm.AlmAppInstallDto">
+ select <include refid="sqlColumns"/>
+ from
+ alm_app_installs
+ where
+ uuid = #{uuid, jdbcType=VARCHAR}
+ </select>
+
<select id="selectAllWithNoOwnerType" parameterType="Map" resultType="org.sonar.db.alm.AlmAppInstallDto">
select <include refid="sqlColumns" />
from
- alm_app_installs
+ alm_app_installs
where
- is_owner_user is null
+ is_owner_user is null
</select>
<insert id="insert" parameterType="Map" useGeneratedKeys="false">
private UuidFactory uuidFactory = mock(UuidFactory.class);
private AlmAppInstallDao underTest = new AlmAppInstallDao(system2, uuidFactory);
+ @Test
+ public void selectByUuid() {
+ when(uuidFactory.create()).thenReturn(A_UUID);
+ when(system2.now()).thenReturn(DATE);
+ underTest.insertOrUpdate(dbSession, GITHUB, A_OWNER, true, AN_INSTALL);
+
+ assertThat(underTest.selectByUuid(dbSession, A_UUID).get())
+ .extracting(AlmAppInstallDto::getUuid, AlmAppInstallDto::getAlm, AlmAppInstallDto::getInstallId, AlmAppInstallDto::getOwnerId,
+ AlmAppInstallDto::getCreatedAt, AlmAppInstallDto::getUpdatedAt)
+ .contains(A_UUID, GITHUB, A_OWNER, AN_INSTALL, DATE, DATE);
+
+ assertThat(underTest.selectByUuid(dbSession, "foo")).isNotPresent();
+ }
+
@Test
public void selectByOwnerId() {
when(uuidFactory.create()).thenReturn(A_UUID);
);
}
-export function getRepositories(): Promise<{
- almIntegration: {
- installed: boolean;
- installationUrl: string;
- };
- repositories: AlmRepository[];
-}> {
- return getJSON('/api/alm_integration/list_repositories').catch(throwGlobalError);
+export function getRepositories(data: {
+ organization: string;
+}): Promise<{ repositories: AlmRepository[] }> {
+ return getJSON('/api/alm_integration/list_repositories', data).catch(throwGlobalError);
}
export function provisionProject(data: {
installationKeys: string[];
+ organization: string;
}): Promise<{ projects: Array<{ projectKey: string }> }> {
return postJSON('/api/alm_integration/provision_projects', {
...data,
openProjectOnboarding = (organization?: string) => {
if (isSonarCloud()) {
this.setState({ automatic: false, modal: undefined });
- this.props.router.push({ pathname: `/projects/create`, query: { organization } });
+ this.props.router.push({ pathname: `/projects/create`, state: { organization } });
} else {
this.setState({ modal: ModalKey.projectOnboarding });
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 handleRequiredAuthentication from '../handleRequiredAuthentication';
+import getHistory from '../getHistory';
+
+jest.mock('../getHistory', () => ({
+ default: jest.fn()
+}));
+
+it('should not render for anonymous user', () => {
+ const replace = jest.fn();
+ (getHistory as jest.Mock<any>).mockReturnValue({ replace });
+ handleRequiredAuthentication();
+ expect(replace).toBeCalledWith(expect.objectContaining({ pathname: '/sessions/new' }));
+});
import Step from '../../tutorials/components/Step';
import { translate } from '../../../helpers/l10n';
import { AlmApplication } from '../../../app/types';
+import { Alert } from '../../../components/ui/Alert';
interface Props {
almApplication: AlmApplication;
return (
<div className="boxed-group-inner">
{almInstallId && (
- <span className="alert alert-warning markdown big-spacer-bottom width-60">
+ <Alert className="markdown big-spacer-bottom width-60" variant="warning">
{translate('onboarding.create_organization.import_org_not_found')}
<ul>
<li>{translate('onboarding.create_organization.import_org_not_found.tips_1')}</li>
<li>{translate('onboarding.create_organization.import_org_not_found.tips_2')}</li>
</ul>
- </span>
+ </Alert>
)}
<IdentityProviderLink
className="display-inline-block"
import { FormattedMessage } from 'react-intl';
import { Link, withRouter, WithRouterProps } from 'react-router';
import { formatPrice, parseQuery } from './utils';
-import { whenLoggedIn } from './whenLoggedIn';
import AutoOrganizationCreate from './AutoOrganizationCreate';
import ManualOrganizationCreate from './ManualOrganizationCreate';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import Tabs from '../../../components/controls/Tabs';
+import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
import { getAlmAppInfo, getAlmOrganization } from '../../../api/alm-integration';
import { getSubscriptionPlans } from '../../../api/billing';
import {
subscriptionPlans?: SubscriptionPlan[];
}
+type TabKeys = 'auto' | 'manual';
+
interface LocationState {
paid?: boolean;
- tab?: 'auto' | 'manual';
+ tab?: TabKeys;
}
export class CreateOrganization extends React.PureComponent<Props & WithRouterProps, State> {
});
};
- onTabChange = (tab: 'auto' | 'manual') => {
+ onTabChange = (tab: TabKeys) => {
this.updateUrl({ tab });
};
updateUrl = (state: Partial<LocationState> = {}) => {
this.props.router.replace({
pathname: this.props.location.pathname,
+ query: this.props.location.query,
state: { ...(this.props.location.state || {}), ...state }
});
};
) : (
<>
{almApplication && (
- <Tabs
+ <Tabs<TabKeys>
onChange={this.onTabChange}
selected={showManualTab ? 'manual' : 'auto'}
tabs={[
almApplication.key
)}
<span
- className={classNames(
- 'rounded alert alert-small spacer-left display-inline-block',
- {
- 'alert-info': !showManualTab,
- 'alert-muted': showManualTab
- }
- )}>
+ className={classNames('beta-badge spacer-left', {
+ 'is-muted': showManualTab
+ })}>
{translate('beta')}
</span>
</>
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
-import AlertErrorIcon from '../../../components/icons-components/AlertErrorIcon';
-import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon';
-
-interface Props {
- description?: string;
- dirty: boolean;
- children: (inputProps: React.InputHTMLAttributes<Element>) => React.ReactElement<any>;
- error: string | undefined;
- id: string;
- isSubmitting: boolean;
- isValidating: boolean;
- label: React.ReactNode;
- name: string;
- onBlur: React.FocusEventHandler;
- onChange: React.ChangeEventHandler;
- required?: boolean;
- touched?: boolean;
- value: string;
-}
-
-export default function OrganizationDetailsInput(props: Props) {
- const hasError = props.dirty && props.touched && !props.isValidating && props.error !== undefined;
- const isValid = props.dirty && props.touched && props.error === undefined;
- return (
- <div>
- <label htmlFor={props.id}>
- <strong>{props.label}</strong>
- {props.required && <em className="mandatory">*</em>}
- </label>
- <div className="little-spacer-top spacer-bottom">
- {props.children({
- className: classNames('input-super-large', 'text-middle', {
- 'is-invalid': hasError,
- 'is-valid': isValid
- }),
- disabled: props.isSubmitting,
- id: props.id,
- name: props.name,
- onBlur: props.onBlur,
- onChange: props.onChange,
- type: 'text',
- value: props.value
- })}
- {hasError && (
- <>
- <AlertErrorIcon className="spacer-left text-middle" />
- <span className="little-spacer-left text-danger text-middle">{props.error}</span>
- </>
- )}
- {isValid && <AlertSuccessIcon className="spacer-left text-middle" />}
- </div>
- {props.description && <div className="note abs-width-400">{props.description}</div>}
- </div>
- );
-}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { isWebUri } from 'valid-url';
-import OrganizationDetailsInput from './OrganizationDetailsInput';
+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 Step from '../../tutorials/components/Step';
-import ValidationForm, { ChildrenProps } from '../../../components/controls/ValidationForm';
import { translate } from '../../../helpers/l10n';
import { ResetButtonLink, SubmitButton } from '../../../components/ui/buttons';
import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon';
import DropdownIcon from '../../../components/icons-components/DropdownIcon';
-import { getHostUrl } from '../../../helpers/urls';
import { OrganizationBase } from '../../../app/types';
-import { getOrganization } from '../../../api/organizations';
-type Values = Required<OrganizationBase>;
-
-const initialValues: Values = {
- avatar: '',
- description: '',
- name: '',
- key: '',
- url: ''
-};
+type RequiredOrganization = Required<OrganizationBase>;
interface Props {
description?: React.ReactNode;
finished: boolean;
- onContinue: (organization: Required<OrganizationBase>) => Promise<void>;
+ onContinue: (organization: RequiredOrganization) => Promise<void>;
onOpen: () => void;
open: boolean;
organization?: OrganizationBase & { key: string };
interface State {
additional: boolean;
+ avatar?: string;
+ description?: string;
+ key?: string;
+ name?: string;
+ submitting: boolean;
+ url?: string;
}
+type ValidState = Pick<State, Exclude<keyof State, RequiredOrganization>> & RequiredOrganization;
+
export default class OrganizationDetailsStep extends React.PureComponent<Props, State> {
- state: State = { additional: false };
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+ const { organization } = props;
+ this.state = {
+ additional: false,
+ avatar: (organization && organization.avatar) || '',
+ description: (organization && organization.description) || '',
+ key: (organization && organization.key) || undefined,
+ name: (organization && organization.name) || '',
+ submitting: false,
+ url: (organization && organization.url) || ''
+ };
+ }
- getInitialValues = (): Values => {
- const { organization } = this.props;
- if (organization) {
- return {
- avatar: organization.avatar || '',
- description: organization.description || '',
- name: organization.name,
- key: organization.key,
- url: organization.url || ''
- };
- } else {
- return initialValues;
- }
- };
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ canSubmit(state: State): state is ValidState {
+ return Boolean(
+ state.key !== undefined &&
+ state.name !== undefined &&
+ state.description !== undefined &&
+ state.avatar !== undefined &&
+ state.url !== undefined
+ );
+ }
handleAdditionalClick = () => {
this.setState(state => ({ additional: !state.additional }));
};
- checkFreeKey = (key: string) => {
- return getOrganization(key).then(organization => organization === undefined, () => true);
+ handleKeyUpdate = (key: string | undefined) => {
+ this.setState({ key });
};
- handleValidate = ({ avatar, name, key, url }: Values) => {
- const errors: { [P in keyof Values]?: string } = {};
+ handleNameUpdate = (name: string | undefined) => {
+ this.setState({ name });
+ };
- if (avatar.length > 0 && !isWebUri(avatar)) {
- errors.avatar = translate('onboarding.create_organization.avatar.error');
- }
+ handleDescriptionUpdate = (description: string | undefined) => {
+ this.setState({ description });
+ };
- if (name.length > 255) {
- errors.name = translate('onboarding.create_organization.display_name.error');
- }
+ handleAvatarUpdate = (avatar: string | undefined) => {
+ this.setState({ avatar });
+ };
- if (key.length > 255 || !/^[a-z0-9][a-z0-9-]*[a-z0-9]?$/.test(key)) {
- errors.key = translate('onboarding.create_organization.organization_name.error');
- }
+ handleUrlUpdate = (url: string | undefined) => {
+ this.setState({ url });
+ };
- if (url.length > 0 && !isWebUri(url)) {
- errors.url = translate('onboarding.create_organization.url.error');
+ handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ const { state } = this;
+ if (this.canSubmit(state)) {
+ this.setState({ submitting: true });
+ this.props
+ .onContinue({
+ avatar: state.avatar,
+ description: state.description,
+ key: state.key,
+ name: state.name,
+ url: state.url
+ })
+ .then(this.stopSubmitting, this.stopSubmitting);
}
+ };
- // don't try to check if the organization key is already taken if the key is invalid
- if (errors.key) {
- return Promise.reject(errors);
+ stopSubmitting = () => {
+ if (this.mounted) {
+ this.setState({ submitting: false });
}
-
- // TODO debounce
- return this.checkFreeKey(key).then(free => {
- if (!free) {
- errors.key = translate('onboarding.create_organization.organization_name.taken');
- }
- return Object.keys(errors).length ? Promise.reject(errors) : Promise.resolve(errors);
- });
};
- renderInnerForm = (props: ChildrenProps<Values>) => {
- const {
- dirty,
- errors,
- handleBlur,
- handleChange,
- isSubmitting,
- isValid,
- isValidating,
- touched,
- values
- } = props;
- const commonProps = {
- dirty,
- isValidating,
- isSubmitting,
- onBlur: handleBlur,
- onChange: handleChange
- };
+ renderForm = () => {
return (
- <>
- <OrganizationDetailsInput
- {...commonProps}
- description={translate('onboarding.create_organization.organization_name.description')}
- error={errors.key}
- id="organization-key"
- label={translate('onboarding.create_organization.organization_name')}
- name="key"
- required={true}
- touched={touched.key}
- value={values.key}>
- {props => (
- <div className="display-inline-flex-baseline">
- <span className="little-spacer-right">
- {getHostUrl().replace(/https*:\/\//, '') + '/organizations/'}
- </span>
- <input autoFocus={true} maxLength={255} {...props} />
- </div>
- )}
- </OrganizationDetailsInput>
- <div className="big-spacer-top">
- <ResetButtonLink onClick={this.handleAdditionalClick}>
- {translate(
- this.state.additional
- ? 'onboarding.create_organization.hide_additional_info'
- : 'onboarding.create_organization.add_additional_info'
- )}
- <DropdownIcon className="little-spacer-left" turned={this.state.additional} />
- </ResetButtonLink>
- </div>
- <div className="js-additional-info" hidden={!this.state.additional}>
- <div className="big-spacer-top">
- <OrganizationDetailsInput
- {...commonProps}
- description={translate('onboarding.create_organization.display_name.description')}
- error={errors.name}
- id="organization-display-name"
- label={translate('onboarding.create_organization.display_name')}
- name="name"
- touched={touched.name && values.name !== ''}
- value={values.name}>
- {props => <input {...props} />}
- </OrganizationDetailsInput>
- </div>
+ <div className="boxed-group-inner">
+ <form id="organization-form" onSubmit={this.handleSubmit}>
+ {this.props.description}
+ <OrganizationKeyInput initialValue={this.state.key} onChange={this.handleKeyUpdate} />
<div className="big-spacer-top">
- <OrganizationDetailsInput
- {...commonProps}
- description={translate('onboarding.create_organization.avatar.description')}
- error={errors.avatar}
- id="organization-avatar"
- label={translate('onboarding.create_organization.avatar')}
- name="avatar"
- touched={touched.avatar && values.avatar !== ''}
- value={values.avatar}>
- {props => (
- <>
- {values.avatar && (
- <img
- alt=""
- className="display-block spacer-bottom rounded"
- src={values.avatar}
- width={48}
- />
- )}
- <input {...props} />
- </>
+ <ResetButtonLink onClick={this.handleAdditionalClick}>
+ {translate(
+ this.state.additional
+ ? 'onboarding.create_organization.hide_additional_info'
+ : 'onboarding.create_organization.add_additional_info'
)}
- </OrganizationDetailsInput>
+ <DropdownIcon className="little-spacer-left" turned={this.state.additional} />
+ </ResetButtonLink>
</div>
- <div className="big-spacer-top">
- <OrganizationDetailsInput
- {...commonProps}
- error={errors.description}
- id="organization-description"
- label={translate('description')}
- name="description"
- touched={touched.description && values.description !== ''}
- value={values.description}>
- {props => <textarea {...props} maxLength={256} rows={3} />}
- </OrganizationDetailsInput>
+ <div className="js-additional-info" hidden={!this.state.additional}>
+ <div className="big-spacer-top">
+ <OrganizationNameInput
+ initialValue={this.state.name}
+ onChange={this.handleNameUpdate}
+ />
+ </div>
+ <div className="big-spacer-top">
+ <OrganizationAvatarInput
+ initialValue={this.state.avatar}
+ onChange={this.handleDescriptionUpdate}
+ />
+ </div>
+ <div className="big-spacer-top">
+ <OrganizationDescriptionInput
+ initialValue={this.state.description}
+ onChange={this.handleAvatarUpdate}
+ />
+ </div>
+ <div className="big-spacer-top">
+ <OrganizationUrlInput initialValue={this.state.url} onChange={this.handleUrlUpdate} />
+ </div>
</div>
<div className="big-spacer-top">
- <OrganizationDetailsInput
- {...commonProps}
- error={errors.url}
- id="organization-url"
- label={translate('onboarding.create_organization.url')}
- name="url"
- touched={touched.url && values.url !== ''}
- value={values.url}>
- {props => <input {...props} />}
- </OrganizationDetailsInput>
+ <SubmitButton disabled={this.state.submitting || !this.canSubmit(this.state)}>
+ {this.props.submitText}
+ </SubmitButton>
</div>
- </div>
- <div className="big-spacer-top">
- <SubmitButton disabled={isSubmitting || !isValid}>{this.props.submitText}</SubmitButton>
- </div>
- </>
- );
- };
-
- renderForm = () => {
- return (
- <div className="boxed-group-inner">
- {this.props.description}
- <ValidationForm<Values>
- initialValues={this.getInitialValues()}
- isInitialValid={this.props.organization !== undefined}
- onSubmit={this.props.onContinue}
- validate={this.handleValidate}>
- {this.renderInnerForm}
- </ValidationForm>
+ </form>
</div>
);
};
*/
import * as React from 'react';
import BillingFormShim from './BillingFormShim';
-import { withCurrentUser } from './withCurrentUser';
import PlanSelect, { Plan } from './PlanSelect';
import Step from '../../tutorials/components/Step';
+import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
import { translate } from '../../../helpers/l10n';
import { getExtensionStart } from '../../../app/components/extensions/utils';
import { SubscriptionPlan } from '../../../app/types';
});
it('should display a warning message', () => {
- expect(shallowRender({ almInstallId: 'foo' }).find('.alert-warning')).toMatchSnapshot();
+ expect(shallowRender({ almInstallId: 'foo' }).find('Alert')).toMatchSnapshot();
});
function shallowRender(props: Partial<ChooseRemoteOrganizationStep['props']> = {}) {
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
-import OrganizationDetailsInput from '../OrganizationDetailsInput';
-
-it('should render', () => {
- const render = jest.fn().mockReturnValue(<div />);
- expect(
- shallow(
- <OrganizationDetailsInput
- dirty={true}
- error="This field is bad!"
- id="field"
- isSubmitting={true}
- isValidating={false}
- label="Label"
- name="field"
- onBlur={jest.fn()}
- onChange={jest.fn()}
- required={true}
- touched={true}
- value="foo">
- {render}
- </OrganizationDetailsInput>
- )
- ).toMatchSnapshot();
- expect(render).toBeCalledWith(
- expect.objectContaining({
- className: 'input-super-large text-middle is-invalid',
- disabled: true,
- id: 'field',
- name: 'field',
- type: 'text',
- value: 'foo'
- })
- );
-});
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { shallow, ShallowWrapper } from 'enzyme';
+import { shallow } from 'enzyme';
import OrganizationDetailsStep from '../OrganizationDetailsStep';
-import { click } from '../../../../helpers/testUtils';
+import { click, submit } from '../../../../helpers/testUtils';
import { getOrganization } from '../../../../api/organizations';
jest.mock('../../../../api/organizations', () => ({
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.dive()).toMatchSnapshot();
- expect(getForm(wrapper)).toMatchSnapshot();
expect(
- getForm(wrapper)
+ wrapper
+ .dive()
.find('.js-additional-info')
.prop('hidden')
).toBe(true);
- click(getForm(wrapper).find('ResetButtonLink'));
+ click(wrapper.dive().find('ResetButtonLink'));
wrapper.update();
expect(
- getForm(wrapper)
+ wrapper
+ .dive()
.find('.js-additional-info')
.prop('hidden')
).toBe(false);
});
-it('should validate', async () => {
+it('should validate before submit', () => {
const wrapper = shallow(
<OrganizationDetailsStep
finished={false}
);
const instance = wrapper.instance() as OrganizationDetailsStep;
- await expect(
- instance.handleValidate({
+ expect(
+ instance.canSubmit({
+ additional: false,
avatar: '',
description: '',
name: '',
key: 'foo',
+ submitting: false,
url: ''
})
- ).resolves.toEqual({});
+ ).toBe(true);
- await expect(
- instance.handleValidate({
+ expect(
+ instance.canSubmit({
+ additional: false,
avatar: '',
description: '',
name: '',
- key: 'x'.repeat(256),
+ key: undefined,
+ submitting: false,
url: ''
})
- ).rejects.toEqual({
- key: 'onboarding.create_organization.organization_name.error'
- });
+ ).toBe(false);
- await expect(
- instance.handleValidate({
- avatar: 'bla',
+ expect(
+ instance.canSubmit({
+ additional: false,
+ avatar: undefined,
description: '',
name: '',
key: 'foo',
+ submitting: false,
url: ''
})
- ).rejects.toEqual({ avatar: 'onboarding.create_organization.avatar.error' });
-
- await expect(
- instance.handleValidate({
- avatar: '',
- description: '',
- name: 'x'.repeat(256),
- key: 'foo',
- url: ''
- })
- ).rejects.toEqual({
- name: 'onboarding.create_organization.display_name.error'
- });
-
- await expect(
- instance.handleValidate({
- avatar: '',
- description: '',
- name: '',
- key: 'foo',
- url: 'bla'
- })
- ).rejects.toEqual({
- url: 'onboarding.create_organization.url.error'
- });
+ ).toBe(false);
- (getOrganization as jest.Mock).mockResolvedValue({});
- await expect(
- instance.handleValidate({
- avatar: '',
- description: '',
- name: '',
- key: 'foo',
- url: ''
- })
- ).rejects.toEqual({
- key: 'onboarding.create_organization.organization_name.taken'
- });
+ instance.canSubmit = jest.fn() as any;
+ submit(wrapper.dive().find('form'));
+ expect(instance.canSubmit).toHaveBeenCalled();
});
-it('should render result', () => {
+it.only('should render result', () => {
const wrapper = shallow(
<OrganizationDetailsStep
finished={true}
submitText="continue"
/>
);
- expect(wrapper.dive()).toMatchSnapshot();
+ expect(wrapper.dive().find('.boxed-group-actions')).toMatchSnapshot();
+ expect(
+ wrapper
+ .dive()
+ .find('.hidden')
+ .exists()
+ ).toBe(true);
});
-
-function getForm(wrapper: ShallowWrapper) {
- return wrapper
- .dive()
- .find('ValidationForm')
- .dive()
- .dive()
- .children();
-}
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should display a warning message 1`] = `
-<span
- className="alert alert-warning markdown big-spacer-bottom width-60"
+<Alert
+ className="markdown big-spacer-bottom width-60"
+ variant="warning"
>
onboarding.create_organization.import_org_not_found
<ul>
onboarding.create_organization.import_org_not_found.tips_2
</li>
</ul>
-</span>
+</Alert>
`;
exports[`should render 1`] = `
"node": <React.Fragment>
onboarding.create_organization.import_organization.github
<span
- className="rounded alert alert-small spacer-left display-inline-block alert-info"
+ className="beta-badge spacer-left"
>
beta
</span>
"node": <React.Fragment>
onboarding.create_organization.import_organization.github
<span
- className="rounded alert alert-small spacer-left display-inline-block alert-info"
+ className="beta-badge spacer-left"
>
beta
</span>
"node": <React.Fragment>
onboarding.create_organization.import_organization.github
<span
- className="rounded alert alert-small spacer-left display-inline-block alert-info"
+ className="beta-badge spacer-left"
>
beta
</span>
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render 1`] = `
-<div>
- <label
- htmlFor="field"
- >
- <strong>
- Label
- </strong>
- <em
- className="mandatory"
- >
- *
- </em>
- </label>
- <div
- className="little-spacer-top spacer-bottom"
- >
- <div />
- <AlertErrorIcon
- className="spacer-left text-middle"
- />
- <span
- className="little-spacer-left text-danger text-middle"
- >
- This field is bad!
- </span>
- </div>
-</div>
-`;
<div
className="boxed-group-inner"
>
+<<<<<<< HEAD
<ValidationForm
initialValues={
Object {
</SubmitButton>
</div>
</form>
+=======
+ <form
+ id="organization-form"
+ onSubmit={[Function]}
+ >
+ <OrganizationKeyInput
+ onChange={[Function]}
+ />
+ <div
+ className="big-spacer-top"
+ >
+ <ResetButtonLink
+ onClick={[Function]}
+ >
+ onboarding.create_organization.add_additional_info
+ <DropdownIcon
+ className="little-spacer-left"
+ turned={false}
+ />
+ </ResetButtonLink>
+ </div>
+ <div
+ className="js-additional-info"
+ hidden={true}
+ >
+ <div
+ className="big-spacer-top"
+ >
+ <OrganizationNameInput
+ initialOrgName=""
+ onChange={[Function]}
+ />
+ </div>
+ <div
+ className="big-spacer-top"
+ >
+ <OrganizationAvatarInput
+ initialOrgAvatar=""
+ onChange={[Function]}
+ />
+ </div>
+ <div
+ className="big-spacer-top"
+ >
+ <OrganizationDescriptionInput
+ initialOrgDescription=""
+ onChange={[Function]}
+ />
+ </div>
+ <div
+ className="big-spacer-top"
+ >
+ <OrganizationUrlInput
+ initialOrgUrl=""
+ onChange={[Function]}
+ />
+ </div>
+ </div>
+ <div
+ className="big-spacer-top"
+ >
+ <SubmitButton
+ disabled={true}
+ >
+ continue
+ </SubmitButton>
+ </div>
+ </form>
+ </div>
+ </div>
+</div>
+>>>>>>> 116a4ec872... SONAR-11322 Import repos from bound organizations
`;
exports[`should render result 1`] = `
<div
- className="boxed-group onboarding-step is-finished"
- onClick={[Function]}
- role="button"
- tabIndex={0}
+ className="boxed-group-actions display-flex-center"
>
- <div
- className="onboarding-step-number"
- >
- 1
- </div>
- <div
- className="boxed-group-actions display-flex-center"
- >
- <AlertSuccessIcon
- className="spacer-right"
- />
- <strong
- className="text-limited"
- >
- org
- </strong>
- </div>
- <div
- className="boxed-group-header"
- >
- <h2>
- onboarding.create_organization.enter_org_details
- </h2>
- </div>
- <div
- className="boxed-group-inner"
+ <AlertSuccessIcon
+ className="spacer-right"
/>
- <div
- className="hidden"
+ <strong
+ className="text-limited"
>
- <div
- className="boxed-group-inner"
- >
- <ValidationForm
- initialValues={
- Object {
- "avatar": "",
- "description": "",
- "key": "org",
- "name": "Organization",
- "url": "",
- }
- }
- isInitialValid={true}
- onSubmit={[MockFunction]}
- validate={[Function]}
- >
- <Component />
- </ValidationForm>
- </div>
- </div>
+ org
+ </strong>
</div>
`;
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { shallow, ShallowWrapper } from 'enzyme';
-import { createStore } from 'redux';
-import { whenLoggedIn } from '../whenLoggedIn';
-import { mockRouter } from '../../../../helpers/testUtils';
-
-class X extends React.Component {
- render() {
- return <div />;
- }
-}
-
-const UnderTest = whenLoggedIn(X);
-
-it('should render for logged in user', () => {
- const store = createStore(state => state, { users: { currentUser: { isLoggedIn: true } } });
- const wrapper = shallow(<UnderTest />, { context: { store } });
- expect(getRenderedType(wrapper)).toBe(X);
-});
-
-it('should not render for anonymous user', () => {
- const store = createStore(state => state, { users: { currentUser: { isLoggedIn: false } } });
- const router = mockRouter({ replace: jest.fn() });
- const wrapper = shallow(<UnderTest />, { context: { store, router } });
- expect(getRenderedType(wrapper)).toBe(null);
- expect(router.replace).toBeCalledWith(expect.objectContaining({ pathname: '/sessions/new' }));
-});
-
-function getRenderedType(wrapper: ShallowWrapper) {
- return wrapper
- .dive()
- .dive()
- .dive()
- .type();
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
-import { createStore } from 'redux';
-import { withCurrentUser } from '../withCurrentUser';
-import { CurrentUser } from '../../../../app/types';
-
-class X extends React.Component<{ currentUser: CurrentUser }> {
- render() {
- return <div />;
- }
-}
-
-const UnderTest = withCurrentUser(X);
-
-it('should pass logged in user', () => {
- const currentUser = { isLoggedIn: false };
- const store = createStore(state => state, { users: { currentUser } });
- const wrapper = shallow(<UnderTest />, { context: { store } });
- expect(wrapper.dive().type()).toBe(X);
- expect(wrapper.dive().prop('currentUser')).toBe(currentUser);
-});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
+import { isWebUri } from 'valid-url';
+import ValidationInput from '../../../../components/controls/ValidationInput';
+import { translate } from '../../../../helpers/l10n';
+import OrganizationAvatar from '../../../../components/common/OrganizationAvatar';
+
+interface Props {
+ initialValue?: string;
+ name?: string;
+ onChange: (value: string | undefined) => void;
+}
+
+interface State {
+ editing: boolean;
+ error?: string;
+ touched: boolean;
+ value: string;
+}
+
+export default class OrganizationAvatarInput extends React.PureComponent<Props, State> {
+ state: State = { error: undefined, editing: false, touched: false, value: '' };
+
+ componentDidMount() {
+ if (this.props.initialValue) {
+ const value = this.props.initialValue;
+ const error = this.validateUrl(value);
+ this.setState({ error, touched: Boolean(error), value });
+ }
+ }
+
+ handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const value = event.currentTarget.value.trim();
+ const error = this.validateUrl(value);
+ this.setState({ error, touched: true, value });
+ this.props.onChange(error === undefined ? value : undefined);
+ };
+
+ handleBlur = () => {
+ this.setState({ editing: false });
+ };
+
+ handleFocus = () => {
+ this.setState({ editing: true });
+ };
+
+ validateUrl(url: string) {
+ if (url.length > 0 && !isWebUri(url)) {
+ return translate('onboarding.create_organization.url.error');
+ }
+ return undefined;
+ }
+
+ render() {
+ const isInvalid = this.state.touched && !this.state.editing && this.state.error !== undefined;
+ const isValidUrl = this.state.error === undefined && this.state.value !== '';
+ const isValid = this.state.touched && isValidUrl;
+ return (
+ <ValidationInput
+ description={translate('onboarding.create_organization.avatar.description')}
+ error={this.state.error}
+ id="organization-avatar"
+ isInvalid={isInvalid}
+ isValid={isValid}
+ label={translate('onboarding.create_organization.avatar')}>
+ <>
+ {(isValidUrl || this.props.name) && (
+ <OrganizationAvatar
+ className="display-block spacer-bottom"
+ organization={{
+ avatar: isValidUrl ? this.state.value : undefined,
+ name: this.props.name || ''
+ }}
+ />
+ )}
+ <input
+ className={classNames('input-super-large', 'text-middle', {
+ 'is-invalid': isInvalid,
+ 'is-valid': isValid
+ })}
+ id="organization-display-name"
+ onBlur={this.handleBlur}
+ onChange={this.handleChange}
+ onFocus={this.handleFocus}
+ type="text"
+ value={this.state.value}
+ />
+ </>
+ </ValidationInput>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
+import ValidationInput from '../../../../components/controls/ValidationInput';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+ initialValue?: string;
+ onChange: (value: string | undefined) => void;
+}
+
+interface State {
+ editing: boolean;
+ error?: string;
+ touched: boolean;
+ value: string;
+}
+
+export default class OrganizationDescriptionInput extends React.PureComponent<Props, State> {
+ state: State = { error: undefined, editing: false, touched: false, value: '' };
+
+ componentDidMount() {
+ if (this.props.initialValue) {
+ const error = this.validateDescription(this.props.initialValue);
+ this.setState({ error, touched: Boolean(error), value: this.props.initialValue });
+ }
+ }
+
+ handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
+ const { value } = event.currentTarget;
+ const error = this.validateDescription(value);
+ this.setState({ error, touched: true, value });
+ this.props.onChange(error === undefined ? value : undefined);
+ };
+
+ handleBlur = () => {
+ this.setState({ editing: false });
+ };
+
+ handleFocus = () => {
+ this.setState({ editing: true });
+ };
+
+ validateDescription(description: string) {
+ if (description.length > 256) {
+ return translate('onboarding.create_organization.description.error');
+ }
+ return undefined;
+ }
+
+ render() {
+ const isInvalid = this.state.touched && !this.state.editing && this.state.error !== undefined;
+ const isValid = this.state.touched && this.state.error === undefined && this.state.value !== '';
+ return (
+ <ValidationInput
+ error={this.state.error}
+ id="organization-display-name"
+ isInvalid={isInvalid}
+ isValid={isValid}
+ label={translate('onboarding.create_organization.description')}>
+ <textarea
+ className={classNames('input-super-large', 'text-middle', {
+ 'is-invalid': isInvalid,
+ 'is-valid': isValid
+ })}
+ id="organization-description"
+ maxLength={256}
+ onBlur={this.handleBlur}
+ onChange={this.handleChange}
+ onFocus={this.handleFocus}
+ rows={3}
+ value={this.state.value}
+ />
+ </ValidationInput>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
+import { debounce } from 'lodash';
+import { getOrganization } from '../../../../api/organizations';
+import ValidationInput from '../../../../components/controls/ValidationInput';
+import { translate } from '../../../../helpers/l10n';
+import { getHostUrl } from '../../../../helpers/urls';
+
+interface Props {
+ initialValue?: string;
+ onChange: (value: string | undefined) => void;
+}
+
+interface State {
+ editing: boolean;
+ error?: string;
+ touched: boolean;
+ validating: boolean;
+ value: string;
+}
+
+export default class OrganizationKeyInput extends React.PureComponent<Props, State> {
+ mounted = false;
+ constructor(props: Props) {
+ super(props);
+ this.state = { error: undefined, editing: false, touched: false, validating: false, value: '' };
+ this.checkFreeKey = debounce(this.checkFreeKey, 250);
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ if (this.props.initialValue !== undefined) {
+ this.setState({ value: this.props.initialValue });
+ this.validateKey(this.props.initialValue);
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ checkFreeKey = (key: string) => {
+ this.setState({ validating: true });
+ return getOrganization(key)
+ .then(organization => organization === undefined, () => true)
+ .then(
+ free => {
+ if (this.mounted) {
+ if (!free) {
+ this.setState({
+ error: translate('onboarding.create_organization.organization_name.taken'),
+ touched: true,
+ validating: false
+ });
+ this.props.onChange(undefined);
+ } else {
+ this.setState({ error: undefined, validating: false });
+ this.props.onChange(key);
+ }
+ }
+ },
+ () => {}
+ );
+ };
+
+ handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const { value } = event.currentTarget;
+ this.setState({ touched: true, value });
+ this.validateKey(value);
+ };
+
+ handleBlur = () => {
+ this.setState({ editing: false });
+ };
+
+ handleFocus = () => {
+ this.setState({ editing: true });
+ };
+
+ validateKey(key: string) {
+ if (key.length > 255 || !/^[a-z0-9][a-z0-9-]*[a-z0-9]?$/.test(key)) {
+ this.setState({
+ error: translate('onboarding.create_organization.organization_name.error'),
+ touched: true
+ });
+ this.props.onChange(undefined);
+ } else {
+ this.checkFreeKey(key);
+ }
+ }
+
+ render() {
+ const isInvalid = this.state.touched && !this.state.editing && this.state.error !== undefined;
+ const isValid = this.state.touched && !this.state.validating && this.state.error === undefined;
+ return (
+ <ValidationInput
+ error={this.state.error}
+ id="organization-key"
+ isInvalid={isInvalid}
+ isValid={isValid}
+ label={translate('onboarding.create_organization.organization_name')}
+ required={true}>
+ <div className="display-inline-flex-baseline">
+ <span className="little-spacer-right">
+ {getHostUrl().replace(/https*:\/\//, '') + '/organizations/'}
+ </span>
+ <input
+ autoFocus={true}
+ className={classNames('input-super-large', 'text-middle', {
+ '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>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
+import ValidationInput from '../../../../components/controls/ValidationInput';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+ initialValue?: string;
+ onChange: (value: string | undefined) => void;
+}
+
+interface State {
+ editing: boolean;
+ error?: string;
+ touched: boolean;
+ value: string;
+}
+
+export default class OrganizationNameInput extends React.PureComponent<Props, State> {
+ state: State = { error: undefined, editing: false, touched: false, value: '' };
+
+ componentDidMount() {
+ if (this.props.initialValue) {
+ const error = this.validateName(this.props.initialValue);
+ this.setState({ error, touched: Boolean(error), value: this.props.initialValue });
+ }
+ }
+
+ handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const { value } = event.currentTarget;
+ const error = this.validateName(value);
+ this.setState({ error, touched: true, value });
+ this.props.onChange(error === undefined ? value : undefined);
+ };
+
+ handleBlur = () => {
+ this.setState({ editing: false });
+ };
+
+ handleFocus = () => {
+ this.setState({ editing: true });
+ };
+
+ validateName(name: string) {
+ if (name.length > 255) {
+ return translate('onboarding.create_organization.display_name.error');
+ }
+ return undefined;
+ }
+
+ render() {
+ const isInvalid = this.state.touched && !this.state.editing && this.state.error !== undefined;
+ const isValid = this.state.touched && this.state.error === undefined && this.state.value !== '';
+ return (
+ <ValidationInput
+ description={translate('onboarding.create_organization.display_name.description')}
+ error={this.state.error}
+ id="organization-display-name"
+ isInvalid={isInvalid}
+ isValid={isValid}
+ label={translate('onboarding.create_organization.display_name')}>
+ <input
+ className={classNames('input-super-large', 'text-middle', {
+ 'is-invalid': isInvalid,
+ 'is-valid': isValid
+ })}
+ id="organization-display-name"
+ maxLength={255}
+ onBlur={this.handleBlur}
+ onChange={this.handleChange}
+ onFocus={this.handleFocus}
+ type="text"
+ value={this.state.value}
+ />
+ </ValidationInput>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
+import { isWebUri } from 'valid-url';
+import ValidationInput from '../../../../components/controls/ValidationInput';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+ initialValue?: string;
+ onChange: (value: string | undefined) => void;
+}
+
+interface State {
+ editing: boolean;
+ error?: string;
+ touched: boolean;
+ value: string;
+}
+
+export default class OrganizationUrlInput extends React.PureComponent<Props, State> {
+ state: State = { error: undefined, editing: false, touched: false, value: '' };
+
+ componentDidMount() {
+ if (this.props.initialValue) {
+ const value = this.props.initialValue;
+ const error = this.validateUrl(value);
+ this.setState({ error, touched: Boolean(error), value });
+ }
+ }
+
+ handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const value = event.currentTarget.value.trim();
+ const error = this.validateUrl(value);
+ this.setState({ error, touched: true, value });
+ this.props.onChange(error === undefined ? value : undefined);
+ };
+
+ handleBlur = () => {
+ this.setState({ editing: false });
+ };
+
+ handleFocus = () => {
+ this.setState({ editing: true });
+ };
+
+ validateUrl(url: string) {
+ if (url.length > 0 && !isWebUri(url)) {
+ return translate('onboarding.create_organization.url.error');
+ }
+ return undefined;
+ }
+
+ render() {
+ const isInvalid = this.state.touched && !this.state.editing && this.state.error !== undefined;
+ const isValid = this.state.touched && this.state.error === undefined && this.state.value !== '';
+ return (
+ <ValidationInput
+ error={this.state.error}
+ id="organization-url"
+ isInvalid={isInvalid}
+ isValid={isValid}
+ label={translate('onboarding.create_organization.url')}>
+ <input
+ className={classNames('input-super-large', 'text-middle', {
+ 'is-invalid': isInvalid,
+ 'is-valid': isValid
+ })}
+ id="organization-url"
+ onBlur={this.handleBlur}
+ onChange={this.handleChange}
+ onFocus={this.handleFocus}
+ type="text"
+ value={this.state.value}
+ />
+ </ValidationInput>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import OrganizationAvatarInput from '../OrganizationAvatarInput';
+
+it('should render correctly', () => {
+ const wrapper = shallow(
+ <OrganizationAvatarInput initialValue="https://my.avatar" onChange={jest.fn()} />
+ );
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({ touched: true });
+ expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot();
+});
+
+it('should have an error when the avatar url is not valid', () => {
+ expect(
+ shallow(<OrganizationAvatarInput initialValue="whatever" onChange={jest.fn()} />)
+ .find('ValidationInput')
+ .prop('isInvalid')
+ ).toBe(true);
+});
+
+it('should display the fallback avatar when there is no url', () => {
+ expect(
+ shallow(<OrganizationAvatarInput initialValue="" name="Luke Skywalker" onChange={jest.fn()} />)
+ ).toMatchSnapshot();
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import OrganizationDescriptionInput from '../OrganizationDescriptionInput';
+
+it('should render correctly', () => {
+ const wrapper = shallow(
+ <OrganizationDescriptionInput initialValue="My description" onChange={jest.fn()} />
+ );
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({ touched: true });
+ expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot();
+});
+
+it('should have an error when description is too long', () => {
+ expect(
+ shallow(<OrganizationDescriptionInput initialValue={'x'.repeat(260)} onChange={jest.fn()} />)
+ .find('ValidationInput')
+ .prop('isInvalid')
+ ).toBe(true);
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import OrganizationKeyInput from '../OrganizationKeyInput';
+import { getOrganization } from '../../../../../api/organizations';
+import { waitAndUpdate } from '../../../../../helpers/testUtils';
+
+jest.mock('../../../../../api/organizations', () => ({
+ getOrganization: jest.fn().mockResolvedValue(undefined)
+}));
+
+beforeEach(() => {
+ (getOrganization as jest.Mock<any>).mockClear();
+});
+
+it('should render correctly', () => {
+ const wrapper = shallow(<OrganizationKeyInput initialValue="key" onChange={jest.fn()} />);
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({ touched: true });
+ expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot();
+});
+
+it('should not display any status when the key is not defined', async () => {
+ const wrapper = shallow(<OrganizationKeyInput onChange={jest.fn()} />);
+ await waitAndUpdate(wrapper);
+ expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(false);
+ expect(wrapper.find('ValidationInput').prop('isValid')).toBe(false);
+});
+
+it('should have an error when the key is invalid', async () => {
+ const wrapper = shallow(
+ <OrganizationKeyInput initialValue="KEy-with#speci@l_char" onChange={jest.fn()} />
+ );
+ await waitAndUpdate(wrapper);
+ expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(true);
+});
+
+it('should have an error when the key already exists', async () => {
+ (getOrganization as jest.Mock<any>).mockResolvedValue({});
+ const wrapper = shallow(<OrganizationKeyInput initialValue="" onChange={jest.fn()} />);
+ await waitAndUpdate(wrapper);
+ expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(true);
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import OrganizationNameInput from '../OrganizationNameInput';
+
+it('should render correctly', () => {
+ const wrapper = shallow(<OrganizationNameInput initialValue="Org Name" onChange={jest.fn()} />);
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({ touched: true });
+ expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot();
+});
+
+it('should have an error when description is too long', () => {
+ expect(
+ shallow(<OrganizationNameInput initialValue={'x'.repeat(256)} onChange={jest.fn()} />)
+ .find('ValidationInput')
+ .prop('isInvalid')
+ ).toBe(true);
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import OrganizationUrlInput from '../OrganizationUrlInput';
+
+it('should render correctly', () => {
+ const wrapper = shallow(
+ <OrganizationUrlInput initialValue="http://my.website" onChange={jest.fn()} />
+ );
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({ touched: true });
+ expect(wrapper.find('ValidationInput').prop('isValid')).toMatchSnapshot();
+});
+
+it('should have an error when the url is invalid', () => {
+ expect(
+ shallow(<OrganizationUrlInput initialValue="whatever" onChange={jest.fn()} />)
+ .find('ValidationInput')
+ .prop('isInvalid')
+ ).toBe(true);
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display the fallback avatar when there is no url 1`] = `
+<ValidationInput
+ description="onboarding.create_organization.avatar.description"
+ id="organization-avatar"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_organization.avatar"
+>
+ <OrganizationAvatar
+ className="display-block spacer-bottom"
+ organization={
+ Object {
+ "avatar": undefined,
+ "name": "Luke Skywalker",
+ }
+ }
+ />
+ <input
+ className="input-super-large text-middle"
+ id="organization-display-name"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ type="text"
+ value=""
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly 1`] = `
+<ValidationInput
+ description="onboarding.create_organization.avatar.description"
+ id="organization-avatar"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_organization.avatar"
+>
+ <OrganizationAvatar
+ className="display-block spacer-bottom"
+ organization={
+ Object {
+ "avatar": "https://my.avatar",
+ "name": "",
+ }
+ }
+ />
+ <input
+ className="input-super-large text-middle"
+ id="organization-display-name"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ type="text"
+ value="https://my.avatar"
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<ValidationInput
+ id="organization-display-name"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_organization.description"
+>
+ <textarea
+ className="input-super-large text-middle"
+ id="organization-description"
+ maxLength={256}
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ rows={3}
+ value="My description"
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<ValidationInput
+ id="organization-key"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_organization.organization_name"
+ required={true}
+>
+ <div
+ className="display-inline-flex-baseline"
+ >
+ <span
+ className="little-spacer-right"
+ >
+ localhost/organizations/
+ </span>
+ <input
+ autoFocus={true}
+ className="input-super-large text-middle"
+ id="organization-key"
+ maxLength={255}
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ type="text"
+ value="key"
+ />
+ </div>
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<ValidationInput
+ description="onboarding.create_organization.display_name.description"
+ id="organization-display-name"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_organization.display_name"
+>
+ <input
+ className="input-super-large text-middle"
+ id="organization-display-name"
+ maxLength={255}
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ type="text"
+ value="Org Name"
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<ValidationInput
+ id="organization-url"
+ isInvalid={false}
+ isValid={false}
+ label="onboarding.create_organization.url"
+>
+ <input
+ className="input-super-large text-middle"
+ id="organization-url"
+ onBlur={[Function]}
+ onChange={[Function]}
+ onFocus={[Function]}
+ type="text"
+ value="http://my.website"
+ />
+</ValidationInput>
+`;
+
+exports[`should render correctly 2`] = `true`;
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { withRouter, WithRouterProps } from 'react-router';
-import { withCurrentUser } from './withCurrentUser';
-import { CurrentUser } from '../../../app/types';
-import { isLoggedIn } from '../../../helpers/users';
-
-export function whenLoggedIn<P>(WrappedComponent: React.ComponentClass<P>) {
- const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
-
- class Wrapper extends React.Component<P & { currentUser: CurrentUser } & WithRouterProps> {
- static displayName = `whenLoggedIn(${wrappedDisplayName})`;
-
- componentDidMount() {
- if (!isLoggedIn(this.props.currentUser)) {
- const returnTo = window.location.pathname + window.location.search + window.location.hash;
- this.props.router.replace({
- pathname: '/sessions/new',
- query: { return_to: returnTo } // eslint-disable-line camelcase
- });
- }
- }
-
- render() {
- if (isLoggedIn(this.props.currentUser)) {
- return <WrappedComponent {...this.props} />;
- } else {
- return null;
- }
- }
- }
-
- return withCurrentUser(withRouter(Wrapper));
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { connect } from 'react-redux';
-import { CurrentUser } from '../../../app/types';
-import { Store, getCurrentUser } from '../../../store/rootReducer';
-
-export function withCurrentUser<P>(
- WrappedComponent: React.ComponentClass<P & { currentUser: CurrentUser }>
-) {
- const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
-
- class Wrapper extends React.Component<P & { currentUser: CurrentUser }> {
- static displayName = `withCurrentUser(${wrappedDisplayName})`;
-
- render() {
- return <WrappedComponent {...this.props} />;
- }
- }
-
- function mapStateToProps(state: Store) {
- return { currentUser: getCurrentUser(state) };
- }
-
- return connect(mapStateToProps)(Wrapper);
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { Link } from 'react-router';
+import * as theme from '../../../app/theme';
+import Checkbox from '../../../components/controls/Checkbox';
+import CheckIcon from '../../../components/icons-components/CheckIcon';
+import { AlmRepository, IdentityProvider } from '../../../app/types';
+import { getBaseUrl, getProjectUrl } from '../../../helpers/urls';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ identityProvider: IdentityProvider;
+ repository: AlmRepository;
+ selected: boolean;
+ toggleRepository: (repository: AlmRepository) => void;
+}
+
+export default class AlmRepositoryItem extends React.PureComponent<Props> {
+ handleChange = () => {
+ this.props.toggleRepository(this.props.repository);
+ };
+
+ render() {
+ const { identityProvider, repository, selected } = this.props;
+ const alreadyImported = Boolean(repository.linkedProjectKey);
+ return (
+ <>
+ <Checkbox
+ checked={selected || alreadyImported}
+ disabled={alreadyImported}
+ onCheck={this.handleChange}>
+ <img
+ alt={identityProvider.name}
+ className="spacer-left"
+ height={14}
+ src={`${getBaseUrl()}/images/sonarcloud/${identityProvider.key}.svg`}
+ style={{ opacity: alreadyImported ? 0.5 : 1 }}
+ width={14}
+ />
+ <span className="spacer-left">{this.props.repository.label}</span>
+ </Checkbox>
+ {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>
+ </span>
+ )}
+ </>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 RemoteRepositories from './RemoteRepositories';
+import OrganizationSelect from './OrganizationSelect';
+import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
+import { AlmApplication, Organization } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ almApplication: AlmApplication;
+ boundOrganizations: Organization[];
+ onProjectCreate: (projectKeys: string[]) => void;
+ organization?: string;
+}
+
+interface State {
+ selectedOrganization: string;
+}
+
+export default class AutoProjectCreate extends React.PureComponent<Props, State> {
+ constructor(props: Props) {
+ super(props);
+ this.state = { selectedOrganization: this.getInitialSelectedOrganization(props) };
+ }
+
+ 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) {
+ return props.boundOrganizations[0].key;
+ }
+ return '';
+ }
+
+ handleOrganizationSelect = ({ key }: Organization) => {
+ this.setState({ selectedOrganization: key });
+ };
+
+ render() {
+ const { almApplication, boundOrganizations, onProjectCreate } = this.props;
+
+ if (boundOrganizations.length === 0) {
+ return (
+ <>
+ <IdentityProviderLink
+ className="display-inline-block"
+ identityProvider={almApplication}
+ small={true}
+ url={almApplication.installationUrl}>
+ {translate(
+ 'onboarding.create_organization.choose_organization_button',
+ almApplication.key
+ )}
+ </IdentityProviderLink>
+ </>
+ );
+ }
+
+ const { selectedOrganization } = this.state;
+ return (
+ <>
+ <OrganizationSelect
+ autoImport={true}
+ onChange={this.handleOrganizationSelect}
+ organization={selectedOrganization}
+ organizations={this.props.boundOrganizations}
+ />
+ {selectedOrganization && (
+ <RemoteRepositories
+ almApplication={almApplication}
+ onProjectCreate={onProjectCreate}
+ organization={selectedOrganization}
+ />
+ )}
+ </>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
+import { connect } from 'react-redux';
+import { WithRouterProps } from 'react-router';
+import Helmet from 'react-helmet';
+import AutoProjectCreate from './AutoProjectCreate';
+import ManualProjectCreate from './ManualProjectCreate';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import Tabs from '../../../components/controls/Tabs';
+import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
+import { fetchMyOrganizations } from '../../account/organizations/actions';
+import { getMyOrganizations, Store } from '../../../store/rootReducer';
+import { skipOnboarding as skipOnboardingAction } from '../../../store/users';
+import { LoggedInUser, AlmApplication, Organization } from '../../../app/types';
+import { getAlmAppInfo } from '../../../api/alm-integration';
+import { skipOnboarding } from '../../../api/users';
+import { hasAdvancedALMIntegration } from '../../../helpers/almIntegrations';
+import { translate } from '../../../helpers/l10n';
+import { getProjectUrl } from '../../../helpers/urls';
+import '../../../app/styles/sonarcloud.css';
+
+interface StateProps {
+ userOrganizations: Organization[];
+}
+
+interface Props {
+ currentUser: LoggedInUser;
+ fetchMyOrganizations: () => Promise<void>;
+ skipOnboardingAction: () => void;
+}
+
+interface State {
+ almApplication?: AlmApplication;
+ loading: boolean;
+}
+
+type TabKeys = 'auto' | 'manual';
+
+interface LocationState {
+ organization?: string;
+ tab?: TabKeys;
+}
+
+export class CreateProjectPage extends React.PureComponent<
+ Props & StateProps & WithRouterProps,
+ State
+> {
+ mounted = false;
+ state: State = { loading: true };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.props.fetchMyOrganizations();
+ if (hasAdvancedALMIntegration(this.props.currentUser)) {
+ this.fetchAlmApplication();
+ } else {
+ this.setState({ loading: false });
+ }
+ document.body.classList.add('white-page');
+ if (document.documentElement) {
+ document.documentElement.classList.add('white-page');
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ document.body.classList.remove('white-page');
+ if (document.documentElement) {
+ document.documentElement.classList.remove('white-page');
+ }
+ }
+
+ handleProjectCreate = (projectKeys: string[]) => {
+ skipOnboarding().catch(() => {});
+ this.props.skipOnboardingAction();
+ if (projectKeys.length > 1) {
+ this.props.router.push({ pathname: '/projects' });
+ } else if (projectKeys.length === 1) {
+ this.props.router.push(getProjectUrl(projectKeys[0]));
+ }
+ };
+
+ fetchAlmApplication = () => {
+ return getAlmAppInfo().then(
+ ({ application }) => {
+ if (this.mounted) {
+ this.setState({ almApplication: application, loading: false });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ };
+
+ onTabChange = (tab: TabKeys) => {
+ this.updateUrl({ tab });
+ };
+
+ updateUrl = (state: Partial<LocationState> = {}) => {
+ this.props.router.replace({
+ pathname: this.props.location.pathname,
+ query: this.props.location.query,
+ state: { ...(this.props.location.state || {}), ...state }
+ });
+ };
+
+ render() {
+ const { currentUser, location, userOrganizations } = this.props;
+ const { almApplication, loading } = this.state;
+ const state: LocationState = location.state || {};
+ const header = translate('onboarding.create_project.header');
+ const showManualTab = state.tab === 'manual';
+
+ return (
+ <>
+ <Helmet title={header} titleTemplate="%s" />
+ <div className="sonarcloud page page-limited">
+ <header className="page-header">
+ <h1 className="page-title">{header}</h1>
+ </header>
+ {loading ? (
+ <DeferredSpinner />
+ ) : (
+ <>
+ {almApplication && (
+ <Tabs<TabKeys>
+ onChange={this.onTabChange}
+ selected={state.tab || 'auto'}
+ tabs={[
+ {
+ key: 'auto',
+ node: (
+ <>
+ {translate('onboarding.create_project.select_repositories')}
+ <span
+ className={classNames('beta-badge spacer-left', {
+ 'is-muted': showManualTab
+ })}>
+ {translate('beta')}
+ </span>
+ </>
+ )
+ },
+ { key: 'manual', node: translate('onboarding.create_project.create_manually') }
+ ]}
+ />
+ )}
+
+ {showManualTab || !almApplication ? (
+ <ManualProjectCreate
+ currentUser={currentUser}
+ onProjectCreate={this.handleProjectCreate}
+ organization={state.organization}
+ userOrganizations={userOrganizations}
+ />
+ ) : (
+ <AutoProjectCreate
+ almApplication={almApplication}
+ boundOrganizations={userOrganizations.filter(o => o.almId)}
+ onProjectCreate={this.handleProjectCreate}
+ organization={state.organization}
+ />
+ )}
+ </>
+ )}
+ </div>
+ </>
+ );
+ }
+}
+
+const mapDispatchToProps = {
+ fetchMyOrganizations,
+ skipOnboardingAction
+};
+
+const mapStateToProps = (state: Store) => {
+ return {
+ userOrganizations: getMyOrganizations(state)
+ };
+};
+
+export default whenLoggedIn(
+ connect<StateProps>(
+ mapStateToProps,
+ mapDispatchToProps
+ )(CreateProjectPage)
+);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 OrganizationSelect from './OrganizationSelect';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { SubmitButton } from '../../../components/ui/buttons';
+import { LoggedInUser, Organization } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+import { createProject } from '../../../api/components';
+
+interface Props {
+ currentUser: LoggedInUser;
+ onProjectCreate: (projectKeys: string[]) => void;
+ organization?: string;
+ userOrganizations: Organization[];
+}
+
+interface State {
+ projectName: string;
+ projectKey: string;
+ selectedOrganization: string;
+ submitting: boolean;
+}
+
+export default class ManualProjectCreate extends React.PureComponent<Props, State> {
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ projectName: '',
+ projectKey: '',
+ selectedOrganization: this.getInitialSelectedOrganization(props),
+ submitting: false
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ getInitialSelectedOrganization(props: Props) {
+ if (props.organization) {
+ return props.organization;
+ } else if (props.userOrganizations.length === 1) {
+ return props.userOrganizations[0].key;
+ } else {
+ return '';
+ }
+ }
+
+ handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+ event.preventDefault();
+
+ if (this.isValid()) {
+ const { projectKey, projectName, selectedOrganization } = this.state;
+ this.setState({ submitting: true });
+ createProject({
+ project: projectKey,
+ name: projectName,
+ organization: selectedOrganization
+ }).then(
+ ({ project }) => this.props.onProjectCreate([project.key]),
+ () => {
+ if (this.mounted) {
+ this.setState({ submitting: false });
+ }
+ }
+ );
+ }
+ };
+
+ handleOrganizationSelect = ({ key }: Organization) => {
+ this.setState({ selectedOrganization: key });
+ };
+
+ handleProjectNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ this.setState({ projectName: event.currentTarget.value });
+ };
+
+ handleProjectKeyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ this.setState({ projectKey: event.currentTarget.value });
+ };
+
+ isValid = () => {
+ const { projectKey, projectName, selectedOrganization } = this.state;
+ return Boolean(projectKey && projectName && selectedOrganization);
+ };
+
+ render() {
+ const { submitting } = this.state;
+ return (
+ <>
+ <form onSubmit={this.handleFormSubmit}>
+ <OrganizationSelect
+ onChange={this.handleOrganizationSelect}
+ organization={this.state.selectedOrganization}
+ organizations={this.props.userOrganizations}
+ />
+ <div className="form-field">
+ <label htmlFor="project-name">
+ {translate('onboarding.create_project.project_name')}
+ <em className="mandatory">*</em>
+ </label>
+ <input
+ className="input-super-large"
+ id="project-name"
+ maxLength={400}
+ minLength={1}
+ onChange={this.handleProjectNameChange}
+ required={true}
+ type="text"
+ value={this.state.projectName}
+ />
+ </div>
+ <div className="form-field">
+ <label htmlFor="project-key">
+ {translate('onboarding.create_project.project_key')}
+ <em className="mandatory">*</em>
+ </label>
+ <input
+ className="input-super-large"
+ id="project-key"
+ maxLength={400}
+ minLength={1}
+ onChange={this.handleProjectKeyChange}
+ required={true}
+ type="text"
+ value={this.state.projectKey}
+ />
+ </div>
+ <SubmitButton disabled={!this.isValid() || submitting}>
+ {translate('create')}
+ </SubmitButton>
+ <DeferredSpinner className="spacer-left" loading={submitting} />
+ </form>
+ </>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { Link } from 'react-router';
+import { sortBy } from 'lodash';
+import Select from '../../../components/controls/Select';
+import { Organization } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+import { sanitizeAlmId } from '../../../helpers/almIntegrations';
+import { getBaseUrl } from '../../../helpers/urls';
+
+interface Props {
+ autoImport?: boolean;
+ onChange: (organization: Organization) => void;
+ organization: string;
+ organizations: Organization[];
+}
+
+export default function OrganizationSelect({
+ autoImport,
+ onChange,
+ organization,
+ organizations
+}: Props) {
+ return (
+ <div className="form-field spacer-bottom">
+ <label htmlFor="select-organization">
+ {translate('onboarding.create_project.organization')}
+ <em className="mandatory">*</em>
+ </label>
+ <Select
+ autoFocus={true}
+ className="input-super-large"
+ clearable={false}
+ id="select-organization"
+ labelKey="name"
+ onChange={onChange}
+ optionRenderer={optionRenderer}
+ options={sortBy(organizations, o => o.name.toLowerCase())}
+ required={true}
+ value={organization}
+ valueKey="key"
+ valueRenderer={optionRenderer}
+ />
+ <Link className="big-spacer-left js-new-org" to="/create-organization">
+ {autoImport
+ ? translate('onboarding.create_project.import_new_org')
+ : translate('onboarding.create_project.create_new_org')}
+ </Link>
+ </div>
+ );
+}
+
+export function optionRenderer(organization: Organization) {
+ return (
+ <span>
+ {organization.almId && (
+ <img
+ alt={organization.almId}
+ className="spacer-right"
+ height={14}
+ src={`${getBaseUrl()}/images/sonarcloud/${sanitizeAlmId(organization.almId)}.svg`}
+ />
+ )}
+ {organization.name}
+ <span className="note little-spacer-left">{organization.key}</span>
+ </span>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 AlmRepositoryItem from './AlmRepositoryItem';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { getRepositories, provisionProject } from '../../../api/alm-integration';
+import { AlmApplication, AlmRepository } from '../../../app/types';
+import { SubmitButton } from '../../../components/ui/buttons';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ almApplication: AlmApplication;
+ onProjectCreate: (projectKeys: string[]) => void;
+ organization: string;
+}
+
+type SelectedRepositories = { [key: string]: AlmRepository | undefined };
+
+interface State {
+ loading: boolean;
+ repositories: AlmRepository[];
+ selectedRepositories: SelectedRepositories;
+ submitting: boolean;
+}
+
+export default class RemoteRepositories extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { loading: true, repositories: [], selectedRepositories: {}, submitting: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchRepositories();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ const { organization } = this.props;
+ if (prevProps.organization !== organization) {
+ this.setState({ loading: true });
+ this.fetchRepositories();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchRepositories = () => {
+ const { organization } = this.props;
+ return getRepositories({
+ organization
+ }).then(
+ ({ repositories }) => {
+ if (this.mounted) {
+ this.setState({ loading: false, repositories });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ };
+
+ handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+ event.preventDefault();
+
+ if (this.isValid()) {
+ const { selectedRepositories } = this.state;
+ this.setState({ submitting: true });
+ provisionProject({
+ installationKeys: Object.keys(selectedRepositories).filter(key =>
+ Boolean(selectedRepositories[key])
+ ),
+ organization: this.props.organization
+ }).then(
+ ({ projects }) => this.props.onProjectCreate(projects.map(project => project.projectKey)),
+ this.handleProvisionFail
+ );
+ }
+ };
+
+ handleProvisionFail = () => {
+ return this.fetchRepositories().then(() => {
+ if (this.mounted) {
+ this.setState(({ repositories, selectedRepositories }) => {
+ const updateSelectedRepositories: SelectedRepositories = {};
+ Object.keys(selectedRepositories).forEach(installationKey => {
+ const newRepository = repositories.find(r => r.installationKey === installationKey);
+ if (newRepository && !newRepository.linkedProjectKey) {
+ updateSelectedRepositories[newRepository.installationKey] = newRepository;
+ }
+ });
+ return { selectedRepositories: updateSelectedRepositories, submitting: false };
+ });
+ }
+ });
+ };
+
+ isValid = () => {
+ return this.state.repositories.some(repo =>
+ Boolean(this.state.selectedRepositories[repo.installationKey])
+ );
+ };
+
+ toggleRepository = (repository: AlmRepository) => {
+ this.setState(({ selectedRepositories }) => ({
+ selectedRepositories: {
+ ...selectedRepositories,
+ [repository.installationKey]: selectedRepositories[repository.installationKey]
+ ? undefined
+ : repository
+ }
+ }));
+ };
+
+ render() {
+ const { loading, selectedRepositories, submitting } = this.state;
+ const { almApplication } = this.props;
+ return (
+ <DeferredSpinner loading={loading}>
+ <form onSubmit={this.handleFormSubmit}>
+ <div className="form-field">
+ <ul>
+ {this.state.repositories.map(repo => (
+ <li className="big-spacer-bottom" key={repo.installationKey}>
+ <AlmRepositoryItem
+ identityProvider={almApplication}
+ repository={repo}
+ selected={Boolean(selectedRepositories[repo.installationKey])}
+ toggleRepository={this.toggleRepository}
+ />
+ </li>
+ ))}
+ </ul>
+ </div>
+ <SubmitButton disabled={!this.isValid() || submitting}>
+ {translate('create')}
+ </SubmitButton>
+ <DeferredSpinner className="spacer-left" loading={submitting} />
+ </form>
+ </DeferredSpinner>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import AlmRepositoryItem from '../AlmRepositoryItem';
+
+const identityProviders = {
+ backgroundColor: 'blue',
+ iconPath: 'icon/path',
+ key: 'foo',
+ name: 'Foo Provider'
+};
+
+const repositories = [
+ {
+ label: 'Cool Project',
+ installationKey: 'github/cool',
+ linkedProjectKey: 'proj_cool',
+ linkedProjectName: 'Proj Cool'
+ },
+ {
+ label: 'Awesome Project',
+ installationKey: 'github/awesome'
+ }
+];
+
+it('should render correctly', () => {
+ expect(getWrapper()).toMatchSnapshot();
+});
+
+it('should render selected', () => {
+ expect(getWrapper({ selected: true })).toMatchSnapshot();
+});
+
+it('should render disabled', () => {
+ expect(getWrapper({ repository: repositories[0] })).toMatchSnapshot();
+});
+
+function getWrapper(props = {}) {
+ return shallow(
+ <AlmRepositoryItem
+ identityProvider={identityProviders}
+ repository={repositories[1]}
+ selected={false}
+ toggleRepository={jest.fn()}
+ {...props}
+ />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import AutoProjectCreate from '../AutoProjectCreate';
+
+const almApplication = {
+ backgroundColor: 'blue',
+ iconPath: 'icon/path',
+ installationUrl: 'https://alm.installation.url',
+ key: 'github',
+ name: 'GitHub'
+};
+
+it('should display the provider app install button', () => {
+ expect(shallowRender({ boundOrganizations: [] })).toMatchSnapshot();
+});
+
+it('should display the bounded organizations dropdown with the list of repositories', () => {
+ expect(shallowRender({ organization: 'foo' })).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<AutoProjectCreate['props']> = {}) {
+ return shallow(
+ <AutoProjectCreate
+ almApplication={almApplication}
+ boundOrganizations={[
+ { almId: 'github', key: 'foo', name: 'Foo' },
+ { almId: 'github', key: 'bar', name: 'Bar' }
+ ]}
+ onProjectCreate={jest.fn()}
+ organization=""
+ {...props}
+ />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import { CreateProjectPage } from '../CreateProjectPage';
+import { LoggedInUser } from '../../../../app/types';
+import { waitAndUpdate, mockRouter } from '../../../../helpers/testUtils';
+import { getAlmAppInfo } from '../../../../api/alm-integration';
+
+jest.mock('../../../../api/alm-integration', () => ({
+ getAlmAppInfo: jest.fn().mockResolvedValue({
+ application: {
+ backgroundColor: 'blue',
+ iconPath: 'icon/path',
+ installationUrl: 'https://alm.installation.url',
+ key: 'github',
+ name: 'GitHub'
+ }
+ })
+}));
+
+const user: LoggedInUser = {
+ externalProvider: 'github',
+ groups: [],
+ isLoggedIn: true,
+ login: 'foo',
+ name: 'Foo',
+ scmAccounts: []
+};
+
+beforeEach(() => {
+ (getAlmAppInfo as jest.Mock<any>).mockClear();
+});
+
+it('should render correctly', async () => {
+ const wrapper = getWrapper();
+ expect(wrapper).toMatchSnapshot();
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should render with Manual creation only', () => {
+ expect(getWrapper({ currentUser: { ...user, externalProvider: 'microsoft' } })).toMatchSnapshot();
+});
+
+it('should switch tabs', async () => {
+ const replace = jest.fn();
+ const wrapper = getWrapper({ router: { replace } });
+ replace.mockImplementation(location => {
+ wrapper.setProps({ location }).update();
+ });
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.find('Tabs').prop<Function>('onChange')('manual');
+ expect(wrapper.find('ManualProjectCreate').exists()).toBeTruthy();
+ wrapper.find('Tabs').prop<Function>('onChange')('auto');
+ expect(wrapper.find('AutoProjectCreate').exists()).toBeTruthy();
+});
+
+function getWrapper(props = {}) {
+ return shallow(
+ <CreateProjectPage
+ addGlobalErrorMessage={jest.fn()}
+ currentUser={user}
+ fetchMyOrganizations={jest.fn()}
+ // @ts-ignore avoid passing everything from WithRouterProps
+ location={{}}
+ router={mockRouter()}
+ skipOnboardingAction={jest.fn()}
+ userOrganizations={[
+ { key: 'foo', name: 'Foo' },
+ { almId: 'github', key: 'bar', name: 'Bar' }
+ ]}
+ {...props}
+ />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import ManualProjectCreate from '../ManualProjectCreate';
+import { change, submit, waitAndUpdate } from '../../../../helpers/testUtils';
+import { createProject } from '../../../../api/components';
+
+jest.mock('../../../../api/components', () => ({
+ createProject: jest.fn().mockResolvedValue({ project: { key: 'bar', name: 'Bar' } })
+}));
+
+beforeEach(() => {
+ (createProject as jest.Mock<any>).mockClear();
+});
+
+it('should render correctly', () => {
+ expect(getWrapper()).toMatchSnapshot();
+});
+
+it('should correctly create a project', async () => {
+ const onProjectCreate = jest.fn();
+ const wrapper = getWrapper({ onProjectCreate });
+ wrapper.find('OrganizationSelect').prop<Function>('onChange')({ key: 'foo' });
+ change(wrapper.find('#project-name'), 'Bar');
+ expect(wrapper.find('SubmitButton')).toMatchSnapshot();
+
+ change(wrapper.find('#project-key'), 'bar');
+ expect(wrapper.find('SubmitButton')).toMatchSnapshot();
+
+ submit(wrapper.find('form'));
+ expect(createProject).toBeCalledWith({ project: 'bar', name: 'Bar', organization: 'foo' });
+
+ await waitAndUpdate(wrapper);
+ expect(onProjectCreate).toBeCalledWith(['bar']);
+});
+
+function getWrapper(props = {}) {
+ return shallow(
+ <ManualProjectCreate
+ currentUser={{ groups: [], isLoggedIn: true, login: 'foo', name: 'Foo', scmAccounts: [] }}
+ onProjectCreate={jest.fn()}
+ userOrganizations={[{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }]}
+ {...props}
+ />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import OrganizationSelect, { optionRenderer } from '../OrganizationSelect';
+
+const organizations = [{ key: 'foo', name: 'Foo' }, { almId: 'github', key: 'bar', name: 'Bar' }];
+
+it('should render correctly', () => {
+ expect(
+ shallow(
+ <OrganizationSelect onChange={jest.fn()} organization="bar" organizations={organizations} />
+ )
+ ).toMatchSnapshot();
+ expect(
+ shallow(
+ <OrganizationSelect
+ autoImport={true}
+ onChange={jest.fn()}
+ organization="bar"
+ organizations={organizations}
+ />
+ )
+ .find('.js-new-org')
+ .contains('onboarding.create_project.import_new_org')
+ ).toBe(true);
+});
+
+it('should render options correctly', () => {
+ expect(shallow(optionRenderer(organizations[0]))).toMatchSnapshot();
+ expect(shallow(optionRenderer(organizations[1]))).toMatchSnapshot();
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import RemoteRepositories from '../RemoteRepositories';
+import { getRepositories, provisionProject } from '../../../../api/alm-integration';
+import { waitAndUpdate, submit } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/alm-integration', () => ({
+ getRepositories: jest.fn().mockResolvedValue({
+ repositories: [
+ {
+ label: 'Cool Project',
+ installationKey: 'github/cool',
+ linkedProjectKey: 'proj_cool',
+ linkedProjectName: 'Proj Cool'
+ },
+ {
+ label: 'Awesome Project',
+ installationKey: 'github/awesome'
+ }
+ ]
+ }),
+ provisionProject: jest.fn().mockResolvedValue({ projects: [{ projectKey: 'awesome' }] })
+}));
+
+const almApplication = {
+ backgroundColor: 'blue',
+ iconPath: 'icon/path',
+ installationUrl: 'https://alm.installation.url',
+ key: 'github',
+ name: 'GitHub'
+};
+
+beforeEach(() => {
+ (getRepositories as jest.Mock<any>).mockClear();
+ (provisionProject as jest.Mock<any>).mockClear();
+});
+
+it('should display the list of repositories', async () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+ await waitAndUpdate(wrapper);
+ expect(getRepositories).toHaveBeenCalledWith({ organization: 'sonarsource' });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should correctly create a project', async () => {
+ const onProjectCreate = jest.fn();
+ const wrapper = shallowRender({ onProjectCreate });
+ (wrapper.instance() as RemoteRepositories).toggleRepository({
+ label: 'Awesome Project',
+ installationKey: 'github/awesome'
+ });
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.find('SubmitButton')).toMatchSnapshot();
+ submit(wrapper.find('form'));
+ expect(provisionProject).toBeCalledWith({
+ installationKeys: ['github/awesome'],
+ organization: 'sonarsource'
+ });
+
+ await waitAndUpdate(wrapper);
+ expect(onProjectCreate).toBeCalledWith(['awesome']);
+});
+
+function shallowRender(props: Partial<RemoteRepositories['props']> = {}) {
+ return shallow(
+ <RemoteRepositories
+ almApplication={almApplication}
+ onProjectCreate={jest.fn()}
+ organization="sonarsource"
+ {...props}
+ />
+ );
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Fragment>
+ <Checkbox
+ checked={false}
+ disabled={false}
+ onCheck={[Function]}
+ thirdState={false}
+ >
+ <img
+ alt="Foo Provider"
+ className="spacer-left"
+ height={14}
+ src="/images/sonarcloud/foo.svg"
+ style={
+ Object {
+ "opacity": 1,
+ }
+ }
+ width={14}
+ />
+ <span
+ className="spacer-left"
+ >
+ Awesome Project
+ </span>
+ </Checkbox>
+</Fragment>
+`;
+
+exports[`should render disabled 1`] = `
+<Fragment>
+ <Checkbox
+ checked={true}
+ disabled={true}
+ onCheck={[Function]}
+ thirdState={false}
+ >
+ <img
+ alt="Foo Provider"
+ className="spacer-left"
+ height={14}
+ src="/images/sonarcloud/foo.svg"
+ style={
+ Object {
+ "opacity": 0.5,
+ }
+ }
+ width={14}
+ />
+ <span
+ className="spacer-left"
+ >
+ Cool Project
+ </span>
+ </Checkbox>
+ <span
+ className="big-spacer-left"
+ >
+ <CheckIcon
+ className="little-spacer-right"
+ fill="#00aa00"
+ />
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": undefined,
+ "id": "proj_cool",
+ },
+ }
+ }
+ >
+ onboarding.create_project.already_imported
+ </Link>
+ </span>
+</Fragment>
+`;
+
+exports[`should render selected 1`] = `
+<Fragment>
+ <Checkbox
+ checked={true}
+ disabled={false}
+ onCheck={[Function]}
+ thirdState={false}
+ >
+ <img
+ alt="Foo Provider"
+ className="spacer-left"
+ height={14}
+ src="/images/sonarcloud/foo.svg"
+ style={
+ Object {
+ "opacity": 1,
+ }
+ }
+ width={14}
+ />
+ <span
+ className="spacer-left"
+ >
+ Awesome Project
+ </span>
+ </Checkbox>
+</Fragment>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display the bounded organizations dropdown with the list of repositories 1`] = `
+<Fragment>
+ <OrganizationSelect
+ autoImport={true}
+ onChange={[Function]}
+ organization="foo"
+ organizations={
+ Array [
+ Object {
+ "almId": "github",
+ "key": "foo",
+ "name": "Foo",
+ },
+ Object {
+ "almId": "github",
+ "key": "bar",
+ "name": "Bar",
+ },
+ ]
+ }
+ />
+ <RemoteRepositories
+ almApplication={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
+ "key": "github",
+ "name": "GitHub",
+ }
+ }
+ onProjectCreate={[MockFunction]}
+ organization="foo"
+ />
+</Fragment>
+`;
+
+exports[`should display the provider app install button 1`] = `
+<Fragment>
+ <IdentityProviderLink
+ className="display-inline-block"
+ identityProvider={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
+ "key": "github",
+ "name": "GitHub",
+ }
+ }
+ small={true}
+ url="https://alm.installation.url"
+ >
+ onboarding.create_organization.choose_organization_button.github
+ </IdentityProviderLink>
+</Fragment>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Fragment>
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="onboarding.create_project.header"
+ titleTemplate="%s"
+ />
+ <div
+ className="sonarcloud page page-limited"
+ >
+ <header
+ className="page-header"
+ >
+ <h1
+ className="page-title"
+ >
+ onboarding.create_project.header
+ </h1>
+ </header>
+ <DeferredSpinner
+ timeout={100}
+ />
+ </div>
+</Fragment>
+`;
+
+exports[`should render correctly 2`] = `
+<Fragment>
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="onboarding.create_project.header"
+ titleTemplate="%s"
+ />
+ <div
+ className="sonarcloud page page-limited"
+ >
+ <header
+ className="page-header"
+ >
+ <h1
+ className="page-title"
+ >
+ onboarding.create_project.header
+ </h1>
+ </header>
+ <Tabs
+ onChange={[Function]}
+ selected="auto"
+ tabs={
+ Array [
+ Object {
+ "key": "auto",
+ "node": <React.Fragment>
+ onboarding.create_project.select_repositories
+ <span
+ className="beta-badge spacer-left"
+ >
+ beta
+ </span>
+ </React.Fragment>,
+ },
+ Object {
+ "key": "manual",
+ "node": "onboarding.create_project.create_manually",
+ },
+ ]
+ }
+ />
+ <AutoProjectCreate
+ almApplication={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
+ "key": "github",
+ "name": "GitHub",
+ }
+ }
+ boundOrganizations={
+ Array [
+ Object {
+ "almId": "github",
+ "key": "bar",
+ "name": "Bar",
+ },
+ ]
+ }
+ onProjectCreate={[Function]}
+ />
+ </div>
+</Fragment>
+`;
+
+exports[`should render with Manual creation only 1`] = `
+<Fragment>
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="onboarding.create_project.header"
+ titleTemplate="%s"
+ />
+ <div
+ className="sonarcloud page page-limited"
+ >
+ <header
+ className="page-header"
+ >
+ <h1
+ className="page-title"
+ >
+ onboarding.create_project.header
+ </h1>
+ </header>
+ <ManualProjectCreate
+ currentUser={
+ Object {
+ "externalProvider": "microsoft",
+ "groups": Array [],
+ "isLoggedIn": true,
+ "login": "foo",
+ "name": "Foo",
+ "scmAccounts": Array [],
+ }
+ }
+ onProjectCreate={[Function]}
+ userOrganizations={
+ Array [
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ },
+ Object {
+ "almId": "github",
+ "key": "bar",
+ "name": "Bar",
+ },
+ ]
+ }
+ />
+ </div>
+</Fragment>
+`;
+
+exports[`should switch tabs 1`] = `
+<Fragment>
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="onboarding.create_project.header"
+ titleTemplate="%s"
+ />
+ <div
+ className="sonarcloud page page-limited"
+ >
+ <header
+ className="page-header"
+ >
+ <h1
+ className="page-title"
+ >
+ onboarding.create_project.header
+ </h1>
+ </header>
+ <Tabs
+ onChange={[Function]}
+ selected="auto"
+ tabs={
+ Array [
+ Object {
+ "key": "auto",
+ "node": <React.Fragment>
+ onboarding.create_project.select_repositories
+ <span
+ className="beta-badge spacer-left"
+ >
+ beta
+ </span>
+ </React.Fragment>,
+ },
+ Object {
+ "key": "manual",
+ "node": "onboarding.create_project.create_manually",
+ },
+ ]
+ }
+ />
+ <AutoProjectCreate
+ almApplication={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
+ "key": "github",
+ "name": "GitHub",
+ }
+ }
+ boundOrganizations={
+ Array [
+ Object {
+ "almId": "github",
+ "key": "bar",
+ "name": "Bar",
+ },
+ ]
+ }
+ onProjectCreate={[Function]}
+ />
+ </div>
+</Fragment>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should correctly create a project 1`] = `
+<SubmitButton
+ disabled={true}
+>
+ create
+</SubmitButton>
+`;
+
+exports[`should correctly create a project 2`] = `
+<SubmitButton
+ disabled={false}
+>
+ create
+</SubmitButton>
+`;
+
+exports[`should render correctly 1`] = `
+<Fragment>
+ <form
+ onSubmit={[Function]}
+ >
+ <OrganizationSelect
+ onChange={[Function]}
+ organization=""
+ organizations={
+ Array [
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ },
+ Object {
+ "key": "bar",
+ "name": "Bar",
+ },
+ ]
+ }
+ />
+ <div
+ className="form-field"
+ >
+ <label
+ htmlFor="project-name"
+ >
+ onboarding.create_project.project_name
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ className="input-super-large"
+ id="project-name"
+ maxLength={400}
+ minLength={1}
+ onChange={[Function]}
+ required={true}
+ type="text"
+ value=""
+ />
+ </div>
+ <div
+ className="form-field"
+ >
+ <label
+ htmlFor="project-key"
+ >
+ onboarding.create_project.project_key
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ className="input-super-large"
+ id="project-key"
+ maxLength={400}
+ minLength={1}
+ onChange={[Function]}
+ required={true}
+ type="text"
+ value=""
+ />
+ </div>
+ <SubmitButton
+ disabled={true}
+ >
+ create
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
+ timeout={100}
+ />
+ </form>
+</Fragment>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+ className="form-field spacer-bottom"
+>
+ <label
+ htmlFor="select-organization"
+ >
+ onboarding.create_project.organization
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <Select
+ autoFocus={true}
+ className="input-super-large"
+ clearable={false}
+ id="select-organization"
+ labelKey="name"
+ onChange={[MockFunction]}
+ optionRenderer={[Function]}
+ options={
+ Array [
+ Object {
+ "almId": "github",
+ "key": "bar",
+ "name": "Bar",
+ },
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ },
+ ]
+ }
+ required={true}
+ value="bar"
+ valueKey="key"
+ valueRenderer={[Function]}
+ />
+ <Link
+ className="big-spacer-left js-new-org"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/create-organization"
+ >
+ onboarding.create_project.create_new_org
+ </Link>
+</div>
+`;
+
+exports[`should render options correctly 1`] = `
+<span>
+ Foo
+ <span
+ className="note little-spacer-left"
+ >
+ foo
+ </span>
+</span>
+`;
+
+exports[`should render options correctly 2`] = `
+<span>
+ <img
+ alt="github"
+ className="spacer-right"
+ height={14}
+ src="/images/sonarcloud/github.svg"
+ />
+ Bar
+ <span
+ className="note little-spacer-left"
+ >
+ bar
+ </span>
+</span>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should correctly create a project 1`] = `
+<SubmitButton
+ disabled={false}
+>
+ create
+</SubmitButton>
+`;
+
+exports[`should display the list of repositories 1`] = `
+<DeferredSpinner
+ loading={true}
+ timeout={100}
+>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="form-field"
+ >
+ <ul />
+ </div>
+ <SubmitButton
+ disabled={true}
+ >
+ create
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
+ timeout={100}
+ />
+ </form>
+</DeferredSpinner>
+`;
+
+exports[`should display the list of repositories 2`] = `
+<DeferredSpinner
+ loading={false}
+ timeout={100}
+>
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="form-field"
+ >
+ <ul>
+ <li
+ className="big-spacer-bottom"
+ key="github/cool"
+ >
+ <AlmRepositoryItem
+ identityProvider={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
+ "key": "github",
+ "name": "GitHub",
+ }
+ }
+ repository={
+ Object {
+ "installationKey": "github/cool",
+ "label": "Cool Project",
+ "linkedProjectKey": "proj_cool",
+ "linkedProjectName": "Proj Cool",
+ }
+ }
+ selected={false}
+ toggleRepository={[Function]}
+ />
+ </li>
+ <li
+ className="big-spacer-bottom"
+ key="github/awesome"
+ >
+ <AlmRepositoryItem
+ identityProvider={
+ Object {
+ "backgroundColor": "blue",
+ "iconPath": "icon/path",
+ "installationUrl": "https://alm.installation.url",
+ "key": "github",
+ "name": "GitHub",
+ }
+ }
+ repository={
+ Object {
+ "installationKey": "github/awesome",
+ "label": "Awesome Project",
+ }
+ }
+ selected={false}
+ toggleRepository={[Function]}
+ />
+ </li>
+ </ul>
+ </div>
+ <SubmitButton
+ disabled={true}
+ >
+ create
+ </SubmitButton>
+ <DeferredSpinner
+ className="spacer-left"
+ loading={false}
+ timeout={100}
+ />
+ </form>
+</DeferredSpinner>
+`;
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { Link } from 'react-router';
-import * as theme from '../../../app/theme';
-import Checkbox from '../../../components/controls/Checkbox';
-import CheckIcon from '../../../components/icons-components/CheckIcon';
-import { AlmRepository, IdentityProvider } from '../../../app/types';
-import { getBaseUrl, getProjectUrl } from '../../../helpers/urls';
-import { translate } from '../../../helpers/l10n';
-
-interface Props {
- identityProvider: IdentityProvider;
- repository: AlmRepository;
- selected: boolean;
- toggleRepository: (repository: AlmRepository) => void;
-}
-
-export default class AlmRepositoryItem extends React.PureComponent<Props> {
- handleChange = () => {
- this.props.toggleRepository(this.props.repository);
- };
-
- render() {
- const { identityProvider, repository, selected } = this.props;
- const alreadyImported = Boolean(repository.linkedProjectKey);
- return (
- <>
- <Checkbox
- checked={selected || alreadyImported}
- disabled={alreadyImported}
- onCheck={this.handleChange}>
- <img
- alt={identityProvider.name}
- className="spacer-left"
- height={14}
- src={`${getBaseUrl()}/images/sonarcloud/${identityProvider.key}.svg`}
- style={{ opacity: alreadyImported ? 0.5 : 1 }}
- width={14}
- />
- <span className="spacer-left">{this.props.repository.label}</span>
- </Checkbox>
- {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>
- </span>
- )}
- </>
- );
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 AlmRepositoryItem from './AlmRepositoryItem';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
-import IdentityProviderLink from '../../../components/ui/IdentityProviderLink';
-import { getRepositories, provisionProject } from '../../../api/alm-integration';
-import { IdentityProvider, AlmRepository } from '../../../app/types';
-import { SubmitButton } from '../../../components/ui/buttons';
-import { translateWithParameters, translate } from '../../../helpers/l10n';
-import { Alert } from '../../../components/ui/Alert';
-
-interface Props {
- identityProvider: IdentityProvider;
- onProjectCreate: (projectKeys: string[]) => void;
-}
-
-interface State {
- installationUrl?: string;
- installed?: boolean;
- loading: boolean;
- repositories: AlmRepository[];
- selectedRepositories: { [key: string]: AlmRepository | undefined };
- submitting: boolean;
-}
-
-export default class AutoProjectCreate extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = {
- loading: true,
- repositories: [],
- selectedRepositories: {},
- submitting: false
- };
-
- componentDidMount() {
- this.mounted = true;
- this.fetchRepositories();
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- fetchRepositories = () => {
- getRepositories().then(
- ({ almIntegration, repositories }) => {
- if (this.mounted) {
- this.setState({ ...almIntegration, loading: false, repositories });
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- }
- );
- };
-
- handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
- event.preventDefault();
-
- if (this.isValid()) {
- const { selectedRepositories } = this.state;
- this.setState({ submitting: true });
- provisionProject({
- installationKeys: Object.keys(selectedRepositories).filter(key =>
- Boolean(selectedRepositories[key])
- )
- }).then(
- ({ projects }) => this.props.onProjectCreate(projects.map(project => project.projectKey)),
- () => {
- if (this.mounted) {
- this.setState({ loading: true, submitting: false });
- this.fetchRepositories();
- }
- }
- );
- }
- };
-
- isValid = () => {
- return this.state.repositories.some(repo =>
- Boolean(this.state.selectedRepositories[repo.installationKey])
- );
- };
-
- toggleRepository = (repository: AlmRepository) => {
- this.setState(({ selectedRepositories }) => ({
- selectedRepositories: {
- ...selectedRepositories,
- [repository.installationKey]: selectedRepositories[repository.installationKey]
- ? undefined
- : repository
- }
- }));
- };
-
- renderContent = () => {
- const { identityProvider } = this.props;
- const { selectedRepositories, submitting } = this.state;
-
- if (this.state.installed) {
- return (
- <form onSubmit={this.handleFormSubmit}>
- <ul>
- {this.state.repositories.map(repo => (
- <li className="big-spacer-bottom" key={repo.installationKey}>
- <AlmRepositoryItem
- identityProvider={identityProvider}
- repository={repo}
- selected={Boolean(selectedRepositories[repo.installationKey])}
- toggleRepository={this.toggleRepository}
- />
- </li>
- ))}
- </ul>
- <SubmitButton disabled={!this.isValid() || submitting}>
- {translate('create')}
- </SubmitButton>
- <DeferredSpinner className="spacer-left" loading={submitting} />
- </form>
- );
- }
- return (
- <div>
- <p className="spacer-bottom">
- {translateWithParameters(
- 'onboarding.create_project.install_app_x',
- identityProvider.name
- )}
- </p>
- <IdentityProviderLink
- className="display-inline-block"
- identityProvider={identityProvider}
- small={true}
- url={this.state.installationUrl}>
- {translateWithParameters(
- 'onboarding.create_project.install_app_x.button',
- identityProvider.name
- )}
- </IdentityProviderLink>
- </div>
- );
- };
-
- render() {
- const { identityProvider } = this.props;
- const { loading } = this.state;
-
- return (
- <>
- <Alert className="width-60 big-spacer-bottom" variant="info">
- {translateWithParameters(
- 'onboarding.create_project.beta_feature_x',
- identityProvider.name
- )}
- </Alert>
- {loading ? <DeferredSpinner /> : this.renderContent()}
- </>
- );
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
-import { connect } from 'react-redux';
-import { InjectedRouter } from 'react-router';
-import { Location } from 'history';
-import Helmet from 'react-helmet';
-import AutoProjectCreate from './AutoProjectCreate';
-import ManualProjectCreate from './ManualProjectCreate';
-import { serializeQuery, Query, parseQuery } from './utils';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
-import Tabs from '../../../components/controls/Tabs';
-import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
-import { getCurrentUser, Store } from '../../../store/rootReducer';
-import { addGlobalErrorMessage } from '../../../store/globalMessages';
-import { skipOnboarding as skipOnboardingAction } from '../../../store/users';
-import { CurrentUser, IdentityProvider, LoggedInUser } from '../../../app/types';
-import { skipOnboarding, getIdentityProviders } from '../../../api/users';
-import { hasAdvancedALMIntegration } from '../../../helpers/almIntegrations';
-import { translate } from '../../../helpers/l10n';
-import { getProjectUrl } from '../../../helpers/urls';
-import { isLoggedIn } from '../../../helpers/users';
-import '../../../app/styles/sonarcloud.css';
-
-interface OwnProps {
- location: Location;
- router: Pick<InjectedRouter, 'push' | 'replace'>;
-}
-
-interface StateProps {
- currentUser: CurrentUser;
-}
-
-interface DispatchProps {
- addGlobalErrorMessage: (message: string) => void;
- skipOnboardingAction: () => void;
-}
-
-type Props = StateProps & DispatchProps & OwnProps;
-
-interface State {
- identityProvider?: IdentityProvider;
- loading: boolean;
-}
-
-export class CreateProjectPage extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { loading: true };
-
- componentDidMount() {
- if (isLoggedIn(this.props.currentUser)) {
- this.mounted = true;
- const query = parseQuery(this.props.location.query);
- if (query.error) {
- this.props.addGlobalErrorMessage(query.error);
- }
- if (!hasAdvancedALMIntegration(this.props.currentUser)) {
- this.setState({ loading: false });
- this.updateQuery({ manual: true });
- } else {
- this.fetchIdentityProviders();
- }
- document.body.classList.add('white-page');
- if (document.documentElement) {
- document.documentElement.classList.add('white-page');
- }
- } else {
- handleRequiredAuthentication();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- document.body.classList.remove('white-page');
- if (document.documentElement) {
- document.documentElement.classList.remove('white-page');
- }
- }
-
- handleProjectCreate = (projectKeys: string[]) => {
- skipOnboarding().catch(() => {});
- this.props.skipOnboardingAction();
- if (projectKeys.length > 1) {
- this.props.router.push({ pathname: '/projects' });
- } else if (projectKeys.length === 1) {
- this.props.router.push(getProjectUrl(projectKeys[0]));
- }
- };
-
- fetchIdentityProviders = () => {
- getIdentityProviders().then(
- ({ identityProviders }) => {
- if (this.mounted) {
- this.setState({
- identityProvider: identityProviders.find(
- identityProvider =>
- identityProvider.key === (this.props.currentUser as LoggedInUser).externalProvider
- ),
- loading: false
- });
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- }
- );
- };
-
- onTabChange = (tab: 'auto' | 'manual') => {
- this.updateQuery({ manual: tab === 'manual' });
- };
-
- updateQuery = (changes: Partial<Query>) => {
- this.props.router.replace({
- pathname: this.props.location.pathname,
- query: serializeQuery({ ...parseQuery(this.props.location.query), ...changes })
- });
- };
-
- render() {
- const { currentUser } = this.props;
-
- if (!isLoggedIn(currentUser)) {
- return null;
- }
-
- const { identityProvider, loading } = this.state;
- const query = parseQuery(this.props.location.query);
- const header = translate('onboarding.create_project.header');
- const hasAutoProvisioning = hasAdvancedALMIntegration(currentUser) && identityProvider;
- return (
- <>
- <Helmet title={header} titleTemplate="%s" />
- <div className="sonarcloud page page-limited">
- <header className="page-header">
- <h1 className="page-title">{header}</h1>
- </header>
- {loading ? (
- <DeferredSpinner />
- ) : (
- <>
- {hasAutoProvisioning && (
- <Tabs
- onChange={this.onTabChange}
- selected={query.manual ? 'manual' : 'auto'}
- tabs={[
- {
- key: 'auto',
- node: (
- <>
- {translate('onboarding.create_project.select_repositories')}
- <span
- className={classNames('beta-badge spacer-left', {
- 'is-muted': query.manual
- })}>
- {translate('beta')}
- </span>
- </>
- )
- },
- { key: 'manual', node: translate('onboarding.create_project.create_manually') }
- ]}
- />
- )}
-
- {query.manual || !hasAutoProvisioning || !identityProvider ? (
- <ManualProjectCreate
- currentUser={currentUser}
- onProjectCreate={this.handleProjectCreate}
- organization={query.organization}
- />
- ) : (
- <AutoProjectCreate
- identityProvider={identityProvider}
- onProjectCreate={this.handleProjectCreate}
- />
- )}
- </>
- )}
- </div>
- </>
- );
- }
-}
-
-const mapStateToProps = (state: Store): StateProps => ({
- currentUser: getCurrentUser(state)
-});
-
-const mapDispatchToProps: DispatchProps = { addGlobalErrorMessage, skipOnboardingAction };
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(CreateProjectPage);
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { sortBy } from 'lodash';
-import { connect } from 'react-redux';
-import { Link } from 'react-router';
-import Select from '../../../components/controls/Select';
-import { SubmitButton } from '../../../components/ui/buttons';
-import { LoggedInUser, Organization } from '../../../app/types';
-import { fetchMyOrganizations } from '../../account/organizations/actions';
-import { getMyOrganizations, Store } from '../../../store/rootReducer';
-import { translate } from '../../../helpers/l10n';
-import { createProject } from '../../../api/components';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
-
-interface StateProps {
- userOrganizations: Organization[];
-}
-
-interface DispatchProps {
- fetchMyOrganizations: () => Promise<void>;
-}
-
-interface OwnProps {
- currentUser: LoggedInUser;
- onProjectCreate: (projectKeys: string[]) => void;
- organization?: string;
-}
-
-type Props = OwnProps & StateProps & DispatchProps;
-
-interface State {
- projectName: string;
- projectKey: string;
- selectedOrganization: string;
- submitting: boolean;
-}
-
-export class ManualProjectCreate extends React.PureComponent<Props, State> {
- mounted = false;
-
- constructor(props: Props) {
- super(props);
- this.state = {
- projectName: '',
- projectKey: '',
- selectedOrganization: this.getInitialSelectedOrganization(props),
- submitting: false
- };
- }
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- getInitialSelectedOrganization(props: Props) {
- if (props.organization) {
- return props.organization;
- } else if (props.userOrganizations.length === 1) {
- return props.userOrganizations[0].key;
- } else {
- return '';
- }
- }
-
- handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
- event.preventDefault();
-
- if (this.isValid()) {
- const { projectKey, projectName, selectedOrganization } = this.state;
- this.setState({ submitting: true });
- createProject({
- project: projectKey,
- name: projectName,
- organization: selectedOrganization
- }).then(
- ({ project }) => this.props.onProjectCreate([project.key]),
- () => {
- if (this.mounted) {
- this.setState({ submitting: false });
- }
- }
- );
- }
- };
-
- handleOrganizationSelect = ({ value }: { value: string }) => {
- this.setState({ selectedOrganization: value });
- };
-
- handleProjectNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- this.setState({ projectName: event.currentTarget.value });
- };
-
- handleProjectKeyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- this.setState({ projectKey: event.currentTarget.value });
- };
-
- isValid = () => {
- const { projectKey, projectName, selectedOrganization } = this.state;
- return Boolean(projectKey && projectName && selectedOrganization);
- };
-
- render() {
- const { submitting } = this.state;
- return (
- <>
- <form onSubmit={this.handleFormSubmit}>
- <div className="form-field">
- <label htmlFor="select-organization">
- {translate('onboarding.create_project.organization')}
- <em className="mandatory">*</em>
- </label>
- <Select
- autoFocus={true}
- className="input-super-large"
- clearable={false}
- id="select-organization"
- onChange={this.handleOrganizationSelect}
- options={sortBy(this.props.userOrganizations, o => o.name.toLowerCase()).map(
- organization => ({
- label: organization.name,
- value: organization.key
- })
- )}
- required={true}
- value={this.state.selectedOrganization}
- />
- <Link className="big-spacer-left js-new-org" to="/create-organization">
- {translate('onboarding.create_project.create_new_org')}
- </Link>
- </div>
- <div className="form-field">
- <label htmlFor="project-name">
- {translate('onboarding.create_project.project_name')}
- <em className="mandatory">*</em>
- </label>
- <input
- className="input-super-large"
- id="project-name"
- maxLength={400}
- minLength={1}
- onChange={this.handleProjectNameChange}
- required={true}
- type="text"
- value={this.state.projectName}
- />
- </div>
- <div className="form-field">
- <label htmlFor="project-key">
- {translate('onboarding.create_project.project_key')}
- <em className="mandatory">*</em>
- </label>
- <input
- className="input-super-large"
- id="project-key"
- maxLength={400}
- minLength={1}
- onChange={this.handleProjectKeyChange}
- required={true}
- type="text"
- value={this.state.projectKey}
- />
- </div>
- <SubmitButton disabled={!this.isValid() || submitting}>
- {translate('create')}
- </SubmitButton>
- <DeferredSpinner className="spacer-left" loading={submitting} />
- </form>
- </>
- );
- }
-}
-
-const mapDispatchToProps = ({
- fetchMyOrganizations
-} as any) as DispatchProps;
-
-const mapStateToProps = (state: Store): StateProps => {
- return {
- userOrganizations: getMyOrganizations(state)
- };
-};
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(ManualProjectCreate);
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
-import AlmRepositoryItem from '../AlmRepositoryItem';
-
-const identityProviders = {
- backgroundColor: 'blue',
- iconPath: 'icon/path',
- key: 'foo',
- name: 'Foo Provider'
-};
-
-const repositories = [
- {
- label: 'Cool Project',
- installationKey: 'github/cool',
- linkedProjectKey: 'proj_cool',
- linkedProjectName: 'Proj Cool'
- },
- {
- label: 'Awesome Project',
- installationKey: 'github/awesome'
- }
-];
-
-it('should render correctly', () => {
- expect(getWrapper()).toMatchSnapshot();
-});
-
-it('should render selected', () => {
- expect(getWrapper({ selected: true })).toMatchSnapshot();
-});
-
-it('should render disabled', () => {
- expect(getWrapper({ repository: repositories[0] })).toMatchSnapshot();
-});
-
-function getWrapper(props = {}) {
- return shallow(
- <AlmRepositoryItem
- identityProvider={identityProviders}
- repository={repositories[1]}
- selected={false}
- toggleRepository={jest.fn()}
- {...props}
- />
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
-import AutoProjectCreate from '../AutoProjectCreate';
-import { getRepositories } from '../../../../api/alm-integration';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-
-jest.mock('../../../../api/alm-integration', () => ({
- getRepositories: jest.fn().mockResolvedValue({
- almIntegration: {
- installationUrl: 'https://alm.foo.com/install',
- installed: false
- },
- repositories: []
- }),
- provisionProject: jest.fn().mockResolvedValue({ projects: [] })
-}));
-
-const identityProvider = {
- backgroundColor: 'blue',
- iconPath: 'icon/path',
- key: 'foo',
- name: 'Foo Provider'
-};
-
-const repositories = [
- {
- label: 'Cool Project',
- installationKey: 'github/cool',
- linkedProjectKey: 'proj_cool',
- linkedProjectName: 'Proj Cool'
- },
- {
- label: 'Awesome Project',
- installationKey: 'github/awesome'
- }
-];
-
-beforeEach(() => {
- (getRepositories as jest.Mock<any>).mockClear();
-});
-
-it('should display the provider app install button', async () => {
- const wrapper = getWrapper();
- expect(wrapper).toMatchSnapshot();
- expect(getRepositories).toHaveBeenCalled();
-
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should display the list of repositories', async () => {
- (getRepositories as jest.Mock<any>).mockResolvedValue({
- almIntegration: {
- installationUrl: 'https://alm.foo.com/install',
- installed: true
- },
- repositories
- });
- const wrapper = getWrapper();
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-});
-
-function getWrapper(props = {}) {
- return shallow(
- <AutoProjectCreate identityProvider={identityProvider} onProjectCreate={jest.fn()} {...props} />
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
-import { Location } from 'history';
-import { CreateProjectPage } from '../CreateProjectPage';
-import { getIdentityProviders } from '../../../../api/users';
-import { LoggedInUser } from '../../../../app/types';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-
-jest.mock('../../../../api/users', () => ({
- getIdentityProviders: jest.fn().mockResolvedValue({
- identityProviders: [
- {
- backgroundColor: 'blue',
- iconPath: 'icon/path',
- key: 'github',
- name: 'GitHub'
- }
- ]
- })
-}));
-
-const user: LoggedInUser = {
- externalProvider: 'github',
- groups: [],
- isLoggedIn: true,
- login: 'foo',
- name: 'Foo',
- scmAccounts: []
-};
-
-beforeEach(() => {
- (getIdentityProviders as jest.Mock<any>).mockClear();
-});
-
-it('should render correctly', async () => {
- const wrapper = getWrapper();
- expect(wrapper).toMatchSnapshot();
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should render with Manual creation only', () => {
- expect(getWrapper({ currentUser: { ...user, externalProvider: 'microsoft' } })).toMatchSnapshot();
-});
-
-it('should switch tabs', async () => {
- const replace = jest.fn();
- const wrapper = getWrapper({ router: { replace } });
- replace.mockImplementation(location => {
- wrapper.setProps({ location }).update();
- });
-
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-
- wrapper.find('Tabs').prop<Function>('onChange')('manual');
- expect(wrapper.find('Connect(ManualProjectCreate)').exists()).toBeTruthy();
- wrapper.find('Tabs').prop<Function>('onChange')('auto');
- expect(wrapper.find('AutoProjectCreate').exists()).toBeTruthy();
-});
-
-it('should display an error message on load', () => {
- const addGlobalErrorMessage = jest.fn();
- getWrapper({
- addGlobalErrorMessage,
- location: { pathname: 'foo', query: { error: 'Foo error' } }
- });
- expect(addGlobalErrorMessage).toHaveBeenCalledWith('Foo error');
-});
-
-function getWrapper(props = {}) {
- return shallow(
- <CreateProjectPage
- addGlobalErrorMessage={jest.fn()}
- currentUser={user}
- location={{ pathname: 'foo', query: { manual: 'false' } } as Location}
- router={{ push: jest.fn(), replace: jest.fn() }}
- skipOnboardingAction={jest.fn()}
- {...props}
- />
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
-import { ManualProjectCreate } from '../ManualProjectCreate';
-import { change, submit, waitAndUpdate } from '../../../../helpers/testUtils';
-import { createProject } from '../../../../api/components';
-
-jest.mock('../../../../api/components', () => ({
- createProject: jest.fn().mockResolvedValue({ project: { key: 'bar', name: 'Bar' } })
-}));
-
-beforeEach(() => {
- (createProject as jest.Mock<any>).mockClear();
-});
-
-it('should render correctly', () => {
- expect(getWrapper()).toMatchSnapshot();
-});
-
-it('should correctly create a project', async () => {
- const onProjectCreate = jest.fn();
- const wrapper = getWrapper({ onProjectCreate });
- wrapper.find('Select').prop<Function>('onChange')({ value: 'foo' });
- change(wrapper.find('#project-name'), 'Bar');
- expect(wrapper.find('SubmitButton')).toMatchSnapshot();
-
- change(wrapper.find('#project-key'), 'bar');
- expect(wrapper.find('SubmitButton')).toMatchSnapshot();
-
- submit(wrapper.find('form'));
- expect(createProject).toBeCalledWith({ project: 'bar', name: 'Bar', organization: 'foo' });
-
- await waitAndUpdate(wrapper);
- expect(onProjectCreate).toBeCalledWith(['bar']);
-});
-
-function getWrapper(props = {}) {
- return shallow(
- <ManualProjectCreate
- currentUser={{ groups: [], isLoggedIn: true, login: 'foo', name: 'Foo', scmAccounts: [] }}
- fetchMyOrganizations={jest.fn()}
- onProjectCreate={jest.fn()}
- userOrganizations={[{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }]}
- {...props}
- />
- );
-}
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<Fragment>
- <Checkbox
- checked={false}
- disabled={false}
- onCheck={[Function]}
- thirdState={false}
- >
- <img
- alt="Foo Provider"
- className="spacer-left"
- height={14}
- src="/images/sonarcloud/foo.svg"
- style={
- Object {
- "opacity": 1,
- }
- }
- width={14}
- />
- <span
- className="spacer-left"
- >
- Awesome Project
- </span>
- </Checkbox>
-</Fragment>
-`;
-
-exports[`should render disabled 1`] = `
-<Fragment>
- <Checkbox
- checked={true}
- disabled={true}
- onCheck={[Function]}
- thirdState={false}
- >
- <img
- alt="Foo Provider"
- className="spacer-left"
- height={14}
- src="/images/sonarcloud/foo.svg"
- style={
- Object {
- "opacity": 0.5,
- }
- }
- width={14}
- />
- <span
- className="spacer-left"
- >
- Cool Project
- </span>
- </Checkbox>
- <span
- className="big-spacer-left"
- >
- <CheckIcon
- className="little-spacer-right"
- fill="#00aa00"
- />
- <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/dashboard",
- "query": Object {
- "branch": undefined,
- "id": "proj_cool",
- },
- }
- }
- >
- onboarding.create_project.already_imported
- </Link>
- </span>
-</Fragment>
-`;
-
-exports[`should render selected 1`] = `
-<Fragment>
- <Checkbox
- checked={true}
- disabled={false}
- onCheck={[Function]}
- thirdState={false}
- >
- <img
- alt="Foo Provider"
- className="spacer-left"
- height={14}
- src="/images/sonarcloud/foo.svg"
- style={
- Object {
- "opacity": 1,
- }
- }
- width={14}
- />
- <span
- className="spacer-left"
- >
- Awesome Project
- </span>
- </Checkbox>
-</Fragment>
-`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display the list of repositories 1`] = `
-<Fragment>
- <Alert
- className="width-60 big-spacer-bottom"
- variant="info"
- >
- onboarding.create_project.beta_feature_x.Foo Provider
- </Alert>
- <form
- onSubmit={[Function]}
- >
- <ul>
- <li
- className="big-spacer-bottom"
- key="github/cool"
- >
- <AlmRepositoryItem
- identityProvider={
- Object {
- "backgroundColor": "blue",
- "iconPath": "icon/path",
- "key": "foo",
- "name": "Foo Provider",
- }
- }
- repository={
- Object {
- "installationKey": "github/cool",
- "label": "Cool Project",
- "linkedProjectKey": "proj_cool",
- "linkedProjectName": "Proj Cool",
- }
- }
- selected={false}
- toggleRepository={[Function]}
- />
- </li>
- <li
- className="big-spacer-bottom"
- key="github/awesome"
- >
- <AlmRepositoryItem
- identityProvider={
- Object {
- "backgroundColor": "blue",
- "iconPath": "icon/path",
- "key": "foo",
- "name": "Foo Provider",
- }
- }
- repository={
- Object {
- "installationKey": "github/awesome",
- "label": "Awesome Project",
- }
- }
- selected={false}
- toggleRepository={[Function]}
- />
- </li>
- </ul>
- <SubmitButton
- disabled={true}
- >
- create
- </SubmitButton>
- <DeferredSpinner
- className="spacer-left"
- loading={false}
- timeout={100}
- />
- </form>
-</Fragment>
-`;
-
-exports[`should display the provider app install button 1`] = `
-<Fragment>
- <Alert
- className="width-60 big-spacer-bottom"
- variant="info"
- >
- onboarding.create_project.beta_feature_x.Foo Provider
- </Alert>
- <DeferredSpinner
- timeout={100}
- />
-</Fragment>
-`;
-
-exports[`should display the provider app install button 2`] = `
-<Fragment>
- <Alert
- className="width-60 big-spacer-bottom"
- variant="info"
- >
- onboarding.create_project.beta_feature_x.Foo Provider
- </Alert>
- <div>
- <p
- className="spacer-bottom"
- >
- onboarding.create_project.install_app_x.Foo Provider
- </p>
- <IdentityProviderLink
- className="display-inline-block"
- identityProvider={
- Object {
- "backgroundColor": "blue",
- "iconPath": "icon/path",
- "key": "foo",
- "name": "Foo Provider",
- }
- }
- small={true}
- url="https://alm.foo.com/install"
- >
- onboarding.create_project.install_app_x.button.Foo Provider
- </IdentityProviderLink>
- </div>
-</Fragment>
-`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<Fragment>
- <HelmetWrapper
- defer={true}
- encodeSpecialCharacters={true}
- title="onboarding.create_project.header"
- titleTemplate="%s"
- />
- <div
- className="sonarcloud page page-limited"
- >
- <header
- className="page-header"
- >
- <h1
- className="page-title"
- >
- onboarding.create_project.header
- </h1>
- </header>
- <DeferredSpinner
- timeout={100}
- />
- </div>
-</Fragment>
-`;
-
-exports[`should render correctly 2`] = `
-<Fragment>
- <HelmetWrapper
- defer={true}
- encodeSpecialCharacters={true}
- title="onboarding.create_project.header"
- titleTemplate="%s"
- />
- <div
- className="sonarcloud page page-limited"
- >
- <header
- className="page-header"
- >
- <h1
- className="page-title"
- >
- onboarding.create_project.header
- </h1>
- </header>
- <Tabs
- onChange={[Function]}
- selected="auto"
- tabs={
- Array [
- Object {
- "key": "auto",
- "node": <React.Fragment>
- onboarding.create_project.select_repositories
- <span
- className="beta-badge spacer-left"
- >
- beta
- </span>
- </React.Fragment>,
- },
- Object {
- "key": "manual",
- "node": "onboarding.create_project.create_manually",
- },
- ]
- }
- />
- <AutoProjectCreate
- identityProvider={
- Object {
- "backgroundColor": "blue",
- "iconPath": "icon/path",
- "key": "github",
- "name": "GitHub",
- }
- }
- onProjectCreate={[Function]}
- />
- </div>
-</Fragment>
-`;
-
-exports[`should render with Manual creation only 1`] = `
-<Fragment>
- <HelmetWrapper
- defer={true}
- encodeSpecialCharacters={true}
- title="onboarding.create_project.header"
- titleTemplate="%s"
- />
- <div
- className="sonarcloud page page-limited"
- >
- <header
- className="page-header"
- >
- <h1
- className="page-title"
- >
- onboarding.create_project.header
- </h1>
- </header>
- <Connect(ManualProjectCreate)
- currentUser={
- Object {
- "externalProvider": "microsoft",
- "groups": Array [],
- "isLoggedIn": true,
- "login": "foo",
- "name": "Foo",
- "scmAccounts": Array [],
- }
- }
- onProjectCreate={[Function]}
- />
- </div>
-</Fragment>
-`;
-
-exports[`should switch tabs 1`] = `
-<Fragment>
- <HelmetWrapper
- defer={true}
- encodeSpecialCharacters={true}
- title="onboarding.create_project.header"
- titleTemplate="%s"
- />
- <div
- className="sonarcloud page page-limited"
- >
- <header
- className="page-header"
- >
- <h1
- className="page-title"
- >
- onboarding.create_project.header
- </h1>
- </header>
- <Tabs
- onChange={[Function]}
- selected="auto"
- tabs={
- Array [
- Object {
- "key": "auto",
- "node": <React.Fragment>
- onboarding.create_project.select_repositories
- <span
- className="beta-badge spacer-left"
- >
- beta
- </span>
- </React.Fragment>,
- },
- Object {
- "key": "manual",
- "node": "onboarding.create_project.create_manually",
- },
- ]
- }
- />
- <AutoProjectCreate
- identityProvider={
- Object {
- "backgroundColor": "blue",
- "iconPath": "icon/path",
- "key": "github",
- "name": "GitHub",
- }
- }
- onProjectCreate={[Function]}
- />
- </div>
-</Fragment>
-`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should correctly create a project 1`] = `
-<SubmitButton
- disabled={true}
->
- create
-</SubmitButton>
-`;
-
-exports[`should correctly create a project 2`] = `
-<SubmitButton
- disabled={false}
->
- create
-</SubmitButton>
-`;
-
-exports[`should render correctly 1`] = `
-<Fragment>
- <form
- onSubmit={[Function]}
- >
- <div
- className="form-field"
- >
- <label
- htmlFor="select-organization"
- >
- onboarding.create_project.organization
- <em
- className="mandatory"
- >
- *
- </em>
- </label>
- <Select
- autoFocus={true}
- className="input-super-large"
- clearable={false}
- id="select-organization"
- onChange={[Function]}
- options={
- Array [
- Object {
- "label": "Bar",
- "value": "bar",
- },
- Object {
- "label": "Foo",
- "value": "foo",
- },
- ]
- }
- required={true}
- value=""
- />
- <Link
- className="big-spacer-left js-new-org"
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/create-organization"
- >
- onboarding.create_project.create_new_org
- </Link>
- </div>
- <div
- className="form-field"
- >
- <label
- htmlFor="project-name"
- >
- onboarding.create_project.project_name
- <em
- className="mandatory"
- >
- *
- </em>
- </label>
- <input
- className="input-super-large"
- id="project-name"
- maxLength={400}
- minLength={1}
- onChange={[Function]}
- required={true}
- type="text"
- value=""
- />
- </div>
- <div
- className="form-field"
- >
- <label
- htmlFor="project-key"
- >
- onboarding.create_project.project_key
- <em
- className="mandatory"
- >
- *
- </em>
- </label>
- <input
- className="input-super-large"
- id="project-key"
- maxLength={400}
- minLength={1}
- onChange={[Function]}
- required={true}
- type="text"
- value=""
- />
- </div>
- <SubmitButton
- disabled={true}
- >
- create
- </SubmitButton>
- <DeferredSpinner
- className="spacer-left"
- loading={false}
- timeout={100}
- />
- </form>
-</Fragment>
-`;
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { memoize } from 'lodash';
-import {
- cleanQuery,
- RawQuery,
- parseAsBoolean,
- serializeOptionalBoolean,
- parseAsOptionalString,
- serializeString
-} from '../../../helpers/query';
-
-export interface Query {
- error?: string;
- manual: boolean;
- organization?: string;
-}
-
-export const parseQuery = memoize(
- (urlQuery: RawQuery): Query => {
- return {
- error: parseAsOptionalString(urlQuery['error']),
- manual: parseAsBoolean(urlQuery['manual'], false),
- organization: parseAsOptionalString(urlQuery['organization'])
- };
- }
-);
-
-export const serializeQuery = memoize(
- (query: Query): RawQuery =>
- cleanQuery({
- manual: serializeOptionalBoolean(query.manual || undefined),
- organization: serializeString(query.organization)
- })
-);
{ path: 'favorite', component: FavoriteProjectsContainer },
isSonarCloud() && {
path: 'create',
- component: lazyLoad(() => import('./create/CreateProjectPage'))
+ component: lazyLoad(() => import('../create/project/CreateProjectPage'))
}
].filter(Boolean);
import * as classNames from 'classnames';
import './Tabs.css';
-interface Props {
- onChange: (tab: string) => void;
- selected?: string;
- tabs: Array<{ disabled?: boolean; key: string; node: React.ReactNode }>;
+interface Props<T extends string> {
+ onChange: (tab: T) => void;
+ selected?: T;
+ tabs: Array<{ disabled?: boolean; key: T; node: React.ReactNode }>;
}
-export default function Tabs({ onChange, selected, tabs }: Props) {
+export default function Tabs<T extends string>({ onChange, selected, tabs }: Props<T>) {
return (
<ul className="flex-tabs">
{tabs.map(tab => (
);
}
-interface TabProps {
+interface TabProps<T> {
children: React.ReactNode;
disabled?: boolean;
- name: string;
- onSelect: (tab: string) => void;
+ name: T;
+ onSelect: (tab: T) => void;
selected: boolean;
}
-export class Tab extends React.PureComponent<TabProps> {
+export class Tab<T> extends React.PureComponent<TabProps<T>> {
handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.stopPropagation();
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 AlertErrorIcon from '../icons-components/AlertErrorIcon';
+import AlertSuccessIcon from '../icons-components/AlertSuccessIcon';
+
+interface Props {
+ description?: string;
+ children: React.ReactNode;
+ error: string | undefined;
+ id: string;
+ isInvalid: boolean;
+ isValid: boolean;
+ label: React.ReactNode;
+ required?: boolean;
+}
+
+export default function ValidationInput(props: Props) {
+ const hasError = props.isInvalid && props.error !== undefined;
+ return (
+ <div>
+ <label htmlFor={props.id}>
+ <strong>{props.label}</strong>
+ {props.required && <em className="mandatory">*</em>}
+ </label>
+ <div className="little-spacer-top spacer-bottom">
+ {props.children}
+ {props.isInvalid && <AlertErrorIcon className="spacer-left text-middle" />}
+ {hasError && (
+ <span className="little-spacer-left text-danger text-middle">{props.error}</span>
+ )}
+ {props.isValid && <AlertSuccessIcon className="spacer-left text-middle" />}
+ </div>
+ {props.description && <div className="note abs-width-400">{props.description}</div>}
+ </div>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import ValidationInput from '../ValidationInput';
+
+it('should render', () => {
+ expect(
+ shallow(
+ <ValidationInput
+ description="My description"
+ error={undefined}
+ id="field-id"
+ isInvalid={false}
+ isValid={false}
+ label="Field label"
+ required={true}>
+ <div />
+ </ValidationInput>
+ )
+ ).toMatchSnapshot();
+});
+
+it('should render with error', () => {
+ expect(
+ shallow(
+ <ValidationInput
+ description="My description"
+ error="Field error message"
+ id="field-id"
+ isInvalid={true}
+ isValid={false}
+ label="Field label">
+ <div />
+ </ValidationInput>
+ )
+ ).toMatchSnapshot();
+});
+
+it('should render when valid', () => {
+ expect(
+ shallow(
+ <ValidationInput
+ description="My description"
+ error={undefined}
+ id="field-id"
+ isInvalid={false}
+ isValid={true}
+ label="Field label"
+ required={true}>
+ <div />
+ </ValidationInput>
+ )
+ ).toMatchSnapshot();
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div>
+ <label
+ htmlFor="field-id"
+ >
+ <strong>
+ Field label
+ </strong>
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <div
+ className="little-spacer-top spacer-bottom"
+ >
+ <div />
+ </div>
+ <div
+ className="note abs-width-400"
+ >
+ My description
+ </div>
+</div>
+`;
+
+exports[`should render when valid 1`] = `
+<div>
+ <label
+ htmlFor="field-id"
+ >
+ <strong>
+ Field label
+ </strong>
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <div
+ className="little-spacer-top spacer-bottom"
+ >
+ <div />
+ <AlertSuccessIcon
+ className="spacer-left text-middle"
+ />
+ </div>
+ <div
+ className="note abs-width-400"
+ >
+ My description
+ </div>
+</div>
+`;
+
+exports[`should render with error 1`] = `
+<div>
+ <label
+ htmlFor="field-id"
+ >
+ <strong>
+ Field label
+ </strong>
+ </label>
+ <div
+ className="little-spacer-top spacer-bottom"
+ >
+ <div />
+ <AlertErrorIcon
+ className="spacer-left text-middle"
+ />
+ <span
+ className="little-spacer-left text-danger text-middle"
+ >
+ Field error message
+ </span>
+ </div>
+ <div
+ className="note abs-width-400"
+ >
+ My description
+ </div>
+</div>
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow, ShallowWrapper } from 'enzyme';
+import { createStore } from 'redux';
+import { mockRouter } from '../../../helpers/testUtils';
+import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
+import { whenLoggedIn } from '../whenLoggedIn';
+
+jest.mock('../../../app/utils/handleRequiredAuthentication', () => ({
+ default: jest.fn()
+}));
+
+class X extends React.Component {
+ render() {
+ return <div />;
+ }
+}
+
+const UnderTest = whenLoggedIn(X);
+
+it('should render for logged in user', () => {
+ const store = createStore(state => state, { users: { currentUser: { isLoggedIn: true } } });
+ const wrapper = shallow(<UnderTest />, { context: { store } });
+ expect(getRenderedType(wrapper)).toBe(X);
+});
+
+it('should not render for anonymous user', () => {
+ const store = createStore(state => state, { users: { currentUser: { isLoggedIn: false } } });
+ const router = mockRouter({ replace: jest.fn() });
+ const wrapper = shallow(<UnderTest />, { context: { store, router } });
+ expect(getRenderedType(wrapper)).toBe(null);
+ expect(handleRequiredAuthentication).toBeCalled();
+});
+
+function getRenderedType(wrapper: ShallowWrapper) {
+ return wrapper
+ .dive()
+ .dive()
+ .dive()
+ .type();
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import { createStore } from 'redux';
+import { CurrentUser } from '../../../app/types';
+import { withCurrentUser } from '../withCurrentUser';
+
+class X extends React.Component<{ currentUser: CurrentUser }> {
+ render() {
+ return <div />;
+ }
+}
+
+const UnderTest = withCurrentUser(X);
+
+it('should pass logged in user', () => {
+ const currentUser = { isLoggedIn: false };
+ const store = createStore(state => state, { users: { currentUser } });
+ const wrapper = shallow(<UnderTest />, { context: { store } });
+ expect(wrapper.dive().type()).toBe(X);
+ expect(wrapper.dive().prop('currentUser')).toBe(currentUser);
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { withRouter, WithRouterProps } from 'react-router';
+import { withCurrentUser } from './withCurrentUser';
+import { CurrentUser } from '../../app/types';
+import { isLoggedIn } from '../../helpers/users';
+import handleRequiredAuthentication from '../../app/utils/handleRequiredAuthentication';
+
+export function whenLoggedIn<P>(WrappedComponent: React.ComponentClass<P>) {
+ const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
+
+ class Wrapper extends React.Component<P & { currentUser: CurrentUser } & WithRouterProps> {
+ static displayName = `whenLoggedIn(${wrappedDisplayName})`;
+
+ componentDidMount() {
+ if (!isLoggedIn(this.props.currentUser)) {
+ handleRequiredAuthentication();
+ }
+ }
+
+ render() {
+ if (isLoggedIn(this.props.currentUser)) {
+ return <WrappedComponent {...this.props} />;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ return withCurrentUser(withRouter(Wrapper));
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { connect } from 'react-redux';
+import { CurrentUser } from '../../app/types';
+import { Store, getCurrentUser } from '../../store/rootReducer';
+
+export function withCurrentUser<P>(
+ WrappedComponent: React.ComponentClass<P & { currentUser: CurrentUser }>
+) {
+ const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
+
+ class Wrapper extends React.Component<P & { currentUser: CurrentUser }> {
+ static displayName = `withCurrentUser(${wrappedDisplayName})`;
+
+ render() {
+ return <WrappedComponent {...this.props} />;
+ }
+ }
+
+ function mapStateToProps(state: Store) {
+ return { currentUser: getCurrentUser(state) };
+ }
+
+ return connect(mapStateToProps)(Wrapper);
+}
onboarding.create_project.header=Create project(s)
onboarding.create_project.already_imported=Repository already imported
-onboarding.create_project.beta_feature_x=This feature is being beta tested. We offer to create projects from your {0} repositories only for public personal projects on your personal SonarCloud organization. For other kind of projects please create them manually.
onboarding.create_project.create_manually=Create manually
onboarding.create_project.create_new_org=I want to create another organization
-onboarding.create_project.install_app_x=We need you to install the Sonarcloud {0} application in order to select which repositories you want to analyze.
+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.organization=Organization
onboarding.create_project.project_key=Project key
onboarding.create_organization.organization_name.taken=This name is already taken.
onboarding.create_organization.add_additional_info=Add additional info
onboarding.create_organization.hide_additional_info=Hide additional info
+onboarding.create_organization.description=Description
+onboarding.create_organization.description.error=The provided value doesn't match the expected format.
onboarding.create_organization.display_name=Display Name
onboarding.create_organization.display_name.description=Up to 255 characters
onboarding.create_organization.display_name.error=The provided value doesn't match the expected format.