diff --git a/client/app/bundles/course/user-invitations/components/forms/IndividualInvitations.tsx b/client/app/bundles/course/user-invitations/components/forms/IndividualInvitations.tsx index 3711bba77c8..e2dbf0e05c9 100644 --- a/client/app/bundles/course/user-invitations/components/forms/IndividualInvitations.tsx +++ b/client/app/bundles/course/user-invitations/components/forms/IndividualInvitations.tsx @@ -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 '; \"Doe, Jane\" '; ...", + }, + 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) => { 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) => { }); }; + 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); + return ( <> +
+ setNameEmailInput(e.target.value)} + placeholder={t(translations.nameEmailInput)} + size="small" + value={nameEmailInput} + variant="filled" + /> + +
+ + {parsedEmail.errors.length > 0 && ( +
+ +
+ )} + {fields.map( (field, index): JSX.Element => ( { + 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; +}; + +export const parseEmails = ( + input: string, +): { results: InvitationEntry[]; errors: string[] } => { + const regex = /^(?:"([^"]+)"|([^"<]+))?\s*<([^>]+)>$|^([^"<\s]+@[^\s,;<>]+)$/; + 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(); + 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 }; +}; diff --git a/client/app/bundles/course/user-invitations/types.ts b/client/app/bundles/course/user-invitations/types.ts index b73ff68113c..4830e913535 100644 --- a/client/app/bundles/course/user-invitations/types.ts +++ b/client/app/bundles/course/user-invitations/types.ts @@ -80,3 +80,8 @@ export interface InvitationsState { manageCourseUsersData: ManageCourseUsersSharedData; courseRegistrationKey: string; } + +export interface InvitationEntry { + name: string; + email: string; +}