btcpayserver/BTCPayServer/Views/UIWallets/WalletTransactions.cshtml
d11n 22c6468a5d
Wallet: Label filter dropdown (#5802)
Uses a dropdown component for the label filter, which also work on mobile. Closes #5722.
2024-03-12 10:48:37 +01:00

226 lines
9.8 KiB
Text

@using BTCPayServer.Client
@using BTCPayServer.Components
@model ListTransactionsViewModel
@{
var walletId = Context.GetRouteValue("walletId").ToString();
var labelFilter = Context.Request.Query["labelFilter"].ToString();
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePage(WalletsNavPages.Transactions, $"{Model.CryptoCode} Transactions", walletId);
}
@section PageHeadContent {
<script src="~/vendor/tom-select/tom-select.complete.min.js" asp-append-version="true"></script>
<link href="~/vendor/tom-select/tom-select.bootstrap5.min.css" asp-append-version="true" rel="stylesheet">
<style>
.smMaxWidth {
max-width: 125px;
}
@@media (min-width: 990px) {
.smMaxWidth {
max-width: 200px;
}
}
/* pull actions area, so that it is besides the search form */
@@media (min-width: 1200px) {
#Filter + #Dropdowns {
margin-top: -4rem;
float: right;
}
#Filter + #Dropdowns #Actions {
order: 1;
}
}
.unconf > * {
opacity: 0.5;
}
#LoadingIndicator {
margin-bottom: 1.5rem;
}
</style>
}
@section PageFootContent {
@*Without async, somehow selenium do not manage to click on links in this page*@
<script src="~/modal/btcpay.js" asp-append-version="true" async></script>
@* Custom Range Modal *@
<script>
const $actions = document.getElementById('ListActions');
const $transactions = document.getElementById('WalletTransactions');
const $list = document.getElementById('WalletTransactionsList');
const $dropdowns = document.getElementById('Dropdowns');
const $indicator = document.getElementById('LoadingIndicator');
delegate('click', '#GoToTop', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
if ($actions && $actions.offsetTop - window.innerHeight > 0) {
document.getElementById('GoToTop').classList.remove('d-none');
}
const count = @Safe.Json(Model.Count);
const skipInitial = @Safe.Json(Model.Skip);
const loadMoreUrl = @Safe.Json(Url.Action("WalletTransactions", new { walletId, labelFilter, skip = Model.Skip, count = Model.Count, loadTransactions = true }));
// The next time we load transactions, skip will become 0
let skip = @Safe.Json(Model.Skip) - count;
async function loadMoreTransactions() {
$indicator.classList.remove('d-none');
const skipNext = skip + count;
const url = loadMoreUrl.replace(`skip=${skipInitial}`, `skip=${skipNext}`)
const response = await fetch(url, {
headers: {
'Accept': 'text/html',
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const html = await response.text();
const responseEmpty = html.trim() === '';
$list.insertAdjacentHTML('beforeend', html);
skip = skipNext;
if (responseEmpty) {
// in case the response html was empty, remove the observer and stop loading
observer.unobserve($actions);
}
if (!$transactions.dataset.loaded) {
$transactions.dataset.loaded = 'true';
// replace table and dropdowns if initial response was empty
if (responseEmpty) {
$dropdowns.remove();
$transactions.innerHTML = '<div class="text-secondary" data-loaded="true">There are no transactions yet.</div>';
}
}
}
$indicator.classList.add('d-none');
formatDateTimes(document.querySelector('#WalletTransactions .switch-time-format').dataset.mode);
initLabelManagers();
}
const observer = new IntersectionObserver(async entries => {
const { isIntersecting } = entries[0];
if (isIntersecting) {
await loadMoreTransactions();
}
}, { rootMargin: '128px' });
// the actions div marks the end of the list table
observer.observe($actions);
</script>
}
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3" id="Dropdowns">
@if (Model.Labels.Any())
{
<div class="btn-group" id="Filter">
<button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
@if (string.IsNullOrEmpty(labelFilter))
{
<span class="text-secondary">Filter by label</span>
}
else
{
<span class="text-secondary">Label:</span>
<span>@labelFilter</span>
}
</button>
<ul class="dropdown-menu">
@foreach (var label in Model.Labels)
{
<li><a asp-route-labelFilter="@label.Text" class="dropdown-item transaction-label-text@(labelFilter == label.Text ? " active" : string.Empty)" style="--btcpay-dropdown-link-active-bg:@label.Color;--btcpay-dropdown-link-active-color:@label.TextColor;">@label.Text</a></li>
}
@if (!string.IsNullOrEmpty(labelFilter))
{
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" asp-route-labelFilter="">Clear filter</a></li>
}
</ul>
</div>
}
<div class="dropdown d-inline-flex align-items-center gap-3 ms-auto" id="Export">
<button class="btn btn-secondary dropdown-toggle" type="button" id="ExportDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Export
</button>
<div class="dropdown-menu" aria-labelledby="ExportDropdownToggle">
<a asp-action="Export" asp-route-walletId="@walletId" asp-route-format="csv" asp-route-labelFilter="@labelFilter" class="dropdown-item export-link" target="_blank" id="ExportCSV">CSV</a>
<a asp-action="Export" asp-route-walletId="@walletId" asp-route-format="json" asp-route-labelFilter="@labelFilter" class="dropdown-item export-link" target="_blank" id="ExportJSON">JSON</a>
<a asp-action="Export" asp-route-walletId="@walletId" asp-route-format="bip329" asp-route-labelFilter="@labelFilter" class="dropdown-item export-link" target="_blank" id="ExportBIP329">Wallet Labels (BIP-329)</a>
</div>
</div>
</div>
<div style="clear:both"></div>
<div id="WalletTransactions" class="table-responsive-md">
<table class="table table-hover mass-action">
<thead class="mass-action-head">
<tr>
<th class="only-for-js mass-action-select-col">
<input type="checkbox" class="form-check-input mass-action-select-all" />
</th>
<th class="date-col">
<div class="d-flex align-items-center gap-1">
Date
<button type="button" class="btn btn-link p-0 fa fa-clock-o switch-time-format only-for-js" title="Switch date format"></button>
</div>
</th>
<th class="text-start">Label</th>
<th>Transaction Id</th>
<th class="amount-col">Amount</th>
<th></th>
</tr>
</thead>
<thead class="mass-action-actions">
<tr>
<th class="only-for-js mass-action-select-col">
<input type="checkbox" class="form-check-input mass-action-select-all" />
</th>
<th colspan="5">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3">
<div>
<strong class="mass-action-selected-count">0</strong>
selected
</div>
<form id="WalletActions" method="post" asp-action="WalletActions" asp-route-walletId="@walletId" permission="@Policies.CanModifyStoreSettings" class="d-inline-flex align-items-center gap-3">
<button id="BumpFee" name="command" type="submit" value="cpfp" class="btn btn-link">
<vc:icon symbol="send" />
Bump fee
</button>
</form>
</div>
</th>
</tr>
</thead>
<tbody id="WalletTransactionsList">
<partial name="_WalletTransactionsList" model="Model" />
</tbody>
</table>
</div>
<noscript>
<vc:pager view-model="Model"/>
</noscript>
<div class="text-center only-for-js d-none" id="LoadingIndicator">
<div class="spinner-border spinner-border-sm text-secondary ms-2" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div class="d-flex flex-wrap align-items-center justify-content-center gap-3 mb-5 only-for-js" id="ListActions">
<button type="button" class="btn btn-secondary d-none" id="GoToTop">Go to top</button>
</div>
<p class="mt-4 mb-0">
If BTCPay Server shows you an invalid balance, <a asp-action="WalletRescan" asp-route-walletId="@Context.GetRouteValue("walletId")">rescan your wallet</a>.
<br />
If some transactions appear in BTCPay Server, but are missing in another wallet, <a href="https://docs.btcpayserver.org/FAQ/Wallet/#missing-payments-in-my-software-or-hardware-wallet" rel="noreferrer noopener">follow these instructions</a>.
</p>