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

View file

@ -634,58 +634,56 @@ namespace BTCPayServer.Controllers
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewInvoices)] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewInvoices)]
public async Task<IActionResult> MassAction(string command, string[] selectedItems, string? storeId = null) public async Task<IActionResult> MassAction(string command, string[] selectedItems, string? storeId = null)
{ {
if (selectedItems != null) IActionResult NotSupported(string err)
{ {
switch (command) TempData[WellKnownTempData.ErrorMessage] = err;
{ return RedirectToAction(nameof(ListInvoices), new { storeId });
case "archive": }
await _InvoiceRepository.MassArchive(selectedItems); if (selectedItems.Length == 0)
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} archived."; return NotSupported("No invoice has been selected");
break;
case "unarchive": switch (command)
await _InvoiceRepository.MassArchive(selectedItems, false); {
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} unarchived."; case "archive":
break; await _InvoiceRepository.MassArchive(selectedItems);
case "cpfp": TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} archived.";
if (selectedItems.Length == 0) break;
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))
return Forbid();
var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(_NetworkProvider, network.CryptoCode))?.AccountDerivation; case "unarchive":
if (derivationScheme is null) await _InvoiceRepository.MassArchive(selectedItems, false);
return NotSupported("This feature is only available to BTC wallets"); TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} unarchived.";
var bumpableAddresses = (await GetAddresses(selectedItems)) break;
.Where(p => p.GetPaymentMethodId().IsBTCOnChain) case "cpfp":
.Select(p => p.GetAddress()).ToHashSet(); var network = _NetworkProvider.DefaultNetwork;
var utxos = await explorer.GetUTXOsAsync(derivationScheme); var explorer = _ExplorerClients.GetExplorerClient(network);
var bumpableUTXOs = utxos.GetUnspentUTXOs().Where(u => u.Confirmations == 0 && bumpableAddresses.Contains(u.ScriptPubKey.Hash.ToString())).ToArray(); if (explorer is null)
var parameters = new MultiValueDictionary<string, string>(); return NotSupported("This feature is only available to BTC wallets");
foreach (var utxo in bumpableUTXOs) if (!GetCurrentStore().HasPermission(GetUserId(), Policies.CanModifyStoreSettings))
{ return Forbid();
parameters.Add($"outpoints[]", utxo.Outpoint.ToString());
} var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(_NetworkProvider, network.CryptoCode))?.AccountDerivation;
return View("PostRedirect", new PostRedirectViewModel if (derivationScheme is null)
{ return NotSupported("This feature is only available to BTC wallets");
AspController = "UIWallets", var bumpableAddresses = (await GetAddresses(selectedItems))
AspAction = nameof(UIWalletsController.WalletCPFP), .Where(p => p.GetPaymentMethodId().IsBTCOnChain)
RouteParameters = { .Select(p => p.GetAddress()).ToHashSet();
{ "walletId", new WalletId(storeId, network.CryptoCode).ToString() }, var utxos = await explorer.GetUTXOsAsync(derivationScheme);
{ "returnUrl", Url.Action(nameof(ListInvoices), new { storeId }) } var bumpableUTXOs = utxos.GetUnspentUTXOs().Where(u => u.Confirmations == 0 && bumpableAddresses.Contains(u.ScriptPubKey.Hash.ToString())).ToArray();
}, var parameters = new MultiValueDictionary<string, string>();
FormParameters = parameters, foreach (var utxo in bumpableUTXOs)
}); {
} parameters.Add($"outpoints[]", utxo.Outpoint.ToString());
}
return View("PostRedirect", new PostRedirectViewModel
{
AspController = "UIWallets",
AspAction = nameof(UIWalletsController.WalletCPFP),
RouteParameters = {
{ "walletId", new WalletId(storeId, network.CryptoCode).ToString() },
{ "returnUrl", Url.Action(nameof(ListInvoices), new { storeId }) }
},
FormParameters = parameters,
});
} }
return RedirectToAction(nameof(ListInvoices), new { storeId }); return RedirectToAction(nameof(ListInvoices), new { storeId });
} }

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@
</a> </a>
</small> </small>
</h2> </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>
<div class="row"> <div class="row">

