feat(export): CSV export scope — All / current filter / date range

Finishes the transactions-export partial. The dialog now offers three scopes:
- All transactions (unchanged default; exported with no copy).
- Current History filter — the Transactions tab publishes a TxFilterSnapshot of
  the visible legs (a merged autoshield row contributes both its send+receive
  legs, so the CSV stays a per-leg record like an unfiltered export); the radio
  is disabled with an explanation when no filter is active, and a snapshot that
  predates the current tx list (revision mismatch) is refused rather than
  silently exporting everything.
- Date range (YYYY-MM-DD, inclusive end-of-day; pending/dateless rows excluded
  with a note; invalid/reversed dates blocked inline).
The live count reflects what will actually be exported. formatTransactionsCsv
and the atomic write are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 00:19:24 -05:00
co-authored by Claude Opus 4.8
parent 8961cbdfc9
commit 8b46ead72a
4 changed files with 161 additions and 5 deletions
+34
View File
@@ -36,6 +36,11 @@ namespace ui {
using namespace material;
// Latest History-tab filter snapshot, published from the filtered-list rebuild below. Used by the
// transactions CSV export to offer "export only the current filter".
static TxFilterSnapshot s_filter_snapshot;
const TxFilterSnapshot& GetTransactionsFilterSnapshot() { return s_filter_snapshot; }
static std::string TrId(const char* key, const char* id) {
return std::string(TR(key)) + "##" + id;
}
@@ -565,6 +570,35 @@ void RenderTransactionsTab(App* app)
s_filtered_search = search_str;
s_filtered_chat_rev = chatRev;
s_filtered_valid = true;
// Publish a snapshot of the visible set for "export current filter". A merged autoshield row
// contributes BOTH original legs, so the CSV stays a per-leg record like an unfiltered export.
s_filter_snapshot.indices.clear();
s_filter_snapshot.indices.reserve(s_filtered_cache.size() * 2);
for (size_t fi : s_filtered_cache) {
const auto& dtx = display_txns[fi];
if (dtx.is_shield) {
if (dtx.send_idx >= 0) s_filter_snapshot.indices.push_back(static_cast<size_t>(dtx.send_idx));
if (dtx.recv_idx >= 0) s_filter_snapshot.indices.push_back(static_cast<size_t>(dtx.recv_idx));
} else if (dtx.orig_idx >= 0) {
s_filter_snapshot.indices.push_back(static_cast<size_t>(dtx.orig_idx));
}
}
std::sort(s_filter_snapshot.indices.begin(), s_filter_snapshot.indices.end());
s_filter_snapshot.active = (type_filter != 0 || !search_str.empty());
s_filter_snapshot.txRevision = contentKey;
s_filter_snapshot.valid = true;
{
const char* typeNames[] = { "", TR("sent_filter"), TR("received_filter"),
TR("mined_filter"), TR("chat_filter") };
std::string desc = (type_filter >= 1 && type_filter <= 4) ? typeNames[type_filter] : "";
if (!search_str.empty()) {
if (!desc.empty()) desc += ", ";
desc += "\"" + search_str + "\"";
}
if (desc.empty()) desc = TR("all_filter");
s_filter_snapshot.description = desc;
}
}
const std::vector<size_t>& filtered_indices = s_filtered_cache;
filtered_count = static_cast<int>(filtered_indices.size());