Skip to content

feat(promo-codes): Tier 2 — search, type filter & multi-select delete for allowed email domains#950

Open
caseylocker wants to merge 10 commits into
masterfrom
feature/domain-authorized-promo-codes-bulk-input-tier2
Open

feat(promo-codes): Tier 2 — search, type filter & multi-select delete for allowed email domains#950
caseylocker wants to merge 10 commits into
masterfrom
feature/domain-authorized-promo-codes-bulk-input-tier2

Conversation

@caseylocker
Copy link
Copy Markdown

@caseylocker caseylocker commented May 20, 2026

ref: https://app.clickup.com/t/86ba1pt2t

Domain-Authorized Promo Codes — Bulk Input Tier 2

Extends ManageAllowedEmailDomainsModal so admins can find and remove entries
within a large allowed_email_domains list. Pure frontend — no API change, the
allowed_email_domains: string[] wire contract is untouched. Independent of the
summit-api Track 2 work.

What's new

  • In-modal search — substring match, debounced 150 ms.
  • Type filterAll / @domain / .tld / user@email; composes with search.
  • Multi-select delete — per-row checkbox, Select All (scoped to the filtered
    view), and Delete Selected.

Tier 1 shipped the modal with a read-only virtualized list; Tier 2 adds three
modal-scoped state slices (debounced search, type filter, a selection Set
keyed by original array index). A visible memo derives the filtered view,
carrying originalIndex so selection and deletion stay correct under filtering.

Implementation notes

Built task-by-task with per-task spec + code-quality review and a Codex review
pass after each task, plus a whole-branch Codex pass. Review-driven fixes folded
in: deferred post-add autoscroll (react-window had the stale itemCount when
scrollToItem ran synchronously after a batched setWorking), a debounce-window
race on Add, removal of an out-of-scope search-flush, the search debounce effect
gated on modal show, and a strengthened filtered-Select-All test that guards
the originalIndex contract.

Testing

  • Automated: 17 modal tests (manage-modal.test.jsx); full
    promocode-form suite green (121); 0 ESLint errors.
  • Manual — component harness: mounted the real modal with 63 synthetic
    domains and drove it in a browser — search narrowing, type filter + search
    composition, multi-select delete, autoscroll-on-add, filtered-add-clears-filter,
    Done/Cancel. No console errors.
  • Manual — full app: end-to-end against api.dev.fnopen.com — opened a
    DOMAIN_AUTHORIZED_* promo code, exercised search / type filter / multi-select
    delete, Saved, reloaded, and confirmed the persisted allowed_email_domains
    round-trips correctly.

Summary by CodeRabbit

  • New Features
    • Added search functionality with debouncing to filter email domains in the management modal.
    • Added type filter (domain/TLD/email) for refined domain list browsing.
    • Added "Select All" and "Delete Selected" bulk actions for managing domains.
    • Improved filter auto-reset behavior when adding new domains.

Review Change Stack

caseylocker and others added 9 commits May 20, 2026 15:37
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Also adds a regression test that confirms searchInput (not just search)
is checked so a typed-but-not-yet-debounced search term is cleared before
new entries are added, keeping additions visible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ders

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Warning

Rate limit exceeded

@caseylocker has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 43 minutes and 8 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: da954a6f-67e6-4354-a870-b9aa379248c8

📥 Commits

Reviewing files that changed from the base of the PR and between f62f024 and a5ea195.

📒 Files selected for processing (3)
  • src/components/forms/promocode-form/forms/domain-authorized/ManageAllowedEmailDomainsModal.jsx
  • src/components/forms/promocode-form/forms/domain-authorized/__tests__/manage-modal.test.jsx
  • src/i18n/en.json
📝 Walkthrough

Walkthrough

The modal now supports debounced full-text search, type-based filtering (at-domain, TLD, or email), multi-select with checkboxes, and bulk delete. Filters reset when the modal opens or domains are added; selection clears when filters change. Autoscroll defers until after the filtered list re-renders. Tests verify debounce interactions, filter composition, selection behavior, and scroll timing.

Changes

Email Domain Modal Search, Filter, and Bulk Selection

