- Shipped
- August 29, 2026 at 8:21 PM UTC
- Author
- Kamo
- Commit
- fb4c71c
Two new screens beside the existing Add buttons on /account?tab=members and ?tab=team-members, at /settings/member/bulk-new and /settings/team-member/bulk-new. Both buttons take their wording from the org's own member/team-member labels. The grid is a spreadsheet: one row per person, fields as columns, a fresh blank row appended the moment the last one is typed into, and a remove control on every row but that trailing one. Above it sit the settings the whole batch is created with -- security level (fixed by which screen this is), timezone (the org's default), department, job title, membership status and the mailbox toggle, which starts on for team members and off for members. Nothing is created until "Finished -- Create ..." is pressed; the button then spins, and the grid goes inert until every record has been answered for. Partial success is the designed outcome. The endpoint creates each record in its own transaction and reports on each one, so rows that landed collapse away and rows that did not stay exactly where they were with the reason attached to the cell that caused it -- correct them and finish again, and only the remainder is attempted. Duplicated emails and usernames are caught in the browser too, so the admin sees them while still editing rather than after two thirds of a file has been created. A username left blank shows the name the server will most likely mint; one typed by hand is checked for availability as it is typed. "Import ... List" fills the grid from a CSV/XLS/XLSX and creates nobody. Column mapping reuses FieldMappingInterface -- the same drag-to-connect component the Import Leads wizard uses, which is already generic over its system-field list -- after an auto-map pass that matches the header names an HR export actually uses. The member-side coercions live in memberBulkFields.ts with tests: work location from "Work From Home" / "In Office" / "Hybrid - 3 days in office", prefixes and suffixes against the org's own configured lists, "Last, First" and "Dr. Jane Q. Public" split into parts, emails lowercased. Two fixes fall out of building on that component: FieldMappingInterface gains an optional onModifiersChange. Its split and value modifiers were purely internal, so a consumer received a mapping whose virtualFieldId named a virtual column but had no way to learn which split produced it -- every "Split Name" the user configured was silently dropped when the rows were read. The prop is additive; the leads wizard is untouched. The import always parses with hasHeaders: true and re-attaches row 1 itself when the admin says the file has none. parseFile's own false branch runs a heuristic that peels row 1 off anyway when it looks header-ish, and then reports hasHeaders: true because it synthesised "Field N" labels -- so echoing that value back into the toggle flips it on behind the admin and the next re-parse eats a row. On a member import that row is a person who never gets an account, with nothing on screen to notice.