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 {
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,
}, this.handleError);
};
- handleUpdateUser = (event: React.SyntheticEvent<HTMLFormElement>) => {
- event.preventDefault();
+ handleUpdateUser = () => {
this.setState({ submitting: true });
updateUser({
email: this.state.email,
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>
);
}
}
--- /dev/null
+/*
+ * 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} />
+ );
+}
--- /dev/null
+// 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>
+`;