-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding an invitation form in a modal dialog, so that supervisors can …
…invite new users in ReCodEx.
- Loading branch information
1 parent
c72fc43
commit bd2b16d
Showing
8 changed files
with
321 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import InviteUserForm from './InviteUserForm'; | ||
export default InviteUserForm; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.