diff --git a/HopFrame.sln b/HopFrame.sln
index f9217e8..2e65007 100644
--- a/HopFrame.sln
+++ b/HopFrame.sln
@@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web", "src\HopFram
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontendTest", "test\FrontendTest\FrontendTest.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}"
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
GlobalSection(NestedProjects) = preSolution
EndGlobalSection
diff --git a/src/HopFrame.Database/HopFrame.Database.csproj b/src/HopFrame.Database/HopFrame.Database.csproj
index f2bcba7..1216958 100644
--- a/src/HopFrame.Database/HopFrame.Database.csproj
+++ b/src/HopFrame.Database/HopFrame.Database.csproj
@@ -22,4 +22,8 @@
+
+
+
+
diff --git a/src/HopFrame.Database/Models/PermissionGroup.cs b/src/HopFrame.Database/Models/PermissionGroup.cs
index 7a70ebd..0a0c669 100644
--- a/src/HopFrame.Database/Models/PermissionGroup.cs
+++ b/src/HopFrame.Database/Models/PermissionGroup.cs
@@ -1,22 +1,27 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
+using HopFrame.Web.Admin.Attributes;
+using HopFrame.Web.Admin.Attributes.Members;
namespace HopFrame.Database.Models;
+[AdminName("Groups")]
+[AdminDescription("On this page you can view, create, edit and delete permission groups.")]
public class PermissionGroup : IPermissionOwner {
[Key, Required, MaxLength(50)]
public string Name { get; init; }
- [Required, DefaultValue(false)]
+ [Required, DefaultValue(false), AdminUnsortable]
public bool IsDefaultGroup { get; set; }
[MaxLength(500)]
public string Description { get; set; }
- [Required]
+ [Required, AdminUneditable]
public DateTime CreatedAt { get; set; }
+ [AdminIgnore(onlyForListing: true)]
public virtual IList Permissions { get; set; }
}
\ No newline at end of file
diff --git a/src/HopFrame.Database/Models/User.cs b/src/HopFrame.Database/Models/User.cs
index 971d899..ab944f0 100644
--- a/src/HopFrame.Database/Models/User.cs
+++ b/src/HopFrame.Database/Models/User.cs
@@ -1,8 +1,11 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
+using HopFrame.Web.Admin.Attributes;
+using HopFrame.Web.Admin.Attributes.Members;
namespace HopFrame.Database.Models;
+[AdminDescription("On this page you can manage all user accounts.")]
public class User : IPermissionOwner {
[Key, Required, MinLength(36), MaxLength(36)]
@@ -14,15 +17,16 @@ public class User : IPermissionOwner {
[Required, MaxLength(50), EmailAddress]
public string Email { get; set; }
- [Required, MinLength(8), MaxLength(255), JsonIgnore]
+ [Required, MinLength(8), MaxLength(255), JsonIgnore, AdminIgnore(onlyForListing: true), AdminHideValue]
public string Password { get; set; }
- [Required]
+ [Required, AdminUneditable]
public DateTime CreatedAt { get; set; }
+ [AdminIgnore(onlyForListing: true)]
public virtual IList Permissions { get; set; }
- [JsonIgnore]
+ [JsonIgnore, AdminIgnore]
public virtual IList Tokens { get; set; }
}
\ No newline at end of file
diff --git a/src/HopFrame.Web.Admin/AdminPagesContext.cs b/src/HopFrame.Web.Admin/AdminPagesContext.cs
new file mode 100644
index 0000000..7be251e
--- /dev/null
+++ b/src/HopFrame.Web.Admin/AdminPagesContext.cs
@@ -0,0 +1,9 @@
+using HopFrame.Web.Admin.Generators;
+
+namespace HopFrame.Web.Admin;
+
+public abstract class AdminPagesContext {
+
+ public abstract void OnModelCreating(IAdminContextGenerator generator);
+
+}
\ No newline at end of file
diff --git a/src/HopFrame.Web.Admin/Attributes/AdminDescriptionAttribute.cs b/src/HopFrame.Web.Admin/Attributes/AdminDescriptionAttribute.cs
new file mode 100644
index 0000000..843f250
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Attributes/AdminDescriptionAttribute.cs
@@ -0,0 +1,6 @@
+namespace HopFrame.Web.Admin.Attributes;
+
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
+public class AdminDescriptionAttribute(string description) : Attribute {
+ public string Description { get; set; } = description;
+}
\ No newline at end of file
diff --git a/src/HopFrame.Web.Admin/Attributes/AdminNameAttribute.cs b/src/HopFrame.Web.Admin/Attributes/AdminNameAttribute.cs
new file mode 100644
index 0000000..aaa8bac
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Attributes/AdminNameAttribute.cs
@@ -0,0 +1,6 @@
+namespace HopFrame.Web.Admin.Attributes;
+
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
+public class AdminNameAttribute(string name) : Attribute {
+ public string Name { get; set; } = name;
+}
\ No newline at end of file
diff --git a/src/HopFrame.Web.Admin/Attributes/Classes/AdminButtonConfigAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Classes/AdminButtonConfigAttribute.cs
new file mode 100644
index 0000000..2995164
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Attributes/Classes/AdminButtonConfigAttribute.cs
@@ -0,0 +1,8 @@
+namespace HopFrame.Web.Admin.Attributes.Classes;
+
+[AttributeUsage(AttributeTargets.Class)]
+public 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;
+}
\ No newline at end of file
diff --git a/src/HopFrame.Web.Admin/Attributes/Classes/AdminPermissionsAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Classes/AdminPermissionsAttribute.cs
new file mode 100644
index 0000000..59b0747
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Attributes/Classes/AdminPermissionsAttribute.cs
@@ -0,0 +1,8 @@
+using HopFrame.Web.Admin.Models;
+
+namespace HopFrame.Web.Admin.Attributes.Classes;
+
+[AttributeUsage(AttributeTargets.Class)]
+public class AdminPermissionsAttribute(AdminPagePermissions permissions) : Attribute {
+ public AdminPagePermissions Permissions { get; set; } = permissions;
+}
diff --git a/src/HopFrame.Web.Admin/Attributes/Members/AdminBoldAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Members/AdminBoldAttribute.cs
new file mode 100644
index 0000000..70fa63e
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Attributes/Members/AdminBoldAttribute.cs
@@ -0,0 +1,4 @@
+namespace HopFrame.Web.Admin.Attributes.Members;
+
+[AttributeUsage(AttributeTargets.Property)]
+public class AdminBoldAttribute : Attribute;
diff --git a/src/HopFrame.Web.Admin/Attributes/Members/AdminHideValueAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Members/AdminHideValueAttribute.cs
new file mode 100644
index 0000000..c40288d
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Attributes/Members/AdminHideValueAttribute.cs
@@ -0,0 +1,4 @@
+namespace HopFrame.Web.Admin.Attributes.Members;
+
+[AttributeUsage(AttributeTargets.Property)]
+public class AdminHideValueAttribute : Attribute;
diff --git a/src/HopFrame.Web.Admin/Attributes/Members/AdminIgnoreAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Members/AdminIgnoreAttribute.cs
new file mode 100644
index 0000000..6022cf3
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Attributes/Members/AdminIgnoreAttribute.cs
@@ -0,0 +1,6 @@
+namespace HopFrame.Web.Admin.Attributes.Members;
+
+[AttributeUsage(AttributeTargets.Property)]
+public class AdminIgnoreAttribute(bool onlyForListing = false) : Attribute {
+ public bool OnlyForListing { get; set; } = onlyForListing;
+}
diff --git a/src/HopFrame.Web.Admin/Attributes/Members/AdminUneditableAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Members/AdminUneditableAttribute.cs
new file mode 100644
index 0000000..f29e575
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Attributes/Members/AdminUneditableAttribute.cs
@@ -0,0 +1,4 @@
+namespace HopFrame.Web.Admin.Attributes.Members;
+
+[AttributeUsage(AttributeTargets.Property)]
+public class AdminUneditableAttribute : Attribute;
diff --git a/src/HopFrame.Web.Admin/Attributes/Members/AdminUnsortableAttribute.cs b/src/HopFrame.Web.Admin/Attributes/Members/AdminUnsortableAttribute.cs
new file mode 100644
index 0000000..6ddb865
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Attributes/Members/AdminUnsortableAttribute.cs
@@ -0,0 +1,4 @@
+namespace HopFrame.Web.Admin.Attributes.Members;
+
+[AttributeUsage(AttributeTargets.Property)]
+public class AdminUnsortableAttribute : Attribute;
diff --git a/src/HopFrame.Web.Admin/Generators/IAdminContextGenerator.cs b/src/HopFrame.Web.Admin/Generators/IAdminContextGenerator.cs
new file mode 100644
index 0000000..49cf115
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Generators/IAdminContextGenerator.cs
@@ -0,0 +1,7 @@
+namespace HopFrame.Web.Admin.Generators;
+
+public interface IAdminContextGenerator {
+
+ IAdminPageGenerator Page();
+
+}
\ No newline at end of file
diff --git a/src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs b/src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs
new file mode 100644
index 0000000..c4f7c36
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs
@@ -0,0 +1,26 @@
+using System.ComponentModel;
+using System.Linq.Expressions;
+
+namespace HopFrame.Web.Admin.Generators;
+
+public interface IAdminPageGenerator {
+
+ IAdminPageGenerator Title(string title);
+ IAdminPageGenerator Description(string description);
+
+ IAdminPageGenerator ViewPermission(string permission);
+ IAdminPageGenerator CreatePermission(string permission);
+ IAdminPageGenerator UpdatePermission(string permission);
+ IAdminPageGenerator DeletePermission(string permission);
+
+ IAdminPageGenerator ShowCreateButton(bool show);
+ IAdminPageGenerator ShowDeleteButton(bool show);
+ IAdminPageGenerator ShowUpdateButton(bool show);
+
+ IAdminPageGenerator DefaultSort(Expression> propertyExpression, ListSortDirection direction);
+
+ IAdminPageGenerator ConfigureRepository() where TRepository : IModelRepository;
+
+ IAdminPropertyGenerator Property(Expression> propertyExpression);
+
+}
diff --git a/src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs b/src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs
new file mode 100644
index 0000000..d6b644d
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs
@@ -0,0 +1,14 @@
+namespace HopFrame.Web.Admin.Generators;
+
+public interface IAdminPropertyGenerator {
+
+ IAdminPropertyGenerator Sortable(bool sortable);
+ IAdminPropertyGenerator Editable(bool editable);
+ IAdminPropertyGenerator DisplayValueWhileEditing(bool display);
+ IAdminPropertyGenerator DisplayInListing(bool display = true);
+ IAdminPropertyGenerator Bold(bool isBold = true);
+
+ IAdminPropertyGenerator DisplayName(string displayName);
+ IAdminPropertyGenerator Description(string description);
+
+}
\ No newline at end of file
diff --git a/src/HopFrame.Web.Admin/Generators/IGenerator.cs b/src/HopFrame.Web.Admin/Generators/IGenerator.cs
new file mode 100644
index 0000000..5cf9d6f
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Generators/IGenerator.cs
@@ -0,0 +1,7 @@
+namespace HopFrame.Web.Admin.Generators;
+
+public interface IGenerator {
+
+ TGeneratedType Compile();
+
+}
\ No newline at end of file
diff --git a/src/HopFrame.Web.Admin/Generators/Implementation/AdminContextGenerator.cs b/src/HopFrame.Web.Admin/Generators/Implementation/AdminContextGenerator.cs
new file mode 100644
index 0000000..314c490
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Generators/Implementation/AdminContextGenerator.cs
@@ -0,0 +1,91 @@
+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 class AdminContextGenerator : IAdminContextGenerator {
+
+ private readonly IDictionary _adminPages = new Dictionary();
+
+ public IAdminPageGenerator Page() {
+ if (_adminPages.TryGetValue(typeof(TModel), out var pageGenerator))
+ return pageGenerator as IAdminPageGenerator;
+
+ var generator = Activator.CreateInstance(typeof(IAdminPageGenerator)) as IAdminPageGenerator;
+
+ ApplyConfigurationFromAttributes(generator, typeof(TModel).GetCustomAttributes(false));
+
+ _adminPages.Add(typeof(TModel), generator);
+
+ return generator;
+ }
+
+ public AdminPage CompilePage() {
+ var generator = _adminPages[typeof(TModel)];
+ if (generator is null) return null;
+
+ return (generator as AdminPageGenerator)?.Compile();
+ }
+
+ public TContext CompileContext() where TContext : AdminPagesContext {
+ var type = typeof(TContext);
+ var compileMethod = typeof(AdminContextGenerator).GetMethod(nameof(CompilePage));
+
+ var properties = type.GetProperties();
+
+ var context = Activator.CreateInstance();
+
+ foreach (var property in properties) {
+ var propertyType = property.PropertyType.GenericTypeArguments[0];
+ var pageGeneratorType = typeof(AdminPageGenerator<>).MakeGenericType(propertyType);
+ var generatorInstance = Activator.CreateInstance(pageGeneratorType, [propertyType.Name]);
+
+ var populatorMethod = typeof(AdminContextGenerator)
+ .GetMethod(nameof(ApplyConfigurationFromAttributes))?
+ .MakeGenericMethod(propertyType);
+ populatorMethod?.Invoke(this, [generatorInstance, propertyType.GetCustomAttributes()]);
+
+ _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 void ApplyConfigurationFromAttributes(IAdminPageGenerator generator, object[] attributes) {
+ if (attributes.Any(a => a is AdminNameAttribute)) {
+ var attribute = attributes.Single(a => a is AdminNameAttribute) as AdminNameAttribute;
+ generator.Title(attribute?.Name);
+ }
+
+ if (attributes.Any(a => a is AdminDescriptionAttribute)) {
+ var attribute = attributes.Single(a => a is AdminDescriptionAttribute) as AdminDescriptionAttribute;
+ generator.Description(attribute?.Description);
+ }
+
+ if (attributes.Any(a => a is AdminPermissionsAttribute)) {
+ var attribute = attributes.Single(a => a is AdminPermissionsAttribute) as AdminPermissionsAttribute;
+ generator.CreatePermission(attribute?.Permissions.Create);
+ generator.UpdatePermission(attribute?.Permissions.Update);
+ generator.ViewPermission(attribute?.Permissions.View);
+ generator.DeletePermission(attribute?.Permissions.Delete);
+ }
+
+ if (attributes.Any(a => a is AdminButtonConfigAttribute)) {
+ var attribute = attributes.Single(a => a is AdminButtonConfigAttribute) as AdminButtonConfigAttribute;
+ generator.ShowCreateButton(attribute?.ShowCreateButton == true);
+ generator.ShowUpdateButton(attribute?.ShowUpdateButton == true);
+ generator.ShowDeleteButton(attribute?.ShowDeleteButton == true);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/HopFrame.Web.Admin/Generators/Implementation/AdminPageGenerator.cs b/src/HopFrame.Web.Admin/Generators/Implementation/AdminPageGenerator.cs
new file mode 100644
index 0000000..a2219ce
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Generators/Implementation/AdminPageGenerator.cs
@@ -0,0 +1,180 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+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 AdminPageGenerator : IAdminPageGenerator, IGenerator> {
+
+ private readonly AdminPage _page;
+ private readonly IDictionary _propertyGenerators;
+
+ public AdminPageGenerator() {
+ _page = new AdminPage {
+ Permissions = new AdminPagePermissions()
+ };
+ _propertyGenerators = new Dictionary();
+
+ var type = typeof(TModel);
+ var properties = type.GetProperties();
+
+ foreach (var property in properties) {
+ var attributes = property.GetCustomAttributes(false);
+ var ignoreProperty = attributes
+ .SingleOrDefault(a => a is AdminIgnoreAttribute) as AdminIgnoreAttribute;
+ if (ignoreProperty?.OnlyForListing == false) continue;
+
+ var generator = Activator.CreateInstance(typeof(AdminPropertyGenerator), [property.Name, property.PropertyType]) as AdminPropertyGenerator;
+
+ ApplyConfigurationFromAttributes(generator, attributes, property);
+
+ _propertyGenerators.Add(property.Name, generator);
+ }
+ }
+
+ public AdminPageGenerator(string title) : this() {
+ Title(title);
+ }
+
+ public IAdminPageGenerator Title(string title) {
+ _page.Title = title;
+ return this;
+ }
+
+ public IAdminPageGenerator Description(string description) {
+ _page.Description = description;
+ return this;
+ }
+
+ public IAdminPageGenerator ViewPermission(string permission) {
+ _page.Permissions.View = permission;
+ return this;
+ }
+
+ public IAdminPageGenerator CreatePermission(string permission) {
+ _page.Permissions.Create = permission;
+ return this;
+ }
+
+ public IAdminPageGenerator UpdatePermission(string permission) {
+ _page.Permissions.Update = permission;
+ return this;
+ }
+
+ public IAdminPageGenerator DeletePermission(string permission) {
+ _page.Permissions.Delete = permission;
+ return this;
+ }
+
+ public IAdminPageGenerator ShowCreateButton(bool show) {
+ _page.ShowCreateButton = show;
+ return this;
+ }
+
+ public IAdminPageGenerator ShowDeleteButton(bool show) {
+ _page.ShowDeleteButton = show;
+ return this;
+ }
+
+ public IAdminPageGenerator ShowUpdateButton(bool show) {
+ _page.ShowUpdateButton = show;
+ return this;
+ }
+
+ public IAdminPageGenerator DefaultSort(Expression> propertyExpression, ListSortDirection direction) {
+ var property = GetPropertyInfo(propertyExpression);
+
+ _page.DefaultSortPropertyName = property.Name;
+ _page.DefaultSortDirection = direction;
+ return this;
+ }
+
+ public IAdminPageGenerator ConfigureRepository() where TRepository : IModelRepository {
+ _page.RepositoryProvider = typeof(TRepository);
+ return this;
+ }
+
+ public IAdminPropertyGenerator Property(Expression> propertyExpression) {
+ var property = GetPropertyInfo(propertyExpression);
+
+ if (_propertyGenerators.TryGetValue(property.Name, out var propertyGenerator))
+ return propertyGenerator;
+
+ var generator = Activator.CreateInstance(typeof(AdminPropertyGenerator), new { property.Name, property.PropertyType }) as AdminPropertyGenerator;
+ ApplyConfigurationFromAttributes(generator, property.GetCustomAttributes(false), property);
+ _propertyGenerators.Add(property.Name, generator);
+
+ return generator;
+ }
+
+ public AdminPage Compile() {
+ var properties = new List();
+
+ foreach (var generator in _propertyGenerators.Values){
+ properties.Add(generator.Compile());
+ }
+
+ _page.Properties = properties;
+
+ return _page;
+ }
+
+ private void ApplyConfigurationFromAttributes(AdminPropertyGenerator generator, object[] attributes, PropertyInfo property) {
+ if (attributes.Any(a => a is KeyAttribute)) {
+ _page.DefaultSortPropertyName = property.Name;
+ generator.Bold();
+ generator.Editable(false);
+ }
+
+ if (attributes.Any(a => a is AdminUnsortableAttribute))
+ generator.Sortable(false);
+
+ if (attributes.Any(a => a is AdminUneditableAttribute))
+ generator.Editable(false);
+
+ if (attributes.Any(a => a is AdminBoldAttribute))
+ generator.Bold();
+
+ if (attributes.Any(a => a is AdminIgnoreAttribute))
+ generator.DisplayInListing(false);
+
+ if (attributes.Any(a => a is AdminHideValueAttribute))
+ generator.DisplayValueWhileEditing(false);
+
+ if (attributes.Any(a => a is AdminNameAttribute)) {
+ var attribute = attributes.Single(a => a is AdminNameAttribute) as AdminNameAttribute;
+ generator.DisplayName(attribute?.Name);
+ }
+
+ if (attributes.Any(a => a is AdminDescriptionAttribute)) {
+ var attribute = attributes.Single(a => a is AdminDescriptionAttribute) as AdminDescriptionAttribute;
+ generator.Description(attribute?.Description);
+ }
+ }
+
+ private static PropertyInfo GetPropertyInfo(Expression> 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;
+ }
+
+}
\ No newline at end of file
diff --git a/src/HopFrame.Web.Admin/Generators/Implementation/AdminPropertyGenerator.cs b/src/HopFrame.Web.Admin/Generators/Implementation/AdminPropertyGenerator.cs
new file mode 100644
index 0000000..f37cd80
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Generators/Implementation/AdminPropertyGenerator.cs
@@ -0,0 +1,52 @@
+using HopFrame.Web.Admin.Models;
+
+namespace HopFrame.Web.Admin.Generators.Implementation;
+
+internal sealed class AdminPropertyGenerator(string name, Type type) : IAdminPropertyGenerator, IGenerator {
+
+ private readonly AdminPageProperty _property = new() {
+ Name = name,
+ Type = type
+ };
+
+ public IAdminPropertyGenerator Sortable(bool sortable) {
+ _property.Sortable = sortable;
+ return this;
+ }
+
+ public IAdminPropertyGenerator Editable(bool editable) {
+ _property.Editable = editable;
+ return this;
+ }
+
+ public IAdminPropertyGenerator DisplayValueWhileEditing(bool display) {
+ _property.EditDisplayValue = display;
+ return this;
+ }
+
+ public IAdminPropertyGenerator DisplayInListing(bool display = true) {
+ _property.DisplayInListing = display;
+ return this;
+ }
+
+ public IAdminPropertyGenerator Bold(bool isBold = true) {
+ _property.Bold = isBold;
+ return this;
+ }
+
+ public IAdminPropertyGenerator DisplayName(string displayName) {
+ _property.DisplayName = displayName;
+ return this;
+ }
+
+ public IAdminPropertyGenerator Description(string description) {
+ _property.Description = description;
+ return this;
+ }
+
+ public AdminPageProperty Compile() {
+ _property.DisplayName ??= _property.Name;
+
+ return _property;
+ }
+}
\ No newline at end of file
diff --git a/src/HopFrame.Web.Admin/HopFrame.Web.Admin.csproj b/src/HopFrame.Web.Admin/HopFrame.Web.Admin.csproj
new file mode 100644
index 0000000..096a35f
--- /dev/null
+++ b/src/HopFrame.Web.Admin/HopFrame.Web.Admin.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ disable
+
+
+
+
+
+
+
diff --git a/src/HopFrame.Web.Admin/IModelRepository.cs b/src/HopFrame.Web.Admin/IModelRepository.cs
new file mode 100644
index 0000000..6bdffe0
--- /dev/null
+++ b/src/HopFrame.Web.Admin/IModelRepository.cs
@@ -0,0 +1,8 @@
+namespace HopFrame.Web.Admin;
+
+public interface IModelRepository {
+ Task> ReadAll();
+ Task Create(TModel model);
+ Task Update(TModel model);
+ Task Delete(TModel model);
+}
diff --git a/src/HopFrame.Web.Admin/Models/AdminPage.cs b/src/HopFrame.Web.Admin/Models/AdminPage.cs
new file mode 100644
index 0000000..bea528f
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Models/AdminPage.cs
@@ -0,0 +1,22 @@
+using System.ComponentModel;
+using System.Text.Json.Serialization;
+
+namespace HopFrame.Web.Admin.Models;
+
+public class AdminPage : IAdminPageEntry {
+ public string Title { get; set; }
+ public string Description { get; set; }
+ public AdminPagePermissions Permissions { get; set; }
+ public IList Properties { get; set; }
+ [JsonIgnore]
+ public Type RepositoryProvider { 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;
+}
+
+public interface IAdminPageEntry;
diff --git a/src/HopFrame.Web.Admin/Models/AdminPagePermissions.cs b/src/HopFrame.Web.Admin/Models/AdminPagePermissions.cs
new file mode 100644
index 0000000..fc99101
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Models/AdminPagePermissions.cs
@@ -0,0 +1,8 @@
+namespace HopFrame.Web.Admin.Models;
+
+public class AdminPagePermissions {
+ public string View { get; set; }
+ public string Create { get; set; }
+ public string Update { get; set; }
+ public string Delete { get; set; }
+}
\ No newline at end of file
diff --git a/src/HopFrame.Web.Admin/Models/AdminPageProperty.cs b/src/HopFrame.Web.Admin/Models/AdminPageProperty.cs
new file mode 100644
index 0000000..5444fae
--- /dev/null
+++ b/src/HopFrame.Web.Admin/Models/AdminPageProperty.cs
@@ -0,0 +1,17 @@
+using System.Text.Json.Serialization;
+
+namespace HopFrame.Web.Admin.Models;
+
+public class AdminPageProperty {
+ public string Name { get; set; }
+ public string DisplayName { get; set; }
+ public string Description { 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 Bold { get; set; }
+ [JsonIgnore]
+ public Type Type { get; set; }
+}
\ No newline at end of file
diff --git a/src/HopFrame.Web.Admin/ServiceCollectionExtensions.cs b/src/HopFrame.Web.Admin/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..4b3c007
--- /dev/null
+++ b/src/HopFrame.Web.Admin/ServiceCollectionExtensions.cs
@@ -0,0 +1,17 @@
+using HopFrame.Web.Admin.Generators.Implementation;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace HopFrame.Web.Admin;
+
+public static class ServiceCollectionExtensions {
+
+ public static IServiceCollection AddAdminContext(this IServiceCollection services) where TContext : AdminPagesContext {
+ services.AddSingleton(_ => {
+ var generator = new AdminContextGenerator();
+ return generator.CompileContext();
+ });
+
+ return services;
+ }
+
+}
\ No newline at end of file
diff --git a/src/HopFrame.Web/HopFrame.Web.csproj b/src/HopFrame.Web/HopFrame.Web.csproj
index ab3c31a..bec6f04 100644
--- a/src/HopFrame.Web/HopFrame.Web.csproj
+++ b/src/HopFrame.Web/HopFrame.Web.csproj
@@ -20,6 +20,7 @@
+
diff --git a/test/FrontendTest/FrontendTest.csproj b/test/FrontendTest/FrontendTest.csproj
index 312aa4b..e043848 100644
--- a/test/FrontendTest/FrontendTest.csproj
+++ b/test/FrontendTest/FrontendTest.csproj
@@ -2,7 +2,7 @@
net8.0
- enable
+ disable
enable
diff --git a/test/FrontendTest/Program.cs b/test/FrontendTest/Program.cs
index af54f28..77989c6 100644
--- a/test/FrontendTest/Program.cs
+++ b/test/FrontendTest/Program.cs
@@ -1,6 +1,7 @@
using FrontendTest;
using FrontendTest.Components;
using HopFrame.Web;
+using HopFrame.Web.Admin;
var builder = WebApplication.CreateBuilder(args);
diff --git a/test/RestApiTest/AdminContext.cs b/test/RestApiTest/AdminContext.cs
new file mode 100644
index 0000000..00bf00e
--- /dev/null
+++ b/test/RestApiTest/AdminContext.cs
@@ -0,0 +1,26 @@
+using System.ComponentModel;
+using HopFrame.Database.Models;
+using HopFrame.Web.Admin;
+using HopFrame.Web.Admin.Generators;
+using HopFrame.Web.Admin.Models;
+
+namespace RestApiTest;
+
+public class AdminContext : AdminPagesContext {
+
+ public AdminPage Users { get; set; }
+ public AdminPage Groups { get; set; }
+
+ public override void OnModelCreating(IAdminContextGenerator generator) {
+
+ /*generator.Page()
+ .Title("Users")
+ .Description("On this page you can manage all user accounts.")
+ .UpdatePermission("update")
+ .ViewPermission("view")
+ .DeletePermission("delete")
+ .CreatePermission("create")
+ .DefaultSort(u => u.Id, ListSortDirection.Descending);*/
+
+ }
+}
\ No newline at end of file
diff --git a/test/RestApiTest/Controllers/TestController.cs b/test/RestApiTest/Controllers/TestController.cs
index 092784f..0d5a1f6 100644
--- a/test/RestApiTest/Controllers/TestController.cs
+++ b/test/RestApiTest/Controllers/TestController.cs
@@ -10,7 +10,7 @@ namespace RestApiTest.Controllers;
[ApiController]
[Route("test")]
-public class TestController(ITokenContext userContext, DatabaseContext context) : ControllerBase {
+public class TestController(ITokenContext userContext, DatabaseContext context, AdminContext adminContext) : ControllerBase {
[HttpGet("permissions"), Authorized]
public ActionResult> Permissions() {
@@ -50,5 +50,10 @@ public class TestController(ITokenContext userContext, DatabaseContext context)
public async Task>> GetAddresses() {
return LogicResult>.Ok(await context.Addresses.Include(e => e.Employee).ToListAsync());
}
+
+ [HttpGet("adminContext")]
+ public ActionResult GetAdminContext() {
+ return LogicResult.Ok(adminContext);
+ }
}
\ No newline at end of file
diff --git a/test/RestApiTest/Program.cs b/test/RestApiTest/Program.cs
index bfcbc38..632e62f 100644
--- a/test/RestApiTest/Program.cs
+++ b/test/RestApiTest/Program.cs
@@ -1,5 +1,6 @@
using RestApiTest;
using HopFrame.Api.Extensions;
+using HopFrame.Web.Admin;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
@@ -13,6 +14,7 @@ builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext();
+builder.Services.AddAdminContext();
builder.Services.AddSwaggerGen(c => {
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {