mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #8259 from overleaf/ds-admin-panel-register-new-user
Migrating admin panel register new user to react GitOrigin-RevId: 520fea91cd9e560e4381504de45e5bedf11a7844
This commit is contained in:
parent
eba2fe9a3e
commit
5498d59c6b
8 changed files with 395 additions and 86 deletions
|
@ -23,7 +23,6 @@ import './main/translations'
|
|||
import './main/subscription-dashboard'
|
||||
import './main/new-subscription'
|
||||
import './main/annual-upgrade'
|
||||
import './main/register-users'
|
||||
import './main/subscription/team-invite-controller'
|
||||
import './main/subscription/upgrade-subscription'
|
||||
import './main/learn'
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
/* eslint-disable
|
||||
max-len,
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import App from '../base'
|
||||
|
||||
export default App.controller(
|
||||
'RegisterUsersController',
|
||||
function ($scope, queuedHttp) {
|
||||
$scope.users = []
|
||||
|
||||
$scope.inputs = { emails: '' }
|
||||
|
||||
const parseEmails = function (emailsString) {
|
||||
const regexBySpaceOrComma = /[\s,]+/
|
||||
let emails = emailsString.split(regexBySpaceOrComma)
|
||||
emails = _.map(emails, email => (email = email.trim()))
|
||||
emails = _.filter(emails, email => email.indexOf('@') !== -1)
|
||||
return emails
|
||||
}
|
||||
|
||||
return ($scope.registerUsers = function () {
|
||||
const emails = parseEmails($scope.inputs.emails)
|
||||
$scope.error = false
|
||||
return Array.from(emails).map(email =>
|
||||
queuedHttp
|
||||
.post('/admin/register', {
|
||||
email,
|
||||
_csrf: window.csrfToken,
|
||||
})
|
||||
.then(function (response) {
|
||||
const { data } = response
|
||||
const user = data
|
||||
$scope.users.push(user)
|
||||
return ($scope.inputs.emails = '')
|
||||
})
|
||||
.catch(() => ($scope.error = true))
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
|
@ -1,40 +1,11 @@
|
|||
extends ../../../../../app/views/layout
|
||||
|
||||
block entrypointVar
|
||||
- entrypoint = 'modules/user-activate/pages/user-activate-page'
|
||||
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
.container
|
||||
.row
|
||||
.col-md-12
|
||||
.card(ng-controller="RegisterUsersController")
|
||||
.page-header
|
||||
h1 Register New Users
|
||||
form.form
|
||||
.row
|
||||
.col-md-4.col-xs-8
|
||||
input.form-control(
|
||||
name="email",
|
||||
type="text",
|
||||
placeholder="jane@example.com, joe@example.com",
|
||||
ng-model="inputs.emails",
|
||||
on-enter="registerUsers()"
|
||||
)
|
||||
.col-md-8.col-xs-4
|
||||
button.btn.btn-primary(ng-click="registerUsers()") #{translate("register")}
|
||||
|
||||
.row-spaced(ng-show="error").ng-cloak.text-danger
|
||||
p Sorry, an error occured
|
||||
|
||||
.row-spaced(ng-show="users.length > 0").ng-cloak.text-success
|
||||
p We've sent out welcome emails to the registered users.
|
||||
p You can also manually send them URLs below to allow them to reset their password and log in for the first time.
|
||||
p (Password reset tokens will expire after one week and the user will need registering again).
|
||||
|
||||
hr(ng-show="users.length > 0").ng-cloak
|
||||
table(ng-show="users.length > 0").table.table-striped.ng-cloak
|
||||
tr
|
||||
th #{translate("email")}
|
||||
th Set Password Url
|
||||
tr(ng-repeat="user in users")
|
||||
td {{ user.email }}
|
||||
td(style="word-break: break-all;") {{ user.setNewPasswordUrl }}
|
||||
|
||||
#user-activate-register-container
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import { postJSON } from '../../../../../frontend/js/infrastructure/fetch-json'
|
||||
|
||||
function RegisterForm({
|
||||
setRegistrationSuccess,
|
||||
setEmails,
|
||||
setRegisterError,
|
||||
setFailedEmails,
|
||||
}) {
|
||||
function handleRegister(event) {
|
||||
event.preventDefault()
|
||||
const formData = new FormData(event.target)
|
||||
const formDataAsEntries = formData.entries()
|
||||
const formDataAsObject = Object.fromEntries(formDataAsEntries)
|
||||
const emailString = formDataAsObject.email
|
||||
setRegistrationSuccess(false)
|
||||
setRegisterError(false)
|
||||
setEmails([])
|
||||
registerGivenUsers(parseEmails(emailString))
|
||||
}
|
||||
|
||||
async function registerGivenUsers(emails) {
|
||||
const registeredEmails = []
|
||||
const failingEmails = []
|
||||
for (const email of emails) {
|
||||
try {
|
||||
const result = await registerUser(email)
|
||||
registeredEmails.push(result)
|
||||
} catch {
|
||||
failingEmails.push(email)
|
||||
}
|
||||
}
|
||||
if (registeredEmails.length > 0) setRegistrationSuccess(true)
|
||||
if (failingEmails.length > 0) {
|
||||
setRegisterError(true)
|
||||
setFailedEmails(failingEmails)
|
||||
}
|
||||
setEmails(registeredEmails)
|
||||
}
|
||||
|
||||
function registerUser(email) {
|
||||
const options = { email }
|
||||
const url = `/admin/register`
|
||||
return postJSON(url, { body: options })
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleRegister}>
|
||||
<div className="row">
|
||||
<div className="col-md-4 col-xs-8">
|
||||
<input
|
||||
className="form-control"
|
||||
name="email"
|
||||
type="text"
|
||||
placeholder="jane@example.com, joe@example.com"
|
||||
aria-label="emails to register"
|
||||
aria-describedby="input-details"
|
||||
/>
|
||||
<p id="input-details" className="sr-only">
|
||||
Enter the emails you would like to register and separate them using
|
||||
commas
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-md-8 col-xs-4">
|
||||
<button className="btn btn-primary">Register</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
function parseEmails(emailsText) {
|
||||
const regexBySpaceOrComma = /[\s,]+/
|
||||
let emails = emailsText.split(regexBySpaceOrComma)
|
||||
emails.map(email => email.trim())
|
||||
emails = emails.filter(email => email.indexOf('@') !== -1)
|
||||
return emails
|
||||
}
|
||||
|
||||
RegisterForm.propTypes = {
|
||||
setRegistrationSuccess: PropTypes.func,
|
||||
setEmails: PropTypes.func,
|
||||
setRegisterError: PropTypes.func,
|
||||
setFailedEmails: PropTypes.func,
|
||||
}
|
||||
|
||||
export default RegisterForm
|
|
@ -0,0 +1,92 @@
|
|||
import { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import RegisterForm from './register-form'
|
||||
function UserActivateRegister() {
|
||||
const [emails, setEmails] = useState([])
|
||||
const [failedEmails, setFailedEmails] = useState([])
|
||||
const [registerError, setRegisterError] = useState(false)
|
||||
const [registrationSuccess, setRegistrationSuccess] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<div className="card">
|
||||
<div className="page-header">
|
||||
<h1> Register New Users</h1>
|
||||
</div>
|
||||
<RegisterForm
|
||||
setRegistrationSuccess={setRegistrationSuccess}
|
||||
setEmails={setEmails}
|
||||
setRegisterError={setRegisterError}
|
||||
setFailedEmails={setFailedEmails}
|
||||
/>
|
||||
{registerError ? (
|
||||
<UserActivateError failedEmails={failedEmails} />
|
||||
) : null}
|
||||
{registrationSuccess ? (
|
||||
<>
|
||||
<SuccessfulRegistrationMessage />
|
||||
<hr />
|
||||
<DisplayEmailsList emails={emails} />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function UserActivateError({ failedEmails }) {
|
||||
return (
|
||||
<div className="row-spaced text-danger">
|
||||
<p>Sorry, an error occured, failed to register these emails.</p>
|
||||
{failedEmails.map(email => (
|
||||
<p key={email}>{email}</p>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SuccessfulRegistrationMessage() {
|
||||
return (
|
||||
<div className="row-spaced text-success">
|
||||
<p>We've sent out welcome emails to the registered users.</p>
|
||||
<p>
|
||||
You can also manually send them URLs below to allow them to reset their
|
||||
password and log in for the first time.
|
||||
</p>
|
||||
<p>
|
||||
(Password reset tokens will expire after one week and the user will need
|
||||
registering again).
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DisplayEmailsList({ emails }) {
|
||||
return (
|
||||
<table className="table table-striped ">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Set Password Url</th>
|
||||
</tr>
|
||||
{emails.map(user => (
|
||||
<tr key={user.email}>
|
||||
<td>{user.email}</td>
|
||||
<td style={{ wordBreak: 'break-all' }}>{user.setNewPasswordUrl}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
DisplayEmailsList.propTypes = {
|
||||
emails: PropTypes.array,
|
||||
}
|
||||
UserActivateError.propTypes = {
|
||||
failedEmails: PropTypes.array,
|
||||
}
|
||||
|
||||
export default UserActivateRegister
|
|
@ -0,0 +1,9 @@
|
|||
import '../../../../../frontend/js/marketing'
|
||||
|
||||
import ReactDOM from 'react-dom'
|
||||
import UserActivateRegister from '../components/user-activate-register'
|
||||
|
||||
ReactDOM.render(
|
||||
<UserActivateRegister />,
|
||||
document.getElementById('user-activate-register-container')
|
||||
)
|
|
@ -0,0 +1,62 @@
|
|||
import { expect } from 'chai'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import sinon from 'sinon'
|
||||
import RegisterForm from '../../../../frontend/js/components/register-form'
|
||||
|
||||
describe('RegisterForm', function () {
|
||||
beforeEach(function () {
|
||||
fetchMock.reset()
|
||||
})
|
||||
afterEach(function () {
|
||||
fetchMock.reset()
|
||||
})
|
||||
it('should render the register form', async function () {
|
||||
const setRegistrationSuccessStub = sinon.stub()
|
||||
const setEmailsStub = sinon.stub()
|
||||
const setRegisterErrorStub = sinon.stub()
|
||||
const setFailedEmailsStub = sinon.stub()
|
||||
|
||||
render(
|
||||
<RegisterForm
|
||||
setRegistrationSuccess={setRegistrationSuccessStub}
|
||||
setEmails={setEmailsStub}
|
||||
setRegisterError={setRegisterErrorStub}
|
||||
setFailedEmails={setFailedEmailsStub}
|
||||
/>
|
||||
)
|
||||
await screen.findByLabelText('emails to register')
|
||||
screen.getByRole('button', { name: /register/i })
|
||||
})
|
||||
|
||||
it('should call the fetch request when register button is pressed', async function () {
|
||||
const email = 'abc@gmail.com'
|
||||
const setRegistrationSuccessStub = sinon.stub()
|
||||
const setEmailsStub = sinon.stub()
|
||||
const setRegisterErrorStub = sinon.stub()
|
||||
const setFailedEmailsStub = sinon.stub()
|
||||
|
||||
const endPointResponse = {
|
||||
status: 200,
|
||||
body: {
|
||||
email,
|
||||
setNewPasswordUrl: 'SetNewPasswordURL',
|
||||
},
|
||||
}
|
||||
const registerMock = fetchMock.post('/admin/register', endPointResponse)
|
||||
|
||||
render(
|
||||
<RegisterForm
|
||||
setRegistrationSuccess={setRegistrationSuccessStub}
|
||||
setEmails={setEmailsStub}
|
||||
setRegisterError={setRegisterErrorStub}
|
||||
setFailedEmails={setFailedEmailsStub}
|
||||
/>
|
||||
)
|
||||
const registerInput = screen.getByLabelText('emails to register')
|
||||
const registerButton = screen.getByRole('button', { name: /register/i })
|
||||
fireEvent.change(registerInput, { target: { value: email } })
|
||||
fireEvent.click(registerButton)
|
||||
expect(registerMock.called()).to.be.true
|
||||
})
|
||||
})
|
|
@ -0,0 +1,139 @@
|
|||
import { expect } from 'chai'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import UserActivateRegister from '../../../../frontend/js/components/user-activate-register'
|
||||
|
||||
describe('UserActivateRegister', function () {
|
||||
beforeEach(function () {
|
||||
fetchMock.reset()
|
||||
})
|
||||
afterEach(function () {
|
||||
fetchMock.reset()
|
||||
})
|
||||
it('should display the error message', async function () {
|
||||
const email = 'abc@gmail.com'
|
||||
render(<UserActivateRegister />)
|
||||
const endPointResponse = {
|
||||
status: 500,
|
||||
}
|
||||
const registerMock = fetchMock.post('/admin/register', endPointResponse)
|
||||
const registerInput = screen.getByLabelText('emails to register')
|
||||
const registerButton = screen.getByRole('button', { name: /register/i })
|
||||
|
||||
fireEvent.change(registerInput, { target: { value: email } })
|
||||
fireEvent.click(registerButton)
|
||||
|
||||
expect(registerMock.called()).to.be.true
|
||||
await screen.findByText('Sorry, an error occured', { exact: false })
|
||||
})
|
||||
|
||||
it('should display the success message', async function () {
|
||||
const email = 'abc@gmail.com'
|
||||
render(<UserActivateRegister />)
|
||||
const endPointResponse = {
|
||||
status: 200,
|
||||
body: {
|
||||
email,
|
||||
setNewPasswordUrl: 'SetNewPasswordURL',
|
||||
},
|
||||
}
|
||||
const registerMock = fetchMock.post('/admin/register', endPointResponse)
|
||||
const registerInput = screen.getByLabelText('emails to register')
|
||||
const registerButton = screen.getByRole('button', { name: /register/i })
|
||||
|
||||
fireEvent.change(registerInput, { target: { value: email } })
|
||||
fireEvent.click(registerButton)
|
||||
|
||||
expect(registerMock.called()).to.be.true
|
||||
await screen.findByText(
|
||||
"We've sent out welcome emails to the registered users."
|
||||
)
|
||||
})
|
||||
|
||||
it('should display the registered emails', async function () {
|
||||
const email = 'abc@gmail.com, def@gmail.com'
|
||||
render(<UserActivateRegister />)
|
||||
const endPointResponse1 = {
|
||||
status: 200,
|
||||
body: {
|
||||
email: 'abc@gmail.com',
|
||||
setNewPasswordUrl: 'SetNewPasswordURL',
|
||||
},
|
||||
}
|
||||
const endPointResponse2 = {
|
||||
status: 200,
|
||||
body: {
|
||||
email: 'def@gmail.com',
|
||||
setNewPasswordUrl: 'SetNewPasswordURL',
|
||||
},
|
||||
}
|
||||
const registerMock = fetchMock.post('/admin/register', (path, req) => {
|
||||
const body = JSON.parse(req.body)
|
||||
if (body.email === 'abc@gmail.com') return endPointResponse1
|
||||
else if (body.email === 'def@gmail.com') return endPointResponse2
|
||||
})
|
||||
const registerInput = screen.getByLabelText('emails to register')
|
||||
const registerButton = screen.getByRole('button', { name: /register/i })
|
||||
|
||||
fireEvent.change(registerInput, { target: { value: email } })
|
||||
fireEvent.click(registerButton)
|
||||
|
||||
expect(registerMock.called()).to.be.true
|
||||
await screen.findByText('abc@gmail.com')
|
||||
await screen.findByText('def@gmail.com')
|
||||
})
|
||||
|
||||
it('should display the failed emails', async function () {
|
||||
const email = 'abc@, def@'
|
||||
render(<UserActivateRegister />)
|
||||
const endPointResponse1 = {
|
||||
status: 500,
|
||||
}
|
||||
const endPointResponse2 = {
|
||||
status: 500,
|
||||
}
|
||||
const registerMock = fetchMock.post('/admin/register', (path, req) => {
|
||||
const body = JSON.parse(req.body)
|
||||
if (body.email === 'abc@') return endPointResponse1
|
||||
else if (body.email === 'def@') return endPointResponse2
|
||||
})
|
||||
const registerInput = screen.getByLabelText('emails to register')
|
||||
const registerButton = screen.getByRole('button', { name: /register/i })
|
||||
|
||||
fireEvent.change(registerInput, { target: { value: email } })
|
||||
fireEvent.click(registerButton)
|
||||
|
||||
expect(registerMock.called()).to.be.true
|
||||
await screen.findByText('abc@')
|
||||
await screen.findByText('def@')
|
||||
})
|
||||
|
||||
it('should display the registered and failed emails together', async function () {
|
||||
const email = 'abc@gmail.com, def@'
|
||||
render(<UserActivateRegister />)
|
||||
const endPointResponse1 = {
|
||||
status: 200,
|
||||
body: {
|
||||
email: 'abc@gmail.com',
|
||||
setNewPasswordUrl: 'SetNewPasswordURL',
|
||||
},
|
||||
}
|
||||
const endPointResponse2 = {
|
||||
status: 500,
|
||||
}
|
||||
const registerMock = fetchMock.post('/admin/register', (path, req) => {
|
||||
const body = JSON.parse(req.body)
|
||||
if (body.email === 'abc@gmail.com') return endPointResponse1
|
||||
else if (body.email === 'def@gmail.com') return endPointResponse2
|
||||
})
|
||||
const registerInput = screen.getByLabelText('emails to register')
|
||||
const registerButton = screen.getByRole('button', { name: /register/i })
|
||||
|
||||
fireEvent.change(registerInput, { target: { value: email } })
|
||||
fireEvent.click(registerButton)
|
||||
|
||||
expect(registerMock.called()).to.be.true
|
||||
await screen.findByText('abc@gmail.com')
|
||||
await screen.findByText('def@')
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue