Unify list views (#5399)

This commit is contained in:
d11n 2023-11-02 08:12:28 +01:00 committed by GitHub
parent 6acc545b66
commit 27c22d5e33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 581 additions and 504 deletions

View file

@ -1457,7 +1457,7 @@ namespace BTCPayServer.Tests
[Trait("Integration", "Integration")]
public async Task CanUseWebhooks()
{
void AssertHook(FakeServer fakeServer, Client.Models.StoreWebhookData hook)
void AssertHook(FakeServer fakeServer, StoreWebhookData hook)
{
Assert.True(hook.Enabled);
Assert.True(hook.AuthorizedEvents.Everything);

View file

@ -196,7 +196,6 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
Assert.Equal(4, new SelectElement(s.Driver.FindElement(By.Id("FormId"))).Options.Count);
}
[Fact(Timeout = TestTimeout)]
@ -216,8 +215,7 @@ namespace BTCPayServer.Tests
s.GoToInvoices(s.StoreId);
}
// Let's CPFP from the invoices page
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.SetCheckbox(By.CssSelector(".mass-action-select-all"), true);
s.Driver.FindElement(By.Id("BumpFee")).Click();
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
s.FindAlertMessage();
@ -225,16 +223,14 @@ namespace BTCPayServer.Tests
// CPFP again should fail because all invoices got bumped
s.GoToInvoices();
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.SetCheckbox(By.CssSelector(".mass-action-select-all"), true);
s.Driver.FindElement(By.Id("BumpFee")).Click();
Assert.Contains($"/stores/{s.StoreId}/invoices", s.Driver.Url);
Assert.Contains("any UTXO available", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error).Text);
// But we should be able to bump from the wallet's page
s.GoToWallet(navPages: WalletsNavPages.Transactions);
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.SetCheckbox(By.CssSelector(".mass-action-select-all"), true);
s.Driver.FindElement(By.Id("BumpFee")).Click();
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
Assert.Contains($"/wallets/{s.WalletId}", s.Driver.Url);
@ -730,9 +726,8 @@ namespace BTCPayServer.Tests
Assert.Contains(invoiceId, s.Driver.PageSource);
// archive via list
s.Driver.FindElement(By.CssSelector($".selector[value=\"{invoiceId}\"]")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownArchive")).Click();
s.Driver.FindElement(By.CssSelector($".mass-action-select[value=\"{invoiceId}\"]")).Click();
s.Driver.FindElement(By.Id("ArchiveSelected")).Click();
Assert.Contains("1 invoice archived", s.FindAlertMessage().Text);
Assert.DoesNotContain(invoiceId, s.Driver.PageSource);
@ -740,9 +735,8 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("StatusOptionsToggle")).Click();
s.Driver.FindElement(By.Id("StatusOptionsIncludeArchived")).Click();
Assert.Contains(invoiceId, s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector($".selector[value=\"{invoiceId}\"]")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownUnarchive")).Click();
s.Driver.FindElement(By.CssSelector($".mass-action-select[value=\"{invoiceId}\"]")).Click();
s.Driver.FindElement(By.Id("UnarchiveSelected")).Click();
Assert.Contains("1 invoice unarchived", s.FindAlertMessage().Text);
Assert.Contains(invoiceId, s.Driver.PageSource);
@ -1377,7 +1371,7 @@ namespace BTCPayServer.Tests
TestLogs.LogInformation("Let's try to update one of them");
s.Driver.FindElement(By.LinkText("Modify")).Click();
using FakeServer server = new FakeServer();
using var server = new FakeServer();
await server.Start();
s.Driver.FindElement(By.Name("PayloadUrl")).Clear();
s.Driver.FindElement(By.Name("PayloadUrl")).SendKeys(server.ServerUri.AbsoluteUri);
@ -1419,7 +1413,7 @@ namespace BTCPayServer.Tests
server.Done();
TestLogs.LogInformation("Let's make a failed event");
s.CreateInvoice();
var invoiceId = s.CreateInvoice();
request = await server.GetNextRequest();
request.Response.StatusCode = 404;
server.Done();
@ -1444,7 +1438,7 @@ namespace BTCPayServer.Tests
CanBrowseContent(s);
s.GoToInvoices();
s.Driver.FindElement(By.LinkText("Details")).Click();
s.Driver.FindElement(By.LinkText(invoiceId)).Click();
CanBrowseContent(s);
var element = s.Driver.FindElement(By.ClassName("redeliver"));
element.Click();
@ -1699,9 +1693,10 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("CancelWizard")).Click();
Assert.Equal(settingsUri.ToString(), s.Driver.Url);
// Transactions list contains export and action, ensure functions are present.
// Transactions list contains export, ensure functions are present.
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id("BumpFee"));
// JSON export
@ -1859,8 +1854,7 @@ namespace BTCPayServer.Tests
payouts[0].Click();
Assert.NotEmpty(s.Driver.FindElements(By.ClassName("payout")));
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
s.Driver.FindElement(By.Id("SignTransaction")).Click();
@ -1935,8 +1929,7 @@ namespace BTCPayServer.Tests
Assert.Contains(PayoutState.AwaitingApproval.GetStateString(), s.Driver.PageSource);
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve")).Click();
s.FindAlertMessage();
var tx = await s.Server.ExplorerNode.SendToAddressAsync(address, Money.FromUnit(0.001m, MoneyUnit.BTC));
@ -1945,8 +1938,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-view")).Click();
Assert.Contains(PayoutState.AwaitingPayment.GetStateString(), s.Driver.PageSource);
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-actions")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-mark-paid")).Click();
s.FindAlertMessage();
@ -2006,8 +1998,7 @@ namespace BTCPayServer.Tests
s.GoToStore(newStore.storeId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
Assert.Contains(bolt, s.Driver.PageSource);
Assert.Contains($"{payoutAmount.ToString()} BTC", s.Driver.PageSource);
@ -2023,8 +2014,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-view")).Click();
Assert.Contains(bolt, s.Driver.PageSource);
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-actions")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-mark-paid")).Click();
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
@ -2553,8 +2543,7 @@ namespace BTCPayServer.Tests
payouts[0].Click();
s.Driver.FindElement(By.Id("BTC_LightningLike-view")).Click();
Assert.NotEmpty(s.Driver.FindElements(By.ClassName("payout")));
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
Assert.Contains(lnurl, s.Driver.PageSource);

View file

