Added automatic relation mapping
This commit is contained in:
28
.idea/.idea.HopFrame/.idea/workspace.xml
generated
28
.idea/.idea.HopFrame/.idea/workspace.xml
generated
@@ -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>
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -55,6 +58,11 @@ public class TableConfig<TModel>(TableConfig config) {
|
|||||||
configurator.Invoke(prop);
|
configurator.Invoke(prop);
|
||||||
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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,26 @@
|
|||||||
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;
|
||||||
return table;
|
|
||||||
|
SeedTableData(table);
|
||||||
|
return table;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -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;
|
||||||
return table;
|
|
||||||
|
SeedTableData(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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user