@@ -190,6 +190,7 @@ export default class Onboarding extends React.PureComponent { | |||
)} | |||
<TokenStep | |||
currentUser={this.props.currentUser} | |||
finished={this.state.token != null} | |||
onContinue={this.handleTokenDone} | |||
onOpen={this.handleTokenOpen} |
@@ -21,12 +21,14 @@ | |||
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, | |||
@@ -37,7 +39,8 @@ type Props = {| | |||
/*:: | |||
type State = { | |||
existingToken:string, | |||
canUseExisting: boolean, | |||
existingToken: string, | |||
loading: boolean, | |||
selection: string, | |||
tokenName?: string, | |||
@@ -49,6 +52,7 @@ export default class TokenStep extends React.PureComponent { | |||
/*:: mounted: boolean; */ | |||
/*:: props: Props; */ | |||
state /*: State */ = { | |||
canUseExisting: false, | |||
existingToken: '', | |||
loading: false, | |||
selection: 'generate' | |||
@@ -56,6 +60,14 @@ export default class TokenStep extends React.PureComponent { | |||
componentDidMount() { | |||
this.mounted = true; | |||
getTokens(this.props.currentUser.login).then( | |||
tokens => { | |||
if (this.mounted) { | |||
this.setState({ canUseExisting: tokens.length > 0 }); | |||
} | |||
}, | |||
() => {} | |||
); | |||
} | |||
componentWillUnmount() { | |||
@@ -65,6 +77,15 @@ export default class TokenStep extends React.PureComponent { | |||
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 }); | |||
}; | |||
@@ -133,17 +154,21 @@ export default class TokenStep extends React.PureComponent { | |||
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}> | |||
@@ -169,37 +194,48 @@ export default class TokenStep extends React.PureComponent { | |||
</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"> | |||
@@ -221,14 +257,13 @@ export default class TokenStep extends React.PureComponent { | |||
) : ( | |||
<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')} |
@@ -24,13 +24,17 @@ import TokenStep from '../TokenStep'; | |||
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()} | |||
@@ -38,6 +42,7 @@ it('generates token', () => { | |||
stepNumber={1} | |||
/> | |||
); | |||
await new Promise(setImmediate); | |||
expect(wrapper).toMatchSnapshot(); | |||
change(wrapper.find('input'), 'my token'); | |||
submit(wrapper.find('form')); | |||
@@ -45,9 +50,10 @@ it('generates token', () => { | |||
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()} | |||
@@ -55,6 +61,7 @@ it('revokes token', () => { | |||
stepNumber={1} | |||
/> | |||
); | |||
await new Promise(setImmediate); | |||
wrapper.setState({ token: 'abcd1234', tokenName: 'my token' }); | |||
expect(wrapper).toMatchSnapshot(); | |||
submit(wrapper.find('form')); | |||
@@ -62,10 +69,11 @@ it('revokes token', () => { | |||
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} | |||
@@ -73,15 +81,17 @@ it('continues', () => { | |||
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} | |||
@@ -89,6 +99,7 @@ it('uses existing token', () => { | |||
stepNumber={1} | |||
/> | |||
); | |||
await new Promise(setImmediate); | |||
wrapper.setState({ existingToken: 'abcd1234', selection: 'use-existing' }); | |||
click(wrapper.find('.js-continue')); | |||
expect(onContinue).toBeCalledWith('abcd1234'); |
@@ -43,6 +43,12 @@ exports[`guides for on-premise 1`] = ` | |||
</div> | |||
</header> | |||
<TokenStep | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": true, | |||
"login": "admin", | |||
} | |||
} | |||
finished={false} | |||
onContinue={[Function]} | |||
onOpen={[Function]} | |||
@@ -103,6 +109,12 @@ exports[`guides for on-premise 2`] = ` | |||
</div> | |||
</header> | |||
<TokenStep | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": true, | |||
"login": "admin", | |||
} | |||
} | |||
finished={true} | |||
onContinue={[Function]} | |||
onOpen={[Function]} | |||
@@ -177,6 +189,12 @@ exports[`guides for sonarcloud 1`] = ` | |||
stepNumber={1} | |||
/> | |||
<TokenStep | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": true, | |||
"login": "admin", | |||
} | |||
} | |||
finished={false} | |||
onContinue={[Function]} | |||
onOpen={[Function]} | |||
@@ -250,6 +268,12 @@ exports[`guides for sonarcloud 2`] = ` | |||
stepNumber={1} | |||
/> | |||
<TokenStep | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": true, | |||
"login": "admin", | |||
} | |||
} | |||
finished={false} | |||
onContinue={[Function]} | |||
onOpen={[Function]} | |||
@@ -324,6 +348,12 @@ exports[`guides for sonarcloud 3`] = ` | |||
stepNumber={1} | |||
/> | |||
<TokenStep | |||
currentUser={ | |||
Object { | |||
"isLoggedIn": true, | |||
"login": "admin", | |||
} | |||
} | |||
finished={true} | |||
onContinue={[Function]} | |||
onOpen={[Function]} |
@@ -2,6 +2,11 @@ | |||
exports[`generates token 1`] = ` | |||
<TokenStep | |||
currentUser={ | |||
Object { | |||
"login": "user", | |||
} | |||
} | |||
finished={false} | |||
onContinue={[Function]} | |||
onOpen={[Function]} | |||
@@ -99,6 +104,11 @@ exports[`generates token 1`] = ` | |||
exports[`generates token 2`] = ` | |||
<TokenStep | |||
currentUser={ | |||
Object { | |||
"login": "user", | |||
} | |||
} | |||
finished={false} | |||
onContinue={[Function]} | |||
onOpen={[Function]} | |||
@@ -193,6 +203,11 @@ exports[`generates token 2`] = ` | |||
exports[`generates token 3`] = ` | |||
<TokenStep | |||
currentUser={ | |||
Object { | |||
"login": "user", | |||
} | |||
} | |||
finished={false} | |||
onContinue={[Function]} | |||
onOpen={[Function]} | |||
@@ -285,6 +300,11 @@ exports[`generates token 3`] = ` | |||
exports[`revokes token 1`] = ` | |||
<TokenStep | |||
currentUser={ | |||
Object { | |||
"login": "user", | |||
} | |||
} | |||
finished={false} | |||
onContinue={[Function]} | |||
onOpen={[Function]} | |||
@@ -377,6 +397,11 @@ exports[`revokes token 1`] = ` | |||
exports[`revokes token 2`] = ` | |||
<TokenStep | |||
currentUser={ | |||
Object { | |||
"login": "user", | |||
} | |||
} | |||
finished={false} | |||
onContinue={[Function]} | |||
onOpen={[Function]} | |||
@@ -451,6 +476,11 @@ exports[`revokes token 2`] = ` | |||
exports[`revokes token 3`] = ` | |||
<TokenStep | |||
currentUser={ | |||
Object { | |||
"login": "user", | |||
} | |||
} | |||
finished={false} | |||
onContinue={[Function]} | |||
onOpen={[Function]} |
@@ -2432,6 +2432,7 @@ onboading.token.generate_token=Generate a token | |||
onboading.token.generate_token.placeholder=Enter a name for your token | |||
onboarding.token.use_existing_token=Use existing token | |||
onboarding.token.use_existing_token.placeholder=Enter your existing token | |||
onboarding.token.invalid_format=The token you have entered has invalid format. | |||
onboarding.organization.header=Choose an organization for your project | |||
onboarding.organization.text=Organizations are where your projects belong. You can add your team members to your organization later to allow them to contribute to your projects. |