-
Notifications
You must be signed in to change notification settings - Fork 78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow Inviting Users to Course by Email #7655
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import { FC } from 'react'; | ||
import { FC, useState } from 'react'; | ||
import { | ||
Control, | ||
UseFieldArrayAppend, | ||
|
@@ -13,6 +13,12 @@ import { | |
IndividualInvites, | ||
} from 'types/course/userInvitations'; | ||
|
||
import { parseEmails } from 'course/user-invitations/operations'; | ||
import { InvitationEntry } from 'course/user-invitations/types'; | ||
import ErrorText from 'lib/components/core/ErrorText'; | ||
import TextField from 'lib/components/core/fields/TextField'; | ||
import useTranslation from 'lib/hooks/useTranslation'; | ||
|
||
import IndividualInvitation from './IndividualInvitation'; | ||
|
||
interface Props extends WrappedComponentProps { | ||
|
@@ -35,11 +41,29 @@ const translations = defineMessages({ | |
id: 'course.userInvitations.IndividualInvitations.invite', | ||
defaultMessage: 'Invite All Users', | ||
}, | ||
nameEmailInput: { | ||
id: 'course.userInvitations.IndividualInvitations.nameEmailInput', | ||
defaultMessage: | ||
"John Doe '<[email protected]'>; \"Doe, Jane\" '<[email protected]'>; ...", | ||
}, | ||
addRowsByEmail: { | ||
id: 'course.userInvitations.IndividualInvitations.addRowsByEmail', | ||
defaultMessage: 'Add Rows by Email', | ||
}, | ||
malformedEmail: { | ||
id: 'course.userInvitations.IndividualInvitations.malformedEmail', | ||
defaultMessage: | ||
'{n, plural, one {This email is } other {These emails are }} wrongly formatted: {emails}', | ||
}, | ||
}); | ||
|
||
const IndividualInvitations: FC<Props> = (props) => { | ||
const { isLoading, permissions, fieldsConfig, intl } = props; | ||
const { append, fields } = fieldsConfig; | ||
const { append, remove, fields } = fieldsConfig; | ||
|
||
const { t } = useTranslation(); | ||
|
||
const [nameEmailInput, setNameEmailInput] = useState(''); | ||
|
||
const appendNewRow = (): void => { | ||
const lastRow = fields[fields.length - 1]; | ||
|
@@ -54,8 +78,70 @@ const IndividualInvitations: FC<Props> = (props) => { | |
}); | ||
}; | ||
|
||
const appendInputs = (results: InvitationEntry[]): void => { | ||
const lastRow = fields[fields.length - 1]; | ||
|
||
if (fields.length === 1 && results.length > 0) { | ||
remove(0); | ||
} | ||
|
||
results.forEach((entry) => { | ||
append({ | ||
name: entry.name, | ||
email: entry.email, | ||
role: lastRow.role, | ||
phantom: lastRow.phantom, | ||
...(permissions.canManagePersonalTimes && { | ||
timelineAlgorithm: lastRow.timelineAlgorithm, | ||
}), | ||
}); | ||
}); | ||
}; | ||
|
||
const parsedEmail = parseEmails(nameEmailInput); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
return ( | ||
<> | ||
<div className="w-full flex justify-between text-center items-center"> | ||
<TextField | ||
className="w-[130rem]" | ||
hiddenLabel | ||
multiline | ||
name="nameEmailInvitation" | ||
onChange={(e): void => setNameEmailInput(e.target.value)} | ||
placeholder={t(translations.nameEmailInput)} | ||
size="small" | ||
value={nameEmailInput} | ||
variant="filled" | ||
/> | ||
<Button | ||
className="w-1/8 h-[3.5rem]" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: avoid hardcoding |
||
color="primary" | ||
onClick={(): void => { | ||
appendInputs(parsedEmail.results); | ||
setNameEmailInput( | ||
parsedEmail.errors.length > 0 | ||
? parsedEmail.errors.join('; ') | ||
: '', | ||
Comment on lines
+123
to
+125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the length is 0 then |
||
); | ||
}} | ||
variant="outlined" | ||
> | ||
{t(translations.addRowsByEmail)} | ||
</Button> | ||
</div> | ||
|
||
{parsedEmail.errors.length > 0 && ( | ||
<div className="mt-1"> | ||
<ErrorText | ||
errors={t(translations.malformedEmail, { | ||
n: parsedEmail.errors.length, | ||
emails: parsedEmail.errors.join(', '), | ||
})} | ||
/> | ||
</div> | ||
)} | ||
|
||
{fields.map( | ||
(field, index): JSX.Element => ( | ||
<IndividualInvitation | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ import { | |
import CourseAPI from 'api/course'; | ||
|
||
import { actions } from './store'; | ||
import { InvitationEntry } from './types'; | ||
|
||
/** | ||
* Prepares and maps answer value in the react-hook-form into server side format. | ||
|
@@ -129,3 +130,62 @@ export function toggleRegistrationCode(shouldEnable: boolean): Operation { | |
); | ||
}); | ||
} | ||
|
||
export const splitEntries = (input: string): string[] => { | ||
const entries: string[] = []; | ||
let currentEntry = ''; | ||
let inQuotes = false; | ||
for (let i = 0; i < input.length; i++) { | ||
const char = input[i]; | ||
if (char === '"') { | ||
inQuotes = !inQuotes; | ||
currentEntry += char; | ||
} else if ((char === ',' || char === ';') && !inQuotes) { | ||
if (currentEntry.trim()) { | ||
entries.push(currentEntry.trim()); | ||
} | ||
currentEntry = ''; | ||
} else { | ||
currentEntry += char; | ||
} | ||
} | ||
if (currentEntry.trim()) { | ||
entries.push(currentEntry.trim()); | ||
} | ||
return entries; | ||
}; | ||
Comment on lines
+134
to
+156
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Split using regex instead of implementing an algorithm here instead. |
||
|
||
export const parseEmails = ( | ||
input: string, | ||
): { results: InvitationEntry[]; errors: string[] } => { | ||
const regex = /^(?:"([^"]+)"|([^"<]+))?\s*<([^>]+)>$|^([^"<\s]+@[^\s,;<>]+)$/; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update variable name to |
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | ||
const results: InvitationEntry[] = []; | ||
const errors: string[] = []; | ||
|
||
const entries = splitEntries(input); | ||
|
||
entries.forEach((entry) => { | ||
const match = regex.exec(entry); | ||
if (match) { | ||
if (match[3]) { | ||
const name = (match[1] || match[2] || '').trim(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Your regex should be written to exclude the surrounding whitespaces, so you won't need to use
Comment on lines
+171
to
+172
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we simplify the regex? Why so many cases? |
||
const email = match[3].trim(); | ||
if (emailRegex.test(email)) { | ||
results.push({ name, email }); | ||
} else { | ||
errors.push(`"${name}" <${email}>`); | ||
} | ||
} else if (match[4]) { | ||
const email = match[4].trim(); | ||
if (emailRegex.test(email)) { | ||
results.push({ name: email, email }); | ||
} else { | ||
errors.push(email); | ||
} | ||
} | ||
} | ||
}); | ||
|
||
return { results, errors }; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code duplication here. You should make use of
appendNewRow
and extend it by allowingname
andemail
arguments (default''
).