Add updated image upload support on Crowdfund plugin (#6254)

* Add updated image upload support on Crowdfund plugin

* Refactor crowdfund image upload fix

* update crowdfund url for greenfield api

* Resolve integration test assertion

* Remove superfluous and unused command argument

* Fix missing validation error

* Minor API controller update

* Property and usage fixes

* Fix test after merge

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
Chukwuleta Tobechi
2024-10-03 02:39:41 +01:00
committed by GitHub
parent 8f062f918b
commit 3a71c45a89
10 changed files with 168 additions and 28 deletions

View File

@@ -2,6 +2,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using BTCPayServer.Abstractions.Models;
namespace BTCPayServer.Abstractions.Contracts;
@@ -14,4 +15,5 @@ public interface IFileService
Task<string?> GetTemporaryFileUrl(Uri baseUri, string fileId, DateTimeOffset expiry,
bool isDownload);
Task RemoveFile(string fileId, string userId);
Task<UploadImageResultModel> UploadImage(IFormFile file, string userId, long maxFileSizeInBytes = 1_000_000);
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
namespace BTCPayServer.Abstractions.Models;
public class UploadImageResultModel
{
public bool Success { get; set; }
public string Response { get; set; } = string.Empty;
public IStoredFile? StoredFile { get; set; }
}

View File

@@ -81,7 +81,7 @@ namespace BTCPayServer.Tests
Assert.False(app.Archived);
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
Assert.IsType<ViewResult>(await crowdfund.ViewCrowdfund(app.Id));
// Delete
Assert.IsType<NotFoundResult>(apps2.DeleteApp(app.Id));
@@ -121,7 +121,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.Enabled = false;
crowdfundViewModel.EndDate = null;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
var anonAppPubsController = tester.PayTester.GetController<UICrowdfundController>();
var crowdfundController = user.GetController<UICrowdfundController>();
@@ -146,7 +146,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.StartDate = DateTime.Today.AddDays(2);
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
@@ -157,7 +157,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.EndDate = DateTime.Today.AddDays(-1);
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
@@ -170,7 +170,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.TargetAmount = 1;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.EnforceTargetAmount = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
{
Amount = new decimal(1.01)
@@ -214,7 +214,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.UseAllStoreInvoices = true;
crowdfundViewModel.EnforceTargetAmount = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
var publicApps = user.GetController<UICrowdfundController>();
@@ -268,7 +268,7 @@ namespace BTCPayServer.Tests
Assert.Contains(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags);
crowdfundViewModel.UseAllStoreInvoices = false;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
TestLogs.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged");
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
@@ -287,7 +287,7 @@ namespace BTCPayServer.Tests
TestLogs.LogInformation("After turning setting a softcap, let's check that only actual payments are counted");
crowdfundViewModel.EnforceTargetAmount = false;
crowdfundViewModel.UseAllStoreInvoices = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
{
Buyer = new Buyer { email = "test@fwf.com" },
@@ -356,7 +356,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.FormId = lstForms[0].Id;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
var vm2 = await crowdfund.CrowdfundForm(app.Id, (decimal?)0.01).AssertViewModelAsync<FormViewModel>();
var res = await crowdfund.CrowdfundFormSubmit(app.Id, (decimal)0.01, "", vm2);
@@ -411,7 +411,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.Enabled = true;
crowdfundViewModel.PerksTemplate = "[{\"id\": \"xxx\",\"title\": \"Perk 1\",\"priceType\": \"Fixed\",\"price\": \"0.001\",\"image\": \"\",\"description\": \"\",\"categories\": [],\"disabled\": false}]";
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
var vm2 = await crowdfund.CrowdfundForm(app.Id, (decimal?)0.01, "xxx").AssertViewModelAsync<FormViewModel>();
var res = await crowdfund.CrowdfundFormSubmit(app.Id, (decimal)0.01, "xxx", vm2);

View File

@@ -9,6 +9,7 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Plugins.Crowdfund;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
@@ -28,12 +29,14 @@ namespace BTCPayServer.Controllers.Greenfield
public class GreenfieldAppsController : ControllerBase
{
private readonly AppService _appService;
private readonly UriResolver _uriResolver;
private readonly StoreRepository _storeRepository;
private readonly CurrencyNameTable _currencies;
private readonly UserManager<ApplicationUser> _userManager;
public GreenfieldAppsController(
AppService appService,
UriResolver uriResolver,
StoreRepository storeRepository,
BTCPayNetworkProvider btcPayNetworkProvider,
CurrencyNameTable currencies,
@@ -41,6 +44,7 @@ namespace BTCPayServer.Controllers.Greenfield
)
{
_appService = appService;
_uriResolver = uriResolver;
_storeRepository = storeRepository;
_currencies = currencies;
_userManager = userManager;
@@ -72,12 +76,12 @@ namespace BTCPayServer.Controllers.Greenfield
Archived = request.Archived ?? false
};
var settings = ToCrowdfundSettings(request, new CrowdfundSettings { Title = request.Title ?? request.AppName });
var settings = ToCrowdfundSettings(request);
appData.SetSettings(settings);
await _appService.UpdateOrCreateApp(appData);
return Ok(ToCrowdfundModel(appData));
var model = await ToCrowdfundModel(appData);
return Ok(model);
}
[HttpPost("~/api/v1/stores/{storeId}/apps/pos")]
@@ -208,7 +212,8 @@ namespace BTCPayServer.Controllers.Greenfield
return AppNotFound();
}
return Ok(ToCrowdfundModel(app));
var model = await ToCrowdfundModel(app);
return Ok(model);
}
[HttpDelete("~/api/v1/apps/{appId}")]
@@ -255,7 +260,7 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateAPIError(404, "app-not-found", "The app with specified ID was not found");
}
private CrowdfundSettings ToCrowdfundSettings(CrowdfundAppRequest request, CrowdfundSettings settings)
private CrowdfundSettings ToCrowdfundSettings(CrowdfundAppRequest request)
{
var parsedSounds = ValidateStringArray(request.Sounds);
var parsedColors = ValidateStringArray(request.AnimationColors);
@@ -271,7 +276,7 @@ namespace BTCPayServer.Controllers.Greenfield
Description = request.Description?.Trim(),
EndDate = request.EndDate?.UtcDateTime,
TargetAmount = request.TargetAmount,
MainImageUrl = request.MainImageUrl?.Trim(),
MainImageUrl = request.MainImageUrl == null ? null : UnresolvedUri.Create(request.MainImageUrl),
NotificationUrl = request.NotificationUrl?.Trim(),
Tagline = request.Tagline?.Trim(),
PerksTemplate = request.PerksTemplate is not null ? AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate.Trim())) : null,
@@ -411,7 +416,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
}
private CrowdfundAppData ToCrowdfundModel(AppData appData)
private async Task<CrowdfundAppData> ToCrowdfundModel(AppData appData)
{
var settings = appData.GetSettings<CrowdfundSettings>();
Enum.TryParse<CrowdfundResetEvery>(settings.ResetEvery.ToString(), true, out var resetEvery);
@@ -432,7 +437,7 @@ namespace BTCPayServer.Controllers.Greenfield
Description = settings.Description,
EndDate = settings.EndDate,
TargetAmount = settings.TargetAmount,
MainImageUrl = settings.MainImageUrl,
MainImageUrl = settings.MainImageUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), settings.MainImageUrl),
NotificationUrl = settings.NotificationUrl,
Tagline = settings.Tagline,
DisqusEnabled = settings.DisqusEnabled,