@ -634,8 +634,14 @@ namespace BTCPayServer.Controllers
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewInvoices)]
public async Task<IActionResult> MassAction(string command, string[] selectedItems, string? storeId = null)
{
if (selectedItems != null)
IActionResult NotSupported(string err)
{
TempData[WellKnownTempData.ErrorMessage] = err;
return RedirectToAction(nameof(ListInvoices), new { storeId });
}
if (selectedItems.Length == 0)
return NotSupported("No invoice has been selected");
switch (command)
{
case "archive":
@ -648,15 +654,8 @@ namespace BTCPayServer.Controllers
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} unarchived.";
break;
case "cpfp":
if (selectedItems.Length == 0)
return NotSupported("No invoice has been selected");
var network = _NetworkProvider.DefaultNetwork;
var explorer = _ExplorerClients.GetExplorerClient(network);
IActionResult NotSupported(string err)
{
TempData[WellKnownTempData.ErrorMessage] = err;
return RedirectToAction(nameof(ListInvoices), new { storeId });
}
if (explorer is null)
return NotSupported("This feature is only available to BTC wallets");
if (!GetCurrentStore().HasPermission(GetUserId(), Policies.CanModifyStoreSettings))
@ -686,7 +685,6 @@ namespace BTCPayServer.Controllers
FormParameters = parameters,
});
}
}
return RedirectToAction(nameof(ListInvoices), new { storeId });
}

View file

@ -26,7 +26,6 @@
}
<form method="post">
<div class="sticky-header-setup"></div>
<div class="sticky-header d-sm-flex align-items-center justify-content-between">
<h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-sm-0">

View file

@ -44,7 +44,6 @@
<h3 class="mb-0">@ViewData["Title"]</h3>
<a asp-action="CreateOrEditRole" asp-route-storeId="@storeId" class="btn btn-primary" role="button" id="CreateRole" asp-route-role="create"
asp-controller="@controller">
<span class="fa fa-plus"></span>
Add Role
</a>
</div>

View file

@ -13,7 +13,6 @@
}
<form method="post">
<div class="sticky-header-setup"></div>
<div class="sticky-header d-sm-flex align-items-center justify-content-between">
<h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-sm-0">

View file

@ -23,7 +23,7 @@
</a>
</small>
</h2>
<a asp-action="CreateApp" asp-route-storeId="@Context.GetStoreData().Id" class="btn btn-primary mt-3 mt-sm-0" role="button" id="CreateNewApp"><span class="fa fa-plus"></span> Create a new app</a>
<a asp-action="CreateApp" asp-route-storeId="@Context.GetStoreData().Id" class="btn btn-primary mt-3 mt-sm-0" role="button" id="CreateNewApp">Create a new app</a>
</div>
<div class="row">

View file

@ -24,7 +24,6 @@
</style>
<div id="custodianAccountView" v-cloak>
<div class="sticky-header-setup"></div>
<div class="sticky-header d-flex flex-wrap gap-3 align-items-center justify-content-between">
<h2 class="mb-0">
@ViewData["Title"]

View file

@ -18,7 +18,6 @@
</a>
</h3>
<a asp-action="Create" asp-route-storeId="@storeId" class="btn btn-primary mt-3 mt-sm-0" role="button" id="CreateForm">
<span class="fa fa-plus"></span>
Create Form
</a>
</div>

View file

@ -29,7 +29,6 @@
}
<form asp-action="CreateInvoice" method="post" id="create-invoice-form">
<div class="sticky-header-setup"></div>
<div class="sticky-header d-flex align-items-center justify-content-between">
<h2 class="mb-0">@ViewData["Title"]</h2>
<input type="submit" value="Create" class="btn btn-primary" id="Create" />

View file

@ -169,7 +169,6 @@
</div>
}
<div class="sticky-header-setup"></div>
<div class="sticky-header d-flex flex-wrap gap-3 align-items-center justify-content-between">
<h2 class="mb-0 text-break">@ViewData["Title"]</h2>
<div class="d-flex flex-wrap gap-3 d-print-none">

View file

