Skip to content

feat(history): add encounter and search filter to loot history (#89)#172

Open
Xerrion wants to merge 12 commits into
masterfrom
feat/89-history-encounter-filter
Open

feat(history): add encounter and search filter to loot history (#89)#172
Xerrion wants to merge 12 commits into
masterfrom
feat/89-history-encounter-filter

Conversation

@Xerrion

@Xerrion Xerrion commented May 26, 2026

Copy link
Copy Markdown
Owner

Closes #89

Summary

Adds an encounter dropdown + free-text search filter to the loot history window. Filter state persists across reloads. A new options toggle hides the filter bar for users who don't want it.

Changes

Data model

  • Schema bumped v4 → v5 with safe partial-merge for nested defaults.
  • New slot: db.profile.history.filter = { encounterID = nil, search = "", barVisible = true }.

Classic encounter tagging

  • New Listeners/EncounterListener_Classic.lua captures ENCOUNTER_START payload into ns.currentEncounter.
  • Classic loot entries now carry encounterID and encounterName at tag time, matching Retail behavior.
  • Sticky cache cleared on instance boundary (select(8, GetInstanceInfo()) change) per ADR-0004.

Filter UI

  • Sub-bar below the history window title bar with UIDropDownMenuTemplate encounter dropdown + SearchBoxTemplate search box + visible/total counter.
  • Dropdown populated dynamically from current history entries (encounter names resolved via captured Classic name → cache → EJ_GetEncounterInfo → numeric fallback).
  • Search matches item link names and winner names, case-insensitive substring (plain, not regex).
  • "Unknown encounter" sentinel filters to entries with nil encounterID.
  • Single codepath across Retail Midnight 12.0.5, MoP Classic, and TBC Anniversary.

Persistence

  • Filter state survives reloads and profile switches.
  • Persist writes happen before refresh in every mutation site so a refresh throw can't desync persisted state.

Options

  • New "Show filter bar" toggle in History → Display section.
  • Live show/hide via ns.HistoryFrame.ApplySettings() — scroll frame re-anchors to reclaim/yield the filter-bar height without reload.

Tests

  • +18 tests for filter pipeline (spec/HistoryFilter_spec.lua): predicate purity, sentinel handling, persistence round-trip.
  • +4 tests for Classic encounter capture (spec/EncounterListener_Classic_spec.lua).
  • Total: 71 successes / 0 failures.

Verification

  • mise exec -- just check — stylua + luacheck + busted all green.
  • Lint: 0 warnings / 0 errors in 52 files.
  • Tests: 71 / 71 passing.

Out of scope

  • Workspace-root AGENTS.md updates (separate concern).
  • Phase 6 reviewer's MEDIUM observation about a redundant startup refresh when searchBox:SetText re-fires the OnTextChanged hook — non-blocking, follow-up if needed.

Summary by CodeRabbit

  • New Features

    • Added encounter and search filtering to loot history with filter bar toggle in Display settings
    • Enhanced encounter context tracking for Classic WoW
    • Filter status now displays visible vs. total entry counts
  • Documentation

    • Updated project structure and configuration schema reference documentation
  • Chores

    • Updated database schema and WoW API reference whitelist

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Xerrion, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 50 minutes and 27 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, 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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: 21f97f60-6089-42da-a97f-8b3021842593

📥 Commits

Reviewing files that changed from the base of the PR and between 6fd49d3 and 16cafd3.

📒 Files selected for processing (5)
  • DragonLoot/Core/Init.lua
  • DragonLoot/Display/HistoryFrame.lua
  • DragonLoot/Listeners/EncounterListener_Classic.lua
  • spec/EncounterListener_Classic_spec.lua
  • spec/wow_mock.lua
📝 Walkthrough

Walkthrough

This PR adds encounter-based filtering to the loot history frame. It introduces a dropdown and search box to filter items by boss/encounter, Classic event capture for encounter context, schema migration to v5, comprehensive test coverage, and a persistent filter bar visibility toggle.

Changes

Encounter filter feature and Classic support

Layer / File(s) Summary
Config schema v4→v5 and nested-table merging
DragonLoot/Core/Config.lua
Schema bumped to 5; profile.history.filter table added to defaults with search and barVisible fields; nested-table default reconciliation refactored to merge key-by-key instead of validating and resetting entire nested tables.
Classic encounter listener and registration
DragonLoot/Listeners/EncounterListener_Classic.lua, DragonLoot/DragonLoot.toc
New listener registers ENCOUNTER_START in non-retail build, caches encounter metadata into ns.currentEncounter, includes retail version guard.
History entry encounter context tagging
DragonLoot/Listeners/HistoryListener_Classic.lua
History entries are built, conditionally tagged with encounter context from ns.currentEncounter when instanceID matches current player instance, then inserted; preserves nil tags on instance departure.
Filter logic: predicate, visibility, persistence
DragonLoot/Display/HistoryFrame.lua (core logic)
Introduces UNKNOWN_ENCOUNTER sentinel, encounter-name caching, MatchesFilter pure predicate for encounterID and case-insensitive item/winner search, GetVisibleEntries() to derive renderable entries, PersistFilter/RestoreFilter syncing to db.profile.history.filter, and ShouldShowFilterBar()/GetTopBarOffset() offset helpers.
Filter bar UI: dropdown, search, count display
DragonLoot/Display/HistoryFrame.lua (UI construction)
Filter bar widget created with encounter dropdown (populated dynamically from history on open), search textbox with live filtering, and count label; includes ResolveEncounterName with fallback and caching, InitEncounterDropdown for option building, and ApplyFilterToWidgets() to sync restored state.
Dynamic layout with variable filter bar height
DragonLoot/Display/HistoryFrame.lua (layout/scroll positioning)
Scroll frame, scroll bar, and scroll clip region positioning updated to use GetTopBarOffset() instead of fixed TITLE_BAR_HEIGHT, accounting for filter bar visibility.
History rendering and visible entry iteration
DragonLoot/Display/HistoryFrame.lua (rendering)
RefreshHistory changed to iterate over filtered entries from GetVisibleEntries() and update filter bar count label with #visible/#total.
Filter state restoration and live settings
DragonLoot/Display/HistoryFrame.lua (initialization/settings)
Initialize() restores persisted filter state and applies it to widgets on startup; ApplySettings() dynamically shows/hides filter bar based on barVisible toggle.
Options UI toggle and locale strings
DragonLoot_Options/Tabs/HistoryTab.lua, DragonLoot/Locales/enUS.lua
Adds "Show filter bar" toggle to History Display section reading/writing db.profile.history.filter.barVisible; adds encounter filter labels ("All Encounters", "Encounter", "Search history", "Unknown encounter") and filter bar visibility strings to English locale.
Test infrastructure: instance and time mocking
spec/wow_mock.lua
Extends mocks with time() global, GetItemInfo(link) stub, instance info mocking via M._instanceInfo/M.SetInstance()/GetInstanceInfo() 8-tuple, and AceDB expansion to support per-test character seeding via db.char and M._charSeed.
Configuration migration and schema tests
spec/Config_spec.lua
Updates test helper to accept charSeed parameter; changes assertions to expect schema v5; expands migration coverage to include v0–v5; adds comprehensive v4→v5 history.filter migration suite covering default insertion, entry preservation, corrupt-filter recovery, and v5 no-op behavior.
EncounterListener_Classic and HistoryListener tagging tests
spec/EncounterListener_Classic_spec.lua
New spec file with runtime guard override for Classic testing, test helpers for namespace/loot history setup, and four test cases verifying encounter metadata caching, history entry tagging on matching instanceID, tag removal on instance departure, and nil behavior when no encounter fires.
Filter predicate and persistence tests
spec/HistoryFilter_spec.lua
New spec file with MatchesFilter test suite covering encounter matching, case-insensitive search, malformed itemLink fallback, and UNKNOWN_ENCOUNTER sentinel; filter persistence test suite validating _RestoreFilter/_PersistFilter round-tripping, default search, sentinel preservation, and graceful no-op when db is unset.
Lint globals and schema documentation
.luacheckrc, AGENTS.md
.luacheckrc whitelists Unit*/instance API symbols, legacy UIDropDownMenu_*, EJ_GetEncounterInfo, and C_LootHistory for tests; AGENTS.md expands project structure, documents history.filter schema, and refreshes version-specific API table.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


Suggested labels

display, listeners, core, options, localization

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(history): add encounter and search filter to loot history (#89)' follows Conventional Commit format with valid type 'feat', descriptive scope 'history', and clear summary of the main changes.
Linked Issues check ✅ Passed All coding requirements from #89 are met: encounter filter dropdown added [#89], search filter implemented [#89], filter persistence implemented, Classic encounter tagging with namespace caching [#89], dropdown populated dynamically from history, case-insensitive search matching, and options toggle for filter bar visibility [#89].
Out of Scope Changes check ✅ Passed The PR includes minor out-of-scope documentation updates to AGENTS.md (workspace-root config schema changes) noted as out of scope in PR objectives, though core functionality changes remain focused on the filter feature.

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


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.

@Xerrion Xerrion added A-History Loot history frame and history listeners C-Feature New feature or enhancement C-Usability UX improvement, better defaults, polish D-Complex Multiple files or systems involved labels May 26, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 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.

Inline comments:
In `@DragonLoot/Display/HistoryFrame.lua`:
- Around line 192-193: RestoreFilter currently sets filterState.search =
persisted.search or "" but doesn't ensure persisted.search is a string; if
persisted.search is a non-string, MatchesFilter (which calls
string_lower(search)) can error. Update RestoreFilter to coerce persisted.search
to a string before assignment (e.g., check type(persisted.search) == "string"
and fall back to ""), so filterState.search is always a string; reference
symbols: RestoreFilter, filterState.search, persisted.search, MatchesFilter,
string_lower.
- Line 1171: CreateFrame calls in the Display layer are leaking globals because
they pass explicit names; replace the named frames (e.g., the
DragonLootHistoryEncounterDropdown and DragonLootHistorySearchBox created at the
CreateFrame calls, and other named frames DragonLootHistoryScroll,
DragonLootHistoryScrollBar, DragonLootHistoryFrame) by passing nil as the name
so no global is created. Also harden RestoreFilter(): when assigning
persisted.search into filterState.search inside RestoreFilter(), coerce
non-string values to an empty string (e.g., if type(persisted.search) ~=
"string" then set filterState.search = "") so MatchesFilter() won’t call
string_lower on non-strings. Ensure these changes are applied to the
functions/locations that use CreateFrame for the named widgets and the
RestoreFilter function.

In `@DragonLoot/Listeners/EncounterListener_Classic.lua`:
- Around line 39-57: The ENCOUNTER_START event is registered on the local frame
but never unregistered, so add an explicit unregister lifecycle: expose a
cleanup/stop method (e.g., ns.EncounterListener_Classic.Stop or .Destroy) that
calls frame:UnregisterEvent("ENCOUNTER_START") and clears the OnEvent script
(frame:SetScript("OnEvent", nil)), and ensure any caller of the listener (tests
or init/shutdown code) invokes this when the listener is no longer needed;
update the ns.EncounterListener_Classic table to include the stop/destroy method
and use the existing local frame and event name (frame, ENCOUNTER_START,
ns.EncounterListener_Classic) to perform the unregister.

In `@DragonLoot/Listeners/HistoryListener_Classic.lua`:
- Around line 104-115: The rebuild in RefreshFromAPI in
HistoryListener_Classic.lua is overwriting earlier drops with the latest
ns.currentEncounter; update the logic in the block that sets
entry.encounterID/encounterName so it only assigns encounter metadata when the
entry does not already have it (e.g., if entry.encounterID == nil or
entry.rollID/uniqueDropKey is not present), or persist encounter info keyed by
the stable drop identifier (roll ID or drop key) and read from that map instead
of always applying ns.currentEncounter; modify the code that reads
ns.currentEncounter and writes entry.encounterID/entry.encounterName to check
existing values first (or use a per-drop cache) so RefreshFromAPI no longer
reattributes earlier drops to the latest boss.

In `@spec/wow_mock.lua`:
- Around line 74-78: M.SetInstanceInfo currently calls pairs(info) unguarded and
will error if info is nil; update M.SetInstanceInfo to first validate the input
(e.g., if not info or type(info) ~= "table" then return end) before iterating,
then merge entries into M._instanceInfo as before (referencing the function
M.SetInstanceInfo and the table M._instanceInfo).
- Around line 36-38: The mock GetItemInfo currently "invent"s data by returning
a default quality when link is nil; change GetItemInfo to return nil (no values)
when link is nil so callers see uncached/missing item behavior (reference
function GetItemInfo in spec/wow_mock.lua). Also add a nil-guard to
M.SetInstanceInfo (the function that calls pairs(info)) so it returns early or
handles a nil info argument (e.g., if not info then return end) to avoid calling
pairs on nil.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: 17b740e6-1a63-432b-b412-d194ba05081f

📥 Commits

Reviewing files that changed from the base of the PR and between dd43cfb and 6fd49d3.

📒 Files selected for processing (13)
  • .luacheckrc
  • AGENTS.md
  • DragonLoot/Core/Config.lua
  • DragonLoot/Display/HistoryFrame.lua
  • DragonLoot/DragonLoot.toc
  • DragonLoot/Listeners/EncounterListener_Classic.lua
  • DragonLoot/Listeners/HistoryListener_Classic.lua
  • DragonLoot/Locales/enUS.lua
  • DragonLoot_Options/Tabs/HistoryTab.lua
  • spec/Config_spec.lua
  • spec/EncounterListener_Classic_spec.lua
  • spec/HistoryFilter_spec.lua
  • spec/wow_mock.lua

Comment thread DragonLoot/Display/HistoryFrame.lua Outdated
Comment thread DragonLoot/Display/HistoryFrame.lua
Comment thread DragonLoot/Listeners/EncounterListener_Classic.lua Outdated
Comment thread DragonLoot/Listeners/HistoryListener_Classic.lua
Comment thread spec/wow_mock.lua
Comment thread spec/wow_mock.lua
Xerrion added 2 commits May 26, 2026 18:27
- RestoreFilter: type-guard persisted encounterID (number) and search (string) before assignment so a corrupted SavedVariable cannot poison string.lower at filter time.
- HistoryFrame: drop named globals on the two #89-introduced frames (encounter dropdown, search box); pre-existing scroll/scrollbar/container names retained.
- EncounterListener_Classic: refactor to Initialize/Shutdown lifecycle matching sister listeners; wire into Init.lua MODULES (Retail-safe via nil-guard in loop and file-level WOW_PROJECT_MAINLINE early return).
- spec/wow_mock: GetItemInfo(nil) returns nil to mirror real API; SetInstanceInfo guards non-table input.
…PI (#89)

UIDropDownMenu_RefreshDropDownSize requires self:GetName() for child icon lookup (envTable[self:GetName().."Button"..i.."Icon"]) with no member fallback. It runs on the next OnUpdate tick after UIDropDownMenu_Initialize sets shouldRefresh = true, so an anonymous dropdown throws 'attempt to concatenate a nil value' on every flavor (Retail, MoP Classic, TBC Anniversary). Search box (SearchBoxTemplate) is unaffected and stays anonymous.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-History Loot history frame and history listeners C-Feature New feature or enhancement C-Usability UX improvement, better defaults, polish core D-Complex Multiple files or systems involved display listeners localization options

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Loot history boss/encounter filter dropdown

1 participant