Add Manage Labels link to Bitcoin Wallet labels (#7435)
What changed, and why it matters
This commit is a straightforward user-interface convenience change: it adds a 'Manage Labels' link inside the label dropdown on wallet transaction and payment request pages. There is no security-relevant behavior change in the diff itself.
No security action required; treat as a normal feature/UI improvement.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch extends the shared LabelSelector view component with an optional manageLabelLink parameter. WalletTransactions.cshtml now passes a link to the wallet labels page only if the current user passes the CanManageWalletSettings authorization check; otherwise the link is null and hidden. GetPaymentRequests.cshtml passes a link to payment-request labels unconditionally. A UI test verifies the new link navigates correctly. No controller, authorization, or data-handling logic is modified.
Changed components
BTCPayServer/Components/LabelSelector/Default.cshtmlBTCPayServer/Components/LabelSelector/LabelSelector.csBTCPayServer/Plugins/Wallets/Views/UIWallets/WalletTransactions.cshtmlBTCPayServer/Views/UIPaymentRequest/GetPaymentRequests.cshtmlInspect captured patch +29 / −6
diff --git a/BTCPayServer.Tests/WalletTests.cs b/BTCPayServer.Tests/WalletTests.cs
index 4dd549c..38efb32 100644
--- a/BTCPayServer.Tests/WalletTests.cs
+++ b/BTCPayServer.Tests/WalletTests.cs
@@ -778,6 +778,11 @@ 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.Page.ClickAsync("#LabelSelectorMenu a:has-text('Manage Labels')");
+ var walletId = new WalletId(s.StoreId , "BTC");
+ await s.Page.WaitForURLAsync(s.ServerUri + $"wallets/{walletId}/labels");
}
[Fact]
diff --git a/BTCPayServer/Components/LabelSelector/Default.cshtml b/BTCPayServer/Components/LabelSelector/Default.cshtml
index ad896ca..2fe4bc0 100644
--- a/BTCPayServer/Components/LabelSelector/Default.cshtml
+++ b/BTCPayServer/Components/LabelSelector/Default.cshtml
@@ -56,8 +56,8 @@
<span text-translate="true">All Labels</span>
}
</button>
- <ul class="dropdown-menu mt-1 py-0" id="LabelSelectorMenu" aria-labelledby="LabelSelectorToggle" style="min-width:280px">
- <li class="px-2 pt-2 pb-2@(Model.Labels.Count > LabelSelector.MaxVisibleLabels ? string.Empty : " d-none")" id="LabelSearchContainer">
+ <ul class="dropdown-menu" id="LabelSelectorMenu" aria-labelledby="LabelSelectorToggle" 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">
<vc:icon symbol="actions-search" />
@@ -86,6 +86,13 @@
</button>
</li>
}
+ @if (!string.IsNullOrEmpty(Model.ManageLabelLink))
+ {
+ <li><hr class="dropdown-divider"></li>
+ <li>
+ <a href="@Model.ManageLabelLink" class="dropdown-item d-flex align-items-center gap-2"><vc:icon symbol="settings" /><span text-translate="true">Manage Labels</span></a>
+ </li>
+ }
</ul>
<template id="label-filter-item-template">
<li class="label-filter-item">
diff --git a/BTCPayServer/Components/LabelSelector/LabelSelector.cs b/BTCPayServer/Components/LabelSelector/LabelSelector.cs
index 21c49b7..fc43cf9 100644
--- a/BTCPayServer/Components/LabelSelector/LabelSelector.cs
+++ b/BTCPayServer/Components/LabelSelector/LabelSelector.cs
@@ -16,6 +16,7 @@ public class LabelSelector : ViewComponent
public IViewComponentResult Invoke(
SearchString search,
bool allowNoLabelFilter = false,
+ string? manageLabelLink = null,
IEnumerable<LabelSelectorItemViewModel>? labels = null)
{
var allLabels = (labels ?? new List<LabelSelectorItemViewModel>())
@@ -40,7 +41,8 @@ public class LabelSelector : ViewComponent
Search = search,
Labels = allLabels,
InitialLabels = popular,
- AllowNoLabelFilter = allowNoLabelFilter
+ AllowNoLabelFilter = allowNoLabelFilter,
+ ManageLabelLink = manageLabelLink
});
}
public class LabelSelectorModel
@@ -52,6 +54,7 @@ public class LabelSelector : ViewComponent
public bool HasNoLabelFilter => Search.GetFilterBool("nolabel") is true;
public int LabelFilterCount => ActiveLabels.Length + (HasNoLabelFilter ? 1 : 0);
public bool AllowNoLabelFilter { get; init; }
+ public string? ManageLabelLink { get; init; }
}
public static void RunFilterCommand(SearchString search, string filterCommand)
diff --git a/BTCPayServer/Plugins/Wallets/Views/UIWallets/WalletTransactions.cshtml b/BTCPayServer/Plugins/Wallets/Views/UIWallets/WalletTransactions.cshtml
index 360ee9d..21e9f07 100644
--- a/BTCPayServer/Plugins/Wallets/Views/UIWallets/WalletTransactions.cshtml
+++ b/BTCPayServer/Plugins/Wallets/Views/UIWallets/WalletTransactions.cshtml
@@ -1,6 +1,9 @@
@using BTCPayServer.Client
-@using System
+@using BTCPayServer.Controllers
+@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Html
+@inject IAuthorizationService AuthorizationService
+
@model ListTransactionsViewModel
@{
@@ -16,6 +19,10 @@
var hasOutgoingOnlyFilter = hasOutgoingFilter && !hasIncomingFilter;
ViewData.SetLayoutModel(new LayoutModel($"{nameof(WalletsNavPages.Transactions)}-{Model.CryptoCode}", StringLocalizer["{0} Transactions", Model.CryptoCode])
.SetCategory(WellKnownCategories.ForWallet(Model.CryptoCode)));
+
+ var walletLabelsLink = @Url.Action(nameof(UIWalletsController.WalletLabels), new { walletId });
+ if (await AuthorizationService.AuthorizeAsync(User, WalletPolicies.CanManageWalletSettings) is not { Succeeded: true })
+ walletLabelsLink = null;
}
@section PageHeadContent {
@@ -219,7 +226,7 @@
<button type="submit" name="FilterCommand" value="set:direction=out" class="dropdown-item @(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"></vc:label-selector>
+ <vc:label-selector labels="Model.Labels" search="Model.Search" allow-no-label-filter="true" manage-label-link="@walletLabelsLink"></vc:label-selector>
<vc:date-range-selector search="Model.Search" custom-range-title='@StringLocalizer["Filter transactions by Custom Range"].Value'></vc:date-range-selector>
<vc:clear-all-filters search="Model.Search"></vc:clear-all-filters>
diff --git a/BTCPayServer/Views/UIPaymentRequest/GetPaymentRequests.cshtml b/BTCPayServer/Views/UIPaymentRequest/GetPaymentRequests.cshtml
index da54d89..6e02e9b 100644
--- a/BTCPayServer/Views/UIPaymentRequest/GetPaymentRequests.cshtml
+++ b/BTCPayServer/Views/UIPaymentRequest/GetPaymentRequests.cshtml
@@ -1,5 +1,6 @@
@using BTCPayServer.Services.PaymentRequests
@using BTCPayServer.Client
+@using BTCPayServer.Controllers
@using BTCPayServer.Services
@inject CallbackGenerator CallbackGenerator
@model BTCPayServer.Models.PaymentRequestViewModels.ListPaymentRequestsViewModel
@@ -82,7 +83,7 @@
<button type="submit" name="FilterCommand" value="set:includearchived=true" class="dropdown-item @(Model.Search.HasBooleanFilter("includearchived") ? "custom-active" : "")" id="StatusOptionsIncludeArchived" text-translate="true">Include Archived</button>
</div>
</div>
- <vc:label-selector labels="Model.Labels" search="Model.Search"></vc:label-selector>
+ <vc:label-selector labels="Model.Labels" search="Model.Search" manage-label-link="@Url.Action(nameof(UIPaymentRequestController.PaymentRequestLabels), new { storeId })"></vc:label-selector>
<vc:date-range-selector search="Model.Search" custom-range-title='@StringLocalizer["Filter payment requests by Custom Range"].Value'></vc:date-range-selector>
<vc:clear-all-filters search="Model.Search"></vc:clear-all-filters>
</form>
Why this scored 15/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.