Added automatic relation mapping

This commit is contained in:
2025-01-16 15:30:52 +01:00
parent 9d9f0ef7e4
commit c3c69466d4
9 changed files with 71 additions and 41 deletions

View File

@@ -9,17 +9,14 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="Added reload button and animation"> <list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="Added relation picker dialog">
<change afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Dialogs/HopFrameRelationPicker.razor" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/HopFrame.Web/Models/RelationPickerDialogData.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Config/PropertyConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/PropertyConfig.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Config/PropertyConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/PropertyConfig.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Config/TableConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/TableConfig.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Config/TableConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/TableConfig.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Services/IContextExplorer.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/IContextExplorer.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameListView.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameListView.razor.css" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor.css" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Dialogs/HopFrameRelationPicker.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Dialogs/HopFrameRelationPicker.razor" afterDir="false" />
<change beforePath="$PROJECT_DIR$/testing/HopFrame.Testing/DatabaseContext.cs" beforeDir="false" afterPath="$PROJECT_DIR$/testing/HopFrame.Testing/DatabaseContext.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor" afterDir="false" />
<change beforePath="$PROJECT_DIR$/testing/HopFrame.Testing/Models/User.cs" beforeDir="false" afterPath="$PROJECT_DIR$/testing/HopFrame.Testing/Models/User.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/testing/HopFrame.Testing/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/testing/HopFrame.Testing/Program.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/testing/HopFrame.Testing/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/testing/HopFrame.Testing/Program.cs" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
@@ -154,7 +151,7 @@
<workItem from="1736875984621" duration="8464000" /> <workItem from="1736875984621" duration="8464000" />
<workItem from="1736884461354" duration="1075000" /> <workItem from="1736884461354" duration="1075000" />
<workItem from="1736962119221" duration="8119000" /> <workItem from="1736962119221" duration="8119000" />
<workItem from="1737021098746" duration="13969000" /> <workItem from="1737021098746" duration="16566000" />
</task> </task>
<task id="LOCAL-00001" summary="Added basic configuration"> <task id="LOCAL-00001" summary="Added basic configuration">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -204,7 +201,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1737023058093</updated> <updated>1737023058093</updated>
</task> </task>
<option name="localTasksCounter" value="7" /> <task id="LOCAL-00007" summary="Added relation picker dialog">
<option name="closed" value="true" />
<created>1737035288104</created>
<option name="number" value="00007" />
<option name="presentableId" value="LOCAL-00007" />
<option name="project" value="LOCAL" />
<updated>1737035288104</updated>
</task>
<option name="localTasksCounter" value="8" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -220,6 +225,7 @@
<MESSAGE value="Started working on listing page" /> <MESSAGE value="Started working on listing page" />
<MESSAGE value="Added entry saving support" /> <MESSAGE value="Added entry saving support" />
<MESSAGE value="Added reload button and animation" /> <MESSAGE value="Added reload button and animation" />
<option name="LAST_COMMIT_MESSAGE" value="Added reload button and animation" /> <MESSAGE value="Added relation picker dialog" />
<option name="LAST_COMMIT_MESSAGE" value="Added relation picker dialog" />
</component> </component>
</project> </project>

View File

@@ -79,10 +79,4 @@ public class PropertyConfig<TProp>(PropertyConfig config) {
InnerConfig.DisplayValue = display; InnerConfig.DisplayValue = display;
return this; return this;
} }
public PropertyConfig<TProp> IsRelation(bool isRelation) {
InnerConfig.IsRelation = isRelation;
return this;
}
} }

View File

