Merge pull request #8 from leonhoppe/feature/generatedAdminPages
Feature/generated admin pages
This commit is contained in:
@@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web", "src\HopFram
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontendTest", "test\FrontendTest\FrontendTest.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontendTest", "test\FrontendTest\FrontendTest.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web.Admin", "src\HopFrame.Web.Admin\HopFrame.Web.Admin.csproj", "{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -42,6 +44,10 @@ Global
|
|||||||
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.Build.0 = Release|Any CPU
|
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEditContextDataAnnotationsExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbc307cd57fb42fc4c7fb9795381958122734d3750f41b6c1735c7d132ecda70_003FEditContextDataAnnotationsExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fb7208b3f72528d22781d25fde9a55271bdf2b5aade4f03b1324579a25493cd8_003FList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValidationMessageStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Ffc81648e473bb3cc818f71427c286ecddc3604d2f4c69c565205bb89e8b4ef4_003FValidationMessageStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue"><AssemblyExplorer>
|
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue"><AssemblyExplorer>
|
||||||
<Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" />
|
<Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" />
|
||||||
<Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" />
|
<Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" />
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ A simple backend management api for ASP.NET Core Web APIs
|
|||||||
- [x] Permission management
|
- [x] Permission management
|
||||||
- [x] Frontend dashboards
|
- [x] Frontend dashboards
|
||||||
|
|
||||||
|
## 2.0 Todo list
|
||||||
|
- [x] 1.0 bug fixes
|
||||||
|
- [x] Code cleanup
|
||||||
|
- [x] Relations in database
|
||||||
|
- [x] Generated Admin pages
|
||||||
|
- [ ] Pretty Login page for administration
|
||||||
|
- [ ] Clean documentation
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
There are two different versions of HopFrame, either the Web API version or the full Blazor web version.
|
There are two different versions of HopFrame, either the Web API version or the full Blazor web version.
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ public class PermissionGroup : IPermissionOwner {
|
|||||||
[Required]
|
[Required]
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
public virtual IList<Permission> Permissions { get; set; }
|
public virtual List<Permission> Permissions { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ public class User : IPermissionOwner {
|
|||||||
[Key, Required, MinLength(36), MaxLength(36)]
|
[Key, Required, MinLength(36), MaxLength(36)]
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
|
|
||||||
[MaxLength(50)]
|
[Required, MaxLength(50)]
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
|
|
||||||
[Required, MaxLength(50), EmailAddress]
|
[Required, MaxLength(50), EmailAddress]
|
||||||
@@ -20,9 +20,9 @@ public class User : IPermissionOwner {
|
|||||||
[Required]
|
[Required]
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
public virtual IList<Permission> Permissions { get; set; }
|
public virtual List<Permission> Permissions { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public virtual IList<Token> Tokens { get; set; }
|
public virtual List<Token> Tokens { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
9
src/HopFrame.Web.Admin/AdminPagesContext.cs
Normal file
9
src/HopFrame.Web.Admin/AdminPagesContext.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using HopFrame.Web.Admin.Generators;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Admin;
|
||||||
|
|
||||||
|
public abstract class AdminPagesContext {
|
||||||
|
|
||||||
|
public virtual void OnModelCreating(IAdminContextGenerator generator) {}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
|
||||||
|
public sealed class AdminDescriptionAttribute(string description) : Attribute {
|
||||||
|
public string Description { get; set; } = description;
|
||||||
|
}
|
||||||
6
src/HopFrame.Web.Admin/Attributes/AdminNameAttribute.cs
Normal file
6
src/HopFrame.Web.Admin/Attributes/AdminNameAttribute.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
|
||||||
|
public sealed class AdminNameAttribute(string name) : Attribute {
|
||||||
|
public string Name { get; set; } = name;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Attributes.Classes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public sealed class AdminButtonConfigAttribute(bool showCreateButton = true, bool showDeleteButton = true, bool showUpdateButton = true) : Attribute {
|
||||||
|
public bool ShowCreateButton { get; set; } = showCreateButton;
|
||||||
|
public bool ShowDeleteButton { get; set; } = showDeleteButton;
|
||||||
|
public bool ShowUpdateButton { get; set; } = showUpdateButton;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using HopFrame.Web.Admin.Models;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Admin.Attributes.Classes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public sealed class AdminPermissionsAttribute(string view = null, string create = null, string update = null, string delete = null) : Attribute {
|
||||||
|
public AdminPagePermissions Permissions { get; set; } = new() {
|
||||||
|
Create = create,
|
||||||
|
Update = update,
|
||||||
|
Delete = delete,
|
||||||
|
View = view
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Attributes.Classes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public class AdminUrlAttribute(string url) : Attribute {
|
||||||
|
public string Url { get; set; } = url;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class AdminBoldAttribute(bool bold = true) : Attribute {
|
||||||
|
public bool Bold { get; set; } = bold;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public sealed class AdminHideValueAttribute : Attribute;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public sealed class AdminIgnoreAttribute(bool onlyForListing = false) : Attribute {
|
||||||
|
public bool OnlyForListing { get; set; } = onlyForListing;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public sealed class AdminPrefixAttribute(string prefix) : Attribute {
|
||||||
|
public string Prefix { get; set; } = prefix;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public sealed class AdminUneditableAttribute : Attribute;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class AdminUniqueAttribute : Attribute;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public sealed class AdminUnsortableAttribute : Attribute;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public sealed class ListingPropertyAttribute : Attribute;
|
||||||
12
src/HopFrame.Web.Admin/Generators/IAdminContextGenerator.cs
Normal file
12
src/HopFrame.Web.Admin/Generators/IAdminContextGenerator.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Generators;
|
||||||
|
|
||||||
|
public interface IAdminContextGenerator {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the generator object for the specified Admin Page. This needs to be within the same Admin Context.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TModel">The Model of the Admin Page</typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPageGenerator<TModel> Page<TModel>();
|
||||||
|
|
||||||
|
}
|
||||||
109
src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs
Normal file
109
src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Admin.Generators;
|
||||||
|
|
||||||
|
public interface IAdminPageGenerator<TModel> {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the title of the Admin Page
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">the specified title</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPageGenerator<TModel> Title(string title);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the description of the Admin Page
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="description">the specified description</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPageGenerator<TModel> Description(string description);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the url for the Admin Page
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">the specified url (administration/{url})</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPageGenerator<TModel> Url(string url);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the permission needed to view the Admin Page
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="permission">the specified permission</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPageGenerator<TModel> ViewPermission(string permission);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the permission needed to create a new Entry
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="permission">the specified permission</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPageGenerator<TModel> CreatePermission(string permission);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the permission needed to update an Entry
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="permission">the specified permission</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPageGenerator<TModel> UpdatePermission(string permission);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the permission needed to delete an Entry
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="permission">the specified permission</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPageGenerator<TModel> DeletePermission(string permission);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables or disables the create button
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="show">the specified state</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPageGenerator<TModel> ShowCreateButton(bool show);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables or disables the delete button
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="show">the specified state</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPageGenerator<TModel> ShowDeleteButton(bool show);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables or disables the update button
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="show">the specified state</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPageGenerator<TModel> ShowUpdateButton(bool show);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the default sort property and direction
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyExpression">Which property should be sorted</param>
|
||||||
|
/// <param name="direction">In which direction should be sorted</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPageGenerator<TModel> DefaultSort<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression, ListSortDirection direction);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the repository for the page
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TRepository">The specified repository</typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPageGenerator<TModel> ConfigureRepository<TRepository>() where TRepository : ModelRepository<TModel>;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the generator of the specified property
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyExpression">The property</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> Property<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the default property that should be displayed as a property in other listings
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyExpression">The property</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPageGenerator<TModel> ListingProperty<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression);
|
||||||
|
|
||||||
|
}
|
||||||
123
src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs
Normal file
123
src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
using System.Linq.Expressions;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Admin.Generators;
|
||||||
|
|
||||||
|
public interface IAdminPropertyGenerator<TProperty, TModel> {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should the property be sortable or not
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> Sortable(bool sortable);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should the admin be able to edit the property after creation or not
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> Editable(bool editable);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should the value of the property be displayed while editing or not (useful for passwords and tokens)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> DisplayValueWhileEditing(bool display);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should the property be a column on the page list or not
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> DisplayInListing(bool display = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should the property be ignored completely
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> Ignore(bool ignore = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the value of the property database generated and is not meant to be changed
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> Generated(bool generated = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should the property value be bold in the listing or not
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> Bold(bool bold = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the value of the property unique under all other entries in the dataset
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unique"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> Unique(bool unique = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the display name in the listing and editing/creation
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> DisplayName(string displayName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Has the value of the property a never changing prefix that doesn't need to be specified or displayed
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> Prefix(string prefix);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The specified function gets called before creation/edit to verify that the entered value matches the property requirements
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> Validator(Func<TProperty, string> validator);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the input type in creation/edit to a selector for the property type. The property type needs to have its own admin page in order for the selector to work!
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> IsSelector(bool selector = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the input type in creation/edit to a selector for the specified type. The specified type needs to have its own admin page in order for the selector to work!
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="selector"></param>
|
||||||
|
/// <typeparam name="TSelectorType"></typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> IsSelector<TSelectorType>(bool selector = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The specified function gets called, whenever the entry is changed/created in order to convert the raw string input to the proper property type
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> Parser(Func<TModel, string, TProperty> parser);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The specified function gets called, whenever the entry is changed/created in order to convert the raw string input to the proper property type
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TInput">Needs to be specified if the field is not a plain string field (like a selector with a different type)</typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> Parser<TInput>(Func<TModel, TInput, TProperty> parser);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The specified function gets called, whenever the entry is changed/created in order to convert the raw string input to the proper property type
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TInput">Needs to be specified if the field is not a plain string field (like a selector with a different type)</typeparam>
|
||||||
|
/// <typeparam name="TInnerProperty">Needs to be specified if the property type is a List</typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> Parser<TInput, TInnerProperty>(Func<TModel, TInput, TInnerProperty> parser);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the default property that should be displayed as a value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyExpression"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> DisplayProperty(Expression<Func<TProperty, object>> propertyExpression);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the default property that should be displayed as a value
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TInnerProperty">Needs to be specified if the property type is a List</typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
IAdminPropertyGenerator<TProperty, TModel> DisplayProperty<TInnerProperty>(Expression<Func<TInnerProperty, object>> propertyExpression);
|
||||||
|
|
||||||
|
}
|
||||||
11
src/HopFrame.Web.Admin/Generators/IGenerator.cs
Normal file
11
src/HopFrame.Web.Admin/Generators/IGenerator.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Generators;
|
||||||
|
|
||||||
|
public interface IGenerator<out TGeneratedType> {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compiles the generator with all specified options
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The compiled data structure</returns>
|
||||||
|
TGeneratedType Compile();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using HopFrame.Web.Admin.Models;
|
||||||
|
using HopFrame.Web.Admin.Providers;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Admin.Generators.Implementation;
|
||||||
|
|
||||||
|
internal class AdminContextGenerator : IAdminContextGenerator {
|
||||||
|
|
||||||
|
private readonly IDictionary<Type, object> _adminPages = new Dictionary<Type, object>();
|
||||||
|
|
||||||
|
public IAdminPageGenerator<TModel> Page<TModel>() {
|
||||||
|
if (_adminPages.TryGetValue(typeof(TModel), out var pageGenerator))
|
||||||
|
return pageGenerator as IAdminPageGenerator<TModel>;
|
||||||
|
|
||||||
|
var generator = Activator.CreateInstance(typeof(IAdminPageGenerator<TModel>)) as AdminPageGenerator<TModel>;
|
||||||
|
generator?.ApplyConfigurationFromAttributes(typeof(TModel).GetCustomAttributes(false));
|
||||||
|
|
||||||
|
_adminPages.Add(typeof(TModel), generator);
|
||||||
|
|
||||||
|
return generator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdminPage<TModel> CompilePage<TModel>() {
|
||||||
|
var generator = _adminPages[typeof(TModel)];
|
||||||
|
if (generator is null) return null;
|
||||||
|
|
||||||
|
return (generator as AdminPageGenerator<TModel>)?.Compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TContext CompileContext<TContext>() where TContext : AdminPagesContext {
|
||||||
|
var type = typeof(TContext);
|
||||||
|
var compileMethod = typeof(AdminContextGenerator).GetMethod(nameof(CompilePage));
|
||||||
|
|
||||||
|
var properties = type.GetProperties();
|
||||||
|
|
||||||
|
var context = Activator.CreateInstance<TContext>();
|
||||||
|
|
||||||
|
foreach (var property in properties) {
|
||||||
|
var propertyType = property.PropertyType.GenericTypeArguments[0];
|
||||||
|
var pageGeneratorType = typeof(AdminPageGenerator<>).MakeGenericType(propertyType);
|
||||||
|
var generatorInstance = Activator.CreateInstance(pageGeneratorType);
|
||||||
|
|
||||||
|
var titleMethod = pageGeneratorType.GetMethod(nameof(AdminPageGenerator<TContext>.Title));
|
||||||
|
titleMethod?.Invoke(generatorInstance, [property.Name]);
|
||||||
|
|
||||||
|
var populateMethod = pageGeneratorType.GetMethod(nameof(AdminPageGenerator<TContext>.ApplyConfigurationFromAttributes));
|
||||||
|
populateMethod?.Invoke(generatorInstance, [propertyType.GetCustomAttributes(false)]);
|
||||||
|
|
||||||
|
_adminPages.Add(propertyType, generatorInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.OnModelCreating(this);
|
||||||
|
|
||||||
|
foreach (var property in properties) {
|
||||||
|
var modelType = property.PropertyType.GenericTypeArguments[0];
|
||||||
|
var method = compileMethod?.MakeGenericMethod(modelType);
|
||||||
|
property.SetValue(context, method?.Invoke(this, []));
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static void RegisterPages(AdminPagesContext context, IAdminPagesProvider provider, IServiceCollection services) {
|
||||||
|
var properties = context.GetType().GetProperties();
|
||||||
|
|
||||||
|
foreach (var property in properties) {
|
||||||
|
var page = property.GetValue(context) as AdminPage;
|
||||||
|
if (page is null) continue;
|
||||||
|
|
||||||
|
provider.RegisterAdminPage(page.Title.ToLower(), page);
|
||||||
|
|
||||||
|
if (page.RepositoryProvider is not null)
|
||||||
|
services.AddScoped(page.RepositoryProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using HopFrame.Web.Admin.Attributes;
|
||||||
|
using HopFrame.Web.Admin.Attributes.Classes;
|
||||||
|
using HopFrame.Web.Admin.Models;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Admin.Generators.Implementation;
|
||||||
|
|
||||||
|
internal sealed class AdminPageGenerator<TModel> : IAdminPageGenerator<TModel>, IGenerator<AdminPage<TModel>> {
|
||||||
|
|
||||||
|
public readonly AdminPage<TModel> Page;
|
||||||
|
private readonly Dictionary<string, object> _propertyGenerators;
|
||||||
|
|
||||||
|
public AdminPageGenerator() {
|
||||||
|
Page = new AdminPage<TModel> {
|
||||||
|
Permissions = new AdminPagePermissions(),
|
||||||
|
ModelType = typeof(TModel)
|
||||||
|
};
|
||||||
|
_propertyGenerators = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
var type = typeof(TModel);
|
||||||
|
var properties = type.GetProperties();
|
||||||
|
var generatorType = typeof(AdminPropertyGenerator<,>);
|
||||||
|
|
||||||
|
foreach (var property in properties) {
|
||||||
|
var attributes = property.GetCustomAttributes(false);
|
||||||
|
var genericType = generatorType.MakeGenericType(property.PropertyType, type);
|
||||||
|
|
||||||
|
var generator = Activator.CreateInstance(genericType, [property.Name, property.PropertyType]);
|
||||||
|
|
||||||
|
var method = genericType
|
||||||
|
.GetMethod(nameof(AdminPropertyGenerator<object, object>.ApplyConfigurationFromAttributes))?
|
||||||
|
.MakeGenericMethod(type);
|
||||||
|
method?.Invoke(generator, [this, attributes, property]);
|
||||||
|
|
||||||
|
_propertyGenerators.Add(property.Name, generator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPageGenerator<TModel> Title(string title) {
|
||||||
|
Page.Title = title;
|
||||||
|
Page.Url ??= title.ToLower();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPageGenerator<TModel> Description(string description) {
|
||||||
|
Page.Description = description;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPageGenerator<TModel> Url(string url) {
|
||||||
|
Page.Url = url;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPageGenerator<TModel> ViewPermission(string permission) {
|
||||||
|
Page.Permissions.View = permission;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPageGenerator<TModel> CreatePermission(string permission) {
|
||||||
|
Page.Permissions.Create = permission;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPageGenerator<TModel> UpdatePermission(string permission) {
|
||||||
|
Page.Permissions.Update = permission;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPageGenerator<TModel> DeletePermission(string permission) {
|
||||||
|
Page.Permissions.Delete = permission;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPageGenerator<TModel> ShowCreateButton(bool show) {
|
||||||
|
Page.ShowCreateButton = show;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPageGenerator<TModel> ShowDeleteButton(bool show) {
|
||||||
|
Page.ShowDeleteButton = show;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPageGenerator<TModel> ShowUpdateButton(bool show) {
|
||||||
|
Page.ShowUpdateButton = show;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPageGenerator<TModel> DefaultSort<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression, ListSortDirection direction) {
|
||||||
|
var property = GetPropertyInfo(propertyExpression);
|
||||||
|
|
||||||
|
Page.DefaultSortPropertyName = property.Name;
|
||||||
|
Page.DefaultSortDirection = direction;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPageGenerator<TModel> ConfigureRepository<TRepository>() where TRepository : ModelRepository<TModel> {
|
||||||
|
Page.RepositoryProvider = typeof(TRepository);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> Property<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression) {
|
||||||
|
var property = GetPropertyInfo(propertyExpression);
|
||||||
|
|
||||||
|
if (_propertyGenerators.TryGetValue(property.Name, out var propertyGenerator))
|
||||||
|
return propertyGenerator as AdminPropertyGenerator<TProperty, TModel>;
|
||||||
|
|
||||||
|
var generator = Activator.CreateInstance(typeof(AdminPropertyGenerator<TProperty, TModel>), new { property.Name, property.PropertyType }) as AdminPropertyGenerator<TProperty, TModel>;
|
||||||
|
generator?.ApplyConfigurationFromAttributes(this, property.GetCustomAttributes(false), property);
|
||||||
|
_propertyGenerators.Add(property.Name, generator);
|
||||||
|
|
||||||
|
return generator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPageGenerator<TModel> ListingProperty<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression) {
|
||||||
|
var property = GetPropertyInfo(propertyExpression);
|
||||||
|
Page.ListingProperty = property.Name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdminPage<TModel> Compile() {
|
||||||
|
var properties = new List<AdminPageProperty>();
|
||||||
|
|
||||||
|
foreach (var generator in _propertyGenerators.Values) {
|
||||||
|
var method = generator.GetType().GetMethod(nameof(AdminPropertyGenerator<object, object>.Compile));
|
||||||
|
var prop = method?.Invoke(generator, []) as AdminPageProperty;
|
||||||
|
properties.Add(prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
Page.Properties = properties;
|
||||||
|
|
||||||
|
return Page;
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member.Member is not PropertyInfo propInfo) {
|
||||||
|
throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Type type = typeof(TSource);
|
||||||
|
if (propInfo.ReflectedType != null && type != propInfo.ReflectedType &&
|
||||||
|
!type.IsSubclassOf(propInfo.ReflectedType)) {
|
||||||
|
throw new ArgumentException($"Expression '{propertyLambda}' refers to a property that is not from type {type}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propInfo.Name is null)
|
||||||
|
throw new ArgumentException($"Expression '{propertyLambda}' refers a not existing property.");
|
||||||
|
|
||||||
|
return propInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyConfigurationFromAttributes(object[] attributes) {
|
||||||
|
if (attributes.Any(a => a is AdminNameAttribute)) {
|
||||||
|
var attribute = attributes.Single(a => a is AdminNameAttribute) as AdminNameAttribute;
|
||||||
|
Title(attribute?.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes.Any(a => a is AdminDescriptionAttribute)) {
|
||||||
|
var attribute = attributes.Single(a => a is AdminDescriptionAttribute) as AdminDescriptionAttribute;
|
||||||
|
Description(attribute?.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes.Any(a => a is AdminUrlAttribute)) {
|
||||||
|
var attribute = attributes.Single(a => a is AdminUrlAttribute) as AdminUrlAttribute;
|
||||||
|
Url(attribute?.Url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes.Any(a => a is AdminPermissionsAttribute)) {
|
||||||
|
var attribute = attributes.Single(a => a is AdminPermissionsAttribute) as AdminPermissionsAttribute;
|
||||||
|
CreatePermission(attribute?.Permissions.Create);
|
||||||
|
UpdatePermission(attribute?.Permissions.Update);
|
||||||
|
ViewPermission(attribute?.Permissions.View);
|
||||||
|
DeletePermission(attribute?.Permissions.Delete);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes.Any(a => a is AdminButtonConfigAttribute)) {
|
||||||
|
var attribute = attributes.Single(a => a is AdminButtonConfigAttribute) as AdminButtonConfigAttribute;
|
||||||
|
ShowCreateButton(attribute?.ShowCreateButton == true);
|
||||||
|
ShowUpdateButton(attribute?.ShowUpdateButton == true);
|
||||||
|
ShowDeleteButton(attribute?.ShowDeleteButton == true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using HopFrame.Web.Admin.Attributes;
|
||||||
|
using HopFrame.Web.Admin.Attributes.Members;
|
||||||
|
using HopFrame.Web.Admin.Models;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Admin.Generators.Implementation;
|
||||||
|
|
||||||
|
internal sealed class AdminPropertyGenerator<TProperty, TModel>(string name, Type type) : IAdminPropertyGenerator<TProperty, TModel>, IGenerator<AdminPageProperty> {
|
||||||
|
|
||||||
|
private readonly AdminPageProperty _property = new() {
|
||||||
|
Name = name,
|
||||||
|
Type = type
|
||||||
|
};
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> Sortable(bool sortable) {
|
||||||
|
_property.Sortable = sortable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> Editable(bool editable) {
|
||||||
|
_property.Editable = editable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> DisplayValueWhileEditing(bool display) {
|
||||||
|
_property.EditDisplayValue = display;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> DisplayInListing(bool display = true) {
|
||||||
|
_property.DisplayInListing = display;
|
||||||
|
_property.Sortable = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> Ignore(bool ignore = false) {
|
||||||
|
_property.Ignore = ignore;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> Generated(bool generated = true) {
|
||||||
|
_property.Generated = generated;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> Bold(bool bold = true) {
|
||||||
|
_property.Bold = bold;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> Unique(bool unique = true) {
|
||||||
|
_property.Unique = unique;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> DisplayName(string displayName) {
|
||||||
|
_property.DisplayName = displayName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> Prefix(string prefix) {
|
||||||
|
_property.Prefix = prefix;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> Validator(Func<TProperty, string> validator) {
|
||||||
|
_property.Validator = o => validator.Invoke((TProperty)o);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> IsSelector(bool selector = true) {
|
||||||
|
_property.Selector = selector;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> IsSelector<TSelectorType>(bool selector = true) {
|
||||||
|
_property.Selector = true;
|
||||||
|
_property.SelectorType = typeof(TSelectorType);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> Parser(Func<TModel, string, TProperty> parser) {
|
||||||
|
_property.Parser = (o, s) => parser.Invoke((TModel)o, s.ToString());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> Parser<TInput>(Func<TModel, TInput, TProperty> parser) {
|
||||||
|
_property.Parser = (o, s) => parser.Invoke((TModel)o, (TInput)s);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> Parser<TInput, TInnerProperty>(Func<TModel, TInput, TInnerProperty> parser) {
|
||||||
|
_property.Parser = (o, s) => parser.Invoke((TModel)o, (TInput)s);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> DisplayProperty(Expression<Func<TProperty, object>> propertyExpression) {
|
||||||
|
var property = AdminPageGenerator<object>.GetPropertyInfo(propertyExpression);
|
||||||
|
_property.DisplayPropertyName = property.Name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAdminPropertyGenerator<TProperty, TModel> DisplayProperty<TInnerProperty>(Expression<Func<TInnerProperty, object>> propertyExpression) {
|
||||||
|
var property = AdminPageGenerator<object>.GetPropertyInfo(propertyExpression);
|
||||||
|
_property.DisplayPropertyName = property.Name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdminPageProperty Compile() {
|
||||||
|
_property.DisplayName ??= _property.Name;
|
||||||
|
return _property;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyConfigurationFromAttributes<T>(AdminPageGenerator<T> pageGenerator, object[] attributes, PropertyInfo property) {
|
||||||
|
if (attributes.Any(a => a is KeyAttribute)) {
|
||||||
|
pageGenerator.Page.DefaultSortPropertyName = property.Name;
|
||||||
|
Editable(false);
|
||||||
|
Bold();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes.Any(a => a is AdminUnsortableAttribute))
|
||||||
|
Sortable(false);
|
||||||
|
|
||||||
|
if (attributes.Any(a => a is AdminUneditableAttribute))
|
||||||
|
Editable(false);
|
||||||
|
|
||||||
|
if (attributes.Any(a => a is AdminUniqueAttribute))
|
||||||
|
Unique();
|
||||||
|
|
||||||
|
if (attributes.Any(a => a is AdminIgnoreAttribute)) {
|
||||||
|
var attribute = attributes.Single(a => a is AdminIgnoreAttribute) as AdminIgnoreAttribute;
|
||||||
|
DisplayInListing(false);
|
||||||
|
Sortable(false);
|
||||||
|
Ignore(attribute?.OnlyForListing == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes.Any(a => a is AdminHideValueAttribute))
|
||||||
|
DisplayValueWhileEditing(false);
|
||||||
|
|
||||||
|
if (attributes.Any(a => a is DatabaseGeneratedAttribute))
|
||||||
|
Generated();
|
||||||
|
|
||||||
|
if (attributes.Any(a => a is AdminNameAttribute)) {
|
||||||
|
var attribute = attributes.Single(a => a is AdminNameAttribute) as AdminNameAttribute;
|
||||||
|
DisplayName(attribute?.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes.Any(a => a is AdminBoldAttribute)) {
|
||||||
|
var attribute = attributes.Single(a => a is AdminBoldAttribute) as AdminBoldAttribute;
|
||||||
|
Bold(attribute?.Bold == true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes.Any(a => a is RequiredAttribute)) {
|
||||||
|
_property.Required = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes.Any(a => a is AdminPrefixAttribute)) {
|
||||||
|
var attribute = attributes.Single(a => a is AdminPrefixAttribute) as AdminPrefixAttribute;
|
||||||
|
Prefix(attribute?.Prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes.Any(a => a is ListingPropertyAttribute)) {
|
||||||
|
var attribute = attributes.Single(a => a is ListingPropertyAttribute) as ListingPropertyAttribute;
|
||||||
|
_property.DisplayPropertyName = property.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/HopFrame.Web.Admin/HopFrame.Web.Admin.csproj
Normal file
13
src/HopFrame.Web.Admin/HopFrame.Web.Admin.csproj
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
33
src/HopFrame.Web.Admin/ModelRepository.cs
Normal file
33
src/HopFrame.Web.Admin/ModelRepository.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
namespace HopFrame.Web.Admin;
|
||||||
|
|
||||||
|
public abstract class ModelRepository<TModel> : IModelRepository {
|
||||||
|
public abstract Task<IEnumerable<TModel>> ReadAll();
|
||||||
|
public abstract Task<TModel> Create(TModel model);
|
||||||
|
public abstract Task<TModel> Update(TModel model);
|
||||||
|
public abstract Task Delete(TModel model);
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<IEnumerable<object>> ReadAllO() {
|
||||||
|
var models = await ReadAll();
|
||||||
|
return models.Select(m => (object)m);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> CreateO(object model) {
|
||||||
|
return await Create((TModel)model);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> UpdateO(object model) {
|
||||||
|
return await Update((TModel)model);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteO(object model) {
|
||||||
|
return Delete((TModel)model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IModelRepository {
|
||||||
|
Task<IEnumerable<object>> ReadAllO();
|
||||||
|
Task<object> CreateO(object model);
|
||||||
|
Task<object> UpdateO(object model);
|
||||||
|
Task DeleteO(object model);
|
||||||
|
}
|
||||||
26
src/HopFrame.Web.Admin/Models/AdminPage.cs
Normal file
26
src/HopFrame.Web.Admin/Models/AdminPage.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Admin.Models;
|
||||||
|
|
||||||
|
public sealed class AdminPage<TModel> : AdminPage;
|
||||||
|
|
||||||
|
public class AdminPage {
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string Url { get; set; }
|
||||||
|
public AdminPagePermissions Permissions { get; set; }
|
||||||
|
public IList<AdminPageProperty> Properties { get; set; }
|
||||||
|
public string ListingProperty { get; set; }
|
||||||
|
|
||||||
|
public Type RepositoryProvider { get; set; }
|
||||||
|
|
||||||
|
public Type ModelType { get; set; }
|
||||||
|
|
||||||
|
public string DefaultSortPropertyName { get; set; }
|
||||||
|
public ListSortDirection DefaultSortDirection { get; set; }
|
||||||
|
|
||||||
|
public bool ShowCreateButton { get; set; } = true;
|
||||||
|
public bool ShowDeleteButton { get; set; } = true;
|
||||||
|
public bool ShowUpdateButton { get; set; } = true;
|
||||||
|
}
|
||||||
8
src/HopFrame.Web.Admin/Models/AdminPagePermissions.cs
Normal file
8
src/HopFrame.Web.Admin/Models/AdminPagePermissions.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace HopFrame.Web.Admin.Models;
|
||||||
|
|
||||||
|
public sealed class AdminPagePermissions {
|
||||||
|
public string View { get; set; }
|
||||||
|
public string Create { get; set; }
|
||||||
|
public string Update { get; set; }
|
||||||
|
public string Delete { get; set; }
|
||||||
|
}
|
||||||
39
src/HopFrame.Web.Admin/Models/AdminPageProperty.cs
Normal file
39
src/HopFrame.Web.Admin/Models/AdminPageProperty.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Admin.Models;
|
||||||
|
|
||||||
|
public sealed class AdminPageProperty {
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
public string Prefix { get; set; }
|
||||||
|
public string DisplayPropertyName { get; set; }
|
||||||
|
|
||||||
|
public bool DisplayInListing { get; set; } = true;
|
||||||
|
public bool Sortable { get; set; } = true;
|
||||||
|
public bool Editable { get; set; } = true;
|
||||||
|
public bool EditDisplayValue { get; set; } = true;
|
||||||
|
public bool Generated { get; set; }
|
||||||
|
public bool Bold { get; set; }
|
||||||
|
public bool Required { get; set; }
|
||||||
|
public bool Ignore { get; set; }
|
||||||
|
public bool Unique { get; set; }
|
||||||
|
public bool Selector { get; set; }
|
||||||
|
public Type SelectorType { get; set; }
|
||||||
|
|
||||||
|
public Type Type { get; set; }
|
||||||
|
|
||||||
|
public Func<object, string> Validator { get; set; }
|
||||||
|
public Func<object, object, object> Parser { get; set; }
|
||||||
|
|
||||||
|
public object GetValue(object entry) {
|
||||||
|
return entry.GetType().GetProperty(Name)?.GetValue(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T GetValue<T>(object entry) {
|
||||||
|
return (T)entry.GetType().GetProperty(Name)?.GetValue(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetValue(object entry, object value) {
|
||||||
|
entry.GetType().GetProperty(Name)?.SetValue(entry, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/HopFrame.Web.Admin/Providers/IAdminPagesProvider.cs
Normal file
12
src/HopFrame.Web.Admin/Providers/IAdminPagesProvider.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using HopFrame.Web.Admin.Models;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Admin.Providers;
|
||||||
|
|
||||||
|
public interface IAdminPagesProvider {
|
||||||
|
|
||||||
|
internal void RegisterAdminPage(string url, AdminPage page);
|
||||||
|
AdminPage LoadAdminPage(string url);
|
||||||
|
IList<AdminPage> LoadRegisteredAdminPages();
|
||||||
|
AdminPage HasPageFor(Type type);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using HopFrame.Web.Admin.Models;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Admin.Providers.Implementation;
|
||||||
|
|
||||||
|
public class AdminPagesProvider : IAdminPagesProvider {
|
||||||
|
private readonly IDictionary<string, AdminPage> _pages = new Dictionary<string, AdminPage>();
|
||||||
|
|
||||||
|
public void RegisterAdminPage(string url, AdminPage page) {
|
||||||
|
_pages.Add(url, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdminPage LoadAdminPage(string url) {
|
||||||
|
return _pages.TryGetValue(url, out var page) ? page : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<AdminPage> LoadRegisteredAdminPages() {
|
||||||
|
return _pages.Values.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdminPage HasPageFor(Type type) {
|
||||||
|
return _pages
|
||||||
|
.Where(p => p.Value.ModelType == type)
|
||||||
|
.Select(p => p.Value)
|
||||||
|
.SingleOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/HopFrame.Web.Admin/ServiceCollectionExtensions.cs
Normal file
29
src/HopFrame.Web.Admin/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using HopFrame.Web.Admin.Generators.Implementation;
|
||||||
|
using HopFrame.Web.Admin.Providers;
|
||||||
|
using HopFrame.Web.Admin.Providers.Implementation;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Admin;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions {
|
||||||
|
|
||||||
|
private static IAdminPagesProvider _provider;
|
||||||
|
|
||||||
|
public static IServiceCollection AddAdminContext<TContext>(this IServiceCollection services) where TContext : AdminPagesContext {
|
||||||
|
var provider = GetProvider();
|
||||||
|
services.TryAddSingleton(provider);
|
||||||
|
|
||||||
|
var generator = new AdminContextGenerator();
|
||||||
|
var context = generator.CompileContext<TContext>();
|
||||||
|
AdminContextGenerator.RegisterPages(context, provider, services);
|
||||||
|
services.AddSingleton(context);
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IAdminPagesProvider GetProvider() {
|
||||||
|
return _provider ??= new AdminPagesProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
373
src/HopFrame.Web/Components/Administration/AdminPageModal.razor
Normal file
373
src/HopFrame.Web/Components/Administration/AdminPageModal.razor
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
@using System.Collections
|
||||||
|
@using System.Globalization
|
||||||
|
@using BlazorStrap
|
||||||
|
@using BlazorStrap.Shared.Components.Modal
|
||||||
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
|
@using BlazorStrap.V5
|
||||||
|
@using CurrieTechnologies.Razor.SweetAlert2
|
||||||
|
@using HopFrame.Database.Repositories
|
||||||
|
@using HopFrame.Security.Claims
|
||||||
|
@using HopFrame.Web.Admin
|
||||||
|
@using HopFrame.Web.Admin.Models
|
||||||
|
@using HopFrame.Web.Admin.Providers
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
|
||||||
|
<BSModal DataId="admin-page-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" @ref="_modal">
|
||||||
|
<BSForm TValue="object" EditContext="_context" OnValidSubmit="Save">
|
||||||
|
@if (!_isEdit) {
|
||||||
|
<BSModalHeader>Create entry</BSModalHeader>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<BSModalHeader>Edit entry</BSModalHeader>
|
||||||
|
}
|
||||||
|
|
||||||
|
<BSModalContent>
|
||||||
|
@foreach (var prop in GetEditableProperties()) {
|
||||||
|
@if (!_isEdit && prop.Generated) continue;
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
@if (IsListType(prop)) {
|
||||||
|
<BSLabel>@prop.DisplayName</BSLabel>
|
||||||
|
<BSListGroup>
|
||||||
|
<BSListGroupItem>
|
||||||
|
<BSListGroup IsFlush="true">
|
||||||
|
@foreach (var element in GetListPropertyValues(prop).Select((e, i) => new { e, i })) {
|
||||||
|
<BSListGroupItem>
|
||||||
|
<BSButton Color="BSColor.Danger" Size="Size.ExtraSmall" MarginEnd="Margins.Small" OnClick="() => DeleteListItem(prop, element.i)">
|
||||||
|
<HopIconDisplay Type="HopIconDisplay.HopIcon.Cross"/>
|
||||||
|
</BSButton>
|
||||||
|
|
||||||
|
<span>@element.e</span>
|
||||||
|
</BSListGroupItem>
|
||||||
|
}
|
||||||
|
</BSListGroup>
|
||||||
|
</BSListGroupItem>
|
||||||
|
<BSListGroupItem>
|
||||||
|
<div>
|
||||||
|
@if (!prop.Selector) {
|
||||||
|
<form style="display: flex; gap: 20px" @onsubmit="() => AddListItem(prop)">
|
||||||
|
<input type="text" class="form-control" @onchange="v => _inputValues[prop] = (string)v.Value" required/>
|
||||||
|
<BSButton Color="BSColor.Secondary" IsSubmit="true">Add</BSButton>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<form style="display: flex; gap: 20px" @onsubmit="() => AddListItem(prop)">
|
||||||
|
<select class="form-select" @onchange="e => _inputValues[prop] = ReadSelectorValue(prop, e.Value)">
|
||||||
|
<option selected>Select</option>
|
||||||
|
|
||||||
|
@foreach (var element in SetupSelectorProperty(prop).GetAwaiter().GetResult()) {
|
||||||
|
<option value="@element.Item2">@element.Item1</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<BSButton Color="BSColor.Secondary" IsSubmit="true">Add</BSButton>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</BSListGroupItem>
|
||||||
|
</BSListGroup>
|
||||||
|
}
|
||||||
|
else if (IsSwitch(prop)) {
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<BSLabel>@prop.DisplayName</BSLabel>
|
||||||
|
<input class="form-check-input" type="checkbox" checked="@_values[prop]" @onchange="e => _values[prop] = Convert.ToBoolean(e.Value)">
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (prop.Prefix is not null && !_isEdit) {
|
||||||
|
<BSInputGroup>
|
||||||
|
<span class="@BS.Input_Group_Text">@prop.Prefix</span>
|
||||||
|
<input type="text" class="form-control" required="@IsRequired(prop)" disabled="@IsDisabled(prop)" value="@GetPropertyValue(prop)" @onchange="e => _values[prop] = prop.Prefix + e.Value"/>
|
||||||
|
</BSInputGroup>
|
||||||
|
}
|
||||||
|
else if (prop.Selector) {
|
||||||
|
<BSLabel>@prop.DisplayName</BSLabel>
|
||||||
|
<select class="form-select" @onchange="e => _values[prop] = ReadSelectorValue(prop, e.Value)">
|
||||||
|
<option>Select</option>
|
||||||
|
|
||||||
|
@foreach (var element in SetupSelectorProperty(prop).GetAwaiter().GetResult()) {
|
||||||
|
<option value="@element.Item2" selected="@IsIndexSelected(prop, element.Item2)">@element.Item1</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<BSLabel>@prop.DisplayName</BSLabel>
|
||||||
|
<input type="@GetInputType(prop)" class="form-control" required="@IsRequired(prop)" disabled="@IsDisabled(prop)" value="@GetPropertyValue(prop)" @onchange="e => _values[prop] = e.Value"/>
|
||||||
|
@if (_validation[_validationIdentifiers[prop]].Any()) {
|
||||||
|
<div class="invalid-feedback" style="display: block">
|
||||||
|
@_validation[_validationIdentifiers[prop]].First()
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</BSModalContent>
|
||||||
|
|
||||||
|
<BSModalFooter>
|
||||||
|
<BSButton Target="admin-page-modal">Cancel</BSButton>
|
||||||
|
<BSButton IsSubmit="true" Color="BSColor.Primary">Save</BSButton>
|
||||||
|
</BSModalFooter>
|
||||||
|
</BSForm>
|
||||||
|
</BSModal>
|
||||||
|
|
||||||
|
@inject IServiceProvider Provider
|
||||||
|
@inject IAdminPagesProvider PageProvider
|
||||||
|
@inject SweetAlertService Alerts
|
||||||
|
@inject IPermissionRepository Permissions
|
||||||
|
@inject ITokenContext Auth
|
||||||
|
|
||||||
|
@code {
|
||||||
|
#pragma warning disable CS4014
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<Task> ReloadDelegate { get; set; }
|
||||||
|
|
||||||
|
private BSModalBase _modal;
|
||||||
|
private EditContext _context;
|
||||||
|
private ValidationMessageStore _validation;
|
||||||
|
private Dictionary<AdminPageProperty, FieldIdentifier> _validationIdentifiers;
|
||||||
|
private IDictionary<AdminPageProperty, object> _values;
|
||||||
|
private Dictionary<AdminPageProperty, object[]> _selectorValues;
|
||||||
|
private IModelRepository _repository;
|
||||||
|
|
||||||
|
private AdminPage _currentPage;
|
||||||
|
private object _entry;
|
||||||
|
private bool _isEdit;
|
||||||
|
private IDictionary<AdminPageProperty, object> _inputValues;
|
||||||
|
|
||||||
|
public async Task Show(AdminPage page, object entryToEdit = null) {
|
||||||
|
_entry = null;
|
||||||
|
_inputValues = new Dictionary<AdminPageProperty, object>();
|
||||||
|
_selectorValues = new Dictionary<AdminPageProperty, object[]>();
|
||||||
|
|
||||||
|
_currentPage = page;
|
||||||
|
_entry = entryToEdit;
|
||||||
|
_isEdit = entryToEdit is not null;
|
||||||
|
_repository = Provider.GetService(_currentPage.RepositoryProvider) as IModelRepository;
|
||||||
|
|
||||||
|
_entry ??= Activator.CreateInstance(_currentPage.ModelType);
|
||||||
|
_context = new EditContext(_entry);
|
||||||
|
_validation = new ValidationMessageStore(_context);
|
||||||
|
_validationIdentifiers = new Dictionary<AdminPageProperty, FieldIdentifier>();
|
||||||
|
_context.OnValidationRequested += Validate;
|
||||||
|
|
||||||
|
_values = new Dictionary<AdminPageProperty, object>();
|
||||||
|
foreach (var property in _currentPage.Properties) {
|
||||||
|
_values.Add(property, property.GetValue(_entry));
|
||||||
|
_validationIdentifiers.Add(property, new FieldIdentifier(_entry, property.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
await _modal.ShowAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IList<AdminPageProperty> GetEditableProperties() {
|
||||||
|
return _currentPage.Properties
|
||||||
|
.Where(p => !p.Ignore)
|
||||||
|
.OrderBy(p => p.Editable)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsDisabled(AdminPageProperty prop) => (_isEdit && !prop.Editable) || prop.Generated;
|
||||||
|
private bool IsRequired(AdminPageProperty prop) => !_isEdit ? prop.Required : prop.Required && prop.EditDisplayValue;
|
||||||
|
private bool IsSwitch(AdminPageProperty prop) => prop.Type == typeof(bool);
|
||||||
|
private bool IsListType(AdminPageProperty prop) => IsListType(prop.Type);
|
||||||
|
|
||||||
|
private bool IsListType(Type type) {
|
||||||
|
if (!type.IsGenericType) return false;
|
||||||
|
var generic = type.GenericTypeArguments[0];
|
||||||
|
var gListType = typeof(IList<>).MakeGenericType(generic);
|
||||||
|
var iListType = typeof(List<>).MakeGenericType(generic);
|
||||||
|
return type.IsAssignableFrom(gListType) || type.IsAssignableFrom(iListType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IList<string> GetListPropertyValues(AdminPageProperty prop) {
|
||||||
|
if (!IsListType(prop)) return new List<string>();
|
||||||
|
var list = new List<string>();
|
||||||
|
|
||||||
|
var values = prop.GetValue(_entry);
|
||||||
|
|
||||||
|
if (values is null) {
|
||||||
|
prop.SetValue(_entry, Activator.CreateInstance(prop.Type));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var value in (IEnumerable)values) {
|
||||||
|
list.Add(MapPropertyValue(value, prop));
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetPropertyValue(AdminPageProperty property) {
|
||||||
|
if (!_isEdit) return "";
|
||||||
|
if (!property.EditDisplayValue) return "";
|
||||||
|
return MapPropertyValue(property.GetValue(_entry), property);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string MapPropertyValue(object value, AdminPageProperty property, bool isSubProperty = false) {
|
||||||
|
if (value is null) return string.Empty;
|
||||||
|
var type = value.GetType();
|
||||||
|
|
||||||
|
var page = PageProvider.HasPageFor(type);
|
||||||
|
if (page is not null && !string.IsNullOrWhiteSpace(page.ListingProperty)) {
|
||||||
|
var prop = page.Properties
|
||||||
|
.SingleOrDefault(p => p.Name == page.ListingProperty);
|
||||||
|
|
||||||
|
if (prop is not null) {
|
||||||
|
return MapPropertyValue(prop.GetValue(value), prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(property.DisplayPropertyName) && !isSubProperty) {
|
||||||
|
var prop = type.GetProperties()
|
||||||
|
.SingleOrDefault(p => p.Name == property.DisplayPropertyName);
|
||||||
|
|
||||||
|
return MapPropertyValue(prop?.GetValue(value), property, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stringValue = value.ToString();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(property.Prefix)) {
|
||||||
|
return stringValue?.Replace(property.Prefix, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetInputType(AdminPageProperty property) {
|
||||||
|
if (!property.EditDisplayValue)
|
||||||
|
return "password";
|
||||||
|
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Validate(object sender, ValidationRequestedEventArgs e) {
|
||||||
|
_validation.Clear();
|
||||||
|
foreach (var value in _values) {
|
||||||
|
if (value.Key.Unique) {
|
||||||
|
if (value.Value == value.Key.GetValue(_entry)) continue;
|
||||||
|
var repo = Provider.GetService(_currentPage.RepositoryProvider) as IModelRepository;
|
||||||
|
var data = repo!.ReadAllO().GetAwaiter().GetResult();
|
||||||
|
foreach (var entry in data) {
|
||||||
|
var other = value.Key.GetValue(entry);
|
||||||
|
if (!other.Equals(value.Value)) continue;
|
||||||
|
_validation.Add(_validationIdentifiers[value.Key], $"This {value.Key.DisplayName ?? value.Key.Name} already exists!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.Key.Validator is null) continue;
|
||||||
|
var error = value.Key.Validator?.Invoke(value.Value);
|
||||||
|
if (string.IsNullOrEmpty(error)) continue;
|
||||||
|
_validation.Add(_validationIdentifiers[value.Key], error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteListItem(AdminPageProperty prop, int index) {
|
||||||
|
var list = prop.GetValue<IList>(_entry);
|
||||||
|
list.RemoveAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddListItem(AdminPageProperty prop) {
|
||||||
|
if (!_inputValues.TryGetValue(prop, out var input) || input is null) {
|
||||||
|
Alerts.FireAsync(new SweetAlertOptions {
|
||||||
|
Title = "Error!",
|
||||||
|
Text = "Please enter a value!",
|
||||||
|
Icon = SweetAlertIcon.Error
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var list = prop.GetValue<IList>(_entry);
|
||||||
|
var value = prop.Parser?.Invoke(_entry, input) ?? input;
|
||||||
|
list?.Add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(string, int)[]> SetupSelectorProperty(AdminPageProperty property) {
|
||||||
|
var type = property.SelectorType ?? property.Type;
|
||||||
|
if (IsListType(type)) {
|
||||||
|
type = type.GenericTypeArguments[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
var page = PageProvider.HasPageFor(type);
|
||||||
|
if (page is null) {
|
||||||
|
throw new ArgumentException($"'{property.Name}' cannot be a selector because a admin page for '{type.Name}' does not exist!");
|
||||||
|
}
|
||||||
|
|
||||||
|
var repo = Provider.GetService(page.RepositoryProvider) as IModelRepository;
|
||||||
|
var objects = (await repo!.ReadAllO()).ToArray();
|
||||||
|
_selectorValues[property] = objects;
|
||||||
|
|
||||||
|
var data = new List<(string, int)>();
|
||||||
|
for (var i = 0; i < objects.Length; i++) {
|
||||||
|
data.Add((MapPropertyValue(objects[i], property), i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsIndexSelected(AdminPageProperty property, int index) {
|
||||||
|
var value = property.GetValue(_entry);
|
||||||
|
if (value is null) return false;
|
||||||
|
return _selectorValues[property][index] == value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private object ReadSelectorValue(AdminPageProperty property, object value) {
|
||||||
|
if (!int.TryParse(value.ToString(), out int result)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _selectorValues[property][result];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Save() {
|
||||||
|
if (_isEdit && _currentPage.Permissions.Update is not null) {
|
||||||
|
if (!await Permissions.HasPermission(Auth.User, _currentPage.Permissions.Update)) {
|
||||||
|
await Alerts.FireAsync(new SweetAlertOptions {
|
||||||
|
Title = "Unauthorized!",
|
||||||
|
Text = "You don't have the required permissions to edit an entry!",
|
||||||
|
Icon = SweetAlertIcon.Error
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}else if (_currentPage.Permissions.Create is not null) {
|
||||||
|
if (!await Permissions.HasPermission(Auth.User, _currentPage.Permissions.Create)) {
|
||||||
|
await Alerts.FireAsync(new SweetAlertOptions {
|
||||||
|
Title = "Unauthorized!",
|
||||||
|
Text = "You don't have the required permissions to add an entry!",
|
||||||
|
Icon = SweetAlertIcon.Error
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var value in _values) {
|
||||||
|
if (IsListType(value.Key)) continue;
|
||||||
|
value.Key.SetValue(_entry, value.Key.Parser?.Invoke(_entry, value.Value) ?? Convert.ChangeType(value.Value, value.Key.Type));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_isEdit) {
|
||||||
|
await _repository.CreateO(_entry);
|
||||||
|
|
||||||
|
Alerts.FireAsync(new SweetAlertOptions {
|
||||||
|
Title = "New entry added!",
|
||||||
|
Icon = SweetAlertIcon.Success,
|
||||||
|
ShowConfirmButton = false,
|
||||||
|
Timer = 1500
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await _repository.UpdateO(_entry);
|
||||||
|
|
||||||
|
Alerts.FireAsync(new SweetAlertOptions {
|
||||||
|
Title = "Entry updated!",
|
||||||
|
Icon = SweetAlertIcon.Success,
|
||||||
|
ShowConfirmButton = false,
|
||||||
|
Timer = 1500
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await ReloadDelegate.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
@rendermode InteractiveServer
|
|
||||||
|
|
||||||
@using BlazorStrap
|
|
||||||
@using BlazorStrap.Shared.Components.Modal
|
|
||||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
|
||||||
@using BlazorStrap.V5
|
|
||||||
@using CurrieTechnologies.Razor.SweetAlert2
|
|
||||||
@using HopFrame.Database.Models
|
|
||||||
@using HopFrame.Database.Repositories
|
|
||||||
@using HopFrame.Security.Claims
|
|
||||||
@using HopFrame.Web.Model
|
|
||||||
|
|
||||||
<BSModal DataId="add-group-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" @ref="_modal">
|
|
||||||
<BSForm Model="_group" OnValidSubmit="AddGroup">
|
|
||||||
@if (_isEdit) {
|
|
||||||
<BSModalHeader>Edit group</BSModalHeader>
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
<BSModalHeader>Add group</BSModalHeader>
|
|
||||||
}
|
|
||||||
<BSModalContent>
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSLabel>Name</BSLabel>
|
|
||||||
@if (!_isEdit) {
|
|
||||||
<BSInputGroup>
|
|
||||||
<span class="@BS.Input_Group_Text">group.</span>
|
|
||||||
<BSInput InputType="InputType.Text" @bind-Value="_group.GroupName" required/>
|
|
||||||
</BSInputGroup>
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
<input type="text" class="form-control" disabled value="@_group.Name"/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (_isEdit) {
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSLabel>Created at</BSLabel>
|
|
||||||
<input type="text" class="form-control" disabled value="@_group.CreatedAt"/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSLabel>Description</BSLabel>
|
|
||||||
<BSInput InputType="InputType.TextArea" @bind-Value="_group.Description"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSInputSwitch @bind-Value="_group.IsDefaultGroup" CheckedValue="true" UnCheckedValue="false">
|
|
||||||
Default group
|
|
||||||
</BSInputSwitch>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSLabel>Inherits from</BSLabel>
|
|
||||||
<BSListGroup>
|
|
||||||
<BSListGroupItem>
|
|
||||||
<BSListGroup IsFlush="true">
|
|
||||||
@foreach (var group in _group.Permissions.Where(g => g.PermissionName.StartsWith("group."))) {
|
|
||||||
<BSListGroupItem>
|
|
||||||
<BSButton Color="BSColor.Danger" Size="Size.ExtraSmall" MarginEnd="Margins.Small" OnClick="() => RemovePermission(group)">
|
|
||||||
<HopIconDisplay Type="HopIconDisplay.HopIcon.Cross"/>
|
|
||||||
</BSButton>
|
|
||||||
|
|
||||||
<span>@group.PermissionName.Replace("group.", "")</span>
|
|
||||||
</BSListGroupItem>
|
|
||||||
}
|
|
||||||
</BSListGroup>
|
|
||||||
</BSListGroupItem>
|
|
||||||
<BSListGroupItem>
|
|
||||||
<div style="display: flex; gap: 20px">
|
|
||||||
<BSInput InputType="InputType.Select" @bind-Value="_groupToAdd">
|
|
||||||
<option selected>Select group</option>
|
|
||||||
|
|
||||||
@foreach (var group in _allGroups) {
|
|
||||||
@if (_group.Permissions.All(g => g.PermissionName != group.Name) && group.Name != _group.Name) {
|
|
||||||
<option value="@group.Name">@group.Name.Replace("group.", "")</option>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</BSInput>
|
|
||||||
<BSButton Color="BSColor.Secondary" OnClick="AddInheritanceGroup">Add</BSButton>
|
|
||||||
</div>
|
|
||||||
</BSListGroupItem>
|
|
||||||
</BSListGroup>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSLabel>Permissions</BSLabel>
|
|
||||||
<BSListGroup>
|
|
||||||
<BSListGroupItem>
|
|
||||||
<BSListGroup IsFlush="true">
|
|
||||||
@foreach (var perm in _group.Permissions.Where(perm => !perm.PermissionName.StartsWith("group."))) {
|
|
||||||
<BSListGroupItem>
|
|
||||||
<BSButton Color="BSColor.Danger" Size="Size.ExtraSmall" MarginEnd="Margins.Small" OnClick="() => RemovePermission(perm)">
|
|
||||||
<HopIconDisplay Type="HopIconDisplay.HopIcon.Cross"/>
|
|
||||||
</BSButton>
|
|
||||||
|
|
||||||
<span>@perm.PermissionName</span>
|
|
||||||
</BSListGroupItem>
|
|
||||||
}
|
|
||||||
</BSListGroup>
|
|
||||||
</BSListGroupItem>
|
|
||||||
<BSListGroupItem>
|
|
||||||
<div style="display: flex; gap: 20px">
|
|
||||||
<BSInput InputType="InputType.Text" @bind-Value="_permissionToAdd"/>
|
|
||||||
<BSButton Color="BSColor.Secondary" OnClick="AddPermission">Add</BSButton>
|
|
||||||
</div>
|
|
||||||
</BSListGroupItem>
|
|
||||||
</BSListGroup>
|
|
||||||
</div>
|
|
||||||
</BSModalContent>
|
|
||||||
<BSModalFooter>
|
|
||||||
<BSButton Target="add-group-modal">Cancel</BSButton>
|
|
||||||
<BSButton IsSubmit="true" Color="BSColor.Primary">Save</BSButton>
|
|
||||||
</BSModalFooter>
|
|
||||||
</BSForm>
|
|
||||||
</BSModal>
|
|
||||||
|
|
||||||
@inject IGroupRepository Groups
|
|
||||||
@inject IPermissionRepository Permissions
|
|
||||||
@inject SweetAlertService Alerts
|
|
||||||
@inject ITokenContext Context
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public Func<Task> ReloadPage { get; set; }
|
|
||||||
|
|
||||||
private PermissionGroupAdd _group;
|
|
||||||
|
|
||||||
private BSModalBase _modal;
|
|
||||||
private string _permissionToAdd;
|
|
||||||
private string _groupToAdd;
|
|
||||||
|
|
||||||
private IList<PermissionGroup> _allGroups;
|
|
||||||
|
|
||||||
private bool _isEdit;
|
|
||||||
|
|
||||||
public async Task ShowAsync(PermissionGroup group = null) {
|
|
||||||
_allGroups = await Groups.GetPermissionGroups();
|
|
||||||
|
|
||||||
if (group is not null) {
|
|
||||||
_group = new PermissionGroupAdd {
|
|
||||||
CreatedAt = group.CreatedAt,
|
|
||||||
Description = group.Description,
|
|
||||||
Name = group.Name,
|
|
||||||
IsDefaultGroup = group.IsDefaultGroup,
|
|
||||||
Permissions = group.Permissions
|
|
||||||
};
|
|
||||||
_isEdit = true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
_group = new PermissionGroupAdd {
|
|
||||||
Permissions = new List<Permission>(),
|
|
||||||
IsDefaultGroup = false
|
|
||||||
};
|
|
||||||
_isEdit = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _modal.ShowAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddPermission() {
|
|
||||||
if (string.IsNullOrWhiteSpace(_permissionToAdd)) {
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Enter a permission name!",
|
|
||||||
Icon = SweetAlertIcon.Error,
|
|
||||||
ShowConfirmButton = true
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isEdit) {
|
|
||||||
if (!await Permissions.HasPermission(Context.User, Security.AdminPermissions.EditGroup)) {
|
|
||||||
await NoEditPermissions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Permissions.AddPermission(_group, _permissionToAdd);
|
|
||||||
}
|
|
||||||
|
|
||||||
_group.Permissions.Add(new Permission {
|
|
||||||
PermissionName = _permissionToAdd,
|
|
||||||
GrantedAt = DateTime.Now
|
|
||||||
});
|
|
||||||
|
|
||||||
_permissionToAdd = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RemovePermission(Permission permission) {
|
|
||||||
if (_isEdit) {
|
|
||||||
await Permissions.RemovePermission(_group, permission.PermissionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
_group.Permissions.Remove(permission);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddInheritanceGroup() {
|
|
||||||
if (string.IsNullOrWhiteSpace(_groupToAdd)) {
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Select a group!",
|
|
||||||
Icon = SweetAlertIcon.Error,
|
|
||||||
ShowConfirmButton = true
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isEdit) {
|
|
||||||
if (!await Permissions.HasPermission(Context.User, Security.AdminPermissions.EditGroup)) {
|
|
||||||
await NoEditPermissions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Permissions.AddPermission(_group, _groupToAdd);
|
|
||||||
}
|
|
||||||
|
|
||||||
_group.Permissions.Add(new Permission {
|
|
||||||
PermissionName = _groupToAdd
|
|
||||||
});
|
|
||||||
|
|
||||||
_groupToAdd = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddGroup() {
|
|
||||||
if (_isEdit) {
|
|
||||||
if (!await Permissions.HasPermission(Context.User, Security.AdminPermissions.EditGroup)) {
|
|
||||||
await NoEditPermissions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Groups.EditPermissionGroup(_group);
|
|
||||||
|
|
||||||
if (ReloadPage is not null)
|
|
||||||
await ReloadPage.Invoke();
|
|
||||||
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Group edited!",
|
|
||||||
Icon = SweetAlertIcon.Success,
|
|
||||||
Timer = 1500,
|
|
||||||
ShowConfirmButton = false
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await Permissions.HasPermission(Context.User, Security.AdminPermissions.AddGroup)) {
|
|
||||||
await NoAddPermissions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_allGroups.Any(group => group.Name == _group.Name)) {
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Something went wrong!",
|
|
||||||
Text = "This group already exists!",
|
|
||||||
Icon = SweetAlertIcon.Error,
|
|
||||||
ShowConfirmButton = false,
|
|
||||||
Timer = 1500
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Groups.CreatePermissionGroup(new PermissionGroup {
|
|
||||||
Description = _group.Description,
|
|
||||||
IsDefaultGroup = _group.IsDefaultGroup,
|
|
||||||
Permissions = _group.Permissions,
|
|
||||||
Name = "group." + _group.GroupName
|
|
||||||
});
|
|
||||||
|
|
||||||
if (ReloadPage is not null)
|
|
||||||
await ReloadPage.Invoke();
|
|
||||||
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Group added!",
|
|
||||||
Icon = SweetAlertIcon.Success,
|
|
||||||
Timer = 1500,
|
|
||||||
ShowConfirmButton = false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task NoEditPermissions() {
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Unauthorized!",
|
|
||||||
Text = "You don't have the required permissions to edit a group!",
|
|
||||||
Icon = SweetAlertIcon.Error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task NoAddPermissions() {
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Unauthorized!",
|
|
||||||
Text = "You don't have the required permissions to add a group!",
|
|
||||||
Icon = SweetAlertIcon.Error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
@rendermode InteractiveServer
|
|
||||||
|
|
||||||
@using BlazorStrap
|
|
||||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
|
||||||
@using BlazorStrap.Shared.Components.Modal
|
|
||||||
@using BlazorStrap.V5
|
|
||||||
@using CurrieTechnologies.Razor.SweetAlert2
|
|
||||||
@using HopFrame.Database.Models
|
|
||||||
@using HopFrame.Database.Repositories
|
|
||||||
@using HopFrame.Security.Claims
|
|
||||||
@using HopFrame.Web.Model
|
|
||||||
|
|
||||||
<BSModal DataId="add-user-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" OnShow="() => _user = new()" @ref="_modal">
|
|
||||||
<BSForm Model="_user" OnValidSubmit="AddUser">
|
|
||||||
<BSModalHeader>Add user</BSModalHeader>
|
|
||||||
<BSModalContent>
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSLabel>E-Mail</BSLabel>
|
|
||||||
<BSInput InputType="InputType.Email" @bind-Value="_user.Email" required/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSLabel>Username</BSLabel>
|
|
||||||
<BSInput InputType="InputType.Text" @bind-Value="_user.Username" required/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSLabel>Password</BSLabel>
|
|
||||||
<BSInput InputType="InputType.Password" @bind-Value="_user.Password" required/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSLabel>Primary group</BSLabel>
|
|
||||||
<BSInput InputType="InputType.Select" @bind-Value="_user.Group">
|
|
||||||
<option value="">Select group</option>
|
|
||||||
|
|
||||||
@foreach (var group in _allGroups) {
|
|
||||||
<option value="@group.Name">@group.Name.Replace("group.", "")</option>
|
|
||||||
}
|
|
||||||
</BSInput>
|
|
||||||
</div>
|
|
||||||
</BSModalContent>
|
|
||||||
<BSModalFooter>
|
|
||||||
<BSButton Target="add-user-modal">Cancel</BSButton>
|
|
||||||
<BSButton IsSubmit="true" Color="BSColor.Primary">Save</BSButton>
|
|
||||||
</BSModalFooter>
|
|
||||||
</BSForm>
|
|
||||||
</BSModal>
|
|
||||||
|
|
||||||
@inject IUserRepository Users
|
|
||||||
@inject IPermissionRepository Permissions
|
|
||||||
@inject IGroupRepository Groups
|
|
||||||
@inject SweetAlertService Alerts
|
|
||||||
@inject ITokenContext Auth
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public Func<Task> ReloadPage { get; set; }
|
|
||||||
|
|
||||||
private IList<PermissionGroup> _allGroups = new List<PermissionGroup>();
|
|
||||||
private IList<User> _allUsers = new List<User>();
|
|
||||||
private UserAdd _user;
|
|
||||||
|
|
||||||
private BSModalBase _modal;
|
|
||||||
|
|
||||||
public async Task ShowAsync() {
|
|
||||||
_allGroups = await Groups.GetPermissionGroups();
|
|
||||||
_allUsers = await Users.GetUsers();
|
|
||||||
|
|
||||||
await _modal.ShowAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddUser() {
|
|
||||||
if (!(await Permissions.HasPermission(Auth.User, Security.AdminPermissions.AddUser))) {
|
|
||||||
await NoAddPermissions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string errorMessage = null;
|
|
||||||
|
|
||||||
if (_allUsers.Any(user => user.Username == _user.Username)) {
|
|
||||||
errorMessage = "Username is already taken!";
|
|
||||||
}
|
|
||||||
else if (_allUsers.Any(user => user.Email == _user.Email)) {
|
|
||||||
errorMessage = "E-Mail is already taken!";
|
|
||||||
}
|
|
||||||
else if (!_user.PasswordIsValid) {
|
|
||||||
errorMessage = "The password needs to be at least 8 characters long!";
|
|
||||||
}
|
|
||||||
else if (!_user.EmailIsValid) {
|
|
||||||
errorMessage = "Invalid E-Mail address!";
|
|
||||||
}
|
|
||||||
else if (string.IsNullOrWhiteSpace(_user.Username)) {
|
|
||||||
errorMessage = "You need to set a username!";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(errorMessage)) {
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Something went wrong!",
|
|
||||||
Text = errorMessage,
|
|
||||||
Icon = SweetAlertIcon.Error,
|
|
||||||
ShowConfirmButton = false,
|
|
||||||
Timer = 1500
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = await Users.AddUser(new User {
|
|
||||||
Username = _user.Username,
|
|
||||||
Email = _user.Email,
|
|
||||||
Password = _user.Password
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(_user.Group)) {
|
|
||||||
await Permissions.AddPermission(user, _user.Group);
|
|
||||||
}
|
|
||||||
|
|
||||||
await ReloadPage.Invoke();
|
|
||||||
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "New user added!",
|
|
||||||
Icon = SweetAlertIcon.Success,
|
|
||||||
ShowConfirmButton = false,
|
|
||||||
Timer = 1500
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task NoAddPermissions() {
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Unauthorized!",
|
|
||||||
Text = "You don't have the required permissions to add a user!",
|
|
||||||
Icon = SweetAlertIcon.Error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
@rendermode InteractiveServer
|
|
||||||
|
|
||||||
@using BlazorStrap
|
|
||||||
@using BlazorStrap.Shared.Components.Modal
|
|
||||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
|
||||||
@using BlazorStrap.V5
|
|
||||||
@using CurrieTechnologies.Razor.SweetAlert2
|
|
||||||
@using HopFrame.Database.Models
|
|
||||||
@using HopFrame.Database.Repositories
|
|
||||||
@using HopFrame.Security.Claims
|
|
||||||
@using HopFrame.Web.Model
|
|
||||||
|
|
||||||
<BSModal DataId="edit-user-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" @ref="_modal">
|
|
||||||
<BSForm Model="_user" OnValidSubmit="EditUser">
|
|
||||||
<BSModalHeader>Edit @_user.Username</BSModalHeader>
|
|
||||||
<BSModalContent>
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSLabel>User id</BSLabel>
|
|
||||||
<input type="text" class="form-control" disabled value="@_user.Id"/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSLabel>Created at</BSLabel>
|
|
||||||
<input type="text" class="form-control" disabled value="@_user.CreatedAt"/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSLabel>E-Mail</BSLabel>
|
|
||||||
<BSInput InputType="InputType.Email" @bind-Value="_user.Email" required/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSLabel>Username</BSLabel>
|
|
||||||
<BSInput InputType="InputType.Text" @bind-Value="_user.Username" required/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSLabel>Password</BSLabel>
|
|
||||||
<BSInput InputType="InputType.Password" @bind-Value="_newPassword"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSLabel>Groups</BSLabel>
|
|
||||||
<BSListGroup>
|
|
||||||
<BSListGroupItem>
|
|
||||||
<BSListGroup IsFlush="true">
|
|
||||||
@foreach (var group in _userGroups) {
|
|
||||||
<BSListGroupItem>
|
|
||||||
<BSButton Color="BSColor.Danger" Size="Size.ExtraSmall" MarginEnd="Margins.Small" OnClick="() => RemoveGroup(group)">
|
|
||||||
<HopIconDisplay Type="HopIconDisplay.HopIcon.Cross"/>
|
|
||||||
</BSButton>
|
|
||||||
|
|
||||||
<span>@group.Name.Replace("group.", "")</span>
|
|
||||||
</BSListGroupItem>
|
|
||||||
}
|
|
||||||
</BSListGroup>
|
|
||||||
</BSListGroupItem>
|
|
||||||
<BSListGroupItem>
|
|
||||||
<div style="display: flex; gap: 20px">
|
|
||||||
<BSInput InputType="InputType.Select" @bind-Value="_selectedGroup">
|
|
||||||
<option selected>Select group</option>
|
|
||||||
|
|
||||||
@foreach (var group in _allGroups) {
|
|
||||||
@if (_userGroups?.All(g => g.Name != group.Name) == true) {
|
|
||||||
<option value="@group.Name">@group.Name.Replace("group.", "")</option>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</BSInput>
|
|
||||||
<BSButton Color="BSColor.Secondary" OnClick="AddGroup">Add</BSButton>
|
|
||||||
</div>
|
|
||||||
</BSListGroupItem>
|
|
||||||
</BSListGroup>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<BSLabel>Permissions</BSLabel>
|
|
||||||
<BSListGroup>
|
|
||||||
<BSListGroupItem>
|
|
||||||
<BSListGroup IsFlush="true">
|
|
||||||
@foreach (var perm in _user.Permissions.Where(perm => !perm.PermissionName.StartsWith("group."))) {
|
|
||||||
<BSListGroupItem>
|
|
||||||
<BSButton Color="BSColor.Danger" Size="Size.ExtraSmall" MarginEnd="Margins.Small" OnClick="() => RemovePermission(perm)">
|
|
||||||
<HopIconDisplay Type="HopIconDisplay.HopIcon.Cross"/>
|
|
||||||
</BSButton>
|
|
||||||
|
|
||||||
<span>@perm.PermissionName</span>
|
|
||||||
</BSListGroupItem>
|
|
||||||
}
|
|
||||||
</BSListGroup>
|
|
||||||
</BSListGroupItem>
|
|
||||||
<BSListGroupItem>
|
|
||||||
<div style="display: flex; gap: 20px">
|
|
||||||
<BSInput InputType="InputType.Text" @bind-Value="_permissionToAdd"/>
|
|
||||||
<BSButton Color="BSColor.Secondary" OnClick="AddPermission">Add</BSButton>
|
|
||||||
</div>
|
|
||||||
</BSListGroupItem>
|
|
||||||
</BSListGroup>
|
|
||||||
</div>
|
|
||||||
</BSModalContent>
|
|
||||||
<BSModalFooter>
|
|
||||||
<BSButton Target="edit-user-modal">Cancel</BSButton>
|
|
||||||
<BSButton IsSubmit="true" Color="BSColor.Primary">Save</BSButton>
|
|
||||||
</BSModalFooter>
|
|
||||||
</BSForm>
|
|
||||||
</BSModal>
|
|
||||||
|
|
||||||
@inject IUserRepository Users
|
|
||||||
@inject IPermissionRepository Permissions
|
|
||||||
@inject IGroupRepository Groups
|
|
||||||
@inject SweetAlertService Alerts
|
|
||||||
@inject ITokenContext Auth
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public Func<Task> ReloadPage { get; set; }
|
|
||||||
|
|
||||||
private BSModalBase _modal;
|
|
||||||
private User _user;
|
|
||||||
private string _newPassword;
|
|
||||||
|
|
||||||
private IList<PermissionGroup> _userGroups;
|
|
||||||
private IList<PermissionGroup> _allGroups;
|
|
||||||
private string _selectedGroup;
|
|
||||||
private string _permissionToAdd;
|
|
||||||
|
|
||||||
public async Task ShowAsync(User user) {
|
|
||||||
if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) {
|
|
||||||
await NoEditPermissions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_user = user;
|
|
||||||
_userGroups = await Groups.GetUserGroups(user);
|
|
||||||
_allGroups = await Groups.GetPermissionGroups();
|
|
||||||
await _modal.ShowAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddGroup() {
|
|
||||||
if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) {
|
|
||||||
await NoEditPermissions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_selectedGroup)) {
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Select a group!",
|
|
||||||
Icon = SweetAlertIcon.Error,
|
|
||||||
ShowConfirmButton = true
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var group = _allGroups.SingleOrDefault(group => group.Name == _selectedGroup);
|
|
||||||
|
|
||||||
await Permissions.AddPermission(_user, group?.Name);
|
|
||||||
_userGroups.Add(group);
|
|
||||||
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Group added!",
|
|
||||||
Icon = SweetAlertIcon.Success,
|
|
||||||
Timer = 1500,
|
|
||||||
ShowConfirmButton = false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RemoveGroup(PermissionGroup group) {
|
|
||||||
if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) {
|
|
||||||
await NoEditPermissions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Are you sure?",
|
|
||||||
Icon = SweetAlertIcon.Warning,
|
|
||||||
ConfirmButtonText = "Yes",
|
|
||||||
ShowCancelButton = true,
|
|
||||||
ShowConfirmButton = true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.IsConfirmed) {
|
|
||||||
await Permissions.RemovePermission(_user, group.Name);
|
|
||||||
_userGroups.Remove(group);
|
|
||||||
StateHasChanged();
|
|
||||||
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Group removed!",
|
|
||||||
Icon = SweetAlertIcon.Success,
|
|
||||||
Timer = 1500,
|
|
||||||
ShowConfirmButton = false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddPermission() {
|
|
||||||
if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) {
|
|
||||||
await NoEditPermissions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_permissionToAdd)) {
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Enter a permission name!",
|
|
||||||
Icon = SweetAlertIcon.Error,
|
|
||||||
ShowConfirmButton = true
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_user.Permissions.Add(await Permissions.AddPermission(_user, _permissionToAdd));
|
|
||||||
_permissionToAdd = "";
|
|
||||||
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Permission added!",
|
|
||||||
Icon = SweetAlertIcon.Success,
|
|
||||||
Timer = 1500,
|
|
||||||
ShowConfirmButton = false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RemovePermission(Permission perm) {
|
|
||||||
if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) {
|
|
||||||
await NoEditPermissions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Are you sure?",
|
|
||||||
Icon = SweetAlertIcon.Warning,
|
|
||||||
ConfirmButtonText = "Yes",
|
|
||||||
ShowCancelButton = true,
|
|
||||||
ShowConfirmButton = true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.IsConfirmed) {
|
|
||||||
await Permissions.RemovePermission(perm.User, perm.PermissionName);
|
|
||||||
_user.Permissions.Remove(perm);
|
|
||||||
StateHasChanged();
|
|
||||||
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Permission removed!",
|
|
||||||
Icon = SweetAlertIcon.Success,
|
|
||||||
Timer = 1500,
|
|
||||||
ShowConfirmButton = false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void EditUser() {
|
|
||||||
if (!await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditUser)) {
|
|
||||||
await NoEditPermissions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string errorMessage = null;
|
|
||||||
var validator = new RegisterData {
|
|
||||||
Password = _newPassword,
|
|
||||||
Email = _user.Email
|
|
||||||
};
|
|
||||||
|
|
||||||
var allUsers = await Users.GetUsers();
|
|
||||||
|
|
||||||
if (allUsers.Any(user => user.Username == _user.Username && user.Id != _user.Id)) {
|
|
||||||
errorMessage = "Username is already taken!";
|
|
||||||
}
|
|
||||||
else if (allUsers.Any(user => user.Email == _user.Email && user.Id != _user.Id)) {
|
|
||||||
errorMessage = "E-Mail is already taken!";
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrWhiteSpace(_newPassword) && !validator.PasswordIsValid) {
|
|
||||||
errorMessage = "The password needs to be at least 8 characters long!";
|
|
||||||
}
|
|
||||||
else if (!validator.EmailIsValid) {
|
|
||||||
errorMessage = "Invalid E-Mail address!";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(errorMessage)) {
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Something went wrong!",
|
|
||||||
Text = errorMessage,
|
|
||||||
Icon = SweetAlertIcon.Error,
|
|
||||||
ShowConfirmButton = false,
|
|
||||||
Timer = 1500
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Users.UpdateUser(_user);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(_newPassword)) {
|
|
||||||
await Users.ChangePassword(_user, _newPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ReloadPage is not null)
|
|
||||||
await ReloadPage.Invoke();
|
|
||||||
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "User edited!",
|
|
||||||
Icon = SweetAlertIcon.Success,
|
|
||||||
Timer = 1500,
|
|
||||||
ShowConfirmButton = false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task NoEditPermissions() {
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Unauthorized!",
|
|
||||||
Text = "You don't have the required permissions to edit a user!",
|
|
||||||
Icon = SweetAlertIcon.Error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
88
src/HopFrame.Web/HopAdminContext.cs
Normal file
88
src/HopFrame.Web/HopAdminContext.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using HopFrame.Database.Models;
|
||||||
|
using HopFrame.Security;
|
||||||
|
using HopFrame.Web.Admin;
|
||||||
|
using HopFrame.Web.Admin.Generators;
|
||||||
|
using HopFrame.Web.Admin.Models;
|
||||||
|
using HopFrame.Web.Repositories;
|
||||||
|
|
||||||
|
namespace HopFrame.Web;
|
||||||
|
|
||||||
|
public class HopAdminContext : AdminPagesContext {
|
||||||
|
|
||||||
|
public AdminPage<User> Users { get; set; }
|
||||||
|
public AdminPage<PermissionGroup> Groups { get; set; }
|
||||||
|
|
||||||
|
public override void OnModelCreating(IAdminContextGenerator generator) {
|
||||||
|
generator.Page<User>()
|
||||||
|
.Description("On this page you can manage all user accounts.")
|
||||||
|
.ConfigureRepository<UserProvider>()
|
||||||
|
.ViewPermission(AdminPermissions.ViewUsers)
|
||||||
|
.CreatePermission(AdminPermissions.AddUser)
|
||||||
|
.UpdatePermission(AdminPermissions.EditUser)
|
||||||
|
.DeletePermission(AdminPermissions.DeleteUser);
|
||||||
|
|
||||||
|
generator.Page<User>().Property(u => u.Password)
|
||||||
|
.DisplayInListing(false)
|
||||||
|
.DisplayValueWhileEditing(false)
|
||||||
|
.Validator(passwd => passwd.Length >= 8 ? null : "The password needs to be at least 8 characters long!");
|
||||||
|
|
||||||
|
generator.Page<User>().Property(u => u.Email)
|
||||||
|
.Validator(email => Regex.Match(email, @"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$").Success ? null : "Invalid E-Mail address!")
|
||||||
|
.Unique();
|
||||||
|
|
||||||
|
generator.Page<User>().Property(u => u.Username)
|
||||||
|
.Validator(uname => uname.Length >= 4 ? null : "The username needs to be at least 4 characters long!")
|
||||||
|
.Unique();
|
||||||
|
|
||||||
|
generator.Page<User>().Property(u => u.CreatedAt)
|
||||||
|
.Editable(false);
|
||||||
|
|
||||||
|
generator.Page<User>().Property(u => u.Permissions)
|
||||||
|
.DisplayInListing(false)
|
||||||
|
.DisplayProperty<Permission>(p => p.PermissionName)
|
||||||
|
.Parser<string, Permission>((user, perm) => new Permission {
|
||||||
|
GrantedAt = DateTime.Now,
|
||||||
|
PermissionName = perm,
|
||||||
|
User = user
|
||||||
|
});
|
||||||
|
|
||||||
|
generator.Page<User>().Property(u => u.CreatedAt)
|
||||||
|
.Generated();
|
||||||
|
|
||||||
|
generator.Page<User>().Property(u => u.Id)
|
||||||
|
.Generated();
|
||||||
|
|
||||||
|
generator.Page<User>().Property(u => u.Tokens)
|
||||||
|
.Ignore();
|
||||||
|
|
||||||
|
|
||||||
|
generator.Page<PermissionGroup>()
|
||||||
|
.Description("On this page you can view, create, edit and delete permission groups.")
|
||||||
|
.ConfigureRepository<GroupProvider>()
|
||||||
|
.ViewPermission(AdminPermissions.ViewGroups)
|
||||||
|
.CreatePermission(AdminPermissions.AddGroup)
|
||||||
|
.UpdatePermission(AdminPermissions.EditGroup)
|
||||||
|
.DeletePermission(AdminPermissions.DeleteGroup)
|
||||||
|
.ListingProperty(g => g.Name);
|
||||||
|
|
||||||
|
generator.Page<PermissionGroup>().Property(g => g.Name)
|
||||||
|
.Prefix("group.");
|
||||||
|
|
||||||
|
generator.Page<PermissionGroup>().Property(g => g.IsDefaultGroup)
|
||||||
|
.DisplayName("Default Group")
|
||||||
|
.Sortable(false);
|
||||||
|
|
||||||
|
generator.Page<PermissionGroup>().Property(g => g.CreatedAt)
|
||||||
|
.Generated();
|
||||||
|
|
||||||
|
generator.Page<PermissionGroup>().Property(g => g.Permissions)
|
||||||
|
.DisplayInListing(false)
|
||||||
|
.DisplayProperty<Permission>(p => p.PermissionName)
|
||||||
|
.Parser<string, Permission>((group, perm) => new Permission {
|
||||||
|
GrantedAt = DateTime.Now,
|
||||||
|
PermissionName = perm,
|
||||||
|
Group = group
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\HopFrame.Database\HopFrame.Database.csproj" />
|
<ProjectReference Include="..\HopFrame.Database\HopFrame.Database.csproj" />
|
||||||
<ProjectReference Include="..\HopFrame.Security\HopFrame.Security.csproj" />
|
<ProjectReference Include="..\HopFrame.Security\HopFrame.Security.csproj" />
|
||||||
|
<ProjectReference Include="..\HopFrame.Web.Admin\HopFrame.Web.Admin.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace HopFrame.Web.Model;
|
|
||||||
|
|
||||||
public sealed class NavigationItem {
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Url { get; set; }
|
|
||||||
public string Permission { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
using HopFrame.Database.Models;
|
|
||||||
|
|
||||||
namespace HopFrame.Web.Model;
|
|
||||||
|
|
||||||
internal sealed class PermissionGroupAdd : PermissionGroup {
|
|
||||||
public string GroupName { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using HopFrame.Security.Models;
|
|
||||||
|
|
||||||
namespace HopFrame.Web.Model;
|
|
||||||
|
|
||||||
internal class RegisterData : UserRegister {
|
|
||||||
public string RepeatedPassword { get; set; }
|
|
||||||
|
|
||||||
public bool PasswordsMatch => Password == RepeatedPassword;
|
|
||||||
public bool PasswordIsValid => Password?.Length >= 8;
|
|
||||||
public bool EmailIsValid => Email?.Contains('@') == true && Email?.Contains('.') == true && Email?.EndsWith('.') == false;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
namespace HopFrame.Web.Model;
|
|
||||||
|
|
||||||
internal sealed class UserAdd : RegisterData {
|
|
||||||
public string Group { get; set; }
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
@using BlazorStrap
|
@using BlazorStrap
|
||||||
@using HopFrame.Web.Pages.Administration.Layout
|
@using HopFrame.Web.Pages.Administration.Layout
|
||||||
@using BlazorStrap.V5
|
@using BlazorStrap.V5
|
||||||
|
@using HopFrame.Web.Admin.Providers
|
||||||
@using HopFrame.Web.Components
|
@using HopFrame.Web.Components
|
||||||
@using Microsoft.AspNetCore.Components.Web
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
@layout AdminLayout
|
@layout AdminLayout
|
||||||
@@ -13,15 +14,15 @@
|
|||||||
|
|
||||||
<BSContainer>
|
<BSContainer>
|
||||||
<BSRow Justify="Justify.Center">
|
<BSRow Justify="Justify.Center">
|
||||||
@foreach (var view in AdminMenu.Subpages) {
|
@foreach (var adminPage in Pages.LoadRegisteredAdminPages()) {
|
||||||
<AuthorizedView Permission="@view.Permission">
|
<AuthorizedView Permission="@adminPage.Permissions.View">
|
||||||
<BSCol Column="4" style="margin-bottom: 10px">
|
<BSCol Column="4" style="margin-bottom: 10px">
|
||||||
<BSCard CardType="CardType.Card" Color="BSColor.Dark" style="min-height: 200px">
|
<BSCard CardType="CardType.Card" Color="BSColor.Dark" style="min-height: 200px">
|
||||||
<BSCard CardType="CardType.Body" style="display: flex; flex-direction: column">
|
<BSCard CardType="CardType.Body" style="display: flex; flex-direction: column">
|
||||||
<BSCard CardType="CardType.Title">@view.Name</BSCard>
|
<BSCard CardType="CardType.Title">@adminPage.Title</BSCard>
|
||||||
<BSCard CardType="CardType.Subtitle"><span style="color: gray">@view.Permission</span></BSCard>
|
<BSCard CardType="CardType.Subtitle"><span style="color: gray">@adminPage.Permissions.View</span></BSCard>
|
||||||
<BSCard CardType="CardType.Text">@view.Description</BSCard>
|
<BSCard CardType="CardType.Text">@adminPage.Description</BSCard>
|
||||||
<BSButton IsOutlined="true" MarginTop="Margins.Auto" style="width: max-content; align-self: center" OnClick="() => Navigator.NavigateTo(view.Url, true)" Color="BSColor.Light">Open</BSButton>
|
<BSButton IsOutlined="true" MarginTop="Margins.Auto" style="width: max-content; align-self: center" OnClick="() => NavigateTo(adminPage.Url)" Color="BSColor.Light">Open</BSButton>
|
||||||
</BSCard>
|
</BSCard>
|
||||||
</BSCard>
|
</BSCard>
|
||||||
</BSCol>
|
</BSCol>
|
||||||
@@ -31,3 +32,12 @@
|
|||||||
</BSContainer>
|
</BSContainer>
|
||||||
|
|
||||||
@inject NavigationManager Navigator
|
@inject NavigationManager Navigator
|
||||||
|
@inject IAdminPagesProvider Pages
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
public void NavigateTo(string url) {
|
||||||
|
Navigator.NavigateTo("administration/" + url, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
246
src/HopFrame.Web/Pages/Administration/AdminPageList.razor
Normal file
246
src/HopFrame.Web/Pages/Administration/AdminPageList.razor
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
@page "/administration/{url}"
|
||||||
|
@layout AdminLayout
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
@using System.ComponentModel
|
||||||
|
@using BlazorStrap
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using HopFrame.Web.Admin.Models
|
||||||
|
@using HopFrame.Web.Admin.Providers
|
||||||
|
@using HopFrame.Web.Pages.Administration.Layout
|
||||||
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
|
@using HopFrame.Web.Components.Administration
|
||||||
|
@using BlazorStrap.V5
|
||||||
|
@using CurrieTechnologies.Razor.SweetAlert2
|
||||||
|
@using HopFrame.Database.Repositories
|
||||||
|
@using HopFrame.Security.Claims
|
||||||
|
@using HopFrame.Web.Admin
|
||||||
|
@using HopFrame.Web.Components
|
||||||
|
|
||||||
|
<PageTitle>@_pageData.Title</PageTitle>
|
||||||
|
<AuthorizedView Permission="@_pageData.Permissions.View" RedirectIfUnauthorized="administration/login" />
|
||||||
|
|
||||||
|
<AdminPageModal ReloadDelegate="Reload" @ref="_modal"/>
|
||||||
|
|
||||||
|
<div class="title">
|
||||||
|
<h3>
|
||||||
|
@_pageData.Title administration
|
||||||
|
<span class="reload" @onclick="Reload">
|
||||||
|
<HopIconDisplay Type="HopIconDisplay.HopIcon.Reload"/>
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="d-flex" role="search" id="search">
|
||||||
|
<input class="form-control me-2 input-dark" type="search" placeholder="Search" aria-label="Search" @oninput="TriggerSearch">
|
||||||
|
</div>
|
||||||
|
<AuthorizedView Permission="@Security.AdminPermissions.AddGroup">
|
||||||
|
<BSButton IsSubmit="false" Color="BSColor.Success" @onclick="Create">Add Entry</BSButton>
|
||||||
|
</AuthorizedView>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BSTable IsStriped="true" IsHoverable="true" IsDark="true" Color="BSColor.Dark">
|
||||||
|
<BSTHead>
|
||||||
|
<BSTR>
|
||||||
|
@foreach (var prop in GetListingProperties()) {
|
||||||
|
<BSTD>
|
||||||
|
@if (prop.Sortable) {
|
||||||
|
<span class="sorter" @onclick="() => OrderBy(prop.Name)">@prop.DisplayName</span>
|
||||||
|
@if (_currentSortProperty == prop.Name) {
|
||||||
|
<HopIconDisplay Type="_currentSortDirection == ListSortDirection.Descending ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
@prop.DisplayName
|
||||||
|
}
|
||||||
|
</BSTD>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (_hasEditPermission || _hasDeletePermission) {
|
||||||
|
<BSTD>Actions</BSTD>
|
||||||
|
}
|
||||||
|
</BSTR>
|
||||||
|
</BSTHead>
|
||||||
|
|
||||||
|
<BSTBody>
|
||||||
|
@foreach (var entry in _displayedModels) {
|
||||||
|
<BSTR>
|
||||||
|
@foreach (var prop in GetListingProperties()) {
|
||||||
|
@if (prop.Bold) {
|
||||||
|
<BSTD Class="bold">
|
||||||
|
@GetPrintableValue(entry, prop)
|
||||||
|
</BSTD>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<BSTD>
|
||||||
|
@GetPrintableValue(entry, prop)
|
||||||
|
</BSTD>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (_hasEditPermission || _hasDeletePermission) {
|
||||||
|
<BSTD>
|
||||||
|
<BSButtonGroup>
|
||||||
|
@if (_hasEditPermission) {
|
||||||
|
<BSButton Color="BSColor.Warning" OnClick="() => Edit(entry)">Edit</BSButton>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (_hasDeletePermission) {
|
||||||
|
<BSButton Color="BSColor.Danger" OnClick="() => Delete(entry)">Delete</BSButton>
|
||||||
|
}
|
||||||
|
</BSButtonGroup>
|
||||||
|
</BSTD>
|
||||||
|
}
|
||||||
|
</BSTR>
|
||||||
|
}
|
||||||
|
</BSTBody>
|
||||||
|
</BSTable>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
@inject IAdminPagesProvider Pages
|
||||||
|
@inject IServiceProvider Provider
|
||||||
|
@inject ITokenContext Auth
|
||||||
|
@inject IPermissionRepository Permissions
|
||||||
|
@inject SweetAlertService Alerts
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public string Url { get; set; }
|
||||||
|
|
||||||
|
private AdminPage _pageData;
|
||||||
|
private IModelRepository _modelRepository;
|
||||||
|
private IEnumerable<object> _modelBuffer;
|
||||||
|
private AdminPageModal _modal;
|
||||||
|
|
||||||
|
private bool _hasEditPermission;
|
||||||
|
private bool _hasDeletePermission;
|
||||||
|
|
||||||
|
private string _currentSortProperty;
|
||||||
|
private ListSortDirection _currentSortDirection;
|
||||||
|
private DateTime _lastSearch;
|
||||||
|
private IList<object> _displayedModels;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() {
|
||||||
|
_pageData = Pages.LoadAdminPage(Url);
|
||||||
|
|
||||||
|
_currentSortProperty = _pageData.DefaultSortPropertyName;
|
||||||
|
_currentSortDirection = _pageData.DefaultSortDirection;
|
||||||
|
|
||||||
|
if (_pageData.RepositoryProvider is null)
|
||||||
|
throw new ArgumentException($"AdminPage '{_pageData.Title}' does not specify a model repository!'");
|
||||||
|
_modelRepository = Provider.GetService(_pageData.RepositoryProvider) as IModelRepository;
|
||||||
|
|
||||||
|
_hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Update);
|
||||||
|
_hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Delete);
|
||||||
|
|
||||||
|
await Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IList<AdminPageProperty> GetListingProperties() {
|
||||||
|
return _pageData.Properties
|
||||||
|
.Where(p => p.Ignore == false)
|
||||||
|
.Where(p => p.DisplayInListing)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Reload() {
|
||||||
|
_modelBuffer = await _modelRepository.ReadAllO();
|
||||||
|
_displayedModels = _modelBuffer.ToList();
|
||||||
|
|
||||||
|
_currentSortDirection = _pageData.DefaultSortDirection;
|
||||||
|
OrderBy(_pageData.DefaultSortPropertyName, false);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OrderBy(string property, bool changeDir = true) {
|
||||||
|
if (_currentSortProperty == property && changeDir)
|
||||||
|
_currentSortDirection = (ListSortDirection)(((int)_currentSortDirection + 1) % 2);
|
||||||
|
if (_currentSortProperty != property)
|
||||||
|
_currentSortDirection = ListSortDirection.Ascending;
|
||||||
|
|
||||||
|
var prop = GetListingProperties()
|
||||||
|
.SingleOrDefault(p => p.Name == property);
|
||||||
|
var comparer = Comparer<object>.Create((x, y) => {
|
||||||
|
if (prop?.Type == typeof(DateTime)) {
|
||||||
|
DateTime dateX = (DateTime) x.GetType().GetProperty(prop.Name)?.GetValue(x)!;
|
||||||
|
DateTime dateY = (DateTime) y.GetType().GetProperty(prop.Name)?.GetValue(y)!;
|
||||||
|
|
||||||
|
return DateTime.Compare(dateX, dateY);
|
||||||
|
}
|
||||||
|
|
||||||
|
var propX = GetPrintableValue(x, prop);
|
||||||
|
var propY = GetPrintableValue(y, prop);
|
||||||
|
|
||||||
|
return String.CompareOrdinal(propX, propY);
|
||||||
|
});
|
||||||
|
|
||||||
|
_displayedModels = _currentSortDirection == ListSortDirection.Ascending ? _displayedModels.Order(comparer).ToList() : _displayedModels.OrderDescending(comparer).ToList();
|
||||||
|
|
||||||
|
_currentSortProperty = property;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TriggerSearch(ChangeEventArgs e) {
|
||||||
|
var search = ((string)e.Value)?.Trim();
|
||||||
|
Search(search);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Search(string search) {
|
||||||
|
_lastSearch = DateTime.Now;
|
||||||
|
await Task.Delay(500);
|
||||||
|
var timeSinceLastKeyPress = DateTime.Now - _lastSearch;
|
||||||
|
if (timeSinceLastKeyPress < TimeSpan.FromMilliseconds(500)) return;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(search)) {
|
||||||
|
_displayedModels = _modelBuffer.ToList();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var props = GetListingProperties();
|
||||||
|
|
||||||
|
_displayedModels = _modelBuffer
|
||||||
|
.Where(model => props.Any(prop => GetPrintableValue(model, prop).Contains(search)))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
OrderBy(_currentSortProperty, false);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetPrintableValue(object element, AdminPageProperty prop) {
|
||||||
|
return _modal?.MapPropertyValue(prop.GetValue(element), prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Create() {
|
||||||
|
await _modal.Show(_pageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Edit(object entry) {
|
||||||
|
await _modal.Show(_pageData, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Delete(object entry) {
|
||||||
|
var result = await Alerts.FireAsync(new SweetAlertOptions {
|
||||||
|
Title = "Are you sure?",
|
||||||
|
Text = "You won't be able to revert this!",
|
||||||
|
Icon = SweetAlertIcon.Warning,
|
||||||
|
ConfirmButtonText = "Yes",
|
||||||
|
ShowCancelButton = true,
|
||||||
|
ShowConfirmButton = true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.IsConfirmed) {
|
||||||
|
await _modelRepository.DeleteO(entry);
|
||||||
|
await Reload();
|
||||||
|
|
||||||
|
await Alerts.FireAsync(new SweetAlertOptions {
|
||||||
|
Title = "Deleted!",
|
||||||
|
Icon = SweetAlertIcon.Success,
|
||||||
|
Timer = 1500,
|
||||||
|
ShowConfirmButton = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
th, h3 {
|
th, h3, .sorter {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +20,3 @@ h3 {
|
|||||||
.reload, .sorter {
|
.reload, .sorter {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bold {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
@page "/administration/groups"
|
|
||||||
@rendermode InteractiveServer
|
|
||||||
@layout AdminLayout
|
|
||||||
|
|
||||||
@using System.Globalization
|
|
||||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
|
||||||
@using BlazorStrap
|
|
||||||
@using Microsoft.AspNetCore.Components.Web
|
|
||||||
@using HopFrame.Web.Components
|
|
||||||
@using HopFrame.Web.Components.Administration
|
|
||||||
@using BlazorStrap.V5
|
|
||||||
@using CurrieTechnologies.Razor.SweetAlert2
|
|
||||||
@using HopFrame.Database.Models
|
|
||||||
@using HopFrame.Database.Repositories
|
|
||||||
@using HopFrame.Security.Claims
|
|
||||||
@using HopFrame.Web.Pages.Administration.Layout
|
|
||||||
|
|
||||||
<PageTitle>Groups</PageTitle>
|
|
||||||
<AuthorizedView Permission="@Security.AdminPermissions.ViewGroups" RedirectIfUnauthorized="administration/login?redirect=/administration/groups"/>
|
|
||||||
|
|
||||||
<GroupAddModal ReloadPage="Reload" @ref="_groupAddModal"/>
|
|
||||||
|
|
||||||
<div class="title">
|
|
||||||
<h3>
|
|
||||||
Groups administration
|
|
||||||
<span class="reload" @onclick="Reload">
|
|
||||||
<HopIconDisplay Type="HopIconDisplay.HopIcon.Reload"/>
|
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<form class="d-flex" role="search" id="search" @onsubmit="Search">
|
|
||||||
<input class="form-control me-2 input-dark" type="search" placeholder="Search" aria-label="Search" @bind="_searchText">
|
|
||||||
<BSButton Color="BSColor.Success" IsOutlined="true" type="submit">Search</BSButton>
|
|
||||||
</form>
|
|
||||||
<AuthorizedView Permission="@Security.AdminPermissions.AddGroup">
|
|
||||||
<BSButton IsSubmit="false" Color="BSColor.Success" Target="add-user" OnClick="() => _groupAddModal.ShowAsync()">Add Group</BSButton>
|
|
||||||
</AuthorizedView>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<BSTable IsStriped="true" IsHoverable="true" IsDark="true" Color="BSColor.Dark">
|
|
||||||
<BSTHead>
|
|
||||||
<BSTR>
|
|
||||||
<BSTD>
|
|
||||||
<span class="sorter" @onclick="() => OrderBy(OrderType.Name)">Name</span>
|
|
||||||
@if (_currentOrder == OrderType.Name) {
|
|
||||||
<HopIconDisplay Type="_currentOrderDirection == OrderDirection.Desc ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
|
|
||||||
}
|
|
||||||
</BSTD>
|
|
||||||
<BSTD>Description</BSTD>
|
|
||||||
<BSTD>Default</BSTD>
|
|
||||||
<BSTD>
|
|
||||||
<span class="sorter" @onclick="() => OrderBy(OrderType.Created)">Created</span>
|
|
||||||
@if (_currentOrder == OrderType.Created) {
|
|
||||||
<HopIconDisplay Type="_currentOrderDirection == OrderDirection.Desc ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
|
|
||||||
}
|
|
||||||
</BSTD>
|
|
||||||
|
|
||||||
@if (_hasEditPrivileges || _hasDeletePrivileges) {
|
|
||||||
<BSTD>Actions</BSTD>
|
|
||||||
}
|
|
||||||
</BSTR>
|
|
||||||
</BSTHead>
|
|
||||||
|
|
||||||
<BSTBody>
|
|
||||||
@foreach (var group in _groups) {
|
|
||||||
<BSTR>
|
|
||||||
<BSTD Class="bold">@group.Name.Replace("group.", "")</BSTD>
|
|
||||||
<BSTD>@group.Description</BSTD>
|
|
||||||
<BSTD>
|
|
||||||
@if (group.IsDefaultGroup) {
|
|
||||||
<span>Yes</span>
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
<span>No</span>
|
|
||||||
}
|
|
||||||
</BSTD>
|
|
||||||
<BSTD>@group.CreatedAt</BSTD>
|
|
||||||
|
|
||||||
@if (_hasEditPrivileges || _hasDeletePrivileges) {
|
|
||||||
<BSTD>
|
|
||||||
<BSButtonGroup>
|
|
||||||
@if (_hasEditPrivileges) {
|
|
||||||
<BSButton Color="BSColor.Warning" OnClick="() => _groupAddModal.ShowAsync(group)">Edit</BSButton>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (_hasDeletePrivileges) {
|
|
||||||
<BSButton Color="BSColor.Danger" OnClick="() => Delete(group)">Delete</BSButton>
|
|
||||||
}
|
|
||||||
</BSButtonGroup>
|
|
||||||
</BSTD>
|
|
||||||
}
|
|
||||||
</BSTR>
|
|
||||||
}
|
|
||||||
</BSTBody>
|
|
||||||
</BSTable>
|
|
||||||
|
|
||||||
@inject IGroupRepository Groups
|
|
||||||
@inject IPermissionRepository Permissions
|
|
||||||
@inject ITokenContext Auth
|
|
||||||
@inject SweetAlertService Alerts
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private IList<PermissionGroup> _groups = new List<PermissionGroup>();
|
|
||||||
|
|
||||||
private bool _hasEditPrivileges = false;
|
|
||||||
private bool _hasDeletePrivileges = false;
|
|
||||||
private string _searchText;
|
|
||||||
private OrderType _currentOrder = OrderType.None;
|
|
||||||
private OrderDirection _currentOrderDirection = OrderDirection.Asc;
|
|
||||||
|
|
||||||
private GroupAddModal _groupAddModal;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
|
||||||
_groups = await Groups.GetPermissionGroups();
|
|
||||||
|
|
||||||
_hasEditPrivileges = await Permissions.HasPermission(Auth.User, Security.AdminPermissions.EditGroup);
|
|
||||||
_hasDeletePrivileges = await Permissions.HasPermission(Auth.User, Security.AdminPermissions.DeleteGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Reload() {
|
|
||||||
_groups = new List<PermissionGroup>();
|
|
||||||
|
|
||||||
_groups = await Groups.GetPermissionGroups();
|
|
||||||
|
|
||||||
OrderBy(_currentOrder, false);
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Search() {
|
|
||||||
var groups = await Groups.GetPermissionGroups();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(_searchText)) {
|
|
||||||
groups = groups
|
|
||||||
.Where(group => group.Name.Contains(_searchText) ||
|
|
||||||
group.Description?.Contains(_searchText) == true ||
|
|
||||||
group.CreatedAt.ToString(CultureInfo.InvariantCulture).Contains(_searchText) ||
|
|
||||||
group.Permissions.Any(perm => perm.PermissionName.Contains(_searchText)))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
_groups = groups;
|
|
||||||
OrderBy(_currentOrder, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OrderBy(OrderType type, bool changeDir = true) {
|
|
||||||
if (_currentOrder == type && changeDir) _currentOrderDirection = (OrderDirection)(((byte)_currentOrderDirection + 1) % 2);
|
|
||||||
if (_currentOrder != type) _currentOrderDirection = OrderDirection.Asc;
|
|
||||||
|
|
||||||
if (type == OrderType.Name) {
|
|
||||||
_groups = _currentOrderDirection == OrderDirection.Asc ? _groups.OrderBy(group => group.Name).ToList() : _groups.OrderByDescending(group => group.Name).ToList();
|
|
||||||
}
|
|
||||||
else if (type == OrderType.Created) {
|
|
||||||
_groups = _currentOrderDirection == OrderDirection.Asc ? _groups.OrderBy(group => group.CreatedAt).ToList() : _groups.OrderByDescending(group => group.CreatedAt).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentOrder = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Delete(PermissionGroup group) {
|
|
||||||
var result = await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Are you sure?",
|
|
||||||
Text = "You won't be able to revert this!",
|
|
||||||
Icon = SweetAlertIcon.Warning,
|
|
||||||
ConfirmButtonText = "Yes",
|
|
||||||
ShowCancelButton = true,
|
|
||||||
ShowConfirmButton = true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.IsConfirmed) {
|
|
||||||
await Groups.DeletePermissionGroup(group);
|
|
||||||
await Reload();
|
|
||||||
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Deleted!",
|
|
||||||
Icon = SweetAlertIcon.Success,
|
|
||||||
Timer = 1500,
|
|
||||||
ShowConfirmButton = false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum OrderType {
|
|
||||||
None,
|
|
||||||
Name,
|
|
||||||
Created
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum OrderDirection : byte {
|
|
||||||
Asc = 0,
|
|
||||||
Desc = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,10 +3,10 @@
|
|||||||
@using BlazorStrap
|
@using BlazorStrap
|
||||||
@using BlazorStrap.V5
|
@using BlazorStrap.V5
|
||||||
@using HopFrame.Security.Claims
|
@using HopFrame.Security.Claims
|
||||||
|
@using HopFrame.Web.Admin.Providers
|
||||||
@using HopFrame.Web.Services
|
@using HopFrame.Web.Services
|
||||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
@using HopFrame.Web.Components.Administration
|
@using HopFrame.Web.Components.Administration
|
||||||
@using HopFrame.Web.Model
|
|
||||||
@using HopFrame.Web.Components
|
@using HopFrame.Web.Components
|
||||||
|
|
||||||
|
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
<BSNav MarginEnd="Margins.Auto" Class="mb-lg-0">
|
<BSNav MarginEnd="Margins.Auto" Class="mb-lg-0">
|
||||||
<BSNavItem IsActive="IsDashboardActive()" OnClick="NavigateToDashboard">Dashboard</BSNavItem>
|
<BSNavItem IsActive="IsDashboardActive()" OnClick="NavigateToDashboard">Dashboard</BSNavItem>
|
||||||
|
|
||||||
@foreach (var nav in Subpages) {
|
@foreach (var adminPage in Pages.LoadRegisteredAdminPages()) {
|
||||||
<AuthorizedView Permission="@nav.Permission">
|
<AuthorizedView Permission="@adminPage.Permissions.View">
|
||||||
<BSNavItem IsActive="IsNavItemActive(nav.Url)" OnClick="() => Navigate(nav.Url)">@nav.Name</BSNavItem>
|
<BSNavItem IsActive="IsNavItemActive(adminPage.Url)" OnClick="() => Navigate(adminPage.Url)">@adminPage.Title</BSNavItem>
|
||||||
</AuthorizedView>
|
</AuthorizedView>
|
||||||
}
|
}
|
||||||
</BSNav>
|
</BSNav>
|
||||||
@@ -46,25 +46,11 @@
|
|||||||
@inject NavigationManager Navigator
|
@inject NavigationManager Navigator
|
||||||
@inject ITokenContext Context
|
@inject ITokenContext Context
|
||||||
@inject IAuthService Auth
|
@inject IAuthService Auth
|
||||||
|
@inject IAdminPagesProvider Pages
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
public static IList<NavigationItem> Subpages = new List<NavigationItem> {
|
|
||||||
new () {
|
|
||||||
Name = "Users",
|
|
||||||
Url = "administration/users",
|
|
||||||
Description = "On this page you can manage all user accounts.",
|
|
||||||
Permission = Security.AdminPermissions.ViewUsers
|
|
||||||
},
|
|
||||||
new () {
|
|
||||||
Name = "Groups",
|
|
||||||
Url = "administration/groups",
|
|
||||||
Description = "On this page you can view, create, edit and delete permission groups.",
|
|
||||||
Permission = Security.AdminPermissions.ViewGroups
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private bool IsNavItemActive(string element) {
|
private bool IsNavItemActive(string element) {
|
||||||
return Navigator.Uri.Contains(element);
|
return Navigator.Uri.TrimEnd('/').EndsWith(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsDashboardActive() {
|
private bool IsDashboardActive() {
|
||||||
@@ -72,11 +58,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void NavigateToDashboard() {
|
private void NavigateToDashboard() {
|
||||||
Navigate("administration");
|
Navigator.NavigateTo("administration", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Navigate(string url) {
|
private void Navigate(string url) {
|
||||||
Navigator.NavigateTo(url, true);
|
Navigator.NavigateTo("administration/" + url, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Logout() {
|
private void Logout() {
|
||||||
|
|||||||
@@ -1,222 +0,0 @@
|
|||||||
@page "/administration/users"
|
|
||||||
@rendermode InteractiveServer
|
|
||||||
@layout AdminLayout
|
|
||||||
|
|
||||||
@using System.Globalization
|
|
||||||
@using BlazorStrap
|
|
||||||
@using CurrieTechnologies.Razor.SweetAlert2
|
|
||||||
@using HopFrame.Database.Models
|
|
||||||
@using HopFrame.Security.Claims
|
|
||||||
@using HopFrame.Web.Pages.Administration.Layout
|
|
||||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
|
||||||
@using Microsoft.AspNetCore.Components.Web
|
|
||||||
@using HopFrame.Web.Components
|
|
||||||
@using BlazorStrap.V5
|
|
||||||
@using HopFrame.Database.Repositories
|
|
||||||
@using HopFrame.Web.Components.Administration
|
|
||||||
|
|
||||||
<PageTitle>Users</PageTitle>
|
|
||||||
<AuthorizedView Permission="@Security.AdminPermissions.ViewUsers" RedirectIfUnauthorized="administration/login?redirect=/administration/users"/>
|
|
||||||
|
|
||||||
<UserAddModal @ref="_userAddModal" ReloadPage="Reload"/>
|
|
||||||
<UserEditModal @ref="_userEditModal" ReloadPage="Reload"/>
|
|
||||||
|
|
||||||
<div class="title">
|
|
||||||
<h3>
|
|
||||||
Users administration
|
|
||||||
<span class="reload" @onclick="Reload">
|
|
||||||
<HopIconDisplay Type="HopIconDisplay.HopIcon.Reload"/>
|
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<form class="d-flex" role="search" @onsubmit="Search" id="search">
|
|
||||||
<input class="form-control me-2 input-dark" type="search" placeholder="Search" aria-label="Search" @bind="_searchText">
|
|
||||||
<BSButton Color="BSColor.Success" IsOutlined="true" type="submit">Search</BSButton>
|
|
||||||
</form>
|
|
||||||
<AuthorizedView Permission="@Security.AdminPermissions.AddUser">
|
|
||||||
<BSButton IsSubmit="false" Color="BSColor.Success" Target="add-user" OnClick="() => _userAddModal.ShowAsync()">Add User</BSButton>
|
|
||||||
</AuthorizedView>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<BSTable IsStriped="true" IsHoverable="true" IsDark="true" Color="BSColor.Dark">
|
|
||||||
<BSTHead>
|
|
||||||
<BSTR>
|
|
||||||
<BSTD>#</BSTD>
|
|
||||||
<BSTD>
|
|
||||||
<span class="sorter" @onclick="() => OrderBy(OrderType.Email)">E-Mail</span>
|
|
||||||
@if (_currentOrder == OrderType.Email) {
|
|
||||||
<HopIconDisplay Type="_currentOrderDirection == OrderDirection.Desc ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
|
|
||||||
}
|
|
||||||
</BSTD>
|
|
||||||
<BSTD>
|
|
||||||
<span class="sorter" @onclick="() => OrderBy(OrderType.Username)">Username</span>
|
|
||||||
@if (_currentOrder == OrderType.Username) {
|
|
||||||
<HopIconDisplay Type="_currentOrderDirection == OrderDirection.Desc ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
|
|
||||||
}
|
|
||||||
</BSTD>
|
|
||||||
<BSTD>
|
|
||||||
<span class="sorter" @onclick="() => OrderBy(OrderType.Registered)">Registered</span>
|
|
||||||
@if (_currentOrder == OrderType.Registered) {
|
|
||||||
<HopIconDisplay Type="_currentOrderDirection == OrderDirection.Desc ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
|
|
||||||
}
|
|
||||||
</BSTD>
|
|
||||||
<BSTD>Primary Group</BSTD>
|
|
||||||
|
|
||||||
@if (_hasEditPrivileges || _hasDeletePrivileges) {
|
|
||||||
<BSTD>Actions</BSTD>
|
|
||||||
}
|
|
||||||
</BSTR>
|
|
||||||
</BSTHead>
|
|
||||||
|
|
||||||
<BSTBody>
|
|
||||||
@foreach (var user in _users) {
|
|
||||||
<BSTR>
|
|
||||||
<BSTD class="bold">@user.Id</BSTD>
|
|
||||||
<BSTD>@user.Email</BSTD>
|
|
||||||
<BSTD>@user.Username</BSTD>
|
|
||||||
<BSTD>@user.CreatedAt</BSTD>
|
|
||||||
<BSTD>@GetFriendlyGroupName(user)</BSTD>
|
|
||||||
|
|
||||||
@if (_hasEditPrivileges || _hasDeletePrivileges) {
|
|
||||||
<BSTD>
|
|
||||||
<BSButtonGroup>
|
|
||||||
@if (_hasEditPrivileges) {
|
|
||||||
<BSButton Color="BSColor.Warning" OnClick="() => _userEditModal.ShowAsync(user)">Edit</BSButton>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (_hasDeletePrivileges) {
|
|
||||||
<BSButton Color="BSColor.Danger" OnClick="() => Delete(user)">Delete</BSButton>
|
|
||||||
}
|
|
||||||
</BSButtonGroup>
|
|
||||||
</BSTD>
|
|
||||||
}
|
|
||||||
</BSTR>
|
|
||||||
}
|
|
||||||
</BSTBody>
|
|
||||||
</BSTable>
|
|
||||||
|
|
||||||
@inject IUserRepository UserService
|
|
||||||
@inject IPermissionRepository PermissionsService
|
|
||||||
@inject IGroupRepository Groups
|
|
||||||
@inject SweetAlertService Alerts
|
|
||||||
@inject ITokenContext Auth
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private IList<User> _users = new List<User>();
|
|
||||||
private IDictionary<Guid, PermissionGroup> _userGroups = new Dictionary<Guid, PermissionGroup>();
|
|
||||||
|
|
||||||
private OrderType _currentOrder = OrderType.None;
|
|
||||||
private OrderDirection _currentOrderDirection = OrderDirection.Asc;
|
|
||||||
|
|
||||||
private string _searchText;
|
|
||||||
|
|
||||||
private bool _hasEditPrivileges = false;
|
|
||||||
private bool _hasDeletePrivileges = false;
|
|
||||||
|
|
||||||
private UserAddModal _userAddModal;
|
|
||||||
private UserEditModal _userEditModal;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
|
||||||
_users = await UserService.GetUsers();
|
|
||||||
|
|
||||||
foreach (var user in _users) {
|
|
||||||
var groups = await Groups.GetUserGroups(user);
|
|
||||||
_userGroups.Add(user.Id, groups.LastOrDefault());
|
|
||||||
}
|
|
||||||
|
|
||||||
_hasEditPrivileges = await PermissionsService.HasPermission(Auth.User, Security.AdminPermissions.EditUser);
|
|
||||||
_hasDeletePrivileges = await PermissionsService.HasPermission(Auth.User, Security.AdminPermissions.DeleteUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Reload() {
|
|
||||||
_users = new List<User>();
|
|
||||||
_userGroups = new Dictionary<Guid, PermissionGroup>();
|
|
||||||
|
|
||||||
_users = await UserService.GetUsers();
|
|
||||||
|
|
||||||
foreach (var user in _users) {
|
|
||||||
var groups = await Groups.GetUserGroups(user);
|
|
||||||
_userGroups.Add(user.Id, groups.LastOrDefault());
|
|
||||||
}
|
|
||||||
|
|
||||||
OrderBy(_currentOrder, false);
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Search() {
|
|
||||||
var users = await UserService.GetUsers();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(_searchText)) {
|
|
||||||
users = users
|
|
||||||
.Where(user =>
|
|
||||||
user.Email.Contains(_searchText) ||
|
|
||||||
user.Username.Contains(_searchText) ||
|
|
||||||
user.Id.ToString().Contains(_searchText) ||
|
|
||||||
user.CreatedAt.ToString(CultureInfo.InvariantCulture).Contains(_searchText) ||
|
|
||||||
_userGroups[user.Id]?.Name.Contains(_searchText) == true)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
_users = users;
|
|
||||||
OrderBy(_currentOrder, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetFriendlyGroupName(User user) {
|
|
||||||
var group = _userGroups[user.Id];
|
|
||||||
if (group is null) return null;
|
|
||||||
|
|
||||||
return group.Name.Replace("group.", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OrderBy(OrderType type, bool changeDir = true) {
|
|
||||||
if (_currentOrder == type && changeDir) _currentOrderDirection = (OrderDirection)(((byte)_currentOrderDirection + 1) % 2);
|
|
||||||
if (_currentOrder != type) _currentOrderDirection = OrderDirection.Asc;
|
|
||||||
|
|
||||||
if (type == OrderType.Email) {
|
|
||||||
_users = _currentOrderDirection == OrderDirection.Asc ? _users.OrderBy(user => user.Email).ToList() : _users.OrderByDescending(user => user.Email).ToList();
|
|
||||||
}
|
|
||||||
else if (type == OrderType.Username) {
|
|
||||||
_users = _currentOrderDirection == OrderDirection.Asc ? _users.OrderBy(user => user.Username).ToList() : _users.OrderByDescending(user => user.Username).ToList();
|
|
||||||
}
|
|
||||||
else if (type == OrderType.Registered) {
|
|
||||||
_users = _currentOrderDirection == OrderDirection.Asc ? _users.OrderBy(user => user.CreatedAt).ToList() : _users.OrderByDescending(user => user.CreatedAt).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentOrder = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Delete(User user) {
|
|
||||||
var result = await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Are you sure?",
|
|
||||||
Text = "You won't be able to revert this!",
|
|
||||||
Icon = SweetAlertIcon.Warning,
|
|
||||||
ConfirmButtonText = "Yes",
|
|
||||||
ShowCancelButton = true,
|
|
||||||
ShowConfirmButton = true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.IsConfirmed) {
|
|
||||||
await UserService.DeleteUser(user);
|
|
||||||
await Reload();
|
|
||||||
|
|
||||||
await Alerts.FireAsync(new SweetAlertOptions {
|
|
||||||
Title = "Deleted!",
|
|
||||||
Icon = SweetAlertIcon.Success,
|
|
||||||
Timer = 1500,
|
|
||||||
ShowConfirmButton = false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum OrderType {
|
|
||||||
None,
|
|
||||||
Email,
|
|
||||||
Username,
|
|
||||||
Registered
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum OrderDirection : byte {
|
|
||||||
Asc = 0,
|
|
||||||
Desc = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
th, h3 {
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reload, .sorter {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bold {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
24
src/HopFrame.Web/Repositories/GroupProvider.cs
Normal file
24
src/HopFrame.Web/Repositories/GroupProvider.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using HopFrame.Database.Models;
|
||||||
|
using HopFrame.Database.Repositories;
|
||||||
|
using HopFrame.Web.Admin;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Repositories;
|
||||||
|
|
||||||
|
internal sealed class GroupProvider(IGroupRepository repo) : ModelRepository<PermissionGroup> {
|
||||||
|
public override async Task<IEnumerable<PermissionGroup>> ReadAll() {
|
||||||
|
return await repo.GetPermissionGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<PermissionGroup> Create(PermissionGroup model) {
|
||||||
|
return await repo.CreatePermissionGroup(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<PermissionGroup> Update(PermissionGroup model) {
|
||||||
|
await repo.EditPermissionGroup(model);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task Delete(PermissionGroup model) {
|
||||||
|
return repo.DeletePermissionGroup(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/HopFrame.Web/Repositories/UserProvider.cs
Normal file
24
src/HopFrame.Web/Repositories/UserProvider.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using HopFrame.Database.Models;
|
||||||
|
using HopFrame.Database.Repositories;
|
||||||
|
using HopFrame.Web.Admin;
|
||||||
|
|
||||||
|
namespace HopFrame.Web.Repositories;
|
||||||
|
|
||||||
|
internal sealed class UserProvider(IUserRepository repo) : ModelRepository<User> {
|
||||||
|
public override async Task<IEnumerable<User>> ReadAll() {
|
||||||
|
return await repo.GetUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<User> Create(User model) {
|
||||||
|
return repo.AddUser(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<User> Update(User model) {
|
||||||
|
await repo.UpdateUser(model);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task Delete(User model) {
|
||||||
|
return repo.DeleteUser(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using BlazorStrap;
|
|||||||
using CurrieTechnologies.Razor.SweetAlert2;
|
using CurrieTechnologies.Razor.SweetAlert2;
|
||||||
using HopFrame.Database;
|
using HopFrame.Database;
|
||||||
using HopFrame.Security.Authentication;
|
using HopFrame.Security.Authentication;
|
||||||
|
using HopFrame.Web.Admin;
|
||||||
using HopFrame.Web.Services;
|
using HopFrame.Web.Services;
|
||||||
using HopFrame.Web.Services.Implementation;
|
using HopFrame.Web.Services.Implementation;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
@@ -15,6 +16,7 @@ public static class ServiceCollectionExtensions {
|
|||||||
services.AddHopFrameRepositories<TDbContext>();
|
services.AddHopFrameRepositories<TDbContext>();
|
||||||
services.AddScoped<IAuthService, AuthService>();
|
services.AddScoped<IAuthService, AuthService>();
|
||||||
services.AddTransient<AuthMiddleware>();
|
services.AddTransient<AuthMiddleware>();
|
||||||
|
services.AddAdminContext<HopAdminContext>();
|
||||||
|
|
||||||
// Component library's
|
// Component library's
|
||||||
services.AddSweetAlert2();
|
services.AddSweetAlert2();
|
||||||
|
|||||||
38
test/FrontendTest/AdminContext.cs
Normal file
38
test/FrontendTest/AdminContext.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using FrontendTest.Providers;
|
||||||
|
using HopFrame.Web.Admin;
|
||||||
|
using HopFrame.Web.Admin.Generators;
|
||||||
|
using HopFrame.Web.Admin.Models;
|
||||||
|
using RestApiTest.Models;
|
||||||
|
|
||||||
|
namespace FrontendTest;
|
||||||
|
|
||||||
|
public class AdminContext : AdminPagesContext {
|
||||||
|
|
||||||
|
public AdminPage<Address> Addresses { get; set; }
|
||||||
|
public AdminPage<Employee> Employees { get; set; }
|
||||||
|
|
||||||
|
public override void OnModelCreating(IAdminContextGenerator generator) {
|
||||||
|
base.OnModelCreating(generator);
|
||||||
|
|
||||||
|
generator.Page<Employee>()
|
||||||
|
.Property(e => e.Address)
|
||||||
|
.IsSelector();
|
||||||
|
|
||||||
|
generator.Page<Address>()
|
||||||
|
.Property(a => a.Employee)
|
||||||
|
.Ignore();
|
||||||
|
|
||||||
|
generator.Page<Address>()
|
||||||
|
.Property(a => a.AddressId)
|
||||||
|
.IsSelector<Employee>()
|
||||||
|
.Parser<Employee>((model, e) => model.AddressId = e.EmployeeId);
|
||||||
|
|
||||||
|
generator.Page<Employee>()
|
||||||
|
.ConfigureRepository<EmployeeProvider>()
|
||||||
|
.ListingProperty(e => e.Name);
|
||||||
|
|
||||||
|
generator.Page<Address>()
|
||||||
|
.ConfigureRepository<AddressProvider>()
|
||||||
|
.ListingProperty(a => a.City);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
@page "/counter"
|
@page "/counter"
|
||||||
|
@using System.Text.Json
|
||||||
|
@using HopFrame.Web
|
||||||
|
@using HopFrame.Web.Admin.Providers
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
<PageTitle>Counter</PageTitle>
|
<PageTitle>Counter</PageTitle>
|
||||||
@@ -9,12 +12,20 @@
|
|||||||
|
|
||||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||||
|
|
||||||
|
@inject IAdminPagesProvider Provider
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private int currentCount = 0;
|
private int currentCount = 0;
|
||||||
private string[] permissions = ["web.counter"];
|
private string[] permissions = ["web.counter"];
|
||||||
|
|
||||||
private void IncrementCount() {
|
private void IncrementCount() {
|
||||||
currentCount++;
|
currentCount++;
|
||||||
|
|
||||||
|
string json = JsonSerializer.Serialize(Provider.LoadRegisteredAdminPages(), new JsonSerializerOptions {
|
||||||
|
WriteIndented = true
|
||||||
|
});
|
||||||
|
|
||||||
|
Console.WriteLine(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,24 @@
|
|||||||
using HopFrame.Database;
|
using HopFrame.Database;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using RestApiTest.Models;
|
||||||
|
|
||||||
namespace FrontendTest;
|
namespace FrontendTest;
|
||||||
|
|
||||||
public class DatabaseContext : HopDbContextBase {
|
public class DatabaseContext : HopDbContextBase {
|
||||||
|
public DbSet<Employee> Employees { get; set; }
|
||||||
|
public DbSet<Address> Addresses { get; set; }
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||||
base.OnConfiguring(optionsBuilder);
|
base.OnConfiguring(optionsBuilder);
|
||||||
|
|
||||||
optionsBuilder.UseSqlite("Data Source=C:\\Users\\Remote\\Documents\\Projekte\\HopFrame\\test\\RestApiTest\\bin\\Debug\\net8.0\\test.db;Mode=ReadWrite;");
|
optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\test\RestApiTest\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity<Employee>()
|
||||||
|
.HasOne(e => e.Address)
|
||||||
|
.WithOne(a => a.Employee);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>disable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
18
test/FrontendTest/Models/Address.cs
Normal file
18
test/FrontendTest/Models/Address.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
|
||||||
|
namespace RestApiTest.Models;
|
||||||
|
|
||||||
|
public class Address {
|
||||||
|
[ForeignKey("Employee")]
|
||||||
|
public int AddressId { get; set; }
|
||||||
|
public string AddressDetails { get; set; }
|
||||||
|
public string City { get; set; }
|
||||||
|
public int ZipCode { get; set; }
|
||||||
|
public string State { get; set; }
|
||||||
|
public string Country { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public virtual Employee Employee { get; set; }
|
||||||
|
}
|
||||||
8
test/FrontendTest/Models/Employee.cs
Normal file
8
test/FrontendTest/Models/Employee.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace RestApiTest.Models;
|
||||||
|
|
||||||
|
public class Employee {
|
||||||
|
public int EmployeeId { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public virtual Address Address { get; set; }
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
using FrontendTest;
|
using FrontendTest;
|
||||||
using FrontendTest.Components;
|
using FrontendTest.Components;
|
||||||
using HopFrame.Web;
|
using HopFrame.Web;
|
||||||
|
using HopFrame.Web.Admin;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
builder.Services.AddDbContext<DatabaseContext>();
|
builder.Services.AddDbContext<DatabaseContext>();
|
||||||
builder.Services.AddHopFrame<DatabaseContext>();
|
builder.Services.AddHopFrame<DatabaseContext>();
|
||||||
|
builder.Services.AddAdminContext<AdminContext>();
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents()
|
||||||
|
|||||||
29
test/FrontendTest/Providers/AddressProvider.cs
Normal file
29
test/FrontendTest/Providers/AddressProvider.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using HopFrame.Web.Admin;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using RestApiTest.Models;
|
||||||
|
|
||||||
|
namespace FrontendTest.Providers;
|
||||||
|
|
||||||
|
public class AddressProvider(DatabaseContext context) : ModelRepository<Address> {
|
||||||
|
|
||||||
|
public override async Task<IEnumerable<Address>> ReadAll() {
|
||||||
|
return await context.Addresses.ToArrayAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<Address> Create(Address model) {
|
||||||
|
await context.Addresses.AddAsync(model);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<Address> Update(Address model) {
|
||||||
|
context.Addresses.Update(model);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task Delete(Address model) {
|
||||||
|
context.Addresses.Remove(model);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
31
test/FrontendTest/Providers/EmployeeProvider.cs
Normal file
31
test/FrontendTest/Providers/EmployeeProvider.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using HopFrame.Web.Admin;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using RestApiTest.Models;
|
||||||
|
|
||||||
|
namespace FrontendTest.Providers;
|
||||||
|
|
||||||
|
public class EmployeeProvider(DatabaseContext context) : ModelRepository<Employee> {
|
||||||
|
|
||||||
|
public override async Task<IEnumerable<Employee>> ReadAll() {
|
||||||
|
return await context.Employees
|
||||||
|
.Include(e => e.Address)
|
||||||
|
.ToArrayAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<Employee> Create(Employee model) {
|
||||||
|
await context.Employees.AddAsync(model);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<Employee> Update(Employee model) {
|
||||||
|
context.Employees.Update(model);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task Delete(Employee model) {
|
||||||
|
context.Employees.Remove(model);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ public class DatabaseContext : HopDbContextBase {
|
|||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||||
base.OnConfiguring(optionsBuilder);
|
base.OnConfiguring(optionsBuilder);
|
||||||
|
|
||||||
optionsBuilder.UseSqlite("Data Source=C:\\Users\\Remote\\Documents\\Projekte\\HopFrame\\test\\RestApiTest\\bin\\Debug\\net8.0\\test.db;Mode=ReadWrite;");
|
optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\test\RestApiTest\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||||
|
|||||||
Reference in New Issue
Block a user