feat: split a pasted address list into recipient chips
Pasting a list of addresses into To/Cc/Bcc now creates one chip per address instead of dropping the whole blob in as a single invalid chip. A paste is split only when it actually contains a separator; a lone address falls through to normal editing. - Separators: commas, semicolons, and any whitespace/newline - covers comma/space dumps, spreadsheet columns and Outlook-style `;` lists. - Display names are preserved: `Name <email>`, a fully-quoted `"Name <email>"` entry, and `"Doe, John" <email>` (comma inside a quoted name) each stay a single chip with the name intact. - Bare-address runs split per address; a `<addr>` token is unwrapped; tokens that aren't valid addresses are left behind in the input for the user to fix rather than becoming junk chips. - Deduped case-insensitively within the paste and against existing chips. Implemented as splitPastedRecipients in email-composer-utils, layered on the shared quote/angle-aware splitter: splitRecipients gains an optional `separators` argument so the composer/mailto serialization boundary (comma-only) and the paste path (`,;\n\r`) share one implementation. Wired into the recipient chip input's onPaste handler (To/Cc/Bcc).
This commit is contained in:
committed by
Linus Rath
parent
638fc7db4e
commit
344795a8d9
@@ -9,6 +9,7 @@ import {
|
||||
parseRecipient,
|
||||
parseRecipientList,
|
||||
formatRecipientList,
|
||||
splitPastedRecipients,
|
||||
} from "../email-composer-utils";
|
||||
|
||||
describe("plainTextToComposerBody", () => {
|
||||
@@ -150,6 +151,16 @@ describe("splitRecipients", () => {
|
||||
it("returns an empty array for an empty string", () => {
|
||||
expect(splitRecipients("")).toEqual([]);
|
||||
});
|
||||
|
||||
it("only splits on the given separators (default comma keeps semicolons/newlines literal)", () => {
|
||||
expect(splitRecipients("a@x.com; b@y.com")).toEqual(["a@x.com; b@y.com"]);
|
||||
});
|
||||
|
||||
it("splits on a wider separator set while keeping quotes/angles literal", () => {
|
||||
expect(
|
||||
splitRecipients('"Doo, John" <john@doo.org>; a@x.com\nb@y.com', ',;\n\r'),
|
||||
).toEqual(['"Doo, John" <john@doo.org>', "a@x.com", "b@y.com"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatRecipient / parseRecipient", () => {
|
||||
@@ -198,3 +209,68 @@ describe("parseRecipientList / formatRecipientList", () => {
|
||||
expect(parseRecipientList("")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("splitPastedRecipients", () => {
|
||||
it("splits on commas, semicolons and whitespace (incl. newline/tab)", () => {
|
||||
const { valid, invalid } = splitPastedRecipients(
|
||||
"a@x.com, b@y.com; c@z.com\nd@w.com\te@v.com f@u.com",
|
||||
);
|
||||
expect(valid.map((r) => r.email)).toEqual([
|
||||
"a@x.com", "b@y.com", "c@z.com", "d@w.com", "e@v.com", "f@u.com",
|
||||
]);
|
||||
expect(invalid).toEqual([]);
|
||||
});
|
||||
|
||||
it("collapses runs of mixed separators and drops empties", () => {
|
||||
const { valid } = splitPastedRecipients(" a@x.com ,; , b@y.com ");
|
||||
expect(valid.map((r) => r.email)).toEqual(["a@x.com", "b@y.com"]);
|
||||
});
|
||||
|
||||
it("partitions invalid tokens into `invalid`, keeping valid as chips", () => {
|
||||
const { valid, invalid } = splitPastedRecipients("a@x.com not-an-email b@y.com");
|
||||
expect(valid.map((r) => r.email)).toEqual(["a@x.com", "b@y.com"]);
|
||||
expect(invalid).toEqual(["not-an-email"]);
|
||||
});
|
||||
|
||||
it("unwraps an angle-bracketed token before validating", () => {
|
||||
const { valid } = splitPastedRecipients("<a@x.com>");
|
||||
expect(valid).toEqual([{ email: "a@x.com" }]);
|
||||
});
|
||||
|
||||
it("keeps a `Name <email>` pair as a single chip with its display name", () => {
|
||||
const { valid, invalid } = splitPastedRecipients("John Doe <j@x.com>");
|
||||
expect(valid).toEqual([{ name: "John Doe", email: "j@x.com" }]);
|
||||
expect(invalid).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps a fully-quoted `\"Name <email>\"` entry with its display name", () => {
|
||||
const { valid, invalid } = splitPastedRecipients(
|
||||
'"Alice Smith <alice@x.com>", "Alex Smith <alex@x.com>"',
|
||||
);
|
||||
expect(valid).toEqual([
|
||||
{ name: "Alice Smith", email: "alice@x.com" },
|
||||
{ name: "Alex Smith", email: "alex@x.com" },
|
||||
]);
|
||||
expect(invalid).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps a comma inside a quoted display name intact", () => {
|
||||
const { valid } = splitPastedRecipients('"Doe, John" <j@x.com>; bob@z.com');
|
||||
expect(valid).toEqual([
|
||||
{ name: "Doe, John", email: "j@x.com" },
|
||||
{ email: "bob@z.com" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("dedupes case-insensitively within the paste and against existing emails", () => {
|
||||
const { valid } = splitPastedRecipients(
|
||||
"a@x.com A@X.com b@y.com c@z.com",
|
||||
["B@Y.com"],
|
||||
);
|
||||
expect(valid.map((r) => r.email)).toEqual(["a@x.com", "c@z.com"]);
|
||||
});
|
||||
|
||||
it("returns empty arrays for blank input", () => {
|
||||
expect(splitPastedRecipients(" ")).toEqual({ valid: [], invalid: [] });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user