Skip to content

Commit bd2b16d

Browse files
committed
Adding an invitation form in a modal dialog, so that supervisors can invite new users in ReCodEx.
1 parent c72fc43 commit bd2b16d

File tree

8 files changed

+321
-35
lines changed

8 files changed

+321
-35
lines changed
Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,90 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import PropTypes from 'prop-types';
3+
import { FormattedMessage } from 'react-intl';
4+
import { Modal } from 'react-bootstrap';
5+
import { defaultMemoize } from 'reselect';
36

7+
import InviteUserForm from '../../forms/InviteUserForm';
48
import LeaveJoinGroupButtonContainer from '../../../containers/LeaveJoinGroupButtonContainer';
59
import AddUserContainer from '../../../containers/AddUserContainer';
10+
import Button from '../../widgets/TheButton';
11+
import InsetPanel from '../../widgets/InsetPanel';
12+
import Icon from '../../icons';
613

7-
const AddStudent = ({ groupId, instanceId }) => (
8-
<AddUserContainer
9-
instanceId={instanceId}
10-
id={`add-student-${groupId}`}
11-
createActions={({ id }) => <LeaveJoinGroupButtonContainer userId={id} groupId={groupId} />}
12-
/>
14+
const inviteUserInitialValues = {
15+
titlesBeforeName: '',
16+
firstName: '',
17+
lastName: '',
18+
titlesAfterName: '',
19+
email: '',
20+
};
21+
22+
const prepareInviteOnSubmitHandler = defaultMemoize(
23+
(inviteUser, setDialogOpen, instanceId) =>
24+
({ email, titlesBeforeName, firstName, lastName, titlesAfterName }) => {
25+
email = email.trim();
26+
firstName = firstName.trim();
27+
lastName = lastName.trim();
28+
titlesBeforeName = titlesBeforeName.trim() || undefined;
29+
titlesAfterName = titlesAfterName.trim() || undefined;
30+
return inviteUser({ email, titlesBeforeName, firstName, lastName, titlesAfterName, instanceId }).then(() =>
31+
setDialogOpen(false)
32+
);
33+
}
1334
);
1435

36+
const AddStudent = ({ groupId, instanceId, inviteUser = null }) => {
37+
const [dialogOpen, setDialogOpen] = useState(false);
38+
return (
39+
<>
40+
<AddUserContainer
41+
instanceId={instanceId}
42+
id={`add-student-${groupId}`}
43+
createActions={({ id }) => <LeaveJoinGroupButtonContainer userId={id} groupId={groupId} />}
44+
/>
45+
46+
{inviteUser && (
47+
<>
48+
<hr />
49+
<div className="text-center">
50+
<Button size="sm" variant="primary" onClick={() => setDialogOpen(true)}>
51+
<Icon icon="hand-holding-heart" gapRight />
52+
<FormattedMessage id="app.addStudent.inviteButton" defaultMessage="Invite to Register" />
53+
...
54+
</Button>
55+
</div>
56+
57+
<Modal show={dialogOpen} backdrop="static" onHide={() => setDialogOpen(false)} size="xl">
58+
<Modal.Header closeButton>
59+
<Modal.Title>
60+
<FormattedMessage id="app.addStudent.inviteDialog.title" defaultMessage="Send invitation to ReCodEx" />
61+
</Modal.Title>
62+
</Modal.Header>
63+
64+
<Modal.Body>
65+
<InsetPanel>
66+
<FormattedMessage
67+
id="app.addStudent.inviteDialog.explain"
68+
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."
69+
/>
70+
</InsetPanel>
71+
72+
<InviteUserForm
73+
onSubmit={prepareInviteOnSubmitHandler(inviteUser, setDialogOpen, instanceId)}
74+
initialValues={inviteUserInitialValues}
75+
/>
76+
</Modal.Body>
77+
</Modal>
78+
</>
79+
)}
80+
</>
81+
);
82+
};
83+
1584
AddStudent.propTypes = {
1685
instanceId: PropTypes.string.isRequired,
1786
groupId: PropTypes.string.isRequired,
87+
inviteUser: PropTypes.func,
1888
};
1989

2090
export default AddStudent;

src/components/forms/CreateUserForm/CreateUserForm.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ const validate = ({ firstName, lastName, email, password, passwordConfirm }) =>
193193

194194
const asyncValidate = ({ email, password = '' }, dispatch) => {
195195
if (password === '') {
196-
dispatch(change('edit-user-profile', 'passwordStrength', null));
196+
dispatch(change('create-user', 'passwordStrength', null));
197197
return Promise.resolve();
198198
}
199199

@@ -211,7 +211,7 @@ const asyncValidate = ({ email, password = '' }, dispatch) => {
211211
);
212212
}
213213

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

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

225225
export default reduxForm({
226-
form: 'edit-user-profile',
226+
form: 'create-user',
227227
validate,
228228
asyncValidate,
229229
asyncBlurFields: ['email', 'password', 'passwordConfirm'],
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { FormattedMessage } from 'react-intl';
4+
import { reduxForm, Field } from 'redux-form';
5+
import isEmail from 'validator/lib/isEmail';
6+
7+
import SubmitButton from '../SubmitButton';
8+
import Callout from '../../widgets/Callout';
9+
import { validateRegistrationData } from '../../../redux/modules/users';
10+
import { TextField } from '../Fields';
11+
12+
const InviteUserForm = ({
13+
submitting,
14+
handleSubmit,
15+
onSubmit,
16+
dirty,
17+
submitFailed = false,
18+
submitSucceeded = false,
19+
asyncValidating,
20+
invalid,
21+
reset,
22+
}) => (
23+
<div>
24+
<Field
25+
name="email"
26+
component={TextField}
27+
autoComplete="off"
28+
maxLength={255}
29+
ignoreDirty
30+
label={<FormattedMessage id="app.inviteUserForm.emailAndLogin" defaultMessage="Email (and login name):" />}
31+
/>
32+
33+
<hr />
34+
35+
<Field
36+
name="titlesBeforeName"
37+
component={TextField}
38+
maxLength={42}
39+
required
40+
label={<FormattedMessage id="app.editUserProfile.titlesBeforeName" defaultMessage="Prefix Title:" />}
41+
/>
42+
43+
<Field
44+
name="firstName"
45+
component={TextField}
46+
maxLength={100}
47+
required
48+
ignoreDirty
49+
label={<FormattedMessage id="app.editUserProfile.firstName" defaultMessage="Given Name:" />}
50+
/>
51+
52+
<Field
53+
name="lastName"
54+
component={TextField}
55+
maxLength={255}
56+
required
57+
ignoreDirty
58+
label={<FormattedMessage id="app.editUserProfile.lastName" defaultMessage="Surname:" />}
59+
/>
60+
61+
<Field
62+
name="titlesAfterName"
63+
component={TextField}
64+
maxLength={42}
65+
required
66+
label={<FormattedMessage id="app.editUserProfile.titlesAfterName" defaultMessage="Suffix Title:" />}
67+
/>
68+
69+
{submitFailed && (
70+
<Callout variant="danger">
71+
<FormattedMessage id="generic.operationFailed" defaultMessage="Operation failed. Please try again later." />
72+
</Callout>
73+
)}
74+
75+
<div className="text-center">
76+
<SubmitButton
77+
id="inviteUser"
78+
handleSubmit={handleSubmit(data => onSubmit(data).then(reset))}
79+
submitting={submitting}
80+
dirty={dirty}
81+
invalid={invalid}
82+
hasSucceeded={submitSucceeded}
83+
hasFailed={submitFailed}
84+
asyncValidating={asyncValidating}
85+
messages={{
86+
submit: <FormattedMessage id="app.inviteUserForm.invite" defaultMessage="Invite" />,
87+
submitting: <FormattedMessage id="app.inviteUserForm.inviting" defaultMessage="Inviting..." />,
88+
success: <FormattedMessage id="app.inviteUserForm.invited" defaultMessage="Invited" />,
89+
}}
90+
/>
91+
</div>
92+
</div>
93+
);
94+
95+
InviteUserForm.propTypes = {
96+
handleSubmit: PropTypes.func.isRequired,
97+
onSubmit: PropTypes.func.isRequired,
98+
asyncValidate: PropTypes.func.isRequired,
99+
submitFailed: PropTypes.bool,
100+
submitSucceeded: PropTypes.bool,
101+
dirty: PropTypes.bool,
102+
submitting: PropTypes.bool,
103+
asyncValidating: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
104+
invalid: PropTypes.bool,
105+
pristine: PropTypes.bool,
106+
reset: PropTypes.func,
107+
};
108+
109+
const validate = ({ firstName, lastName, email, password, passwordConfirm }) => {
110+
const errors = {};
111+
112+
if (!firstName) {
113+
errors.firstName = (
114+
<FormattedMessage
115+
id="app.editUserProfile.validation.emptyFirstName"
116+
defaultMessage="First name cannot be empty."
117+
/>
118+
);
119+
}
120+
121+
if (firstName && firstName.length < 2) {
122+
errors.firstName = (
123+
<FormattedMessage
124+
id="app.editUserProfile.validation.shortFirstName"
125+
defaultMessage="First name must contain at least 2 characters."
126+
/>
127+
);
128+
}
129+
130+
if (!lastName) {
131+
errors.lastName = (
132+
<FormattedMessage id="app.editUserProfile.validation.emptyLastName" defaultMessage="Last name cannot be empty." />
133+
);
134+
}
135+
136+
if (lastName && lastName.length < 2) {
137+
errors.lastName = (
138+
<FormattedMessage
139+
id="app.editUserProfile.validation.shortLastName"
140+
defaultMessage="Last name must contain at least 2 characters."
141+
/>
142+
);
143+
}
144+
145+
if (email && isEmail(email) === false) {
146+
errors.email = (
147+
<FormattedMessage
148+
id="app.editUserProfile.validation.emailNotValid"
149+
defaultMessage="E-mail address is not valid."
150+
/>
151+
);
152+
} else if (!email) {
153+
errors.email = (
154+
<FormattedMessage
155+
id="app.editUserProfile.validation.emptyEmail"
156+
defaultMessage="E-mail address cannot be empty."
157+
/>
158+
);
159+
}
160+
161+
return errors;
162+
};
163+
164+
const asyncValidate = ({ email }, dispatch) => {
165+
return new Promise((resolve, reject) =>
166+
dispatch(validateRegistrationData(email))
167+
.then(res => res.value)
168+
.then(({ usernameIsFree }) => {
169+
if (!usernameIsFree) {
170+
const errors = {
171+
email: (
172+
<FormattedMessage
173+
id="app.createUserForm.validation.emailTaken"
174+
defaultMessage="This email address is already taken by someone else."
175+
/>
176+
),
177+
};
178+
throw errors;
179+
}
180+
})
181+
.then(resolve())
182+
.catch(errors => reject(errors))
183+
);
184+
};
185+
186+
export default reduxForm({
187+
form: 'invite-user',
188+
validate,
189+
asyncValidate,
190+
asyncBlurFields: ['email'],
191+
enableReinitialize: true,
192+
keepDirtyOnReinitialize: false,
193+
})(InviteUserForm);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import InviteUserForm from './InviteUserForm';
2+
export default InviteUserForm;

src/locales/cs.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
"app.addSisTermForm.title": "Přidat nový semestr",
3939
"app.addSisTermForm.winter": "Zimní semestr",
4040
"app.addSisTermForm.year": "Rok:",
41+
"app.addStudent.inviteButton": "Poslat pozvánku",
42+
"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.",
43+
"app.addStudent.inviteDialog.title": "Poslat pozvánku do ReCodExu",
4144
"app.addUserContainer.emptyQuery": "Žádné výsledky. Zadejte vyhledávací dotaz...",
4245
"app.allowUserButton.confirmAllow": "Uživatel mohl být zablokován z dobrého důvodu. Opravdu si přejete povolit účet?",
4346
"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?",
@@ -1019,6 +1022,10 @@
10191022
"app.instances.title": "Instance",
10201023
"app.instancesTable.admin": "Admin",
10211024
"app.instancesTable.validLicence": "Má platnou licenci",
1025+
"app.inviteUserForm.emailAndLogin": "Email (a přihlašovací jméno):",
1026+
"app.inviteUserForm.invite": "Zaslat pozvánku",
1027+
"app.inviteUserForm.invited": "Pozvánka zaslána",
1028+
"app.inviteUserForm.inviting": "Zasílám pozvánku...",
10221029
"app.leaveGroup.confirm": "Opravdu chcete opustit tuto skupinu?",
10231030
"app.licencesTable.isValid": "Bez revokace",
10241031
"app.licencesTable.noLicences": "Nejsou zde žádné licence.",

src/locales/en.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
"app.addSisTermForm.title": "Add new term",
3939
"app.addSisTermForm.winter": "Winter term",
4040
"app.addSisTermForm.year": "Year:",
41+
"app.addStudent.inviteButton": "Invite to Register",
42+
"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.",
43+
"app.addStudent.inviteDialog.title": "Send invitation to ReCodEx",
4144
"app.addUserContainer.emptyQuery": "No results. Enter a search query...",
4245
"app.allowUserButton.confirmAllow": "The user may have been disabled for a reason. Do you really wish to enable the account?",
4346
"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?",
@@ -1019,6 +1022,10 @@
10191022
"app.instances.title": "Instances",
10201023
"app.instancesTable.admin": "Admin",
10211024
"app.instancesTable.validLicence": "Has valid licence",
1025+
"app.inviteUserForm.emailAndLogin": "Email (and login name):",
1026+
"app.inviteUserForm.invite": "Invite",
1027+
"app.inviteUserForm.invited": "Invited",
1028+
"app.inviteUserForm.inviting": "Inviting...",
10221029
"app.leaveGroup.confirm": "Are you sure you want to leave this group?",
10231030
"app.licencesTable.isValid": "Without revocation",
10241031
"app.licencesTable.noLicences": "There are no licences.",

0 commit comments

Comments
 (0)