@@ -8,8 +8,10 @@ namespace HopFrame.Core.Config;
public class TableConfig { public class TableConfig {
public Type TableType { get; } public Type TableType { get; }
public string PropertyName { get; } public string PropertyName { get; }
public string DisplayName { get; set; }
public DbContextConfig ContextConfig { get; } public DbContextConfig ContextConfig { get; }
public bool Ignored { get; set; } public bool Ignored { get; set; }
internal bool Seeded { get; set; }
public List<PropertyConfig> Properties { get; } = new(); public List<PropertyConfig> Properties { get; } = new();
@@ -17,6 +19,7 @@ public class TableConfig {
TableType = tableType; TableType = tableType;
PropertyName = propertyName; PropertyName = propertyName;
ContextConfig = config; ContextConfig = config;
DisplayName = PropertyName;
foreach (var info in tableType.GetProperties()) { foreach (var info in tableType.GetProperties()) {
var propConfig = new PropertyConfig(info, this); var propConfig = new PropertyConfig(info, this);
@@ -56,6 +59,11 @@ public class TableConfig<TModel>(TableConfig config) {
return this; return this;
} }
public TableConfig<TModel> SetDisplayName(string name) {
InnerConfig.DisplayName = name;
return this;
}
internal 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.");

View File

@@ -4,7 +4,7 @@ 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 tableDisplayName);
public TableConfig? GetTable(Type tableEntity); public TableConfig? GetTable(Type tableEntity);
public ITableManager? GetTableManager(string tableName); public ITableManager? GetTableManager(string tablePropertyName);
} }

View File

@@ -1,22 +1,25 @@
using HopFrame.Core.Config; using HopFrame.Core.Config;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace HopFrame.Core.Services.Implementations; namespace HopFrame.Core.Services.Implementations;
internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider provider) : IContextExplorer { internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider provider, ILogger<ContextExplorer> logger) : IContextExplorer {
public IEnumerable<string> GetTableNames() { public IEnumerable<string> GetTableNames() {
foreach (var context in config.Contexts) { foreach (var context in config.Contexts) {
foreach (var table in context.Tables) { foreach (var table in context.Tables) {
if (table.Ignored) continue; if (table.Ignored) continue;
yield return table.PropertyName; yield return table.DisplayName;
} }
} }
} }
public TableConfig? GetTable(string tableName) { public TableConfig? GetTable(string tableDisplayName) {
foreach (var context in config.Contexts) { foreach (var context in config.Contexts) {
var table = context.Tables.FirstOrDefault(table => table.PropertyName.Equals(tableName, StringComparison.CurrentCultureIgnoreCase)); var table = context.Tables.FirstOrDefault(table => table.DisplayName.Equals(tableDisplayName, StringComparison.CurrentCultureIgnoreCase));
if (table is not null) if (table is null) continue;
SeedTableData(table);
return table; return table;
} }
@@ -26,16 +29,18 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
public TableConfig? GetTable(Type tableEntity) { public TableConfig? GetTable(Type tableEntity) {
foreach (var context in config.Contexts) { foreach (var context in config.Contexts) {
var table = context.Tables.FirstOrDefault(table => table.TableType == tableEntity); var table = context.Tables.FirstOrDefault(table => table.TableType == tableEntity);
if (table is not null) if (table is null) continue;
SeedTableData(table);
return table; return table;
} }
return null; return null;
} }
public ITableManager? GetTableManager(string tableName) { public ITableManager? GetTableManager(string tablePropertyName) {
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 == tablePropertyName);
if (table is null) continue; if (table is null) continue;
var dbContext = provider.GetService(context.ContextType) as DbContext; var dbContext = provider.GetService(context.ContextType) as DbContext;
@@ -48,4 +53,20 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
return null; return null;
} }
private void SeedTableData(TableConfig table) {
if (table.Seeded) return;
var dbContext = (provider.GetService(table.ContextConfig.ContextType) as DbContext)!;
var entity = dbContext.Model.FindEntityType(table.TableType)!;
foreach (var key in entity.GetForeignKeys()) {
var propConfig = table.Properties
.SingleOrDefault(prop => prop.Info == key.DependentToPrincipal?.PropertyInfo);
if (propConfig is null) continue;
propConfig.IsRelation = true;
}
logger.LogInformation("Extracted information for table '" + table.PropertyName + "'");
table.Seeded = true;
}
} }

View File