Layer / File(s) Summary
Search debounce and filter state
src/components/forms/promocode-form/forms/domain-authorized/ManageAllowedEmailDomainsModal.jsx
Introduces debounce timing, typeOf classifier, and checkbox-enabled row renderer; adds state for searchInput, debounced search, typeFilter, and selection with scroll control refs.
Filter and visible list computation
src/components/forms/promocode-form/forms/domain-authorized/ManageAllowedEmailDomainsModal.jsx
Adjusts add-domains handler to clear filters and defer autoscroll; computes filtered visible array, implements autoscroll effect after list updates, and adds select-all and delete-selected handlers.
UI controls and list rendering
src/components/forms/promocode-form/forms/domain-authorized/ManageAllowedEmailDomainsModal.jsx
Adds search and type filter controls; replaces count-only header with count plus bulk action buttons; updates virtualized list to render filtered visible array with selection.
Enhanced test mocking
src/components/forms/promocode-form/forms/domain-authorized/__tests__/manage-modal.test.jsx
Implements scrollLog mechanism capturing scroll events with render-time itemCount; expands imports and clears scroll history per test.
Filter and debounce behavior tests
src/components/forms/promocode-form/forms/domain-authorized/__tests__/manage-modal.test.jsx
Covers search debounce narrowing, adding during debounce window, type filter composition, and filter reset on domain additions.
Selection and bulk operation tests
src/components/forms/promocode-form/forms/domain-authorized/__tests__/manage-modal.test.jsx
Tests single/bulk deletion, select-all respecting filters, selection clearing on search changes, deferred autoscroll with post-add counts, and debounce cleanup on close/reopen.
Translation strings
src/i18n/en.json
Adds i18n keys for search placeholder, type filter options (all, @domain, .tld, user@email), and bulk actions with count placeholder.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • fntechgit/summit-admin#947: Introduces the ManageAllowedEmailDomainsModal component with react-window virtualization; this PR extends it with debounced search, type filtering, and selection/bulk-delete behavior.

Suggested reviewers

  • smarcet

Poem

🐰 A modal hops through domains with flair,
Search debounces gently through filtered air,
Checkboxes bloom, bulk actions gleam,
Test coverage makes this reviewer's dream! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: search functionality, type filtering, and multi-select delete for managing allowed email domains in promo codes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/domain-authorized-promo-codes-bulk-input-tier2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/components/forms/promocode-form/forms/domain-authorized/__tests__/manage-modal.test.jsx (1)

417-464: ⚡ Quick win

Ensure fake timers are always restored, even on assertion failure.

If this test fails before the final cleanup call, fake timers can leak into subsequent tests. Wrap the test body in try/finally so jest.useRealTimers() always runs.

