mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Wallet transactions: Add label manager (#4796)
* Wallet transactions: Add label manager * Update BTCPayServer/Views/UIWallets/WalletTransactions.cshtml Co-authored-by: Andrew Camilleri <evilkukka@gmail.com> * Add rich label info * Fixes * support labels in wallet send * add labels to tx info page * Remove noscript parts * Allow click on transaction label info * update psbt info labelstyling * revert red pixel fix as it broke all --------- Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
This commit is contained in:
parent
8635fcfe84
commit
95f3e429b4
18 changed files with 453 additions and 227 deletions
|
@ -1374,11 +1374,11 @@ namespace BTCPayServer.Tests
|
|||
// Can add a label?
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input ")).Click();
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).Click();
|
||||
await Task.Delay(500);
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input ")).SendKeys("test-label" + Keys.Enter);
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).SendKeys("test-label" + Keys.Enter);
|
||||
await Task.Delay(500);
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input ")).SendKeys("label2" + Keys.Enter);
|
||||
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).SendKeys("label2" + Keys.Enter);
|
||||
});
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace BTCPayServer
|
|||
var bg = ColorTranslator.FromHtml(bgColor);
|
||||
int bgDelta = Convert.ToInt32((bg.R * 0.299) + (bg.G * 0.587) + (bg.B * 0.114));
|
||||
Color color = (255 - bgDelta < nThreshold) ? Color.Black : Color.White;
|
||||
return ColorTranslator.ToHtml(color);
|
||||
return ColorTranslator.ToHtml(color).ToLowerInvariant();
|
||||
}
|
||||
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
|
||||
public static readonly ColorPalette Default = new ColorPalette(new string[] {
|
||||
|
|
|
@ -1,104 +1,22 @@
|
|||
@using NBitcoin.DataEncoders
|
||||
@using NBitcoin
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@model BTCPayServer.Components.LabelManager.LabelViewModel
|
||||
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery
|
||||
@{
|
||||
var commonCall = Model.ObjectId.Type + Model.ObjectId.Id;
|
||||
var elementId = "a" + Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
||||
var fetchUrl = Url.Action("GetLabels", "UIWallets", new {
|
||||
walletId = Model.WalletObjectId.WalletId,
|
||||
excludeTypes = Safe.Json(Model.ExcludeTypes)
|
||||
});
|
||||
var updateUrl = Model.AutoUpdate? Url.Action("UpdateLabels", "UIWallets", new {
|
||||
walletId = Model.WalletObjectId.WalletId
|
||||
}): string.Empty;
|
||||
}
|
||||
|
||||
<link href="~/vendor/tom-select/tom-select.bootstrap5.min.css" rel="stylesheet">
|
||||
<script src="~/vendor/tom-select/tom-select.complete.min.js"></script>
|
||||
<script>
|
||||
const updateUrl = @Safe.Json(Url.Action("UpdateLabels", "UIWallets", new {
|
||||
Model.ObjectId.WalletId
|
||||
}));
|
||||
const getUrl = @Safe.Json(@Url.Action("GetLabels", "UIWallets", new {
|
||||
walletId = Model.ObjectId.WalletId,
|
||||
excludeTypes = true
|
||||
}));
|
||||
const commonCall = @Safe.Json(commonCall);
|
||||
const elementId = @Safe.Json(elementId);
|
||||
if (!window[commonCall]) {
|
||||
window[commonCall] = fetch(getUrl, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}).then(response => {
|
||||
return response.json();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const element = document.querySelector(`#${elementId}`);
|
||||
|
||||
if (element) {
|
||||
const labelsFetchTask = await window[commonCall];
|
||||
const config = {
|
||||
create: true,
|
||||
items: @Safe.Json(Model.SelectedLabels),
|
||||
options: labelsFetchTask,
|
||||
valueField: "label",
|
||||
labelField: "label",
|
||||
searchField: "label",
|
||||
allowEmptyOption: false,
|
||||
closeAfterSelect: false,
|
||||
persist: true,
|
||||
render: {
|
||||
option: function(data, escape) {
|
||||
return `<div ${data.color? `style='background-color:${data.color}; color:${data.textColor}'`: ""}>${escape(data.label)}</div>`;
|
||||
},
|
||||
item: function(data, escape) {
|
||||
return `<div ${data.color? `style='background-color:${data.color}; color:${data.textColor}'`: ""}>${escape(data.label)}</div>`;
|
||||
}
|
||||
},
|
||||
onItemAdd: (val) => {
|
||||
window[commonCall] = window[commonCall].then(labels => {
|
||||
return [...labels, { label: val }]
|
||||
});
|
||||
|
||||
document.dispatchEvent(new CustomEvent(`${commonCall}-option-added`, {
|
||||
detail: val
|
||||
}));
|
||||
},
|
||||
onChange: async (values) => {
|
||||
select.lock();
|
||||
try {
|
||||
const response = await fetch(updateUrl, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
address: @Safe.Json(Model.ObjectId.Id),
|
||||
labels: select.items
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not OK');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('There has been a problem with your fetch operation:', error);
|
||||
} finally {
|
||||
select.unlock();
|
||||
}
|
||||
}
|
||||
};
|
||||
const select = new TomSelect(element, config);
|
||||
|
||||
document.addEventListener(`${commonCall}-option-added`, evt => {
|
||||
if (!(evt.detail in select.options)) {
|
||||
select.addOption({
|
||||
label: evt.detail
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<input id="@elementId" placeholder="Select labels to associate with this object" autocomplete="off" class="form-control label-manager"/>
|
||||
<input id="@elementId" placeholder="Select labels" autocomplete="off" value="@string.Join(",", Model.SelectedLabels)"
|
||||
class="only-for-js form-control label-manager ts-wrapper @(Model.DisplayInline ? "ts-inline" : "")"
|
||||
data-fetch-url="@fetchUrl"
|
||||
data-update-url="@updateUrl"
|
||||
data-wallet-id="@Model.WalletObjectId.WalletId"
|
||||
data-wallet-object-id="@Model.WalletObjectId.Id"
|
||||
data-wallet-object-type="@Model.WalletObjectId.Type"
|
||||
data-select-element="@Model.SelectElement"
|
||||
data-labels='@Safe.Json(Model.RichLabelInfo)' />
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
@ -5,14 +7,25 @@ namespace BTCPayServer.Components.LabelManager
|
|||
{
|
||||
public class LabelManager : ViewComponent
|
||||
{
|
||||
public IViewComponentResult Invoke(WalletObjectId walletObjectId, string[] selectedLabels)
|
||||
public IViewComponentResult Invoke(WalletObjectId walletObjectId, string[] selectedLabels, bool excludeTypes = true, bool displayInline = false, Dictionary<string, RichLabelInfo> richLabelInfo = null, bool autoUpdate = true, string selectElement = null)
|
||||
{
|
||||
var vm = new LabelViewModel
|
||||
{
|
||||
ObjectId = walletObjectId,
|
||||
SelectedLabels = selectedLabels
|
||||
ExcludeTypes = excludeTypes,
|
||||
WalletObjectId = walletObjectId,
|
||||
SelectedLabels = selectedLabels?? Array.Empty<string>(),
|
||||
DisplayInline = displayInline,
|
||||
RichLabelInfo = richLabelInfo,
|
||||
AutoUpdate = autoUpdate,
|
||||
SelectElement = selectElement
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
public class RichLabelInfo
|
||||
{
|
||||
public string Link { get; set; }
|
||||
public string Tooltip { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Components.LabelManager
|
||||
|
@ -5,6 +6,11 @@ namespace BTCPayServer.Components.LabelManager
|
|||
public class LabelViewModel
|
||||
{
|
||||
public string[] SelectedLabels { get; set; }
|
||||
public WalletObjectId ObjectId { get; set; }
|
||||
public WalletObjectId WalletObjectId { get; set; }
|
||||
public bool ExcludeTypes { get; set; }
|
||||
public bool DisplayInline { get; set; }
|
||||
public Dictionary<string, RichLabelInfo> RichLabelInfo { get; set; }
|
||||
public bool AutoUpdate { get; set; }
|
||||
public string SelectElement { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ using BTCPayServer.Abstractions.Constants;
|
|||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models;
|
||||
|
@ -266,7 +267,7 @@ namespace BTCPayServer.Controllers
|
|||
ModelState.Remove(nameof(vm.PSBT));
|
||||
ModelState.Remove(nameof(vm.FileName));
|
||||
ModelState.Remove(nameof(vm.UploadedPSBTFile));
|
||||
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
|
||||
await FetchTransactionDetails(walletId,derivationSchemeSettings, vm, network);
|
||||
return View("WalletPSBTDecoded", vm);
|
||||
|
||||
case "save-psbt":
|
||||
|
@ -320,7 +321,7 @@ namespace BTCPayServer.Controllers
|
|||
return await _payjoinClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, cts.Token);
|
||||
}
|
||||
|
||||
private async Task FetchTransactionDetails(DerivationSchemeSettings derivationSchemeSettings, WalletPSBTReadyViewModel vm, BTCPayNetwork network)
|
||||
private async Task FetchTransactionDetails(WalletId walletId, DerivationSchemeSettings derivationSchemeSettings, WalletPSBTReadyViewModel vm, BTCPayNetwork network)
|
||||
{
|
||||
var psbtObject = PSBT.Parse(vm.SigningContext.PSBT, network.NBitcoinNetwork);
|
||||
if (!psbtObject.IsAllFinalized())
|
||||
|
@ -371,17 +372,29 @@ namespace BTCPayServer.Controllers
|
|||
vm.Positive = balanceChange >= Money.Zero;
|
||||
}
|
||||
vm.Inputs = new List<WalletPSBTReadyViewModel.InputViewModel>();
|
||||
var inputToObjects = new Dictionary<uint, ObjectTypeId[]>();
|
||||
var outputToObjects = new Dictionary<string, ObjectTypeId>();
|
||||
foreach (var input in psbtObject.Inputs)
|
||||
{
|
||||
var inputVm = new WalletPSBTReadyViewModel.InputViewModel();
|
||||
vm.Inputs.Add(inputVm);
|
||||
var txOut = input.GetTxOut();
|
||||
var mine = input.HDKeysFor(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath).Any();
|
||||
var balanceChange2 = input.GetTxOut()?.Value ?? Money.Zero;
|
||||
var balanceChange2 = txOut?.Value ?? Money.Zero;
|
||||
if (mine)
|
||||
balanceChange2 = -balanceChange2;
|
||||
inputVm.BalanceChange = ValueToString(balanceChange2, network);
|
||||
inputVm.Positive = balanceChange2 >= Money.Zero;
|
||||
inputVm.Index = (int)input.Index;
|
||||
|
||||
var walletObjectIds = new List<ObjectTypeId>();
|
||||
walletObjectIds.Add(new ObjectTypeId(WalletObjectData.Types.Utxo, input.PrevOut.ToString()));
|
||||
walletObjectIds.Add(new ObjectTypeId(WalletObjectData.Types.Tx, input.PrevOut.Hash.ToString()));
|
||||
var address = txOut?.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString();
|
||||
if(address != null)
|
||||
walletObjectIds.Add(new ObjectTypeId(WalletObjectData.Types.Address, address));
|
||||
inputToObjects.Add(input.Index, walletObjectIds.ToArray());
|
||||
|
||||
}
|
||||
vm.Destinations = new List<WalletPSBTReadyViewModel.DestinationViewModel>();
|
||||
foreach (var output in psbtObject.Outputs)
|
||||
|
@ -395,6 +408,10 @@ namespace BTCPayServer.Controllers
|
|||
dest.Balance = ValueToString(balanceChange2, network);
|
||||
dest.Positive = balanceChange2 >= Money.Zero;
|
||||
dest.Destination = output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString() ?? output.ScriptPubKey.ToString();
|
||||
var address = output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString();
|
||||
if(address != null)
|
||||
outputToObjects.Add(dest.Destination, new ObjectTypeId(WalletObjectData.Types.Address, address));
|
||||
|
||||
}
|
||||
|
||||
if (psbtObject.TryGetFee(out var fee))
|
||||
|
@ -420,6 +437,38 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
vm.SetErrors(errors);
|
||||
}
|
||||
|
||||
var combinedTypeIds = inputToObjects.Values.SelectMany(ids => ids).Concat(outputToObjects.Values)
|
||||
.DistinctBy(id => $"{id.Type}:{id.Id}").ToArray();
|
||||
|
||||
var labelInfo = await WalletRepository.GetWalletTransactionsInfo(walletId, combinedTypeIds);
|
||||
foreach (KeyValuePair<uint,ObjectTypeId[]> inputToObject in inputToObjects)
|
||||
{
|
||||
var keys = inputToObject.Value.Select(id => id.Id).ToArray();
|
||||
WalletTransactionInfo ix = null;
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (!labelInfo.TryGetValue(key, out var i)) continue;
|
||||
if (ix is null)
|
||||
{
|
||||
ix = i;
|
||||
}
|
||||
else
|
||||
{
|
||||
ix.Merge(i);
|
||||
}
|
||||
}
|
||||
if (ix is null) continue;
|
||||
var input = vm.Inputs.First(model => model.Index == inputToObject.Key);
|
||||
input.Labels = ix.LabelColors;
|
||||
}
|
||||
foreach (var outputToObject in outputToObjects)
|
||||
{
|
||||
if (!labelInfo.TryGetValue(outputToObject.Value.Id, out var ix)) continue;
|
||||
var destination = vm.Destinations.First(model => model.Destination == outputToObject.Key);
|
||||
destination.Labels = ix.LabelColors;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[HttpPost("{walletId}/psbt/ready")]
|
||||
|
@ -439,7 +488,7 @@ namespace BTCPayServer.Controllers
|
|||
if (derivationSchemeSettings == null)
|
||||
return NotFound();
|
||||
|
||||
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
|
||||
await FetchTransactionDetails(walletId,derivationSchemeSettings, vm, network);
|
||||
|
||||
switch (command)
|
||||
{
|
||||
|
@ -570,7 +619,7 @@ namespace BTCPayServer.Controllers
|
|||
BackUrl = vm.BackUrl
|
||||
});
|
||||
case "decode":
|
||||
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
|
||||
await FetchTransactionDetails(walletId,derivationSchemeSettings, vm, network);
|
||||
return View("WalletPSBTDecoded", vm);
|
||||
default:
|
||||
vm.Errors.Add("Unknown command");
|
||||
|
|
|
@ -235,8 +235,7 @@ namespace BTCPayServer.Controllers
|
|||
var model = new ListTransactionsViewModel { Skip = skip, Count = count };
|
||||
model.Labels.AddRange(
|
||||
(await WalletRepository.GetWalletLabels(walletId))
|
||||
.Select(c => (c.Label, c.Color, ColorPalette.Default.TextColor(c.Color)))
|
||||
);
|
||||
.Select(c => (c.Label, c.Color, ColorPalette.Default.TextColor(c.Color))));
|
||||
|
||||
if (labelFilter != null)
|
||||
{
|
||||
|
@ -733,6 +732,18 @@ namespace BTCPayServer.Controllers
|
|||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
|
||||
foreach (var transactionOutput in vm.Outputs.Where(output => output.Labels?.Any() is true))
|
||||
{
|
||||
var labels = transactionOutput.Labels.Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
|
||||
var walletObjectAddress = new WalletObjectId(walletId, WalletObjectData.Types.Address, transactionOutput.DestinationAddress.ToLowerInvariant());
|
||||
var obj = await WalletRepository.GetWalletObject(walletObjectAddress);
|
||||
if (obj is null)
|
||||
{
|
||||
await WalletRepository.EnsureWalletObject(walletObjectAddress);
|
||||
}
|
||||
await WalletRepository.AddWalletObjectLabels(walletObjectAddress, labels);
|
||||
}
|
||||
|
||||
var derivationScheme = GetDerivationSchemeSettings(walletId);
|
||||
if (derivationScheme is null)
|
||||
return NotFound();
|
||||
|
@ -1324,18 +1335,21 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
public class UpdateLabelsRequest
|
||||
{
|
||||
public string? Address { get; set; }
|
||||
public string? Id { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public string[]? Labels { get; set; }
|
||||
}
|
||||
|
||||
[HttpPost("{walletId}/update-labels")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> UpdateLabels([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, [FromBody] UpdateLabelsRequest request)
|
||||
public async Task<IActionResult> UpdateLabels(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
|
||||
[FromBody] UpdateLabelsRequest request)
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.Address) || request.Labels is null)
|
||||
if (string.IsNullOrEmpty(request.Type) || string.IsNullOrEmpty(request.Id) || request.Labels is null)
|
||||
return BadRequest();
|
||||
|
||||
var objid = new WalletObjectId(walletId, WalletObjectData.Types.Address, request.Address);
|
||||
var objid = new WalletObjectId(walletId, request.Type, request.Id);
|
||||
var obj = await WalletRepository.GetWalletObject(objid);
|
||||
if (obj is null)
|
||||
{
|
||||
|
@ -1353,17 +1367,26 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
[HttpGet("{walletId}/labels")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> GetLabels( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, bool excludeTypes)
|
||||
public async Task<IActionResult> GetLabels(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
|
||||
bool excludeTypes,
|
||||
string? type = null,
|
||||
string? id = null)
|
||||
{
|
||||
|
||||
return Ok(( await WalletRepository.GetWalletLabels(walletId))
|
||||
.Where(l => !excludeTypes || !WalletObjectData.Types.AllTypes.Contains(l.Label))
|
||||
.Select(tuple => new
|
||||
{
|
||||
label = tuple.Label,
|
||||
color = tuple.Color,
|
||||
textColor = ColorPalette.Default.TextColor(tuple.Color)
|
||||
}));
|
||||
var walletObjectId = !string.IsNullOrEmpty(type) && !string.IsNullOrEmpty(id)
|
||||
? new WalletObjectId(walletId, type, id)
|
||||
: null;
|
||||
var labels = walletObjectId == null
|
||||
? await WalletRepository.GetWalletLabels(walletId)
|
||||
: await WalletRepository.GetWalletLabels(walletObjectId);
|
||||
return Ok(labels
|
||||
.Where(l => !excludeTypes || !WalletObjectData.Types.AllTypes.Contains(l.Label))
|
||||
.Select(tuple => new
|
||||
{
|
||||
label = tuple.Label,
|
||||
color = tuple.Color,
|
||||
textColor = ColorPalette.Default.TextColor(tuple.Color)
|
||||
}));
|
||||
}
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Services.Labels;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
|
@ -15,10 +14,10 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||
public string Link { get; set; }
|
||||
public bool Positive { get; set; }
|
||||
public string Balance { get; set; }
|
||||
public HashSet<TransactionTagModel> Tags { get; set; } = new HashSet<TransactionTagModel>();
|
||||
public HashSet<TransactionTagModel> Tags { get; set; } = new ();
|
||||
}
|
||||
public HashSet<(string Text, string Color, string TextColor)> Labels { get; set; } = new HashSet<(string Text, string Color, string TextColor)>();
|
||||
public List<TransactionViewModel> Transactions { get; set; } = new List<TransactionViewModel>();
|
||||
public HashSet<(string Text, string Color, string TextColor)> Labels { get; set; } = new ();
|
||||
public List<TransactionViewModel> Transactions { get; set; } = new ();
|
||||
public override int CurrentPageCount => Transactions.Count;
|
||||
public string CryptoCode { get; set; }
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||
public bool Positive { get; set; }
|
||||
public string Destination { get; set; }
|
||||
public string Balance { get; set; }
|
||||
public Dictionary<string, string> Labels { get; set; } = new();
|
||||
}
|
||||
|
||||
public class InputViewModel
|
||||
|
@ -24,6 +25,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||
public string Error { get; set; }
|
||||
public bool Positive { get; set; }
|
||||
public string BalanceChange { get; set; }
|
||||
public Dictionary<string, string> Labels { get; set; } = new();
|
||||
}
|
||||
public bool HasErrors => Inputs.Count == 0 || Inputs.Any(i => !string.IsNullOrEmpty(i.Error));
|
||||
public string BalanceChange { get; set; }
|
||||
|
|
|
@ -34,6 +34,8 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||
public bool SubtractFeesFromOutput { get; set; }
|
||||
|
||||
public string PayoutId { get; set; }
|
||||
|
||||
public string[] Labels { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
public decimal CurrentBalance { get; set; }
|
||||
public decimal ImmatureBalance { get; set; }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
@ -309,21 +310,33 @@ namespace BTCPayServer.Services
|
|||
#nullable enable
|
||||
|
||||
public async Task<(string Label, string Color)[]> GetWalletLabels(WalletId walletId)
|
||||
{
|
||||
return await GetWalletLabels(w =>
|
||||
w.WalletId == walletId.ToString() &&
|
||||
w.Type == WalletObjectData.Types.Label);
|
||||
}
|
||||
|
||||
public async Task<(string Label, string Color)[]> GetWalletLabels(WalletObjectId objectId)
|
||||
{
|
||||
return await GetWalletLabels(w =>
|
||||
w.WalletId == objectId.WalletId.ToString() &&
|
||||
w.Type == objectId.Type &&
|
||||
w.Id == objectId.Id);
|
||||
}
|
||||
|
||||
private async Task<(string Label, string Color)[]> GetWalletLabels(Expression<Func<WalletObjectData, bool>> predicate)
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
return (await
|
||||
ctx.WalletObjects.AsNoTracking().Where(w => w.WalletId == walletId.ToString() && w.Type == WalletObjectData.Types.Label)
|
||||
.ToArrayAsync())
|
||||
.Select(o =>
|
||||
{
|
||||
if (o.Data is null)
|
||||
{
|
||||
return (o.Id, ColorPalette.Default.DeterministicColor(o.Id));
|
||||
}
|
||||
return (o.Id,
|
||||
JObject.Parse(o.Data)["color"]?.Value<string>() ??
|
||||
ColorPalette.Default.DeterministicColor(o.Id));
|
||||
}).ToArray();
|
||||
return (await ctx.WalletObjects.AsNoTracking().Where(predicate).ToArrayAsync())
|
||||
.Select(FormatToLabel).ToArray();
|
||||
}
|
||||
|
||||
private (string Label, string Color) FormatToLabel(WalletObjectData o)
|
||||
{
|
||||
return o.Data is null
|
||||
? (o.Id, ColorPalette.Default.DeterministicColor(o.Id))
|
||||
: (o.Id,
|
||||
JObject.Parse(o.Data)["color"]?.Value<string>() ?? ColorPalette.Default.DeterministicColor(o.Id));
|
||||
}
|
||||
|
||||
public async Task<bool> RemoveWalletObjects(WalletObjectId walletObjectId)
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
@section PageHeadContent
|
||||
{
|
||||
<link href="~/main/qrcode.css" rel="stylesheet" asp-append-version="true"/>
|
||||
<link href="~/vendor/tom-select/tom-select.bootstrap5.min.css" asp-append-version="true" rel="stylesheet">
|
||||
<script src="~/vendor/tom-select/tom-select.complete.min.js" asp-append-version="true"></script>
|
||||
}
|
||||
|
||||
@section Navbar {
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
@inject BTCPayServer.Security.ContentSecurityPolicies csp
|
||||
@using Microsoft.AspNetCore.Mvc.ModelBinding
|
||||
@using BTCPayServer.Controllers
|
||||
@using BTCPayServer.Services
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Components.LabelManager
|
||||
@using BTCPayServer.Components.UIExtensionPoint
|
||||
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
|
||||
@model WalletSendModel
|
||||
@{
|
||||
|
@ -35,6 +39,8 @@
|
|||
border-bottom-right-radius: .2rem !important;
|
||||
}
|
||||
</style>
|
||||
<link href="~/vendor/tom-select/tom-select.bootstrap5.min.css" asp-append-version="true" rel="stylesheet">
|
||||
<script src="~/vendor/tom-select/tom-select.complete.min.js" asp-append-version="true"></script>
|
||||
}
|
||||
|
||||
@section PageFootContent
|
||||
|
@ -115,6 +121,21 @@
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Labels</label>
|
||||
<select asp-for="Outputs[0].Labels" class="d-none">
|
||||
@foreach (var t in Model.Outputs[0].Labels)
|
||||
{
|
||||
<option value="@t" selected></option>
|
||||
}
|
||||
</select>
|
||||
<vc:label-manager
|
||||
selected-labels="Model.Outputs[0].Labels"
|
||||
exclude-types="true"
|
||||
select-element="Outputs_0__Labels"
|
||||
wallet-object-id="new WalletObjectId(WalletId.Parse(walletId), WalletObjectData.Types.Address, Model.Outputs[0].DestinationAddress)"
|
||||
auto-update="false" />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -158,6 +179,21 @@
|
|||
<label asp-for="Outputs[index].SubtractFeesFromOutput" class="form-check-label"></label>
|
||||
<span asp-validation-for="Outputs[index].SubtractFeesFromOutput" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Labels</label>
|
||||
<select asp-for="Outputs[index].Labels" class="d-none">
|
||||
@foreach (var t in Model.Outputs[index].Labels)
|
||||
{
|
||||
<option value="@t" selected></option>
|
||||
}
|
||||
</select>
|
||||
<vc:label-manager
|
||||
selected-labels="Model.Outputs[index].Labels"
|
||||
exclude-types="true"
|
||||
select-element="Outputs_@(index)__Labels"
|
||||
wallet-object-id="new WalletObjectId(WalletId.Parse(walletId), WalletObjectData.Types.Address, Model.Outputs[index].DestinationAddress)"
|
||||
auto-update="false" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
}
|
||||
|
||||
@section PageHeadContent {
|
||||
<script src="~/vendor/tom-select/tom-select.complete.min.js" asp-append-version="true"></script>
|
||||
<link href="~/vendor/tom-select/tom-select.bootstrap5.min.css" asp-append-version="true" rel="stylesheet">
|
||||
<style>
|
||||
.smMaxWidth {
|
||||
max-width: 125px;
|
||||
|
@ -45,7 +47,6 @@
|
|||
@section PageFootContent {
|
||||
@*Without async, somehow selenium do not manage to click on links in this page*@
|
||||
<script src="~/modal/btcpay.js" asp-append-version="true" async></script>
|
||||
|
||||
@* Custom Range Modal *@
|
||||
<script>
|
||||
let observer = null;
|
||||
|
@ -92,7 +93,7 @@
|
|||
|
||||
if (response.ok) {
|
||||
const html = await response.text();
|
||||
$list.innerHTML += html;
|
||||
$list.insertAdjacentHTML('beforeend', html);
|
||||
skip = skipNext;
|
||||
|
||||
if ($loadMore) {
|
||||
|
@ -122,6 +123,7 @@
|
|||
|
||||
$indicator.classList.add('d-none');
|
||||
formatDateTimes(document.getElementById('switchTimeFormat').dataset.mode);
|
||||
initLabelManagers();
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
@model WalletPSBTReadyViewModel
|
||||
|
||||
<script src="~/js/wallet/wallet-camera-scanner.js" asp-append-version="true"></script>
|
||||
<script src="~/js/wallet/WalletSend.js" asp-append-version="true"></script>
|
||||
@if (Model.CanCalculateBalance)
|
||||
{
|
||||
<p class="lead text-center text-secondary">
|
||||
|
@ -15,6 +17,7 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th>Index</th>
|
||||
<th>Labels</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -31,8 +34,14 @@
|
|||
else
|
||||
{
|
||||
<td>@input.Index</td>
|
||||
}
|
||||
}<td>
|
||||
@foreach (var label in input.Labels)
|
||||
{
|
||||
<div class="transaction-label" style="--label-bg:@label.Value;--label-fg:@ColorPalette.Default.TextColor(label.Value)">@label.Key</div>
|
||||
}
|
||||
</td>
|
||||
<td class="text-end text-@(input.Positive ? "success" : "danger")">@input.BalanceChange</td>
|
||||
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
@ -45,6 +54,7 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th>Destination</th>
|
||||
<th>Labels</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -53,7 +63,15 @@
|
|||
{
|
||||
<tr>
|
||||
<td class="text-break">@destination.Destination</td>
|
||||
<td>
|
||||
|
||||
@foreach (var label in destination.Labels)
|
||||
{
|
||||
<div class="transaction-label" style="--label-bg:@label.Value;--label-fg:@ColorPalette.Default.TextColor(label.Value)">@label.Key</div>
|
||||
}
|
||||
</td>
|
||||
<td class="text-end text-@(destination.Positive ? "success" : "danger")">@destination.Balance</td>
|
||||
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
@using BTCPayServer.Services
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Components.LabelManager
|
||||
@model ListTransactionsViewModel
|
||||
|
||||
@{
|
||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
}
|
||||
@foreach (var transaction in Model.Transactions)
|
||||
{
|
||||
<tr>
|
||||
|
@ -10,33 +15,12 @@
|
|||
@transaction.Timestamp.ToBrowserDate()
|
||||
</td>
|
||||
<td class="text-start">
|
||||
@if (transaction.Tags.Any())
|
||||
{
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
@foreach (var label in transaction.Tags)
|
||||
{
|
||||
<div class="transaction-label" style="--label-bg:@label.Color;--label-fg:@label.TextColor;">
|
||||
<a asp-route-labelFilter="@label.Text">@label.Text</a>
|
||||
<form method="post" asp-action="ModifyTransaction" asp-route-walletId="@Context.GetRouteValue("walletId")">
|
||||
<input type="hidden" name="transactionId" value="@transaction.Id" />
|
||||
<button name="removelabel" type="submit" value="@label.Text" class="transaction-label-action">
|
||||
<vc:icon symbol="remove"/>
|
||||
</button>
|
||||
</form>
|
||||
@if (!string.IsNullOrEmpty(label.Link))
|
||||
{
|
||||
<a href="@label.Link" target="_blank" rel="noreferrer noopener" class="transaction-label-info transaction-details-icon"
|
||||
title="@label.Tooltip"
|
||||
data-bs-html="true"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-custom-class="transaction-label-tooltip">
|
||||
<vc:icon symbol="info"/>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<vc:label-manager
|
||||
wallet-object-id="new WalletObjectId(WalletId.Parse(walletId), WalletObjectData.Types.Tx, transaction.Id)"
|
||||
selected-labels="transaction.Tags.Select(t => t.Text).ToArray()"
|
||||
rich-label-info="transaction.Tags.Where(t=> !string.IsNullOrEmpty(t.Link)).ToDictionary(t => t.Text, t => new RichLabelInfo { Link = t.Link, Tooltip = t.Tooltip })"
|
||||
exclude-types="false"
|
||||
display-inline="true"/>
|
||||
</td>
|
||||
<td class="smMaxWidth text-truncate@(transaction.IsConfirmed ? "" : " unconf")">
|
||||
<a href="@transaction.Link" target="_blank" rel="noreferrer noopener">
|
||||
|
@ -52,35 +36,6 @@
|
|||
<td class="text-end text-danger">@transaction.Balance</td>
|
||||
}
|
||||
<td class="text-end">
|
||||
<div class="dropstart d-inline-block me-2">
|
||||
<span class="fa fa-tags cursor-pointer" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
|
||||
<div class="dropdown-menu">
|
||||
<form asp-action="ModifyTransaction" method="post" style="width:260px;" asp-route-walletId="@Context.GetRouteValue("walletId")">
|
||||
<input type="hidden" name="transactionId" value="@transaction.Id" />
|
||||
<div class="input-group input-group-sm p-2">
|
||||
<input name="addlabel" placeholder="Label name" maxlength="20" type="text" class="form-control form-control-sm" />
|
||||
<button type="submit" class="btn btn-primary btn-sm"><span class="fa fa-plus"></span></button>
|
||||
</div>
|
||||
@if (Model.Labels.Count > 0)
|
||||
{
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="py-2 px-3 d-flex flex-wrap gap-2">
|
||||
@foreach (var label in Model.Labels)
|
||||
{
|
||||
var isActive = transaction.Tags.Any(l => l.Text == label.Text);
|
||||
<button name="@(isActive ? "removelabel" : "addlabelclick")" type="submit" value="@label.Text"
|
||||
class="transaction-label@(isActive ? " active" : string.Empty)" style="--label-bg:@label.Color;--label-fg:@label.TextColor">
|
||||
<span>@label.Text</span>
|
||||
<span class="transaction-label-action">
|
||||
<vc:icon symbol="@(isActive ? "remove" : "new")"/>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropstart d-inline-block">
|
||||
@if (string.IsNullOrEmpty(transaction.Comment))
|
||||
{
|
||||
|
|
|
@ -698,23 +698,6 @@ a.store-powered-by:hover .logo-brand-dark {
|
|||
color: var(--btcpay-brand-tertiary);
|
||||
}
|
||||
|
||||
/* Tom Select */
|
||||
.ts-wrapper.form-control .ts-control {
|
||||
padding: .5rem .75rem .2rem .5rem !important;
|
||||
}
|
||||
|
||||
.ts-wrapper.form-control .ts-control > .item {
|
||||
margin: 0 .3rem .3rem 0 !important;
|
||||
}
|
||||
|
||||
.ts-wrapper.form-control .ts-control > input {
|
||||
margin: 0 0 .3rem .5rem !important;
|
||||
}
|
||||
|
||||
.form-floating .ts-wrapper {
|
||||
margin-top: 1.85rem;
|
||||
}
|
||||
|
||||
/* Payment Box */
|
||||
.payment-box {
|
||||
--qr-size: 256px;
|
||||
|
@ -773,18 +756,89 @@ a.store-powered-by:hover .logo-brand-dark {
|
|||
font-weight: var(--btcpay-font-weight-semibold);
|
||||
}
|
||||
|
||||
/* Tom Select */
|
||||
.ts-control, .ts-control input, .ts-dropdown {
|
||||
color: var(--btcpay-body-text-muted) !important;
|
||||
}
|
||||
|
||||
.ts-wrapper.form-control .ts-control {
|
||||
gap: .5rem;
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
.ts-wrapper.form-control:not(.ts-inline) .ts-control {
|
||||
padding: .5rem !important;
|
||||
border-radius: var(--btcpay-border-radius) !important;
|
||||
}
|
||||
|
||||
.ts-wrapper.form-control.focus:not(.ts-inline) .ts-control {
|
||||
border-color: var(--btcpay-form-border-focus);
|
||||
box-shadow: 0 0 0 1px var(--btcpay-form-border-focus) inset;
|
||||
}
|
||||
|
||||
.ts-wrapper.form-control.ts-inline,
|
||||
.ts-wrapper.form-control.ts-inline .ts-control {
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
min-height: auto !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.form-floating .ts-wrapper {
|
||||
margin-top: 1.85rem;
|
||||
}
|
||||
|
||||
.form-floating input.ts-wrapper.form-control:not(.ts-hidden-accessible) {
|
||||
padding: .5rem .75rem !important;
|
||||
height: 39px;
|
||||
}
|
||||
|
||||
.form-floating input.label-manager.form-control ~ label {
|
||||
opacity: 0.65;
|
||||
transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
|
||||
}
|
||||
|
||||
input.ts-wrapper.form-control:not(.ts-hidden-accessible,.ts-inline) {
|
||||
padding: .5rem .75rem !important;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.ts-dropdown.dropdown-menu,
|
||||
.ts-dropdown.dropdown-menu.form-control,
|
||||
.ts-dropdown.dropdown-menu.form-select {
|
||||
color: var(--btcpay-dropdown-color);
|
||||
background: var(--btcpay-dropdown-bg);
|
||||
border: var(--btcpay-dropdown-border-width) solid var(--btcpay-dropdown-border-color);
|
||||
border-radius: var(--btcpay-dropdown-border-radius);
|
||||
padding: var(--btcpay-dropdown-padding-y) var(--btcpay-dropdown-padding-x);
|
||||
min-width: var(--btcpay-dropdown-min-width);
|
||||
}
|
||||
|
||||
.ts-dropdown.dropdown-menu .ts-dropdown-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--btcpay-space-s);
|
||||
padding: 0 var(--btcpay-space-s);
|
||||
}
|
||||
|
||||
/* Transaction Labels */
|
||||
.transaction-label {
|
||||
.transaction-label,
|
||||
.ts-dropdown.dropdown-menu .transaction-label,
|
||||
.ts-dropdown.dropdown-menu .create.transaction-label,
|
||||
.ts-wrapper.form-control .ts-control > .item.transaction-label {
|
||||
display: inline-flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: var(--btcpay-space-xs);
|
||||
color: var(--btcpay-body-text);
|
||||
background: transparent;
|
||||
border: 1px solid var(--label-bg);
|
||||
border: 1px solid var(--label-bg, var(--btcpay-neutral-300));
|
||||
border-radius: var(--btcpay-border-radius-l);
|
||||
font-size: var(--btcpay-font-size-s);
|
||||
padding: var(--btcpay-space-xs) var(--btcpay-space-s);
|
||||
margin: 0;
|
||||
padding: 1px var(--btcpay-space-s);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
@ -796,9 +850,14 @@ a.store-powered-by:hover .logo-brand-dark {
|
|||
|
||||
.transaction-label:focus,
|
||||
.transaction-label:hover,
|
||||
.transaction-label.active {
|
||||
color: var(--label-fg);
|
||||
background: var(--label-bg);
|
||||
.transaction-label.active,
|
||||
.ts-dropdown.dropdown-menu .transaction-label:focus,
|
||||
.ts-dropdown.dropdown-menu .transaction-label:hover,
|
||||
.ts-wrapper.multi .ts-control > .item.transaction-label:focus,
|
||||
.ts-wrapper.multi .ts-control > .item.transaction-label:hover,
|
||||
.ts-wrapper.multi .ts-control > .item.transaction-label.active {
|
||||
color: var(--label-fg, var(--btcpay-body-text));
|
||||
background: var(--label-bg, var(--btcpay-neutral-300));
|
||||
}
|
||||
|
||||
.transaction-label:focus a,
|
||||
|
|
|
@ -24,6 +24,133 @@ const switchTimeFormat = event => {
|
|||
event.target.dataset.mode = mode;
|
||||
};
|
||||
|
||||
async function initLabelManager (elementId) {
|
||||
const element = document.getElementById(elementId);
|
||||
|
||||
const labelStyle = data =>
|
||||
data && data.color && data.textColor
|
||||
? `--label-bg:${data.color};--label-fg:${data.textColor}`
|
||||
: '--label-bg:var(--btcpay-neutral-300);--label-fg:var(--btcpay-neutral-800)'
|
||||
|
||||
if (element) {
|
||||
const { fetchUrl, updateUrl, walletId, walletObjectType, walletObjectId, labels,selectElement } = element.dataset;
|
||||
const commonCallId = `walletLabels-${walletId}`;
|
||||
if (!window[commonCallId]) {
|
||||
window[commonCallId] = fetch(fetchUrl, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
const selectElementI = document.getElementById(selectElement);
|
||||
const items = element.value.split(',').filter(x => !!x);
|
||||
const options = await window[commonCallId].then(labels => {
|
||||
const newItems = items.filter(item => !labels.find(label => label.label === item));
|
||||
labels = [...labels, ...newItems.map(item => ({ label: item }))];
|
||||
return labels;
|
||||
});
|
||||
const richInfo = labels ? JSON.parse(labels) : {};
|
||||
const config = {
|
||||
options,
|
||||
items,
|
||||
valueField: "label",
|
||||
labelField: "label",
|
||||
searchField: "label",
|
||||
create: true,
|
||||
persist: true,
|
||||
allowEmptyOption: false,
|
||||
closeAfterSelect: false,
|
||||
render: {
|
||||
dropdown (){
|
||||
return '<div class="dropdown-menu"></div>';
|
||||
},
|
||||
option_create: function(data, escape) {
|
||||
return `<div class="transaction-label create" style="${labelStyle(null)}">Add <strong>${escape(data.input)}</strong>…</div>`;
|
||||
},
|
||||
option (data, escape) {
|
||||
return `<div class="transaction-label" style="${labelStyle(data)}"><span>${escape(data.label)}</span></div>`;
|
||||
},
|
||||
item (data, escape) {
|
||||
const info = richInfo && richInfo[data.label];
|
||||
const additionalInfo = info
|
||||
? `<a href="${info.link}" target="_blank" rel="noreferrer noopener" class="transaction-label-info transaction-details-icon" title="${info.tooltip}" data-bs-html="true"
|
||||
data-bs-toggle="tooltip" data-bs-custom-class="transaction-label-tooltip"><svg role="img" class="icon icon-info"><use href="/img/icon-sprite.svg#info"></use></svg></a>`
|
||||
: '';
|
||||
const inner = `<span>${escape(data.label)}</span>${additionalInfo}`;
|
||||
return `<div class="transaction-label" style="${labelStyle(data)}">${inner}</div>`;
|
||||
}
|
||||
},
|
||||
onItemAdd (val) {
|
||||
window[commonCallId] = window[commonCallId].then(labels => {
|
||||
return [...labels, { label: val }]
|
||||
});
|
||||
|
||||
document.dispatchEvent(new CustomEvent(`${commonCallId}-option-added`, {
|
||||
detail: val
|
||||
}));
|
||||
},
|
||||
async onChange (values) {
|
||||
if(selectElementI){
|
||||
while (selectElementI.options.length > 0) {
|
||||
selectElementI.remove(0);
|
||||
}
|
||||
select.items.forEach((item) => {
|
||||
selectElementI.add(new Option(item, item, true, true));
|
||||
})
|
||||
}
|
||||
if(!updateUrl)
|
||||
return;
|
||||
select.lock();
|
||||
try {
|
||||
const response = await fetch(updateUrl, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: walletObjectId,
|
||||
type: walletObjectType,
|
||||
labels: select.items
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not OK');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('There has been a problem with your fetch operation:', error);
|
||||
} finally {
|
||||
select.unlock();
|
||||
}
|
||||
}
|
||||
};
|
||||
const select = new TomSelect(element, config);
|
||||
|
||||
element.parentElement.querySelectorAll('.ts-control .transaction-label a').forEach(lbl => {
|
||||
lbl.addEventListener('click', e => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
})
|
||||
|
||||
document.addEventListener(`${commonCallId}-option-added`, evt => {
|
||||
if (!(evt.detail in select.options)) {
|
||||
select.addOption({
|
||||
label: evt.detail
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const initLabelManagers = () => {
|
||||
// select only elements which haven't been initialized before, those without data-localized
|
||||
document.querySelectorAll("input.label-manager:not(.tomselected)").forEach($el => {
|
||||
initLabelManager($el.id)
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// sticky header
|
||||
const stickyHeader = document.querySelector('.sticky-header-setup + .sticky-header');
|
||||
|
@ -37,6 +164,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
|
||||
// localize all elements that have localizeDate class
|
||||
formatDateTimes();
|
||||
|
||||
initLabelManagers();
|
||||
|
||||
function updateTimeAgo(){
|
||||
var timeagoElements = $("[data-timeago-unixms]");
|
||||
|
|
Loading…
Add table
Reference in a new issue