@ -1,13 +1,10 @@
@using BTCPayServer.Client
@using BTCPayServer.Client.Models
@using BTCPayServer.Services
@using SetPasswordViewModel = BTCPayServer.Models.ManageViewModels.SetPasswordViewModel
@inject DisplayFormatter DisplayFormatter
@inject ReportService ReportService
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
@model InvoicesModel
@{
var reportNames = ReportService.ReportProviders.Select(p => p.Value.Name).OrderBy(c => c).ToArray();
ViewData.SetActivePage(InvoiceNavPages.Index, "Invoices");
var statusFilterCount = CountArrayFilter("status") + CountArrayFilter("exceptionstatus") + (HasBooleanFilter("includearchived") ? 1 : 0) + (HasBooleanFilter("unusual") ? 1 : 0);
var hasDateFilter = HasArrayFilter("startdate") || HasArrayFilter("enddate");
@ -32,8 +29,11 @@
@section PageHeadContent
{
<style>
.invoiceId-col {
min-width: 8rem;
}
.invoice-details-row > td {
padding: 1.5rem .5rem 0 2.65rem;
padding: 1.5rem 1rem 0 2.65rem;
}
.dropdown > .btn {
min-width: 7rem;
@ -54,11 +54,7 @@
@* Custom Range Modal *@
<script>
delegate('click', '#selectAllCheckbox', e => {
document.querySelectorAll(".selector").forEach(checkbox => {
checkbox.checked = e.target.checked;
});
});
const timezoneOffset = new Date().getTimezoneOffset();
delegate('click', '.showInvoice', e => {
e.preventDefault();
@ -99,41 +95,11 @@
var str = newDate.toLocaleDateString() + " " + newDate.toLocaleTimeString();
return str;
}
document.addEventListener("DOMContentLoaded", function () {
var timezoneOffset = new Date().getTimezoneOffset();
$("#invoices")
.on("click", ".invoice-row .invoice-details-toggle", function (e) {
e.preventDefault();
e.stopPropagation(true);
const $btnToggle = $(e.currentTarget);
const $invoiceRow = $btnToggle.parents(".invoice-row");
const $detailsRow = $invoiceRow.next(".invoice-details-row");
$detailsRow.toggle(0, function () {
const $icon = $btnToggle.children().first();
if ($(this).is(':visible')) {
$icon.removeClass('fa-angle-double-down').addClass('fa-angle-double-up');
} else {
$icon.removeClass('fa-angle-double-up').addClass('fa-angle-double-down');
}
});
})
.on("click", ".invoice-row", function (e) {
const $invoiceRow = $(e.currentTarget);
if ($(e.target).is("td")) {
$invoiceRow.find(".selector").trigger("click");
}
});
});
</script>
}
@Html.HiddenFor(a => a.Count)
<div class="sticky-header-setup"></div>
<div class="sticky-header d-sm-flex align-items-center justify-content-between">
<h2 class="mb-0">
@ViewData["Title"]
@ -142,7 +108,6 @@
</a>
</h2>
<a id="CreateNewInvoice" asp-action="CreateInvoice" asp-route-storeId="@Model.StoreId" asp-route-searchTerm="@Model.SearchTerm" class="btn btn-primary mt-3 mt-sm-0">
<span class="fa fa-plus"></span>
Create Invoice
</a>
</div>
@ -169,7 +134,6 @@
</div>
<partial name="_StatusMessage" />
<partial name="InvoiceStatusChangePartial" />
@* Custom Range Modal *@
@ -305,97 +269,100 @@
@if (Model.Invoices.Any())
{
<form method="post" id="MassAction" asp-action="MassAction" class="">
<div class="d-inline-flex align-items-center pb-2 float-xxl-end mb-2 gap-3">
<form method="post" asp-action="MassAction">
<input type="hidden" name="storeId" value="@Model.StoreId" />
<div class="dropdown order-xxl-1">
<button class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions
</button>
<div class="dropdown-menu dropdown-menu-xxl-end" aria-labelledby="ActionsDropdownToggle">
<button type="submit" class="dropdown-item" name="command" value="archive" id="ActionsDropdownArchive">Archive</button>
@if (HasBooleanFilter("includearchived"))
{
<button type="submit" asp-action="MassAction" class="dropdown-item" name="command" value="unarchive" id="ActionsDropdownUnarchive">Unarchive</button>
}
<button id="BumpFee" type="submit" permission="@Policies.CanModifyStoreSettings" class="dropdown-item" name="command" value="cpfp">Bump fee</button>
</div>
</div>
<div class="dropdown d-inline-flex align-items-center gap-3">
<button class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret order-xxl-1" type="button" id="ExportDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Reports
</button>
<div class="dropdown-menu" aria-labelledby="ExportDropdownToggle">
@foreach (var report in reportNames)
{
<a asp-controller="UIReports" asp-action="StoreReports" asp-route-viewName="@report" asp-route-storeId="@Model.StoreId" class="dropdown-item export-link">@report</a>
}
</div>
<a href="https://docs.btcpayserver.org/Accounting/" target="_blank" rel="noreferrer noopener" title="More information...">
<vc:icon symbol="info" />
</a>
</div>
</div>
<div style="clear:both"></div>
<div class="table-responsive">
<table id="invoices" class="table table-hover">
<thead>
<table id="invoices" class="table table-hover mass-action">
<thead class="mass-action-head">
<tr>
<th style="width:2rem;" class="only-for-js">
<input id="selectAllCheckbox" type="checkbox" class="form-check-input" />
<th class="w-150px">
<th class="mass-action-select-col only-for-js">
<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-nowrap">Order Id</th>
<th class="text-nowrap">Invoice Id</th>
<th class="text-nowrap">Order Id</th>
<th>Status</th>
<th class="text-end">Amount</th>
<th class="text-end">Actions</th>
<th class="amount-col">Amount</th>
<th></th>
</tr>
</thead>
<thead class="mass-action-actions">
<tr>
<th class="mass-action-select-col only-for-js">
<input type="checkbox" class="form-check-input mass-action-select-all" />
</th>
<th colspan="6">
<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>
<div class="d-inline-flex align-items-center gap-3">
<button type="submit" name="command" value="archive" id="ArchiveSelected" class="btn btn-link">
<vc:icon symbol="archive" />
Archive
</button>
@if (HasBooleanFilter("includearchived"))
{
<button type="submit" name="command" value="unarchive" id="UnarchiveSelected" class="btn btn-link">
<vc:icon symbol="archive" />
Unarchive
</button>
}
<button type="submit" name="command" value="cpfp" id="BumpFee" class="btn btn-link">
<vc:icon symbol="send" />
Bump fee
</button>
</div>
</div>
</th>
</tr>
</thead>
<tbody>
@foreach (var invoice in Model.Invoices)
{
<tr id="invoice_@invoice.InvoiceId" class="invoice-row">
<td class="only-for-js">
<input name="selectedItems" type="checkbox" class="selector form-check-input" value="@invoice.InvoiceId" />
var detailsId = $"invoice_details_{invoice.InvoiceId}";
<tr id="invoice_@invoice.InvoiceId" class="mass-action-row">
<td class="only-for-js align-middle">
<input name="selectedItems" type="checkbox" class="form-check-input mass-action-select" value="@invoice.InvoiceId" />
</td>
<td>@invoice.Date.ToBrowserDate()</td>
<td>
<td class="align-middle date-col">@invoice.Date.ToBrowserDate()</td>
<td class="text-break align-middle invoiceId-col">
<a asp-action="Invoice" class="invoice-details-link" asp-route-invoiceId="@invoice.InvoiceId">@invoice.InvoiceId</a>
</td>
<td class="align-middle">
<vc:truncate-center text="@invoice.OrderId" link="@invoice.RedirectUrl" classes="truncate-center-id" />
</td>
<td class="text-break">@invoice.InvoiceId</td>
<td>
<td class="align-middle">
<div class="d-inline-flex align-items-center gap-2">
<vc:invoice-status state="invoice.Status" payments="invoice.Details.Payments" invoice-id="@invoice.InvoiceId"
is-archived="invoice.Details.Archived" has-refund="invoice.HasRefund" />
</td>
<td class="text-end text-nowrap">
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
</td>
<td class="text-end text-nowrap">
@if (invoice.ShowCheckout)
{
<span>
<span>&nbsp;</span>
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId" class="invoice-checkout-link" id="invoice-checkout-@invoice.InvoiceId">Checkout</a>
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId" class="showInvoice only-for-js" data-invoice-id="@invoice.InvoiceId">[^]</a>
@if (!invoice.CanMarkStatus)
{
<span>-</span>
}
</span>
}
&nbsp;
<a asp-action="Invoice" class="invoice-details-link" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
<a class="only-for-js invoice-details-toggle" href="#">
<span title="Invoice Details Toggle" class="fa fa-1x fa-angle-double-down"></span>
</a>
</div>
</td>
<td class="align-middle amount-col">
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
</td>
<td class="align-middle text-end">
<div class="d-inline-flex align-items-center gap-2">
<button class="accordion-button collapsed only-for-js ms-0 d-inline-block" type="button" data-bs-toggle="collapse" data-bs-target="#@detailsId" aria-expanded="false" aria-controls="@detailsId">
<vc:icon symbol="caret-down" />
</button>
</div>
</td>
</tr>
<tr id="invoice_details_@invoice.InvoiceId" class="invoice-details-row" style="display:none;">
<td colspan="99" class="border-top-0">
<tr id="@detailsId" class="invoice-details-row collapse">
<td colspan="7" class="border-top-0">
@* Leaving this as partial because it abstracts complexity of Invoice Payments *@
<partial name="ListInvoicesPaymentsPartial" model="(invoice.Details, true)" />
</td>

View file

@ -40,7 +40,6 @@
<div class="d-flex align-items-center justify-content-between mb-2">
<h2 class="mb-0">@ViewData["Title"]</h2>
<a data-bs-toggle="collapse" data-bs-target="#AddAddress" class="btn btn-primary" role="button">
<span class="fa fa-plus"></span>
Add Address
</a>
</div>

View file

@ -12,7 +12,6 @@
<div class="d-flex align-items-center justify-content-between mb-3">
<h3 class="mb-0">@ViewData["Title"]</h3>
<a class="btn btn-primary" asp-action="AddApiKey" id="AddApiKey">
<span class="fa fa-plus"></span>
Generate Key
</a>
</div>

View file

@ -135,7 +135,6 @@
<input type="text" class="form-control" name="Name" placeholder="Security device name"/>
<select asp-items="@Html.GetEnumSelectList<Fido2Credential.CredentialType>()" class="form-select w-auto" name="type"></select>
<button id="btn-add" type="submit" class="btn btn-primary">
<span class="fa fa-plus"></span>
Add
</button>
</div>

View file

@ -1,6 +1,5 @@
@inject SignInManager<ApplicationUser> SignInManager
<div class="sticky-header-setup"></div>
<div class="sticky-header mb-l">
<h2 class="mt-1 mb-2 mb-lg-4">Account Settings</h2>
<nav id="SectionNav">

View file

@ -14,61 +14,75 @@
@if (Model.Items.Count > 0)
{
<form method="post" asp-action="MassAction">
<div class="row button-row">
<div class="col-lg-6">
<span class="dropdown" style="display:none;" id="MassAction">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions
</button>
<div class="dropdown-menu">
<button type="submit" class="dropdown-item" name="command" value="mark-seen"><i class="fa fa-eye"></i> Mark seen</button>
<button type="submit" class="dropdown-item" name="command" value="mark-unseen"><i class="fa fa-eye-slash"></i> Mark unseen</button>
<button type="submit" class="dropdown-item" name="command" value="delete"><i class="fa fa-trash-o"></i> Delete</button>
</div>
</span>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<table class="table table-hover table-responsive-md">
<thead>
<tr>
<th style="width:30px" class="only-for-js">
@if (Model.Items.Count > 0)
@if (Model.Items.Any())
{
<input name="selectedItems" id="selectAllCheckbox" type="checkbox" class="form-check-input" />
}
<div class="table-responsive-md">
<table class="table table-hover mass-action">
<thead class="mass-action-head">
<tr>
<th class="mass-action-select-col only-for-js">
<input name="selectedItems" type="checkbox" class="form-check-input mass-action-select-all" />
</th>
<th class="w-150px">
<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" title="Switch date format"></button>
</div>
</th>
<th>Message</th>
<th class="text-end">Actions</th>
<th></th>
</tr>
</thead>
<thead class="mass-action-actions">
<tr>
<th class="mass-action-select-col only-for-js">
<input type="checkbox" class="form-check-input mass-action-select-all" />
</th>
<th colspan="6">
<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>
<div class="d-inline-flex align-items-center gap-3">
<button type="submit" name="command" value="mark-seen" class="btn btn-link gap-1">
<i class="fa fa-eye"></i>
Mark seen
</button>
<button type="submit" name="command" value="mark-unseen" class="btn btn-link gap-1">
<i class="fa fa-eye-slash"></i>
Mark unseen
</button>
<button type="submit" name="command" value="delete" class="btn btn-link gap-1">
<i class="fa fa-trash-o"></i>
Delete
</button>
</div>
</div>
</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr data-guid="@item.Id" class="notification-row @(item.Seen ? "seen" : "")">
<td class="only-for-js">
<input name="selectedItems" type="checkbox" class="selector form-check-input" value="@item.Id" />
<tr data-guid="@item.Id" class="notification-row mass-action-row @(item.Seen ? "seen" : "")">
<td class="only-for-js mass-action-select-col">
<input name="selectedItems" type="checkbox" class="form-check-input mass-action-select" value="@item.Id" />
</td>
<td class="toggleRowCheckbox">@item.Created.ToBrowserDate()</td>
<td class="toggleRowCheckbox">
<td class="date-col">@item.Created.ToBrowserDate()</td>
<td>
@item.Body
</td>
<td class="text-end fw-normal">
@if (!String.IsNullOrEmpty(item.ActionLink))
<td class="text-end">
<div class="d-inline-flex align-items-center gap-3">
<button class="btn btn-link p-0 btn-toggle-seen" type="submit" name="command" value="flip-individual:@(item.Id)">
<span>Mark</span>&nbsp;<span class="seen-text"></span>
</button>
@if (!string.IsNullOrEmpty(item.ActionLink))
{
<a href="@item.ActionLink" class="btn btn-link p-0" rel="noreferrer noopener">Details</a>
<span class="d-none d-md-inline-block"> - </span>
}
<button class="btn btn-link p-0 btn-toggle-seen" type="submit" name="command" value="flip-individual:@(item.Id)">
<span>Mark&nbsp;</span><span class="seen-text"></span>
</button>
</div>
</td>
</tr>
}
@ -76,9 +90,8 @@
</table>
<vc:pager view-model="Model" />
</div>
</div>
}
</form>
}
else
@ -113,19 +126,6 @@ else
@section PageFootContent {
<script type="text/javascript">
delegate('click', '#selectAllCheckbox', e => {
document.querySelectorAll('.notification-row .selector').forEach(checkbox => {
checkbox.checked = e.target.checked;
});
updateSelectors();
});
delegate('click', '.toggleRowCheckbox', e => {
const input = $(e.target).parents(".notification-row").find(".selector");
input.prop('checked', !input.prop("checked"));
updateSelectors();
})
delegate('click', '.btn-toggle-seen', e => {
const row = $(e.target).parents(".notification-row").toggleClass("loading");
const guid = row.data("guid");
@ -135,20 +135,5 @@ else
});
return false;
})
document.addEventListener("DOMContentLoaded", function () {
$(".selector").change(updateSelectors);
updateSelectors();
});
function updateSelectors() {
var count = $(".selector:checked").length;
if (count > 0) {
$("#MassAction").children().eq(0).text("Batch Action (" + count + ")");
$("#MassAction").show();
} else {
$("#MassAction").hide();
}
}
</script>
}

View file

@ -23,7 +23,6 @@
}
<form method="post" action="@Url.Action("EditPaymentRequest", "UIPaymentRequest", new { storeId = Model.StoreId, payReqId = Model.Id }, Context.Request.Scheme)">
<div class="sticky-header-setup"></div>
<div class="sticky-header d-sm-flex align-items-center justify-content-between">
<h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-sm-0">

