Skip to content

Commit

Permalink
Adding an invitation form in a modal dialog, so that supervisors can …
Browse files Browse the repository at this point in the history
…invite new users in ReCodEx.
  • Loading branch information
krulis-martin committed Sep 20, 2022
1 parent c72fc43 commit bd2b16d
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 35 deletions.
84 changes: 77 additions & 7 deletions src/components/Groups/AddStudent/AddStudent.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,90 @@
import React from 'react';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Modal } from 'react-bootstrap';
import { defaultMemoize } from 'reselect';

import InviteUserForm from '../../forms/InviteUserForm';
import LeaveJoinGroupButtonContainer from '../../../containers/LeaveJoinGroupButtonContainer';
import AddUserContainer from '../../../containers/AddUserContainer';
import Button from '../../widgets/TheButton';
import InsetPanel from '../../widgets/InsetPanel';
import Icon from '../../icons';

const AddStudent = ({ groupId, instanceId }) => (
<AddUserContainer
instanceId={instanceId}
id={`add-student-${groupId}`}
createActions={({ id }) => <LeaveJoinGroupButtonContainer userId={id} groupId={groupId} />}
/>
const inviteUserInitialValues = {
titlesBeforeName: '',
firstName: '',
lastName: '',
titlesAfterName: '',
email: '',
};

const prepareInviteOnSubmitHandler = defaultMemoize(
(inviteUser, setDialogOpen, instanceId) =>
({ email, titlesBeforeName, firstName, lastName, titlesAfterName }) => {
email = email.trim();
firstName = firstName.trim();
lastName = lastName.trim();
titlesBeforeName = titlesBeforeName.trim() || undefined;
titlesAfterName = titlesAfterName.trim() || undefined;
return inviteUser({ email, titlesBeforeName, firstName, lastName, titlesAfterName, instanceId }).then(() =>
setDialogOpen(false)
);
}
);

const AddStudent = ({ groupId, instanceId, inviteUser = null }) => {
const [dialogOpen, setDialogOpen] = useState(false);
return (
<>
<AddUserContainer
instanceId={instanceId}
id={`add-student-${groupId}`}
createActions={({ id }) => <LeaveJoinGroupButtonContainer userId={id} groupId={groupId} />}
/>

{inviteUser && (
<>
<hr />
<div className="text-center">
<Button size="sm" variant="primary" onClick={() => setDialogOpen(true)}>
<Icon icon="hand-holding-heart" gapRight />
<FormattedMessage id="app.addStudent.inviteButton" defaultMessage="Invite to Register" />
...
</Button>
</div>

<Modal show={dialogOpen} backdrop="static" onHide={() => setDialogOpen(false)} size="xl">
<Modal.Header closeButton>
<Modal.Title>
<FormattedMessage id="app.addStudent.inviteDialog.title" defaultMessage="Send invitation to ReCodEx" />
</Modal.Title>
</Modal.Header>

<Modal.Body>
<InsetPanel>
<FormattedMessage
id="app.addStudent.inviteDialog.explain"
defaultMessage="An invitation will be sent to the user at given email address. The user will receive a link for registration as a local user. User profile details (name and email) must be filled in correctly, since the user will not be able to modify them."
/>
</InsetPanel>

<InviteUserForm
onSubmit={prepareInviteOnSubmitHandler(inviteUser, setDialogOpen, instanceId)}
initialValues={inviteUserInitialValues}
/>
</Modal.Body>
</Modal>
</>
)}
</>
);
};

AddStudent.propTypes = {
instanceId: PropTypes.string.isRequired,
groupId: PropTypes.string.isRequired,
inviteUser: PropTypes.func,
};

export default AddStudent;
6 changes: 3 additions & 3 deletions src/components/forms/CreateUserForm/CreateUserForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ const validate = ({ firstName, lastName, email, password, passwordConfirm }) =>

const asyncValidate = ({ email, password = '' }, dispatch) => {
if (password === '') {
dispatch(change('edit-user-profile', 'passwordStrength', null));
dispatch(change('create-user', 'passwordStrength', null));
return Promise.resolve();
}

Expand All @@ -211,7 +211,7 @@ const asyncValidate = ({ email, password = '' }, dispatch) => {
);
}

dispatch(change('edit-user-profile', 'passwordStrength', passwordScore));
dispatch(change('create-user', 'passwordStrength', passwordScore));

if (Object.keys(errors).length > 0) {
throw errors;
Expand All @@ -223,7 +223,7 @@ const asyncValidate = ({ email, password = '' }, dispatch) => {
};

export default reduxForm({
form: 'edit-user-profile',
form: 'create-user',
validate,
asyncValidate,
asyncBlurFields: ['email', 'password', 'passwordConfirm'],
Expand Down
193 changes: 193 additions & 0 deletions src/components/forms/InviteUserForm/InviteUserForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { reduxForm, Field } from 'redux-form';
import isEmail from 'validator/lib/isEmail';

import SubmitButton from '../SubmitButton';
import Callout from '../../widgets/Callout';
import { validateRegistrationData } from '../../../redux/modules/users';
import { TextField } from '../Fields';

const InviteUserForm = ({
submitting,
handleSubmit,
onSubmit,
dirty,
submitFailed = false,
submitSucceeded = false,
asyncValidating,
invalid,
reset,
}) => (
<div>
<Field
name="email"
component={TextField}
autoComplete="off"
maxLength={255}
ignoreDirty
label={<FormattedMessage id="app.inviteUserForm.emailAndLogin" defaultMessage="Email (and login name):" />}
/>

<hr />

<Field
name="titlesBeforeName"
component={TextField}
maxLength={42}
required
label={<FormattedMessage id="app.editUserProfile.titlesBeforeName" defaultMessage="Prefix Title:" />}
/>

<Field
name="firstName"
component={TextField}
maxLength={100}
required
ignoreDirty
label={<FormattedMessage id="app.editUserProfile.firstName" defaultMessage="Given Name:" />}
/>

<Field
name="lastName"
component={TextField}
maxLength={255}
required
ignoreDirty
label={<FormattedMessage id="app.editUserProfile.lastName" defaultMessage="Surname:" />}
/>

<Field
name="titlesAfterName"
component={TextField}
maxLength={42}
required
label={<FormattedMessage id="app.editUserProfile.titlesAfterName" defaultMessage="Suffix Title:" />}
/>

{submitFailed && (
<Callout variant="danger">
<FormattedMessage id="generic.operationFailed" defaultMessage="Operation failed. Please try again later." />
</Callout>
)}

<div className="text-center">
<SubmitButton
id="inviteUser"
handleSubmit={handleSubmit(data => onSubmit(data).then(reset))}
submitting={submitting}
dirty={dirty}
invalid={invalid}
hasSucceeded={submitSucceeded}
hasFailed={submitFailed}
asyncValidating={asyncValidating}
messages={{
submit: <FormattedMessage id="app.inviteUserForm.invite" defaultMessage="Invite" />,
submitting: <FormattedMessage id="app.inviteUserForm.inviting" defaultMessage="Inviting..." />,
success: <FormattedMessage id="app.inviteUserForm.invited" defaultMessage="Invited" />,
}}
/>
</div>
</div>
);

InviteUserForm.propTypes = {
handleSubmit: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
asyncValidate: PropTypes.func.isRequired,
submitFailed: PropTypes.bool,
submitSucceeded: PropTypes.bool,
dirty: PropTypes.bool,
submitting: PropTypes.bool,
asyncValidating: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
invalid: PropTypes.bool,
pristine: PropTypes.bool,
reset: PropTypes.func,
};

const validate = ({ firstName, lastName, email, password, passwordConfirm }) => {
const errors = {};

if (!firstName) {
errors.firstName = (
<FormattedMessage
id="app.editUserProfile.validation.emptyFirstName"
defaultMessage="First name cannot be empty."
/>
);
}

if (firstName && firstName.length < 2) {
errors.firstName = (
<FormattedMessage
id="app.editUserProfile.validation.shortFirstName"
defaultMessage="First name must contain at least 2 characters."
/>
);
}

if (!lastName) {
errors.lastName = (
<FormattedMessage id="app.editUserProfile.validation.emptyLastName" defaultMessage="Last name cannot be empty." />
);
}

if (lastName && lastName.length < 2) {
errors.lastName = (
<FormattedMessage
id="app.editUserProfile.validation.shortLastName"
defaultMessage="Last name must contain at least 2 characters."
/>
);
}

if (email && isEmail(email) === false) {
errors.email = (
<FormattedMessage
id="app.editUserProfile.validation.emailNotValid"
defaultMessage="E-mail address is not valid."
/>
);
} else if (!email) {
errors.email = (
<FormattedMessage
id="app.editUserProfile.validation.emptyEmail"
defaultMessage="E-mail address cannot be empty."
/>
);
}

return errors;
};

const asyncValidate = ({ email }, dispatch) => {
return new Promise((resolve, reject) =>
dispatch(validateRegistrationData(email))
.then(res => res.value)
.then(({ usernameIsFree }) => {
if (!usernameIsFree) {
const errors = {
email: (
<FormattedMessage
id="app.createUserForm.validation.emailTaken"
defaultMessage="This email address is already taken by someone else."
/>
),
};
throw errors;
}
})
.then(resolve())
.catch(errors => reject(errors))
);
};

export default reduxForm({
form: 'invite-user',
validate,
asyncValidate,
asyncBlurFields: ['email'],
enableReinitialize: true,
keepDirtyOnReinitialize: false,
})(InviteUserForm);
2 changes: 2 additions & 0 deletions src/components/forms/InviteUserForm/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import InviteUserForm from './InviteUserForm';
export default InviteUserForm;
7 changes: 7 additions & 0 deletions src/locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
"app.addSisTermForm.title": "Přidat nový semestr",
"app.addSisTermForm.winter": "Zimní semestr",
"app.addSisTermForm.year": "Rok:",
"app.addStudent.inviteButton": "Poslat pozvánku",
"app.addStudent.inviteDialog.explain": "Pozvánka bude zaslána uživateli na danou mailovou adresu. Uživatel obdrží odkaz pro registraci lokálním účtem. Detaily uživatelského profilu (jméno a email) vyplňte pečlivě, uživatel nebude mít možnost je změnit.",
"app.addStudent.inviteDialog.title": "Poslat pozvánku do ReCodExu",
"app.addUserContainer.emptyQuery": "Žádné výsledky. Zadejte vyhledávací dotaz...",
"app.allowUserButton.confirmAllow": "Uživatel mohl být zablokován z dobrého důvodu. Opravdu si přejete povolit účet?",
"app.allowUserButton.confirmDisallow": "Pokud zakážete tento uživatelský účet, uživatel nebude moci provést žádnou operaci ani vidět žádná data. Opravdu si přejete účet zakázat?",
Expand Down Expand Up @@ -1019,6 +1022,10 @@
"app.instances.title": "Instance",
"app.instancesTable.admin": "Admin",
"app.instancesTable.validLicence": "Má platnou licenci",
"app.inviteUserForm.emailAndLogin": "Email (a přihlašovací jméno):",
"app.inviteUserForm.invite": "Zaslat pozvánku",
"app.inviteUserForm.invited": "Pozvánka zaslána",
"app.inviteUserForm.inviting": "Zasílám pozvánku...",
"app.leaveGroup.confirm": "Opravdu chcete opustit tuto skupinu?",
"app.licencesTable.isValid": "Bez revokace",
"app.licencesTable.noLicences": "Nejsou zde žádné licence.",
Expand Down
7 changes: 7 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
"app.addSisTermForm.title": "Add new term",
"app.addSisTermForm.winter": "Winter term",
"app.addSisTermForm.year": "Year:",
"app.addStudent.inviteButton": "Invite to Register",
"app.addStudent.inviteDialog.explain": "An invitation will be sent to the user at given email address. The user will receive a link for registration as a local user. User profile details (name and email) must be filled in correctly, since the user will not be able to modify them.",
"app.addStudent.inviteDialog.title": "Send invitation to ReCodEx",
"app.addUserContainer.emptyQuery": "No results. Enter a search query...",
"app.allowUserButton.confirmAllow": "The user may have been disabled for a reason. Do you really wish to enable the account?",
"app.allowUserButton.confirmDisallow": "If you disable the account, the user will not be able to perform any operation nor access any data. Do you wish to disable it?",
Expand Down Expand Up @@ -1019,6 +1022,10 @@
"app.instances.title": "Instances",
"app.instancesTable.admin": "Admin",
"app.instancesTable.validLicence": "Has valid licence",
"app.inviteUserForm.emailAndLogin": "Email (and login name):",
"app.inviteUserForm.invite": "Invite",
"app.inviteUserForm.invited": "Invited",
"app.inviteUserForm.inviting": "Inviting...",
"app.leaveGroup.confirm": "Are you sure you want to leave this group?",
"app.licencesTable.isValid": "Without revocation",
"app.licencesTable.noLicences": "There are no licences.",
Expand Down
Loading

0 comments on commit bd2b16d

Please sign in to comment.