Added policy validation, ordering and virtual listing properties
This commit is contained in:
68
.idea/.idea.HopFrame/.idea/workspace.xml
generated
68
.idea/.idea.HopFrame/.idea/workspace.xml
generated
@@ -9,13 +9,25 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="Added creation/modification confirmation">
|
||||
<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$/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/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/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.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/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/ServiceCollectionExtensions.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/ServiceCollectionExtensions.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/Services/AuthService.cs" beforeDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -58,8 +70,10 @@
|
||||
}</component>
|
||||
<component name="HighlightingSettingsPerFile">
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/26c9a2fb5243863babc926e4be763daf4128d4f97c4a769cdce1e2e3e5c532/FluentButton.razor.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/2751d5afefca5424bfc4b21347f581372f7a739c0ae4df661ea557fcb97ef20/EnumExtensions.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/60e7b22380df80ef6fefe43138047f49ec6eff4b25c12b42ce3d6ed5aac/MethodInvokerCommon.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/87c584767b46b5fd42769be76547105558e6690f785614efddca134b2d682/Type.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/adcd2c45092dd8e4fc412325c8adb75d6e7d8b3e90a9523f167583fb9c60/ServiceCollectionExtensions.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/b3ccb66df3646cb51df73ad51716136ebd2eefb4edb1308dd52a7e999582d59e/IBindableColumn.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/d858ddb35a8e36df5573b7612542f9ad50f426b8ab43818587d1ac65fab14829/DatabaseGeneratedAttribute.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/ece8533187fe96ce67b3ef1c9cc3502ef8da5510aadb132a9b21c5605d7c2119/PropertyColumn.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
@@ -78,24 +92,24 @@
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
".NET Launch Settings Profile.HopFrame.Testing.executor": "Run",
|
||||
".NET Launch Settings Profile.HopFrame.Testing: https.executor": "Run",
|
||||
".NET Project.HopFrame.Testing.executor": "Run",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"git-widget-placeholder": "feature/setup",
|
||||
"list.type.of.created.stylesheet": "CSS",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "preferences.environmentSetup",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
".NET Launch Settings Profile.HopFrame.Testing.executor": "Run",
|
||||
".NET Launch Settings Profile.HopFrame.Testing: https.executor": "Run",
|
||||
".NET Project.HopFrame.Testing.executor": "Run",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"git-widget-placeholder": "feature/setup",
|
||||
"list.type.of.created.stylesheet": "CSS",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "preferences.environmentSetup",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}</component>
|
||||
}]]></component>
|
||||
<component name="RunManager" selected=".NET Launch Settings Profile.HopFrame.Testing: https">
|
||||
<configuration name="HopFrame.Testing: http" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing/HopFrame.Testing.csproj" />
|
||||
@@ -149,7 +163,8 @@
|
||||
<workItem from="1736875984621" duration="8464000" />
|
||||
<workItem from="1736884461354" duration="1075000" />
|
||||
<workItem from="1736962119221" duration="8119000" />
|
||||
<workItem from="1737021098746" duration="21001000" />
|
||||
<workItem from="1737021098746" duration="21112000" />
|
||||
<workItem from="1737047730756" duration="7350000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="Added basic configuration">
|
||||
<option name="closed" value="true" />
|
||||
@@ -231,7 +246,15 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737040946489</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="11" />
|
||||
<task id="LOCAL-00011" summary="Removed Template">
|
||||
<option name="closed" value="true" />
|
||||
<created>1737042229086</created>
|
||||
<option name="number" value="00011" />
|
||||
<option name="presentableId" value="LOCAL-00011" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1737042229086</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="12" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -251,6 +274,7 @@
|
||||
<MESSAGE value="Added automatic relation mapping" />
|
||||
<MESSAGE value="Added property validation" />
|
||||
<MESSAGE value="Added creation/modification confirmation" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Added creation/modification confirmation" />
|
||||
<MESSAGE value="Removed Template" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Removed Template" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -4,7 +4,7 @@ namespace HopFrame.Core.Config;
|
||||
|
||||
public class DbContextConfig {
|
||||
public Type ContextType { get; }
|
||||
public List<TableConfig> Tables { get; init; } = new();
|
||||
public List<TableConfig> Tables { get; } = new();
|
||||
|
||||
public DbContextConfig(Type context) {
|
||||
ContextType = context;
|
||||
@@ -15,7 +15,7 @@ public class DbContextConfig {
|
||||
var setType = typeof(DbSet<>).MakeGenericType(innerType);
|
||||
if (property.PropertyType != setType) continue;
|
||||
|
||||
var table = new TableConfig(this, innerType, property.Name);
|
||||
var table = new TableConfig(this, innerType, property.Name, Tables.Count);
|
||||
Tables.Add(table);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Reflection;
|
||||
|
||||
namespace HopFrame.Core.Config;
|
||||
|
||||
public class PropertyConfig(PropertyInfo info, TableConfig table) {
|
||||
public class PropertyConfig(PropertyInfo info, TableConfig table, int nthProperty) {
|
||||
public PropertyInfo Info { get; } = info;
|
||||
public TableConfig Table { get; } = table;
|
||||
public string Name { get; set; } = info.Name;
|
||||
@@ -19,6 +19,8 @@ public class PropertyConfig(PropertyInfo info, TableConfig table) {
|
||||
public bool DisplayValue { get; set; } = true;
|
||||
public bool IsRelation { get; set; }
|
||||
public bool IsRequired { get; set; }
|
||||
public bool IsListingProperty { get; set; }
|
||||
public int Order { get; set; } = nthProperty;
|
||||
}
|
||||
|
||||
public class PropertyConfig<TProp>(PropertyConfig config) {
|
||||
@@ -84,4 +86,9 @@ public class PropertyConfig<TProp>(PropertyConfig config) {
|
||||
InnerConfig.Validator = obj => validator.Invoke((TProp?)obj);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PropertyConfig<TProp> OrderIndex(int index) {
|
||||
InnerConfig.Order = index;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,20 +9,30 @@ public class TableConfig {
|
||||
public Type TableType { get; }
|
||||
public string PropertyName { get; }
|
||||
public string DisplayName { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public DbContextConfig ContextConfig { get; }
|
||||
public bool Ignored { get; set; }
|
||||
public int Order { get; set; }
|
||||
internal bool Seeded { get; set; }
|
||||
|
||||
public string? ViewPolicy { get; set; }
|
||||
public string? CreatePolicy { get; set; }
|
||||
public string? UpdatePolicy { get; set; }
|
||||
public string? DeletePolicy { get; set; }
|
||||
|
||||
public List<PropertyConfig> Properties { get; } = new();
|
||||
|
||||
public TableConfig(DbContextConfig config, Type tableType, string propertyName) {
|
||||
public TableConfig(DbContextConfig config, Type tableType, string propertyName, int nthTable) {
|
||||
TableType = tableType;
|
||||
PropertyName = propertyName;
|
||||
ContextConfig = config;
|
||||
DisplayName = PropertyName;
|
||||
Order = nthTable;
|
||||
|
||||
foreach (var info in tableType.GetProperties()) {
|
||||
var propConfig = new PropertyConfig(info, this);
|
||||
var properties = tableType.GetProperties();
|
||||
for (var i = 0; i < properties.Length; i++) {
|
||||
var info = properties[i];
|
||||
var propConfig = new PropertyConfig(info, this, i);
|
||||
|
||||
if (info.GetCustomAttributes(true).Any(a => a is DatabaseGeneratedAttribute)) {
|
||||
propConfig.Creatable = false;
|
||||
@@ -59,11 +69,56 @@ public class TableConfig<TModel>(TableConfig config) {
|
||||
return this;
|
||||
}
|
||||
|
||||
public PropertyConfig<string> AddListingProperty(string name, Func<TModel, string> template) {
|
||||
var prop = new PropertyConfig(InnerConfig.Properties.First().Info, InnerConfig, InnerConfig.Properties.Count);
|
||||
prop.Name = name;
|
||||
prop.IsListingProperty = true;
|
||||
prop.Formatter = obj => template.Invoke((TModel)obj);
|
||||
InnerConfig.Properties.Add(prop);
|
||||
return new PropertyConfig<string>(prop);
|
||||
}
|
||||
|
||||
public TableConfig<TModel> AddListingProperty(string name, Func<TModel, string> template, Action<PropertyConfig<string>> configurator) {
|
||||
var prop = AddListingProperty(name, template);
|
||||
configurator.Invoke(prop);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TableConfig<TModel> SetDisplayName(string name) {
|
||||
InnerConfig.DisplayName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TableConfig<TModel> SetDescription(string description) {
|
||||
InnerConfig.Description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TableConfig<TModel> OrderIndex(int index) {
|
||||
InnerConfig.Order = index;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TableConfig<TModel> SetViewPolicy(string policy) {
|
||||
InnerConfig.ViewPolicy = policy;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TableConfig<TModel> SetUpdatePolicy(string policy) {
|
||||
InnerConfig.UpdatePolicy = policy;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TableConfig<TModel> SetCreatePolicy(string policy) {
|
||||
InnerConfig.CreatePolicy = policy;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TableConfig<TModel> SetDeletePolicy(string policy) {
|
||||
InnerConfig.DeletePolicy = policy;
|
||||
return this;
|
||||
}
|
||||
|
||||
internal static PropertyInfo GetPropertyInfo<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda) {
|
||||
if (propertyLambda.Body is not MemberExpression member) {
|
||||
throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property.");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using HopFrame.Core.Services;
|
||||
using HopFrame.Core.Services.Implementations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace HopFrame.Core;
|
||||
|
||||
@@ -8,6 +9,7 @@ public static class ServiceCollectionExtensions {
|
||||
|
||||
public static IServiceCollection AddHopFrameServices(this IServiceCollection services) {
|
||||
services.AddTransient<IContextExplorer, ContextExplorer>();
|
||||
services.TryAddTransient<IHopFrameAuthHandler, DefaultAuthHandler>();
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace HopFrame.Core.Services;
|
||||
|
||||
public interface IContextExplorer {
|
||||
public IEnumerable<string> GetTableNames();
|
||||
public IEnumerable<TableConfig> GetTables();
|
||||
public TableConfig? GetTable(string tableDisplayName);
|
||||
public TableConfig? GetTable(Type tableEntity);
|
||||
public ITableManager? GetTableManager(string tablePropertyName);
|
||||
|
||||
@@ -12,5 +12,5 @@ public interface ITableManager {
|
||||
public Task AddItem(object item);
|
||||
public Task RevertChanges(object item);
|
||||
|
||||
public string DisplayProperty(object? item, PropertyInfo info, TableConfig? tableConfig);
|
||||
public string DisplayProperty(object? item, PropertyConfig prop, TableConfig? tableConfig);
|
||||
}
|
||||
@@ -5,11 +5,11 @@ using Microsoft.Extensions.Logging;
|
||||
namespace HopFrame.Core.Services.Implementations;
|
||||
|
||||
internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider provider, ILogger<ContextExplorer> logger) : IContextExplorer {
|
||||
public IEnumerable<string> GetTableNames() {
|
||||
public IEnumerable<TableConfig> GetTables() {
|
||||
foreach (var context in config.Contexts) {
|
||||
foreach (var table in context.Tables) {
|
||||
if (table.Ignored) continue;
|
||||
yield return table.DisplayName;
|
||||
yield return table;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
||||
|
||||
foreach (var key in entity.GetForeignKeys()) {
|
||||
var propConfig = table.Properties
|
||||
.Where(prop => !prop.IsListingProperty)
|
||||
.SingleOrDefault(prop => prop.Info == key.DependentToPrincipal?.PropertyInfo);
|
||||
if (propConfig is null) continue;
|
||||
propConfig.IsRelation = true;
|
||||
@@ -67,6 +68,7 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
|
||||
|
||||
foreach (var property in entity.GetProperties()) {
|
||||
var propConfig = table.Properties
|
||||
.Where(prop => !prop.IsListingProperty)
|
||||
.SingleOrDefault(prop => prop.Info == property.PropertyInfo);
|
||||
if (propConfig is null) continue;
|
||||
propConfig.IsRequired = !property.IsNullable;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using HopFrame.Core.Services;
|
||||
namespace HopFrame.Core.Services.Implementations;
|
||||
|
||||
namespace HopFrame.Testing.Services;
|
||||
|
||||
public class AuthService : IHopFrameAuthHandler {
|
||||
internal sealed class DefaultAuthHandler : IHopFrameAuthHandler {
|
||||
public Task<bool> IsAuthenticatedAsync(string? policy) {
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
public Task<string> GetCurrentUserDisplayNameAsync() {
|
||||
return Task.FromResult("Leon Hoppe");
|
||||
return Task.FromResult(string.Empty);
|
||||
}
|
||||
}
|
||||
@@ -67,10 +67,11 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
||||
return false;
|
||||
}
|
||||
|
||||
public string DisplayProperty(object? item, PropertyInfo info, TableConfig? tableConfig) {
|
||||
public string DisplayProperty(object? item, PropertyConfig prop, TableConfig? tableConfig) {
|
||||
if (item is null) return string.Empty;
|
||||
var prop = tableConfig?.Properties.Find(prop => prop.Info.Name == info.Name);
|
||||
if (prop is null) return item.ToString() ?? string.Empty;
|
||||
|
||||
if (prop.IsListingProperty)
|
||||
return prop.Formatter!.Invoke(item);
|
||||
|
||||
var propValue = prop.Info.GetValue(item);
|
||||
if (propValue is null)
|
||||
@@ -83,14 +84,17 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
|
||||
if (prop.DisplayedProperty is null) {
|
||||
var key = prop.Info.PropertyType
|
||||
.GetProperties()
|
||||
.Where(p => p.GetCustomAttributes(true).Any(a => a is KeyAttribute))
|
||||
.FirstOrDefault();
|
||||
.FirstOrDefault(p => p.GetCustomAttributes(true).Any(a => a is KeyAttribute));
|
||||
|
||||
return key?.GetValue(propValue)?.ToString() ?? propValue.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
var innerConfig = explorer.GetTable(propValue.GetType());
|
||||
return DisplayProperty(propValue, prop.DisplayedProperty, innerConfig);
|
||||
var innerProp = innerConfig!.Properties
|
||||
.SingleOrDefault(p => p.Info == prop.DisplayedProperty && !p.IsListingProperty);
|
||||
|
||||
if (innerProp is null) return propValue.ToString() ?? string.Empty;
|
||||
return DisplayProperty(propValue, innerProp, innerConfig);
|
||||
}
|
||||
|
||||
private IQueryable<TModel> IncludeForgeinKeys(IQueryable<TModel> query) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@using Microsoft.EntityFrameworkCore.Internal
|
||||
|
||||
<FluentDialogBody>
|
||||
@foreach (var property in Content.Config.Properties) {
|
||||
@foreach (var property in Content.Config.Properties.Where(prop => !prop.IsListingProperty).OrderBy(prop => prop.Order)) {
|
||||
if (!_currentlyEditing && !property.Creatable) continue;
|
||||
|
||||
<div style="margin-bottom: 20px">
|
||||
@@ -21,10 +21,10 @@
|
||||
Required="@property.IsRequired"
|
||||
ReadOnly="true"
|
||||
Style="width: 100%"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Text))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Text))" />
|
||||
</div>
|
||||
<div style="display: flex; gap: 5px; margin-bottom: 4px">
|
||||
<FluentButton OnClick="() => 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" />
|
||||
</FluentButton>
|
||||
<FluentButton OnClick="async () => await OpenRelationalPicker(property)" Disabled="@(!property.Editable)">
|
||||
@@ -41,7 +41,7 @@
|
||||
Style="width: 100%;"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Number))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Number))" />
|
||||
}
|
||||
else if (Type.GetTypeCode(property.Info.PropertyType) == TypeCode.Boolean) {
|
||||
<FluentSwitch
|
||||
@@ -49,24 +49,24 @@
|
||||
Value="GetPropertyValue<bool>(property)"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Switch))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Switch))" />
|
||||
}
|
||||
else if (Type.GetTypeCode(property.Info.PropertyType) == TypeCode.DateTime) {
|
||||
<div style="display: flex; gap: 10px">
|
||||
<div style="display: flex; gap: 5px">
|
||||
<div style="display: flex; flex-direction: column; width: 100%">
|
||||
<FluentDatePicker
|
||||
Label="@property.Name"
|
||||
Value="GetPropertyValue<DateTime>(property)"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Date))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Date))" />
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; justify-content: flex-end">
|
||||
<FluentTimePicker
|
||||
Value="GetPropertyValue<DateTime>(property)"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Time))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Time))" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -77,7 +77,7 @@
|
||||
Style="width: 100%"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Date))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Date))" />
|
||||
}
|
||||
else if (property.Info.PropertyType == typeof(TimeOnly)) {
|
||||
<FluentTimePicker
|
||||
@@ -86,7 +86,7 @@
|
||||
Style="width: 100%"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Time))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Time))" />
|
||||
}
|
||||
else if (property.Info.PropertyType.IsEnum) {
|
||||
<FluentSelect
|
||||
@@ -98,7 +98,28 @@
|
||||
Height="250px"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Enum))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Enum))" />
|
||||
}
|
||||
else if (property.Info.PropertyType.IsNullableEnum()) {
|
||||
<div style="display: flex; gap: 5px; align-items: flex-end">
|
||||
<div style="flex-grow: 1">
|
||||
<FluentSelect
|
||||
TOption="string"
|
||||
Label="@property.Name"
|
||||
Items="@(["", ..Enum.GetNames(Nullable.GetUnderlyingType(property.Info.PropertyType)!)])"
|
||||
Value="@(GetPropertyValue<string>(property))"
|
||||
Style="width: 100%"
|
||||
Height="250px"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Enum))" />
|
||||
</div>
|
||||
<div style="display: flex; gap: 5px">
|
||||
<FluentButton OnClick="async () => await SetPropertyValue(property, null, InputType.Enum)" Disabled="@(!property.Editable)">
|
||||
<FluentIcon Value="@(new Icons.Regular.Size20.Dismiss())" Color="Color.Neutral" />
|
||||
</FluentButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else {
|
||||
<FluentTextField
|
||||
@@ -107,7 +128,7 @@
|
||||
Style="width: 100%;"
|
||||
Disabled="@(!property.Editable)"
|
||||
Required="@property.IsRequired"
|
||||
ValueChanged="@(v => SetPropertyValue(property, v, InputType.Text))" />
|
||||
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Text))" />
|
||||
}
|
||||
|
||||
@foreach (var error in _validationErrors[property.Info.Name]) {
|
||||
@@ -119,6 +140,7 @@
|
||||
|
||||
@inject IContextExplorer Explorer
|
||||
@inject IDialogService Dialogs
|
||||
@inject IHopFrameAuthHandler Handler
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
@@ -141,6 +163,7 @@
|
||||
Content.CurrentObject ??= Activator.CreateInstance(Content.Config.TableType);
|
||||
|
||||
foreach (var property in Content.Config.Properties) {
|
||||
if (property.IsListingProperty) continue;
|
||||
_validationErrors.Add(property.Info.Name, []);
|
||||
}
|
||||
}
|
||||
@@ -157,15 +180,18 @@
|
||||
return (TValue)value;
|
||||
|
||||
if (typeof(TValue) == typeof(string))
|
||||
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config.Info, Content.Config);
|
||||
return (TValue)(object)_manager!.DisplayProperty(Content.CurrentObject, config, Content.Config);
|
||||
|
||||
return (TValue)Convert.ChangeType(value, typeof(TValue));
|
||||
}
|
||||
|
||||
private void SetPropertyValue(PropertyConfig config, object? value, InputType senderType) {
|
||||
private async Task SetPropertyValue(PropertyConfig config, object? value, InputType senderType) {
|
||||
if (!await Handler.IsAuthenticatedAsync(_currentlyEditing ? Content.Config.UpdatePolicy : Content.Config.CreatePolicy))
|
||||
return;
|
||||
|
||||
object? result = null;
|
||||
|
||||
if (value is not null) {
|
||||
if (value is not null && config.Parser is null) {
|
||||
switch (senderType) {
|
||||
case InputType.Number:
|
||||
result = Convert.ChangeType(value, config.Info.PropertyType);
|
||||
@@ -180,7 +206,10 @@
|
||||
break;
|
||||
|
||||
case InputType.Enum:
|
||||
result = Enum.Parse(config.Info.PropertyType, (string)value);
|
||||
var type = Nullable.GetUnderlyingType(config.Info.PropertyType);
|
||||
if (type is not null && string.IsNullOrEmpty((string)value)) break;
|
||||
type ??= config.Info.PropertyType;
|
||||
result = Enum.Parse(type, (string)value);
|
||||
break;
|
||||
|
||||
case InputType.Date:
|
||||
@@ -218,6 +247,9 @@
|
||||
}
|
||||
|
||||
private async Task OpenRelationalPicker(PropertyConfig config) {
|
||||
if (!await Handler.IsAuthenticatedAsync(_currentlyEditing ? Content.Config.UpdatePolicy : Content.Config.CreatePolicy))
|
||||
return;
|
||||
|
||||
var relationTable = Explorer.GetTable(config.Info.PropertyType);
|
||||
if (relationTable is null) return;
|
||||
|
||||
@@ -227,11 +259,16 @@
|
||||
if (result.Cancelled) return;
|
||||
|
||||
var data = (RelationPickerDialogData)result.Data!;
|
||||
SetPropertyValue(config, data.Object, InputType.Relation);
|
||||
await SetPropertyValue(config, data.Object, InputType.Relation);
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateInputs() {
|
||||
if (!await Handler.IsAuthenticatedAsync(_currentlyEditing ? Content.Config.UpdatePolicy : Content.Config.CreatePolicy))
|
||||
return false;
|
||||
|
||||
foreach (var property in Content.Config.Properties) {
|
||||
if (property.IsListingProperty) continue;
|
||||
|
||||
var errorList = _validationErrors[property.Info.Name];
|
||||
errorList.Clear();
|
||||
var value = property.Info.GetValue(Content.CurrentObject);
|
||||
@@ -242,7 +279,7 @@
|
||||
}
|
||||
|
||||
if (value is null && property.IsRequired)
|
||||
errorList.Add("Value cannot be null");
|
||||
errorList.Add($"{property.Name} is required");
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
@using HopFrame.Core.Services
|
||||
@using HopFrame.Core.Config
|
||||
@using HopFrame.Core.Services
|
||||
|
||||
<FluentAppBar Orientation="Orientation.Vertical" Style="background-color: var(--neutral-layer-2); height: auto">
|
||||
<FluentAppBarItem Href="/admin"
|
||||
Match="NavLinkMatch.All"
|
||||
@@ -9,7 +11,7 @@
|
||||
|
||||
<br>
|
||||
|
||||
@foreach (var table in Explorer.GetTableNames()) {
|
||||
@foreach (var table in _tables.OrderBy(t => t.Order).Select(t => t.DisplayName)) {
|
||||
<FluentAppBarItem Href="@("/admin/" + table.ToLower())"
|
||||
Match="NavLinkMatch.All"
|
||||
IconActive="new Icons.Filled.Size24.Database()"
|
||||
@@ -20,3 +22,18 @@
|
||||
</FluentAppBar>
|
||||
|
||||
@inject IContextExplorer Explorer
|
||||
@inject IHopFrameAuthHandler Handler
|
||||
|
||||
@code {
|
||||
|
||||
private readonly List<TableConfig> _tables = [];
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
foreach (var table in Explorer.GetTables()) {
|
||||
if (table.Ignored) continue;
|
||||
if (!await Handler.IsAuthenticatedAsync(table.ViewPolicy)) continue;
|
||||
_tables.Add(table);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,45 @@
|
||||
@page "/admin"
|
||||
@using HopFrame.Core.Config
|
||||
@using HopFrame.Core.Services
|
||||
@layout HopFrameLayout
|
||||
|
||||
<h3>HopFrameHome</h3>
|
||||
<PageTitle>HopFrame</PageTitle>
|
||||
|
||||
<div style="padding: 1.5rem 1.5rem;">
|
||||
<h2>Tables</h2>
|
||||
|
||||
<FluentStack Orientation="Orientation.Horizontal" Wrap="true" Style="margin-top: 1.5rem">
|
||||
@foreach (var table in _tables.OrderBy(t => t.Order)) {
|
||||
<FluentCard Width="350px" Height="200px" Style="display: flex; flex-direction: column; background-color: var(--neutral-layer-1)">
|
||||
<h3 style="margin-bottom: 0;">@table.DisplayName</h3>
|
||||
<FluentLabel Typo="Typography.Body" Color="Color.Info" Style="margin-bottom: 0.5rem">@table.ViewPolicy</FluentLabel>
|
||||
<span>@table.Description</span>
|
||||
<FluentSpacer />
|
||||
<div style="display: flex">
|
||||
<FluentSpacer/>
|
||||
|
||||
<a href="@("/admin/" + table.DisplayName.ToLower())" style="display: inline-block">
|
||||
<FluentButton>Open</FluentButton>
|
||||
</a>
|
||||
</div>
|
||||
</FluentCard>
|
||||
}
|
||||
</FluentStack>
|
||||
</div>
|
||||
|
||||
@inject IContextExplorer Explorer
|
||||
@inject IHopFrameAuthHandler Handler
|
||||
|
||||
@code {
|
||||
|
||||
private readonly List<TableConfig> _tables = [];
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
foreach (var table in Explorer.GetTables()) {
|
||||
if (table.Ignored) continue;
|
||||
if (!await Handler.IsAuthenticatedAsync(table.ViewPolicy)) continue;
|
||||
_tables.Add(table);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,11 +9,16 @@
|
||||
@using Microsoft.JSInterop
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
@if (!DisplaySelection) {
|
||||
<PageTitle>@_config?.DisplayName</PageTitle>
|
||||
}
|
||||
|
||||
<FluentDialogProvider />
|
||||
|
||||
<div style="display: flex; flex-direction: column; height: 100%">
|
||||
<FluentToolbar Class="hopframe-toolbar">
|
||||
<h3>@_config?.DisplayName</h3>
|
||||
@if (!DisplaySelection) {
|
||||
<FluentButton
|
||||
IconStart="@(new Icons.Regular.Size16.ArrowClockwise())"
|
||||
OnClick="Reload"
|
||||
@@ -21,11 +26,12 @@
|
||||
Style="margin-left: 10px">
|
||||
Refresh
|
||||
</FluentButton>
|
||||
}
|
||||
|
||||
<FluentSpacer />
|
||||
<FluentSearch @oninput="OnSearch" @onchange="OnSearch" />
|
||||
<FluentSearch @oninput="OnSearch" @onchange="OnSearch" Style="width: 350px" />
|
||||
|
||||
@if (!DisplaySelection) {
|
||||
@if (!DisplaySelection && _hasCreatePolicy) {
|
||||
<FluentButton OnClick="async () => { await CreateOrEdit(null); }">Add Entry</FluentButton>
|
||||
}
|
||||
</FluentToolbar>
|
||||
@@ -47,25 +53,30 @@
|
||||
|
||||
<div style="flex-grow: 1">
|
||||
<FluentDataGrid Items="_currentlyDisplayedModels.AsQueryable()">
|
||||
@foreach (var property in _config!.Properties.Where(prop => prop.List)) {
|
||||
@foreach (var property in _config!.Properties.Where(prop => prop.List).OrderBy(prop => prop.Order)) {
|
||||
<PropertyColumn
|
||||
Title="@property.Name" Property="o => _manager!.DisplayProperty(o, property.Info, _config)"
|
||||
Title="@property.Name" Property="o => _manager!.DisplayProperty(o, property, _config)"
|
||||
Style="min-width: max-content; height: 44px;"
|
||||
Sortable="@property.Sortable"/>
|
||||
}
|
||||
|
||||
@if (DisplayActions) {
|
||||
@if (DisplayActions && (_hasDeletePolicy || _hasUpdatePolicy)) {
|
||||
var dataIndex = 0;
|
||||
|
||||
<TemplateColumn Title="Actions" Align="@Align.End" Style="min-height: 44px; min-width: max-content">
|
||||
@{ var currentElement = _currentlyDisplayedModels.ElementAtOrDefault(dataIndex); }
|
||||
<FluentButton aria-label="Edit entry" OnClick="async () => { await CreateOrEdit(currentElement); }">
|
||||
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())"/>
|
||||
</FluentButton>
|
||||
|
||||
<FluentButton aria-label="Delete entry" OnClick="async () => { await DeleteEntry(currentElement!); }">
|
||||
<FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning"/>
|
||||
@if (_hasUpdatePolicy) {
|
||||
<FluentButton aria-label="Edit entry" OnClick="async () => { await CreateOrEdit(currentElement); }">
|
||||
<FluentIcon Value="@(new Icons.Regular.Size16.Edit())" />
|
||||
</FluentButton>
|
||||
}
|
||||
|
||||
@if (_hasDeletePolicy) {
|
||||
<FluentButton aria-label="Delete entry" OnClick="async () => { await DeleteEntry(currentElement!); }">
|
||||
<FluentIcon Value="@(new Icons.Regular.Size16.Delete())" Color="Color.Warning" />
|
||||
</FluentButton>
|
||||
}
|
||||
|
||||
@{
|
||||
dataIndex++;
|
||||
@@ -116,6 +127,7 @@
|
||||
@inject NavigationManager Navigator
|
||||
@inject IJSRuntime Js
|
||||
@inject IDialogService Dialogs
|
||||
@inject IHopFrameAuthHandler Handler
|
||||
|
||||
@code {
|
||||
|
||||
@@ -142,7 +154,10 @@
|
||||
private int _totalPages;
|
||||
private string? _searchTerm;
|
||||
private bool _loading;
|
||||
private int _selectedIndex = -1;
|
||||
|
||||
private bool _hasUpdatePolicy;
|
||||
private bool _hasDeletePolicy;
|
||||
private bool _hasCreatePolicy;
|
||||
|
||||
protected override void OnInitialized() {
|
||||
_config ??= Explorer.GetTable(TableDisplayName);
|
||||
@@ -153,6 +168,15 @@
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
if (!await Handler.IsAuthenticatedAsync(_config?.ViewPolicy)) {
|
||||
Navigator.NavigateTo("/admin", true);
|
||||
return;
|
||||
}
|
||||
|
||||
_hasUpdatePolicy = await Handler.IsAuthenticatedAsync(_config?.UpdatePolicy);
|
||||
_hasDeletePolicy = await Handler.IsAuthenticatedAsync(_config?.DeletePolicy);
|
||||
_hasCreatePolicy = await Handler.IsAuthenticatedAsync(_config?.CreatePolicy);
|
||||
|
||||
_manager ??= Explorer.GetTableManager(_config!.PropertyName);
|
||||
_currentlyDisplayedModels = await _manager!.LoadPage(_currentPage, PerPage).ToArrayAsync();
|
||||
_totalPages = await _manager.TotalPages(PerPage);
|
||||
@@ -201,6 +225,11 @@
|
||||
}
|
||||
|
||||
private async Task DeleteEntry(object element) {
|
||||
if (!await Handler.IsAuthenticatedAsync(_config?.DeletePolicy)) {
|
||||
Navigator.NavigateTo("/admin", true);
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = await Dialogs.ShowConfirmationAsync("Do you really want to delete this entry?");
|
||||
var result = await dialog.Result;
|
||||
if (result.Cancelled) return;
|
||||
@@ -210,6 +239,11 @@
|
||||
}
|
||||
|
||||
private async Task CreateOrEdit(object? element) {
|
||||
if (!await Handler.IsAuthenticatedAsync(element is null ? _config?.CreatePolicy : _config?.UpdatePolicy)) {
|
||||
Navigator.NavigateTo("/admin", true);
|
||||
return;
|
||||
}
|
||||
|
||||
var panel = await Dialogs.ShowPanelAsync<HopFrameEditor>(new EditorDialogData(_config!, element), new DialogParameters {
|
||||
TrapFocus = false
|
||||
});
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
using HopFrame.Core;
|
||||
using HopFrame.Core.Config;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.FluentUI.AspNetCore.Components;
|
||||
|
||||
namespace HopFrame.Web;
|
||||
|
||||
public static class ServiceCollectionExtensions {
|
||||
|
||||
public static IServiceCollection AddHopFrame(this IServiceCollection services, Action<HopFrameConfigurator> configurator) {
|
||||
public static IServiceCollection AddHopFrame(this IServiceCollection services, Action<HopFrameConfigurator> configurator, LibraryConfiguration? fluentUiLibraryConfiguration = null) {
|
||||
var config = new HopFrameConfig();
|
||||
configurator.Invoke(new HopFrameConfigurator(config));
|
||||
return AddHopFrame(services, config);
|
||||
return AddHopFrame(services, config, fluentUiLibraryConfiguration);
|
||||
}
|
||||
|
||||
public static IServiceCollection AddHopFrame(this IServiceCollection services, HopFrameConfig config) {
|
||||
public static IServiceCollection AddHopFrame(this IServiceCollection services, HopFrameConfig config, LibraryConfiguration? fluentUiLibraryConfiguration = null) {
|
||||
services.AddSingleton(config);
|
||||
services.AddHopFrameServices();
|
||||
services.AddFluentUIComponents(fluentUiLibraryConfiguration);
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,4 +25,6 @@ public class Post {
|
||||
public TimeOnly At { get; set; }
|
||||
|
||||
public ListSortDirection Type { get; set; }
|
||||
|
||||
public TypeCode? TypeCode { get; set; }
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
using HopFrame.Core.Services;
|
||||
using HopFrame.Testing;
|
||||
using Microsoft.FluentUI.AspNetCore.Components;
|
||||
using HopFrame.Testing.Components;
|
||||
using HopFrame.Testing.Models;
|
||||
using HopFrame.Testing.Services;
|
||||
using HopFrame.Web;
|
||||
using HopFrame.Web.Components.Pages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -20,21 +18,29 @@ builder.Services.AddDbContext<DatabaseContext>(options => {
|
||||
});
|
||||
|
||||
builder.Services.AddHopFrame(options => {
|
||||
options.DisplayUserInfo(false);
|
||||
options.AddDbContext<DatabaseContext>(context => {
|
||||
context.Table<User>(table => {
|
||||
table.Property(u => u.Password)
|
||||
.ValueParser(pwd => pwd + "-edited");
|
||||
|
||||
table.Property(u => u.FirstName)
|
||||
.SetDisplayName("First Name");
|
||||
.List(false);
|
||||
|
||||
table.Property(u => u.LastName)
|
||||
.SetDisplayName("Last Name");
|
||||
.List(false);
|
||||
|
||||
table.Property(u => u.Id)
|
||||
.Sortable(false);
|
||||
.Sortable(false)
|
||||
.OrderIndex(3);
|
||||
|
||||
table.AddListingProperty("Name", user => $"{user.FirstName} {user.LastName}")
|
||||
.OrderIndex(2);
|
||||
|
||||
table.SetDisplayName("Benutzer");
|
||||
table.SetDescription("This table is used for user data store and user authentication");
|
||||
|
||||
table.SetViewPolicy("policy");
|
||||
});
|
||||
|
||||
context.Table<Post>()
|
||||
@@ -61,11 +67,12 @@ builder.Services.AddHopFrame(options => {
|
||||
|
||||
return errors;
|
||||
});
|
||||
|
||||
context.Table<Post>()
|
||||
.OrderIndex(-1);
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddTransient<IHopFrameAuthHandler, AuthService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
|
||||
Reference in New Issue
Block a user