View file

@ -22,7 +22,6 @@
Model.Search.ContainsFilter(key) && Model.Search.GetFilterBool(key) is true;
}
<div class="sticky-header-setup"></div>
<div class="sticky-header d-sm-flex align-items-center justify-content-between">
<h2 class="mb-0">
@ViewData["Title"]
@ -50,7 +49,7 @@
<partial name="_StatusMessage" />
<form asp-action="GetPaymentRequests" method="get" class="d-flex flex-wrap flex-sm-nowrap align-items-center gap-3 mb-4 col-lg-9 col-xl-8 col-xxl-6">
<form asp-action="GetPaymentRequests" method="get" class="d-flex flex-wrap flex-sm-nowrap align-items-center gap-3 mb-4 col-xxl-8">
<input type="hidden" asp-for="Count" />
<input type="hidden" asp-for="TimezoneOffset" />
<input asp-for="SearchTerm" type="hidden" value="@Model.Search.WithoutSearchText()"/>
@ -78,43 +77,44 @@
@if (Model.Items.Any())
{
<table class="table table-hover table-responsive-md" id="tableId">
<div class="table-responsive-md">
<table class="table table-hover">
<thead>
<tr>
<th>Title</th>
<th class="w-150px">
<th class="date-col">
<div class="d-flex align-items-center gap-1">
Expiry
<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>Price</th>
<th>Status</th>
<th class="text-end">Actions</th>
<th class="amount-col">Amount</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<tr class="mass-action-row">
<td>
<a asp-action="ViewPaymentRequest" asp-route-payReqId="@item.Id" id="PaymentRequest-@item.Id">@item.Title</a>
<a asp-action="EditPaymentRequest" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id" id="Edit-@item.Id">@item.Title</a>
</td>
<td>
<td class="date-col">
@(item.ExpiryDate?.ToBrowserDate() ?? new HtmlString("<span class=\"text-muted\">No Expiry</span>"))
</td>
<td>
<span data-sensitive>@item.AmountFormatted</span>
</td>
<td>
<span class="badge badge-@item.Status.ToLower() status-badge">@item.Status</span>
</td>
<td class="text-end">
<span data-sensitive>@item.AmountFormatted</span>
</td>
<td class="text-end">
<div class="d-inline-flex align-items-center gap-3">
<a asp-action="EditPaymentRequest" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id" id="Edit-@item.Id">Edit</a>
<a asp-action="ViewPaymentRequest" asp-route-payReqId="@item.Id" id="PaymentRequest-@item.Id">View</a>
<div class="dropdown">
<button class="btn btn-link dropdown-toggle p-0 dropdown-toggle-no-caret" type="button" data-bs-toggle="dropdown" aria-expanded="false" id="ToggleActions-@item.Id">
<i class="fa fa-ellipsis-h"></i>
<button class="btn btn-link dropdown-toggle p-0 dropdown-toggle-no-caret text-body" type="button" data-bs-toggle="dropdown" aria-expanded="false" id="ToggleActions-@item.Id">
<vc:icon symbol="dots" />
</button>
<ul class="dropdown-menu" aria-labelledby="actionDropdown">
<li><a class="dropdown-item" asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@item.StoreId" asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(item.Id)}")">Invoices</a></li>
@ -129,6 +129,7 @@
}
</tbody>
</table>
</div>
<vc:pager view-model="Model" />
}

