diff --git a/.idea/.idea.HopFrame/.idea/workspace.xml b/.idea/.idea.HopFrame/.idea/workspace.xml index 9b21c57..bd6cb12 100644 --- a/.idea/.idea.HopFrame/.idea/workspace.xml +++ b/.idea/.idea.HopFrame/.idea/workspace.xml @@ -12,13 +12,32 @@ - - - + + + + + + + + + + + + + - - - + + + + + + + + + + + + - { - "keyToString": { - ".NET Launch Settings Profile.HopFrame.Testing.Api: https.executor": "Run", - ".NET Launch Settings Profile.HopFrame.Testing.executor": "Run", - ".NET Launch Settings Profile.HopFrame.Testing: https.executor": "Run", - ".NET Project.HopFrame.Testing.executor": "Run", - "72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor": "Debug", - "RunOnceActivity.ShowReadmeOnStart": "true", - "RunOnceActivity.git.unshallow": "true", - "b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug", - "dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug", - "git-widget-placeholder": "!29 on feature/custom-views", - "list.type.of.created.stylesheet": "CSS", - "node.js.detected.package.eslint": "true", - "node.js.detected.package.tslint": "true", - "node.js.selected.package.eslint": "(autodetect)", - "node.js.selected.package.tslint": "(autodetect)", - "nodejs_package_manager_path": "npm", - "settings.editor.selected.configurable": "preferences.environmentSetup", - "vue.rearranger.settings.migration": "true" + +}]]> @@ -565,7 +599,6 @@ \ No newline at end of file diff --git a/src/HopFrame.Core/Events/EventTypes.cs b/src/HopFrame.Core/Callbacks/CallbackTypes.cs similarity index 66% rename from src/HopFrame.Core/Events/EventTypes.cs rename to src/HopFrame.Core/Callbacks/CallbackTypes.cs index d2ad751..6a66864 100644 --- a/src/HopFrame.Core/Events/EventTypes.cs +++ b/src/HopFrame.Core/Callbacks/CallbackTypes.cs @@ -1,8 +1,8 @@ using HopFrame.Core.Config; -namespace HopFrame.Core.Events; +namespace HopFrame.Core.Callbacks; -public static class EventTypes { +public static class CallbackTypes { private const string Prefix = "HopFrame."; private const string CreateEntryPrefix = Prefix + "Entry.Create."; @@ -13,18 +13,18 @@ public static class EventTypes { public static string UpdateEntry(TableConfig config) => UpdateEntryPrefix + config.PropertyName; public static string DeleteEntry(TableConfig config) => DeleteEntryPrefix + config.PropertyName; - public static string ConstructEventName(EventType type, TableConfig config) { + public static string ConstructCallbackName(CallbackType type, TableConfig config) { return type switch { - EventType.CreateEntry => CreateEntry(config), - EventType.UpdateEntry => UpdateEntry(config), - EventType.DeleteEntry => DeleteEntry(config), + CallbackType.CreateEntry => CreateEntry(config), + CallbackType.UpdateEntry => UpdateEntry(config), + CallbackType.DeleteEntry => DeleteEntry(config), _ => Prefix }; } } -public enum EventType { +public enum CallbackType { CreateEntry = 0, UpdateEntry = 1, DeleteEntry = 2 diff --git a/src/HopFrame.Core/Events/HopEventHandler.cs b/src/HopFrame.Core/Callbacks/HopCallbackHandler.cs similarity index 55% rename from src/HopFrame.Core/Events/HopEventHandler.cs rename to src/HopFrame.Core/Callbacks/HopCallbackHandler.cs index b3767b4..ec61d0a 100644 --- a/src/HopFrame.Core/Events/HopEventHandler.cs +++ b/src/HopFrame.Core/Callbacks/HopCallbackHandler.cs @@ -1,6 +1,6 @@ -namespace HopFrame.Core.Events; +namespace HopFrame.Core.Callbacks; -public readonly struct HopEventHandler(string eventType, Func handler) { +public readonly struct HopCallbackHandler(string eventType, Func handler) { public Guid Id { get; } = Guid.CreateVersion7(); public Func Handler { get; } = handler; public string EventType { get; } = eventType; diff --git a/src/HopFrame.Core/Callbacks/ICallbackEmitter.cs b/src/HopFrame.Core/Callbacks/ICallbackEmitter.cs new file mode 100644 index 0000000..18ff572 --- /dev/null +++ b/src/HopFrame.Core/Callbacks/ICallbackEmitter.cs @@ -0,0 +1,14 @@ +namespace HopFrame.Core.Callbacks; + +public interface ICallbackEmitter { + + Guid RegisterCallbackHandler(string @event, Func handler); + + bool RemoveCallbackHandler(Guid id); + + Task DispatchCallback(string @event, object argument = null!); + + void RemoveAllCallbackHandlers(string @event); + void RemoveAllCallbackHandlers(); + +} \ No newline at end of file diff --git a/src/HopFrame.Core/Config/DbContextConfig.cs b/src/HopFrame.Core/Config/DbContextConfig.cs index 005e0bf..7a408a4 100644 --- a/src/HopFrame.Core/Config/DbContextConfig.cs +++ b/src/HopFrame.Core/Config/DbContextConfig.cs @@ -1,5 +1,4 @@ -using HopFrame.Core.Events; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; namespace HopFrame.Core.Config; diff --git a/src/HopFrame.Core/Config/HopFrameConfig.cs b/src/HopFrame.Core/Config/HopFrameConfig.cs index d5593bd..5cc8dc8 100644 --- a/src/HopFrame.Core/Config/HopFrameConfig.cs +++ b/src/HopFrame.Core/Config/HopFrameConfig.cs @@ -1,5 +1,6 @@ -using HopFrame.Core.Events; +using HopFrame.Core.Callbacks; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; namespace HopFrame.Core.Config; @@ -8,18 +9,20 @@ public class HopFrameConfig { public bool DisplayUserInfo { get; set; } = true; public string? BasePolicy { get; set; } public string? LoginPageRewrite { get; set; } - public List Handlers { get; } = new(); + public List Handlers { get; } = new(); } /// /// A helper class for editing the /// -public class HopFrameConfigurator(HopFrameConfig config) { +public class HopFrameConfigurator(HopFrameConfig config, IServiceCollection collection = null!) { /// /// The Internal HopFrame configuration that's modified by the helper functions /// public HopFrameConfig InnerConfig { get; } = config; + + public IServiceCollection ServiceCollection { get; } = collection; /// /// Adds all tables defined in the DbContext to the HopFrame ui and configures it using the provided configurator @@ -45,6 +48,18 @@ public class HopFrameConfigurator(HopFrameConfig config) { return new DbContextConfigurator(context); } + public bool HasDbContext() where TDbContext : DbContext { + return InnerConfig.Contexts.Any(context => context.ContextType == typeof(TDbContext)); + } + + public DbContextConfigurator? GetDbContext() where TDbContext : DbContext { + var config = InnerConfig.Contexts + .SingleOrDefault(context => context.ContextType == typeof(TDbContext)); + if (config is null) return null; + + return new DbContextConfigurator(config); + } + /// /// Determines if the name of the currently logged-in user should be displayed in the top right corner of the admin ui /// diff --git a/src/HopFrame.Core/Config/TableConfig.cs b/src/HopFrame.Core/Config/TableConfig.cs index fe5d9e3..929140c 100644 --- a/src/HopFrame.Core/Config/TableConfig.cs +++ b/src/HopFrame.Core/Config/TableConfig.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Linq.Expressions; using System.Reflection; -using HopFrame.Core.Events; +using HopFrame.Core.Callbacks; namespace HopFrame.Core.Config; @@ -191,26 +191,26 @@ public class TableConfigurator(TableConfig config) { } /// - /// Adds an event handler of the provided type + /// Adds a callback handler of the provided type /// - /// The type of event that triggers the handler + /// The type of callback that triggers the handler /// The handler delegate - public TableConfigurator AddEventHandler(EventType type, Func handler) { - var eventName = EventTypes.ConstructEventName(type, InnerConfig); - var handlerStore = new HopEventHandler(eventName, (o, provider) => handler.Invoke((TModel)o, provider)); + public TableConfigurator AddCallbackHandler(CallbackType type, Func handler) { + var eventName = CallbackTypes.ConstructCallbackName(type, InnerConfig); + var handlerStore = new HopCallbackHandler(eventName, (o, provider) => handler.Invoke((TModel)o, provider)); InnerConfig.ContextConfig.ParentConfig.Handlers.Add(handlerStore); return this; } /// - /// Adds an event handler of the provided type + /// Adds a callback handler of the provided type /// - /// The type of event that triggers the handler + /// The type of callback that triggers the handler /// The handler delegate - public TableConfigurator AddEventHandler(EventType type, Action handler) { - var eventName = EventTypes.ConstructEventName(type, InnerConfig); - var handlerStore = new HopEventHandler(eventName, (o, provider) => { + public TableConfigurator AddCallbackHandler(CallbackType type, Action handler) { + var eventName = CallbackTypes.ConstructCallbackName(type, InnerConfig); + var handlerStore = new HopCallbackHandler(eventName, (o, provider) => { handler.Invoke((TModel)o, provider); return Task.CompletedTask; }); diff --git a/src/HopFrame.Core/Events/IEventEmitter.cs b/src/HopFrame.Core/Events/IEventEmitter.cs deleted file mode 100644 index 804cf42..0000000 --- a/src/HopFrame.Core/Events/IEventEmitter.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace HopFrame.Core.Events; - -public interface IEventEmitter { - - Guid RegisterEventHandler(string @event, Func handler); - - bool RemoveEventHandler(Guid id); - - Task DispatchEvent(string @event, object argument = null!); - - void RemoveAllEventHandlers(string @event); - void RemoveAllEventHandlers(); - -} \ No newline at end of file diff --git a/src/HopFrame.Core/ServiceCollectionExtensions.cs b/src/HopFrame.Core/ServiceCollectionExtensions.cs index 1753e12..daf73dd 100644 --- a/src/HopFrame.Core/ServiceCollectionExtensions.cs +++ b/src/HopFrame.Core/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -using HopFrame.Core.Events; +using HopFrame.Core.Callbacks; using HopFrame.Core.Services; using HopFrame.Core.Services.Implementations; using Microsoft.Extensions.DependencyInjection; @@ -16,7 +16,7 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddHopFrameServices(this IServiceCollection services) { services.AddScoped(); services.TryAddScoped(); - services.TryAddScoped(); + services.TryAddScoped(); return services; } diff --git a/src/HopFrame.Core/Services/Implementations/EventEmitter.cs b/src/HopFrame.Core/Services/Implementations/CallbackEmitter.cs similarity index 57% rename from src/HopFrame.Core/Services/Implementations/EventEmitter.cs rename to src/HopFrame.Core/Services/Implementations/CallbackEmitter.cs index 375e696..e33025b 100644 --- a/src/HopFrame.Core/Services/Implementations/EventEmitter.cs +++ b/src/HopFrame.Core/Services/Implementations/CallbackEmitter.cs @@ -1,22 +1,22 @@ using HopFrame.Core.Config; -using HopFrame.Core.Events; +using HopFrame.Core.Callbacks; namespace HopFrame.Core.Services.Implementations; -internal sealed class EventEmitter(IServiceProvider provider, HopFrameConfig config) : IEventEmitter { +internal sealed class CallbackEmitter(IServiceProvider provider, HopFrameConfig config) : ICallbackEmitter { - public Guid RegisterEventHandler(string @event, Func handler) { - var handlerStore = new HopEventHandler(@event, handler); + public Guid RegisterCallbackHandler(string @event, Func handler) { + var handlerStore = new HopCallbackHandler(@event, handler); config.Handlers.Add(handlerStore); return handlerStore.Id; } - public bool RemoveEventHandler(Guid id) { + public bool RemoveCallbackHandler(Guid id) { var count = config.Handlers.RemoveAll(handler => handler.Id == id); return count > 0; } - public async Task DispatchEvent(string @event, object argument = null!) { + public async Task DispatchCallback(string @event, object argument = null!) { var handlers = config.Handlers.Where(handler => handler.EventType == @event); var tasks = new List(); @@ -28,11 +28,11 @@ internal sealed class EventEmitter(IServiceProvider provider, HopFrameConfig con await Task.WhenAll(tasks); } - public void RemoveAllEventHandlers(string @event) { + public void RemoveAllCallbackHandlers(string @event) { config.Handlers.RemoveAll(handler => handler.EventType == @event); } - public void RemoveAllEventHandlers() { + public void RemoveAllCallbackHandlers() { config.Handlers.Clear(); } diff --git a/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor b/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor index f0741d7..5b157d5 100644 --- a/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor +++ b/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor @@ -6,6 +6,8 @@ @using HopFrame.Core.Services @using HopFrame.Web.Models @using HopFrame.Web.Helpers +@using HopFrame.Web.Plugins +@using HopFrame.Web.Plugins.Events @foreach (var property in Content.Config.Properties.Where(prop => !prop.IsListingProperty).OrderBy(prop => prop.Order)) { @@ -169,6 +171,7 @@ @inject IHopFrameAuthHandler Handler @inject IToastService Toasts @inject IServiceProvider Provider +@inject IPluginOrchestrator PluginOrchestrator @code { [Parameter] @@ -374,6 +377,14 @@ if (value is null && property.IsRequired) errorList.Add($"{property.Name} is required"); + + var eventResult = await PluginOrchestrator.DispatchEvent(new ValidationEvent(this) { + Errors = errorList, + Property = property, + Table = Content.Config + }); + + if (eventResult.IsCanceled) return false; } StateHasChanged(); diff --git a/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor b/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor index 90884af..39ad6da 100644 --- a/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor +++ b/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor @@ -4,9 +4,11 @@ @implements IDisposable @using HopFrame.Core.Config -@using HopFrame.Core.Events +@using HopFrame.Core.Callbacks @using HopFrame.Core.Services @using HopFrame.Web.Models +@using HopFrame.Web.Plugins +@using HopFrame.Web.Plugins.Events @using Microsoft.JSInterop @using Microsoft.EntityFrameworkCore @@ -118,7 +120,8 @@ @inject IJSRuntime Js @inject IDialogService Dialogs @inject IHopFrameAuthHandler Handler -@inject IEventEmitter Emitter +@inject ICallbackEmitter Emitter +@inject IPluginOrchestrator PluginOrchestrator @code { @@ -199,11 +202,28 @@ _searchCancel = new(); await Task.Delay(500, _searchCancel.Token); + + var eventResult = await PluginOrchestrator.DispatchEvent(new SearchEvent(this) { + SearchTerm = _searchTerm, + Table = _config! + }); + if (eventResult.IsCanceled) return; + _searchTerm = eventResult.SearchTerm; + await Reload(); } private async Task Reload() { _loading = true; + + var eventResult = await PluginOrchestrator.DispatchEvent(new ReloadEvent(this) { + Table = _config! + }); + if (eventResult.IsCanceled) { + _loading = false; + return; + } + if (!string.IsNullOrEmpty(_searchTerm)) { (var query, _totalPages) = await _manager!.Search(_searchTerm, 0, PerPage); _currentlyDisplayedModels = query.ToArray(); @@ -215,6 +235,15 @@ } private async Task ChangePage(int page) { + var eventResult = await PluginOrchestrator.DispatchEvent(new PageChangeEvent(this) { + CurrentPage = _currentPage, + NewPage = page, + TotalPages = _totalPages, + Table = _config! + }); + if (eventResult.IsCanceled) return; + page = eventResult.NewPage; + if (page < 0 || page > _totalPages - 1) return; _currentPage = page; await Reload(); @@ -225,13 +254,19 @@ Navigator.NavigateTo("/admin", true); return; } + + var eventResult = await PluginOrchestrator.DispatchEvent(new DeleteEntryEvent(this) { + Entity = element, + Table = _config! + }); + if (eventResult.IsCanceled) return; var dialog = await Dialogs.ShowConfirmationAsync("Do you really want to delete this entry?"); var result = await dialog.Result; if (result.Cancelled) return; await _manager!.DeleteItem(element); - await Emitter.DispatchEvent(EventTypes.DeleteEntry(_config!), element); + await Emitter.DispatchCallback(CallbackTypes.DeleteEntry(_config!), element); await Reload(); } @@ -240,6 +275,22 @@ Navigator.NavigateTo("/admin", true); return; } + + HopFrameTablePageEventArgs eventArgs; + if (element is null) { + eventArgs = new CreateEntryEvent(this) { + Table = _config! + }; + } + else { + eventArgs = new UpdateEntryEvent(this) { + Table = _config!, + Entity = element + }; + } + + var eventResult = await PluginOrchestrator.DispatchEvent(eventArgs); + if (eventResult.IsCanceled) return; var panel = await Dialogs.ShowPanelAsync(new EditorDialogData(_config!, element), new DialogParameters { TrapFocus = false @@ -251,17 +302,25 @@ if (element is null) { await _manager!.AddItem(data!.CurrentObject!); - await Emitter.DispatchEvent(EventTypes.CreateEntry(_config!), data.CurrentObject!); + await Emitter.DispatchCallback(CallbackTypes.CreateEntry(_config!), data.CurrentObject!); } else { await _manager!.EditItem(data!.CurrentObject!); - await Emitter.DispatchEvent(EventTypes.UpdateEntry(_config!), data.CurrentObject!); + await Emitter.DispatchCallback(CallbackTypes.UpdateEntry(_config!), data.CurrentObject!); } await Reload(); } private void SelectItem(object item, bool selected) { + var eventResult = PluginOrchestrator.DispatchEvent(new SelectEntryEvent(this) { + Entity = item, + Selected = selected, + Table = _config! + }).Result; + if (eventResult.IsCanceled) return; + selected = eventResult.Selected; + if (!selected) DialogData!.SelectedObjects.Remove(item); else DialogData!.SelectedObjects.Add(item); diff --git a/src/HopFrame.Web/HopFrameConfiguratorExtensions.cs b/src/HopFrame.Web/HopFrameConfiguratorExtensions.cs index 3705ce3..7ebe959 100644 --- a/src/HopFrame.Web/HopFrameConfiguratorExtensions.cs +++ b/src/HopFrame.Web/HopFrameConfiguratorExtensions.cs @@ -1,6 +1,10 @@ -using HopFrame.Core.Config; +using System.Reflection; +using HopFrame.Core.Config; using HopFrame.Web.Components.Layout; using HopFrame.Web.Models; +using HopFrame.Web.Plugins; +using HopFrame.Web.Plugins.Annotations; +using HopFrame.Web.Plugins.Internal; namespace HopFrame.Web; @@ -28,5 +32,23 @@ public static class HopFrameConfiguratorExtensions { configuratorDelegate.Invoke(viewConfigurator); return configurator; } + + public static HopFrameConfigurator AddPlugin(this HopFrameConfigurator configurator) where TPlugin : HopFramePlugin { + PluginOrchestrator.RegisterPlugin(configurator.ServiceCollection, typeof(TPlugin)); + + var methods = typeof(TPlugin).GetMethods() + .Where(method => method.IsStatic) + .Where(method => method.GetCustomAttributes(true) + .Any(attr => attr is PluginConfiguratorAttribute)) + .Where(method => method.GetParameters().Length < 2); + + foreach (var method in methods) { + if (method.GetParameters().Length > 0) + method.Invoke(null, [configurator]); + else method.Invoke(null, []); + } + + return configurator; + } } \ No newline at end of file diff --git a/src/HopFrame.Web/Plugins/Annotations/EventHandlerAttribute.cs b/src/HopFrame.Web/Plugins/Annotations/EventHandlerAttribute.cs new file mode 100644 index 0000000..d6148ee --- /dev/null +++ b/src/HopFrame.Web/Plugins/Annotations/EventHandlerAttribute.cs @@ -0,0 +1,4 @@ +namespace HopFrame.Web.Plugins.Annotations; + +[AttributeUsage(AttributeTargets.Method)] +public class EventHandlerAttribute : Attribute; diff --git a/src/HopFrame.Web/Plugins/Annotations/PluginConfiguratorAttribute.cs b/src/HopFrame.Web/Plugins/Annotations/PluginConfiguratorAttribute.cs new file mode 100644 index 0000000..faf3a9b --- /dev/null +++ b/src/HopFrame.Web/Plugins/Annotations/PluginConfiguratorAttribute.cs @@ -0,0 +1,8 @@ +namespace HopFrame.Web.Plugins.Annotations; + +/// +/// Configures the method as a plugin configurator, so the method gets called, when the plugin is registered. +/// Only works on static methods +/// +[AttributeUsage(AttributeTargets.Method)] +public class PluginConfiguratorAttribute : Attribute; diff --git a/src/HopFrame.Web/Plugins/Events/EntryEvent.cs b/src/HopFrame.Web/Plugins/Events/EntryEvent.cs new file mode 100644 index 0000000..89ada2f --- /dev/null +++ b/src/HopFrame.Web/Plugins/Events/EntryEvent.cs @@ -0,0 +1,18 @@ +using HopFrame.Web.Components.Pages; + +namespace HopFrame.Web.Plugins.Events; + +public sealed class DeleteEntryEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) { + public required object Entity { get; init; } +} + +public sealed class CreateEntryEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender); + +public sealed class UpdateEntryEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) { + public required object Entity { get; init; } +} + +public sealed class SelectEntryEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) { + public required object Entity { get; init; } + public required bool Selected { get; set; } +} diff --git a/src/HopFrame.Web/Plugins/Events/HopFrameEventArgs.cs b/src/HopFrame.Web/Plugins/Events/HopFrameEventArgs.cs new file mode 100644 index 0000000..d15d8d5 --- /dev/null +++ b/src/HopFrame.Web/Plugins/Events/HopFrameEventArgs.cs @@ -0,0 +1,27 @@ +using HopFrame.Core.Config; +using HopFrame.Web.Components.Dialogs; +using HopFrame.Web.Components.Pages; + +namespace HopFrame.Web.Plugins.Events; + +public abstract class HopFrameEventArgs(object internalSender) { + internal object InternalSender { get; } = internalSender; + public bool IsCanceled { get; protected set; } + + + public void SetCancelled(bool canceled) => IsCanceled = canceled; +} + +public abstract class HopFrameEventArgs(TSender sender) : HopFrameEventArgs(sender) where TSender : class { + public TSender Sender => (TSender)InternalSender; +} + +public abstract class HopFrameTablePageEventArgs(HopFrameTablePage sender) + : HopFrameEventArgs(sender) { + public required TableConfig Table { get; init; } +} + +public abstract class HopFrameEditorEventArgs(HopFrameEditor sender) + : HopFrameEventArgs(sender) { + public required TableConfig Table { get; init; } +} diff --git a/src/HopFrame.Web/Plugins/Events/PageChangeEvent.cs b/src/HopFrame.Web/Plugins/Events/PageChangeEvent.cs new file mode 100644 index 0000000..44570ed --- /dev/null +++ b/src/HopFrame.Web/Plugins/Events/PageChangeEvent.cs @@ -0,0 +1,9 @@ +using HopFrame.Web.Components.Pages; + +namespace HopFrame.Web.Plugins.Events; + +public sealed class PageChangeEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) { + public required int CurrentPage { get; init; } + public required int TotalPages { get; init; } + public required int NewPage { get; set; } +} \ No newline at end of file diff --git a/src/HopFrame.Web/Plugins/Events/PluginEventContainer.cs b/src/HopFrame.Web/Plugins/Events/PluginEventContainer.cs new file mode 100644 index 0000000..6839424 --- /dev/null +++ b/src/HopFrame.Web/Plugins/Events/PluginEventContainer.cs @@ -0,0 +1,9 @@ +using System.Reflection; + +namespace HopFrame.Web.Plugins.Events; + +internal sealed class PluginEventContainer { + public required MethodInfo Handler { get; init; } + public required Type EventType { get; init; } + public required bool IsAwaitable { get; init; } +} \ No newline at end of file diff --git a/src/HopFrame.Web/Plugins/Events/ReloadEvent.cs b/src/HopFrame.Web/Plugins/Events/ReloadEvent.cs new file mode 100644 index 0000000..384197e --- /dev/null +++ b/src/HopFrame.Web/Plugins/Events/ReloadEvent.cs @@ -0,0 +1,7 @@ +using HopFrame.Web.Components.Pages; + +namespace HopFrame.Web.Plugins.Events; + +public sealed class ReloadEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) { + +} \ No newline at end of file diff --git a/src/HopFrame.Web/Plugins/Events/SearchEvent.cs b/src/HopFrame.Web/Plugins/Events/SearchEvent.cs new file mode 100644 index 0000000..2c0d4f7 --- /dev/null +++ b/src/HopFrame.Web/Plugins/Events/SearchEvent.cs @@ -0,0 +1,7 @@ +using HopFrame.Web.Components.Pages; + +namespace HopFrame.Web.Plugins.Events; + +public sealed class SearchEvent(HopFrameTablePage sender) : HopFrameTablePageEventArgs(sender) { + public required string SearchTerm { get; set; } +} \ No newline at end of file diff --git a/src/HopFrame.Web/Plugins/Events/ValidationEvent.cs b/src/HopFrame.Web/Plugins/Events/ValidationEvent.cs new file mode 100644 index 0000000..6c92f4d --- /dev/null +++ b/src/HopFrame.Web/Plugins/Events/ValidationEvent.cs @@ -0,0 +1,9 @@ +using HopFrame.Core.Config; +using HopFrame.Web.Components.Dialogs; + +namespace HopFrame.Web.Plugins.Events; + +public sealed class ValidationEvent(HopFrameEditor sender) : HopFrameEditorEventArgs(sender) { + public required IList Errors { get; init; } + public required PropertyConfig Property { get; init; } +} \ No newline at end of file diff --git a/src/HopFrame.Web/Plugins/HopFramePlugin.cs b/src/HopFrame.Web/Plugins/HopFramePlugin.cs new file mode 100644 index 0000000..84a3968 --- /dev/null +++ b/src/HopFrame.Web/Plugins/HopFramePlugin.cs @@ -0,0 +1,7 @@ +namespace HopFrame.Web.Plugins; + +public abstract class HopFramePlugin { + + + +} diff --git a/src/HopFrame.Web/Plugins/IPluginOrchestrator.cs b/src/HopFrame.Web/Plugins/IPluginOrchestrator.cs new file mode 100644 index 0000000..22b5cac --- /dev/null +++ b/src/HopFrame.Web/Plugins/IPluginOrchestrator.cs @@ -0,0 +1,5 @@ +namespace HopFrame.Web.Plugins; + +public interface IPluginOrchestrator { + public Task DispatchEvent(TEvent @event, CancellationToken ct = new()); +} \ No newline at end of file diff --git a/src/HopFrame.Web/Plugins/Internal/PluginOrchestrator.cs b/src/HopFrame.Web/Plugins/Internal/PluginOrchestrator.cs new file mode 100644 index 0000000..d0920b4 --- /dev/null +++ b/src/HopFrame.Web/Plugins/Internal/PluginOrchestrator.cs @@ -0,0 +1,46 @@ +using HopFrame.Web.Plugins.Annotations; +using HopFrame.Web.Plugins.Events; +using Microsoft.Extensions.DependencyInjection; + +namespace HopFrame.Web.Plugins.Internal; + +internal sealed class PluginOrchestrator(IServiceProvider services) : IPluginOrchestrator { + + public static void RegisterPlugin(IServiceCollection collection, Type plugin) { + var methods = plugin.GetMethods() + .Where(method => method.GetCustomAttributes(true) + .Any(attr => attr is EventHandlerAttribute)); + + foreach (var method in methods) { + var awaitable = method.ReturnType.IsAssignableFrom(typeof(Task)); + var eventType = method + .GetParameters() + .FirstOrDefault(param => param.ParameterType.IsAssignableTo(typeof(HopFrameEventArgs)))?.ParameterType; + + if (eventType is null) continue; + var container = new PluginEventContainer { + EventType = eventType, + IsAwaitable = awaitable, + Handler = method + }; + collection.AddSingleton(container); + collection.AddScoped(plugin); + } + } + + public async Task DispatchEvent(TEvent @event, CancellationToken ct = new()) { + var eventContainers = services.GetRequiredService>() + .Where(container => container.EventType == typeof(TEvent)); + + foreach (var container in eventContainers) { + var plugin = services.GetRequiredService(container.Handler.DeclaringType!); + var result = container.Handler.Invoke(plugin, [@event]); + + if (container.IsAwaitable) + await (Task)result!; + } + + return @event; + } + +} \ No newline at end of file diff --git a/src/HopFrame.Web/ServiceCollectionExtensions.cs b/src/HopFrame.Web/ServiceCollectionExtensions.cs index 6e72caf..ba4d590 100644 --- a/src/HopFrame.Web/ServiceCollectionExtensions.cs +++ b/src/HopFrame.Web/ServiceCollectionExtensions.cs @@ -1,8 +1,10 @@ using HopFrame.Core; using HopFrame.Core.Config; -using HopFrame.Core.Events; +using HopFrame.Core.Callbacks; using HopFrame.Web.Components; using HopFrame.Web.Components.Pages; +using HopFrame.Web.Plugins; +using HopFrame.Web.Plugins.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.AspNetCore.Builder; @@ -21,7 +23,7 @@ public static class ServiceCollectionExtensions { /// The same service collection that is passed in public static IServiceCollection AddHopFrame(this IServiceCollection services, Action configurator, LibraryConfiguration? fluentUiLibraryConfiguration = null, bool addRazorComponents = true) { var config = new HopFrameConfig(); - configurator.Invoke(new HopFrameConfigurator(config)); + configurator.Invoke(new HopFrameConfigurator(config, services)); return AddHopFrame(services, config, fluentUiLibraryConfiguration, addRazorComponents); } @@ -38,6 +40,8 @@ public static class ServiceCollectionExtensions { services.AddHopFrameServices(); services.AddFluentUIComponents(fluentUiLibraryConfiguration); + services.AddScoped(); + if (addRazorComponents) { services.AddRazorComponents() .AddInteractiveServerComponents(); diff --git a/testing/HopFrame.Testing/Program.cs b/testing/HopFrame.Testing/Program.cs index 843462a..c1ab677 100644 --- a/testing/HopFrame.Testing/Program.cs +++ b/testing/HopFrame.Testing/Program.cs @@ -80,6 +80,8 @@ builder.Services.AddHopFrame(options => { options.AddCustomView("Counter", "/counter") .SetDescription("A custom view") .SetPolicy("counter.view"); + + options.AddPlugin(); }); var app = builder.Build(); diff --git a/testing/HopFrame.Testing/TestPlugin.cs b/testing/HopFrame.Testing/TestPlugin.cs new file mode 100644 index 0000000..ed5f54b --- /dev/null +++ b/testing/HopFrame.Testing/TestPlugin.cs @@ -0,0 +1,26 @@ +using HopFrame.Core.Config; +using HopFrame.Testing.Models; +using HopFrame.Web.Plugins; +using HopFrame.Web.Plugins.Annotations; +using HopFrame.Web.Plugins.Events; + +namespace HopFrame.Testing; + +public class TestPlugin : HopFramePlugin { + + [PluginConfigurator] + public static void OnConfiguring(HopFrameConfigurator configurator) { + Console.WriteLine("Configurator invoked!"); + configurator.GetDbContext()! + .Table() + .Property(u => u.Email) + .SetDisplayName("Modified by Plugin!"); + } + + [EventHandler] + public void OnDelete(DeleteEntryEvent e) { + Console.WriteLine("Event called!"); + e.SetCancelled(true); + } + +} \ No newline at end of file