From 60d6e98c67eaac23a9602a101c97c6227fda71dc Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Tue, 4 Apr 2023 04:01:34 +0200 Subject: [PATCH] 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 --- BTCPayServer.Abstractions/Form/Form.cs | 22 +-------- .../{FormTes.cs => FormTests.cs} | 15 +++--- .../UICustodianAccountsController.cs | 10 ++-- .../Controllers/UIPaymentRequestController.cs | 2 +- BTCPayServer/Forms/FormDataExtensions.cs | 2 +- BTCPayServer/Forms/FormDataService.cs | 47 +++++++++++++++++-- .../Forms/HtmlFieldsetFormProvider.cs | 4 +- BTCPayServer/Forms/HtmlInputFormProvider.cs | 22 ++++++++- BTCPayServer/Forms/HtmlSelectFormProvider.cs | 2 - BTCPayServer/Forms/IFormComponentProvider.cs | 6 +++ .../Controllers/UIPointOfSaleController.cs | 4 +- .../Views/Shared/Forms/FieldSetElement.cshtml | 2 +- BTCPayServer/Views/Shared/_Form.cshtml | 2 +- 13 files changed, 92 insertions(+), 48 deletions(-) rename BTCPayServer.Tests/{FormTes.cs => FormTests.cs} (95%) diff --git a/BTCPayServer.Abstractions/Form/Form.cs b/BTCPayServer.Abstractions/Form/Form.cs index a6dac6a4f..81805906e 100644 --- a/BTCPayServer.Abstractions/Form/Form.cs +++ b/BTCPayServer.Abstractions/Form/Form.cs @@ -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; - } + } diff --git a/BTCPayServer.Tests/FormTes.cs b/BTCPayServer.Tests/FormTests.cs similarity index 95% rename from BTCPayServer.Tests/FormTes.cs rename to BTCPayServer.Tests/FormTests.cs index 1d56291b3..dfd6d1178 100644 --- a/BTCPayServer.Tests/FormTes.cs +++ b/BTCPayServer.Tests/FormTests.cs @@ -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()); + var service = new FormDataService(null, providers); Assert.False(service.IsFormSchemaValid(form.ToString(), out _, out _)); - form = new Form() + form = new Form { Fields = new List { @@ -161,12 +160,12 @@ public class FormTests : UnitTestBase } } - var obj = form.GetValues(); + var obj = service.GetValues(form); Assert.Equal("original", obj["invoice"]["test"].Value()); Assert.Equal("updated", obj["invoice_item3"].Value()); Clear(form); form.SetValues(obj); - obj = form.GetValues(); + obj = service.GetValues(form); Assert.Equal("original", obj["invoice"]["test"].Value()); Assert.Equal("updated", obj["invoice_item3"].Value()); @@ -184,10 +183,10 @@ public class FormTests : UnitTestBase } }; form.SetValues(obj); - obj = form.GetValues(); + obj = service.GetValues(form); Assert.Null(obj["test"].Value()); form.SetValues(new JObject{ ["test"] = "hello" }); - obj = form.GetValues(); + obj = service.GetValues(form); Assert.Equal("hello", obj["test"].Value()); } diff --git a/BTCPayServer/Controllers/UICustodianAccountsController.cs b/BTCPayServer/Controllers/UICustodianAccountsController.cs index 5fd63d110..f57081cbe 100644 --- a/BTCPayServer/Controllers/UICustodianAccountsController.cs +++ b/BTCPayServer/Controllers/UICustodianAccountsController.cs @@ -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 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"; diff --git a/BTCPayServer/Controllers/UIPaymentRequestController.cs b/BTCPayServer/Controllers/UIPaymentRequestController.cs index db5ee2e61..533dc0259 100644 --- a/BTCPayServer/Controllers/UIPaymentRequestController.cs +++ b/BTCPayServer/Controllers/UIPaymentRequestController.cs @@ -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}); diff --git a/BTCPayServer/Forms/FormDataExtensions.cs b/BTCPayServer/Forms/FormDataExtensions.cs index eb53ce732..ec98c9fda 100644 --- a/BTCPayServer/Forms/FormDataExtensions.cs +++ b/BTCPayServer/Forms/FormDataExtensions.cs @@ -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(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); } public static JObject Deserialize(this FormData form) diff --git a/BTCPayServer/Forms/FormDataService.cs b/BTCPayServer/Forms/FormDataService.cs index bc1b4753a..8db337bab 100644 --- a/BTCPayServer/Forms/FormDataService.cs +++ b/BTCPayServer/Forms/FormDataService.cs @@ -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; + } } diff --git a/BTCPayServer/Forms/HtmlFieldsetFormProvider.cs b/BTCPayServer/Forms/HtmlFieldsetFormProvider.cs index d48226a68..ef5fc5c3a 100644 --- a/BTCPayServer/Forms/HtmlFieldsetFormProvider.cs +++ b/BTCPayServer/Forms/HtmlFieldsetFormProvider.cs @@ -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) diff --git a/BTCPayServer/Forms/HtmlInputFormProvider.cs b/BTCPayServer/Forms/HtmlInputFormProvider.cs index 5cb1789e2..e9ea4cd3d 100644 --- a/BTCPayServer/Forms/HtmlInputFormProvider.cs +++ b/BTCPayServer/Forms/HtmlInputFormProvider.cs @@ -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() {$"{field.Name} requires {field.Value} to be present"}; + } + } + + public void Register(Dictionary 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 typeToComponentProvider) diff --git a/BTCPayServer/Forms/HtmlSelectFormProvider.cs b/BTCPayServer/Forms/HtmlSelectFormProvider.cs index ed5ba2bed..8a4b670bd 100644 --- a/BTCPayServer/Forms/HtmlSelectFormProvider.cs +++ b/BTCPayServer/Forms/HtmlSelectFormProvider.cs @@ -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; diff --git a/BTCPayServer/Forms/IFormComponentProvider.cs b/BTCPayServer/Forms/IFormComponentProvider.cs index 3612da422..db492cfc3 100644 --- a/BTCPayServer/Forms/IFormComponentProvider.cs +++ b/BTCPayServer/Forms/IFormComponentProvider.cs @@ -9,12 +9,18 @@ public interface IFormComponentProvider string View { get; } void Validate(Form form, Field field); void Register(Dictionary typeToComponentProvider); + string GetValue(Form form, Field field); } public abstract class FormComponentProviderBase : IFormComponentProvider { public abstract string View { get; } public abstract void Register(Dictionary typeToComponentProvider); + public virtual string GetValue(Form form, Field field) + { + return field.Value; + } + public abstract void Validate(Form form, Field field); public void ValidateField(Field field) where T : ValidationAttribute, new() diff --git a/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs b/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs index 9746816a7..1709c88eb 100644 --- a/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs +++ b/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs @@ -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, diff --git a/BTCPayServer/Views/Shared/Forms/FieldSetElement.cshtml b/BTCPayServer/Views/Shared/Forms/FieldSetElement.cshtml index 5c2731704..0666a7144 100644 --- a/BTCPayServer/Views/Shared/Forms/FieldSetElement.cshtml +++ b/BTCPayServer/Views/Shared/Forms/FieldSetElement.cshtml @@ -8,7 +8,7 @@ @Model.Label @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}"; diff --git a/BTCPayServer/Views/Shared/_Form.cshtml b/BTCPayServer/Views/Shared/_Form.cshtml index b742ff6c5..e470117ee 100644 --- a/BTCPayServer/Views/Shared/_Form.cshtml +++ b/BTCPayServer/Views/Shared/_Form.cshtml @@ -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)) { }