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..4bf4c340bdf 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 { parseInvitationInput } 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,17 +41,37 @@ 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 appendNewRow = (): void => { - const lastRow = fields[fields.length - 1]; + const { t } = useTranslation(); + + const [nameEmailInput, setNameEmailInput] = useState(''); + + const appendRow = ( + lastRow: IndividualInvite, + entry?: InvitationEntry, + ): void => { append({ - name: '', - email: '', + name: entry?.name ?? '', + email: entry?.email ?? '', role: lastRow.role, phantom: lastRow.phantom, ...(permissions.canManagePersonalTimes && { @@ -54,8 +80,62 @@ const IndividualInvitations: FC = (props) => { }); }; + const appendNewRow = (): void => { + const lastRow = fields[fields.length - 1]; + appendRow(lastRow, undefined); + }; + + const appendInputs = (results: InvitationEntry[]): void => { + const lastRow = fields[fields.length - 1]; + + for (let idx = fields.length - 1; idx >= 0; idx--) { + const { name, email } = fields[idx]; + if (!name && !email) remove(idx); + } + + results.forEach((entry) => appendRow(lastRow, entry)); + }; + + const parsedInput = parseInvitationInput(nameEmailInput); + return ( <> +
+ setNameEmailInput(e.target.value)} + placeholder={t(translations.nameEmailInput)} + size="small" + value={nameEmailInput} + variant="filled" + /> + +
+ + {parsedInput.errors.length > 0 && ( +
+ +
+ )} + {fields.map( (field, index): JSX.Element => ( \s]+?)\s*>$|^\s*([^"<\s]+@[^\s,;<>]+)\s*$/; +const formattedEmailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +export const splitEntries = (input: string): string[] => { + return input.split(/\s*[;,\n\u200B]\s*(?=(?:[^"]*"[^"]*")*[^"]*$)/); +}; + +const processInvitationEntry = ( + entry: string, + errors: string[], + results: InvitationEntry[], +): void => { + if (!entry) return; + const match = splitNameAndEmailRegex.exec(entry); + if (match) { + const email = match[3] || match[4]; + const name = match[1] || match[2] || email; + if (formattedEmailRegex.test(email)) { + results.push({ name, email }); + return; + } + } + errors.push(entry); +}; + +export const parseInvitationInput = ( + input: string, +): { results: InvitationEntry[]; errors: string[] } => { + const results: InvitationEntry[] = []; + const errors: string[] = []; + + const entries = splitEntries(input); + + entries.forEach((entry) => processInvitationEntry(entry, errors, results)); + + 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; +}