import React from 'react';
import classNames from 'classnames';
import Step from './Step';
+import { getTokens, generateToken, revokeToken } from '../../../api/user-tokens';
+import AlertErrorIcon from '../../../components/icons-components/AlertErrorIcon';
import CloseIcon from '../../../components/icons-components/CloseIcon';
-import { generateToken, revokeToken } from '../../../api/user-tokens';
import { translate } from '../../../helpers/l10n';
/*::
type Props = {|
+ currentUser: { login: string },
finished: boolean,
open: boolean,
onContinue: (token: string) => void,
/*::
type State = {
- existingToken:string,
+ canUseExisting: boolean,
+ existingToken: string,
loading: boolean,
selection: string,
tokenName?: string,
/*:: mounted: boolean; */
/*:: props: Props; */
state /*: State */ = {
+ canUseExisting: false,
existingToken: '',
loading: false,
selection: 'generate'
componentDidMount() {
this.mounted = true;
+ getTokens(this.props.currentUser.login).then(
+ tokens => {
+ if (this.mounted) {
+ this.setState({ canUseExisting: tokens.length > 0 });
+ }
+ },
+ () => {}
+ );
}
componentWillUnmount() {
getToken = () =>
this.state.selection === 'generate' ? this.state.token : this.state.existingToken;
+ canContinue = () => {
+ const { existingToken, selection, token } = this.state;
+ const validExistingToken = existingToken.match(/^[a-z0-9]+$/) != null;
+ return (
+ (selection === 'generate' && token != null) ||
+ (selection === 'use-existing' && existingToken && validExistingToken)
+ );
+ };
+
handleTokenNameChange = (event /*: { target: HTMLInputElement } */) => {
this.setState({ tokenName: event.target.value });
};
renderGenerateOption = () => (
<div>
- <a
- className="js-new link-base-color link-no-underline"
- href="#"
- onClick={this.handleGenerateClick}>
- <i
- className={classNames('icon-radio', 'spacer-right', {
- 'is-checked': this.state.selection === 'generate'
- })}
- />
- {translate('onboading.token.generate_token')}
- </a>
+ {this.state.canUseExisting ? (
+ <a
+ className="js-new link-base-color link-no-underline"
+ href="#"
+ onClick={this.handleGenerateClick}>
+ <i
+ className={classNames('icon-radio', 'spacer-right', {
+ 'is-checked': this.state.selection === 'generate'
+ })}
+ />
+ {translate('onboading.token.generate_token')}
+ </a>
+ ) : (
+ translate('onboading.token.generate_token')
+ )}
{this.state.selection === 'generate' && (
<div className="big-spacer-top">
<form onSubmit={this.handleTokenGenerate}>
</div>
);
- renderUseExistingOption = () => (
- <div className="big-spacer-top">
- <a
- className="js-new link-base-color link-no-underline"
- href="#"
- onClick={this.handleUseExistingClick}>
- <i
- className={classNames('icon-radio', 'spacer-right', {
- 'is-checked': this.state.selection === 'use-existing'
- })}
- />
- {translate('onboarding.token.use_existing_token')}
- </a>
- {this.state.selection === 'use-existing' && (
- <div className="big-spacer-top">
- <input
- autoFocus={true}
- className="input-large spacer-right text-middle"
- onChange={this.handleExisingTokenChange}
- placeholder={translate('onboarding.token.use_existing_token.placeholder')}
- required={true}
- type="text"
- value={this.state.existingToken}
+ renderUseExistingOption = () => {
+ const { existingToken } = this.state;
+ const validInput = !existingToken || existingToken.match(/^[a-z0-9]+$/) != null;
+
+ return (
+ <div className="big-spacer-top">
+ <a
+ className="js-new link-base-color link-no-underline"
+ href="#"
+ onClick={this.handleUseExistingClick}>
+ <i
+ className={classNames('icon-radio', 'spacer-right', {
+ 'is-checked': this.state.selection === 'use-existing'
+ })}
/>
- </div>
- )}
- </div>
- );
+ {translate('onboarding.token.use_existing_token')}
+ </a>
+ {this.state.selection === 'use-existing' && (
+ <div className="big-spacer-top">
+ <input
+ autoFocus={true}
+ className="input-large spacer-right text-middle"
+ onChange={this.handleExisingTokenChange}
+ placeholder={translate('onboarding.token.use_existing_token.placeholder')}
+ required={true}
+ type="text"
+ value={this.state.existingToken}
+ />
+ {!validInput && (
+ <span className="text-danger">
+ <AlertErrorIcon className="little-spacer-right text-text-top" />
+ {translate('onboarding.token.invalid_format')}
+ </span>
+ )}
+ </div>
+ )}
+ </div>
+ );
+ };
renderForm = () => {
- const { existingToken, loading, selection, token, tokenName } = this.state;
+ const { canUseExisting, loading, token, tokenName } = this.state;
return (
<div className="boxed-group-inner">
) : (
<div>
{this.renderGenerateOption()}
- {this.renderUseExistingOption()}
+ {canUseExisting && this.renderUseExistingOption()}
</div>
)}
<div className="note big-spacer-top width-50">{translate('onboarding.token.text')}</div>
- {((selection === 'generate' && token != null) ||
- (selection === 'use-existing' && existingToken)) && (
+ {this.canContinue() && (
<div className="big-spacer-top">
<button className="js-continue" onClick={this.handleContinueClick}>
{translate('continue')}
import { change, click, doAsync, submit } from '../../../../helpers/testUtils';
jest.mock('../../../../api/user-tokens', () => ({
+ getTokens: () => Promise.resolve([{ name: 'foo' }]),
generateToken: () => Promise.resolve({ token: 'abcd1234' }),
revokeToken: () => Promise.resolve()
}));
-it('generates token', () => {
+const currentUser = { login: 'user' };
+
+it('generates token', async () => {
const wrapper = mount(
<TokenStep
+ currentUser={currentUser}
finished={false}
open={true}
onContinue={jest.fn()}
stepNumber={1}
/>
);
+ await new Promise(setImmediate);
expect(wrapper).toMatchSnapshot();
change(wrapper.find('input'), 'my token');
submit(wrapper.find('form'));
return doAsync(() => expect(wrapper).toMatchSnapshot());
});
-it('revokes token', () => {
+it('revokes token', async () => {
const wrapper = mount(
<TokenStep
+ currentUser={currentUser}
finished={false}
open={true}
onContinue={jest.fn()}
stepNumber={1}
/>
);
+ await new Promise(setImmediate);
wrapper.setState({ token: 'abcd1234', tokenName: 'my token' });
expect(wrapper).toMatchSnapshot();
submit(wrapper.find('form'));
return doAsync(() => expect(wrapper).toMatchSnapshot());
});
-it('continues', () => {
+it('continues', async () => {
const onContinue = jest.fn();
const wrapper = mount(
<TokenStep
+ currentUser={currentUser}
finished={false}
open={true}
onContinue={onContinue}
stepNumber={1}
/>
);
+ await new Promise(setImmediate);
wrapper.setState({ token: 'abcd1234', tokenName: 'my token' });
click(wrapper.find('.js-continue'));
expect(onContinue).toBeCalledWith('abcd1234');
});
-it('uses existing token', () => {
+it('uses existing token', async () => {
const onContinue = jest.fn();
const wrapper = mount(
<TokenStep
+ currentUser={currentUser}
finished={false}
open={true}
onContinue={onContinue}
stepNumber={1}
/>
);
+ await new Promise(setImmediate);
wrapper.setState({ existingToken: 'abcd1234', selection: 'use-existing' });
click(wrapper.find('.js-continue'));
expect(onContinue).toBeCalledWith('abcd1234');