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.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using BTCPayServer.Abstractions.Form; using BTCPayServer.Abstractions.Form;
using BTCPayServer.Forms; 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 _)); Assert.False(service.IsFormSchemaValid(form.ToString(), out _, out _));
form = new Form() form = new Form
{ {
Fields = new List<Field> 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("original", obj["invoice"]["test"].Value<string>());
Assert.Equal("updated", obj["invoice_item3"].Value<string>()); Assert.Equal("updated", obj["invoice_item3"].Value<string>());
Clear(form); Clear(form);
form.SetValues(obj); form.SetValues(obj);
obj = form.GetValues(); obj = service.GetValues(form);
Assert.Equal("original", obj["invoice"]["test"].Value<string>()); Assert.Equal("original", obj["invoice"]["test"].Value<string>());
Assert.Equal("updated", obj["invoice_item3"].Value<string>()); Assert.Equal("updated", obj["invoice_item3"].Value<string>());
@ -184,10 +183,10 @@ public class FormTests : UnitTestBase
} }
}; };
form.SetValues(obj); form.SetValues(obj);
obj = form.GetValues(); obj = service.GetValues(form);
Assert.Null(obj["test"].Value<string>()); Assert.Null(obj["test"].Value<string>());
form.SetValues(new JObject{ ["test"] = "hello" }); form.SetValues(new JObject{ ["test"] = "hello" });
obj = form.GetValues(); obj = service.GetValues(form);
Assert.Equal("hello", obj["test"].Value<string>()); Assert.Equal("hello", obj["test"].Value<string>());
} }

View file

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

View file

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

View file

@ -1,4 +1,3 @@
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Data; using BTCPayServer.Data;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -15,6 +14,7 @@ public static class FormDataExtensions
serviceCollection.AddSingleton<IFormComponentProvider, HtmlInputFormProvider>(); serviceCollection.AddSingleton<IFormComponentProvider, HtmlInputFormProvider>();
serviceCollection.AddSingleton<IFormComponentProvider, HtmlFieldsetFormProvider>(); serviceCollection.AddSingleton<IFormComponentProvider, HtmlFieldsetFormProvider>();
serviceCollection.AddSingleton<IFormComponentProvider, HtmlSelectFormProvider>(); serviceCollection.AddSingleton<IFormComponentProvider, HtmlSelectFormProvider>();
serviceCollection.AddSingleton<IFormComponentProvider, FieldValueMirror>();
} }
public static JObject Deserialize(this FormData form) 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.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Forms; namespace BTCPayServer.Forms;
@ -150,14 +151,50 @@ public class FormDataService
public CreateInvoiceRequest GenerateInvoiceParametersFromForm(Form form) public CreateInvoiceRequest GenerateInvoiceParametersFromForm(Form form)
{ {
var amt = form.GetFieldByFullName($"{InvoiceParameterPrefix}amount")?.Value; var amt = GetValue(form, $"{InvoiceParameterPrefix}amount");
return new CreateInvoiceRequest return new CreateInvoiceRequest
{ {
Currency = form.GetFieldByFullName($"{InvoiceParameterPrefix}currency")?.Value, Currency = GetValue(form, $"{InvoiceParameterPrefix}currency"),
Amount = string.IsNullOrEmpty(amt) ? null : decimal.Parse(amt, CultureInfo.InvariantCulture), Amount = string.IsNullOrEmpty(amt) ? null : decimal.Parse(amt, CultureInfo.InvariantCulture),
Metadata = GetValues(form),
Metadata = form.GetValues(),
}; };
} }
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.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Form; using BTCPayServer.Abstractions.Form;
namespace BTCPayServer.Forms; namespace BTCPayServer.Forms;
@ -13,8 +12,9 @@ public class HtmlFieldsetFormProvider : IFormComponentProvider
typeToComponentProvider.Add("fieldset", this); 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) public void Validate(Form form, Field field)

View file

@ -1,11 +1,31 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq;
using BTCPayServer.Abstractions.Form; using BTCPayServer.Abstractions.Form;
using BTCPayServer.Validation; using BTCPayServer.Validation;
namespace BTCPayServer.Forms; 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 class HtmlInputFormProvider : FormComponentProviderBase
{ {
public override void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider) public override void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)

View file

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

View file

@ -9,12 +9,18 @@ public interface IFormComponentProvider
string View { get; } string View { get; }
void Validate(Form form, Field field); void Validate(Form form, Field field);
void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider); void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider);
string GetValue(Form form, Field field);
} }
public abstract class FormComponentProviderBase : IFormComponentProvider public abstract class FormComponentProviderBase : IFormComponentProvider
{ {
public abstract string View { get; } public abstract string View { get; }
public abstract void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider); 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 abstract void Validate(Form form, Field field);
public void ValidateField<T>(Field field) where T : ValidationAttribute, new() 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 }); return RedirectToAction(nameof(ViewPointOfSale), new { appId, viewType });
} }
formResponseJObject = form.GetValues(); formResponseJObject = FormDataService.GetValues(form);
break; break;
} }
try try
@ -406,7 +406,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
var controller = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture); var controller = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture);
var redirectUrl = var redirectUrl =
Request.GetAbsoluteUri(Url.Action(nameof(ViewPointOfSale), controller, new {appId, viewType})); 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 return View("PostRedirect", new PostRedirectViewModel
{ {
FormUrl = redirectUrl, FormUrl = redirectUrl,

View file

@ -8,7 +8,7 @@
<legend class="h3 mt-4 mb-3">@Model.Label</legend> <legend class="h3 mt-4 mb-3">@Model.Label</legend>
@foreach (var field in Model.Fields) @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}"; field.Name = $"{(string.IsNullOrEmpty(Model.Name)? string.Empty: $"{Model.Name}_")}{field.Name}";
<partial name="@partial.View" for="@field"></partial> <partial name="@partial.View" for="@field"></partial>

View file

@ -5,7 +5,7 @@
@foreach (var field in Model.Fields) @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" /> <partial name="@partial.View" for="@field" />
} }