View File

@@ -4,8 +4,10 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
@@ -44,6 +46,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
EventAggregator eventAggregator,
UriResolver uriResolver,
StoreRepository storeRepository,
IFileService fileService,
UIInvoiceController invoiceController,
UserManager<ApplicationUser> userManager,
FormDataService formDataService,
@@ -53,6 +56,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
_appService = appService;
_userManager = userManager;
_app = app;
_fileService = fileService;
_storeRepository = storeRepository;
_eventAggregator = eventAggregator;
_uriResolver = uriResolver;
@@ -61,6 +65,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
}
private readonly EventAggregator _eventAggregator;
private readonly IFileService _fileService;
private readonly UriResolver _uriResolver;
private readonly CurrencyNameTable _currencies;
private readonly StoreRepository _storeRepository;
@@ -393,6 +398,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
var settings = app.GetSettings<CrowdfundSettings>();
var resetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery);
var vm = new UpdateCrowdfundViewModel
{
Title = settings.Title,
@@ -405,8 +411,8 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
EnforceTargetAmount = settings.EnforceTargetAmount,
StartDate = settings.StartDate,
TargetCurrency = settings.TargetCurrency,
MainImageUrl = settings.MainImageUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), settings.MainImageUrl),
Description = settings.Description,
MainImageUrl = settings.MainImageUrl,
EndDate = settings.EndDate,
TargetAmount = settings.TargetAmount,
NotificationUrl = settings.NotificationUrl,
@@ -434,8 +440,13 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm, string command)
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm,
[FromForm] bool RemoveLogoFile = false)
{
var userId = GetUserId();
if (userId is null)
return NotFound();
var app = GetCurrentApp();
if (app == null)
return NotFound();
@@ -503,6 +514,16 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
parsedAnimationColors = new CrowdfundSettings().AnimationColors;
}
UploadImageResultModel imageUpload = null;
if (vm.MainImageFile != null)
{
imageUpload = await _fileService.UploadImage(vm.MainImageFile, userId);
if (!imageUpload.Success)
{
ModelState.AddModelError(nameof(vm.MainImageFile), imageUpload.Response);
}
}
if (!ModelState.IsValid)
{
return View("Crowdfund/UpdateCrowdfund", vm);
@@ -520,7 +541,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
Description = vm.Description,
EndDate = vm.EndDate?.ToUniversalTime(),
TargetAmount = vm.TargetAmount,
MainImageUrl = vm.MainImageUrl,
MainImageUrl = app.GetSettings<CrowdfundSettings>()?.MainImageUrl,
NotificationUrl = vm.NotificationUrl,
Tagline = vm.Tagline,
PerksTemplate = vm.PerksTemplate,
@@ -538,6 +559,17 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
FormId = vm.FormId
};
if (imageUpload?.Success is true)
{
newSettings.MainImageUrl = new UnresolvedUri.FileIdUri(imageUpload.StoredFile.Id);
}
else if (RemoveLogoFile)
{
newSettings.MainImageUrl = null;
vm.MainImageUrl = null;
vm.MainImageFile = null;
}
app.TagAllInvoices = vm.UseAllStoreInvoices;
app.SetSettings(newSettings);

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
using BTCPayServer.Client.Models;
@@ -49,22 +50,28 @@ namespace BTCPayServer.Plugins.Crowdfund
private readonly IOptions<BTCPayServerOptions> _options;
private readonly DisplayFormatter _displayFormatter;
private readonly CurrencyNameTable _currencyNameTable;
private readonly UriResolver _uriResolver;
private readonly InvoiceRepository _invoiceRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly PrettyNameProvider _prettyNameProvider;
public const string AppType = "Crowdfund";
public CrowdfundAppType(
LinkGenerator linkGenerator,
IOptions<BTCPayServerOptions> options,
UriResolver uriResolver,
InvoiceRepository invoiceRepository,
PrettyNameProvider prettyNameProvider,
DisplayFormatter displayFormatter,
IHttpContextAccessor httpContextAccessor,
CurrencyNameTable currencyNameTable)
{
Description = Type = AppType;
_linkGenerator = linkGenerator;
_options = options;
_uriResolver = uriResolver;
_displayFormatter = displayFormatter;
_httpContextAccessor = httpContextAccessor;
_currencyNameTable = currencyNameTable;
_invoiceRepository = invoiceRepository;
_prettyNameProvider = prettyNameProvider;
@@ -186,12 +193,11 @@ namespace BTCPayServer.Plugins.Crowdfund
? _linkGenerator.GetPathByAction(nameof(UICrowdfundController.CrowdfundForm), "UICrowdfund",
new { appId = appData.Id }, _options.Value.RootPath)
: null;
return new ViewCrowdfundViewModel
var vm = new ViewCrowdfundViewModel
{
Title = settings.Title,
Tagline = settings.Tagline,
Description = settings.Description,
MainImageUrl = settings.MainImageUrl,
StoreName = store.StoreName,
StoreId = appData.StoreDataId,
AppId = appData.Id,
@@ -230,6 +236,12 @@ namespace BTCPayServer.Plugins.Crowdfund
CurrentAmount = currentPayments.TotalCurrency
}
};
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext != null && settings.MainImageUrl != null)
{
vm.MainImageUrl = await _uriResolver.Resolve(httpContext.Request.GetAbsoluteRootUri(), settings.MainImageUrl);
}
return vm;
}
private Dictionary<string, PaymentStat> GetPaymentStats(InvoiceStatistics stats)

