Working on Admin page modal

This commit is contained in:
2024-10-13 17:42:40 +02:00
parent d2729870e3
commit 599ce2bf43
15 changed files with 157 additions and 14 deletions

View File

@@ -0,0 +1,4 @@
namespace HopFrame.Database.Attributes;
[AttributeUsage(AttributeTargets.Property)]
public sealed class ListingPropertyAttribute : Attribute;

View File

@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using HopFrame.Database.Attributes;
namespace HopFrame.Database.Models; namespace HopFrame.Database.Models;
@@ -9,7 +10,7 @@ public class Permission {
[Key, Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)] [Key, Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; init; } public long Id { get; init; }
[Required, MaxLength(255)] [Required, MaxLength(255), ListingProperty]
public string PermissionName { get; set; } public string PermissionName { get; set; }
[Required] [Required]

View File

@@ -1,11 +1,12 @@
using System.ComponentModel; using System.ComponentModel;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using HopFrame.Database.Attributes;
namespace HopFrame.Database.Models; namespace HopFrame.Database.Models;
public class PermissionGroup : IPermissionOwner { public class PermissionGroup : IPermissionOwner {
[Key, Required, MaxLength(50)] [Key, Required, MaxLength(50), ListingProperty]
public string Name { get; init; } public string Name { get; init; }
[Required, DefaultValue(false)] [Required, DefaultValue(false)]

View File

@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using HopFrame.Database.Attributes;
namespace HopFrame.Database.Models; namespace HopFrame.Database.Models;
@@ -8,7 +9,7 @@ public class User : IPermissionOwner {
[Key, Required, MinLength(36), MaxLength(36)] [Key, Required, MinLength(36), MaxLength(36)]
public Guid Id { get; init; } public Guid Id { get; init; }
[MaxLength(50)] [MaxLength(50), ListingProperty]
public string Username { get; set; } public string Username { get; set; }
[Required, MaxLength(50), EmailAddress] [Required, MaxLength(50), EmailAddress]

View File

@@ -23,5 +23,6 @@ public interface IAdminPageGenerator<TModel> {
IAdminPageGenerator<TModel> ConfigureRepository<TRepository>() where TRepository : ModelRepository<TModel>; IAdminPageGenerator<TModel> ConfigureRepository<TRepository>() where TRepository : ModelRepository<TModel>;
IAdminPropertyGenerator Property<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression); IAdminPropertyGenerator Property<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression);
IAdminPageGenerator<TModel> ListingProperty<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression);
} }

View File

@@ -14,5 +14,6 @@ public interface IAdminPropertyGenerator {
IAdminPropertyGenerator Description(string description); IAdminPropertyGenerator Description(string description);
IAdminPropertyGenerator Prefix(string prefix); IAdminPropertyGenerator Prefix(string prefix);
IAdminPropertyGenerator Validator(Func<object, bool> validator); IAdminPropertyGenerator Validator(Func<object, bool> validator);
IAdminPropertyGenerator IsSelector<TSelector>();
} }

View File

