Form System Flexibility improvements (#4774)

* Introduce very flexible form input system

* Refactorings after rebase

* Test fix

* Update BTCPayServer/Forms/FormDataService.cs

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
Andrew Camilleri 2023-04-04 04:01:34 +02:00 committed by GitHub
parent 11f05285a1
commit 60d6e98c67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 92 additions and 48 deletions

View file

@ -135,25 +135,5 @@ public class Form
}
}
public JObject GetValues()
{
var r = new JObject();
foreach (var f in GetAllFields())
{
var node = r;
for (int i = 0; i < f.Path.Count - 1; i++)
{
var p = f.Path[i];
var child = node[p] as JObject;
if (child is null)
{
child = new JObject();
node[p] = child;
}
node = child;
}
node[f.Field.Name] = f.Field.Value;
}
return r;
}
}

View file

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Forms;
@ -42,9 +40,10 @@ public class FormTests : UnitTestBase
}
}
};
var service = new FormDataService(null, null);
var providers = new FormComponentProviders(new List<IFormComponentProvider>());
var service = new FormDataService(null, providers);
Assert.False(service.IsFormSchemaValid(form.ToString(), out _, out _));
form = new Form()
form = new Form
{
Fields = new List<Field>
{
@ -161,12 +160,12 @@ public class FormTests : UnitTestBase
}
}
var obj = form.GetValues();
var obj = service.GetValues(form);
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
Clear(form);
form.SetValues(obj);
obj = form.GetValues();
obj = service.GetValues(form);
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
@ -184,10 +183,10 @@ public class FormTests : UnitTestBase
}
};
form.SetValues(obj);
obj = form.GetValues();
obj = service.GetValues(form);
Assert.Null(obj["test"].Value<string>());
form.SetValues(new JObject{ ["test"] = "hello" });
obj = form.GetValues();
obj = service.GetValues(form);
Assert.Equal("hello", obj["test"].Value<string>());
}

View file

