Started working on frontend
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/src/">
|
<Folder Name="/src/">
|
||||||
<Project Path="src/HopFrame.Core/HopFrame.Core.csproj" />
|
<Project Path="src/HopFrame.Core/HopFrame.Core.csproj" />
|
||||||
|
<Project Path="src/HopFrame.Web/HopFrame.Web.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
<Project Path="tests/HopFrame.Tests.Core/HopFrame.Tests.Core.csproj" />
|
<Project Path="tests/HopFrame.Tests.Core/HopFrame.Tests.Core.csproj" />
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbCont
|
|||||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity<Post>()
|
||||||
|
.HasKey(p => p.Id);
|
||||||
|
|
||||||
|
modelBuilder.Entity<User>()
|
||||||
|
.HasKey(u => u.Id);
|
||||||
|
|
||||||
modelBuilder.Entity<User>()
|
modelBuilder.Entity<User>()
|
||||||
.HasMany(u => u.Posts)
|
.HasMany(u => u.Posts)
|
||||||
.WithOne(p => p.Sender)
|
.WithOne(p => p.Sender)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using HopFrame.Core;
|
|
||||||
using HopFrame.Core.EFCore;
|
using HopFrame.Core.EFCore;
|
||||||
|
using HopFrame.Web;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using TestApplication;
|
using TestApplication;
|
||||||
using TestApplication.Components;
|
using TestApplication.Components;
|
||||||
|
using TestApplication.Models;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -10,13 +11,23 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents();
|
.AddInteractiveServerComponents();
|
||||||
|
|
||||||
builder.Services.AddEntityFrameworkInMemoryDatabase();
|
|
||||||
builder.Services.AddDbContext<DatabaseContext>(options => {
|
builder.Services.AddDbContext<DatabaseContext>(options => {
|
||||||
options.UseInMemoryDatabase("testing");
|
options.UseInMemoryDatabase("testing");
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddHopFrame(config => {
|
builder.Services.AddHopFrame(config => {
|
||||||
config.AddDbContext<DatabaseContext>();
|
config.AddDbContext<DatabaseContext>();
|
||||||
|
|
||||||
|
config.Table<User>(table => {
|
||||||
|
table.SetDescription("The user dataset. It contains all information for the users of the application.");
|
||||||
|
|
||||||
|
table.Property(u => u.Password)
|
||||||
|
.Listable(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
config.Table<Post>(table => {
|
||||||
|
table.SetDescription("The posts dataset. It contains all posts sent via the application.");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
@@ -28,6 +39,27 @@ if (!app.Environment.IsDevelopment()) {
|
|||||||
app.UseHsts();
|
app.UseHsts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await using (var scope = app.Services.CreateAsyncScope()) {
|
||||||
|
var context = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
|
||||||
|
|
||||||
|
foreach (var _ in Enumerable.Range(1, 100)) {
|
||||||
|
var firstName = Faker.Name.First();
|
||||||
|
var lastName = Faker.Name.Last();
|
||||||
|
|
||||||
|
context.Users.Add(new() {
|
||||||
|
Email = $"{firstName}.{lastName}@gmail.com".ToLower(),
|
||||||
|
Username = $"{firstName}.{lastName}".ToLower(),
|
||||||
|
FirstName = firstName,
|
||||||
|
LastName = lastName,
|
||||||
|
Description = Faker.Lorem.Paragraph(),
|
||||||
|
Birth = DateOnly.FromDateTime(Faker.Identification.DateOfBirth()),
|
||||||
|
Password = Faker.RandomNumber.Next(100000L, 99999999999999999L).ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
|
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
@@ -35,6 +67,7 @@ app.UseAntiforgery();
|
|||||||
|
|
||||||
app.MapStaticAssets();
|
app.MapStaticAssets();
|
||||||
app.MapRazorComponents<App>()
|
app.MapRazorComponents<App>()
|
||||||
.AddInteractiveServerRenderMode();
|
.AddInteractiveServerRenderMode()
|
||||||
|
.AddHopFrame();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
@@ -9,9 +9,11 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\HopFrame.Core\HopFrame.Core.csproj" />
|
<ProjectReference Include="..\..\src\HopFrame.Core\HopFrame.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\HopFrame.Web\HopFrame.Web.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Faker.Net" Version="2.0.163" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -50,4 +50,21 @@ public class HopFrameConfigurator(HopFrameConfig config, IServiceCollection serv
|
|||||||
configurator?.Invoke(new TableConfigurator<TModel>(config));
|
configurator?.Invoke(new TableConfigurator<TModel>(config));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads the configurator for an existing table in the configuration
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configurator">The configurator for the table</param>
|
||||||
|
/// <typeparam name="TModel">The model of the table</typeparam>
|
||||||
|
/// <exception cref="ArgumentException">Is thrown when no table with the requested type was found</exception>
|
||||||
|
public TableConfigurator<TModel> Table<TModel>(Action<TableConfigurator<TModel>>? configurator = null) where TModel : class {
|
||||||
|
var table = Config.Tables.FirstOrDefault(t => t.TableType == typeof(TModel));
|
||||||
|
|
||||||
|
if (table is null)
|
||||||
|
throw new ArgumentException($"Table '{typeof(TModel).Name}' not found");
|
||||||
|
|
||||||
|
var modeller = new TableConfigurator<TModel>(table);
|
||||||
|
configurator?.Invoke(modeller);
|
||||||
|
return modeller;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,42 +3,36 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
|
|
||||||
namespace HopFrame.Core.EFCore;
|
namespace HopFrame.Core.EFCore;
|
||||||
|
|
||||||
/// <summary>
|
internal class EfCoreRepository<TModel, TContext>(TContext context) : HopFrameRepository<TModel> where TModel : class where TContext : DbContext {
|
||||||
/// The generic repository that handles data source communication for managed tables
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TModel">The model that is managed by the repo</typeparam>
|
|
||||||
/// <typeparam name="TContext">The underlying context that handles database communication</typeparam>
|
|
||||||
public class EfCoreRepository<TModel, TContext>(TContext context) : HopFrameRepository<TModel> where TModel : class where TContext : DbContext {
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
public override async Task<IEnumerable<TModel>> LoadPageAsync(int page, int perPage, CancellationToken ct = default) {
|
||||||
public override Task<IEnumerable<TModel>> LoadPageAsync(int page, int perPage, CancellationToken ct = default) {
|
var set = context.Set<TModel>();
|
||||||
throw new NotImplementedException(); //TODO: Implement loading functionality
|
return await set
|
||||||
|
.AsNoTracking()
|
||||||
|
.Skip(page * perPage)
|
||||||
|
.Take(perPage)
|
||||||
|
.ToArrayAsync(ct); //TODO: Implement FK loading
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override async Task<int> CountAsync(CancellationToken ct = default) {
|
public override async Task<int> CountAsync(CancellationToken ct = default) {
|
||||||
var table = context.Set<TModel>();
|
var set = context.Set<TModel>();
|
||||||
return await table.CountAsync(ct);
|
return await set.CountAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override Task<IEnumerable<TModel>> SearchAsync(string searchTerm, int page, int perPage, CancellationToken ct = default) {
|
public override Task<IEnumerable<TModel>> SearchAsync(string searchTerm, int page, int perPage, CancellationToken ct = default) {
|
||||||
throw new NotImplementedException(); //TODO: Implement search functionality
|
return LoadPageAsync(page, perPage, ct); //TODO: Implement search functionality
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override async Task CreateAsync(TModel entry, CancellationToken ct = default) {
|
public override async Task CreateAsync(TModel entry, CancellationToken ct = default) {
|
||||||
await context.AddAsync(entry, ct);
|
await context.AddAsync(entry, ct);
|
||||||
await context.SaveChangesAsync(ct);
|
await context.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override async Task UpdateAsync(TModel entry, CancellationToken ct = default) {
|
public override async Task UpdateAsync(TModel entry, CancellationToken ct = default) {
|
||||||
context.Update(entry);
|
context.Update(entry);
|
||||||
await context.SaveChangesAsync(ct);
|
await context.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override async Task DeleteAsync(TModel entry, CancellationToken ct = default) {
|
public override async Task DeleteAsync(TModel entry, CancellationToken ct = default) {
|
||||||
context.Remove(entry);
|
context.Remove(entry);
|
||||||
await context.SaveChangesAsync(ct);
|
await context.SaveChangesAsync(ct);
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ internal static class ConfigurationHelper {
|
|||||||
RepositoryType = repositoryType,
|
RepositoryType = repositoryType,
|
||||||
TableType = modelType,
|
TableType = modelType,
|
||||||
Identifier = identifier,
|
Identifier = identifier,
|
||||||
Route = modelType.Name.ToLower(),
|
Route = modelType.Name.ToLower() + 's',
|
||||||
DisplayName = modelType.Name,
|
DisplayName = modelType.Name + 's',
|
||||||
OrderIndex = global.Tables.Count
|
OrderIndex = global.Tables.Count
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ namespace HopFrame.Core;
|
|||||||
public static class ServiceCollectionExtensions {
|
public static class ServiceCollectionExtensions {
|
||||||
|
|
||||||
/// Configures the library using the provided configurator
|
/// Configures the library using the provided configurator
|
||||||
public static void AddHopFrame(this IServiceCollection services, Action<HopFrameConfigurator> configurator) {
|
public static IServiceCollection AddHopFrameServices(this IServiceCollection services, Action<HopFrameConfigurator> configurator) {
|
||||||
var config = new HopFrameConfig();
|
var config = new HopFrameConfig();
|
||||||
services.AddSingleton(config);
|
services.AddSingleton(config);
|
||||||
|
|
||||||
services.AddTransient<IConfigAccessor, ConfigAccessor>();
|
services.AddTransient<IConfigAccessor, ConfigAccessor>();
|
||||||
|
services.AddTransient<IEntityAccessor, EntityAccessor>();
|
||||||
|
|
||||||
configurator.Invoke(new HopFrameConfigurator(config, services));
|
configurator.Invoke(new HopFrameConfigurator(config, services));
|
||||||
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
23
src/HopFrame.Core/Services/IEntityAccessor.cs
Normal file
23
src/HopFrame.Core/Services/IEntityAccessor.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using HopFrame.Core.Configuration;
|
||||||
|
|
||||||
|
namespace HopFrame.Core.Services;
|
||||||
|
|
||||||
|
/// A service used to modify the actual properties of a model
|
||||||
|
public interface IEntityAccessor {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the formatted content of the property, ready to be displayed
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">The model to pull the property from</param>
|
||||||
|
/// <param name="property">The property that shall be extracted</param>
|
||||||
|
public Task<string?> GetValue(object model, PropertyConfig property);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Properly formats and sets the new value of the property
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">The model to save the property to</param>
|
||||||
|
/// <param name="property">The property that shall be modified</param>
|
||||||
|
/// <param name="value">The new value of the property</param>
|
||||||
|
public Task SetValue(object model, PropertyConfig property, object value);
|
||||||
|
|
||||||
|
}
|
||||||
25
src/HopFrame.Core/Services/Implementation/EntityAccessor.cs
Normal file
25
src/HopFrame.Core/Services/Implementation/EntityAccessor.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using HopFrame.Core.Configuration;
|
||||||
|
|
||||||
|
namespace HopFrame.Core.Services.Implementation;
|
||||||
|
|
||||||
|
internal class EntityAccessor : IEntityAccessor {
|
||||||
|
|
||||||
|
public async Task<string?> GetValue(object model, PropertyConfig property) {
|
||||||
|
var prop = model.GetType().GetProperty(property.Identifier);
|
||||||
|
|
||||||
|
if (prop is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return prop.GetValue(model)?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetValue(object model, PropertyConfig property, object value) {
|
||||||
|
var prop = model.GetType().GetProperty(property.Identifier);
|
||||||
|
|
||||||
|
if (prop is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
prop.SetValue(model, Convert.ChangeType(value, property.Type));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
22
src/HopFrame.Web/Components/App.razor
Normal file
22
src/HopFrame.Web/Components/App.razor
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<base href="/"/>
|
||||||
|
<ImportMap/>
|
||||||
|
<HeadOutlet/>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<Router AppAssembly="typeof(ServiceCollectionExtensions).Assembly">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<RouteView RouteData="routeData"/>
|
||||||
|
</Found>
|
||||||
|
</Router>
|
||||||
|
<script src="@Assets["_framework/blazor.web.js"]"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
11
src/HopFrame.Web/Components/CancellableComponent.cs
Normal file
11
src/HopFrame.Web/Components/CancellableComponent.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Components;
|
||||||
|
|
||||||
|
public class CancellableComponent : ComponentBase, IDisposable {
|
||||||
|
protected CancellationTokenSource TokenSource { get; } = new();
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
TokenSource.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/HopFrame.Web/Components/Components/Card.razor
Normal file
33
src/HopFrame.Web/Components/Components/Card.razor
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<MudCard Style="width: 350px; height: 200px">
|
||||||
|
<MudCardHeader>
|
||||||
|
<CardHeaderAvatar>
|
||||||
|
<MudIcon Icon="@Icon" Style="margin: auto" />
|
||||||
|
</CardHeaderAvatar>
|
||||||
|
<CardHeaderContent>
|
||||||
|
<MudText Typo="Typo.h6">@Title</MudText>
|
||||||
|
</CardHeaderContent>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent>
|
||||||
|
<MudText>@Description</MudText>
|
||||||
|
</MudCardContent>
|
||||||
|
<MudCardActions>
|
||||||
|
<MudButton Href="@Href">Open</MudButton>
|
||||||
|
</MudCardActions>
|
||||||
|
</MudCard>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public required string Title { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? Subtitle { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public required string Href { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public required string Icon { get; set; }
|
||||||
|
}
|
||||||
40
src/HopFrame.Web/Components/Components/Sidebar.razor
Normal file
40
src/HopFrame.Web/Components/Components/Sidebar.razor
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
@using HopFrame.Core.Configuration
|
||||||
|
|
||||||
|
<MudDrawer Open="true" Fixed="true" ClipMode="DrawerClipMode.Docked" Width="200px">
|
||||||
|
<MudNavMenu>
|
||||||
|
<MudNavLink Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.SpaceDashboard" Href="admin">Dashboard</MudNavLink>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
@foreach (var route in Routes) {
|
||||||
|
<MudNavLink Match="NavLinkMatch.All" Icon="@route.Icon" Href="@route.Route">@route.Name</MudNavLink>
|
||||||
|
}
|
||||||
|
</MudNavMenu>
|
||||||
|
</MudDrawer>
|
||||||
|
|
||||||
|
@inject HopFrameConfig Config
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
private readonly struct RouteDefinition {
|
||||||
|
public required string Route { get; init; }
|
||||||
|
public required string Icon { get; init; }
|
||||||
|
public required string Name { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private RouteDefinition[] Routes { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override void OnInitialized() {
|
||||||
|
base.OnInitialized();
|
||||||
|
|
||||||
|
Routes = Config.Tables
|
||||||
|
.OrderBy(t => t.OrderIndex)
|
||||||
|
.Select(table => new RouteDefinition {
|
||||||
|
Route = "admin/" + table.Route,
|
||||||
|
Icon = Icons.Material.Filled.TableChart,
|
||||||
|
Name = table.DisplayName
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
59
src/HopFrame.Web/Components/Components/Table.razor
Normal file
59
src/HopFrame.Web/Components/Components/Table.razor
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
<MudTable ServerData="Reload"
|
||||||
|
@ref="Manager"
|
||||||
|
Hover="true"
|
||||||
|
Breakpoint="Breakpoint.Sm"
|
||||||
|
LoadingProgressColor="Color.Info"
|
||||||
|
HorizontalScrollbar="true"
|
||||||
|
FixedHeader="true"
|
||||||
|
FixedFooter="true"
|
||||||
|
Height="calc(100vh - 164px)">
|
||||||
|
<ToolBarContent>
|
||||||
|
<MudText Typo="Typo.h6">@Config.DisplayName</MudText>
|
||||||
|
<MudSpacer />
|
||||||
|
<MudStack Row="true" Spacing="2" Style="min-width: 500px">
|
||||||
|
<MudTextField
|
||||||
|
T="string"
|
||||||
|
Placeholder="Search"
|
||||||
|
Adornment="Adornment.Start"
|
||||||
|
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||||
|
IconSize="Size.Medium"
|
||||||
|
Class="mt-0"
|
||||||
|
FullWidth="true"
|
||||||
|
Clearable="true"
|
||||||
|
DebounceInterval="200"
|
||||||
|
OnDebounceIntervalElapsed="@(s => OnSearch(s))"/>
|
||||||
|
<MudButton EndIcon="@Icons.Material.Filled.Add" Style="margin-right: 0.5rem">Add</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
</ToolBarContent>
|
||||||
|
<HeaderContent>
|
||||||
|
@foreach (var prop in OrderedProperties) {
|
||||||
|
<MudTh>
|
||||||
|
@if (prop.Sortable) {
|
||||||
|
<MudTableSortLabel SortLabel="@prop.Identifier" T="string">@prop.DisplayName</MudTableSortLabel>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
@prop.DisplayName
|
||||||
|
}
|
||||||
|
</MudTh>
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudTh>Actions</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
@foreach (var prop in OrderedProperties) {
|
||||||
|
<MudTd DataLabel="@prop.DisplayName" Style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden; max-width: 500px">@context[prop.Identifier]</MudTd>
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudTd DataLabel="Actions">
|
||||||
|
<MudStack Row="true" Spacing="1">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small" />
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error" />
|
||||||
|
</MudStack>
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
<PagerContent>
|
||||||
|
<MudTablePager />
|
||||||
|
</PagerContent>
|
||||||
|
</MudTable>
|
||||||
68
src/HopFrame.Web/Components/Components/Table.razor.cs
Normal file
68
src/HopFrame.Web/Components/Components/Table.razor.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using HopFrame.Core.Configuration;
|
||||||
|
using HopFrame.Core.Repositories;
|
||||||
|
using HopFrame.Core.Services;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Components.Components;
|
||||||
|
|
||||||
|
public partial class Table(IEntityAccessor accessor, IConfigAccessor configAccessor) : ComponentBase {
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public required TableConfig Config { get; set; }
|
||||||
|
|
||||||
|
private IHopFrameRepository Repository { get; set; } = null!;
|
||||||
|
|
||||||
|
private PropertyConfig[] OrderedProperties { get; set; } = null!;
|
||||||
|
|
||||||
|
private MudTable<Dictionary<string, string>> Manager { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override void OnInitialized() {
|
||||||
|
base.OnInitialized();
|
||||||
|
|
||||||
|
Repository = configAccessor.LoadRepository(Config);
|
||||||
|
|
||||||
|
OrderedProperties = Config.Properties
|
||||||
|
.Where(p => p.Listable)
|
||||||
|
.OrderBy(p => p.OrderIndex)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<Dictionary<string, string>>> PrepareData(object[] entries) {
|
||||||
|
var list = new List<Dictionary<string, string>>();
|
||||||
|
|
||||||
|
foreach (var entry in entries) {
|
||||||
|
var taskDict = new Dictionary<string, Task<string?>>();
|
||||||
|
foreach (var prop in OrderedProperties) {
|
||||||
|
taskDict.Add(prop.Identifier, accessor.GetValue(entry, prop));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(taskDict.Values);
|
||||||
|
|
||||||
|
var dict = new Dictionary<string, string>();
|
||||||
|
foreach (var prop in taskDict) {
|
||||||
|
dict.Add(prop.Key, prop.Value.Result ?? string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(dict);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<TableData<Dictionary<string, string>>> Reload(TableState state, CancellationToken ct) {
|
||||||
|
var entries = await Repository.LoadPageGenericAsync(state.Page, state.PageSize, ct);
|
||||||
|
var data = await PrepareData(entries.Cast<object>().ToArray());
|
||||||
|
var total = await Repository.CountAsync(ct);
|
||||||
|
|
||||||
|
return new TableData<Dictionary<string, string>> {
|
||||||
|
TotalItems = total,
|
||||||
|
Items = data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSearch(string searchText) {
|
||||||
|
Console.WriteLine(searchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
3
src/HopFrame.Web/Components/Components/Topbar.razor
Normal file
3
src/HopFrame.Web/Components/Components/Topbar.razor
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<MudAppBar Dense="true" Elevation="0">
|
||||||
|
HopFrame
|
||||||
|
</MudAppBar>
|
||||||
18
src/HopFrame.Web/Components/HopFrameLayout.razor
Normal file
18
src/HopFrame.Web/Components/HopFrameLayout.razor
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
@using HopFrame.Web.Components.Components
|
||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="@Assets["_content/HopFrame.Web/HopFrame.Web.bundle.scp.css"]"/>
|
||||||
|
<link rel="stylesheet" href="@Assets["_content/HopFrame.Web/hopframe.css"]"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
||||||
|
<link href="@Assets["_content/MudBlazor/MudBlazor.min.css"]" rel="stylesheet" />
|
||||||
|
<script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]" defer></script>
|
||||||
|
|
||||||
|
<MudThemeProvider IsDarkMode="true" />
|
||||||
|
|
||||||
|
<MudLayout>
|
||||||
|
<Topbar />
|
||||||
|
<Sidebar />
|
||||||
|
<MudMainContent>
|
||||||
|
@Body
|
||||||
|
</MudMainContent>
|
||||||
|
</MudLayout>
|
||||||
51
src/HopFrame.Web/Components/Pages/HomePage.razor
Normal file
51
src/HopFrame.Web/Components/Pages/HomePage.razor
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
@page "/admin"
|
||||||
|
@using HopFrame.Core.Configuration
|
||||||
|
@using HopFrame.Web.Components.Components
|
||||||
|
@layout HopFrameLayout
|
||||||
|
|
||||||
|
<PageTitle>HopFrame</PageTitle>
|
||||||
|
|
||||||
|
<section style="padding: 1.5rem">
|
||||||
|
<MudText Typo="Typo.h5">Pages</MudText>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<MudStack Wrap="Wrap.Wrap" Row="true">
|
||||||
|
@foreach (var route in Routes) {
|
||||||
|
<Card
|
||||||
|
Title="@route.Name"
|
||||||
|
Href="@route.Route"
|
||||||
|
Icon="@route.Icon"
|
||||||
|
Description="@route.Description"/>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@inject HopFrameConfig Config
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
private readonly struct RouteDefinition {
|
||||||
|
public required string Route { get; init; }
|
||||||
|
public required string Icon { get; init; }
|
||||||
|
public required string Name { get; init; }
|
||||||
|
public string? Description { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private RouteDefinition[] Routes { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override void OnInitialized() {
|
||||||
|
base.OnInitialized();
|
||||||
|
|
||||||
|
Routes = Config.Tables
|
||||||
|
.OrderBy(t => t.OrderIndex)
|
||||||
|
.Select(table => new RouteDefinition {
|
||||||
|
Route = "admin/" + table.Route,
|
||||||
|
Icon = Icons.Material.Filled.TableChart,
|
||||||
|
Name = table.DisplayName,
|
||||||
|
Description = table.Description,
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
13
src/HopFrame.Web/Components/Pages/TablePage.razor
Normal file
13
src/HopFrame.Web/Components/Pages/TablePage.razor
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
@page "/admin/{TableRoute}"
|
||||||
|
@using HopFrame.Web.Components.Components
|
||||||
|
@inherits CancellableComponent
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
@layout HopFrameLayout
|
||||||
|
|
||||||
|
<MudPopoverProvider />
|
||||||
|
<MudDialogProvider />
|
||||||
|
<MudSnackbarProvider/>
|
||||||
|
|
||||||
|
<PageTitle>HopFrame - @Table.DisplayName</PageTitle>
|
||||||
|
|
||||||
|
<Table Config="Table"></Table>
|
||||||
27
src/HopFrame.Web/Components/Pages/TablePage.razor.cs
Normal file
27
src/HopFrame.Web/Components/Pages/TablePage.razor.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using HopFrame.Core.Configuration;
|
||||||
|
using HopFrame.Core.Services;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Components.Pages;
|
||||||
|
|
||||||
|
public partial class TablePage(IConfigAccessor accessor, NavigationManager navigator) : CancellableComponent {
|
||||||
|
private const int PerPage = 25;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string TableRoute { get; set; } = null!;
|
||||||
|
|
||||||
|
public TableConfig Table { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override void OnInitialized() {
|
||||||
|
base.OnInitialized();
|
||||||
|
|
||||||
|
var table = accessor.GetTableByRoute(TableRoute);
|
||||||
|
|
||||||
|
if (table is null) {
|
||||||
|
navigator.NavigateTo("/admin", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Table = table;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/HopFrame.Web/HopFrame.Web.csproj
Normal file
30
src/HopFrame.Web/HopFrame.Web.csproj
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
|
||||||
|
<IsPackable>true</IsPackable>
|
||||||
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
<PackageId>HopFrame.Web</PackageId>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<SupportedPlatform Include="browser"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.0"/>
|
||||||
|
<PackageReference Include="MudBlazor" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\HopFrame.Core\HopFrame.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
43
src/HopFrame.Web/ServiceCollectionExtensions.cs
Normal file
43
src/HopFrame.Web/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using HopFrame.Core;
|
||||||
|
using HopFrame.Core.Configurators;
|
||||||
|
using HopFrame.Web.Components;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using MudBlazor.Services;
|
||||||
|
|
||||||
|
namespace HopFrame.Web;
|
||||||
|
|
||||||
|
/// An extension class to provide access to the setup of the library
|
||||||
|
public static class ServiceCollectionExtensions {
|
||||||
|
|
||||||
|
/// Configures the library using the provided configurator
|
||||||
|
public static IServiceCollection AddHopFrame(this IServiceCollection services, Action<HopFrameConfigurator> configurator) {
|
||||||
|
services.AddHopFrameServices(configurator);
|
||||||
|
|
||||||
|
services.AddMudServices();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the HopFrame admin ui endpoints
|
||||||
|
/// </summary>
|
||||||
|
public static RazorComponentsEndpointConventionBuilder AddHopFrame(this RazorComponentsEndpointConventionBuilder builder) {
|
||||||
|
builder
|
||||||
|
.AddInteractiveServerRenderMode()
|
||||||
|
.AddAdditionalAssemblies(typeof(ServiceCollectionExtensions).Assembly);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the HopFrame admin ui endpoints
|
||||||
|
/// </summary>
|
||||||
|
public static WebApplication MapHopFrame(this WebApplication app) {
|
||||||
|
app.UseAntiforgery();
|
||||||
|
app.MapStaticAssets();
|
||||||
|
app.MapRazorComponents<App>()
|
||||||
|
.AddInteractiveServerRenderMode();
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
8
src/HopFrame.Web/_Imports.razor
Normal file
8
src/HopFrame.Web/_Imports.razor
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using MudBlazor
|
||||||
3
src/HopFrame.Web/wwwroot/hopframe.css
Normal file
3
src/HopFrame.Web/wwwroot/hopframe.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.mud-card-header-avatar {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
@@ -87,8 +87,8 @@ public class ConfigurationHelperTests {
|
|||||||
|
|
||||||
Assert.Equal(typeof(string), config.RepositoryType);
|
Assert.Equal(typeof(string), config.RepositoryType);
|
||||||
Assert.Equal(typeof(TestModel), config.TableType);
|
Assert.Equal(typeof(TestModel), config.TableType);
|
||||||
Assert.Equal("testmodel", config.Route);
|
Assert.Equal("testmodels", config.Route);
|
||||||
Assert.Equal("TestModel", config.DisplayName);
|
Assert.Equal("TestModels", config.DisplayName);
|
||||||
Assert.Equal(0, config.OrderIndex);
|
Assert.Equal(0, config.OrderIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
public class TestModel {
|
public class TestModel {
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; } = null!;
|
||||||
|
|
||||||
public int Method() => 42;
|
public int Method() => 42;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user