@@ -113,6 +113,12 @@ internal sealed class AdminPageGenerator<TModel> : IAdminPageGenerator<TModel>,
return generator; return generator;
} }
public IAdminPageGenerator<TModel> ListingProperty<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression) {
var property = GetPropertyInfo(propertyExpression);
Page.ListingProperty = property.Name;
return this;
}
public AdminPage<TModel> Compile() { public AdminPage<TModel> Compile() {
var properties = new List<AdminPageProperty>(); var properties = new List<AdminPageProperty>();

View File

@@ -70,6 +70,11 @@ internal sealed class AdminPropertyGenerator(string name, Type type) : IAdminPro
return this; return this;
} }
public IAdminPropertyGenerator IsSelector<TSelector>() {
_property.SelectorType = typeof(TSelector);
return this;
}
public AdminPageProperty Compile() { public AdminPageProperty Compile() {
_property.DisplayName ??= _property.Name; _property.DisplayName ??= _property.Name;
return _property; return _property;

View File

@@ -11,6 +11,7 @@ public class AdminPage {
public string Url { get; set; } public string Url { get; set; }
public AdminPagePermissions Permissions { get; set; } public AdminPagePermissions Permissions { get; set; }
public IList<AdminPageProperty> Properties { get; set; } public IList<AdminPageProperty> Properties { get; set; }
public string ListingProperty { get; set; }
[JsonIgnore] [JsonIgnore]
public Type RepositoryProvider { get; set; } public Type RepositoryProvider { get; set; }

View File

@@ -19,6 +19,8 @@ public sealed class AdminPageProperty {
[JsonIgnore] [JsonIgnore]
public Type Type { get; set; } public Type Type { get; set; }
public Type SelectorType { get; set; }
public Func<object, bool> Validator { get; set; } public Func<object, bool> Validator { get; set; }
public object GetValue(object entry) { public object GetValue(object entry) {

View File

@@ -7,5 +7,6 @@ public interface IAdminPagesProvider {
internal void RegisterAdminPage(string url, AdminPage page); internal void RegisterAdminPage(string url, AdminPage page);
AdminPage LoadAdminPage(string url); AdminPage LoadAdminPage(string url);
IList<AdminPage> LoadRegisteredAdminPages(); IList<AdminPage> LoadRegisteredAdminPages();
AdminPage HasPageFor(Type type);
} }

View File

@@ -16,4 +16,11 @@ public class AdminPagesProvider : IAdminPagesProvider {
public IList<AdminPage> LoadRegisteredAdminPages() { public IList<AdminPage> LoadRegisteredAdminPages() {
return _pages.Values.ToList(); return _pages.Values.ToList();
} }
public AdminPage HasPageFor(Type type) {
return _pages
.Where(p => p.Value.ModelType == type)
.Select(p => p.Value)
.SingleOrDefault();
}
} }

View File

@@ -1,11 +1,15 @@
@rendermode InteractiveServer @rendermode InteractiveServer
@using System.Collections
@using System.ComponentModel.DataAnnotations
@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
@using BlazorStrap.V5 @using BlazorStrap.V5
@using HopFrame.Database.Attributes
@using HopFrame.Web.Admin @using HopFrame.Web.Admin
@using HopFrame.Web.Admin.Models @using HopFrame.Web.Admin.Models
@using HopFrame.Web.Admin.Providers
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
@@ -23,9 +27,54 @@
@if (!_isEdit && !prop.Editable) continue; @if (!_isEdit && !prop.Editable) continue;
<div class="mb-3"> <div class="mb-3">
<BSLabel>@prop.DisplayName</BSLabel> @if (IsListType(prop)) {
<BSLabel>@prop.DisplayName</BSLabel>
<input type="@GetInputType(prop)" class="form-control" disabled="@IsDisabled(prop)" required="@IsRequired(prop)" value="@GetPropertyValue(prop)" @onchange="e => _values[prop] = e.Value"/> <BSListGroup>
<BSListGroupItem>
<BSListGroup IsFlush="true">
@foreach (var element in GetListPropertyValues(prop).Select((e, i) => new {e, i})) {
<BSListGroupItem>
<BSButton Color="BSColor.Danger" Size="Size.ExtraSmall" MarginEnd="Margins.Small" OnClick="() => DeleteListItem(prop, element.i)">
<HopIconDisplay Type="HopIconDisplay.HopIcon.Cross"/>
</BSButton>
<span>@element.e</span>
</BSListGroupItem>
}
</BSListGroup>
</BSListGroupItem>
<BSListGroupItem>
<div style="display: flex; gap: 20px">
@if (prop.SelectorType is null) {
<input type="text" class="form-control" @onchange="v => _inputValues[prop] = (string)v.Value"/>
<BSButton Color="BSColor.Secondary">Add</BSButton>
}
else {
@*<BSInput InputType="InputType.Select"> TODO: implement selector
<option selected>Select group</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>
}
}
</BSInput>
<BSButton Color="BSColor.Secondary">Add</BSButton>*@
}
</div>
</BSListGroupItem>
</BSListGroup>
}
else if (IsSwitch(prop)) {
<div class="form-check form-switch">
<BSLabel>@prop.DisplayName</BSLabel>
<input class="form-check-input" type="checkbox" checked="@_values[prop]" @onchange="e => _values[prop] = Convert.ToBoolean(e.Value)">
</div>
}
else {
<BSLabel>@prop.DisplayName</BSLabel>
<input type="@GetInputType(prop)" class="form-control" disabled="@IsDisabled(prop)" required="@IsRequired(prop)" value="@GetPropertyValue(prop)" @onchange="e => _values[prop] = e.Value"/>
}
</div> </div>
} }
</BSModalContent> </BSModalContent>
@@ -38,6 +87,7 @@
</BSModal> </BSModal>
@inject IServiceProvider Provider @inject IServiceProvider Provider
@inject IAdminPagesProvider PageProvider
@code { @code {
[Parameter] [Parameter]
@@ -51,9 +101,11 @@
private AdminPage _currentPage; private AdminPage _currentPage;
private object _entry; private object _entry;
private bool _isEdit; private bool _isEdit;
private IDictionary<AdminPageProperty, string> _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>();
_currentPage = page; _currentPage = page;
_entry = entryToEdit; _entry = entryToEdit;
@@ -81,11 +133,67 @@
private bool IsDisabled(AdminPageProperty prop) => !prop.Editable; private bool IsDisabled(AdminPageProperty prop) => !prop.Editable;
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 IsListType(AdminPageProperty prop) {
if (!prop.Type.IsGenericType) return false;
var generic = prop.Type.GenericTypeArguments[0];
var listType = typeof(IList<>).MakeGenericType(generic);
return prop.Type.IsAssignableFrom(listType);
}
private IList<string> GetListPropertyValues(AdminPageProperty prop) {
if (!IsListType(prop)) return new List<string>();
var list = new List<string>();
var values = prop.GetValue(_entry);
if (values is null) {
prop.SetValue(_entry, Activator.CreateInstance(prop.Type));
return list;
}
foreach (var value in (IEnumerable)values) {
list.Add(MapPropertyValue(value, prop));
}
return list;
}
private string GetPropertyValue(AdminPageProperty property) { private string GetPropertyValue(AdminPageProperty property) {
if (!_isEdit) return ""; if (!_isEdit) return "";
if (!property.EditDisplayValue) return ""; if (!property.EditDisplayValue) return "";
return property.GetValue(_entry)?.ToString(); return MapPropertyValue(property.GetValue(_entry), property);
}
public string MapPropertyValue(object value, AdminPageProperty property) {
if (value is null) return string.Empty;
var type = value.GetType();
var page = PageProvider.HasPageFor(type);
if (page is not null && !string.IsNullOrWhiteSpace(page.ListingProperty)) {
var prop = page.Properties
.SingleOrDefault(p => p.Name == page.ListingProperty);
if (prop is not null) {
return MapPropertyValue(prop.GetValue(value), prop);
}
}
if (type.GetProperties().Any(p => p.GetCustomAttributes(false).Any(a => a is ListingPropertyAttribute))) {
var prop = type.GetProperties()
.SingleOrDefault(p => p.GetCustomAttributes(false).Any(a => a is ListingPropertyAttribute));
return MapPropertyValue(prop?.GetValue(value), property);
}
var stringValue = value.ToString();
if (!string.IsNullOrWhiteSpace(property.Prefix)) {
return stringValue?.Replace(property.Prefix, "");
}
return stringValue;
} }
private string GetInputType(AdminPageProperty property) { private string GetInputType(AdminPageProperty property) {
@@ -97,11 +205,18 @@
private void Validate(object sender, ValidationRequestedEventArgs e) { private void Validate(object sender, ValidationRequestedEventArgs e) {
foreach (var value in _values) { foreach (var value in _values) {
if (value.Key.Validator is null) continue;
if (value.Key.Validator?.Invoke(value.Value) == true) continue; if (value.Key.Validator?.Invoke(value.Value) == true) continue;
Console.WriteLine("INVALID"); Console.WriteLine("INVALID");
} }
} }
private void DeleteListItem(AdminPageProperty prop, int index) {
Console.WriteLine(index);
var list = prop.GetValue<IList>(_entry);
list.RemoveAt(index);
}
private async void Save() { private async void Save() {
foreach (var value in _values) { foreach (var value in _values) {
value.Key.SetValue(_entry, value.Value); value.Key.SetValue(_entry, value.Value);

View File

@@ -29,7 +29,8 @@ public class HopAdminContext : AdminPagesContext {
.Editable(false); .Editable(false);
generator.Page<User>().Property(u => u.Permissions) generator.Page<User>().Property(u => u.Permissions)
.DisplayInListing(false); .DisplayInListing(false)
.IsSelector<PermissionGroup>();
generator.Page<User>().Property(u => u.Tokens) generator.Page<User>().Property(u => u.Tokens)
.Ignore(); .Ignore();

View File

@@ -214,12 +214,8 @@
StateHasChanged(); StateHasChanged();
} }
private static string GetPrintableValue(object element, AdminPageProperty prop) { private string GetPrintableValue(object element, AdminPageProperty prop) {
var entry = prop.GetValue(element); return _modal?.MapPropertyValue(prop.GetValue(element), prop);
//TODO: convert relational types
return entry?.ToString();
} }
private async void Create() { private async void Create() {