View File

@@ -4,6 +4,8 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using BTCPayServer.Services.Apps;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
namespace BTCPayServer.Plugins.Crowdfund.Models
{
@@ -32,6 +34,10 @@ namespace BTCPayServer.Plugins.Crowdfund.Models
[Display(Name = "Featured Image URL")]
public string MainImageUrl { get; set; }
[Display(Name = "Featured Image URL")]
[JsonIgnore]
public IFormFile MainImageFile { get; set; }
[Display(Name = "Callback Notification URL")]
[Uri]
public string NotificationUrl { get; set; }

View File

@@ -1,4 +1,6 @@
using System;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Apps
{
@@ -27,7 +29,8 @@ namespace BTCPayServer.Services.Apps
}
public bool EnforceTargetAmount { get; set; }
public string MainImageUrl { get; set; }
[JsonConverter(typeof(UnresolvedUriJsonConverter))]
public UnresolvedUri MainImageUrl { get; set; }
public string NotificationUrl { get; set; }
public string Tagline { get; set; }
public string PerksTemplate { get; set; }

View File

@@ -6,10 +6,12 @@ using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Services;
using BTCPayServer.Storage.Models;
@@ -47,6 +49,44 @@ namespace BTCPayServer.Storage.Services
return settings is not null;
}
public async Task<UploadImageResultModel> UploadImage(IFormFile file, string userId, long maxFileSizeInBytes = 1_000_000)
{
var result = new UploadImageResultModel();
if (file.Length > maxFileSizeInBytes)
{
result.Success = false;
result.Response = $"The uploaded image file should be less than {maxFileSizeInBytes / 1_000_000}MB";
return result;
}
if (!file.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
{
result.Success = false;
result.Response = "The uploaded file needs to be an image (based on content type)";
return result;
}
var formFile = await file.Bufferize();
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
{
result.Success = false;
result.Response = "The uploaded file needs to be an image (based on file content)";
return result;
}
try
{
result.StoredFile = await AddFile(formFile, userId);
result.Success = true;
result.Response = "Image uploaded successfully";
}
catch (Exception e)
{
result.Success = false;
result.Response = $"Could not save image: {e.Message}";
}
return result;
}
public async Task<IStoredFile> AddFile(IFormFile file, string userId)
{
var settings = await _settingsRepository.GetSettingAsync<StorageSettings>();

View File

@@ -1,4 +1,5 @@
@using System.Globalization
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Abstractions.Models
@using BTCPayServer.Client
@using BTCPayServer.TagHelpers
@@ -6,11 +7,13 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Forms
@inject FormDataService FormDataService
@inject IFileService FileService
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
@model BTCPayServer.Plugins.Crowdfund.Models.UpdateCrowdfundViewModel
@{
ViewData.SetActivePage(AppsNavPages.Update, "Update Crowdfund", Model.AppId);
Csp.UnsafeEval();
var canUpload = await FileService.IsAvailable();
var checkoutFormOptions = await FormDataService.GetSelect(Model.StoreId, Model.FormId);
}
@@ -25,7 +28,7 @@
<script src="~/crowdfund/admin.js" asp-append-version="true"></script>
}
<form method="post" permissioned="@Policies.CanModifyStoreSettings">
<form method="post" enctype="multipart/form-data" permissioned="@Policies.CanModifyStoreSettings">
<div class="sticky-header">
<h2 text-translate="true">@ViewData["Title"]</h2>
<div>
@@ -74,9 +77,31 @@
<span asp-validation-for="Tagline" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="MainImageUrl" class="form-label"></label>
<input asp-for="MainImageUrl" class="form-control" />
<span asp-validation-for="MainImageUrl" class="text-danger"></span>
<div class="d-flex align-items-center justify-content-between gap-2">
<label asp-for="MainImageFile" class="form-label"></label>
@if (!string.IsNullOrEmpty(Model.MainImageUrl))
{
<button type="submit" class="btn btn-link p-0 text-danger" name="RemoveLogoFile" value="true">
<vc:icon symbol="cross" /> Remove
</button>
}
</div>
@if (canUpload)
{
<div class="d-flex align-items-center gap-3">
<input asp-for="MainImageFile" type="file" class="form-control flex-grow">
@if (!string.IsNullOrEmpty(Model.MainImageUrl))
{
<img src="@Model.MainImageUrl" alt="Logo" style="height:2.1rem;max-width:10.5rem;" />
}
</div>
<span asp-validation-for="MainImageFile" class="text-danger"></span>
}
else
{
<input asp-for="MainImageFile" type="file" class="form-control" disabled>
<div class="form-text">In order to upload an image, a <a asp-controller="UIServer" asp-action="Files">file storage</a> must be configured.</div>
}
</div>
<div class="form-group">
<div class="d-flex align-items-center">