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)}
+ />
+
+