Added auto form stuff and started implementing auto crud and store admin things

This commit is contained in:
Baumgartner Marcel 2023-10-25 16:26:42 +02:00
parent f07e3c5a5a
commit b0d9837256
11 changed files with 613 additions and 6 deletions

View file

@ -0,0 +1,8 @@
namespace Moonlight.App.Extensions.Attributes;
public class SelectorAttribute : Attribute
{
public string SelectorProp { get; set; } = "";
public string DisplayProp { get; set; } = "";
public bool UseDropdown { get; set; } = false;
}

View file

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Components;
namespace Moonlight.App.Helpers;
public static class ComponentHelper
{
public static RenderFragment FromType(Type type) => builder =>
{
builder.OpenComponent(0, type);
builder.CloseComponent();
};
}

View file

@ -0,0 +1,57 @@
using System.Reflection;
namespace Moonlight.App.Helpers;
public class PropBinder<T>
{
private PropertyInfo PropertyInfo;
private object DataObject;
public PropBinder(PropertyInfo propertyInfo, object dataObject)
{
PropertyInfo = propertyInfo;
DataObject = dataObject;
}
public string StringValue
{
get => (string)PropertyInfo.GetValue(DataObject)!;
set => PropertyInfo.SetValue(DataObject, value);
}
public int IntValue
{
get => (int)PropertyInfo.GetValue(DataObject)!;
set => PropertyInfo.SetValue(DataObject, value);
}
public long LongValue
{
get => (long)PropertyInfo.GetValue(DataObject)!;
set => PropertyInfo.SetValue(DataObject, value);
}
public bool BoolValue
{
get => (bool)PropertyInfo.GetValue(DataObject)!;
set => PropertyInfo.SetValue(DataObject, value);
}
public DateTime DateTimeValue
{
get => (DateTime)PropertyInfo.GetValue(DataObject)!;
set => PropertyInfo.SetValue(DataObject, value);
}
public T Class
{
get => (T)PropertyInfo.GetValue(DataObject)!;
set => PropertyInfo.SetValue(DataObject, value);
}
public double DoubleValue
{
get => (double)PropertyInfo.GetValue(DataObject)!;
set => PropertyInfo.SetValue(DataObject, value);
}
}

View file

@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms.Store;
public class AddCouponForm
{
[MinLength(5, ErrorMessage = "The code needs to be longer than 4")]
[MaxLength(15, ErrorMessage = "The code should not be longer than 15 characters")]
public string Code { get; set; } = "";
[Range(1, 99, ErrorMessage = "The percent needs to be between 1 and 99")]
public int Percent { get; set; }
[Range(0, int.MaxValue, ErrorMessage = "The amount needs to be equals or greater than 0")]
public int Amount { get; set; }
}

View file