View file

@ -16,7 +16,6 @@
}
<form method="post" asp-action="EditPullPayment" asp-route-storeId="@Context.GetRouteValue("storeId")" asp-route-pullPaymentId="@Model.Id">
<div class="sticky-header-setup"></div>
<div class="sticky-header d-sm-flex align-items-center justify-content-between">
<h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-sm-0">

View file

@ -20,8 +20,13 @@
}
<div class="sticky-header">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-3">
<h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3">
<h2 class="mb-0">
@ViewData["Title"]
<a href="https://docs.btcpayserver.org/Accounting/" target="_blank" rel="noreferrer noopener" title="More information...">
<vc:icon symbol="info" />
</a>
</h2>
<div class="d-flex flex-wrap gap-3">
<a cheat-mode="true" class="btn btn-outline-info text-nowrap" asp-action="StoreReports" asp-route-fakeData="true" asp-route-viewName="@Model.Request?.ViewName">Create fake date</a>
<button id="exportCSV" class="btn btn-primary text-nowrap" type="button">
@ -29,7 +34,8 @@
</button>
</div>
</div>
<div class="d-flex flex-column flex-sm-row align-items-sm-0center gap-3">
</div>
<div class="d-flex flex-column flex-sm-row align-items-center gap-3 mb-l">
<div class="dropdown" v-pre>
<button id="ViewNameToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret" type="button" data-bs-toggle="dropdown" aria-expanded="false">@Model.Request.ViewName</button>
<div class="dropdown-menu" aria-labelledby="ViewNameToggle">
@ -56,14 +62,13 @@
</button>
</div>
</div>
</div>
<div id="app" v-cloak class="w-100-fixed">
<article v-for="chart in srv.charts" class="mb-5">
<h3>{{ chart.name }}</h3>
<div class="table-responsive" v-if="chart.rows.length || chart.hasGrandTotal">
<table class="table table-hover w-auto">
<thead class="sticky-top bg-body">
<thead class="bg-body">
<tr>
<th v-for="group in chart.groups">{{ titleCase(group) }}</th>
<th v-for="agg in chart.aggregates" class="text-end">{{ titleCase(agg) }}</th>

View file

@ -16,7 +16,7 @@
</small>
</h3>
<form method="post" asp-action="DynamicDnsService">
<button id="AddDynamicDNS" class="btn btn-primary mt-2" type="submit"><span class="fa fa-plus"></span> Add service</button>
<button id="AddDynamicDNS" class="btn btn-primary mt-2" type="submit">Add service</button>
</form>
</div>