View file

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

View file

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

View file

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

View file

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

View file

@ -1,13 +1,10 @@
@using BTCPayServer.Client @using BTCPayServer.Client
@using BTCPayServer.Client.Models @using BTCPayServer.Client.Models
@using BTCPayServer.Services @using BTCPayServer.Services
@using SetPasswordViewModel = BTCPayServer.Models.ManageViewModels.SetPasswordViewModel
@inject DisplayFormatter DisplayFormatter @inject DisplayFormatter DisplayFormatter
@inject ReportService ReportService
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
@model InvoicesModel @model InvoicesModel
@{ @{
var reportNames = ReportService.ReportProviders.Select(p => p.Value.Name).OrderBy(c => c).ToArray();
ViewData.SetActivePage(InvoiceNavPages.Index, "Invoices"); ViewData.SetActivePage(InvoiceNavPages.Index, "Invoices");
var statusFilterCount = CountArrayFilter("status") + CountArrayFilter("exceptionstatus") + (HasBooleanFilter("includearchived") ? 1 : 0) + (HasBooleanFilter("unusual") ? 1 : 0); var statusFilterCount = CountArrayFilter("status") + CountArrayFilter("exceptionstatus") + (HasBooleanFilter("includearchived") ? 1 : 0) + (HasBooleanFilter("unusual") ? 1 : 0);
var hasDateFilter = HasArrayFilter("startdate") || HasArrayFilter("enddate"); var hasDateFilter = HasArrayFilter("startdate") || HasArrayFilter("enddate");
@ -32,8 +29,11 @@
@section PageHeadContent @section PageHeadContent
{ {
<style> <style>
.invoiceId-col {
min-width: 8rem;
}
.invoice-details-row > td { .invoice-details-row > td {
padding: 1.5rem .5rem 0 2.65rem; padding: 1.5rem 1rem 0 2.65rem;
} }
.dropdown > .btn { .dropdown > .btn {
min-width: 7rem; min-width: 7rem;
@ -54,11 +54,7 @@
@* Custom Range Modal *@ @* Custom Range Modal *@
<script> <script>
delegate('click', '#selectAllCheckbox', e => { const timezoneOffset = new Date().getTimezoneOffset();
document.querySelectorAll(".selector").forEach(checkbox => {
checkbox.checked = e.target.checked;
});
});
delegate('click', '.showInvoice', e => { delegate('click', '.showInvoice', e => {
e.preventDefault(); e.preventDefault();
@ -99,41 +95,11 @@
var str = newDate.toLocaleDateString() + " " + newDate.toLocaleTimeString(); var str = newDate.toLocaleDateString() + " " + newDate.toLocaleTimeString();
return str; 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> </script>
} }
@Html.HiddenFor(a => a.Count) @Html.HiddenFor(a => a.Count)
<div class="sticky-header-setup"></div>
<div class="sticky-header d-sm-flex align-items-center justify-content-between"> <div class="sticky-header d-sm-flex align-items-center justify-content-between">
<h2 class="mb-0"> <h2 class="mb-0">
@ViewData["Title"] @ViewData["Title"]
@ -142,7 +108,6 @@
</a> </a>
</h2> </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"> <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 Create Invoice
</a> </a>
</div> </div>
@ -169,7 +134,6 @@
</div> </div>
<partial name="_StatusMessage" /> <partial name="_StatusMessage" />
<partial name="InvoiceStatusChangePartial" /> <partial name="InvoiceStatusChangePartial" />
@* Custom Range Modal *@ @* Custom Range Modal *@
@ -305,97 +269,100 @@
@if (Model.Invoices.Any()) @if (Model.Invoices.Any())
{ {
<form method="post" id="MassAction" asp-action="MassAction" class=""> <form method="post" asp-action="MassAction">
<div class="d-inline-flex align-items-center pb-2 float-xxl-end mb-2 gap-3"> <input type="hidden" name="storeId" value="@Model.StoreId" />
<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"> <div class="table-responsive">
<table id="invoices" class="table table-hover"> <table id="invoices" class="table table-hover mass-action">
<thead> <thead class="mass-action-head">
<tr> <tr>
<th style="width:2rem;" class="only-for-js"> <th class="mass-action-select-col only-for-js">
<input id="selectAllCheckbox" type="checkbox" class="form-check-input" /> <input type="checkbox" class="form-check-input mass-action-select-all" />
<th class="w-150px"> </th>
<th class="date-col">
<div class="d-flex align-items-center gap-1"> <div class="d-flex align-items-center gap-1">
Date 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> <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> </div>
</th> </th>
<th class="text-nowrap">Order Id</th>
<th class="text-nowrap">Invoice Id</th> <th class="text-nowrap">Invoice Id</th>
<th class="text-nowrap">Order Id</th>
<th>Status</th> <th>Status</th>
<th class="text-end">Amount</th> <th class="amount-col">Amount</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="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> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var invoice in Model.Invoices) @foreach (var invoice in Model.Invoices)
{ {
<tr id="invoice_@invoice.InvoiceId" class="invoice-row"> var detailsId = $"invoice_details_{invoice.InvoiceId}";
<td class="only-for-js"> <tr id="invoice_@invoice.InvoiceId" class="mass-action-row">
<input name="selectedItems" type="checkbox" class="selector form-check-input" value="@invoice.InvoiceId" /> <td class="only-for-js align-middle">
<input name="selectedItems" type="checkbox" class="form-check-input mass-action-select" value="@invoice.InvoiceId" />
</td> </td>
<td>@invoice.Date.ToBrowserDate()</td> <td class="align-middle date-col">@invoice.Date.ToBrowserDate()</td>
<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" /> <vc:truncate-center text="@invoice.OrderId" link="@invoice.RedirectUrl" classes="truncate-center-id" />
</td> </td>
<td class="text-break">@invoice.InvoiceId</td> <td class="align-middle">
<td> <div class="d-inline-flex align-items-center gap-2">
<vc:invoice-status state="invoice.Status" payments="invoice.Details.Payments" invoice-id="@invoice.InvoiceId" <vc:invoice-status state="invoice.Status" payments="invoice.Details.Payments" invoice-id="@invoice.InvoiceId"
is-archived="invoice.Details.Archived" has-refund="invoice.HasRefund" /> is-archived="invoice.Details.Archived" has-refund="invoice.HasRefund" />
</td> @if (invoice.ShowCheckout)
<td class="text-end text-nowrap"> {
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span> <span>&nbsp;</span>
</td>
<td class="text-end text-nowrap">
@if (invoice.ShowCheckout)
{
<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="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> <a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId" class="showInvoice only-for-js" data-invoice-id="@invoice.InvoiceId">[^]</a>
@if (!invoice.CanMarkStatus) }
{ </div>
<span>-</span> </td>
} <td class="align-middle amount-col">
</span> <span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
} </td>
&nbsp; <td class="align-middle text-end">
<a asp-action="Invoice" class="invoice-details-link" asp-route-invoiceId="@invoice.InvoiceId">Details</a> <div class="d-inline-flex align-items-center gap-2">
<a class="only-for-js invoice-details-toggle" href="#"> <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">
<span title="Invoice Details Toggle" class="fa fa-1x fa-angle-double-down"></span> <vc:icon symbol="caret-down" />
</a> </button>
</div>
</td> </td>
</tr> </tr>
<tr id="invoice_details_@invoice.InvoiceId" class="invoice-details-row" style="display:none;"> <tr id="@detailsId" class="invoice-details-row collapse">
<td colspan="99" class="border-top-0"> <td colspan="7" class="border-top-0">
@* Leaving this as partial because it abstracts complexity of Invoice Payments *@ @* Leaving this as partial because it abstracts complexity of Invoice Payments *@
<partial name="ListInvoicesPaymentsPartial" model="(invoice.Details, true)" /> <partial name="ListInvoicesPaymentsPartial" model="(invoice.Details, true)" />
</td> </td>

View file

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

View file

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

View file

@ -135,7 +135,6 @@
<input type="text" class="form-control" name="Name" placeholder="Security device name"/> <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> <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"> <button id="btn-add" type="submit" class="btn btn-primary">
<span class="fa fa-plus"></span>
Add Add
</button> </button>
</div> </div>

View file

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

View file

@ -14,61 +14,75 @@
@if (Model.Items.Count > 0) @if (Model.Items.Count > 0)
{ {
<form method="post" asp-action="MassAction"> <form method="post" asp-action="MassAction">
<div class="row button-row"> @if (Model.Items.Any())
<div class="col-lg-6"> {
<span class="dropdown" style="display:none;" id="MassAction"> <div class="table-responsive-md">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <table class="table table-hover mass-action">
Actions <thead class="mass-action-head">
</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> <tr>
<th style="width:30px" class="only-for-js"> <th class="mass-action-select-col only-for-js">
@if (Model.Items.Count > 0) <input name="selectedItems" type="checkbox" class="form-check-input mass-action-select-all" />
{
<input name="selectedItems" id="selectAllCheckbox" type="checkbox" class="form-check-input" />
}
</th> </th>
<th class="w-150px"> <th class="date-col">
<div class="d-flex align-items-center gap-1"> <div class="d-flex align-items-center gap-1">
Date Date
<button type="button" class="btn btn-link p-0 fa fa-clock-o switch-time-format" title="Switch date format"></button> <button type="button" class="btn btn-link p-0 fa fa-clock-o switch-time-format" title="Switch date format"></button>
</div> </div>
</th> </th>
<th>Message</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> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var item in Model.Items) @foreach (var item in Model.Items)
{ {
<tr data-guid="@item.Id" class="notification-row @(item.Seen ? "seen" : "")"> <tr data-guid="@item.Id" class="notification-row mass-action-row @(item.Seen ? "seen" : "")">
<td class="only-for-js"> <td class="only-for-js mass-action-select-col">
<input name="selectedItems" type="checkbox" class="selector form-check-input" value="@item.Id" /> <input name="selectedItems" type="checkbox" class="form-check-input mass-action-select" value="@item.Id" />
</td> </td>
<td class="toggleRowCheckbox">@item.Created.ToBrowserDate()</td> <td class="date-col">@item.Created.ToBrowserDate()</td>
<td class="toggleRowCheckbox"> <td>
@item.Body @item.Body
</td> </td>
<td class="text-end fw-normal"> <td class="text-end">
@if (!String.IsNullOrEmpty(item.ActionLink)) <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)">
<a href="@item.ActionLink" class="btn btn-link p-0" rel="noreferrer noopener">Details</a> <span>Mark</span>&nbsp;<span class="seen-text"></span>
<span class="d-none d-md-inline-block"> - </span> </button>
} @if (!string.IsNullOrEmpty(item.ActionLink))
<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> <a href="@item.ActionLink" class="btn btn-link p-0" rel="noreferrer noopener">Details</a>
</button> }
</div>
</td> </td>
</tr> </tr>
} }
@ -76,9 +90,8 @@
</table> </table>
<vc:pager view-model="Model" /> <vc:pager view-model="Model" />
</div> </div>
</div> }
</form> </form>
} }
else else
@ -113,19 +126,6 @@ else
@section PageFootContent { @section PageFootContent {
<script type="text/javascript"> <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 => { delegate('click', '.btn-toggle-seen', e => {
const row = $(e.target).parents(".notification-row").toggleClass("loading"); const row = $(e.target).parents(".notification-row").toggleClass("loading");
const guid = row.data("guid"); const guid = row.data("guid");
@ -135,20 +135,5 @@ else
}); });
return false; 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> </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)"> <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"> <div class="sticky-header d-sm-flex align-items-center justify-content-between">
<h2 class="mb-0">@ViewData["Title"]</h2> <h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-sm-0"> <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; 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"> <div class="sticky-header d-sm-flex align-items-center justify-content-between">
<h2 class="mb-0"> <h2 class="mb-0">
@ViewData["Title"] @ViewData["Title"]
@ -50,7 +49,7 @@
<partial name="_StatusMessage" /> <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="Count" />
<input type="hidden" asp-for="TimezoneOffset" /> <input type="hidden" asp-for="TimezoneOffset" />
<input asp-for="SearchTerm" type="hidden" value="@Model.Search.WithoutSearchText()"/> <input asp-for="SearchTerm" type="hidden" value="@Model.Search.WithoutSearchText()"/>
@ -78,57 +77,59 @@
@if (Model.Items.Any()) @if (Model.Items.Any())
{ {
<table class="table table-hover table-responsive-md" id="tableId"> <div class="table-responsive-md">
<thead> <table class="table table-hover">
<tr> <thead>
<th>Title</th>
<th class="w-150px">
<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>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr> <tr>
<td> <th>Title</th>
<a asp-action="ViewPaymentRequest" asp-route-payReqId="@item.Id" id="PaymentRequest-@item.Id">@item.Title</a> <th class="date-col">
</td> <div class="d-flex align-items-center gap-1">
<td> Expiry
@(item.ExpiryDate?.ToBrowserDate() ?? new HtmlString("<span class=\"text-muted\">No Expiry</span>")) <button type="button" class="btn btn-link p-0 fa fa-clock-o switch-time-format only-for-js" title="Switch date format"></button>
</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">
<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>
<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>
<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>
<li><a class="dropdown-item" asp-action="ClonePaymentRequest" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id" id="Clone-@item.Id">Clone</a></li>
<li class="dropdown-divider"></li>
<li><a class="dropdown-item" asp-action="TogglePaymentRequestArchival" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id" id="ToggleArchival-@item.Id">@(item.Archived ? "Unarchive" : "Archive")</a></li>
</ul>
</div>
</div> </div>
</td> </th>
<th>Status</th>
<th class="amount-col">Amount</th>
<th></th>
</tr> </tr>
} </thead>
</tbody> <tbody>
</table> @foreach (var item in Model.Items)
{
<tr class="mass-action-row">
<td>
<a asp-action="EditPaymentRequest" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id" id="Edit-@item.Id">@item.Title</a>
</td>
<td class="date-col">
@(item.ExpiryDate?.ToBrowserDate() ?? new HtmlString("<span class=\"text-muted\">No Expiry</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="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 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>
<li><a class="dropdown-item" asp-action="ClonePaymentRequest" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id" id="Clone-@item.Id">Clone</a></li>
<li class="dropdown-divider"></li>
<li><a class="dropdown-item" asp-action="TogglePaymentRequestArchival" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id" id="ToggleArchival-@item.Id">@(item.Archived ? "Unarchive" : "Archive")</a></li>
</ul>
</div>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
<vc:pager view-model="Model" /> <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"> <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"> <div class="sticky-header d-sm-flex align-items-center justify-content-between">
<h2 class="mb-0">@ViewData["Title"]</h2> <h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-sm-0"> <div class="d-flex gap-3 mt-3 mt-sm-0">

View file

@ -20,8 +20,13 @@
} }
<div class="sticky-header"> <div class="sticky-header">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-3"> <div class="d-flex flex-wrap align-items-center justify-content-between gap-3">
<h2 class="mb-0">@ViewData["Title"]</h2> <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"> <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> <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"> <button id="exportCSV" class="btn btn-primary text-nowrap" type="button">
@ -29,32 +34,32 @@
</button> </button>
</div> </div>
</div> </div>
<div class="d-flex flex-column flex-sm-row align-items-sm-0center gap-3"> </div>
<div class="dropdown" v-pre> <div class="d-flex flex-column flex-sm-row align-items-center gap-3 mb-l">
<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" v-pre>
<div class="dropdown-menu" aria-labelledby="ViewNameToggle"> <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>
@foreach (var v in Model.AvailableViews) <div class="dropdown-menu" aria-labelledby="ViewNameToggle">
{ @foreach (var v in Model.AvailableViews)
<a href="#" data-view="@v" class="available-view dropdown-item @(Model.Request.ViewName == v ? "custom-active" : "")">@v</a> {
} <a href="#" data-view="@v" class="available-view dropdown-item @(Model.Request.ViewName == v ? "custom-active" : "")">@v</a>
</div> }
</div> </div>
<div class="input-group"> </div>
<input id="fromDate" class="form-control flatdtpicker" type="datetime-local" <div class="input-group">
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }' <input id="fromDate" class="form-control flatdtpicker" type="datetime-local"
placeholder="Start Date" /> data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
<button type="button" class="btn btn-primary input-group-clear" title="Clear"> placeholder="Start Date" />
<span class="fa fa-times"></span> <button type="button" class="btn btn-primary input-group-clear" title="Clear">
</button> <span class="fa fa-times"></span>
</div> </button>
<div class="input-group"> </div>
<input id="toDate" class="form-control flatdtpicker" type="datetime-local" <div class="input-group">
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }' <input id="toDate" class="form-control flatdtpicker" type="datetime-local"
placeholder="End Date" /> data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
<button type="button" class="btn btn-primary input-group-clear" title="Clear"> placeholder="End Date" />
<span class="fa fa-times"></span> <button type="button" class="btn btn-primary input-group-clear" title="Clear">
</button> <span class="fa fa-times"></span>
</div> </button>
</div> </div>
</div> </div>
@ -63,7 +68,7 @@
<h3>{{ chart.name }}</h3> <h3>{{ chart.name }}</h3>
<div class="table-responsive" v-if="chart.rows.length || chart.hasGrandTotal"> <div class="table-responsive" v-if="chart.rows.length || chart.hasGrandTotal">
<table class="table table-hover w-auto"> <table class="table table-hover w-auto">
<thead class="sticky-top bg-body"> <thead class="bg-body">
<tr> <tr>
<th v-for="group in chart.groups">{{ titleCase(group) }}</th> <th v-for="group in chart.groups">{{ titleCase(group) }}</th>
<th v-for="agg in chart.aggregates" class="text-end">{{ titleCase(agg) }}</th> <th v-for="agg in chart.aggregates" class="text-end">{{ titleCase(agg) }}</th>

View file

@ -16,7 +16,7 @@
</small> </small>
</h3> </h3>
<form method="post" asp-action="DynamicDnsService"> <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> </form>
</div> </div>

View file

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

View file

@ -154,7 +154,6 @@
<h5 class="d-flex align-items-center justify-content-between mt-5 gap-3"> <h5 class="d-flex align-items-center justify-content-between mt-5 gap-3">
Domain to app mapping 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"> <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 Add domain mapping
</button> </button>
</h5> </h5>

View file

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

View file

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

View file

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

View file

@ -38,7 +38,6 @@
</style> </style>
} }
<div class="sticky-header-setup"></div>
<div class="sticky-header d-flex align-items-center justify-content-between"> <div class="sticky-header d-flex align-items-center justify-content-between">
<h2 class="mb-0"> <h2 class="mb-0">
@ViewData["Title"] @ViewData["Title"]
@ -47,7 +46,7 @@
</a> </a>
</h2> </h2>
<a permission="@Policies.CanCreateNonApprovedPullPayments" asp-action="NewPullPayment" asp-route-storeId="@storeId" class="btn btn-primary" role="button" id="NewPullPayment"> <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> </a>
</div> </div>
@ -104,27 +103,30 @@
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th scope="col"> <th scope="col" class="date-col">
<a asp-action="PullPayments" <div class="d-flex align-items-center gap-2">
asp-route-sortOrder="@(nextStartDateSortOrder ?? "asc")" <a asp-action="PullPayments"
asp-route-pullPaymentState="@Model.ActiveState" asp-route-sortOrder="@(nextStartDateSortOrder ?? "asc")"
class="text-nowrap" asp-route-pullPaymentState="@Model.ActiveState"
title="@(nextStartDateSortOrder == "desc" ? sortByAsc : sortByDesc)"> class="text-nowrap"
Start title="@(nextStartDateSortOrder == "desc" ? sortByAsc : sortByDesc)">
<span class="fa @(sortIconClass)"></span> Start
</a> <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>
<th scope="col">Name</th> <th scope="col">Name</th>
<th scope="col">Automatically Approved</th> <th scope="col">Automatically Approved</th>
<th scope="col">Refunded</th> <th scope="col">Refunded</th>
<th scope="col" class="text-end">Actions</th> <th scope="col"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var pp in Model.PullPayments) @foreach (var pp in Model.PullPayments)
{ {
<tr> <tr class="mass-action-row">
<td>@pp.StartDate.ToBrowserDate()</td> <td class="date-col">@pp.StartDate.ToBrowserDate()</td>
<td> <td>
<a asp-action="EditPullPayment" <a asp-action="EditPullPayment"
asp-controller="UIPullPayment" asp-controller="UIPullPayment"
@ -145,31 +147,31 @@
</div> </div>
</td> </td>
<td class="text-end"> <td class="text-end">
<a class="pp-payout" <div class="d-inline-flex align-items-center gap-3">
asp-action="Payouts" <a asp-action="ViewPullPayment"
asp-route-storeId="@storeId" asp-controller="UIPullPayment"
asp-route-pullPaymentId="@pp.Id"> asp-route-pullPaymentId="@pp.Id">
Payouts View
</a>
@if (!pp.Archived)
{
<span permission="@Policies.CanArchivePullPayments"> - </span>
<a asp-action="ArchivePullPayment"
permission="@Policies.CanArchivePullPayments"
asp-route-storeId="@storeId"
asp-route-pullPaymentId="@pp.Id"
data-bs-toggle="modal"
data-bs-target="#ConfirmModal"
data-description="Do you really want to archive the pull payment <strong>@Html.Encode(pp.Name)</strong>?">
Archive
</a> </a>
} <a class="pp-payout"
<span> - </span> asp-action="Payouts"
<a asp-action="ViewPullPayment" asp-route-storeId="@storeId"
asp-controller="UIPullPayment" asp-route-pullPaymentId="@pp.Id">
asp-route-pullPaymentId="@pp.Id"> Payouts
View </a>
</a> @if (!pp.Archived)
{
<a asp-action="ArchivePullPayment"
permission="@Policies.CanArchivePullPayments"
asp-route-storeId="@storeId"
asp-route-pullPaymentId="@pp.Id"
data-bs-toggle="modal"
data-bs-target="#ConfirmModal"
data-description="Do you really want to archive the pull payment <strong>@Html.Encode(pp.Name)</strong>?">
Archive
</a>
}
</div>
</td> </td>
</tr> </tr>
} }

View file

@ -21,7 +21,6 @@
<div class="d-flex align-items-center justify-content-between mb-3"> <div class="d-flex align-items-center justify-content-between mb-3">
<h3 class="mb-0">@ViewData["Title"]</h3> <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")"> <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 Create Token
</a> </a>
</div> </div>

View file

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

View file

@ -33,7 +33,7 @@
</select> </select>
</div> </div>
<div class="ms-3"> <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>
</div> </div>
</form> </form>

View file

@ -9,7 +9,6 @@
<div class="d-flex align-items-center justify-content-between mb-3"> <div class="d-flex align-items-center justify-content-between mb-3">
<h3 class="mb-0">@ViewData["Title"]</h3> <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")"> <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 Create Webhook
</a> </a>
</div> </div>

View file

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

View file

@ -1,3 +1,5 @@
@using BTCPayServer.Client
@using BTCPayServer.Components
@model ListTransactionsViewModel @model ListTransactionsViewModel
@{ @{
@ -55,12 +57,6 @@
const $dropdowns = document.getElementById('Dropdowns'); const $dropdowns = document.getElementById('Dropdowns');
const $indicator = document.getElementById('LoadingIndicator'); const $indicator = document.getElementById('LoadingIndicator');
delegate('click', '#selectAllCheckbox', e => {
document.querySelectorAll(".selector").forEach(checkbox => {
checkbox.checked = e.target.checked;
});
});
delegate('click', '#GoToTop', () => { delegate('click', '#GoToTop', () => {
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
}); });
@ -108,7 +104,7 @@
} }
$indicator.classList.add('d-none'); $indicator.classList.add('d-none');
formatDateTimes(document.getElementById('switchTimeFormat').dataset.mode); formatDateTimes(document.querySelector('#WalletTransactions .switch-time-format').dataset.mode);
initLabelManagers(); initLabelManagers();
} }
@ -144,16 +140,6 @@
} }
<div class="d-inline-flex align-items-center gap-3" id="Dropdowns"> <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"> <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"> <button class="btn btn-secondary dropdown-toggle" type="button" id="ExportDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Export Export
@ -168,23 +154,44 @@
<div style="clear:both"></div> <div style="clear:both"></div>
<div id="WalletTransactions" class="table-responsive-md"> <div id="WalletTransactions" class="table-responsive-md">
<table class="table table-hover"> <table class="table table-hover mass-action">
<thead> <thead class="mass-action-head">
<tr> <tr>
<th style="width:2rem;" class="only-for-js"> <th class="only-for-js mass-action-select-col">
<input id="selectAllCheckbox" type="checkbox" class="form-check-input" /> <input type="checkbox" class="form-check-input mass-action-select-all" />
</th> </th>
<th class="w-150px"> <th class="date-col">
<div class="d-flex align-items-center gap-1"> <div class="d-flex align-items-center gap-1">
Date 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> </div>
</th> </th>
<th class="text-start">Label</th> <th class="text-start">Label</th>
<th>Transaction Id</th> <th>Transaction Id</th>
<th class="text-end">Amount</th> <th class="amount-col">Amount</th>
<th class="text-end" style="min-width:60px"></th> <th></th>
</tr> </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> </thead>
<tbody id="WalletTransactionsList"> <tbody id="WalletTransactionsList">
<partial name="_WalletTransactionsList" model="Model" /> <partial name="_WalletTransactionsList" model="Model" />

View file

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

View file

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

View file

@ -9,6 +9,9 @@
--mobile-header-height: 4rem; --mobile-header-height: 4rem;
--desktop-header-height: 8rem; --desktop-header-height: 8rem;
--sidebar-width: 280px; --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 */ /* Main Menu */

View file

@ -154,11 +154,7 @@ h2 svg.icon.icon-info {
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
} }
@media (min-width: 1400px) {
#MassAction {
margin-top: -4rem;
}
}
/* Prevent layout from breaking on hyperlinks with very long URLs as the visible text */ /* Prevent layout from breaking on hyperlinks with very long URLs as the visible text */
.invoice-details a { .invoice-details a {
word-break: break-word; word-break: break-word;
@ -1060,3 +1056,90 @@ input.ts-wrapper.form-control:not(.ts-hidden-accessible,.ts-inline) {
height: .75rem; 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 // sticky header
const stickyHeader = document.querySelector('#mainContent > section > .sticky-header'); const stickyHeader = document.querySelector('#mainContent > section > .sticky-header');
if (stickyHeader) { 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 // 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)) 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 // 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)
}