@@ -19,7 +19,7 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import InstanceMessage from '../../../components/common/InstanceMessage'; | |||
import TokenForm from '../../users/components/TokensForm'; | |||
import TokensForm from '../../users/components/TokensForm'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
@@ -35,7 +35,7 @@ export default function Tokens({ login }: Props) { | |||
<InstanceMessage message={translate('my_account.tokens_description')} /> | |||
</div> | |||
<TokenForm login={login} /> | |||
<TokensForm deleteConfirmation="modal" login={login} /> | |||
</div> | |||
</div> | |||
); |
@@ -18,6 +18,7 @@ exports[`renders 1`] = ` | |||
/> | |||
</div> | |||
<TokensForm | |||
deleteConfirmation="modal" | |||
login="user" | |||
/> | |||
</div> |
@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import TokensFormItem from './TokensFormItem'; | |||
import TokensFormItem, { TokenDeleteConfirmation } from './TokensFormItem'; | |||
import TokensFormNewToken from './TokensFormNewToken'; | |||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import { SubmitButton } from '../../../components/ui/buttons'; | |||
@@ -26,6 +26,7 @@ import { getTokens, generateToken } from '../../../api/user-tokens'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
deleteConfirmation: TokenDeleteConfirmation; | |||
login: string; | |||
updateTokensCount?: (login: string, tokensCount: number) => void; | |||
} | |||
@@ -128,6 +129,7 @@ export default class TokensForm extends React.PureComponent<Props, State> { | |||
} | |||
return tokens.map(token => ( | |||
<TokensFormItem | |||
deleteConfirmation={this.props.deleteConfirmation} | |||
key={token.name} | |||
login={this.props.login} | |||
onRevokeToken={this.handleRevokeToken} | |||
@@ -171,7 +173,7 @@ export default class TokensForm extends React.PureComponent<Props, State> { | |||
{newToken && <TokensFormNewToken token={newToken} />} | |||
<table className="data zebra big-spacer-top "> | |||
<table className="data zebra big-spacer-top"> | |||
<thead> | |||
<tr> | |||
<th>{translate('name')}</th> |
@@ -18,29 +18,34 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { Button } from '../../../components/ui/buttons'; | |||
import ConfirmButton from '../../../components/controls/ConfirmButton'; | |||
import DateFormatter from '../../../components/intl/DateFormatter'; | |||
import DateFromNowHourPrecision from '../../../components/intl/DateFromNowHourPrecision'; | |||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import { Button } from '../../../components/ui/buttons'; | |||
import { limitComponentName } from '../../../helpers/path'; | |||
import { revokeToken } from '../../../api/user-tokens'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export type TokenDeleteConfirmation = 'inline' | 'modal'; | |||
interface Props { | |||
deleteConfirmation: TokenDeleteConfirmation; | |||
login: string; | |||
onRevokeToken: (token: T.UserToken) => void; | |||
token: T.UserToken; | |||
} | |||
interface State { | |||
deleting: boolean; | |||
loading: boolean; | |||
showConfirmation: boolean; | |||
} | |||
export default class TokensFormItem extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { deleting: false, loading: false }; | |||
state: State = { loading: false, showConfirmation: false }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
@@ -50,25 +55,33 @@ export default class TokensFormItem extends React.PureComponent<Props, State> { | |||
this.mounted = false; | |||
} | |||
handleRevoke = () => { | |||
if (this.state.deleting) { | |||
this.setState({ loading: true }); | |||
revokeToken({ login: this.props.login, name: this.props.token.name }).then( | |||
() => this.props.onRevokeToken(this.props.token), | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false, deleting: false }); | |||
} | |||
handleClick = () => { | |||
if (this.state.showConfirmation) { | |||
this.handleRevoke().then(() => { | |||
if (this.mounted) { | |||
this.setState({ showConfirmation: false }); | |||
} | |||
); | |||
}); | |||
} else { | |||
this.setState({ deleting: true }); | |||
this.setState({ showConfirmation: true }); | |||
} | |||
}; | |||
handleRevoke = () => { | |||
this.setState({ loading: true }); | |||
return revokeToken({ login: this.props.login, name: this.props.token.name }).then( | |||
() => this.props.onRevokeToken(this.props.token), | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
} | |||
); | |||
}; | |||
render() { | |||
const { token } = this.props; | |||
const { loading } = this.state; | |||
const { deleteConfirmation, token } = this.props; | |||
const { loading, showConfirmation } = this.state; | |||
return ( | |||
<tr> | |||
<td> | |||
@@ -86,14 +99,37 @@ export default class TokensFormItem extends React.PureComponent<Props, State> { | |||
<DeferredSpinner loading={loading}> | |||
<i className="spinner-placeholder" /> | |||
</DeferredSpinner> | |||
<Button | |||
className="button-red input-small spacer-left" | |||
disabled={loading} | |||
onClick={this.handleRevoke}> | |||
{this.state.deleting | |||
? translate('users.tokens.sure') | |||
: translate('users.tokens.revoke')} | |||
</Button> | |||
{deleteConfirmation === 'modal' ? ( | |||
<ConfirmButton | |||
confirmButtonText={translate('users.tokens.revoke_token')} | |||
isDestructive={true} | |||
modalBody={ | |||
<FormattedMessage | |||
defaultMessage={translate('users.tokens.sure_X')} | |||
id="users.tokens.sure_X" | |||
values={{ token: <strong>{token.name}</strong> }} | |||
/> | |||
} | |||
modalHeader={translate('users.tokens.revoke_token')} | |||
onConfirm={this.handleRevoke}> | |||
{({ onClick }) => ( | |||
<Button | |||
className="spacer-left button-red input-small" | |||
disabled={loading} | |||
onClick={onClick} | |||
title={translate('users.tokens.revoke_token')}> | |||
{translate('users.tokens.revoke')} | |||
</Button> | |||
)} | |||
</ConfirmButton> | |||
) : ( | |||
<Button | |||
className="button-red input-small spacer-left" | |||
disabled={loading} | |||
onClick={this.handleClick}> | |||
{showConfirmation ? translate('users.tokens.sure') : translate('users.tokens.revoke')} | |||
</Button> | |||
)} | |||
</td> | |||
</tr> | |||
); |
@@ -43,7 +43,11 @@ export default function TokensFormModal(props: Props) { | |||
</h2> | |||
</header> | |||
<div className="modal-body modal-container"> | |||
<TokensForm login={props.user.login} updateTokensCount={props.updateTokensCount} /> | |||
<TokensForm | |||
deleteConfirmation="inline" | |||
login={props.user.login} | |||
updateTokensCount={props.updateTokensCount} | |||
/> | |||
</div> | |||
<footer className="modal-foot"> | |||
<ResetButtonLink onClick={props.onClose}>{translate('Done')}</ResetButtonLink> |
@@ -79,5 +79,7 @@ it('should revoke tokens', async () => { | |||
}); | |||
function shallowRender(props: Partial<TokensForm['props']> = {}) { | |||
return shallow<TokensForm>(<TokensForm login="luke" updateTokensCount={jest.fn()} {...props} />); | |||
return shallow<TokensForm>( | |||
<TokensForm deleteConfirmation="inline" login="luke" updateTokensCount={jest.fn()} {...props} /> | |||
); | |||
} |
@@ -43,11 +43,12 @@ beforeEach(() => { | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ deleteConfirmation: 'modal' })).toMatchSnapshot(); | |||
}); | |||
it('should revoke the token', async () => { | |||
it('should revoke the token using inline confirmation', async () => { | |||
const onRevokeToken = jest.fn(); | |||
const wrapper = shallowRender({ onRevokeToken }); | |||
const wrapper = shallowRender({ deleteConfirmation: 'inline', onRevokeToken }); | |||
expect(wrapper.find('Button')).toMatchSnapshot(); | |||
click(wrapper.find('Button')); | |||
expect(wrapper.find('Button')).toMatchSnapshot(); | |||
@@ -58,8 +59,23 @@ it('should revoke the token', async () => { | |||
expect(onRevokeToken).toHaveBeenCalledWith(userToken); | |||
}); | |||
it('should revoke the token using modal confirmation', async () => { | |||
const onRevokeToken = jest.fn(); | |||
const wrapper = shallowRender({ deleteConfirmation: 'modal', onRevokeToken }); | |||
wrapper.find('ConfirmButton').prop<Function>('onConfirm')(); | |||
expect(revokeToken).toHaveBeenCalledWith({ login: 'luke', name: 'foo' }); | |||
await waitAndUpdate(wrapper); | |||
expect(onRevokeToken).toHaveBeenCalledWith(userToken); | |||
}); | |||
function shallowRender(props: Partial<TokensFormItem['props']> = {}) { | |||
return shallow( | |||
<TokensFormItem login="luke" onRevokeToken={jest.fn()} token={userToken} {...props} /> | |||
<TokensFormItem | |||
deleteConfirmation="inline" | |||
login="luke" | |||
onRevokeToken={jest.fn()} | |||
token={userToken} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -30,7 +30,7 @@ exports[`should render correctly 1`] = ` | |||
</SubmitButton> | |||
</form> | |||
<table | |||
className="data zebra big-spacer-top " | |||
className="data zebra big-spacer-top" | |||
> | |||
<thead> | |||
<tr> | |||
@@ -106,7 +106,7 @@ exports[`should render correctly 2`] = ` | |||
</SubmitButton> | |||
</form> | |||
<table | |||
className="data zebra big-spacer-top " | |||
className="data zebra big-spacer-top" | |||
> | |||
<thead> | |||
<tr> | |||
@@ -139,6 +139,7 @@ exports[`should render correctly 2`] = ` | |||
timeout={100} | |||
> | |||
<TokensFormItem | |||
deleteConfirmation="inline" | |||
key="foo" | |||
login="luke" | |||
onRevokeToken={[Function]} | |||
@@ -151,6 +152,7 @@ exports[`should render correctly 2`] = ` | |||
} | |||
/> | |||
<TokensFormItem | |||
deleteConfirmation="inline" | |||
key="bar" | |||
login="luke" | |||
onRevokeToken={[Function]} |
@@ -48,7 +48,69 @@ exports[`should render correctly 1`] = ` | |||
</tr> | |||
`; | |||
exports[`should revoke the token 1`] = ` | |||
exports[`should render correctly 2`] = ` | |||
<tr> | |||
<td> | |||
<Tooltip | |||
overlay="foo" | |||
> | |||
<span> | |||
foo | |||
</span> | |||
</Tooltip> | |||
</td> | |||
<td | |||
className="nowrap" | |||
> | |||
<DateFromNowHourPrecision | |||
date="2019-01-18T15:06:33+0100" | |||
/> | |||
</td> | |||
<td | |||
className="thin nowrap text-right" | |||
> | |||
<DateFormatter | |||
date="2019-01-15T15:06:33+0100" | |||
long={true} | |||
/> | |||
</td> | |||
<td | |||
className="thin nowrap text-right" | |||
> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
> | |||
<i | |||
className="spinner-placeholder" | |||
/> | |||
</DeferredSpinner> | |||
<ConfirmButton | |||
confirmButtonText="users.tokens.revoke_token" | |||
isDestructive={true} | |||
modalBody={ | |||
<FormattedMessage | |||
defaultMessage="users.tokens.sure_X" | |||
id="users.tokens.sure_X" | |||
values={ | |||
Object { | |||
"token": <strong> | |||
foo | |||
</strong>, | |||
} | |||
} | |||
/> | |||
} | |||
modalHeader="users.tokens.revoke_token" | |||
onConfirm={[Function]} | |||
> | |||
<Component /> | |||
</ConfirmButton> | |||
</td> | |||
</tr> | |||
`; | |||
exports[`should revoke the token using inline confirmation 1`] = ` | |||
<Button | |||
className="button-red input-small spacer-left" | |||
disabled={false} | |||
@@ -58,7 +120,7 @@ exports[`should revoke the token 1`] = ` | |||
</Button> | |||
`; | |||
exports[`should revoke the token 2`] = ` | |||
exports[`should revoke the token using inline confirmation 2`] = ` | |||
<Button | |||
className="button-red input-small spacer-left" | |||
disabled={false} |
@@ -26,6 +26,7 @@ exports[`should render correctly 1`] = ` | |||
className="modal-body modal-container" | |||
> | |||
<TokensForm | |||
deleteConfirmation="inline" | |||
login="john.doe" | |||
updateTokensCount={[MockFunction]} | |||
/> |
@@ -3098,9 +3098,11 @@ users.remove=Remove user | |||
users.search_description=Search users by login or name | |||
users.update=Update users | |||
users.tokens=Tokens | |||
users.user_X_tokens=Tokens of {user} | |||
users.tokens.sure=Sure? | |||
users.tokens.sure_X=Are you sure you want to revoke token {token}? | |||
users.tokens.revoke=Revoke | |||
users.user_X_tokens=Tokens of {user} | |||
users.tokens.revoke_token=Revoke token | |||
users.no_tokens=No tokens | |||
users.generate=Generate | |||
users.generate_tokens=Generate Tokens |