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> Description(string description);
IAdminPropertyGenerator<TProperty> Prefix(string prefix); IAdminPropertyGenerator<TProperty> Prefix(string prefix);
IAdminPropertyGenerator<TProperty> Validator(Func<TProperty, string> validator); 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>(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>(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> DisplayProperty<TListingProperty>(Expression<Func<TProperty, TListingProperty>> propertyExpression);
IAdminPropertyGenerator<TProperty> DisplayPropertyForListType<TInnerProperty>(Expression<Func<TInnerProperty, object>> 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; return this;
} }
public IAdminPropertyGenerator<TProperty> IsSelector<TSelector>() { public IAdminPropertyGenerator<TProperty> IsSelector(bool selector = true) {
_property.SelectorType = typeof(TSelector); _property.Selector = selector;
return this;
}
public IAdminPropertyGenerator<TProperty> IsSelector<TSelectorType>(bool selector = true) {
_property.Selector = true;
_property.SelectorType = typeof(TSelectorType);
return this; return this;
} }
public IAdminPropertyGenerator<TProperty> Parser<TModel>(Func<TModel, string, TProperty> parser) { 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; return this;
} }
public IAdminPropertyGenerator<TProperty> ParserForListType<TModel, TInnerProperty>(Func<TModel, string, TInnerProperty> parser) { 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; return this;
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
using FrontendTest.Providers;
using HopFrame.Web.Admin; using HopFrame.Web.Admin;
using HopFrame.Web.Admin.Generators;
using HopFrame.Web.Admin.Models; using HopFrame.Web.Admin.Models;
using RestApiTest.Models; using RestApiTest.Models;
@@ -8,5 +10,29 @@ public class AdminContext : AdminPagesContext {
public AdminPage<Address> Addresses { get; set; } public AdminPage<Address> Addresses { get; set; }
public AdminPage<Employee> Employees { 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 HopFrame.Database;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RestApiTest.Models;
namespace FrontendTest; namespace FrontendTest;
public class DatabaseContext : HopDbContextBase { public class DatabaseContext : HopDbContextBase {
public DbSet<Employee> Employees { get; set; }
public DbSet<Address> Addresses { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
base.OnConfiguring(optionsBuilder); base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\test\RestApiTest\bin\Debug\net8.0\test.db;Mode=ReadWrite;"); 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();
}
}