Proposed fix
   it("closing the modal within the debounce window cancels the pending setSearch", () => {
-    jest.useFakeTimers();
-    const { rerender } = render(
-      <ManageAllowedEmailDomainsModal
-        show
-        onHide={onHide}
-        onApply={onApply}
-        existing={["`@a.com`"]}
-      />
-    );
+    jest.useFakeTimers();
+    try {
+      const { rerender } = render(
+        <ManageAllowedEmailDomainsModal
+          show
+          onHide={onHide}
+          onApply={onApply}
+          existing={["`@a.com`"]}
+        />
+      );
 
-    fireEvent.change(screen.getByTestId("manage-modal-search"), {
-      target: { value: "acme" }
-    });
-    expect(screen.getByTestId("manage-modal-search")).toHaveValue("acme");
+      fireEvent.change(screen.getByTestId("manage-modal-search"), {
+        target: { value: "acme" }
+      });
+      expect(screen.getByTestId("manage-modal-search")).toHaveValue("acme");
 
-    rerender(
-      <ManageAllowedEmailDomainsModal
-        show={false}
-        onHide={onHide}
-        onApply={onApply}
-        existing={["`@a.com`"]}
-      />
-    );
+      rerender(
+        <ManageAllowedEmailDomainsModal
+          show={false}
+          onHide={onHide}
+          onApply={onApply}
+          existing={["`@a.com`"]}
+        />
+      );
 
-    expect(() => jest.runAllTimers()).not.toThrow();
+      expect(() => jest.runAllTimers()).not.toThrow();
 
-    rerender(
-      <ManageAllowedEmailDomainsModal
-        show
-        onHide={onHide}
-        onApply={onApply}
-        existing={["`@a.com`"]}
-      />
-    );
-    expect(screen.getByTestId("manage-modal-search")).toHaveValue("");
-
-    jest.useRealTimers();
+      rerender(
+        <ManageAllowedEmailDomainsModal
+          show
+          onHide={onHide}
+          onApply={onApply}
+          existing={["`@a.com`"]}
+        />
+      );
+      expect(screen.getByTestId("manage-modal-search")).toHaveValue("");
+    } finally {
+      jest.useRealTimers();
+    }
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/components/forms/promocode-form/forms/domain-authorized/__tests__/manage-modal.test.jsx`
around lines 417 - 464, The test "closing the modal within the debounce window
cancels the pending setSearch" currently calls jest.useFakeTimers() and
jest.useRealTimers() but may leak fake timers on assertion failure; wrap the
test body (the code that renders ManageAllowedEmailDomainsModal, fires change,
rerenders, advances timers, and assertions) in a try/finally block so that
jest.useRealTimers() is invoked in the finally, ensuring timers are always
restored even if an assertion throws.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@src/components/forms/promocode-form/forms/domain-authorized/__tests__/manage-modal.test.jsx`:
- Around line 417-464: The test "closing the modal within the debounce window
cancels the pending setSearch" currently calls jest.useFakeTimers() and
jest.useRealTimers() but may leak fake timers on assertion failure; wrap the
test body (the code that renders ManageAllowedEmailDomainsModal, fires change,
rerenders, advances timers, and assertions) in a try/finally block so that
jest.useRealTimers() is invoked in the finally, ensuring timers are always
restored even if an assertion throws.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0aa563fc-5072-4af7-bd8d-f94d59277ad8

📥 Commits

Reviewing files that changed from the base of the PR and between 6505d4e and f62f024.

📒 Files selected for processing (3)
  • src/components/forms/promocode-form/forms/domain-authorized/ManageAllowedEmailDomainsModal.jsx
  • src/components/forms/promocode-form/forms/domain-authorized/__tests__/manage-modal.test.jsx
  • src/i18n/en.json

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends the promo code “Manage Allowed Email Domains” modal UI to better handle large lists by adding debounced search, an entry-type filter, and multi-select bulk delete, while keeping the allowed_email_domains: string[] contract unchanged.

Changes:

  • Added debounced substring search and a type filter (All / @domain / .tld / user@email) to narrow the visible list.
  • Added per-row selection, “Select All” (scoped to the filtered view), and “Delete Selected”.
  • Expanded the modal test suite to cover the new filtering, selection, and deferred autoscroll behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/i18n/en.json Adds new i18n strings for search/filter and bulk actions in the manage modal.
src/components/forms/promocode-form/forms/domain-authorized/ManageAllowedEmailDomainsModal.jsx Implements search, type filter, selection state, bulk delete, and deferred autoscroll.
src/components/forms/promocode-form/forms/domain-authorized/tests/manage-modal.test.jsx Adds Tier 2 tests for debounced search, filter composition, selection semantics, and autoscroll timing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Address PR #950 review feedback (Copilot + CodeRabbit):
- Add aria-label to row checkbox, search input, type-filter select, and
  add-domains textarea — none had an accessible name (label/aria-label).
  Mirrors the aria-label pattern already used in AllowedEmailDomainsRow.
- Correct the misleading comment on the debounce-cancel test and wrap its
  body in try/finally so jest.useRealTimers() always runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@caseylocker
Copy link
Copy Markdown
Author

Addressed all review feedback in a5ea195.

Copilot (4 inline comments) — all valid, all fixed:

  • Row checkbox, search input, and type-filter <select> had no accessible name → added aria-label to each via new i18n keys (row_select_aria, search_aria, type_filter_aria).
  • Also added an aria-label to the add-domains <textarea>, which had the same placeholder-only issue (not flagged, but fixed for consistency).
  • Corrected the misleading comment on the debounce-cancel test.

CodeRabbit (1 nitpick) — valid, fixed:

  • Wrapped the useFakeTimers test body in try/finally so jest.useRealTimers() always runs, even on assertion failure.

All i18n labels go through T.translate, matching the existing aria-label pattern in AllowedEmailDomainsRow. No changes to the allowed_email_domains: string[] contract. Full domain-authorized suite passes (77/77).

@caseylocker caseylocker requested a review from smarcet May 20, 2026 23:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants