When pasting text containing multiple email addresses into the attendee input, the component now splits by comma, semicolon, newline, or space and adds each valid email as an individual attendee chip. Duplicates are silently skipped. Any invalid chunks remain in the input field with an error message so the user can correct them manually. Co-authored-by: JacobiusMakes <jgalperin98@gmail.com>
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { ResourceIcon } from "./ResourceIcon";
|
||||
import { isValidEmail } from "../../utils/isValidEmail";
|
||||
import { usePasteHandler } from "./usePasteHandler";
|
||||
|
||||
export interface User {
|
||||
email: string;
|
||||
@@ -124,6 +125,15 @@ export function PeopleSearch({
|
||||
[query, selectedUsers, onChange, t, setInputError, setQuery]
|
||||
);
|
||||
|
||||
const handlePaste = usePasteHandler({
|
||||
freeSolo,
|
||||
selectedUsers,
|
||||
onChange,
|
||||
setQuery,
|
||||
setInputError,
|
||||
t,
|
||||
});
|
||||
|
||||
const defaultRenderInput = useCallback(
|
||||
(params: AutocompleteRenderInputParams) => {
|
||||
const inputProps = {
|
||||
@@ -150,6 +160,7 @@ export function PeopleSearch({
|
||||
inputProps: {
|
||||
...params.inputProps,
|
||||
autoComplete: "off",
|
||||
onPaste: handlePaste,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -206,7 +217,14 @@ export function PeopleSearch({
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[inputError, t, onToggleEventPreview, loading, searchPlaceholder]
|
||||
[
|
||||
inputError,
|
||||
t,
|
||||
onToggleEventPreview,
|
||||
loading,
|
||||
searchPlaceholder,
|
||||
handlePaste,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useCallback } from "react";
|
||||
import type { SyntheticEvent } from "react";
|
||||
import { isValidEmail } from "../../utils/isValidEmail";
|
||||
import type { User } from "./PeopleSearch";
|
||||
|
||||
export function usePasteHandler({
|
||||
freeSolo,
|
||||
selectedUsers,
|
||||
onChange,
|
||||
setQuery,
|
||||
setInputError,
|
||||
t,
|
||||
}: {
|
||||
freeSolo?: boolean;
|
||||
selectedUsers: User[];
|
||||
onChange: (event: SyntheticEvent, users: User[]) => void;
|
||||
setQuery: (value: string) => void;
|
||||
setInputError: (error: string | null) => void;
|
||||
t: (key: string) => string;
|
||||
}) {
|
||||
return useCallback(
|
||||
(event: React.ClipboardEvent<HTMLInputElement>) => {
|
||||
if (!freeSolo) return;
|
||||
|
||||
const pasted = event.clipboardData.getData("text/plain");
|
||||
if (!pasted) return;
|
||||
|
||||
// Split by comma, semicolon, newline, or whitespace
|
||||
const chunks = pasted
|
||||
.split(/[,;\n\r\s]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
// If there is only one chunk, let the default Autocomplete behaviour handle it
|
||||
if (chunks.length <= 1) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const existingEmails = new Set(selectedUsers.map((u) => u.email));
|
||||
const validUsers: User[] = [];
|
||||
const invalid: string[] = [];
|
||||
|
||||
for (const chunk of chunks) {
|
||||
if (!isValidEmail(chunk)) {
|
||||
invalid.push(chunk);
|
||||
} else if (!existingEmails.has(chunk)) {
|
||||
existingEmails.add(chunk);
|
||||
validUsers.push({ email: chunk, displayName: chunk });
|
||||
}
|
||||
// silently skip duplicates
|
||||
}
|
||||
|
||||
if (validUsers.length > 0) {
|
||||
onChange(event, [...selectedUsers, ...validUsers]);
|
||||
}
|
||||
|
||||
if (invalid.length > 0) {
|
||||
// Leave the invalid text in the input for manual correction
|
||||
setQuery(invalid.join(", "));
|
||||
setInputError(
|
||||
t("peopleSearch.invalidEmail").replace("%{email}", invalid.join(", "))
|
||||
);
|
||||
} else {
|
||||
setQuery("");
|
||||
setInputError(null);
|
||||
}
|
||||
},
|
||||
[freeSolo, selectedUsers, onChange, setQuery, setInputError, t]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user