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:
Davinder Singh 2022-06-16 14:16:58 +01:00 committed by Copybot
parent eba2fe9a3e
commit 5498d59c6b
8 changed files with 395 additions and 86 deletions

View file

@ -23,7 +23,6 @@ import './main/translations'
import './main/subscription-dashboard' import './main/subscription-dashboard'
import './main/new-subscription' import './main/new-subscription'
import './main/annual-upgrade' import './main/annual-upgrade'
import './main/register-users'
import './main/subscription/team-invite-controller' import './main/subscription/team-invite-controller'
import './main/subscription/upgrade-subscription' import './main/subscription/upgrade-subscription'
import './main/learn' import './main/learn'

View file

@ -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))
)
})
}
)

View file

@ -1,40 +1,11 @@
extends ../../../../../app/views/layout extends ../../../../../app/views/layout
block entrypointVar
- entrypoint = 'modules/user-activate/pages/user-activate-page'
block content block content
.content.content-alt .content.content-alt
.container .container
.row #user-activate-register-container
.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 }}

View file

@ -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

View file

@ -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

View file

@ -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')
)

View file

@ -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
})
})

View file

@ -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@')
})
})