@@ -5,7 +5,7 @@
@using HopFrame.Web.Components.Pages @using HopFrame.Web.Components.Pages
<FluentDialogBody Style="overflow-x: auto"> <FluentDialogBody Style="overflow-x: auto">
<HopFrameTablePage DisplayActions="false" DisplaySelection="true" TableName="@Content.SourceTable.PropertyName" PerPage="15" DialogData="Content" /> <HopFrameTablePage DisplayActions="false" DisplaySelection="true" TableDisplayName="@Content.SourceTable.DisplayName" PerPage="15" DialogData="Content" />
</FluentDialogBody> </FluentDialogBody>
@code { @code {

View File

@@ -1,4 +1,4 @@
@page "/admin/{TableName}" @page "/admin/{TableDisplayName}"
@layout HopFrameLayout @layout HopFrameLayout
@rendermode InteractiveServer @rendermode InteractiveServer
@implements IDisposable @implements IDisposable
@@ -13,7 +13,7 @@
<div style="display: flex; flex-direction: column; height: 100%"> <div style="display: flex; flex-direction: column; height: 100%">
<FluentToolbar Class="hopframe-toolbar"> <FluentToolbar Class="hopframe-toolbar">
<h3>@_config?.PropertyName</h3> <h3>@_config?.DisplayName</h3>
<FluentButton <FluentButton
IconStart="@(new Icons.Regular.Size16.ArrowClockwise())" IconStart="@(new Icons.Regular.Size16.ArrowClockwise())"
OnClick="Reload" OnClick="Reload"
@@ -120,7 +120,7 @@
@code { @code {
[Parameter] [Parameter]
public required string TableName { get; set; } public required string TableDisplayName { get; set; }
[Parameter] [Parameter]
public bool DisplaySelection { get; set; } public bool DisplaySelection { get; set; }
@@ -145,9 +145,9 @@
private int _selectedIndex = -1; private int _selectedIndex = -1;
protected override void OnInitialized() { protected override void OnInitialized() {
_config ??= Explorer.GetTable(TableName); _config ??= Explorer.GetTable(TableDisplayName);
if (_config is null) { if (_config is null || (_config.Ignored && DialogData is null)) {
Navigator.NavigateTo("/admin", true); Navigator.NavigateTo("/admin", true);
} }
} }

View File

@@ -16,13 +16,13 @@ public class Post {
[ForeignKey("author")] [ForeignKey("author")]
public User? Author { get; set; } public User? Author { get; set; }
/*public bool Published { get; set; } public bool Published { get; set; }
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public DateOnly Created { get; set; } public DateOnly Created { get; set; }
public TimeOnly At { get; set; }*/ public TimeOnly At { get; set; }
public ListSortDirection Type { get; set; } public ListSortDirection Type { get; set; }
} }

View File

@@ -23,8 +23,7 @@ builder.Services.AddHopFrame(options => {
options.AddDbContext<DatabaseContext>(context => { options.AddDbContext<DatabaseContext>(context => {
context.Table<User>(table => { context.Table<User>(table => {
table.Property(u => u.Password) table.Property(u => u.Password)
.List(false) .ValueParser(pwd => pwd + "-edited");
.DisplayValue(false);
table.Property(u => u.FirstName) table.Property(u => u.FirstName)
.SetDisplayName("First Name"); .SetDisplayName("First Name");
@@ -35,6 +34,8 @@ builder.Services.AddHopFrame(options => {
table.Property(u => u.Id) table.Property(u => u.Id)
.Sortable(false) .Sortable(false)
.ValueTemplate(Guid.CreateVersion7); .ValueTemplate(Guid.CreateVersion7);
table.SetDisplayName("Benutzer");
}); });
context.Table<Post>() context.Table<Post>()
@@ -46,8 +47,8 @@ builder.Services.AddHopFrame(options => {
.SetDisplayName("ID"); .SetDisplayName("ID");
context.Table<Post>() context.Table<Post>()
.Property(p => p.Author) .Property(p => p.CreatedAt)
.IsRelation(true); .ValueTemplate(() => DateTime.UtcNow);
}); });
}); });