diff --git a/zero.Api/Endpoints/Integrations/IntegrationsController.cs b/zero.Api/Endpoints/Integrations/IntegrationsController.cs index 6ca90858..54bc668c 100644 --- a/zero.Api/Endpoints/Integrations/IntegrationsController.cs +++ b/zero.Api/Endpoints/Integrations/IntegrationsController.cs @@ -116,7 +116,7 @@ public class IntegrationsController : ZeroApiController //[ZeroAuthorize(CountryPermissions.Update)] public virtual async Task> Update(string alias, Integration updateModel, [FromQuery] string changeToken = null) { - if (alias != updateModel.TypeAlias) + if (alias != updateModel.Flavor) { return BadRequest(Result.Fail(nameof(alias), "@integration.errors.noaliasmatch")); } diff --git a/zero.Backoffice/Plugin.cs b/zero.Backoffice/Plugin.cs index 1e968bc7..70d2a79b 100644 --- a/zero.Backoffice/Plugin.cs +++ b/zero.Backoffice/Plugin.cs @@ -34,7 +34,7 @@ public class ZeroBackofficePlugin : ZeroPlugin services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddScoped(); services.AddSingleton(svc => { diff --git a/zero.Backoffice/Services/Sections/SectionService.cs b/zero.Backoffice/Services/Sections/SectionService.cs index d3fb5a02..6df2a2b9 100644 --- a/zero.Backoffice/Services/Sections/SectionService.cs +++ b/zero.Backoffice/Services/Sections/SectionService.cs @@ -5,7 +5,7 @@ namespace zero.Backoffice.Services; public class SectionService : ISectionService { - protected IOptions Options { get; set; } + protected IZeroOptions Options { get; set; } protected IBackofficeAssetFileSystem FileSystem { get; set; } @@ -16,7 +16,7 @@ public class SectionService : ISectionService protected IEnumerable SettingsGroups { get; set; } - public SectionService(IOptions options, IBackofficeAssetFileSystem fileSystem, ILogger logger, IEnumerable sections, IEnumerable settingsGroups) + public SectionService(IZeroOptions options, IBackofficeAssetFileSystem fileSystem, ILogger logger, IEnumerable sections, IEnumerable settingsGroups) { Options = options; FileSystem = fileSystem; @@ -88,7 +88,7 @@ public class SectionService : ISectionService List groups = new(); - //bool hasIntegrations = Options.For().Any(); + bool hasIntegrations = Options.For().GetAll().Any(); foreach (SettingsGroup group in SettingsGroups) { @@ -100,10 +100,11 @@ public class SectionService : ISectionService //{ // continue; //} - //if (area.Alias == Constants.Settings.Integrations && !hasIntegrations) - //{ - // continue; - //} + + if (area.Alias == Constants.Settings.Integrations && !hasIntegrations) + { + continue; + } //bool isPlugin = !(area is InternalSettingsArea); diff --git a/zero.Core/Configuration/ConfigurationModule.cs b/zero.Core/Configuration/ConfigurationModule.cs index b8d0e271..fc88f729 100644 --- a/zero.Core/Configuration/ConfigurationModule.cs +++ b/zero.Core/Configuration/ConfigurationModule.cs @@ -23,7 +23,13 @@ public class ConfigurationModule : ZeroModule services.AddScoped(); services.AddOptions().Bind(configuration.GetSection("Zero:Features")); - services.AddOptions(); - services.ConfigureOptions(); + + services.Configure(opts => + { + opts.Configure(cfg => + { + cfg.CanUseWithoutFlavors = false; + }); + }); } } \ No newline at end of file diff --git a/zero.Core/Configuration/Integrations/ConfigureFlavorJsonOptions.cs b/zero.Core/Configuration/Integrations/ConfigureFlavorJsonOptions.cs deleted file mode 100644 index 47087287..00000000 --- a/zero.Core/Configuration/Integrations/ConfigureFlavorJsonOptions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace zero.Configuration; - - -public class ConfigureIntegrationJsonOptions : IConfigureOptions -{ - private readonly IZeroOptions zeroOptions; - - public ConfigureIntegrationJsonOptions(IZeroOptions options) - { - zeroOptions = options; - } - - public void Configure(JsonOptions options) - { - options.JsonSerializerOptions.Converters.Add(new JsonIntegrationTypeVariantConverter(zeroOptions)); - } -} \ No newline at end of file diff --git a/zero.Core/Configuration/Integrations/FlavorOptionsExtensions.cs b/zero.Core/Configuration/Integrations/FlavorOptionsExtensions.cs new file mode 100644 index 00000000..3f8987eb --- /dev/null +++ b/zero.Core/Configuration/Integrations/FlavorOptionsExtensions.cs @@ -0,0 +1,21 @@ +using FluentValidation; + +namespace zero.Configuration; + +public static class FlavorOptionsExtensions +{ + public static void AddIntegration(this FlavorOptions options, string alias, string name, string description, string editorAlias = null, List tags = default, string imagePath = null, IValidator validator = null) where T : Integration, new() + { + options.Add(new IntegrationType(typeof(T)) + { + Alias = alias, + Name = name, + Description = description, + ImagePath = imagePath, + Tags = tags, + Validator = validator, + Construct = cfg => new T(), + EditorAlias = editorAlias + }); + } +} diff --git a/zero.Core/Configuration/Integrations/Integration.cs b/zero.Core/Configuration/Integrations/Integration.cs index 223a1326..d714cbd3 100644 --- a/zero.Core/Configuration/Integrations/Integration.cs +++ b/zero.Core/Configuration/Integrations/Integration.cs @@ -9,6 +9,4 @@ namespace zero.Configuration; [RavenCollection("Integrations")] public class Integration : ZeroEntity { - /// - public string TypeAlias { get; set; } } \ No newline at end of file diff --git a/zero.Core/Configuration/Integrations/IntegrationOptions.cs b/zero.Core/Configuration/Integrations/IntegrationOptions.cs deleted file mode 100644 index f014749c..00000000 --- a/zero.Core/Configuration/Integrations/IntegrationOptions.cs +++ /dev/null @@ -1,42 +0,0 @@ -using FluentValidation; - -namespace zero.Configuration; - -public class IntegrationOptions : List -{ - public void Add(string alias, string name, string description, string editorAlias = null, List tags = default, string imagePath = null, IValidator validator = null) where T : Integration , new() - { - Add(new IntegrationType(typeof(T)) - { - Alias = alias, - Name = name, - Description = description, - ImagePath = imagePath, - Tags = tags, - Validator = validator, - Construct = cfg => new T(), - EditorAlias = editorAlias - }); - } - - - public void Add(Type type, string alias, string name, string description, string editorAlias = null, List tags = default, string imagePath = null, IValidator validator = null) - { - if (!typeof(Integration).IsAssignableFrom(type)) - { - throw new ArgumentException("Type has to inherit the Integration base model", nameof(type)); - } - - Add(new IntegrationType(type) - { - Alias = alias, - Name = name, - Description = description, - ImagePath = imagePath, - Tags = tags, - Validator = validator, - Construct = cfg => Activator.CreateInstance(type) as Integration, - EditorAlias = editorAlias - }); - } -} diff --git a/zero.Core/Configuration/Integrations/IntegrationStore.cs b/zero.Core/Configuration/Integrations/IntegrationStore.cs index 99b9bf0a..e4ed5ea2 100644 --- a/zero.Core/Configuration/Integrations/IntegrationStore.cs +++ b/zero.Core/Configuration/Integrations/IntegrationStore.cs @@ -17,24 +17,16 @@ public class IntegrationStore : IIntegrationStore /// - public virtual Task Empty(string alias) + public virtual async Task Empty(string alias) { - IntegrationType type = IntegrationTypes.GetByAlias(alias); - Integration integration = type?.Construct(type); - - if (integration != null) - { - integration.TypeAlias = alias; - } - - return Task.FromResult(integration); + return await Operations.Empty(alias); } /// public virtual async Task Load(string alias) { - Paged result = await Operations.Load(1, 1, q => q.Where(x => x.TypeAlias == alias)); + Paged result = await Operations.Load(1, 1, q => q.Where(x => x.Flavor == alias)); return result.Items.FirstOrDefault(); } @@ -104,19 +96,19 @@ public class IntegrationStore : IIntegrationStore return Result.Fail("@integration.errors.notfound"); } - IntegrationType type = IntegrationTypes.GetByAlias(model.TypeAlias); + IntegrationType type = IntegrationTypes.GetByAlias(model.Flavor); if (type == null) { return Result.Fail("@integration.errors.typenotfound"); } - if (create && await Operations.Any(q => q.Where(x => x.TypeAlias == model.TypeAlias))) + if (create && await Operations.Any(q => q.Where(x => x.Flavor == model.Flavor))) { return Result.Fail("@integration.errors.multiplenotallowed"); } - if (!create && await Operations.Any(q => q.Where(x => x.TypeAlias == model.TypeAlias && x.Id != model.Id))) + if (!create && await Operations.Any(q => q.Where(x => x.Flavor == model.Flavor && x.Id != model.Id))) { return Result.Fail("@integration.errors.alreadycreated"); } diff --git a/zero.Core/Configuration/Integrations/IntegrationType.cs b/zero.Core/Configuration/Integrations/IntegrationType.cs index 801da235..eaddbac2 100644 --- a/zero.Core/Configuration/Integrations/IntegrationType.cs +++ b/zero.Core/Configuration/Integrations/IntegrationType.cs @@ -7,39 +7,20 @@ namespace zero.Configuration; /// An integration is an application part which has a public configuration per app. /// It's up to the user to provide functionality. /// -public class IntegrationType +public class IntegrationType : FlavorConfig { - /// - /// Type of the associated entity - /// - [JsonIgnore] - public Type ModelType { get; private set; } + public IntegrationType(Type type) : base(type) { } /// /// Alias to find the editor schema /// public string EditorAlias { get; set; } - /// - /// Alias for querying - /// - public string Alias { get; set; } - - /// - /// Name of the flavor - /// - public string Name { get; set; } - /// /// Group integrations by tags /// public List Tags { get; set; } = new(); - /// - /// Optional description - /// - public string Description { get; set; } - /// /// Image of the integration /// @@ -50,12 +31,4 @@ public class IntegrationType /// [JsonIgnore] public IValidator Validator { get; set; } - - [JsonIgnore] - public Func Construct { get; set; } - - public IntegrationType(Type type) - { - ModelType = type; - } } diff --git a/zero.Core/Configuration/Integrations/IntegrationTypeService.cs b/zero.Core/Configuration/Integrations/IntegrationTypeService.cs index 8de09210..9627b393 100644 --- a/zero.Core/Configuration/Integrations/IntegrationTypeService.cs +++ b/zero.Core/Configuration/Integrations/IntegrationTypeService.cs @@ -6,36 +6,35 @@ public class IntegrationTypeService : IIntegrationTypeService protected IHandlerHolder Handler { get; private set; } - protected IntegrationOptions Types { get; set; } + protected FlavorOptions Flavors { get; set; } public IntegrationTypeService(IZeroContext context, IZeroOptions options, IHandlerHolder handler) { Context = context; Handler = handler; - Types = options.For(); + Flavors = options.For(); } /// public IEnumerable GetAll() { - return Types; + return Flavors.GetAll().Select(x => (IntegrationType)x); } /// public IntegrationType GetByAlias(string integrationTypeAlias) { - return Types.FirstOrDefault(x => x.Alias == integrationTypeAlias); + return Flavors.Get(integrationTypeAlias) as IntegrationType; } /// public IntegrationType GetByType() where T : Integration { - Type type = typeof(T); - return Types.FirstOrDefault(x => x.ModelType == type); + return Flavors.Get() as IntegrationType; } } diff --git a/zero.Core/Configuration/Integrations/IntegrationsCollection.cs b/zero.Core/Configuration/Integrations/IntegrationsCollection.cs deleted file mode 100644 index 4e727868..00000000 --- a/zero.Core/Configuration/Integrations/IntegrationsCollection.cs +++ /dev/null @@ -1,281 +0,0 @@ -//using Microsoft.Extensions.Logging; -//using Raven.Client.Documents; -//using Raven.Client.Documents.Linq; - -//namespace zero.Core.Configuration; - -//public class IntegrationsCollection : EntityStore, IIntegrationsCollection -//{ -// /// -// public IReadOnlyCollection RegisteredTypes { get; private set; } - -// protected ILogger Logger { get; private set; } - - -// public IntegrationsCollection(IStoreContext context, ILogger logger) : base(context) -// { -// Options = new(true); -// RegisteredTypes = Context.Options.Integrations.GetAllItems(); -// Logger = logger; -// } - - -// /// -// public Integration GetEmpty(string alias) -// { -// IntegrationType type = RegisteredTypes.FirstOrDefault(x => x.Alias.Equals(alias, StringComparison.InvariantCultureIgnoreCase)); - -// if (type == null) -// { -// return null; -// } - -// try -// { -// Integration model = Activator.CreateInstance(type.ContentType) as Integration; -// model.TypeAlias = type.Alias; -// return model; -// } -// catch -// { -// Logger.LogWarning("Could not create integration with type {alias}", alias); -// } - -// return null; -// } - - -// /// -// public async Task GetByAlias(string alias) -// { -// IntegrationType type = RegisteredTypes.FirstOrDefault(x => x.Alias.Equals(alias, StringComparison.InvariantCultureIgnoreCase)); -// return await Load(type); -// } - - -// /// -// public async Task Get(string alias = null) where T : Integration, new() -// { -// IntegrationType type = RegisteredTypes.FirstOrDefault(x => x.ContentType == typeof(T) && (alias.IsNullOrEmpty() || x.Alias.Equals(alias, StringComparison.InvariantCultureIgnoreCase))); -// return await Load(type); -// } - - -// /// -// public async Task> GetByTag(string tag) -// { -// IEnumerable types = RegisteredTypes.Where(x => x.Tags.Contains(tag, StringComparer.InvariantCultureIgnoreCase)); - -// if (!types.Any()) -// { -// return new List(); -// } - -// string[] aliases = types.Select(x => x.Alias).ToArray(); -// return await Session.Query().Where(x => x.TypeAlias.In(aliases)).ToListAsync(); -// } - - -// /// -// public async Task Any(string tag) -// { -// return (await GetByTag(tag)).Any(x => x.IsActive); -// } - - -// /// -// public override async Task> Load(ListQuery query) -// { -// List result = new(); -// List models = await Session.Query().ToListAsync(); - -// foreach (IntegrationType type in RegisteredTypes) -// { -// Integration model = models.FirstOrDefault(x => x.TypeAlias == type.Alias); - -// if (model != null) -// { -// model.Name = type.Name; -// result.Add(model); -// } -// } - -// return result.ToQueriedList(query); -// } - - -// /// -// public async Task> GetTypesWithStatus() -// { -// List result = new(); -// List models = await Session.Query().ToListAsync(); - -// foreach (IntegrationType type in RegisteredTypes) -// { -// Integration model = models.FirstOrDefault(x => x.TypeAlias == type.Alias); - -// result.Add(new() -// { -// Type = type, -// Id = model?.Id, -// IsActive = model?.IsActive ?? false, -// IsConfigured = model != null -// }); -// } - -// return result.OrderByDescending(x => x.IsActive).ThenByDescending(x => x.IsConfigured).ThenByDescending(x => x.Type.Name).ToList(); -// } - - -// /// -// public override async Task> Save(Integration model) -// { -// if (model == null) -// { -// return Result.Fail("@integration.errors.notfound"); -// } - -// IntegrationType type = RegisteredTypes.FirstOrDefault(x => x.Alias.Equals(model.TypeAlias, StringComparison.InvariantCultureIgnoreCase)); - -// if (type == null) -// { -// return Result.Fail("@integration.errors.typenotfound"); -// } - -// string existingId = await Session.Query().Where(x => x.TypeAlias == type.Alias).Select(x => x.Id).FirstOrDefaultAsync(); - -// if (!existingId.IsNullOrEmpty() && existingId != model.Id) -// { -// return Result.Fail("@integration.errors.multiplenotallowed"); -// } - -// model.Alias = type.Alias; -// model.Name = null; - -// Result result = await base.Save(model); - -// if (result.IsSuccess) -// { -// result.Model.Name = type.Name; -// } - -// return result; -// } - - -// /// -// public async Task> Activate(string alias) -// { -// Integration model = await GetByAlias(alias); -// if (model != null) -// { -// model.IsActive = true; -// } -// return await Save(model); -// } - - -// /// -// public async Task> Deactivate(string alias) -// { -// Integration model = await GetByAlias(alias); -// if (model != null) -// { -// model.IsActive = false; -// } -// return await Save(model); -// } - - -// /// -// public async Task> DeleteByAlias(string alias) -// { -// return await base.Delete(await GetByAlias(alias)); -// } - - -// /// -// /// Get integration data from database -// /// -// protected async Task Load(IntegrationType type) where T : Integration, new() -// { -// if (type == null) -// { -// return default; -// } - -// T entity = await Session.Query().FirstOrDefaultAsync(x => x.TypeAlias == type.Alias); - -// if (entity != null) -// { -// entity.Name = type.Name; -// entity.TypeAlias = type.Alias; -// } - -// return WhenActive(entity) as T; -// } -//} - - -//public interface IIntegrationsCollection -//{ -// /// -// /// Get all registered integration types -// /// -// IReadOnlyCollection RegisteredTypes { get; } - -// /// -// /// Get new integration model for the specified integration type alias -// /// -// Integration GetEmpty(string alias); - -// /// -// /// Get integration by an alias -// /// -// Task GetByAlias(string alias); - -// /// -// /// Get an integration by type and optional alias -// /// -// Task Get(string alias = null) where T : Integration, new(); - -// /// -// /// Get all integrations by a certain tag -// /// -// Task> GetByTag(string tag); - -// /// -// /// Check if any integrations of certain tag are activated -// /// -// Task Any(string tag); - -// /// -// /// Get all integrations with the specified query -// /// -// Task> Load(ListQuery query); - -// /// -// /// Get all integration types with their configuration status -// /// -// Task> GetTypesWithStatus(); - -// /// -// /// Saves an integration -// /// -// Task> Save(Integration model); - -// /// -// /// Activates a configured integration -// /// -// Task> Activate(string alias); - -// /// -// /// Disables a configured integration -// /// -// Task> Deactivate(string alias); - -// /// -// /// Deletes configuration of an integration and disables it -// /// -// Task> DeleteByAlias(string alias); -//} diff --git a/zero.Core/Configuration/Integrations/JsonFlavorVariantConverter.cs b/zero.Core/Configuration/Integrations/JsonFlavorVariantConverter.cs deleted file mode 100644 index c96103fe..00000000 --- a/zero.Core/Configuration/Integrations/JsonFlavorVariantConverter.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace zero.Configuration; - -public class JsonIntegrationTypeVariantConverter : JsonDiscriminatorConverter -{ - protected IntegrationOptions Integrations { get; private set; } - - - public JsonIntegrationTypeVariantConverter(IZeroOptions options) : base("typeAlias") - { - Integrations = options.For(); - } - - protected override Type GetTypeFromDiscriminator(Type requestedType, string discriminator) - { - IntegrationType type = Integrations.FirstOrDefault(x => x.Alias == discriminator); - return type?.ModelType ?? requestedType; - } -} \ No newline at end of file diff --git a/zero.Demo/Program.cs b/zero.Demo/Program.cs index 35c055d6..c69970e7 100644 --- a/zero.Demo/Program.cs +++ b/zero.Demo/Program.cs @@ -29,10 +29,13 @@ builder.Services.Configure(opts => builder.Services.Configure(opts => { opts.AddSpaceList("team", "Team", "Members of our team", "fth-users", "spaces.team"); + opts.AddIntegration("analytics.fathom", "Fathom Analytics", "Connect your website to Fathom and track page views", tags: new() { "analytics" }, imagePath: "/assets/fathom.png"); + opts.AddIntegration("analytics.google", "Google Analytics", "Connect your website to google analytics", tags: new() { "analytics" }, imagePath: "/assets/googleanalytics.png"); opts.Configure(x => x.CanUseWithoutFlavors = false); opts.Add("eu_country", "EU country", "A country within the European Union", "fth-globe"); opts.Add("usa_country", "USA", "A country in the United States", "fth-flag"); + }); builder.Services.Configure(opts => @@ -42,12 +45,6 @@ builder.Services.Configure(opts => opts.Add(); }); -builder.Services.Configure(opts => -{ - opts.Add("analytics.fathom", "Fathom Analytics", "Connect your website to Fathom and track page views", tags: new() { "analytics" }, imagePath: "/assets/fathom.png"); - opts.Add("analytics.google", "Google Analytics", "Connect your website to google analytics", tags: new() { "analytics" }, imagePath: "/assets/googleanalytics.png"); -}); - var app = builder.Build(); // Configure the HTTP request pipeline.