Added n -> m relation support
This commit is contained in:
30
.idea/.idea.HopFrame/.idea/workspace.xml
generated
30
.idea/.idea.HopFrame/.idea/workspace.xml
generated
@@ -10,24 +10,22 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="">
|
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="">
|
||||||
<change afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/DefaultAuthHandler.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/DbContextConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/DbContextConfig.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/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/ServiceCollectionExtensions.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/ServiceCollectionExtensions.cs" 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.Core/Services/ITableManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/ITableManager.cs" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Services/ITableManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/ITableManager.cs" 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.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.Core/Services/Implementations/TableManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/TableManager.cs" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/TableManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/TableManager.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.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.Web/Components/Dialogs/HopFrameRelationPicker.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Dialogs/HopFrameRelationPicker.razor" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Layout/HopFrameSideMenu.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Layout/HopFrameSideMenu.razor" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Layout/HopFrameSideMenu.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Layout/HopFrameSideMenu.razor" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameHome.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Pages/HopFrameHome.razor" 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$/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$/src/HopFrame.Web/ServiceCollectionExtensions.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/ServiceCollectionExtensions.cs" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Models/RelationPickerDialogData.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Models/RelationPickerDialogData.cs" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/wwwroot/hopframe.css" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/wwwroot/hopframe.css" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/testing/HopFrame.Testing/Components/Pages/Home.razor" beforeDir="false" afterPath="$PROJECT_DIR$/testing/HopFrame.Testing/Components/Pages/Home.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$/testing/HopFrame.Testing/Models/Post.cs" beforeDir="false" afterPath="$PROJECT_DIR$/testing/HopFrame.Testing/Models/Post.cs" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/testing/HopFrame.Testing/Models/Post.cs" beforeDir="false" afterPath="$PROJECT_DIR$/testing/HopFrame.Testing/Models/Post.cs" 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" />
|
||||||
<change beforePath="$PROJECT_DIR$/testing/HopFrame.Testing/Services/AuthService.cs" beforeDir="false" />
|
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -164,7 +162,8 @@
|
|||||||
<workItem from="1736884461354" duration="1075000" />
|
<workItem from="1736884461354" duration="1075000" />
|
||||||
<workItem from="1736962119221" duration="8119000" />
|
<workItem from="1736962119221" duration="8119000" />
|
||||||
<workItem from="1737021098746" duration="21112000" />
|
<workItem from="1737021098746" duration="21112000" />
|
||||||
<workItem from="1737047730756" duration="7350000" />
|
<workItem from="1737047730756" duration="7678000" />
|
||||||
|
<workItem from="1737120164342" duration="8932000" />
|
||||||
</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" />
|
||||||
@@ -254,7 +253,15 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1737042229086</updated>
|
<updated>1737042229086</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="12" />
|
<task id="LOCAL-00012" summary="Added policy validation, ordering and virtual listing properties">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1737055409534</created>
|
||||||
|
<option name="number" value="00012" />
|
||||||
|
<option name="presentableId" value="LOCAL-00012" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1737055409535</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="13" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -275,6 +282,7 @@
|
|||||||
<MESSAGE value="Added property validation" />
|
<MESSAGE value="Added property validation" />
|
||||||
<MESSAGE value="Added creation/modification confirmation" />
|
<MESSAGE value="Added creation/modification confirmation" />
|
||||||
<MESSAGE value="Removed Template" />
|
<MESSAGE value="Removed Template" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="Removed Template" />
|
<MESSAGE value="Added policy validation, ordering and virtual listing properties" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="Added policy validation, ordering and virtual listing properties" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Linq.Expressions;
|
using System.Collections;
|
||||||
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace HopFrame.Core.Config;
|
namespace HopFrame.Core.Config;
|
||||||
@@ -12,6 +13,7 @@ public class PropertyConfig(PropertyInfo info, TableConfig table, int nthPropert
|
|||||||
public bool Searchable { get; set; } = true;
|
public bool Searchable { get; set; } = true;
|
||||||
public PropertyInfo? DisplayedProperty { get; set; }
|
public PropertyInfo? DisplayedProperty { get; set; }
|
||||||
public Func<object, string>? Formatter { get; set; }
|
public Func<object, string>? Formatter { get; set; }
|
||||||
|
public Func<object, string>? EnumerableFormatter { get; set; }
|
||||||
public Func<string, object>? Parser { get; set; }
|
public Func<string, object>? Parser { get; set; }
|
||||||
public Func<object?, Task<IEnumerable<string>>>? Validator { get; set; }
|
public Func<object?, Task<IEnumerable<string>>>? Validator { get; set; }
|
||||||
public bool Editable { get; set; } = true;
|
public bool Editable { get; set; } = true;
|
||||||
@@ -19,6 +21,7 @@ public class PropertyConfig(PropertyInfo info, TableConfig table, int nthPropert
|
|||||||
public bool DisplayValue { get; set; } = true;
|
public bool DisplayValue { get; set; } = true;
|
||||||
public bool IsRelation { get; set; }
|
public bool IsRelation { get; set; }
|
||||||
public bool IsRequired { get; set; }
|
public bool IsRequired { get; set; }
|
||||||
|
public bool IsEnumerable { get; set; }
|
||||||
public bool IsListingProperty { get; set; }
|
public bool IsListingProperty { get; set; }
|
||||||
public int Order { get; set; } = nthProperty;
|
public int Order { get; set; } = nthProperty;
|
||||||
}
|
}
|
||||||
@@ -57,6 +60,11 @@ public class PropertyConfig<TProp>(PropertyConfig config) {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PropertyConfig<TProp> FormatEach<TInnerProp>(Func<TInnerProp, string> formatter) {
|
||||||
|
InnerConfig.EnumerableFormatter = obj => formatter.Invoke((TInnerProp)obj);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public PropertyConfig<TProp> ValueParser(Func<string, TProp> parser) {
|
public PropertyConfig<TProp> ValueParser(Func<string, TProp> parser) {
|
||||||
InnerConfig.Parser = str => parser.Invoke(str)!;
|
InnerConfig.Parser = str => parser.Invoke(str)!;
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ public interface ITableManager {
|
|||||||
public Task AddItem(object item);
|
public Task AddItem(object item);
|
||||||
public Task RevertChanges(object item);
|
public Task RevertChanges(object item);
|
||||||
|
|
||||||
public string DisplayProperty(object? item, PropertyConfig prop, TableConfig? tableConfig);
|
public string DisplayProperty(object? item, PropertyConfig prop, object? value = null);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using HopFrame.Core.Config;
|
using System.Text.Json;
|
||||||
|
using HopFrame.Core.Config;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -58,12 +59,15 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
|||||||
var dbContext = (provider.GetService(table.ContextConfig.ContextType) as DbContext)!;
|
var dbContext = (provider.GetService(table.ContextConfig.ContextType) as DbContext)!;
|
||||||
var entity = dbContext.Model.FindEntityType(table.TableType)!;
|
var entity = dbContext.Model.FindEntityType(table.TableType)!;
|
||||||
|
|
||||||
foreach (var key in entity.GetForeignKeys()) {
|
foreach (var propertyConfig in table.Properties) {
|
||||||
var propConfig = table.Properties
|
if (propertyConfig.IsListingProperty) continue;
|
||||||
.Where(prop => !prop.IsListingProperty)
|
var prop = entity.FindProperty(propertyConfig.Info.Name);
|
||||||
.SingleOrDefault(prop => prop.Info == key.DependentToPrincipal?.PropertyInfo);
|
if (prop is not null) continue;
|
||||||
if (propConfig is null) continue;
|
var nav = entity.FindNavigation(propertyConfig.Info.Name);
|
||||||
propConfig.IsRelation = true;
|
if (nav is null) continue;
|
||||||
|
propertyConfig.IsRelation = true;
|
||||||
|
propertyConfig.IsRequired = nav.ForeignKey.IsRequired;
|
||||||
|
propertyConfig.IsEnumerable = nav.IsCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var property in entity.GetProperties()) {
|
foreach (var property in entity.GetProperties()) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.Collections;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using HopFrame.Core.Config;
|
using HopFrame.Core.Config;
|
||||||
@@ -67,13 +68,13 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string DisplayProperty(object? item, PropertyConfig prop, TableConfig? tableConfig) {
|
public string DisplayProperty(object? item, PropertyConfig prop, object? value = null) {
|
||||||
if (item is null) return string.Empty;
|
if (item is null) return string.Empty;
|
||||||
|
|
||||||
if (prop.IsListingProperty)
|
if (prop.IsListingProperty)
|
||||||
return prop.Formatter!.Invoke(item);
|
return prop.Formatter!.Invoke(item);
|
||||||
|
|
||||||
var propValue = prop.Info.GetValue(item);
|
var propValue = value ?? prop.Info.GetValue(item);
|
||||||
if (propValue is null)
|
if (propValue is null)
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|
||||||
@@ -81,6 +82,18 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
|||||||
return prop.Formatter.Invoke(propValue);
|
return prop.Formatter.Invoke(propValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prop.IsEnumerable) {
|
||||||
|
if (value is not null) {
|
||||||
|
if (prop.EnumerableFormatter is not null) {
|
||||||
|
return prop.EnumerableFormatter.Invoke(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (propValue as IEnumerable)!.OfType<object>().Count().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
if (prop.DisplayedProperty is null) {
|
if (prop.DisplayedProperty is null) {
|
||||||
var key = prop.Info.PropertyType
|
var key = prop.Info.PropertyType
|
||||||
.GetProperties()
|
.GetProperties()
|
||||||
@@ -94,19 +107,13 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
|||||||
.SingleOrDefault(p => p.Info == prop.DisplayedProperty && !p.IsListingProperty);
|
.SingleOrDefault(p => p.Info == prop.DisplayedProperty && !p.IsListingProperty);
|
||||||
|
|
||||||
if (innerProp is null) return propValue.ToString() ?? string.Empty;
|
if (innerProp is null) return propValue.ToString() ?? string.Empty;
|
||||||
return DisplayProperty(propValue, innerProp, innerConfig);
|
return DisplayProperty(propValue, innerProp);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IQueryable<TModel> IncludeForgeinKeys(IQueryable<TModel> query) {
|
private IQueryable<TModel> IncludeForgeinKeys(IQueryable<TModel> query) {
|
||||||
var pendingQuery = query;
|
var pendingQuery = query;
|
||||||
|
|
||||||
foreach (var property in config.Properties) {
|
foreach (var property in config.Properties.Where(prop => prop.IsRelation)) {
|
||||||
var attr = property.Info
|
|
||||||
.GetCustomAttributes(true)
|
|
||||||
.FirstOrDefault(att => att is ForeignKeyAttribute) as ForeignKeyAttribute;
|
|
||||||
|
|
||||||
if (attr is null) continue;
|
|
||||||
|
|
||||||
pendingQuery = pendingQuery.Include(property.Info.Name);
|
pendingQuery = pendingQuery.Include(property.Info.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
@implements IDialogContentComponent<EditorDialogData>
|
@implements IDialogContentComponent<EditorDialogData>
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
@using System.Collections
|
||||||
@using HopFrame.Core.Config
|
@using HopFrame.Core.Config
|
||||||
@using HopFrame.Core.Services
|
@using HopFrame.Core.Services
|
||||||
@using HopFrame.Web.Models
|
@using HopFrame.Web.Models
|
||||||
@using HopFrame.Web.Helpers
|
@using HopFrame.Web.Helpers
|
||||||
@using Microsoft.EntityFrameworkCore.Internal
|
|
||||||
|
|
||||||
<FluentDialogBody>
|
<FluentDialogBody>
|
||||||
@foreach (var property in Content.Config.Properties.Where(prop => !prop.IsListingProperty).OrderBy(prop => prop.Order)) {
|
@foreach (var property in Content.Config.Properties.Where(prop => !prop.IsListingProperty).OrderBy(prop => prop.Order)) {
|
||||||
@@ -15,18 +15,31 @@
|
|||||||
@if (property.IsRelation) {
|
@if (property.IsRelation) {
|
||||||
<div style="display: flex; gap: 5px; align-items: flex-end">
|
<div style="display: flex; gap: 5px; align-items: flex-end">
|
||||||
<div style="flex-grow: 1">
|
<div style="flex-grow: 1">
|
||||||
|
@if (property.IsEnumerable) {
|
||||||
|
<FluentLabel Style="margin-bottom: calc(var(--design-unit) * 1px)">@property.Name</FluentLabel>
|
||||||
|
<div class="hopframe-listview">
|
||||||
|
<FluentOverflow Style="width: 100%">
|
||||||
|
@foreach (var item in GetPropertyValue<IEnumerable>(property) ?? Enumerable.Empty<object>()) {
|
||||||
|
<FluentOverflowItem><FluentBadge>@(GetPropertyValue<string>(property, item))</FluentBadge></FluentOverflowItem>
|
||||||
|
}
|
||||||
|
</FluentOverflow>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else {
|
||||||
<FluentTextField
|
<FluentTextField
|
||||||
Label="@property.Name"
|
Label="@property.Name"
|
||||||
Value="@(GetPropertyValue<string>(property))"
|
Value="@(GetPropertyValue<string>(property))"
|
||||||
Required="@property.IsRequired"
|
Required="@property.IsRequired"
|
||||||
ReadOnly="true"
|
ReadOnly="true"
|
||||||
Style="width: 100%"
|
Style="width: 100%" />
|
||||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Text))" />
|
}
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 5px; margin-bottom: 4px">
|
<div style="display: flex; gap: 5px; margin-bottom: 4px">
|
||||||
|
@if (!property.IsRequired) {
|
||||||
<FluentButton OnClick="async () => await SetPropertyValue(property, null, InputType.Relation)" Disabled="@(!property.Editable)">
|
<FluentButton OnClick="async () => await SetPropertyValue(property, null, InputType.Relation)" Disabled="@(!property.Editable)">
|
||||||
<FluentIcon Value="@(new Icons.Regular.Size20.Dismiss())" Color="Color.Neutral" />
|
<FluentIcon Value="@(new Icons.Regular.Size20.Dismiss())" Color="Color.Neutral" />
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
|
}
|
||||||
<FluentButton OnClick="async () => await OpenRelationalPicker(property)" Disabled="@(!property.Editable)">
|
<FluentButton OnClick="async () => await OpenRelationalPicker(property)" Disabled="@(!property.Editable)">
|
||||||
<FluentIcon Value="@(new Icons.Regular.Size20.Open())" Color="Color.Neutral" />
|
<FluentIcon Value="@(new Icons.Regular.Size20.Open())" Color="Color.Neutral" />
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
@@ -168,19 +181,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private TValue? GetPropertyValue<TValue>(PropertyConfig config) {
|
private TValue? GetPropertyValue<TValue>(PropertyConfig config, object? listItem = null) {
|
||||||
if (!config.DisplayValue) return default;
|
if (!config.DisplayValue) return default;
|
||||||
if (Content.CurrentObject is null) return default;
|
if (Content.CurrentObject is null) return default;
|
||||||
|
|
||||||
|
if (listItem is not null) {
|
||||||
|
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config, listItem);
|
||||||
|
}
|
||||||
|
|
||||||
var value = config.Info.GetValue(Content.CurrentObject);
|
var value = config.Info.GetValue(Content.CurrentObject);
|
||||||
|
|
||||||
if (value is null)
|
if (value is null)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
if (config.Info.PropertyType == typeof(TValue))
|
if (typeof(TValue).IsAssignableFrom(config.Info.PropertyType))
|
||||||
return (TValue)value;
|
return (TValue)value;
|
||||||
|
|
||||||
if (typeof(TValue) == typeof(string))
|
if (typeof(TValue) == typeof(string))
|
||||||
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config, Content.Config);
|
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config);
|
||||||
|
|
||||||
return (TValue)Convert.ChangeType(value, typeof(TValue));
|
return (TValue)Convert.ChangeType(value, typeof(TValue));
|
||||||
}
|
}
|
||||||
@@ -190,6 +208,7 @@
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
object? result = null;
|
object? result = null;
|
||||||
|
var needsOverride = true;
|
||||||
|
|
||||||
if (value is not null && config.Parser is null) {
|
if (value is not null && config.Parser is null) {
|
||||||
switch (senderType) {
|
switch (senderType) {
|
||||||
@@ -231,7 +250,16 @@
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case InputType.Relation:
|
case InputType.Relation:
|
||||||
result = value;
|
if (!config.IsEnumerable)
|
||||||
|
result = ((IEnumerable)value).OfType<object>().FirstOrDefault();
|
||||||
|
else {
|
||||||
|
needsOverride = false;
|
||||||
|
var asList = (IList)config.Info.GetValue(Content.CurrentObject)!;
|
||||||
|
asList.Clear();
|
||||||
|
foreach (var element in (IEnumerable)value) {
|
||||||
|
asList.Add(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -243,6 +271,7 @@
|
|||||||
result = config.Parser(result.ToString()!);
|
result = config.Parser(result.ToString()!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (needsOverride)
|
||||||
config.Info.SetValue(Content.CurrentObject, result);
|
config.Info.SetValue(Content.CurrentObject, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,16 +279,32 @@
|
|||||||
if (!await Handler.IsAuthenticatedAsync(_currentlyEditing ? Content.Config.UpdatePolicy : Content.Config.CreatePolicy))
|
if (!await Handler.IsAuthenticatedAsync(_currentlyEditing ? Content.Config.UpdatePolicy : Content.Config.CreatePolicy))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var relationTable = Explorer.GetTable(config.Info.PropertyType);
|
var relationType = config.Info.PropertyType;
|
||||||
|
if (config.IsEnumerable) {
|
||||||
|
relationType = config.Info.PropertyType.GetGenericArguments().First();
|
||||||
|
}
|
||||||
|
|
||||||
|
var relationTable = Explorer.GetTable(relationType);
|
||||||
if (relationTable is null) return;
|
if (relationTable is null) return;
|
||||||
|
|
||||||
var currentValue = config.Info.GetValue(Content.CurrentObject);
|
var currentValues = new List<object>();
|
||||||
var dialog = await Dialogs.ShowDialogAsync<HopFrameRelationPicker>(new RelationPickerDialogData(relationTable, currentValue), new DialogParameters());
|
if (config.IsEnumerable) {
|
||||||
|
foreach (var o in GetPropertyValue<IEnumerable>(config) ?? Enumerable.Empty<object>()) {
|
||||||
|
currentValues.Add(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var raw = config.Info.GetValue(Content.CurrentObject);
|
||||||
|
if (raw is not null)
|
||||||
|
currentValues.Add(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dialog = await Dialogs.ShowDialogAsync<HopFrameRelationPicker>(new RelationPickerDialogData(relationTable, currentValues, config.IsEnumerable), new DialogParameters());
|
||||||
var result = await dialog.Result;
|
var result = await dialog.Result;
|
||||||
if (result.Cancelled) return;
|
if (result.Cancelled) return;
|
||||||
|
|
||||||
var data = (RelationPickerDialogData)result.Data!;
|
var data = (RelationPickerDialogData)result.Data!;
|
||||||
await SetPropertyValue(config, data.Object, InputType.Relation);
|
await SetPropertyValue(config, data.SelectedObjects, InputType.Relation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> ValidateInputs() {
|
private async Task<bool> ValidateInputs() {
|
||||||
|
|||||||
@@ -5,7 +5,13 @@
|
|||||||
@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" TableDisplayName="@Content.SourceTable.DisplayName" PerPage="15" DialogData="Content" />
|
<HopFrameTablePage
|
||||||
|
DisplayActions="false"
|
||||||
|
DisplaySelection="true"
|
||||||
|
TableDisplayName="@Content.SourceTable.DisplayName"
|
||||||
|
PerPage="15"
|
||||||
|
DialogData="Content"
|
||||||
|
SelectionMode="@(Content.AllowMultiple ? DataGridSelectMode.Multiple : DataGridSelectMode.Single)"/>
|
||||||
</FluentDialogBody>
|
</FluentDialogBody>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@using HopFrame.Core.Config
|
@using HopFrame.Core.Config
|
||||||
@using HopFrame.Core.Services
|
@using HopFrame.Core.Services
|
||||||
|
|
||||||
<FluentAppBar Orientation="Orientation.Vertical" Style="background-color: var(--neutral-layer-2); height: auto">
|
<FluentAppBar Orientation="Orientation.Vertical" PopoverShowSearch="false" Style="background-color: var(--neutral-layer-2); height: auto">
|
||||||
<FluentAppBarItem Href="/admin"
|
<FluentAppBarItem Href="/admin"
|
||||||
Match="NavLinkMatch.All"
|
Match="NavLinkMatch.All"
|
||||||
IconActive="new Icons.Filled.Size24.Grid()"
|
IconActive="new Icons.Filled.Size24.Grid()"
|
||||||
|
|||||||
@@ -31,31 +31,29 @@
|
|||||||
<FluentSpacer />
|
<FluentSpacer />
|
||||||
<FluentSearch @oninput="OnSearch" @onchange="OnSearch" Style="width: 350px" />
|
<FluentSearch @oninput="OnSearch" @onchange="OnSearch" Style="width: 350px" />
|
||||||
|
|
||||||
@if (!DisplaySelection && _hasCreatePolicy) {
|
@if (_hasCreatePolicy && DisplayActions) {
|
||||||
<FluentButton OnClick="async () => { await CreateOrEdit(null); }">Add Entry</FluentButton>
|
<FluentButton OnClick="async () => { await CreateOrEdit(null); }">Add Entry</FluentButton>
|
||||||
}
|
}
|
||||||
</FluentToolbar>
|
</FluentToolbar>
|
||||||
<FluentProgress Visible="_loading" Width="100%" />
|
<FluentProgress Visible="_loading" Width="100%" />
|
||||||
|
|
||||||
<div style="display: flex; overflow-y: auto; flex-grow: 1">
|
<div style="display: flex; overflow-y: auto; flex-grow: 1">
|
||||||
@if (DisplaySelection) {
|
|
||||||
<div style="margin-top: calc(44px - (var(--stroke-width) * 1px)); border-top: calc(var(--stroke-width)* 1px) solid var(--neutral-stroke-divider-rest);">
|
|
||||||
@foreach (var model in _currentlyDisplayedModels) {
|
|
||||||
<div class="hopframe-radio">
|
|
||||||
<FluentRadioGroup TValue="int" Value="@(DialogData!.Object == model ? 1 : 0)">
|
|
||||||
<FluentRadio Value="0" Style="display: none" />
|
|
||||||
<FluentRadio Value="1" @onclick="() => DialogData!.Object = model" Style="width: 20px"/>
|
|
||||||
</FluentRadioGroup>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div style="flex-grow: 1">
|
<div style="flex-grow: 1">
|
||||||
<FluentDataGrid Items="_currentlyDisplayedModels.AsQueryable()">
|
<FluentDataGrid Items="_currentlyDisplayedModels.AsQueryable()">
|
||||||
|
@if (DisplaySelection) {
|
||||||
|
<SelectColumn
|
||||||
|
TGridItem="object"
|
||||||
|
SelectMode="SelectionMode"
|
||||||
|
SelectFromEntireRow="true"
|
||||||
|
SelectedItems="DialogData?.SelectedObjects.ToArray()"
|
||||||
|
OnSelect="data => SelectItem(data.Item, data.Selected)"
|
||||||
|
SelectAllChanged="SelectAll"
|
||||||
|
Style="min-width: max-content; height: 44px; display: grid; align-items: center" @ref="_selectColumn" />
|
||||||
|
}
|
||||||
|
|
||||||
@foreach (var property in _config!.Properties.Where(prop => prop.List).OrderBy(prop => prop.Order)) {
|
@foreach (var property in _config!.Properties.Where(prop => prop.List).OrderBy(prop => prop.Order)) {
|
||||||
<PropertyColumn
|
<PropertyColumn
|
||||||
Title="@property.Name" Property="o => _manager!.DisplayProperty(o, property, _config)"
|
Title="@property.Name" Property="o => _manager!.DisplayProperty(o, property, null)"
|
||||||
Style="min-width: max-content; height: 44px;"
|
Style="min-width: max-content; height: 44px;"
|
||||||
Sortable="@property.Sortable"/>
|
Sortable="@property.Sortable"/>
|
||||||
}
|
}
|
||||||
@@ -68,13 +66,13 @@
|
|||||||
|
|
||||||
@if (_hasUpdatePolicy) {
|
@if (_hasUpdatePolicy) {
|
||||||
<FluentButton aria-label="Edit entry" OnClick="async () => { await CreateOrEdit(currentElement); }">
|
<FluentButton aria-label="Edit entry" OnClick="async () => { await CreateOrEdit(currentElement); }">
|
||||||
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())" />
|
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/>
|
||||||
</FluentButton>
|
</FluentButton>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (_hasDeletePolicy) {
|
@if (_hasDeletePolicy) {
|
||||||
<FluentButton aria-label="Delete entry" OnClick="async () => { await 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>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,13 +141,16 @@
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public RelationPickerDialogData? DialogData { get; set; }
|
public RelationPickerDialogData? DialogData { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public DataGridSelectMode SelectionMode { get; set; } = DataGridSelectMode.Single;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public int PerPage { get; set; } = 20;
|
public int PerPage { get; set; } = 20;
|
||||||
|
|
||||||
private TableConfig? _config;
|
private TableConfig? _config;
|
||||||
private ITableManager? _manager;
|
private ITableManager? _manager;
|
||||||
|
|
||||||
private IEnumerable<object> _currentlyDisplayedModels = [];
|
private object[] _currentlyDisplayedModels = [];
|
||||||
private int _currentPage;
|
private int _currentPage;
|
||||||
private int _totalPages;
|
private int _totalPages;
|
||||||
private string? _searchTerm;
|
private string? _searchTerm;
|
||||||
@@ -159,6 +160,8 @@
|
|||||||
private bool _hasDeletePolicy;
|
private bool _hasDeletePolicy;
|
||||||
private bool _hasCreatePolicy;
|
private bool _hasCreatePolicy;
|
||||||
|
|
||||||
|
private SelectColumn<object>? _selectColumn;
|
||||||
|
|
||||||
protected override void OnInitialized() {
|
protected override void OnInitialized() {
|
||||||
_config ??= Explorer.GetTable(TableDisplayName);
|
_config ??= Explorer.GetTable(TableDisplayName);
|
||||||
|
|
||||||
@@ -263,4 +266,19 @@
|
|||||||
|
|
||||||
await Reload();
|
await Reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SelectItem(object item, bool selected) {
|
||||||
|
if (!selected)
|
||||||
|
DialogData?.SelectedObjects.Remove(item);
|
||||||
|
else DialogData?.SelectedObjects.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectAll(bool? selected) {
|
||||||
|
selected = _currentlyDisplayedModels.Any(obj => DialogData?.SelectedObjects.Contains(obj) != true);
|
||||||
|
foreach (var displayedModel in _currentlyDisplayedModels) {
|
||||||
|
SelectItem(displayedModel, selected == true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectColumn!.SelectAll = selected;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
namespace HopFrame.Web.Models;
|
namespace HopFrame.Web.Models;
|
||||||
|
|
||||||
public sealed class RelationPickerDialogData(TableConfig sourceTable, object? current) {
|
public sealed class RelationPickerDialogData(TableConfig sourceTable, List<object> current, bool multiple) {
|
||||||
public object? Object { get; set; } = current;
|
public List<object> SelectedObjects { get; set; } = current;
|
||||||
public TableConfig SourceTable { get; init; } = sourceTable;
|
public TableConfig SourceTable { get; init; } = sourceTable;
|
||||||
|
public bool AllowMultiple { get; set; } = multiple;
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,18 @@
|
|||||||
padding: 0.5rem 1.5rem;
|
padding: 0.5rem 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hopframe-listview {
|
||||||
|
background: padding-box linear-gradient(var(--neutral-fill-input-rest), var(--neutral-fill-input-rest)), border-box var(--neutral-stroke-input-rest);
|
||||||
|
border: calc(var(--stroke-width) * 1px) solid transparent;
|
||||||
|
border-radius: calc(var(--control-corner-radius) * 1px);
|
||||||
|
padding: 0 calc(var(--design-unit) * 2px + 1px);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
.hopframe-content .empty-content-row.empty-content-cell {
|
.hopframe-content .empty-content-row.empty-content-cell {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@using HopFrame.Testing.Models
|
@using HopFrame.Testing.Models
|
||||||
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
|
||||||
<PageTitle>Home</PageTitle>
|
<PageTitle>Home</PageTitle>
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbCont
|
|||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity<Post>()
|
modelBuilder.Entity<Post>()
|
||||||
.HasOne<User>()
|
.HasOne<User>(p => p.Author)
|
||||||
.WithMany()
|
.WithMany(u => u.Posts)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ public class Post {
|
|||||||
[MaxLength(255)]
|
[MaxLength(255)]
|
||||||
public required string Caption { get; set; }
|
public required string Caption { get; set; }
|
||||||
|
|
||||||
public required string Content { get; set; }
|
public required string? Content { get; set; }
|
||||||
|
|
||||||
[ForeignKey("author")]
|
[ForeignKey("author")]
|
||||||
public virtual required User Author { get; set; }
|
public virtual required User Author { get; set; }
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ public class User {
|
|||||||
public string? FirstName { get; set; }
|
public string? FirstName { get; set; }
|
||||||
public string? LastName { get; set; }
|
public string? LastName { get; set; }
|
||||||
|
|
||||||
|
public virtual List<Post> Posts { get; set; } = new();
|
||||||
|
|
||||||
public override string ToString() {
|
public override string ToString() {
|
||||||
return Username;
|
return Username;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections;
|
||||||
using HopFrame.Testing;
|
using HopFrame.Testing;
|
||||||
using Microsoft.FluentUI.AspNetCore.Components;
|
using Microsoft.FluentUI.AspNetCore.Components;
|
||||||
using HopFrame.Testing.Components;
|
using HopFrame.Testing.Components;
|
||||||
@@ -41,6 +42,9 @@ builder.Services.AddHopFrame(options => {
|
|||||||
table.SetDescription("This table is used for user data store and user authentication");
|
table.SetDescription("This table is used for user data store and user authentication");
|
||||||
|
|
||||||
table.SetViewPolicy("policy");
|
table.SetViewPolicy("policy");
|
||||||
|
|
||||||
|
table.Property(u => u.Posts)
|
||||||
|
.FormatEach<Post>(post => post.Caption);
|
||||||
});
|
});
|
||||||
|
|
||||||
context.Table<Post>()
|
context.Table<Post>()
|
||||||
@@ -56,7 +60,7 @@ builder.Services.AddHopFrame(options => {
|
|||||||
|
|
||||||
context.Table<Post>()
|
context.Table<Post>()
|
||||||
.Property(p => p.Caption)
|
.Property(p => p.Caption)
|
||||||
.Validator(input => {
|
/*.Validator(input => {
|
||||||
var errors = new List<string>();
|
var errors = new List<string>();
|
||||||
|
|
||||||
if (input is null)
|
if (input is null)
|
||||||
@@ -66,7 +70,7 @@ builder.Services.AddHopFrame(options => {
|
|||||||
errors.Add("Value can only be 10 characters long");
|
errors.Add("Value can only be 10 characters long");
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
});
|
})*/;
|
||||||
|
|
||||||
context.Table<Post>()
|
context.Table<Post>()
|
||||||
.OrderIndex(-1);
|
.OrderIndex(-1);
|
||||||
|
|||||||
Reference in New Issue
Block a user