View file

@ -27,7 +27,6 @@
<div class="d-flex align-items-center justify-content-between mb-3">
<h3 class="mb-0">@ViewData["Title"]</h3>
<a asp-action="CreateUser" class="btn btn-primary" role="button" id="CreateUser">
<span class="fa fa-plus"></span>
Add User
</a>
</div>

View file

@ -154,7 +154,6 @@
<h5 class="d-flex align-items-center justify-content-between mt-5 gap-3">
Domain to app mapping
<button id="AddDomainButton" type="submit" name="command" value="add-domain" class="d-inline-block btn text-primary btn-link p-0">
<span class="fa fa-plus"></span>
Add domain mapping
</button>
</h5>

View file

@ -1,7 +1,6 @@
@using BTCPayServer.Configuration
@inject BTCPayServerOptions _btcPayServerOptions
<div class="sticky-header-setup"></div>
<div class="sticky-header mb-l">
<h2 class="mt-1 mb-2 mb-lg-4">Server Settings</h2>
<nav id="SectionNav">

View file

@ -16,7 +16,6 @@
}
<form method="post" asp-route-walletId="@Context.GetRouteValue("walletId")" asp-action="NewPullPayment">
<div class="sticky-header-setup"></div>
<div class="sticky-header d-flex align-items-center justify-content-between">
<h2 class="mb-0">@ViewData["Title"]</h2>
<input type="submit" value="Create" class="btn btn-primary" id="Create"/>

View file

@ -27,14 +27,14 @@
switch (Model.PayoutState)
{
case PayoutState.AwaitingApproval:
stateActions.Add(("approve", "Approve selected payouts"));
stateActions.Add(("approve-pay", "Approve & Send selected payouts"));
stateActions.Add(("cancel", "Cancel selected payouts"));
stateActions.Add(("approve", "Approve"));
stateActions.Add(("approve-pay", "Approve & Send"));
stateActions.Add(("cancel", "Cancel"));
break;
case PayoutState.AwaitingPayment:
stateActions.Add(("pay", "Send selected payouts"));
stateActions.Add(("cancel", "Cancel selected payouts"));
stateActions.Add(("mark-paid", "Mark selected payouts as already paid"));
stateActions.Add(("pay", "Send"));
stateActions.Add(("cancel", "Cancel"));
stateActions.Add(("mark-paid", "Mark as already paid"));
break;
}
}
@ -59,7 +59,6 @@
</script>
}
<div class="sticky-header-setup"></div>
<div class="sticky-header d-flex align-items-center justify-content-between">
<h2 class="mb-0">
@ViewData["Title"]
@ -125,20 +124,7 @@
</li>
}
</ul>
@if (Model.Payouts.Any() && stateActions.Any())
{
<div class="dropdown ms-xl-auto mt-xl-0" permission="@Policies.CanModifyStoreSettings">
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" id="@Model.PayoutState-actions">Actions</button>
<div class="dropdown-menu" aria-labelledby="@Model.PayoutState-actions">
@foreach (var action in stateActions)
{
<button type="submit" id="@Model.PayoutState-@action.Action" name="Command" class="dropdown-item" role="button" value="@Model.PayoutState-@action.Action">@action.Text</button>
}
</div>
</div>
}
</div>
<nav id="SectionNav" class="mb-3">
<div class="nav">
@foreach (var state in Model.PayoutStateCount)
@ -159,41 +145,71 @@
}
</div>
</nav>
@if (Model.Payouts.Any())
{
<div class="table-responsive">
<table class="table table-hover">
<thead>
<table class="table table-hover mass-action">
<thead class="mass-action-head">
<tr>
<th permission="@Policies.CanModifyStoreSettings">
<input id="@Model.PayoutState-selectAllCheckbox" type="checkbox" class="form-check-input selectAll" data-payout-state="@Model.PayoutState.ToString()" />
@if (stateActions.Any())
{
<th class="only-for-js mass-action-select-col" permission="@Policies.CanModifyStoreSettings">
<input type="checkbox" class="form-check-input mass-action-select-all" data-payout-state="@Model.PayoutState.ToString()" />
</th>
<th style="min-width: 90px;" class="col-md-auto">
}
<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">Source</th>
<th class="text-start">Destination</th>
<th class="text-end">Amount</th>
<th>Source</th>
<th>Destination</th>
<th class="amount-col">Amount</th>
@if (Model.PayoutState != PayoutState.AwaitingApproval)
{
<th class="text-end">Transaction</th>
}
</tr>
</thead>
@if (stateActions.Any())
{
<thead class="mass-action-actions" permission="@Policies.CanModifyStoreSettings">
<tr>
<th class="mass-action-select-col only-for-js">
<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>
<div class="d-inline-flex align-items-center gap-3">
@foreach (var action in stateActions)
{
<button type="submit" id="@Model.PayoutState-@action.Action" name="Command" class="btn btn-link" value="@Model.PayoutState-@action.Action">@action.Text</button>
}
</div>
</div>
</th>
</tr>
</thead>
}
<tbody>
@for (int i = 0; i < Model.Payouts.Count; i++)
@for (var i = 0; i < Model.Payouts.Count; i++)
{
var pp = Model.Payouts[i];
<tr class="payout">
<td permission="@Policies.CanModifyStoreSettings">
<span>
<input type="checkbox" class="selection-item-@Model.PayoutState.ToString() form-check-input" asp-for="Payouts[i].Selected" />
<tr class="payout mass-action-row">
@if (stateActions.Any())
{
<td class="only-for-js mass-action-select-col" permission="@Policies.CanModifyStoreSettings">
<input type="checkbox" class="selection-item-@Model.PayoutState.ToString() form-check-input mass-action-select" asp-for="Payouts[i].Selected" />
<input type="hidden" asp-for="Payouts[i].PayoutId" />
</span>
</td>
<td>
<span>@pp.Date.ToBrowserDate()</span>
}
<td class="date-col">
@pp.Date.ToBrowserDate()
</td>
<td class="mw-100">
@if (pp.SourceLink is not null && pp.Source is not null)
@ -208,7 +224,7 @@
<td title="@pp.Destination">
<span class="text-break">@pp.Destination</span>
</td>
<td class="text-end text-nowrap">
<td class="amount-col">
<span data-sensitive>@pp.Amount</span>
</td>
@if (Model.PayoutState != PayoutState.AwaitingApproval)

View file

@ -38,7 +38,6 @@
</style>
}
<div class="sticky-header-setup"></div>
<div class="sticky-header d-flex align-items-center justify-content-between">
<h2 class="mb-0">
@ViewData["Title"]
@ -47,7 +46,7 @@
</a>
</h2>
<a permission="@Policies.CanCreateNonApprovedPullPayments" asp-action="NewPullPayment" asp-route-storeId="@storeId" class="btn btn-primary" role="button" id="NewPullPayment">
<span class="fa fa-plus"></span> Create Pull Payment
Create Pull Payment
</a>
</div>
@ -104,7 +103,8 @@
<table class="table table-hover">
<thead>
<tr>
<th scope="col">
<th scope="col" class="date-col">
<div class="d-flex align-items-center gap-2">
<a asp-action="PullPayments"
asp-route-sortOrder="@(nextStartDateSortOrder ?? "asc")"
asp-route-pullPaymentState="@Model.ActiveState"
@ -113,18 +113,20 @@
Start
<span class="fa @(sortIconClass)"></span>
</a>
<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 scope="col">Name</th>
<th scope="col">Automatically Approved</th>
<th scope="col">Refunded</th>
<th scope="col" class="text-end">Actions</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
@foreach (var pp in Model.PullPayments)
{
<tr>
<td>@pp.StartDate.ToBrowserDate()</td>
<tr class="mass-action-row">
<td class="date-col">@pp.StartDate.ToBrowserDate()</td>
<td>
<a asp-action="EditPullPayment"
asp-controller="UIPullPayment"
@ -145,6 +147,12 @@
</div>
</td>
<td class="text-end">
<div class="d-inline-flex align-items-center gap-3">
<a asp-action="ViewPullPayment"
asp-controller="UIPullPayment"
asp-route-pullPaymentId="@pp.Id">
View
</a>
<a class="pp-payout"
asp-action="Payouts"
asp-route-storeId="@storeId"
@ -153,7 +161,6 @@
</a>
@if (!pp.Archived)
{
<span permission="@Policies.CanArchivePullPayments"> - </span>
<a asp-action="ArchivePullPayment"
permission="@Policies.CanArchivePullPayments"
asp-route-storeId="@storeId"
@ -164,12 +171,7 @@
Archive
</a>
}
<span> - </span>
<a asp-action="ViewPullPayment"
asp-controller="UIPullPayment"
asp-route-pullPaymentId="@pp.Id">
View
</a>
</div>
</td>
</tr>
}

View file

@ -21,7 +21,6 @@
<div class="d-flex align-items-center justify-content-between mb-3">
<h3 class="mb-0">@ViewData["Title"]</h3>
<a id="CreateNewToken" asp-action="CreateToken" class="btn btn-primary" role="button" asp-route-storeId="@Context.GetRouteValue("storeId")">
<span class="fa fa-plus"></span>
Create Token
</a>
</div>

View file

@ -22,7 +22,6 @@
</button>
}
<button class="btn btn-primary" name="command" type="submit" value="add" id="CreateEmailRule">
<span class="fa fa-plus"></span>
Create
</button>
</div>