@ -11,6 +11,7 @@ using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Forms;
using BTCPayServer.Models.CustodianAccountViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
@ -38,6 +39,7 @@ namespace BTCPayServer.Controllers
private readonly BTCPayServerClient _btcPayServerClient;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly LinkGenerator _linkGenerator;
private readonly FormDataService _formDataService;
public UICustodianAccountsController(
DisplayFormatter displayFormatter,
@ -46,7 +48,8 @@ namespace BTCPayServer.Controllers
IEnumerable<ICustodian> custodianRegistry,
BTCPayServerClient btcPayServerClient,
BTCPayNetworkProvider networkProvider,
LinkGenerator linkGenerator
LinkGenerator linkGenerator,
FormDataService formDataService
)
{
_displayFormatter = displayFormatter;
@ -55,6 +58,7 @@ namespace BTCPayServer.Controllers
_btcPayServerClient = btcPayServerClient;
_networkProvider = networkProvider;
_linkGenerator = linkGenerator;
_formDataService = formDataService;
}
public string CreatedCustodianAccountId { get; set; }
@ -247,7 +251,7 @@ namespace BTCPayServer.Controllers
if (configForm.IsValid())
{
var newData = configForm.GetValues();
var newData = _formDataService.GetValues(configForm);
custodianAccount.SetBlob(newData);
custodianAccount = await _custodianAccountRepository.CreateOrUpdate(custodianAccount);
return RedirectToAction(nameof(ViewCustodianAccount),
@ -301,7 +305,7 @@ namespace BTCPayServer.Controllers
configForm.ApplyValuesFromForm(Request.Form);
if (configForm.IsValid())
{
var configData = configForm.GetValues();
var configData = _formDataService.GetValues(configForm);
custodianAccountData.SetBlob(configData);
custodianAccountData = await _custodianAccountRepository.CreateOrUpdate(custodianAccountData);
TempData[WellKnownTempData.SuccessMessage] = "Custodian account successfully created";

View file

@ -240,7 +240,7 @@ namespace BTCPayServer.Controllers
form.ApplyValuesFromForm(Request.Form);
if (FormDataService.Validate(form, ModelState))
{
prBlob.FormResponse = form.GetValues();
prBlob.FormResponse = FormDataService.GetValues(form);
result.SetBlob(prBlob);
await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(result);
return RedirectToAction("PayPaymentRequest", new {payReqId});

View file

@ -1,4 +1,3 @@
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Data;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
@ -15,6 +14,7 @@ public static class FormDataExtensions
serviceCollection.AddSingleton<IFormComponentProvider, HtmlInputFormProvider>();
serviceCollection.AddSingleton<IFormComponentProvider, HtmlFieldsetFormProvider>();
serviceCollection.AddSingleton<IFormComponentProvider, HtmlSelectFormProvider>();
serviceCollection.AddSingleton<IFormComponentProvider, FieldValueMirror>();
}
public static JObject Deserialize(this FormData form)

View file

@ -11,6 +11,7 @@ using BTCPayServer.Data;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Forms;
@ -150,14 +151,50 @@ public class FormDataService
public CreateInvoiceRequest GenerateInvoiceParametersFromForm(Form form)
{
var amt = form.GetFieldByFullName($"{InvoiceParameterPrefix}amount")?.Value;
var amt = GetValue(form, $"{InvoiceParameterPrefix}amount");
return new CreateInvoiceRequest
{
Currency = form.GetFieldByFullName($"{InvoiceParameterPrefix}currency")?.Value,
Currency = GetValue(form, $"{InvoiceParameterPrefix}currency"),
Amount = string.IsNullOrEmpty(amt) ? null : decimal.Parse(amt, CultureInfo.InvariantCulture),
Metadata = form.GetValues(),
Metadata = GetValues(form),
};
}
public string? GetValue(Form form, string field)
{
return GetValue(form, form.GetFieldByFullName(field));
}
public string? GetValue(Form form, Field? field)
{
if (field is null)
{
return null;
}
return _formProviders.TypeToComponentProvider.TryGetValue(field.Type, out var formComponentProvider) ? formComponentProvider.GetValue(form, field) : field.Value;
}
public JObject GetValues(Form form)
{
var r = new JObject();
foreach (var f in form.GetAllFields())
{
var node = r;
for (int i = 0; i < f.Path.Count - 1; i++)
{
var p = f.Path[i];
var child = node[p] as JObject;
if (child is null)
{
child = new JObject();
node[p] = child;
}
node = child;
}
node[f.Field.Name] = GetValue(form, f.FullName);
}
return r;
}
}

View file

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Form;
namespace BTCPayServer.Forms;
@ -13,8 +12,9 @@ public class HtmlFieldsetFormProvider : IFormComponentProvider
typeToComponentProvider.Add("fieldset", this);
}
public void Validate(Field field)
public string GetValue(Form form, Field field)
{
return null;
}
public void Validate(Form form, Field field)

View file

@ -1,11 +1,31 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Validation;
namespace BTCPayServer.Forms;
public class FieldValueMirror : IFormComponentProvider
{
public string View { get; } = null;
public void Validate(Form form, Field field)
{
if (form.GetFieldByFullName(field.Value) is null)
{
field.ValidationErrors = new List<string>() {$"{field.Name} requires {field.Value} to be present"};
}
}
public void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
{
typeToComponentProvider.Add("mirror", this);
}
public string GetValue(Form form, Field field)
{
return form.GetFieldByFullName(field.Value)?.Value;
}
}
public class HtmlInputFormProvider : FormComponentProviderBase
{
public override void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)

View file

@ -1,8 +1,6 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Forms;

View file

@ -9,12 +9,18 @@ public interface IFormComponentProvider
string View { get; }
void Validate(Form form, Field field);
void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider);
string GetValue(Form form, Field field);
}
public abstract class FormComponentProviderBase : IFormComponentProvider
{
public abstract string View { get; }
public abstract void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider);
public virtual string GetValue(Form form, Field field)
{
return field.Value;
}
public abstract void Validate(Form form, Field field);
public void ValidateField<T>(Field field) where T : ValidationAttribute, new()

View file

@ -267,7 +267,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
return RedirectToAction(nameof(ViewPointOfSale), new { appId, viewType });
}
formResponseJObject = form.GetValues();
formResponseJObject = FormDataService.GetValues(form);
break;
}
try
@ -406,7 +406,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
var controller = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture);
var redirectUrl =
Request.GetAbsoluteUri(Url.Action(nameof(ViewPointOfSale), controller, new {appId, viewType}));
formParameters.Add("formResponse", form.GetValues().ToString());
formParameters.Add("formResponse", FormDataService.GetValues(form).ToString());
return View("PostRedirect", new PostRedirectViewModel
{
FormUrl = redirectUrl,

View file

@ -8,7 +8,7 @@
<legend class="h3 mt-4 mb-3">@Model.Label</legend>
@foreach (var field in Model.Fields)
{
if (FormComponentProviders.TypeToComponentProvider.TryGetValue(field.Type, out var partial))
if (FormComponentProviders.TypeToComponentProvider.TryGetValue(field.Type, out var partial) && !string.IsNullOrEmpty(partial.View))
{
field.Name = $"{(string.IsNullOrEmpty(Model.Name)? string.Empty: $"{Model.Name}_")}{field.Name}";
<partial name="@partial.View" for="@field"></partial>

View file

@ -5,7 +5,7 @@
@foreach (var field in Model.Fields)
{
if (FormComponentProviders.TypeToComponentProvider.TryGetValue(field.Type, out var partial))
if (FormComponentProviders.TypeToComponentProvider.TryGetValue(field.Type, out var partial) && !string.IsNullOrEmpty(partial.View))
{
<partial name="@partial.View" for="@field" />
}