diff --git a/src/components/forms/promocode-form/forms/domain-authorized/ManageAllowedEmailDomainsModal.jsx b/src/components/forms/promocode-form/forms/domain-authorized/ManageAllowedEmailDomainsModal.jsx index 1f6184f34..5d496fb2b 100644 --- a/src/components/forms/promocode-form/forms/domain-authorized/ManageAllowedEmailDomainsModal.jsx +++ b/src/components/forms/promocode-form/forms/domain-authorized/ManageAllowedEmailDomainsModal.jsx @@ -12,16 +12,33 @@ import { parseTextBlob, classifyEntries } from "./bulk-input-parser"; const ROW_HEIGHT = 32; const LIST_HEIGHT = 320; +const SEARCH_DEBOUNCE_MS = 150; + +const typeOf = (entry) => { + if (entry.startsWith("@")) return "at_domain"; + if (entry.startsWith(".")) return "tld"; + return "email"; +}; const Row = React.memo(({ index, style, data }) => { - const entry = data[index]; + const { items, selection, onToggle } = data; + const { entry, originalIndex } = items[index]; return (
- {entry} + onToggle(originalIndex)} + /> + {entry}
); }); @@ -35,7 +52,12 @@ const ManageAllowedEmailDomainsModal = ({ const [working, setWorking] = useState([]); const [draftText, setDraftText] = useState(""); const [toast, setToast] = useState(null); + const [searchInput, setSearchInput] = useState(""); + const [search, setSearch] = useState(""); + const [typeFilter, setTypeFilter] = useState("all"); + const [selection, setSelection] = useState(() => new Set()); const listRef = useRef(null); + const scrollToEndRef = useRef(false); // Intentionally depend only on `show`: snapshot `existing` when the modal opens // and ignore subsequent prop changes — modal owns the working copy until Done/Cancel. @@ -44,9 +66,23 @@ const ManageAllowedEmailDomainsModal = ({ setWorking(Array.isArray(existing) ? [...existing] : []); setDraftText(""); setToast(null); + setSearchInput(""); + setSearch(""); + setTypeFilter("all"); + setSelection(new Set()); } }, [show]); + useEffect(() => { + if (!show) return undefined; + const id = setTimeout(() => setSearch(searchInput), SEARCH_DEBOUNCE_MS); + return () => clearTimeout(id); + }, [searchInput, show]); + + useEffect(() => { + setSelection(new Set()); + }, [search, typeFilter]); + const handleAddDomains = useCallback(() => { const rows = parseTextBlob(draftText); if (rows.length === 0) return; @@ -63,10 +99,30 @@ const ManageAllowedEmailDomainsModal = ({ }); setDraftText(""); - if (listRef.current && next.length > 0) { - listRef.current.scrollToItem(next.length - 1, "end"); + // Adds append to the end of the working copy. Clear any active or pending + // filter so the additions are not filtered out of the visible list. + if (search !== "" || searchInput !== "" || typeFilter !== "all") { + setSearchInput(""); + setSearch(""); + setTypeFilter("all"); + } + // Defer the autoscroll: `setWorking` above is batched and not yet committed, + // so react-window still has the old itemCount and would clamp the target + // index to the old last row. A flag + post-render effect scrolls once the + // list has re-rendered with the new (larger) itemCount. + if (additions.length > 0) { + scrollToEndRef.current = true; } - }, [draftText, working]); + }, [draftText, working, search, searchInput, typeFilter]); + + const handleToggleSelect = useCallback((originalIndex) => { + setSelection((prev) => { + const next = new Set(prev); + if (next.has(originalIndex)) next.delete(originalIndex); + else next.add(originalIndex); + return next; + }); + }, []); const handleKeyDown = (ev) => { if (ev.key === "Enter" && (ev.metaKey || ev.ctrlKey)) { @@ -100,6 +156,40 @@ const ManageAllowedEmailDomainsModal = ({ } ); + const visible = useMemo(() => { + const q = search.trim().toLowerCase(); + const indexed = working.map((entry, originalIndex) => ({ + entry, + originalIndex + })); + return indexed.filter((x) => { + if (typeFilter !== "all" && typeOf(x.entry) !== typeFilter) return false; + if (q && !x.entry.toLowerCase().includes(q)) return false; + return true; + }); + }, [working, search, typeFilter]); + + useEffect(() => { + if (scrollToEndRef.current && listRef.current && visible.length > 0) { + listRef.current.scrollToItem(visible.length - 1, "end"); + } + scrollToEndRef.current = false; + }, [visible]); + + const handleSelectAll = useCallback(() => { + setSelection(new Set(visible.map((x) => x.originalIndex))); + }, [visible]); + + const handleDeleteSelected = useCallback(() => { + setWorking((prev) => prev.filter((_, idx) => !selection.has(idx))); + setSelection(new Set()); + }, [selection]); + + const itemData = useMemo( + () => ({ items: visible, selection, onToggle: handleToggleSelect }), + [visible, selection, handleToggleSelect] + ); + return ( @@ -108,12 +198,52 @@ const ManageAllowedEmailDomainsModal = ({ +
+ setSearchInput(ev.target.value)} + /> + +