From 5498d59c6ba9754731cea9a04b7826818d960fdb Mon Sep 17 00:00:00 2001 From: Davinder Singh Date: Thu, 16 Jun 2022 14:16:58 +0100 Subject: [PATCH] Merge pull request #8259 from overleaf/ds-admin-panel-register-new-user Migrating admin panel register new user to react GitOrigin-RevId: 520fea91cd9e560e4381504de45e5bedf11a7844 --- services/web/frontend/js/main.js | 1 - .../web/frontend/js/main/register-users.js | 50 ------- .../user-activate/app/views/user/register.pug | 41 +----- .../frontend/js/components/register-form.js | 87 +++++++++++ .../js/components/user-activate-register.js | 92 ++++++++++++ .../frontend/js/pages/user-activate-page.js | 9 ++ .../js/components/register-form.test.js | 62 ++++++++ .../components/user-activate-register.test.js | 139 ++++++++++++++++++ 8 files changed, 395 insertions(+), 86 deletions(-) delete mode 100644 services/web/frontend/js/main/register-users.js create mode 100644 services/web/modules/user-activate/frontend/js/components/register-form.js create mode 100644 services/web/modules/user-activate/frontend/js/components/user-activate-register.js create mode 100644 services/web/modules/user-activate/frontend/js/pages/user-activate-page.js create mode 100644 services/web/modules/user-activate/test/frontend/js/components/register-form.test.js create mode 100644 services/web/modules/user-activate/test/frontend/js/components/user-activate-register.test.js diff --git a/services/web/frontend/js/main.js b/services/web/frontend/js/main.js index 6e5fe076b8..d75e6a74fa 100644 --- a/services/web/frontend/js/main.js +++ b/services/web/frontend/js/main.js @@ -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' diff --git a/services/web/frontend/js/main/register-users.js b/services/web/frontend/js/main/register-users.js deleted file mode 100644 index d7e2296768..0000000000 --- a/services/web/frontend/js/main/register-users.js +++ /dev/null @@ -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)) - ) - }) - } -) diff --git a/services/web/modules/user-activate/app/views/user/register.pug b/services/web/modules/user-activate/app/views/user/register.pug index 3dc0921a06..faca16ea9b 100644 --- a/services/web/modules/user-activate/app/views/user/register.pug +++ b/services/web/modules/user-activate/app/views/user/register.pug @@ -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 }} - \ No newline at end of file + #user-activate-register-container + \ No newline at end of file diff --git a/services/web/modules/user-activate/frontend/js/components/register-form.js b/services/web/modules/user-activate/frontend/js/components/register-form.js new file mode 100644 index 0000000000..0fc8211af6 --- /dev/null +++ b/services/web/modules/user-activate/frontend/js/components/register-form.js @@ -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 ( +
+
+
+ +

+ Enter the emails you would like to register and separate them using + commas +

+
+
+ +
+
+
+ ) +} + +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 diff --git a/services/web/modules/user-activate/frontend/js/components/user-activate-register.js b/services/web/modules/user-activate/frontend/js/components/user-activate-register.js new file mode 100644 index 0000000000..d3c66c8f59 --- /dev/null +++ b/services/web/modules/user-activate/frontend/js/components/user-activate-register.js @@ -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 ( +
+
+
+
+

Register New Users

+
+ + {registerError ? ( + + ) : null} + {registrationSuccess ? ( + <> + +
+ + + ) : null} +
+
+
+ ) +} + +function UserActivateError({ failedEmails }) { + return ( +
+

Sorry, an error occured, failed to register these emails.

+ {failedEmails.map(email => ( +

{email}

+ ))} +
+ ) +} + +function SuccessfulRegistrationMessage() { + return ( +
+

We've sent out welcome emails to the registered users.

+

+ You can also manually send them URLs below to allow them to reset their + password and log in for the first time. +

+

+ (Password reset tokens will expire after one week and the user will need + registering again). +

+
+ ) +} + +function DisplayEmailsList({ emails }) { + return ( + + + + + + + {emails.map(user => ( + + + + + ))} + +
EmailSet Password Url
{user.email}{user.setNewPasswordUrl}
+ ) +} + +DisplayEmailsList.propTypes = { + emails: PropTypes.array, +} +UserActivateError.propTypes = { + failedEmails: PropTypes.array, +} + +export default UserActivateRegister diff --git a/services/web/modules/user-activate/frontend/js/pages/user-activate-page.js b/services/web/modules/user-activate/frontend/js/pages/user-activate-page.js new file mode 100644 index 0000000000..b7d9a69959 --- /dev/null +++ b/services/web/modules/user-activate/frontend/js/pages/user-activate-page.js @@ -0,0 +1,9 @@ +import '../../../../../frontend/js/marketing' + +import ReactDOM from 'react-dom' +import UserActivateRegister from '../components/user-activate-register' + +ReactDOM.render( + , + document.getElementById('user-activate-register-container') +) diff --git a/services/web/modules/user-activate/test/frontend/js/components/register-form.test.js b/services/web/modules/user-activate/test/frontend/js/components/register-form.test.js new file mode 100644 index 0000000000..39c6e98c1b --- /dev/null +++ b/services/web/modules/user-activate/test/frontend/js/components/register-form.test.js @@ -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( + + ) + 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( + + ) + 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 + }) +}) diff --git a/services/web/modules/user-activate/test/frontend/js/components/user-activate-register.test.js b/services/web/modules/user-activate/test/frontend/js/components/user-activate-register.test.js new file mode 100644 index 0000000000..351336a17e --- /dev/null +++ b/services/web/modules/user-activate/test/frontend/js/components/user-activate-register.test.js @@ -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() + 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() + 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() + 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() + 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() + 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@') + }) +})