Added relation support
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using System.Reflection;
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
namespace HopFrame.Core.Config;
|
namespace HopFrame.Core.Config;
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ public class PropertyConfig(PropertyInfo info) {
|
|||||||
public bool List { get; set; } = true;
|
public bool List { get; set; } = true;
|
||||||
public bool Sortable { get; set; } = true;
|
public bool Sortable { get; set; } = true;
|
||||||
public bool Searchable { get; set; } = true;
|
public bool Searchable { get; set; } = true;
|
||||||
|
public PropertyInfo? DisplayedProperty { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PropertyConfig<TProp>(PropertyConfig config) {
|
public class PropertyConfig<TProp>(PropertyConfig config) {
|
||||||
@@ -33,4 +35,9 @@ public class PropertyConfig<TProp>(PropertyConfig config) {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PropertyConfig<TProp> DisplayedProperty<TInnerProp>(Expression<Func<TProp, TInnerProp>> propertyExpression) {
|
||||||
|
config.DisplayedProperty = TableConfig<TProp>.GetPropertyInfo(propertyExpression);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public class TableConfig<TModel>(TableConfig innerConfig) {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PropertyInfo GetPropertyInfo<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda) {
|
internal static PropertyInfo GetPropertyInfo<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda) {
|
||||||
if (propertyLambda.Body is not MemberExpression member) {
|
if (propertyLambda.Body is not MemberExpression member) {
|
||||||
throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property.");
|
throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ namespace HopFrame.Core.Services;
|
|||||||
public interface IContextExplorer {
|
public interface IContextExplorer {
|
||||||
public IEnumerable<string> GetTableNames();
|
public IEnumerable<string> GetTableNames();
|
||||||
public TableConfig? GetTable(string tableName);
|
public TableConfig? GetTable(string tableName);
|
||||||
|
public TableConfig? GetTable(Type tableEntity);
|
||||||
public ITableManager? GetTableManager(string tableName);
|
public ITableManager? GetTableManager(string tableName);
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
namespace HopFrame.Core.Services;
|
using System.Reflection;
|
||||||
|
using HopFrame.Core.Config;
|
||||||
|
|
||||||
|
namespace HopFrame.Core.Services;
|
||||||
|
|
||||||
public interface ITableManager {
|
public interface ITableManager {
|
||||||
public IQueryable<object> LoadPage(int page, int perPage = 20);
|
public IQueryable<object> LoadPage(int page, int perPage = 20);
|
||||||
public (IEnumerable<object>, int) Search(string searchTerm, int page = 0, int perPage = 20);
|
public (IEnumerable<object>, int) Search(string searchTerm, int page = 0, int perPage = 20);
|
||||||
public int TotalPages(int perPage = 20);
|
public int TotalPages(int perPage = 20);
|
||||||
public Task DeleteItem(object item);
|
public Task DeleteItem(object item);
|
||||||
|
|
||||||
|
public string DisplayProperty(object item, PropertyInfo info, TableConfig? tableConfig);
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,16 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TableConfig? GetTable(Type tableEntity) {
|
||||||
|
foreach (var context in config.Contexts) {
|
||||||
|
var table = context.Tables.FirstOrDefault(table => table.TableType == tableEntity);
|
||||||
|
if (table is not null)
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public ITableManager? GetTableManager(string tableName) {
|
public ITableManager? GetTableManager(string tableName) {
|
||||||
foreach (var context in config.Contexts) {
|
foreach (var context in config.Contexts) {
|
||||||
var table = context.Tables.FirstOrDefault(table => table.PropertyName == tableName);
|
var table = context.Tables.FirstOrDefault(table => table.PropertyName == tableName);
|
||||||
@@ -32,9 +42,10 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
|||||||
if (dbContext is null) return null;
|
if (dbContext is null) return null;
|
||||||
|
|
||||||
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
var type = typeof(TableManager<>).MakeGenericType(table.TableType);
|
||||||
return Activator.CreateInstance(type, dbContext, table) as ITableManager;
|
return Activator.CreateInstance(type, dbContext, table, this) as ITableManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,23 @@
|
|||||||
using HopFrame.Core.Config;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Reflection;
|
||||||
|
using HopFrame.Core.Config;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace HopFrame.Core.Services.Implementations;
|
namespace HopFrame.Core.Services.Implementations;
|
||||||
|
|
||||||
internal sealed class TableManager<TModel>(DbContext context, TableConfig config) : ITableManager where TModel : class {
|
internal sealed class TableManager<TModel>(DbContext context, TableConfig config, IContextExplorer explorer) : ITableManager where TModel : class {
|
||||||
|
|
||||||
public IQueryable<object> LoadPage(int page, int perPage = 20) {
|
public IQueryable<object> LoadPage(int page, int perPage = 20) {
|
||||||
var table = context.Set<TModel>();
|
var table = context.Set<TModel>();
|
||||||
return table
|
var data = IncludeForgeinKeys(table);
|
||||||
|
return data
|
||||||
.Skip(page * perPage)
|
.Skip(page * perPage)
|
||||||
.Take(perPage);
|
.Take(perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public (IEnumerable<object>, int) Search(string searchTerm, int page = 0, int perPage = 20) {
|
public (IEnumerable<object>, int) Search(string searchTerm, int page = 0, int perPage = 20) {
|
||||||
var table = context.Set<TModel>();
|
var table = context.Set<TModel>();
|
||||||
var all = table
|
var all = IncludeForgeinKeys(table)
|
||||||
.AsEnumerable()
|
.AsEnumerable()
|
||||||
.Where(item => ItemSearched(item, searchTerm))
|
.Where(item => ItemSearched(item, searchTerm))
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -46,4 +49,34 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string DisplayProperty(object item, PropertyInfo info, TableConfig? tableConfig) {
|
||||||
|
if (item is null) return string.Empty;
|
||||||
|
var prop = tableConfig?.Properties.Find(prop => prop.Info.Name == info.Name);
|
||||||
|
if (prop is null) return item.ToString() ?? string.Empty;
|
||||||
|
|
||||||
|
var propValue = prop.Info.GetValue(item);
|
||||||
|
if (propValue is null || prop.DisplayedProperty is null)
|
||||||
|
return propValue?.ToString() ?? string.Empty;
|
||||||
|
|
||||||
|
var innerConfig = explorer.GetTable(propValue.GetType());
|
||||||
|
return DisplayProperty(propValue, prop.DisplayedProperty, innerConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IQueryable<TModel> IncludeForgeinKeys(IQueryable<TModel> query) {
|
||||||
|
var pendingQuery = query;
|
||||||
|
|
||||||
|
foreach (var property in config.Properties) {
|
||||||
|
var attr = property.Info
|
||||||
|
.GetCustomAttributes(true)
|
||||||
|
.FirstOrDefault(att => att is ForeignKeyAttribute) as ForeignKeyAttribute;
|
||||||
|
|
||||||
|
if (attr is null) continue;
|
||||||
|
|
||||||
|
pendingQuery = pendingQuery.Include(property.Info.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pendingQuery;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
private string? _displayName;
|
private string? _displayName;
|
||||||
private string? _initials;
|
private string? _initials;
|
||||||
private string? _searchValue;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
protected override async Task OnInitializedAsync() {
|
||||||
if (Config.DisplayUserInfo) {
|
if (Config.DisplayUserInfo) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<FluentDataGrid Items="_currentlyDisplayedModels?.AsQueryable()">
|
<FluentDataGrid Items="_currentlyDisplayedModels?.AsQueryable()">
|
||||||
@{ var dataIndex = 0; }
|
@{ var dataIndex = 0; }
|
||||||
@foreach (var property in _config!.Properties.Where(prop => prop.List)) {
|
@foreach (var property in _config!.Properties.Where(prop => prop.List)) {
|
||||||
<PropertyColumn Title="@property.Name" Property="o => property.Info.GetValue(o)" Style="min-width: max-content; min-height: 43px" Sortable="@property.Sortable"/>
|
<PropertyColumn Title="@property.Name" Property="o => _manager!.DisplayProperty(o, property.Info, _config)" Style="min-width: max-content; min-height: 43px" Sortable="@property.Sortable"/>
|
||||||
}
|
}
|
||||||
|
|
||||||
<TemplateColumn Title="Actions" Align="@Align.End" Style="min-height: 43px; min-width: max-content">
|
<TemplateColumn Title="Actions" Align="@Align.End" Style="min-height: 43px; min-width: max-content">
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/>
|
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/>
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
|
|
||||||
<FluentButton aria-label="Delete entry" OnClick="() => { DeleteEntry(currentElement); }">
|
<FluentButton aria-label="Delete entry" OnClick="async () => { await DeleteEntry(currentElement); }">
|
||||||
<FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning"/>
|
<FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning"/>
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
|
@using HopFrame.Testing.Models
|
||||||
|
|
||||||
<PageTitle>Home</PageTitle>
|
<PageTitle>Home</PageTitle>
|
||||||
|
|
||||||
@@ -11,20 +12,32 @@ Welcome to your new Fluent Blazor app.
|
|||||||
@code {
|
@code {
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
protected override async Task OnInitializedAsync() {
|
||||||
|
User? user = null;
|
||||||
for (int i = 0; i < 100; i++) {
|
for (int i = 0; i < 100; i++) {
|
||||||
var first = GenerateName(Random.Shared.Next(4, 6));
|
var first = GenerateName(Random.Shared.Next(4, 6));
|
||||||
var last = GenerateName(Random.Shared.Next(4, 6));
|
var last = GenerateName(Random.Shared.Next(4, 6));
|
||||||
var username = $"{first}.{last}";
|
var username = $"{first}.{last}";
|
||||||
Context.Users.Add(new() {
|
|
||||||
|
user = new() {
|
||||||
Email = $"{username}-{Random.Shared.Next(0, 20)}@gmail.com",
|
Email = $"{username}-{Random.Shared.Next(0, 20)}@gmail.com",
|
||||||
Id = Guid.CreateVersion7(),
|
Id = Guid.CreateVersion7(),
|
||||||
FirstName = first,
|
FirstName = first,
|
||||||
LastName = last,
|
LastName = last,
|
||||||
Username = username
|
Username = username
|
||||||
});
|
};
|
||||||
|
|
||||||
|
Context.Users.Add(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Context.SaveChangesAsync();
|
await Context.SaveChangesAsync();
|
||||||
|
|
||||||
|
Context.Posts.Add(new() {
|
||||||
|
Caption = "Cool Post",
|
||||||
|
Content = "This post is cool",
|
||||||
|
Author = user
|
||||||
|
});
|
||||||
|
|
||||||
|
await Context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GenerateName(int len) {
|
public static string GenerateName(int len) {
|
||||||
@@ -46,3 +59,5 @@ Welcome to your new Fluent Blazor app.
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
using HopFrame.Testing.Models;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema;
|
|||||||
namespace HopFrame.Testing.Models;
|
namespace HopFrame.Testing.Models;
|
||||||
|
|
||||||
public class Post {
|
public class Post {
|
||||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
[MaxLength(255)]
|
[MaxLength(255)]
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
namespace HopFrame.Testing.Models;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace HopFrame.Testing.Models;
|
||||||
|
|
||||||
public class User {
|
public class User {
|
||||||
|
[Key]
|
||||||
public required Guid Id { get; init; }
|
public required Guid Id { get; init; }
|
||||||
public required string Email { get; init; }
|
public required string Email { get; init; }
|
||||||
public string? Username { get; set; }
|
public string? Username { get; set; }
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ builder.Services.AddHopFrame(options => {
|
|||||||
table.Property(u => u.Id)
|
table.Property(u => u.Id)
|
||||||
.Sortable(false);
|
.Sortable(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context.Table<Post>()
|
||||||
|
.Property(p => p.Author)
|
||||||
|
.DisplayedProperty(u => u!.Username);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user