Implemented selector properties for admin pages

This commit is contained in:
2024-11-05 18:45:34 +01:00
parent d38cce6dc2
commit 0cc4eb44da
10 changed files with 200 additions and 30 deletions

View File

@@ -17,9 +17,12 @@ public interface IAdminPropertyGenerator<TProperty> {
IAdminPropertyGenerator<TProperty> Description(string description);
IAdminPropertyGenerator<TProperty> Prefix(string prefix);
IAdminPropertyGenerator<TProperty> Validator(Func<TProperty, string> validator);
IAdminPropertyGenerator<TProperty> IsSelector<TSelector>();
IAdminPropertyGenerator<TProperty> IsSelector(bool selector = true);
IAdminPropertyGenerator<TProperty> IsSelector<TSelectorType>(bool selector = true);
IAdminPropertyGenerator<TProperty> Parser<TModel>(Func<TModel, string, TProperty> parser);
IAdminPropertyGenerator<TProperty> Parser<TModel, TInput>(Func<TModel, TInput, TProperty> parser);
IAdminPropertyGenerator<TProperty> ParserForListType<TModel, TInnerProperty>(Func<TModel, string, TInnerProperty> parser);
IAdminPropertyGenerator<TProperty> ParserForListType<TModel, TInnerProperty, TInput>(Func<TModel, TInput, TInnerProperty> parser);
IAdminPropertyGenerator<TProperty> DisplayProperty<TListingProperty>(Expression<Func<TProperty, TListingProperty>> propertyExpression);
IAdminPropertyGenerator<TProperty> DisplayPropertyForListType<TInnerProperty>(Expression<Func<TInnerProperty, object>> propertyExpression);

View File

@@ -76,18 +76,34 @@ internal sealed class AdminPropertyGenerator<TProperty>(string name, Type type)
return this;
}
public IAdminPropertyGenerator<TProperty> IsSelector<TSelector>() {
_property.SelectorType = typeof(TSelector);
public IAdminPropertyGenerator<TProperty> IsSelector(bool selector = true) {
_property.Selector = selector;
return this;
}
public IAdminPropertyGenerator<TProperty> IsSelector<TSelectorType>(bool selector = true) {
_property.Selector = true;
_property.SelectorType = typeof(TSelectorType);
return this;
}
public IAdminPropertyGenerator<TProperty> Parser<TModel>(Func<TModel, string, TProperty> parser) {
_property.Parser = (o, s) => parser.Invoke((TModel)o, s);
_property.Parser = (o, s) => parser.Invoke((TModel)o, s.ToString());
return this;
}
public IAdminPropertyGenerator<TProperty> Parser<TModel, TInput>(Func<TModel, TInput, TProperty> parser) {
_property.Parser = (o, s) => parser.Invoke((TModel)o, (TInput)s);
return this;
}
public IAdminPropertyGenerator<TProperty> ParserForListType<TModel, TInnerProperty>(Func<TModel, string, TInnerProperty> parser) {
_property.Parser = (o, s) => parser.Invoke((TModel)o, s);
_property.Parser = (o, s) => parser.Invoke((TModel)o, s.ToString());
return this;
}
public IAdminPropertyGenerator<TProperty> ParserForListType<TModel, TInnerProperty, TInput>(Func<TModel, TInput, TInnerProperty> parser) {
_property.Parser = (o, s) => parser.Invoke((TModel)o, (TInput)s);
return this;
}

View File

@@ -18,14 +18,14 @@ public sealed class AdminPageProperty {
public bool Required { get; set; }
public bool Ignore { get; set; }
public bool Unique { get; set; }
public bool Selector { get; set; }
public Type SelectorType { get; set; }
[JsonIgnore]
public Type Type { get; set; }
public Type SelectorType { get; set; }
public Func<object, string> Validator { get; set; }
public Func<object, string, object> Parser { get; set; }
public Func<object, object, object> Parser { get; set; }
public object GetValue(object entry) {
return entry.GetType().GetProperty(Name)?.GetValue(entry);

View File

@@ -1,6 +1,7 @@
@rendermode InteractiveServer
@using System.Collections
@using System.Globalization
@using BlazorStrap
@using BlazorStrap.Shared.Components.Modal
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@@ -46,23 +47,23 @@
</BSListGroupItem>
<BSListGroupItem>
<div>
@if (prop.SelectorType is null) {
@if (!prop.Selector) {
<form style="display: flex; gap: 20px" @onsubmit="() => AddListItem(prop)">
<input type="text" class="form-control" @onchange="v => _inputValues[prop] = (string)v.Value" required/>
<BSButton Color="BSColor.Secondary" IsSubmit="true">Add</BSButton>
</form>
}
else {
@*<BSInput InputType="InputType.Select"> TODO: implement selector
<option selected>Select group</option>
<form style="display: flex; gap: 20px" @onsubmit="() => AddListItem(prop)">
<select class="form-select" @onchange="e => _inputValues[prop] = ReadSelectorValue(prop, e.Value)">
<option selected>Select</option>
@foreach (var group in _allGroups) {
@if (_group.Permissions.All(g => g.PermissionName != group.Name) && group.Name != _group.Name) {
<option value="@group.Name">@group.Name.Replace("group.", "")</option>
@foreach (var element in SetupSelectorProperty(prop).GetAwaiter().GetResult()) {
<option value="@element.Item2">@element.Item1</option>
}
}
</BSInput>
<BSButton Color="BSColor.Secondary">Add</BSButton>*@
</select>
<BSButton Color="BSColor.Secondary" IsSubmit="true">Add</BSButton>
</form>
}
</div>
</BSListGroupItem>
@@ -80,6 +81,16 @@
<input type="text" class="form-control" required="@IsRequired(prop)" disabled="@IsDisabled(prop)" value="@GetPropertyValue(prop)" @onchange="e => _values[prop] = prop.Prefix + e.Value"/>
</BSInputGroup>
}
else if (prop.Selector) {
<BSLabel>@prop.DisplayName</BSLabel>
<select class="form-select" @onchange="e => _values[prop] = ReadSelectorValue(prop, e.Value)">
<option>Select</option>
@foreach (var element in SetupSelectorProperty(prop).GetAwaiter().GetResult()) {
<option value="@element.Item2" selected="@IsIndexSelected(prop, element.Item2)">@element.Item1</option>
}
</select>
}
else {
<BSLabel>@prop.DisplayName</BSLabel>
<input type="@GetInputType(prop)" class="form-control" required="@IsRequired(prop)" disabled="@IsDisabled(prop)" value="@GetPropertyValue(prop)" @onchange="e => _values[prop] = e.Value"/>
@@ -117,16 +128,18 @@
private ValidationMessageStore _validation;
private Dictionary<AdminPageProperty, FieldIdentifier> _validationIdentifiers;
private IDictionary<AdminPageProperty, object> _values;
private Dictionary<AdminPageProperty, object[]> _selectorValues;
private IModelRepository _repository;
private AdminPage _currentPage;
private object _entry;
private bool _isEdit;
private IDictionary<AdminPageProperty, string> _inputValues;
private IDictionary<AdminPageProperty, object> _inputValues;
public async Task Show(AdminPage page, object entryToEdit = null) {
_entry = null;
_inputValues = new Dictionary<AdminPageProperty, string>();
_inputValues = new Dictionary<AdminPageProperty, object>();
_selectorValues = new Dictionary<AdminPageProperty, object[]>();
_currentPage = page;
_entry = entryToEdit;
@@ -158,13 +171,14 @@
private bool IsDisabled(AdminPageProperty prop) => (_isEdit && !prop.Editable) || prop.Generated;
private bool IsRequired(AdminPageProperty prop) => !_isEdit ? prop.Required : prop.Required && prop.EditDisplayValue;
private bool IsSwitch(AdminPageProperty prop) => prop.Type == typeof(bool);
private bool IsListType(AdminPageProperty prop) => IsListType(prop.Type);
private bool IsListType(AdminPageProperty prop) {
if (!prop.Type.IsGenericType) return false;
var generic = prop.Type.GenericTypeArguments[0];
private bool IsListType(Type type) {
if (!type.IsGenericType) return false;
var generic = type.GenericTypeArguments[0];
var gListType = typeof(IList<>).MakeGenericType(generic);
var iListType = typeof(List<>).MakeGenericType(generic);
return prop.Type.IsAssignableFrom(gListType) || prop.Type.IsAssignableFrom(iListType);
return type.IsAssignableFrom(gListType) || type.IsAssignableFrom(iListType);
}
private IList<string> GetListPropertyValues(AdminPageProperty prop) {
@@ -232,6 +246,7 @@
_validation.Clear();
foreach (var value in _values) {
if (value.Key.Unique) {
if (value.Value == value.Key.GetValue(_entry)) continue;
var repo = Provider.GetService(_currentPage.RepositoryProvider) as IModelRepository;
var data = repo!.ReadAllO().GetAwaiter().GetResult();
foreach (var entry in data) {
@@ -255,7 +270,7 @@
}
private void AddListItem(AdminPageProperty prop) {
if (!_inputValues.TryGetValue(prop, out var input)) {
if (!_inputValues.TryGetValue(prop, out var input) || input is null) {
Alerts.FireAsync(new SweetAlertOptions {
Title = "Error!",
Text = "Please enter a value!",
@@ -269,6 +284,43 @@
list?.Add(value);
}
private async Task<(string, int)[]> SetupSelectorProperty(AdminPageProperty property) {
var type = property.SelectorType ?? property.Type;
if (IsListType(type)) {
type = type.GenericTypeArguments[0];
}
var page = PageProvider.HasPageFor(type);
if (page is null) {
throw new ArgumentException($"'{property.Name}' cannot be a selector because a admin page for '{type.Name}' does not exist!");
}
var repo = Provider.GetService(page.RepositoryProvider) as IModelRepository;
var objects = (await repo!.ReadAllO()).ToArray();
_selectorValues[property] = objects;
var data = new List<(string, int)>();
for (var i = 0; i < objects.Length; i++) {
data.Add((MapPropertyValue(objects[i], property), i));
}
return data.ToArray();
}
private bool IsIndexSelected(AdminPageProperty property, int index) {
var value = property.GetValue(_entry);
if (value is null) return false;
return _selectorValues[property][index] == value;
}
private object ReadSelectorValue(AdminPageProperty property, object value) {
if (!int.TryParse(value.ToString(), out int result)) {
return null;
}
return _selectorValues[property][result];
}
private async void Save() {
if (_isEdit && _currentPage.Permissions.Update is not null) {
if (!await Permissions.HasPermission(Auth.User, _currentPage.Permissions.Update)) {
@@ -292,7 +344,7 @@
foreach (var value in _values) {
if (IsListType(value.Key)) continue;
value.Key.SetValue(_entry, value.Key.Parser?.Invoke(_entry, (string)value.Value) ?? value.Value);
value.Key.SetValue(_entry, value.Key.Parser?.Invoke(_entry, value.Value) ?? Convert.ChangeType(value.Value, value.Key.Type));
}
if (!_isEdit) {

View File

@@ -63,7 +63,8 @@ public class HopAdminContext : AdminPagesContext {
.ViewPermission(AdminPermissions.ViewGroups)
.CreatePermission(AdminPermissions.AddGroup)
.UpdatePermission(AdminPermissions.EditGroup)
.DeletePermission(AdminPermissions.DeleteGroup);
.DeletePermission(AdminPermissions.DeleteGroup)
.ListingProperty(g => g.Name);
generator.Page<PermissionGroup>().Property(g => g.Name)
.Prefix("group.");

View File

@@ -134,8 +134,8 @@
throw new ArgumentException($"AdminPage '{_pageData.Title}' does not specify a model repository!'");
_modelRepository = Provider.GetService(_pageData.RepositoryProvider) as IModelRepository;
_hasEditPermission = await Permissions.HasPermission(Auth.User, _pageData.Permissions.Update);
_hasDeletePermission = await Permissions.HasPermission(Auth.User, _pageData.Permissions.Delete);
_hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Update);
_hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Delete);
await Reload();
}

View File

@@ -1,4 +1,6 @@
using FrontendTest.Providers;
using HopFrame.Web.Admin;
using HopFrame.Web.Admin.Generators;
using HopFrame.Web.Admin.Models;
using RestApiTest.Models;
@@ -8,5 +10,29 @@ public class AdminContext : AdminPagesContext {
public AdminPage<Address> Addresses { get; set; }
public AdminPage<Employee> Employees { get; set; }
public override void OnModelCreating(IAdminContextGenerator generator) {
base.OnModelCreating(generator);
generator.Page<Employee>()
.Property(e => e.Address)
.IsSelector();
generator.Page<Address>()
.Property(a => a.Employee)
.Ignore();
generator.Page<Address>()
.Property(a => a.AddressId)
.IsSelector<Employee>()
.Parser<Address, Employee>((model, e) => model.AddressId = e.EmployeeId);
generator.Page<Employee>()
.ConfigureRepository<EmployeeProvider>()
.ListingProperty(e => e.Name);
generator.Page<Address>()
.ConfigureRepository<AddressProvider>()
.ListingProperty(a => a.City);
}
}

View File

@@ -1,12 +1,24 @@
using HopFrame.Database;
using Microsoft.EntityFrameworkCore;
using RestApiTest.Models;
namespace FrontendTest;
public class DatabaseContext : HopDbContextBase {
public DbSet<Employee> Employees { get; set; }
public DbSet<Address> Addresses { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\test\RestApiTest\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
}
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Employee>()
.HasOne(e => e.Address)
.WithOne(a => a.Employee);
}
}

View File

@@ -0,0 +1,29 @@
using HopFrame.Web.Admin;
using Microsoft.EntityFrameworkCore;
using RestApiTest.Models;
namespace FrontendTest.Providers;
public class AddressProvider(DatabaseContext context) : ModelRepository<Address> {
public override async Task<IEnumerable<Address>> ReadAll() {
return await context.Addresses.ToArrayAsync();
}
public override async Task<Address> Create(Address model) {
await context.Addresses.AddAsync(model);
await context.SaveChangesAsync();
return model;
}
public override async Task<Address> Update(Address model) {
context.Addresses.Update(model);
await context.SaveChangesAsync();
return model;
}
public override async Task Delete(Address model) {
context.Addresses.Remove(model);
await context.SaveChangesAsync();
}
}

View File

@@ -0,0 +1,31 @@
using HopFrame.Web.Admin;
using Microsoft.EntityFrameworkCore;
using RestApiTest.Models;
namespace FrontendTest.Providers;
public class EmployeeProvider(DatabaseContext context) : ModelRepository<Employee> {
public override async Task<IEnumerable<Employee>> ReadAll() {
return await context.Employees
.Include(e => e.Address)
.ToArrayAsync();
}
public override async Task<Employee> Create(Employee model) {
await context.Employees.AddAsync(model);
await context.SaveChangesAsync();
return model;
}
public override async Task<Employee> Update(Employee model) {
context.Employees.Update(model);
await context.SaveChangesAsync();
return model;
}
public override async Task Delete(Employee model) {
context.Employees.Remove(model);
await context.SaveChangesAsync();
}
}