From 5fdfa1d19245ba022c1c28b9ee8b47a5b4b01dc5 Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Tue, 18 Jun 2019 10:03:00 +0200 Subject: [PATCH] SONAR-11985 Use a confirmation modal for token revoking --- .../js/apps/account/components/Tokens.tsx | 4 +- .../__snapshots__/Tokens-test.tsx.snap | 1 + .../js/apps/users/components/TokensForm.tsx | 6 +- .../apps/users/components/TokensFormItem.tsx | 84 +++++++++++++------ .../apps/users/components/TokensFormModal.tsx | 6 +- .../components/__tests__/TokensForm-test.tsx | 4 +- .../__tests__/TokensFormItem-test.tsx | 22 ++++- .../__snapshots__/TokensForm-test.tsx.snap | 6 +- .../TokensFormItem-test.tsx.snap | 66 ++++++++++++++- .../TokensFormModal-test.tsx.snap | 1 + .../resources/org/sonar/l10n/core.properties | 4 +- 11 files changed, 166 insertions(+), 38 deletions(-) diff --git a/server/sonar-web/src/main/js/apps/account/components/Tokens.tsx b/server/sonar-web/src/main/js/apps/account/components/Tokens.tsx index 4e385bd815b..aeef02fbf00 100644 --- a/server/sonar-web/src/main/js/apps/account/components/Tokens.tsx +++ b/server/sonar-web/src/main/js/apps/account/components/Tokens.tsx @@ -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) { - + ); diff --git a/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Tokens-test.tsx.snap b/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Tokens-test.tsx.snap index e7bd83f8209..67efe5168da 100644 --- a/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Tokens-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Tokens-test.tsx.snap @@ -18,6 +18,7 @@ exports[`renders 1`] = ` /> diff --git a/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx b/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx index ef7b4103bf1..cee5f77432b 100644 --- a/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx @@ -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 { } return tokens.map(token => ( { {newToken && } - +
diff --git a/server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx b/server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx index 09673e108dc..4c42e26003d 100644 --- a/server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx @@ -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 { 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 { 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 ( ); diff --git a/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx b/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx index 567a143b99c..89e6652db2b 100644 --- a/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx @@ -43,7 +43,11 @@ export default function TokensFormModal(props: Props) {
- +
{translate('Done')} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensForm-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensForm-test.tsx index 71d9002127c..19ebe0feea0 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensForm-test.tsx @@ -79,5 +79,7 @@ it('should revoke tokens', async () => { }); function shallowRender(props: Partial = {}) { - return shallow(); + return shallow( + + ); } diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx index 1a65f7c4756..71614fc6ff7 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx @@ -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('onConfirm')(); + expect(revokeToken).toHaveBeenCalledWith({ login: 'luke', name: 'foo' }); + await waitAndUpdate(wrapper); + expect(onRevokeToken).toHaveBeenCalledWith(userToken); +}); + function shallowRender(props: Partial = {}) { return shallow( - + ); } diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensForm-test.tsx.snap index 401bae31b0a..f45ecb861b4 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensForm-test.tsx.snap @@ -30,7 +30,7 @@ exports[`should render correctly 1`] = `
{translate('name')}
@@ -86,14 +99,37 @@ export default class TokensFormItem extends React.PureComponent { - + {deleteConfirmation === 'modal' ? ( + {token.name} }} + /> + } + modalHeader={translate('users.tokens.revoke_token')} + onConfirm={this.handleRevoke}> + {({ onClick }) => ( + + )} + + ) : ( + + )}
@@ -106,7 +106,7 @@ exports[`should render correctly 2`] = `
@@ -139,6 +139,7 @@ exports[`should render correctly 2`] = ` timeout={100} > `; -exports[`should revoke the token 1`] = ` +exports[`should render correctly 2`] = ` + + + + + + +`; + +exports[`should revoke the token using inline confirmation 1`] = `
+ + + foo + + + + + + + + + + + + foo + , + } + } + /> + } + modalHeader="users.tokens.revoke_token" + onConfirm={[Function]} + > + + +