diff --git a/src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs b/src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs index 3d35d52..e8442b1 100644 --- a/src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs +++ b/src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs @@ -17,9 +17,12 @@ public interface IAdminPropertyGenerator { IAdminPropertyGenerator Description(string description); IAdminPropertyGenerator Prefix(string prefix); IAdminPropertyGenerator Validator(Func validator); - IAdminPropertyGenerator IsSelector(); + IAdminPropertyGenerator IsSelector(bool selector = true); + IAdminPropertyGenerator IsSelector(bool selector = true); IAdminPropertyGenerator Parser(Func parser); + IAdminPropertyGenerator Parser(Func parser); IAdminPropertyGenerator ParserForListType(Func parser); + IAdminPropertyGenerator ParserForListType(Func parser); IAdminPropertyGenerator DisplayProperty(Expression> propertyExpression); IAdminPropertyGenerator DisplayPropertyForListType(Expression> propertyExpression); diff --git a/src/HopFrame.Web.Admin/Generators/Implementation/AdminPropertyGenerator.cs b/src/HopFrame.Web.Admin/Generators/Implementation/AdminPropertyGenerator.cs index 9673508..5bbe01f 100644 --- a/src/HopFrame.Web.Admin/Generators/Implementation/AdminPropertyGenerator.cs +++ b/src/HopFrame.Web.Admin/Generators/Implementation/AdminPropertyGenerator.cs @@ -76,18 +76,34 @@ internal sealed class AdminPropertyGenerator(string name, Type type) return this; } - public IAdminPropertyGenerator IsSelector() { - _property.SelectorType = typeof(TSelector); + public IAdminPropertyGenerator IsSelector(bool selector = true) { + _property.Selector = selector; + return this; + } + + public IAdminPropertyGenerator IsSelector(bool selector = true) { + _property.Selector = true; + _property.SelectorType = typeof(TSelectorType); return this; } public IAdminPropertyGenerator Parser(Func parser) { - _property.Parser = (o, s) => parser.Invoke((TModel)o, s); + _property.Parser = (o, s) => parser.Invoke((TModel)o, s.ToString()); + return this; + } + + public IAdminPropertyGenerator Parser(Func parser) { + _property.Parser = (o, s) => parser.Invoke((TModel)o, (TInput)s); return this; } public IAdminPropertyGenerator ParserForListType(Func parser) { - _property.Parser = (o, s) => parser.Invoke((TModel)o, s); + _property.Parser = (o, s) => parser.Invoke((TModel)o, s.ToString()); + return this; + } + + public IAdminPropertyGenerator ParserForListType(Func parser) { + _property.Parser = (o, s) => parser.Invoke((TModel)o, (TInput)s); return this; } diff --git a/src/HopFrame.Web.Admin/Models/AdminPageProperty.cs b/src/HopFrame.Web.Admin/Models/AdminPageProperty.cs index 2ae5497..a86cf47 100644 --- a/src/HopFrame.Web.Admin/Models/AdminPageProperty.cs +++ b/src/HopFrame.Web.Admin/Models/AdminPageProperty.cs @@ -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 Validator { get; set; } - public Func Parser { get; set; } + public Func Parser { get; set; } public object GetValue(object entry) { return entry.GetType().GetProperty(Name)?.GetValue(entry); diff --git a/src/HopFrame.Web/Components/Administration/AdminPageModal.razor b/src/HopFrame.Web/Components/Administration/AdminPageModal.razor index 6a97503..f410739 100644 --- a/src/HopFrame.Web/Components/Administration/AdminPageModal.razor +++ b/src/HopFrame.Web/Components/Administration/AdminPageModal.razor @@ -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 @@
- @if (prop.SelectorType is null) { + @if (!prop.Selector) {
Add
} else { - @* TODO: implement selector - +
+ + Add +
}
@@ -80,6 +81,16 @@ } + else if (prop.Selector) { + @prop.DisplayName + + } else { @prop.DisplayName @@ -117,16 +128,18 @@ private ValidationMessageStore _validation; private Dictionary _validationIdentifiers; private IDictionary _values; + private Dictionary _selectorValues; private IModelRepository _repository; private AdminPage _currentPage; private object _entry; private bool _isEdit; - private IDictionary _inputValues; + private IDictionary _inputValues; public async Task Show(AdminPage page, object entryToEdit = null) { _entry = null; - _inputValues = new Dictionary(); + _inputValues = new Dictionary(); + _selectorValues = new Dictionary(); _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 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) { diff --git a/src/HopFrame.Web/HopAdminContext.cs b/src/HopFrame.Web/HopAdminContext.cs index 68fdd90..4e45dfe 100644 --- a/src/HopFrame.Web/HopAdminContext.cs +++ b/src/HopFrame.Web/HopAdminContext.cs @@ -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().Property(g => g.Name) .Prefix("group."); diff --git a/src/HopFrame.Web/Pages/Administration/AdminPageList.razor b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor index 947f961..c794eaf 100644 --- a/src/HopFrame.Web/Pages/Administration/AdminPageList.razor +++ b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor @@ -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(); } diff --git a/test/FrontendTest/AdminContext.cs b/test/FrontendTest/AdminContext.cs index 016c488..6be48c9 100644 --- a/test/FrontendTest/AdminContext.cs +++ b/test/FrontendTest/AdminContext.cs @@ -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
Addresses { get; set; } public AdminPage Employees { get; set; } - + + public override void OnModelCreating(IAdminContextGenerator generator) { + base.OnModelCreating(generator); + + generator.Page() + .Property(e => e.Address) + .IsSelector(); + + generator.Page
() + .Property(a => a.Employee) + .Ignore(); + + generator.Page
() + .Property(a => a.AddressId) + .IsSelector() + .Parser((model, e) => model.AddressId = e.EmployeeId); + + generator.Page() + .ConfigureRepository() + .ListingProperty(e => e.Name); + + generator.Page
() + .ConfigureRepository() + .ListingProperty(a => a.City); + } } \ No newline at end of file diff --git a/test/FrontendTest/DatabaseContext.cs b/test/FrontendTest/DatabaseContext.cs index 9a89c5d..0dede8a 100644 --- a/test/FrontendTest/DatabaseContext.cs +++ b/test/FrontendTest/DatabaseContext.cs @@ -1,12 +1,24 @@ using HopFrame.Database; using Microsoft.EntityFrameworkCore; +using RestApiTest.Models; namespace FrontendTest; public class DatabaseContext : HopDbContextBase { + public DbSet Employees { get; set; } + public DbSet
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() + .HasOne(e => e.Address) + .WithOne(a => a.Employee); + } } \ No newline at end of file diff --git a/test/FrontendTest/Providers/AddressProvider.cs b/test/FrontendTest/Providers/AddressProvider.cs new file mode 100644 index 0000000..94b203e --- /dev/null +++ b/test/FrontendTest/Providers/AddressProvider.cs @@ -0,0 +1,29 @@ +using HopFrame.Web.Admin; +using Microsoft.EntityFrameworkCore; +using RestApiTest.Models; + +namespace FrontendTest.Providers; + +public class AddressProvider(DatabaseContext context) : ModelRepository
{ + + public override async Task> ReadAll() { + return await context.Addresses.ToArrayAsync(); + } + + public override async Task
Create(Address model) { + await context.Addresses.AddAsync(model); + await context.SaveChangesAsync(); + return model; + } + + public override async Task
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(); + } +} \ No newline at end of file diff --git a/test/FrontendTest/Providers/EmployeeProvider.cs b/test/FrontendTest/Providers/EmployeeProvider.cs new file mode 100644 index 0000000..0eb2aff --- /dev/null +++ b/test/FrontendTest/Providers/EmployeeProvider.cs @@ -0,0 +1,31 @@ +using HopFrame.Web.Admin; +using Microsoft.EntityFrameworkCore; +using RestApiTest.Models; + +namespace FrontendTest.Providers; + +public class EmployeeProvider(DatabaseContext context) : ModelRepository { + + public override async Task> ReadAll() { + return await context.Employees + .Include(e => e.Address) + .ToArrayAsync(); + } + + public override async Task Create(Employee model) { + await context.Employees.AddAsync(model); + await context.SaveChangesAsync(); + return model; + } + + public override async Task 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(); + } +} \ No newline at end of file