Implemented selector properties for admin pages
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using FrontendTest.Providers;
|
||||
using HopFrame.Web.Admin;
|
||||
using HopFrame.Web.Admin.Generators;
|
||||
using HopFrame.Web.Admin.Models;
|
||||
using RestApiTest.Models;
|
||||
|
||||
@@ -9,4 +11,28 @@ 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
29
test/FrontendTest/Providers/AddressProvider.cs
Normal file
29
test/FrontendTest/Providers/AddressProvider.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
31
test/FrontendTest/Providers/EmployeeProvider.cs
Normal file
31
test/FrontendTest/Providers/EmployeeProvider.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user