@ -1,15 +1,19 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Moonlight.App.Database.Entities.Store;
using Moonlight.App.Database.Enums;
using Moonlight.App.Extensions.Attributes;
namespace Moonlight.App.Models.Forms.Store;
public class AddProductForm
{
[Required(ErrorMessage = "You need to specify a category")]
[Selector(DisplayProp = "Name", SelectorProp = "Name", UseDropdown = true)]
public Category Category { get; set; }
[Required(ErrorMessage = "You need to specify a name")]
[Description("Teeeeeeeeeeeeeeeeeeeeeeeeeest")]
public string Name { get; set; } = "";
[Required(ErrorMessage = "You need to specify a description")]
@ -19,16 +23,16 @@ public class AddProductForm
[RegularExpression("^[a-z0-9-]+$", ErrorMessage = "You need to enter a valid slug only containing lowercase characters and numbers")]
public string Slug { get; set; } = "";
[Range(0, double.MaxValue, ErrorMessage = "The price needs to be equals or above 0")]
[Range(0, double.MaxValue, ErrorMessage = "The price needs to be equals or greater than 0")]
public double Price { get; set; }
[Range(0, double.MaxValue, ErrorMessage = "The stock needs to be equals or above 0")]
[Range(0, double.MaxValue, ErrorMessage = "The stock needs to be equals or greater than 0")]
public int Stock { get; set; }
[Range(0, double.MaxValue, ErrorMessage = "The max per user amount needs to be equals or above 0")]
[Range(0, double.MaxValue, ErrorMessage = "The max per user amount needs to be equals or greater than 0")]
public int MaxPerUser { get; set; }
[Range(0, double.MaxValue, ErrorMessage = "The duration needs to be equals or above 0")]
[Range(0, double.MaxValue, ErrorMessage = "The duration needs to be equals or greater than 0")]
public int Duration { get; set; }
public ServiceType Type { get; set; }

View file

@ -0,0 +1,67 @@
@using BlazorTable
@using Moonlight.App.Repositories
@typeparam TItem where TItem : class
@inject Repository<TItem> ItemRepository
<div class="card">
<div class="card-header">
<h3 class="card-title">@(Title)</h3>
<div class="card-toolbar">
<button class="btn btn-icon btn-success">
<i class="bx bx-sm bx-plus"></i>
</button>
</div>
</div>
<div class="card-body">
<LazyLoader Load="LoadItems">
<Table TableItem="TItem"
Items="Items"
PageSize="50"
TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3 fs-6"
TableHeadClass="fw-bold text-muted">
@ChildContent
</Table>
</LazyLoader>
</div>
</div>
@code
{
[Parameter]
public string Title { get; set; } = "";
[Parameter]
public Type CreateForm { get; set; }
[Parameter]
public Type UpdateForm { get; set; }
[Parameter]
public Func<Repository<TItem>, TItem[]> Load { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
private TItem[] Items;
protected override void OnInitialized()
{
if (CreateForm == null)
throw new ArgumentNullException(nameof(CreateForm));
if (UpdateForm == null)
throw new ArgumentNullException(nameof(UpdateForm));
if (Load == null)
throw new ArgumentNullException(nameof(Load));
}
private Task LoadItems(LazyLoader _)
{
Items = Load.Invoke(ItemRepository);
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,27 @@
@using Moonlight.App.Extensions
@typeparam TForm
@foreach (var prop in typeof(TForm).GetProperties())
{
<div class="col-md-@(Columns) col-12">
<CascadingValue Name="Property" Value="prop">
<CascadingValue Name="Data" Value="(object)Model">
@{
var typeToCreate = typeof(AutoProperty<>).MakeGenericType(prop.PropertyType);
}
@ComponentHelper.FromType(typeToCreate)
</CascadingValue>
</CascadingValue>
</div>
}
@code
{
[Parameter]
public TForm Model { get; set; }
[Parameter]
public int Columns { get; set; } = 6;
}

View file

@ -0,0 +1,151 @@
@using System.Reflection
@using System.ComponentModel
@using Microsoft.AspNetCore.Components.Forms
@using Moonlight.App.Extensions.Attributes
@using Moonlight.App.Repositories
@typeparam TProp
@inject IServiceProvider ServiceProvider
<label class="form-label">
@(Formatter.ConvertCamelCaseToSpaces(Property.Name))
</label>
@* Description using attribute *@
@{
var attrs = Property.GetCustomAttributes(true);
var descAttr = attrs
.FirstOrDefault(x => x.GetType() == typeof(DescriptionAttribute));
}
@if (descAttr != null)
{
var attribute = descAttr as DescriptionAttribute;
<div class="form-text fs-5 mb-2 mt-0">
@(attribute!.Description)
</div>
}
@* Actual value binding *@
<div class="input-group mb-5">
@if (Property.PropertyType == typeof(string))
{
<div class="w-100">
<InputText id="@Property.Name" @bind-Value="Binder.StringValue" class="form-control"/>
</div>
}
else if (Property.PropertyType == typeof(int))
{
<InputNumber id="@Property.Name" @bind-Value="Binder.IntValue" class="form-control"/>
}
else if (Property.PropertyType == typeof(double))
{
<InputNumber id="@Property.Name" @bind-Value="Binder.DoubleValue" class="form-control"/>
}
else if (Property.PropertyType == typeof(long))
{
<InputNumber id="@Property.Name" @bind-Value="Binder.LongValue" class="form-control"/>
}
else if (Property.PropertyType == typeof(bool))
{
<div class="form-check">
<InputCheckbox id="@Property.Name" @bind-Value="Binder.BoolValue" class="form-check-input"/>
</div>
}
else if (Property.PropertyType == typeof(DateTime))
{
<InputDate id="@Property.Name" @bind-Value="Binder.DateTimeValue" class="form-control"/>
}
else if (Property.PropertyType == typeof(decimal))
{
<InputNumber id="@Property.Name" step="0.01" @bind-Value="Binder.DoubleValue" class="form-control"/>
}
else if (Property.PropertyType.IsEnum)
{
<select @bind="Binder.Class" class="form-select">
@foreach (var status in (TProp[])Enum.GetValues(typeof(TProp)))
{
if (Binder.Class.ToString() == status.ToString())
{
<option value="@(status)" selected="">@(status)</option>
}
else
{
<option value="@(status)">@(status)</option>
}
}
</select>
}
else if (Property.PropertyType.IsClass)
{
var attribute = Property.GetCustomAttributes(true)
.FirstOrDefault(x => x.GetType() == typeof(SelectorAttribute)) as SelectorAttribute;
if (attribute != null)
{
if (attribute.UseDropdown)
{
var displayFunc = new Func<TProp, string>(x =>
{
var prop = typeof(TProp).GetProperties().First(x => x.Name == attribute.DisplayProp);
return prop.GetValue(x) as string ?? "N/A";
});
var searchFunc = new Func<TProp, string>(x =>
{
var prop = typeof(TProp).GetProperties().First(x => x.Name == attribute.SelectorProp);
return prop.GetValue(x) as string ?? "N/A";
});
<SmartDropdown @bind-Value="Binder.Class" DisplayFunc="displayFunc" SearchProp="searchFunc" Items="Items" />
}
else
{
var displayFunc = new Func<TProp, string>(x =>
{
var prop = typeof(TProp).GetProperties().First(x => x.Name == attribute.DisplayProp);
return prop.GetValue(x) as string ?? "N/A";
});
<SmartSelect @bind-Value="Binder.Class" DisplayField="displayFunc" Items="Items" CanBeNull="true" />
}
}
}
</div>
@code
{
[CascadingParameter(Name = "Data")]
public object Data { get; set; }
[CascadingParameter(Name = "Property")]
public PropertyInfo Property { get; set; }
private PropBinder<TProp> Binder;
private TProp[] Items = Array.Empty<TProp>();
protected override void OnInitialized()
{
Binder = new(Property, Data);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
if (Property.GetCustomAttributes(true).Any(x => x.GetType() == typeof(SelectorAttribute)))
{
var typeToGetByDi = typeof(Repository<>).MakeGenericType(typeof(TProp));
var repo = ServiceProvider.GetRequiredService(typeToGetByDi);
var dbSet = repo.GetType().GetMethods().First(x => x.Name == "Get").Invoke(repo, null) as IEnumerable<TProp>;
Items = dbSet!.ToArray();
await InvokeAsync(StateHasChanged);
}
}
}
}

View file

@ -0,0 +1,141 @@
@using Microsoft.AspNetCore.Components.Forms
@typeparam T
@inherits InputBase<T>
<div class="dropdown w-100">
<div class="input-group">
@if (CurrentValue == null)
{
<input class="form-control" type="text" @bind-value="SearchTerm" @bind-value:event="oninput" placeholder="@(Placeholder)">
}
else
{
<input class="form-control" type="text" value="@(DisplayFunc(CurrentValue))">
<button class="btn btn-sm btn-primary" @onclick="() => SelectItem(default(T)!)">
<i class="bx bx-sm bx-x"></i>
</button>
}
</div>
@{
var anyItems = FilteredItems.Any();
}
<div class="dropdown-menu w-100 @(anyItems ? "show" : "")" style="max-height: 200px; overflow-y: auto;">
@if (anyItems)
{
foreach (var item in FilteredItems)
{
<button class="dropdown-item py-2" type="button" @onclick="() => SelectItem(item)">@DisplayFunc(item)</button>
}
}
</div>
</div>
@code {
[Parameter]
public IEnumerable<T> Items { get; set; }
[Parameter]
public Func<T, string> DisplayFunc { get; set; }
[Parameter]
public Func<T, string> SearchProp { get; set; }
[Parameter]
public Func<Task>? OnSelected { get; set; }
[Parameter]
public string Placeholder { get; set; } = "Search...";
private string SearchTerm
{
get => searchTerm;
set
{
searchTerm = value;
FilteredItems = Items.OrderByDescending(x => Matches(SearchProp(x), searchTerm)).Take(30).ToList();
}
}
private string searchTerm = "";
private List<T> FilteredItems = new();
private async void SelectItem(T item)
{
CurrentValue = item;
SearchTerm = "";
FilteredItems.Clear();
if (OnSelected != null)
await OnSelected.Invoke();
}
protected override bool TryParseValueFromString(string? value, out T result, out string? validationErrorMessage)
{
// Check if the value is null or empty
if (string.IsNullOrEmpty(value))
{
result = default(T)!;
validationErrorMessage = "Value cannot be null or empty";
return false;
}
// Try to find an item that matches the search term
var item = FilteredItems.OrderByDescending(x => Matches(SearchProp(x), value)).FirstOrDefault();
if (item != null)
{
result = item;
validationErrorMessage = null;
return true;
}
else
{
result = default(T)!;
validationErrorMessage = $"No item found for search term '{value}'";
return false;
}
}
private float Matches(string input, string search)
{
if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(search))
return 0;
var cleanedSearch = search
.ToLower()
.Replace(" ", "")
.Replace("-", "");
var cleanedInput = input
.ToLower()
.Replace(" ", "")
.Replace("-", "");
if (cleanedInput == cleanedSearch)
return 10000;
float matches = 0;
int i = 0;
foreach (var c in cleanedInput)
{
if (cleanedSearch.Length > i)
{
if (c == cleanedSearch[i])
matches++;
else
matches--;
}
i++;
}
matches = matches / searchTerm.Length;
return matches;
}
}

View file

@ -0,0 +1,59 @@
@using Moonlight.App.Models.Forms.Store
@using Moonlight.App.Repositories
@using Moonlight.App.Database.Entities.Store
@using Mappy.Net
@inject Repository<Coupon> CouponRepository
@inject ToastService ToastService
<SmartModal @ref="Modal" CssClasses="modal-dialog-centered modal-lg">
<div class="modal-header">
<h5 class="modal-title fs-3">Add new modal</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<SmartForm Model="Form" OnValidSubmit="OnValidSubmit">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Code</label>
<input @bind="Form.Code" class="form-control" type="text"/>
</div>
<div class="mb-3">
<label class="form-label">Percent</label>
<input @bind="Form.Percent" class="form-control" type="number"/>
</div>
<div>
<label class="form-label">Amount</label>
<input @bind="Form.Amount" class="form-control" type="number"/>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save changes</button>
</div>
</SmartForm>
</SmartModal>
@code
{
[Parameter]
public Func<Task> OnUpdate { get; set; }
private SmartModal Modal;
private AddCouponForm Form = new();
public async Task Show()
{
await Modal.Show();
}
private async Task OnValidSubmit()
{
var coupon = Mapper.Map<Coupon>(Form);
CouponRepository.Add(coupon);
Form = new();
await ToastService.Success("Successfully added new coupon");
await Modal.Hide();
await OnUpdate.Invoke();
}
}

View file

@ -1,5 +1,70 @@
@page "/admin/store/coupons"
@using BlazorTable
@using Moonlight.App.Database.Entities.Store
@using Moonlight.App.Repositories
@using Moonlight.Shared.Components.Modals.Store
@inject Repository<Coupon> CouponRepository
@inject Repository<CouponUse> CouponUseRepository
@inject ToastService ToastService
<AdminStoreNavigation Index="1" />
@*<div class="card card"*@
<div class="card">
<div class="card-header">
<h3 class="card-title">Coupons</h3>
<div class="card-toolbar">
<button @onclick="() => AddCouponModal.Show()" class="btn btn-icon btn-success"><i class="bx bx-sm bx-plus"></i></button>
</div>
</div>
<div class="card-body">
<LazyLoader @ref="LazyLoader" Load="Load">
<Table TableItem="Coupon"
Items="AllCoupons"
PageSize="50"
TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3 fs-6"
TableHeadClass="fw-bold text-muted">
<Column TableItem="Coupon" Title="Id" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
<Column TableItem="Coupon" Title="Code" Field="@(x => x.Code)" Sortable="true" Filterable="true"/>
<Column TableItem="Coupon" Title="Amount" Field="@(x => x.Amount)" Sortable="true" Filterable="true"/>
<Column TableItem="Coupon" Title="Percent" Field="@(x => x.Percent)" Sortable="true" Filterable="true"/>
<Column TableItem="Coupon" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
<Template>
<a @onclick="() => Remove(context)" @onclick:preventDefault href="#" class="text-danger">Remove</a>
</Template>
</Column>
</Table>
</LazyLoader>
</div>
</div>
<AddCouponModal @ref="AddCouponModal" OnUpdate="() => LazyLoader.Reload()" />
@code
{
private Coupon[] AllCoupons;
private LazyLoader LazyLoader;
private AddCouponModal AddCouponModal;
private Task Load(LazyLoader _)
{
AllCoupons = CouponRepository
.Get()
.ToArray();
return Task.CompletedTask;
}
private async Task Remove(Coupon coupon)
{
if (CouponUseRepository.Get().Any(x => x.Coupon.Id == coupon.Id))
throw new DisplayException("The coupon has been used so it cannot be deleted");
CouponRepository.Delete(coupon);
await ToastService.Success("Successfully deleted coupon");
await LazyLoader.Reload();
}
}