View file

@ -33,7 +33,7 @@
</select>
</div>
<div class="ms-3">
<button type="submit" role="button" class="btn btn-primary"><span class="fa fa-plus"></span> Add User</button>
<button type="submit" role="button" class="btn btn-primary">Add User</button>
</div>
</div>
</form>

View file

@ -9,7 +9,6 @@
<div class="d-flex align-items-center justify-content-between mb-3">
<h3 class="mb-0">@ViewData["Title"]</h3>
<a id="CreateWebhook" asp-action="NewWebhook" class="btn btn-primary" role="button" asp-route-storeId="@Context.GetRouteValue("storeId")">
<span class="fa fa-plus"></span>
Create Webhook
</a>
</div>

View file

@ -4,7 +4,6 @@
var storeId = Context.GetStoreData()?.Id;
}
<div class="sticky-header-setup"></div>
<div class="sticky-header mb-l">
<h2 class="mt-1 mb-2 mb-lg-4">Store Settings</h2>
<nav id="SectionNav">

View file

@ -1,3 +1,5 @@
@using BTCPayServer.Client
@using BTCPayServer.Components
@model ListTransactionsViewModel
@{
@ -55,12 +57,6 @@
const $dropdowns = document.getElementById('Dropdowns');
const $indicator = document.getElementById('LoadingIndicator');
delegate('click', '#selectAllCheckbox', e => {
document.querySelectorAll(".selector").forEach(checkbox => {
checkbox.checked = e.target.checked;
});
});
delegate('click', '#GoToTop', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
@ -108,7 +104,7 @@
}
$indicator.classList.add('d-none');
formatDateTimes(document.getElementById('switchTimeFormat').dataset.mode);
formatDateTimes(document.querySelector('#WalletTransactions .switch-time-format').dataset.mode);
initLabelManagers();
}
@ -144,16 +140,6 @@
}
<div class="d-inline-flex align-items-center gap-3" id="Dropdowns">
<div class="dropdown ms-auto" id="Actions">
<button class="btn btn-secondary dropdown-toggle" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions
</button>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="ActionsDropdownToggle">
<form id="WalletActions" method="post" asp-action="WalletActions" asp-route-walletId="@walletId">
<button id="BumpFee" name="command" type="submit" class="dropdown-item" value="cpfp">Bump fee (CPFP)</button>
</form>
</div>
</div>
<div class="dropdown d-inline-flex align-items-center gap-3" id="Export">
<button class="btn btn-secondary dropdown-toggle" type="button" id="ExportDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Export
@ -168,22 +154,43 @@
<div style="clear:both"></div>
<div id="WalletTransactions" class="table-responsive-md">
<table class="table table-hover">
<thead>
<table class="table table-hover mass-action">
<thead class="mass-action-head">
<tr>
<th style="width:2rem;" class="only-for-js">
<input id="selectAllCheckbox" type="checkbox" class="form-check-input" />
<th class="only-for-js mass-action-select-col">
<input type="checkbox" class="form-check-input mass-action-select-all" />
</th>
<th class="w-150px">
<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" title="Switch date format" id="switchTimeFormat"></button>
<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="text-end">Amount</th>
<th class="text-end" style="min-width:60px"></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">

View file

@ -5,7 +5,6 @@
var wallet = walletId != null ? WalletId.Parse(walletId) : new WalletId(storeId, cryptoCode);
}
<div class="sticky-header-setup"></div>
<div class="sticky-header">
<vc:wallet-nav wallet-id="wallet"/>
</div>

View file

