@@ -22,16 +22,16 @@ import { uniq } from 'lodash'; | |||
import UserScmAccountInput from './UserScmAccountInput'; | |||
import { createUser, updateUser } from '../../../api/users'; | |||
import throwGlobalError from '../../../app/utils/throwGlobalError'; | |||
import Modal from '../../../components/controls/Modal'; | |||
import SimpleModal from '../../../components/controls/SimpleModal'; | |||
import { Button, ResetButtonLink, SubmitButton } from '../../../components/ui/buttons'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { parseError } from '../../../helpers/request'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
export interface Props { | |||
user?: T.User; | |||
onClose: () => void; | |||
onUpdateUsers: () => void; | |||
user?: T.User; | |||
} | |||
interface State { | |||
@@ -102,8 +102,7 @@ export default class UserForm extends React.PureComponent<Props, State> { | |||
handlePasswordChange = (event: React.SyntheticEvent<HTMLInputElement>) => | |||
this.setState({ password: event.currentTarget.value }); | |||
handleCreateUser = (event: React.SyntheticEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
handleCreateUser = () => { | |||
this.setState({ submitting: true }); | |||
createUser({ | |||
email: this.state.email || undefined, | |||
@@ -117,8 +116,7 @@ export default class UserForm extends React.PureComponent<Props, State> { | |||
}, this.handleError); | |||
}; | |||
handleUpdateUser = (event: React.SyntheticEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
handleUpdateUser = () => { | |||
this.setState({ submitting: true }); | |||
updateUser({ | |||
email: this.state.email, | |||
@@ -149,123 +147,132 @@ export default class UserForm extends React.PureComponent<Props, State> { | |||
render() { | |||
const { user } = this.props; | |||
const { error, submitting } = this.state; | |||
const { error } = this.state; | |||
const header = user ? translate('users.update_user') : translate('users.create_user'); | |||
return ( | |||
<Modal contentLabel={header} onRequestClose={this.props.onClose} size="small"> | |||
<form | |||
autoComplete="off" | |||
id="user-form" | |||
onSubmit={this.props.user ? this.handleUpdateUser : this.handleCreateUser}> | |||
<header className="modal-head"> | |||
<h2>{header}</h2> | |||
</header> | |||
<SimpleModal | |||
header={header} | |||
onClose={this.props.onClose} | |||
onSubmit={user ? this.handleUpdateUser : this.handleCreateUser} | |||
size="small"> | |||
{({ onCloseClick, onFormSubmit, submitting }) => ( | |||
<form autoComplete="off" id="user-form" onSubmit={onFormSubmit}> | |||
<header className="modal-head"> | |||
<h2>{header}</h2> | |||
</header> | |||
<div className="modal-body"> | |||
{error && <Alert variant="error">{error}</Alert>} | |||
<div className="modal-body"> | |||
{error && <Alert variant="error">{error}</Alert>} | |||
{!error && user && !user.local && ( | |||
<Alert variant="warning">{translate('users.cannot_update_delegated_user')}</Alert> | |||
)} | |||
{!user && ( | |||
{!user && ( | |||
<div className="modal-field"> | |||
<label htmlFor="create-user-login"> | |||
{translate('login')} | |||
<em className="mandatory">*</em> | |||
</label> | |||
{/* keep this fake field to hack browser autofill */} | |||
<input className="hidden" name="login-fake" type="text" /> | |||
<input | |||
autoFocus={true} | |||
id="create-user-login" | |||
maxLength={255} | |||
minLength={3} | |||
name="login" | |||
onChange={this.handleLoginChange} | |||
required={true} | |||
type="text" | |||
value={this.state.login} | |||
/> | |||
<p className="note">{translateWithParameters('users.minimum_x_characters', 3)}</p> | |||
</div> | |||
)} | |||
<div className="modal-field"> | |||
<label htmlFor="create-user-login"> | |||
{translate('login')} | |||
<label htmlFor="create-user-name"> | |||
{translate('name')} | |||
<em className="mandatory">*</em> | |||
</label> | |||
{/* keep this fake field to hack browser autofill */} | |||
<input className="hidden" name="login-fake" type="text" /> | |||
<input className="hidden" name="name-fake" type="text" /> | |||
<input | |||
autoFocus={true} | |||
id="create-user-login" | |||
maxLength={255} | |||
minLength={3} | |||
name="login" | |||
onChange={this.handleLoginChange} | |||
autoFocus={!!user} | |||
disabled={user && !user.local} | |||
id="create-user-name" | |||
maxLength={200} | |||
name="name" | |||
onChange={this.handleNameChange} | |||
required={true} | |||
type="text" | |||
value={this.state.login} | |||
value={this.state.name} | |||
/> | |||
<p className="note">{translateWithParameters('users.minimum_x_characters', 3)}</p> | |||
</div> | |||
)} | |||
<div className="modal-field"> | |||
<label htmlFor="create-user-name"> | |||
{translate('name')} | |||
<em className="mandatory">*</em> | |||
</label> | |||
{/* keep this fake field to hack browser autofill */} | |||
<input className="hidden" name="name-fake" type="text" /> | |||
<input | |||
autoFocus={!!user} | |||
id="create-user-name" | |||
maxLength={200} | |||
name="name" | |||
onChange={this.handleNameChange} | |||
required={true} | |||
type="text" | |||
value={this.state.name} | |||
/> | |||
</div> | |||
<div className="modal-field"> | |||
<label htmlFor="create-user-email">{translate('users.email')}</label> | |||
{/* keep this fake field to hack browser autofill */} | |||
<input className="hidden" name="email-fake" type="email" /> | |||
<input | |||
id="create-user-email" | |||
maxLength={100} | |||
name="email" | |||
onChange={this.handleEmailChange} | |||
type="email" | |||
value={this.state.email} | |||
/> | |||
</div> | |||
{!user && ( | |||
<div className="modal-field"> | |||
<label htmlFor="create-user-password"> | |||
{translate('password')} | |||
<em className="mandatory">*</em> | |||
</label> | |||
<label htmlFor="create-user-email">{translate('users.email')}</label> | |||
{/* keep this fake field to hack browser autofill */} | |||
<input className="hidden" name="password-fake" type="password" /> | |||
<input className="hidden" name="email-fake" type="email" /> | |||
<input | |||
id="create-user-password" | |||
maxLength={50} | |||
name="password" | |||
onChange={this.handlePasswordChange} | |||
required={true} | |||
type="password" | |||
value={this.state.password} | |||
disabled={user && !user.local} | |||
id="create-user-email" | |||
maxLength={100} | |||
name="email" | |||
onChange={this.handleEmailChange} | |||
type="email" | |||
value={this.state.email} | |||
/> | |||
</div> | |||
)} | |||
<div className="modal-field"> | |||
<label>{translate('my_profile.scm_accounts')}</label> | |||
{this.state.scmAccounts.map((scm, idx) => ( | |||
<UserScmAccountInput | |||
idx={idx} | |||
key={idx} | |||
onChange={this.handleUpdateScmAccount} | |||
onRemove={this.handleRemoveScmAccount} | |||
scmAccount={scm} | |||
/> | |||
))} | |||
<div className="spacer-bottom"> | |||
<Button className="js-scm-account-add" onClick={this.handleAddScmAccount}> | |||
{translate('add_verb')} | |||
</Button> | |||
{!user && ( | |||
<div className="modal-field"> | |||
<label htmlFor="create-user-password"> | |||
{translate('password')} | |||
<em className="mandatory">*</em> | |||
</label> | |||
{/* keep this fake field to hack browser autofill */} | |||
<input className="hidden" name="password-fake" type="password" /> | |||
<input | |||
id="create-user-password" | |||
maxLength={50} | |||
name="password" | |||
onChange={this.handlePasswordChange} | |||
required={true} | |||
type="password" | |||
value={this.state.password} | |||
/> | |||
</div> | |||
)} | |||
<div className="modal-field"> | |||
<label>{translate('my_profile.scm_accounts')}</label> | |||
{this.state.scmAccounts.map((scm, idx) => ( | |||
<UserScmAccountInput | |||
idx={idx} | |||
key={idx} | |||
onChange={this.handleUpdateScmAccount} | |||
onRemove={this.handleRemoveScmAccount} | |||
scmAccount={scm} | |||
/> | |||
))} | |||
<div className="spacer-bottom"> | |||
<Button className="js-scm-account-add" onClick={this.handleAddScmAccount}> | |||
{translate('add_verb')} | |||
</Button> | |||
</div> | |||
<p className="note">{translate('user.login_or_email_used_as_scm_account')}</p> | |||
</div> | |||
<p className="note">{translate('user.login_or_email_used_as_scm_account')}</p> | |||
</div> | |||
</div> | |||
<footer className="modal-foot"> | |||
{submitting && <i className="spinner spacer-right" />} | |||
<SubmitButton disabled={submitting}> | |||
{user ? translate('update_verb') : translate('create')} | |||
</SubmitButton> | |||
<ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink> | |||
</footer> | |||
</form> | |||
</Modal> | |||
<footer className="modal-foot"> | |||
{submitting && <i className="spinner spacer-right" />} | |||
<SubmitButton disabled={submitting}> | |||
{user ? translate('update_verb') : translate('create')} | |||
</SubmitButton> | |||
<ResetButtonLink onClick={onCloseClick}>{translate('cancel')}</ResetButtonLink> | |||
</footer> | |||
</form> | |||
)} | |||
</SimpleModal> | |||
); | |||
} | |||
} |
@@ -0,0 +1,110 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 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 UserForm from '../UserForm'; | |||
import { mockUser } from '../../../../helpers/testMocks'; | |||
import { createUser, updateUser } from '../../../../api/users'; | |||
import { waitAndUpdate, submit } from '../../../../helpers/testUtils'; | |||
jest.mock('../../../../api/users', () => ({ | |||
createUser: jest.fn().mockResolvedValue({}), | |||
updateUser: jest.fn().mockResolvedValue({}) | |||
})); | |||
it('should render correctly', () => { | |||
expect(shallowRender().dive()).toMatchSnapshot(); | |||
expect(shallowRender({ user: undefined }).dive()).toMatchSnapshot(); | |||
}); | |||
it('should correctly show errors', async () => { | |||
(updateUser as jest.Mock).mockRejectedValue({ | |||
response: { | |||
status: 400 | |||
} | |||
}); | |||
const wrapper = shallowRender(); | |||
submit(wrapper.dive().find('form')); | |||
await waitAndUpdate(wrapper); | |||
expect( | |||
wrapper | |||
.dive() | |||
.find('Alert') | |||
.dive() | |||
.text() | |||
).toMatch('default_error_message'); | |||
}); | |||
it('should correctly disable name and email fields for non-local users', () => { | |||
const wrapper = shallowRender({ user: mockUser({ local: false }) }).dive(); | |||
expect(wrapper.find('#create-user-name').prop('disabled')).toBe(true); | |||
expect(wrapper.find('#create-user-email').prop('disabled')).toBe(true); | |||
expect(wrapper.find('Alert').exists()).toBe(true); | |||
expect( | |||
wrapper | |||
.find('Alert') | |||
.dive() | |||
.text() | |||
).toMatch('users.cannot_update_delegated_user'); | |||
}); | |||
it('should correctly create a new user', () => { | |||
const email = 'foo@bar.ch'; | |||
const login = 'foo'; | |||
const name = 'Foo'; | |||
const password = 'bar'; | |||
const scmAccounts = ['gh', 'gh', 'bitbucket']; | |||
const wrapper = shallowRender({ user: undefined }); | |||
wrapper.setState({ email, login, name, password, scmAccounts }); | |||
submit(wrapper.dive().find('form')); | |||
expect(createUser).toBeCalledWith({ | |||
email, | |||
login, | |||
name, | |||
password, | |||
scmAccount: ['gh', 'bitbucket'] | |||
}); | |||
}); | |||
it('should correctly update a user', () => { | |||
const email = 'foo@bar.ch'; | |||
const login = 'foo'; | |||
const name = 'Foo'; | |||
const scmAccounts = ['gh', 'gh', 'bitbucket']; | |||
const wrapper = shallowRender({ user: mockUser({ email, login, name, scmAccounts }) }).dive(); | |||
submit(wrapper.find('form')); | |||
expect(updateUser).toBeCalledWith({ | |||
email, | |||
login, | |||
name, | |||
scmAccount: ['gh', 'bitbucket'] | |||
}); | |||
}); | |||
function shallowRender(props: Partial<UserForm['props']> = {}) { | |||
return shallow( | |||
<UserForm onClose={jest.fn()} onUpdateUsers={jest.fn()} user={mockUser()} {...props} /> | |||
); | |||
} |
@@ -0,0 +1,292 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<Modal | |||
contentLabel="users.update_user" | |||
onRequestClose={[MockFunction]} | |||
size="small" | |||
> | |||
<form | |||
autoComplete="off" | |||
id="user-form" | |||
onSubmit={[Function]} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
users.update_user | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-user-name" | |||
> | |||
name | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
className="hidden" | |||
name="name-fake" | |||
type="text" | |||
/> | |||
<input | |||
autoFocus={true} | |||
disabled={false} | |||
id="create-user-name" | |||
maxLength={200} | |||
name="name" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="John Doe" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-user-email" | |||
> | |||
users.email | |||
</label> | |||
<input | |||
className="hidden" | |||
name="email-fake" | |||
type="email" | |||
/> | |||
<input | |||
disabled={false} | |||
id="create-user-email" | |||
maxLength={100} | |||
name="email" | |||
onChange={[Function]} | |||
type="email" | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label> | |||
my_profile.scm_accounts | |||
</label> | |||
<div | |||
className="spacer-bottom" | |||
> | |||
<Button | |||
className="js-scm-account-add" | |||
onClick={[Function]} | |||
> | |||
add_verb | |||
</Button> | |||
</div> | |||
<p | |||
className="note" | |||
> | |||
user.login_or_email_used_as_scm_account | |||
</p> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
update_verb | |||
</SubmitButton> | |||
<ResetButtonLink | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</ResetButtonLink> | |||
</footer> | |||
</form> | |||
</Modal> | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<Modal | |||
contentLabel="users.create_user" | |||
onRequestClose={[MockFunction]} | |||
size="small" | |||
> | |||
<form | |||
autoComplete="off" | |||
id="user-form" | |||
onSubmit={[Function]} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
users.create_user | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-user-login" | |||
> | |||
login | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
className="hidden" | |||
name="login-fake" | |||
type="text" | |||
/> | |||
<input | |||
autoFocus={true} | |||
id="create-user-login" | |||
maxLength={255} | |||
minLength={3} | |||
name="login" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="" | |||
/> | |||
<p | |||
className="note" | |||
> | |||
users.minimum_x_characters.3 | |||
</p> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-user-name" | |||
> | |||
name | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
className="hidden" | |||
name="name-fake" | |||
type="text" | |||
/> | |||
<input | |||
autoFocus={false} | |||
id="create-user-name" | |||
maxLength={200} | |||
name="name" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-user-email" | |||
> | |||
users.email | |||
</label> | |||
<input | |||
className="hidden" | |||
name="email-fake" | |||
type="email" | |||
/> | |||
<input | |||
id="create-user-email" | |||
maxLength={100} | |||
name="email" | |||
onChange={[Function]} | |||
type="email" | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-user-password" | |||
> | |||
password | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
className="hidden" | |||
name="password-fake" | |||
type="password" | |||
/> | |||
<input | |||
id="create-user-password" | |||
maxLength={50} | |||
name="password" | |||
onChange={[Function]} | |||
required={true} | |||
type="password" | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label> | |||
my_profile.scm_accounts | |||
</label> | |||
<div | |||
className="spacer-bottom" | |||
> | |||
<Button | |||
className="js-scm-account-add" | |||
onClick={[Function]} | |||
> | |||
add_verb | |||
</Button> | |||
</div> | |||
<p | |||
className="note" | |||
> | |||
user.login_or_email_used_as_scm_account | |||
</p> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
create | |||
</SubmitButton> | |||
<ResetButtonLink | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</ResetButtonLink> | |||
</footer> | |||
</form> | |||
</Modal> | |||
`; |
@@ -3175,6 +3175,7 @@ users.deactivate_user=Deactivate User | |||
users.deactivate_user.confirmation=Are you sure you want to deactivate "{0} ({1})"? | |||
users.create_user=Create User | |||
users.update_user=Update User | |||
users.cannot_update_delegated_user=You cannot update the name and email of this user, as it is controlled by an external identity provider. | |||
users.minimum_x_characters=Minimum {0} characters | |||
users.email=Email | |||
users.last_connection=Last connection |