Add SearchFiltersPMO to avoid repeat (#7453)
What changed, and why it matters
This commit is mostly a code cleanup and test-helper refactor. It introduces a shared test helper for search filters, switches many HTML IDs to CSS classes for easier testing, fixes a duplicate date-range filter bug, and changes a Telegram footer link from t.me to telegram.me. There is no clear security vulnerability in the diff, and the changes appear to be routine maintenance.
No security action required. Treat as routine refactoring. If desired, verify the RemoveDups('daterange') change does not alter expected query behavior for users who intentionally combine date filters.
Security signals we found
No security-relevant code paths modified (auth, authz, crypto, input validation, output encoding)
Refactoring-only: CSS class additions and test helper extraction
Minor behavior change: SearchString.RemoveDups('daterange') prevents duplicate date-range filters
External link change: t.me -> telegram.me in footer (no observed security impact)
Evidence from the diff
The commit refactors search/filter UI components in BTCPay Server by adding a SearchFiltersPMO Playwright page-model object, replacing hardcoded element selectors in tests with semantic CSS-class-based locators, and updating Razor views to use those classes. It also adds a RemoveDups method on SearchString to prevent duplicate ‘daterange’ filters when combining SearchTerm and SearchText, and changes the Telegram social link domain. No authentication, authorization, input validation, or cryptographic logic is modified in a security-sensitive way.
Changed components
BTCPayServer/Components/ClearAllFilters/Default.cshtmlBTCPayServer/Components/DateRangeSelector/Default.cshtmlBTCPayServer/Components/LabelSelector/Default.cshtmlBTCPayServer/Components/SearchStringInput/Default.cshtmlBTCPayServer/Models/BasePagingViewModel.csBTCPayServer/Plugins/Wallets/Views/UIWallets/WalletTransactions.cshtmlBTCPayServer/SearchString.csBTCPayServer/Views/Shared/_Footer.cshtmlBTCPayServer/Views/UIInvoice/ListInvoices.cshtmlBTCPayServer/Views/UINotifications/Index.cshtmlBTCPayServer/Views/UIReports/StoreReports.cshtmlBTCPayServer.Tests/PMO/SearchFiltersPMO.csBTCPayServer.Tests/PlaywrightTester.csBTCPayServer.Tests/PlaywrightTests.csBTCPayServer.Tests/WalletTests.csInspect captured patch +236 / −150
diff --git a/BTCPayServer.Tests/PMO/SearchFiltersPMO.cs b/BTCPayServer.Tests/PMO/SearchFiltersPMO.cs
new file mode 100644
index 0000000..69c71a4
--- /dev/null
+++ b/BTCPayServer.Tests/PMO/SearchFiltersPMO.cs
@@ -0,0 +1,70 @@
+using System.Threading.Tasks;
+using Microsoft.Playwright;
+using static Microsoft.Playwright.Assertions;
+
+namespace BTCPayServer.Tests.PMO;
+
+public class SearchFiltersPMO(PlaywrightTester s)
+{
+ public ILocator SearchText => s.Page.Locator(".search-string-input__text");
+ public ILocator SearchTerm => s.Page.Locator(".search-string-input__term");
+ public ILocator ClearAllFiltersButton => s.Page.Locator(".clear-all-filters__button");
+ public ILocator DateRangeSelector => s.Page.Locator(".date-range-selector__toggle");
+ public ILocator DateRangeTimeZone => s.Page.Locator(".date-range-selector__timezone");
+ public ILocator LabelSelectorToggle => s.Page.Locator(".label-selector__toggle");
+
+ public async Task FillSearchText(string searchText)
+ {
+ await SearchText.FillAsync(searchText);
+ await SearchText.PressAsync("Enter");
+ await s.Page.WaitForLoadStateAsync();
+ }
+
+ public Task<string> SearchTextValue() => SearchText.InputValueAsync();
+ public Task<string> SearchTermValue() => SearchTerm.InputValueAsync();
+
+ public Task AssertSearchText(string searchText) =>
+ Expect(SearchText).ToHaveValueAsync(searchText);
+
+ public async Task ClearAllFilters()
+ {
+ await ClearAllFiltersButton.ClickAsync();
+ await s.Page.WaitForLoadStateAsync();
+ }
+
+ public async Task SelectDateRangePreset(string preset)
+ {
+ await DateRangeSelector.ClickAsync();
+ await s.Page.ClickAsync($".date-range-selector__preset:has-text('{preset}')");
+ }
+
+ public Task OpenDateRange() => DateRangeSelector.ClickAsync();
+
+ public async Task SelectCustomStartDate(string startDate)
+ {
+ await OpenDateRange();
+ await s.Page.ClickAsync(".date-range-selector__custom-range");
+ await s.Page.Locator(".date-range-selector__custom-range-start[type='hidden']")
+ .EvaluateAsync("(input, startDate) => { input.value = startDate; }", startDate);
+ await s.Page.ClickAsync(".date-range-selector__custom-range-submit");
+ }
+
+ public async Task SelectTimeZone(string timeZone)
+ {
+ await DateRangeTimeZone.FillAsync(timeZone);
+ await DateRangeTimeZone.PressAsync("Enter");
+ await s.Page.WaitForLoadStateAsync();
+ }
+
+ public async Task SelectLabel(string label)
+ {
+ await s.Page.ClickAsync(".label-selector");
+ await s.Page.ClickAsync($".label-selector__item span:has-text('{label}')");
+ }
+
+ public async Task SelectNoLabel()
+ {
+ await s.Page.ClickAsync(".label-selector");
+ await s.Page.ClickAsync(".label-selector__no-label:text-is('No Label')");
+ }
+}
diff --git a/BTCPayServer.Tests/PlaywrightTester.cs b/BTCPayServer.Tests/PlaywrightTester.cs
index 0b3ca0f..e7cd1af 100644
--- a/BTCPayServer.Tests/PlaywrightTester.cs
+++ b/BTCPayServer.Tests/PlaywrightTester.cs
@@ -667,7 +667,15 @@ namespace BTCPayServer.Tests
await Server.ExplorerNode.GenerateAsync(1);
await Page.ReloadAsync();
- await Page.Locator("#CancelWizard").ClickAsync();
+ try
+ {
+ await Page.Locator("#CancelWizard").ClickAsync();
+ }
+ catch
+ {
+ await TakeScreenshot("flaky-FundStoreWallet.png");
+ throw;
+ }
return addressStr;
}
@@ -1005,6 +1013,8 @@ namespace BTCPayServer.Tests
public GlobalSearchPMO GlobalSearch => new GlobalSearchPMO(this);
+ public SearchFiltersPMO SearchFilters => new SearchFiltersPMO(this);
+
public async Task WaitLoggedIn()
{
if (StoreId is null)
diff --git a/BTCPayServer.Tests/PlaywrightTests.cs b/BTCPayServer.Tests/PlaywrightTests.cs
index ec964a2..60658b6 100644
--- a/BTCPayServer.Tests/PlaywrightTests.cs
+++ b/BTCPayServer.Tests/PlaywrightTests.cs
@@ -351,7 +351,16 @@ namespace BTCPayServer.Tests
await s.AddLightningNode(LightningTestImplementation.CoreLightning, false);
await s.GoToLightningSettings();
// LNURL is true by default
- await Expect(s.Page.Locator("#LNURLEnabled")).ToBeCheckedAsync();
+ try
+ {
+ await Expect(s.Page.Locator("#LNURLEnabled")).ToBeCheckedAsync();
+ }
+ catch
+ {
+ await s.TakeScreenshot("flaky-canlnurl.png");
+ throw;
+ }
+
await s.Page.CheckAsync("#LUD12Enabled");
await s.ClickPagePrimary();
@@ -1379,9 +1388,7 @@ namespace BTCPayServer.Tests
await Expect(s.Page.Locator("table tbody tr")).ToHaveCountAsync(2);
// Filter by Title
- await s.Page.FillAsync("input[name='SearchText']", paymentRequestTitle);
- await s.Page.PressAsync("input[name='SearchText']", "Enter");
- await s.Page.WaitForLoadStateAsync();
+ await s.SearchFilters.FillSearchText(paymentRequestTitle);
await Expect(s.Page.Locator("table tbody tr")).ToHaveCountAsync(1);
Assert.Contains(paymentRequestTitle, await s.Page.Locator("table tbody tr").First.InnerTextAsync());
@@ -1390,16 +1397,13 @@ namespace BTCPayServer.Tests
await s.Page.ClickAsync("#StatusOptionsToggle");
await s.Page.ClickAsync("*:has-text('Settled')");
await s.Page.WaitForLoadStateAsync();
- await Expect(s.Page.Locator("input[name='SearchText']"))
- .ToHaveValueAsync(paymentRequestTitle);
+ await s.SearchFilters.AssertSearchText(paymentRequestTitle);
var urlAfterStatusFilter = new Uri(s.Page.Url);
var qsAfterStatusFilter = HttpUtility.ParseQueryString(urlAfterStatusFilter.Query);
Assert.Equal(paymentRequestTitle, qsAfterStatusFilter["SearchText"]);
// Filter by Amount
- await s.Page.FillAsync("input[name='SearchText']", "0.1");
- await s.Page.PressAsync("input[name='SearchText']", "Enter");
- await s.Page.WaitForLoadStateAsync();
+ await s.SearchFilters.FillSearchText("0.1");
var rowsAfterAmountSearch = s.Page.Locator("table tbody tr");
await Expect(rowsAfterAmountSearch).ToHaveCountAsync(1);
@@ -1407,19 +1411,16 @@ namespace BTCPayServer.Tests
Assert.Contains(paymentRequestTitle, amountRowText);
// Filter by Id
- await s.Page.FillAsync("input[name='SearchText']", payReqId);
- await s.Page.PressAsync("input[name='SearchText']", "Enter");
- await s.Page.WaitForLoadStateAsync();
+ await s.SearchFilters.FillSearchText(payReqId);
var rowsAfterIdSearch = s.Page.Locator("table tbody tr");
await Expect(rowsAfterIdSearch).ToHaveCountAsync(1);
var idRowText = await rowsAfterIdSearch.First.InnerTextAsync();
Assert.Contains(paymentRequestTitle, idRowText);
// Clear All
- await Expect(s.Page.Locator("#clearAllFiltersBtn")).ToHaveCountAsync(1);
- await s.Page.ClickAsync("#clearAllFiltersBtn");
- await s.Page.WaitForLoadStateAsync();
- await Expect(s.Page.Locator("input[name='SearchText']")).ToHaveValueAsync(string.Empty);
+ await Expect(s.SearchFilters.ClearAllFiltersButton).ToHaveCountAsync(1);
+ await s.SearchFilters.ClearAllFilters();
+ await s.SearchFilters.AssertSearchText(string.Empty);
var urlAfterClearAll = new Uri(s.Page.Url);
var qsAfterClearAll = HttpUtility.ParseQueryString(urlAfterClearAll.Query);
Assert.True(string.IsNullOrEmpty(qsAfterClearAll["SearchText"]));
@@ -1440,7 +1441,7 @@ namespace BTCPayServer.Tests
//Filter by Label
await s.Page.ClickAsync("#menu-item-PaymentRequests");
await s.Page.WaitForLoadStateAsync();
- await s.Page.ClickAsync("#LabelSelectorToggle");
+ await s.SearchFilters.LabelSelectorToggle.ClickAsync();
await s.Page.ClickAsync($"#LabelSelectorMenu button:has-text(\"{labelName}\")");
await s.Page.WaitForLoadStateAsync();
await TestUtils.EventuallyAsync(async () =>
@@ -1454,7 +1455,7 @@ namespace BTCPayServer.Tests
// Report
await s.Page.ClickAsync("#view-report");
await Expect(s.Page.Locator("#ReportViewOptionsToggle")).ToContainTextAsync("Requests");
- await Expect(s.Page.Locator("#DateRangeSelector")).ToContainTextAsync("This month");
+ await Expect(s.SearchFilters.DateRangeSelector).ToContainTextAsync("This month");
await Expect(s.Page.Locator("#ReportViewOptionsToggle")).ToContainTextAsync("Requests");
await s.Page.WaitForSelectorAsync("#app table tbody tr");
await Expect(s.Page.Locator("#app table tbody tr").Filter(new LocatorFilterOptions { HasText = "Payment Request" })).ToHaveCountAsync(2);
@@ -1511,8 +1512,8 @@ namespace BTCPayServer.Tests
await s.Page.ReloadAsync();
await s.Page.WaitForLoadStateAsync();
- await s.Page.WaitForSelectorAsync("#LabelSelectorToggle");
- await s.Page.ClickAsync("#LabelSelectorToggle");
+ await s.SearchFilters.LabelSelectorToggle.WaitForAsync();
+ await s.SearchFilters.LabelSelectorToggle.ClickAsync();
var labelItems = await s.Page.Locator("#LabelSelectorMenu .label-filter-text").AllInnerTextsAsync();
var matches = labelItems.Where(t => t.Equals(labelOriginal, StringComparison.OrdinalIgnoreCase)).ToArray();
Assert.Single(matches);
diff --git a/BTCPayServer.Tests/WalletTests.cs b/BTCPayServer.Tests/WalletTests.cs
index 23434ba..a57aecb 100644
--- a/BTCPayServer.Tests/WalletTests.cs
+++ b/BTCPayServer.Tests/WalletTests.cs
@@ -741,7 +741,7 @@ public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper)
}
await s.GoToWalletTransactions(s.WalletId);
- await s.Page.ClickAsync("#LabelSelectorToggle");
+ await s.SearchFilters.LabelSelectorToggle.ClickAsync();
await s.Page.Locator("#LabelSelectorMenu").WaitForAsync();
await s.Page.Locator("#LabelSearch").WaitForAsync();
@@ -779,7 +779,7 @@ public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper)
await s.InWalletTransactions().AssertHasLabels(targetLabel);
// The label dropdown exposes a "Manage Labels" link that navigates to the wallet labels page (#7252)
- await s.Page.ClickAsync("#LabelSelectorToggle");
+ await s.SearchFilters.LabelSelectorToggle.ClickAsync();
await s.Page.ClickAsync("#LabelSelectorMenu a:has-text('Manage Labels')");
var walletId = new WalletId(s.StoreId , "BTC");
await s.Page.WaitForURLAsync(s.ServerUri + $"wallets/{walletId}/labels");
@@ -834,13 +834,11 @@ public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper)
});
await s.GoToWalletTransactions(s.WalletId);
- await s.Page.FillAsync("#SearchText", targetSearchText);
- await s.Page.PressAsync("#SearchText", "Enter");
- await s.Page.WaitForLoadStateAsync();
+ await s.SearchFilters.FillSearchText(targetSearchText);
await TestUtils.EventuallyAsync(async () =>
{
- Assert.Equal(targetSearchText, await s.Page.InputValueAsync("#SearchText"));
+ await s.SearchFilters.AssertSearchText(targetSearchText);
var urlAfterSearch = new Uri(s.Page.Url);
var qsAfterSearch = HttpUtility.ParseQueryString(urlAfterSearch.Query);
@@ -848,12 +846,11 @@ public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper)
Assert.True(string.IsNullOrEmpty(qsAfterSearch["SearchTerm"]));
});
- await s.Page.ClickAsync("#LabelSelector");
- await s.Page.ClickAsync($"#LabelSelector .label-filter-item span:has-text('{targetLabel}')");
+ await s.SearchFilters.SelectLabel(targetLabel);
- await Expect(s.Page.Locator("#SearchText")).ToHaveValueAsync(targetSearchText);
- Assert.Contains($"label:{targetLabel}", await s.Page.InputValueAsync("#SearchTerm"));
- await Expect(s.Page.Locator("#LabelSelectorToggle")).ToContainTextAsync(targetLabel);
+ await s.SearchFilters.AssertSearchText(targetSearchText);
+ Assert.Contains($"label:{targetLabel}", await s.SearchFilters.SearchTermValue());
+ await Expect(s.SearchFilters.LabelSelectorToggle).ToContainTextAsync(targetLabel);
var urlAfterLabelFilter = new Uri(s.Page.Url);
var qsAfterLabelFilter = HttpUtility.ParseQueryString(urlAfterLabelFilter.Query);
@@ -891,13 +888,11 @@ public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper)
var targetSearchText = transactions[0].TransactionHash.ToString()[..12];
await s.GoToWalletTransactions(s.WalletId);
- await s.Page.FillAsync("#SearchText", targetSearchText);
- await s.Page.PressAsync("#SearchText", "Enter");
- await s.Page.WaitForLoadStateAsync();
+ await s.SearchFilters.FillSearchText(targetSearchText);
await TestUtils.EventuallyAsync(async () =>
{
- Assert.Equal(targetSearchText, await s.Page.InputValueAsync("#SearchText"));
+ await s.SearchFilters.AssertSearchText(targetSearchText);
var urlAfterSearch = new Uri(s.Page.Url);
var qsAfterSearch = HttpUtility.ParseQueryString(urlAfterSearch.Query);
@@ -905,13 +900,12 @@ public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper)
Assert.True(string.IsNullOrEmpty(qsAfterSearch["SearchTerm"]));
});
- await s.Page.ClickAsync("#DateRangeSelector");
- await s.Page.ClickAsync("#DateRangeDropdown button:has-text('This month')");
+ await s.SearchFilters.SelectDateRangePreset("This month");
- await Expect(s.Page.Locator("#SearchText")).ToHaveValueAsync(targetSearchText);
- await Expect(s.Page.Locator("#DateRangeSelector")).ToHaveTextAsync("This month");
+ await s.SearchFilters.AssertSearchText(targetSearchText);
+ await Expect(s.SearchFilters.DateRangeSelector).ToHaveTextAsync("This month");
- Assert.Contains("daterange:thismonth", await s.Page.InputValueAsync("#SearchTerm"));
+ Assert.Contains("daterange:thismonth", await s.SearchFilters.SearchTermValue());
var urlAfterPreset = new Uri(s.Page.Url);
var qsAfterPreset = HttpUtility.ParseQueryString(urlAfterPreset.Query);
@@ -960,11 +954,10 @@ public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper)
});
await s.GoToWalletTransactions(s.WalletId);
- await s.Page.ClickAsync("#LabelSelector");
- await s.Page.ClickAsync("#LabelSelector button:text-is('No Label')");
+ await s.SearchFilters.SelectNoLabel();
- await Expect(s.Page.Locator("#LabelSelectorToggle")).ToContainTextAsync("No Label");
- Assert.Contains("nolabel:true", await s.Page.InputValueAsync("#SearchTerm"));
+ await Expect(s.SearchFilters.LabelSelectorToggle).ToContainTextAsync("No Label");
+ Assert.Contains("nolabel:true", await s.SearchFilters.SearchTermValue());
var urlAfterNoLabelFilter = new Uri(s.Page.Url);
var qsAfterNoLabelFilter = HttpUtility.ParseQueryString(urlAfterNoLabelFilter.Query);
@@ -1036,16 +1029,14 @@ public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper)
});
await s.GoToWalletTransactions(s.WalletId);
- await s.Page.ClickAsync("#DirectionDropdown");
- await s.Page.ClickAsync("#DirectionDropdown button:has-text('Outgoing')");
- await s.Page.ClickAsync("#LabelSelector");
- await s.Page.ClickAsync($"#LabelSelector .label-filter-item span:has-text('{targetLabel}')");
+ await SelectWalletDirection(s, "Outgoing");
+ await s.SearchFilters.SelectLabel(targetLabel);
await s.Page.WaitForLoadStateAsync();
- await Expect(s.Page.Locator("#DirectionOptionsToggle")).ToContainTextAsync("Outgoing");
- await Expect(s.Page.Locator("#LabelSelectorToggle")).ToContainTextAsync(targetLabel);
+ await AssertDirectionToggle(s, "Outgoing");
+ await Expect(s.SearchFilters.LabelSelectorToggle).ToContainTextAsync(targetLabel);
- var hiddenSearchTerm = await s.Page.InputValueAsync("#SearchTerm");
+ var hiddenSearchTerm = await s.SearchFilters.SearchTermValue();
Assert.Contains("direction:out", hiddenSearchTerm);
Assert.Contains($"label:{targetLabel}", hiddenSearchTerm);
@@ -1099,14 +1090,13 @@ public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper)
});
await s.GoToWalletTransactions(s.WalletId);
- Assert.Equal(string.Empty, await s.Page.InputValueAsync("#SearchText"));
+ await s.SearchFilters.AssertSearchText(string.Empty);
- await s.Page.ClickAsync("#DirectionDropdown");
- await s.Page.ClickAsync("#DirectionDropdown button:has-text('Outgoing')");
+ await SelectWalletDirection(s, "Outgoing");
- await Expect(s.Page.Locator("#DirectionOptionsToggle")).ToContainTextAsync("Outgoing");
- Assert.Contains("direction:out", await s.Page.InputValueAsync("#SearchTerm"));
- await Expect(s.Page.Locator("#SearchText")).ToHaveValueAsync(string.Empty);
+ await AssertDirectionToggle(s, "Outgoing");
+ Assert.Contains("direction:out", await s.SearchFilters.SearchTermValue());
+ await s.SearchFilters.AssertSearchText(string.Empty);
await Expect(s.Page.Locator(".transaction-row .amount-col .text-danger").First).ToBeVisibleAsync();
await Expect(s.Page.Locator(".transaction-row .amount-col .text-success")).ToHaveCountAsync(0);
}
@@ -1116,6 +1106,13 @@ public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper)
public async Task CanClearAllWalletTransactionFiltersAndSearchText()
{
await using var s = CreatePlaywrightTester();
+
+ async Task AssertDateRangeTimeZone(string timeZone)
+ {
+ await s.SearchFilters.OpenDateRange();
+ await Expect(s.SearchFilters.DateRangeTimeZone).ToHaveValueAsync(timeZone);
+ }
+
await s.StartAsync();
await s.Server.ExplorerNode.GenerateAsync(1);
await s.RegisterNewUser(true);
@@ -1162,44 +1159,37 @@ public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper)
await s.GoToWalletTransactions(s.WalletId);
- await s.Page.ClickAsync("#DateRangeSelector");
+ await s.SearchFilters.OpenDateRange();
var browserTimeZone = await s.Page.EvaluateAsync<string>("() => Intl.DateTimeFormat().resolvedOptions().timeZone");
- await Expect(s.Page.Locator("#DateRangeTimeZone")).ToHaveValueAsync(browserTimeZone + " (Default)");
- await s.Page.ClickAsync("#DateRangeTimeZone");
- await Expect(s.Page.Locator("#DateRangeTimeZone")).ToHaveValueAsync(string.Empty);
- await Expect(s.Page.Locator("#DateRangeTimeZone")).ToHaveAttributeAsync("placeholder", browserTimeZone + " (Default)");
- await s.Page.PressAsync("#DateRangeTimeZone", "Tab");
- await Expect(s.Page.Locator("#DateRangeTimeZone")).ToHaveValueAsync(browserTimeZone + " (Default)");
+ await Expect(s.SearchFilters.DateRangeTimeZone).ToHaveValueAsync(browserTimeZone + " (Default)");
+ await s.SearchFilters.DateRangeTimeZone.ClickAsync();
+ await Expect(s.SearchFilters.DateRangeTimeZone).ToHaveValueAsync(string.Empty);
+ await Expect(s.SearchFilters.DateRangeTimeZone).ToHaveAttributeAsync("placeholder", browserTimeZone + " (Default)");
+ await s.SearchFilters.DateRangeTimeZone.PressAsync("Tab");
+ await Expect(s.SearchFilters.DateRangeTimeZone).ToHaveValueAsync(browserTimeZone + " (Default)");
const string selectedTimeZone = "America/New_York";
- await s.Page.FillAsync("#DateRangeTimeZone", selectedTimeZone);
- await s.Page.PressAsync("#DateRangeTimeZone", "Enter");
- await s.Page.WaitForLoadStateAsync();
- await s.Page.ClickAsync("#DateRangeSelector");
- await Expect(s.Page.Locator("#DateRangeTimeZone")).ToHaveValueAsync(selectedTimeZone);
+ await s.SearchFilters.SelectTimeZone(selectedTimeZone);
+ await AssertDateRangeTimeZone(selectedTimeZone);
- await s.Page.FillAsync("#SearchText", targetLabel);
- await s.Page.PressAsync("#SearchText", "Enter");
- await s.Page.ClickAsync("#DirectionDropdown");
- await s.Page.ClickAsync("#DirectionDropdown button:has-text('Outgoing')");
+ await s.SearchFilters.FillSearchText(targetLabel);
+ await SelectWalletDirection(s, "Outgoing");
- await Expect(s.Page.Locator("#SearchText")).ToHaveValueAsync(targetLabel);
- var searchTermWithFilters = await s.Page.InputValueAsync("#SearchTerm");
+ await s.SearchFilters.AssertSearchText(targetLabel);
+ var searchTermWithFilters = await s.SearchFilters.SearchTermValue();
Assert.Contains("direction:out", searchTermWithFilters);
Assert.Contains($"timezone:{selectedTimeZone}", searchTermWithFilters);
- await Expect(s.Page.Locator("#DirectionOptionsToggle")).ToContainTextAsync("Outgoing");
+ await AssertDirectionToggle(s, "Outgoing");
await Expect(s.Page.Locator(".transaction-row")).ToHaveCountAsync(1);
- await Expect(s.Page.Locator("#clearAllFiltersBtn")).ToHaveCountAsync(1);
+ await Expect(s.SearchFilters.ClearAllFiltersButton).ToHaveCountAsync(1);
- await s.Page.ClickAsync("#clearAllFiltersBtn");
- await s.Page.WaitForLoadStateAsync();
+ await s.SearchFilters.ClearAllFilters();
- await Expect(s.Page.Locator("#SearchText")).ToHaveValueAsync(string.Empty);
- await Expect(s.Page.Locator("#SearchTerm")).ToHaveValueAsync($"timezone:{selectedTimeZone}");
- await s.Page.ClickAsync("#DateRangeSelector");
- await Expect(s.Page.Locator("#DateRangeTimeZone")).ToHaveValueAsync(selectedTimeZone);
- await Expect(s.Page.Locator("#DirectionOptionsToggle")).ToContainTextAsync("All Directions");
- await Expect(s.Page.Locator("#clearAllFiltersBtn")).ToHaveCountAsync(0);
+ await s.SearchFilters.AssertSearchText(string.Empty);
+ await Expect(s.SearchFilters.SearchTerm).ToHaveValueAsync($"timezone:{selectedTimeZone}");
+ await AssertDateRangeTimeZone(selectedTimeZone);
+ await AssertDirectionToggle(s, "All Directions");
+ await Expect(s.SearchFilters.ClearAllFiltersButton).ToHaveCountAsync(0);
Assert.True(await s.Page.Locator(".transaction-row").CountAsync() >= 2);
await Expect(s.Page.Locator(".transaction-row .amount-col .text-danger").First).ToBeVisibleAsync();
await Expect(s.Page.Locator(".transaction-row .amount-col .text-success").First).ToBeVisibleAsync();
@@ -1210,8 +1200,7 @@ public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper)
Assert.Equal($"timezone:{selectedTimeZone}", qsAfterClearAll["SearchTerm"]);
await s.GoToInvoices(s.StoreId);
- await s.Page.ClickAsync("#DateRangeSelector");
- await Expect(s.Page.Locator("#DateRangeTimeZone")).ToHaveValueAsync(selectedTimeZone);
+ await AssertDateRangeTimeZone(selectedTimeZone);
}
[Fact]
@@ -1278,24 +1267,17 @@ public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper)
});
await s.GoToWalletTransactions(s.WalletId);
- await s.Page.FillAsync("#SearchText", targetSearchText);
- await s.Page.PressAsync("#SearchText", "Enter");
- await s.Page.WaitForLoadStateAsync();
+ await s.SearchFilters.FillSearchText(targetSearchText);
- await s.Page.ClickAsync("#DirectionDropdown");
- await s.Page.ClickAsync("#DirectionDropdown button:has-text('Outgoing')");
+ await SelectWalletDirection(s, "Outgoing");
- await s.Page.ClickAsync("#LabelSelector");
- await s.Page.ClickAsync($"#LabelSelector .label-filter-item span:has-text('{targetLabel}')");
+ await s.SearchFilters.SelectLabel(targetLabel);
- await s.Page.ClickAsync("#DateRangeSelector");
- await s.Page.ClickAsync("[data-bs-target='#customRangeModal']");
- await s.Page.EvaluateAsync("() => { document.getElementById('dtpStartDate').value = '2026-03-01T12:34:56'; }");
- await s.Page.ClickAsync("#btnCustomRangeDate");
+ await s.SearchFilters.SelectCustomStartDate("2026-03-01T12:34:56");
- await Expect(s.Page.Locator("#SearchText")).ToHaveValueAsync(targetSearchText);
+ await s.SearchFilters.AssertSearchText(targetSearchText);
- var hiddenSearchTerm = await s.Page.InputValueAsync("#SearchTerm");
+ var hiddenSearchTerm = await s.SearchFilters.SearchTermValue();
Assert.Contains("direction:out", hiddenSearchTerm);
Assert.Contains($"label:{targetLabel}", hiddenSearchTerm);
Assert.Contains("startdate:2026-03-01T12:34:56", hiddenSearchTerm);
@@ -1321,6 +1303,15 @@ public class WalletTests(ITestOutputHelper helper) : UnitTestBase(helper)
}
}
+ private static Task AssertDirectionToggle(PlaywrightTester tester, string text) =>
+ Expect(tester.Page.Locator(".wallet-transactions__direction-toggle")).ToContainTextAsync(text);
+
+ private static async Task SelectWalletDirection(PlaywrightTester tester, string direction)
+ {
+ await tester.Page.ClickAsync(".wallet-transactions__direction");
+ await tester.Page.ClickAsync($".wallet-transactions__direction-option:has-text('{direction}')");
+ }
+
[Fact]
[Trait("Playwright", "Playwright-2")]
public async Task CanUseCoinSelection()
diff --git a/BTCPayServer/Components/ClearAllFilters/Default.cshtml b/BTCPayServer/Components/ClearAllFilters/Default.cshtml
index 3133ec8..2b11f0c 100644
--- a/BTCPayServer/Components/ClearAllFilters/Default.cshtml
+++ b/BTCPayServer/Components/ClearAllFilters/Default.cshtml
@@ -8,7 +8,7 @@
@if (Model is not null && !IsEmpty(Model)) {
<button id="clearAllFiltersBtn"
type="submit" name="FilterCommand" value="reset"
- class="btn btn-secondary"
+ class="btn btn-secondary clear-all-filters__button"
style="min-width: 7rem;"
title="@StringLocalizer["Clear all filters"]">
<span class="align-middle" text-translate="true">Clear All</span>
diff --git a/BTCPayServer/Components/DateRangeSelector/Default.cshtml b/BTCPayServer/Components/DateRangeSelector/Default.cshtml
index 0c7c5be..e11a507 100644
--- a/BTCPayServer/Components/DateRangeSelector/Default.cshtml
+++ b/BTCPayServer/Components/DateRangeSelector/Default.cshtml
@@ -17,8 +17,8 @@
};
}
-<div class="dropdown" id="DateRangeDropdown">
- <button id="DateRangeSelector" class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret" type="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
+<div class="dropdown date-range-selector" id="DateRangeDropdown">
+ <button id="DateRangeSelector" class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret date-range-selector__toggle" type="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
@if (Model.HasDateFilter)
{
var filter = filters.FirstOrDefault(f => Model.HasDateRange(f.DateRange));
@@ -51,22 +51,22 @@
<span text-translate="true">All Time</span>
}
</button>
- <div class="dropdown-menu" aria-labelledby="DateRangeSelector">
+ <div class="dropdown-menu date-range-selector__menu" aria-labelledby="DateRangeSelector">
<div class="px-3 py-2" style="min-width: 18rem;">
- <button type="submit" id="DateRangeTimeZoneButton" name="FilterCommand" class="d-none"></button>
- <label for="DateRangeTimeZone" class="form-label small text-muted mb-1" text-translate="true">Query timezone</label>
+ <button type="submit" id="DateRangeTimeZoneButton" name="FilterCommand" class="d-none date-range-selector__timezone-submit"></button>
+ <label for="DateRangeTimeZone" class="form-label small text-muted mb-1 date-range-selector__timezone-label" text-translate="true">Query timezone</label>
<!-- The input will get filled automatically by the browser (thanks to the timezone attribute) if the search string doesn't have a timezone -->
- <input id="DateRangeTimeZone" class="form-control form-control-sm" timezone value="@Model.Search.GetExplicitTimeZone()" list="DateRangeTimeZoneOptions" autocomplete="off" />
- <datalist id="DateRangeTimeZoneOptions">
+ <input id="DateRangeTimeZone" class="form-control form-control-sm date-range-selector__timezone" timezone value="@Model.Search.GetExplicitTimeZone()" list="DateRangeTimeZoneOptions" autocomplete="off" />
+ <datalist id="DateRangeTimeZoneOptions" class="date-range-selector__timezone-options">
@foreach (var timeZone in Model.TimeZones)
{
if (timeZone.Id is null)
{
- <option id="DateRangeTimeZone-DefaultOption" value="Default"><span text-translate="true">Detect from browser</span> <span>(<span browser-timezone></span>)</span></option>
+ <option id="DateRangeTimeZone-DefaultOption" class="date-range-selector__timezone-option date-range-selector__timezone-option--default" value="Default"><span text-translate="true">Detect from browser</span> <span>(<span browser-timezone></span>)</span></option>
}
else
{
- <option value="@timeZone.Id">@timeZone.Name</option>
+ <option class="date-range-selector__timezone-option" value="@timeZone.Id">@timeZone.Name</option>
}
}
</datalist>
@@ -74,18 +74,18 @@
<hr class="dropdown-divider" />
@if (Model.ShowAllTime)
{
- <button type="submit" name="FilterCommand" value="alltime" class="dropdown-item @(!Model.HasDateFilter ? "custom-active" : string.Empty)" text-translate="true">All Time</button>
+ <button type="submit" name="FilterCommand" value="alltime" class="dropdown-item date-range-selector__preset @(!Model.HasDateFilter ? "custom-active" : string.Empty)" text-translate="true">All Time</button>
<hr class="dropdown-divider" />
}
@foreach (var filter in filters)
{
- <button type="submit" name="FilterCommand" value="set-daterange:@filter.DateRange" class="dropdown-item @(Model.HasDateRange(@filter.DateRange) ? "custom-active" : string.Empty)" text-translate="true">@filter.Label</button>
+ <button type="submit" name="FilterCommand" value="set-daterange:@filter.DateRange" class="dropdown-item date-range-selector__preset @(Model.HasDateRange(@filter.DateRange) ? "custom-active" : string.Empty)" text-translate="true">@filter.Label</button>
}
- <button type="button" class="dropdown-item @(Model.HasCustomDateFilter ? "custom-active" : string.Empty)" data-bs-toggle="modal" data-bs-target="#customRangeModal" text-translate="true">Custom range</button>
+ <button type="button" class="dropdown-item date-range-selector__custom-range @(Model.HasCustomDateFilter ? "custom-active" : string.Empty)" data-bs-toggle="modal" data-bs-target="#customRangeModal" text-translate="true">Custom range</button>
</div>
</div>
-<div class="modal fade" id="customRangeModal" tabindex="-1" role="dialog" aria-labelledby="customRangeModalTitle" aria-hidden="true" data-bs-backdrop="static">
+<div class="modal fade date-range-selector__custom-range-modal" id="customRangeModal" tabindex="-1" role="dialog" aria-labelledby="customRangeModalTitle" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered" role="document" style="max-width: 550px;">
<div class="modal-content">
<div class="modal-header">
@@ -99,7 +99,7 @@
<label for="dtpStartDate" class="col-sm-3 col-form-label" text-translate="true">Start Date</label>
<div class="col-sm-9">
<div class="input-group">
- <input id="dtpStartDate" class="form-control flatdtpicker" type="datetime-local"
+ <input id="dtpStartDate" class="form-control flatdtpicker date-range-selector__custom-range-start" type="datetime-local"
data-fdtp='{ "altInput": true, "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "minuteIncrement": 1, "defaultHour": 0 }'
value="@Model.StartDateInputValue"
placeholder="@StringLocalizer["Start Date"]" />
@@ -113,7 +113,7 @@
<label for="dtpEndDate" class="col-sm-3 col-form-label" text-translate="true">End Date</label>
<div class="col-sm-9">
<div class="input-group">
- <input id="dtpEndDate" class="form-control flatdtpicker" type="datetime-local"
+ <input id="dtpEndDate" class="form-control flatdtpicker date-range-selector__custom-range-end" type="datetime-local"
data-fdtp='{ "altInput": true, "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "minuteIncrement": 1, "defaultHour": 0 }'
value="@Model.EndDateInputValue"
placeholder="@StringLocalizer["End Date"]" />
@@ -125,7 +125,7 @@
</div>
</div>
<div class="modal-footer">
- <button id="btnCustomRangeDate" type="button" class="btn btn-primary" text-translate="true">Filter</button>
+ <button id="btnCustomRangeDate" type="button" class="btn btn-primary date-range-selector__custom-range-submit" text-translate="true">Filter</button>
</div>
</div>
</div>
@@ -133,9 +133,12 @@
<script>
(function() {
- var dateRangeTimeZone = document.getElementById('DateRangeTimeZone');
- var defaultOption = document.getElementById('DateRangeTimeZone-DefaultOption');
- var tzSubmit = document.getElementById("DateRangeTimeZoneButton");
+ var dateRangeTimeZone = document.querySelector('.date-range-selector__timezone');
+ var defaultOption = document.querySelector('.date-range-selector__timezone-option--default');
+ var tzSubmit = document.querySelector('.date-range-selector__timezone-submit');
+ var customRangeSubmit = document.querySelector('.date-range-selector__custom-range-submit');
+ var customRangeStart = document.querySelector('.date-range-selector__custom-range-start');
+ var customRangeEnd = document.querySelector('.date-range-selector__custom-range-end');
var browserTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
var defaultValue = browserTz + ' (' + @Safe.Json(StringLocalizer["Default"].Value) + ')';
defaultOption.value = defaultValue;
@@ -190,12 +193,12 @@
}
});
- document.getElementById('btnCustomRangeDate').addEventListener('click', function () {
- const dtpStartDate = document.getElementById("dtpStartDate").value;
- const dtpEndDate = document.getElementById("dtpEndDate").value;
+ customRangeSubmit.addEventListener('click', function () {
+ const dtpStartDate = customRangeStart.value;
+ const dtpEndDate = customRangeEnd.value;
if (!dtpStartDate && !dtpEndDate) {
- document.getElementById("dtpStartDate").nextElementSibling.focus();
+ customRangeStart.nextElementSibling.focus();
return;
}
@@ -210,7 +213,7 @@
const cleanedSearchTerm = @Safe.Json(DateRangeSelectorModel.RemoveDatePreset(Model.Search.ToString(SearchStringFormat.OnlyUIFilters)));
const combinedSearchTerm = [cleanedSearchTerm, dateFilters.join(',')].filter(Boolean).join(',');
- const searchTermInput = document.getElementById("SearchTerm");
+ const searchTermInput = customRangeStart.closest('form').querySelector('.search-string-input__term');
searchTermInput.value = combinedSearchTerm;
searchTermInput.closest('form').submit();
});
diff --git a/BTCPayServer/Components/LabelSelector/Default.cshtml b/BTCPayServer/Components/LabelSelector/Default.cshtml
index 2fe4bc0..0b16b6a 100644
--- a/BTCPayServer/Components/LabelSelector/Default.cshtml
+++ b/BTCPayServer/Components/LabelSelector/Default.cshtml
@@ -36,8 +36,8 @@
}
</style>
- <div class="dropdown" id="LabelSelector">
- <button id="LabelSelectorToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret" type="button" data-bs-toggle="dropdown" aria-expanded="false">
+ <div class="dropdown label-selector" id="LabelSelector">
+ <button id="label-selector__toggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret label-selector__toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
@if (Model.LabelFilterCount > 1)
{
<span>@StringLocalizer["{0} Labels", Model.LabelFilterCount]</span>
@@ -56,7 +56,7 @@
<span text-translate="true">All Labels</span>
}
</button>
- <ul class="dropdown-menu" id="LabelSelectorMenu" aria-labelledby="LabelSelectorToggle" style="min-width:280px">
+ <ul class="dropdown-menu label-selector__menu" id="LabelSelectorMenu" aria-labelledby="label-selector__toggle" style="min-width:280px">
<li class="px-3 py-2 pb-2@(Model.Labels.Count > LabelSelector.MaxVisibleLabels ? string.Empty : " d-none")" id="LabelSearchContainer">
<div class="input-group border rounded">
<span class="input-group-text border-0 text-muted px-2" style="background:transparent">
@@ -72,15 +72,15 @@
@if (Model.AllowNoLabelFilter)
{
<li>
- <button type="submit" name="FilterCommand" value="nolabel" class="dropdown-item @(Model.HasNoLabelFilter ? "custom-active" : string.Empty)" text-translate="true">No Label</button>
+ <button type="submit" name="FilterCommand" value="nolabel" class="dropdown-item label-selector__no-label @(Model.HasNoLabelFilter ? "custom-active" : string.Empty)" text-translate="true">No Label</button>
</li>
}
<li><hr class="dropdown-divider"></li>
<li id="LabelFilterResultsAnchor" class="d-none"></li>
@foreach (var label in Model.InitialLabels)
{
- <li class="label-filter-item">
- <button type="submit" name="FilterCommand" value="addlabel:@label.Text" class="dropdown-item transaction-label-text label-filter-link@(Model.ActiveLabels.Any(value => value.Equals(label.Text, StringComparison.OrdinalIgnoreCase)) ? " active" : string.Empty)" style="--btcpay-dropdown-link-active-bg:@label.Color;--btcpay-dropdown-link-active-color:@label.TextColor;">
+ <li class="label-filter-item label-selector__item">
+ <button type="submit" name="FilterCommand" value="addlabel:@label.Text" class="dropdown-item transaction-label-text label-filter-link label-selector__item-link@(Model.ActiveLabels.Any(value => value.Equals(label.Text, StringComparison.OrdinalIgnoreCase)) ? " active" : string.Empty)" style="--btcpay-dropdown-link-active-bg:@label.Color;--btcpay-dropdown-link-active-color:@label.TextColor;">
<span class="label-filter-text">@label.Text</span>
<small class="label-filter-count@(label.UsageCount > 0 ? string.Empty : " d-none")">@label.UsageCount</small>
</button>
@@ -95,8 +95,8 @@
}
</ul>
<template id="label-filter-item-template">
- <li class="label-filter-item">
- <button type="submit" name="FilterCommand" class="dropdown-item transaction-label-text label-filter-link">
+ <li class="label-filter-item label-selector__item">
+ <button type="submit" name="FilterCommand" class="dropdown-item transaction-label-text label-filter-link label-selector__item-link">
<span class="label-filter-text"></span>
<small class="label-filter-count d-none"></small>
</button>
diff --git a/BTCPayServer/Components/SearchStringInput/Default.cshtml b/BTCPayServer/Components/SearchStringInput/Default.cshtml
index b9f8751..da53cc3 100644
--- a/BTCPayServer/Components/SearchStringInput/Default.cshtml
+++ b/BTCPayServer/Components/SearchStringInput/Default.cshtml
@@ -1,6 +1,6 @@
@model BTCPayServer.Components.SearchStringInput.SearchStringInput.Model
-<input id="SearchTerm" name="SearchTerm" type="hidden" value="@Model.SearchString.ToString(SearchStringFormat.OnlyUIFilters)"/>
-<input id="SearchText" name="SearchText" class="form-control" value="@Model.SearchString.ToString(SearchStringFormat.ExceptUIFilters)" placeholder="@Model.Placeholder" />
+<input id="SearchTerm" name="SearchTerm" class="search-string-input__term" type="hidden" value="@Model.SearchString.ToString(SearchStringFormat.OnlyUIFilters)"/>
+<input id="SearchText" name="SearchText" class="form-control search-string-input__text" value="@Model.SearchString.ToString(SearchStringFormat.ExceptUIFilters)" placeholder="@Model.Placeholder" />
@* So hitting enter will not submit another button in the page *@
-<button type="submit" class="d-none"></button>
+<button type="submit" class="d-none search-string-input__submit"></button>
diff --git a/BTCPayServer/Models/BasePagingViewModel.cs b/BTCPayServer/Models/BasePagingViewModel.cs
index 9e10377..df70bd6 100644
--- a/BTCPayServer/Models/BasePagingViewModel.cs
+++ b/BTCPayServer/Models/BasePagingViewModel.cs
@@ -42,6 +42,7 @@ namespace BTCPayServer.Models
public SearchString GetSearch()
{
var search = SearchString.Combine([SearchTerm, SearchText]);
+ search.RemoveDups("daterange");
if (FilterCommand is not null)
RunFilterCommand(search);
AddUIFilters(search);
diff --git a/BTCPayServer/Plugins/Wallets/Views/UIWallets/WalletTransactions.cshtml b/BTCPayServer/Plugins/Wallets/Views/UIWallets/WalletTransactions.cshtml
index 21e9f07..a1d10b8 100644
--- a/BTCPayServer/Plugins/Wallets/Views/UIWallets/WalletTransactions.cshtml
+++ b/BTCPayServer/Plugins/Wallets/Views/UIWallets/WalletTransactions.cshtml
@@ -39,7 +39,7 @@
}
}
- #SearchText {
+ .search-string-input__text {
flex-shrink: 0;
min-width: 200px;
max-width: 320px;
@@ -76,7 +76,7 @@
}
@@media (max-width: 568px) {
- #SearchText {
+ .search-string-input__text {
width: 100%;
}
}
@@ -205,8 +205,8 @@
<input id="Count" name="Count" type="hidden" value="@Model.Count" />
<vc:search-string-input search-string="Model.Search" placeholder="@StringLocalizer["Search transactions..."]"></vc:search-string-input>
- <div class="dropdown" id="DirectionDropdown">
- <button id="DirectionOptionsToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret" type="button" data-bs-toggle="dropdown" aria-expanded="false">
+ <div class="dropdown wallet-transactions__direction" id="DirectionDropdown">
+ <button id="wallet-transactions__direction-toggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret wallet-transactions__direction-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
@if (hasIncomingOnlyFilter)
{
<span text-translate="true">Incoming</span>
@@ -220,10 +220,10 @@
<span text-translate="true">All Directions</span>
}
</button>
- <div class="dropdown-menu" aria-labelledby="DirectionOptionsToggle">
- <button type="submit" name="FilterCommand" value="unset:direction" class="dropdown-item @((!hasIncomingOnlyFilter && !hasOutgoingOnlyFilter) ? "custom-active" : string.Empty)" text-translate="true">All Directions</button>
- <button type="submit" name="FilterCommand" value="set:direction=in" class="dropdown-item @(hasIncomingOnlyFilter ? "custom-active" : string.Empty)" text-translate="true">Incoming</button>
- <button type="submit" name="FilterCommand" value="set:direction=out" class="dropdown-item @(hasOutgoingOnlyFilter ? "custom-active" : string.Empty)" text-translate="true">Outgoing</button>
+ <div class="dropdown-menu wallet-transactions__direction-menu" aria-labelledby="wallet-transactions__direction-toggle">
+ <button type="submit" name="FilterCommand" value="unset:direction" class="dropdown-item wallet-transactions__direction-option @((!hasIncomingOnlyFilter && !hasOutgoingOnlyFilter) ? "custom-active" : string.Empty)" text-translate="true">All Directions</button>
+ <button type="submit" name="FilterCommand" value="set:direction=in" class="dropdown-item wallet-transactions__direction-option @(hasIncomingOnlyFilter ? "custom-active" : string.Empty)" text-translate="true">Incoming</button>
+ <button type="submit" name="FilterCommand" value="set:direction=out" class="dropdown-item wallet-transactions__direction-option @(hasOutgoingOnlyFilter ? "custom-active" : string.Empty)" text-translate="true">Outgoing</button>
</div>
</div>
<vc:label-selector labels="Model.Labels" search="Model.Search" allow-no-label-filter="true" manage-label-link="@walletLabelsLink"></vc:label-selector>
diff --git a/BTCPayServer/SearchString.cs b/BTCPayServer/SearchString.cs
index 742d926..97bb240 100644
--- a/BTCPayServer/SearchString.cs
+++ b/BTCPayServer/SearchString.cs
@@ -283,5 +283,15 @@ namespace BTCPayServer
}
public string? GetExplicitTimeZone() => GetFilterString("timezone");
+
+ public void RemoveDups(string filter)
+ {
+ filter = NormalizeKey(filter);
+ if (Filters.TryGetValue(filter, out var c) && c.Count > 1)
+ {
+ Filters.Remove(filter);
+ Filters.Add(filter, c.First());
+ }
+ }
}
}
diff --git a/BTCPayServer/Views/Shared/_Footer.cshtml b/BTCPayServer/Views/Shared/_Footer.cshtml
index 3224ba9..db77d23 100644
--- a/BTCPayServer/Views/Shared/_Footer.cshtml
+++ b/BTCPayServer/Views/Shared/_Footer.cshtml
@@ -15,7 +15,7 @@
<vc:icon symbol="social-twitter"/>
<span>Twitter</span>
</a>
- <a href="https://t.me/btcpayserver" target="_blank" rel="noreferrer noopener">
+ <a href="https://telegram.me/btcpayserver" target="_blank" rel="noreferrer noopener">
<vc:icon symbol="social-telegram"/>
<span>Telegram</span>
</a>
diff --git a/BTCPayServer/Views/UIInvoice/ListInvoices.cshtml b/BTCPayServer/Views/UIInvoice/ListInvoices.cshtml
index b36feef..6d24147 100644
--- a/BTCPayServer/Views/UIInvoice/ListInvoices.cshtml
+++ b/BTCPayServer/Views/UIInvoice/ListInvoices.cshtml
@@ -23,7 +23,7 @@
padding-left: 1rem;
text-align: left;
}
- #SearchText {
+ .search-string-input__text {
flex-shrink: 0;
min-width: 200px;
max-width: 300px;
@@ -37,7 +37,7 @@
z-index: 1080;
}
@@media (max-width: 568px) {
- #SearchText {
+ .search-string-input__text {
width: 100%;
}
}
diff --git a/BTCPayServer/Views/UINotifications/Index.cshtml b/BTCPayServer/Views/UINotifications/Index.cshtml
index 5095831..6cbeba1 100644
--- a/BTCPayServer/Views/UINotifications/Index.cshtml
+++ b/BTCPayServer/Views/UINotifications/Index.cshtml
@@ -15,7 +15,7 @@
text-align: left;
}
@@media (max-width: 568px) {
- #SearchText {
+ .search-string-input__text {
width: 100%;
}
}
diff --git a/BTCPayServer/Views/UIReports/StoreReports.cshtml b/BTCPayServer/Views/UIReports/StoreReports.cshtml
index c5efe03..9dc4fce 100644
--- a/BTCPayServer/Views/UIReports/StoreReports.cshtml
+++ b/BTCPayServer/Views/UIReports/StoreReports.cshtml
@@ -47,7 +47,7 @@
<div class="col-xl-8 col-xxl-constrain">
<form id="StoreReportsSearch" class="d-flex flex-wrap align-items-center gap-3 mb-4" asp-action="StoreReports" method="get">
<button id="StoreReportsSearchSubmit" type="submit" name="FilterCommand" class="d-none"></button>
- <input id="SearchTerm" name="SearchTerm" type="hidden" value="@Model.SearchTerm" />
+ <input id="SearchTerm" name="SearchTerm" class="search-string-input__term" type="hidden" value="@Model.SearchTerm" />
<input name="SearchText" type="hidden" value="@Model.SearchText" />
<div class="dropdown" id="ReportViewDropdown">
<button id="ReportViewOptionsToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Why this scored 20/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.