@ -1,5 +1,4 @@
@using BTCPayServer.Services
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Components.LabelManager
@model ListTransactionsViewModel
@{
@ -7,11 +6,11 @@
}
@foreach (var transaction in Model.Transactions)
{
<tr>
<td class="only-for-js">
<input name="selectedTransactions" type="checkbox" class="selector form-check-input" form="WalletActions" value="@transaction.Id" />
<tr class="mass-action-row">
<td class="only-for-js mass-action-select-col">
<input name="selectedTransactions" type="checkbox" class="form-check-input mass-action-select" form="WalletActions" value="@transaction.Id" />
</td>
<td>
<td class="date-col">
@transaction.Timestamp.ToBrowserDate()
</td>
<td class="text-start">
@ -27,18 +26,9 @@
@transaction.Id
</a>
</td>
@if (transaction.Positive)
{
<td class="text-end text-success">
<span data-sensitive>@transaction.Balance</span>
<td class="amount-col">
<span data-sensitive class="text-@(transaction.Positive ? "success" : "danger")">@transaction.Balance</span>
</td>
}
else
{
<td class="text-end text-danger">
<span data-sensitive>@transaction.Balance</span>
</td>
}
<td class="text-end">
<div class="dropstart d-inline-block">
@if (string.IsNullOrEmpty(transaction.Comment))

View file

@ -9,6 +9,9 @@
--mobile-header-height: 4rem;
--desktop-header-height: 8rem;
--sidebar-width: 280px;
--sticky-header-height: 0; /* gets dynamically set via JavaScript */
scroll-padding-top: calc(var(--sticky-header-height) + var(--btcpay-space-m));
}
/* Main Menu */

View file

@ -154,11 +154,7 @@ h2 svg.icon.icon-info {
cursor: pointer;
padding: 0;
}
@media (min-width: 1400px) {
#MassAction {
margin-top: -4rem;
}
}
/* Prevent layout from breaking on hyperlinks with very long URLs as the visible text */
.invoice-details a {
word-break: break-word;
@ -1060,3 +1056,90 @@ input.ts-wrapper.form-control:not(.ts-hidden-accessible,.ts-inline) {
height: .75rem;
}
/* Tables */
.date-col {
min-width: 8rem;
}
.amount-col {
text-align: right;
white-space: nowrap;
}
/* Mass Actions */
.mass-action-head,
.mass-action-actions {
position: -webkit-sticky;
position: sticky;
top: var(--sticky-header-height);
z-index: 10;
background-color: var(--btcpay-body-bg);
}
.mass-action thead th::after {
content: '';
position: absolute;
top: -1px;
left: -1px;
right: -1px;
bottom: 0;
border-bottom: 1px solid;
border-color: inherit;
pointer-events: none;
}
.mass-action > .mass-action-actions,
.mass-action[data-selected] > .mass-action-head {
display: none;
}
.mass-action[data-selected] > .mass-action-actions {
display: table-header-group;
border-top-width: 0;
}
.mass-action > .mass-action-actions button {
display: inline-flex;
align-items: center;
height: 1.4rem;
padding: 0;
font-weight: var(--btcpay-font-weight-semibold);
}
.mass-action > .mass-action-actions button .icon {
--btn-icon-size: 1.75rem;
}
.mass-action .mass-action-select-col {
width: 2rem;
}
/*
Responsive table adjustments: Reset sticky header height,
because it doesn't work in containers with overflow auto.
*/
.table-responsive{
--sticky-header-height: 0;
}
@media (max-width: 575.98px) {
.table-responsive-sm {
--sticky-header-height: 0;
}
}
@media (max-width: 767.98px) {
.table-responsive-md {
--sticky-header-height: 0;
}
}
@media (max-width: 991.98px) {
.table-responsive-lg {
--sticky-header-height: 0;
}
}
@media (max-width: 1199.98px) {
.table-responsive-xl {
--sticky-header-height: 0;
}
}
@media (max-width: 1399.98px) {
.table-responsive-xxl {
--sticky-header-height: 0;
}
}

View file

@ -157,7 +157,14 @@ document.addEventListener("DOMContentLoaded", () => {
// sticky header
const stickyHeader = document.querySelector('#mainContent > section > .sticky-header');
if (stickyHeader) {
document.documentElement.style.scrollPaddingTop = `calc(${stickyHeader.offsetHeight}px + var(--btcpay-space-m))`;
const setStickyHeaderHeight = () => {
document.documentElement.style.setProperty('--sticky-header-height', `${stickyHeader.offsetHeight}px`)
}
window.addEventListener('resize', e => {
debounce('resize', setStickyHeaderHeight, 50)
});
setStickyHeaderHeight();
}
// initialize timezone offset value if field is present in page
@ -355,6 +362,46 @@ document.addEventListener("DOMContentLoaded", () => {
window.localStorage.setItem(COLLAPSED_KEY, JSON.stringify(collapsed))
})
}
// Mass Action Tables
const updateSelectedCount = ($table) => {
const selectedCount = document.querySelectorAll('.mass-action-select:checked').length;
const $selectedCount = $table.querySelector('.mass-action-selected-count');
if ($selectedCount) $selectedCount.innerText = selectedCount;
if (selectedCount === 0) {
$table.removeAttribute('data-selected');
} else {
$table.setAttribute('data-selected', selectedCount.toString());
}
}
delegate('click', '.mass-action .mass-action-select-all', e => {
const $table = e.target.closest('.mass-action');
const { checked } = e.target;
$table.querySelectorAll('.mass-action-select,.mass-action-select-all').forEach($checkbox => {
$checkbox.checked = checked;
});
updateSelectedCount($table);
});
delegate('change', '.mass-action .mass-action-select', e => {
const $table = e.target.closest('.mass-action');
const selectedCount = $table.querySelectorAll('.mass-action-select:checked').length;
if (selectedCount === 0) {
$table.querySelectorAll('.mass-action-select-all').forEach(checkbox => {
checkbox.checked = false;
});
}
updateSelectedCount($table);
});
delegate('click', '.mass-action .mass-action-row', e => {
const $target = e.target
if ($target.matches('td,time,span[data-sensitive]')) {
const $row = $target.closest('.mass-action-row');
$row.querySelector('.mass-action-select').click();
}
});
});
// Initialize Blazor

View file

@ -9,3 +9,9 @@ function delegate(eventType, selector, handler, root) {
}
})
}
const DEBOUNCE_TIMERS = {}
function debounce(key, fn, delay = 250) {
clearTimeout(DEBOUNCE_TIMERS[key])
DEBOUNCE_TIMERS[key] = setTimeout(fn, delay)
}