diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringExtensions.cs index 5140451e..0ce75535 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringExtensions.cs @@ -1,6 +1,8 @@ using Microsoft.AspNetCore.Html; using Newtonsoft.Json; using System.IO; +using System.Linq; +using System.Text; using System.Text.Encodings.Web; namespace Microsoft.AspNetCore.Mvc.Localization; @@ -29,4 +31,29 @@ public static string Html(this IHtmlContent htmlContent) htmlContent.WriteTo(stringWriter, HtmlEncoder.Default); return stringWriter.ToString(); } + + /// + /// Concatenates the localized HTML string with the provided HTML + /// content. This is suitable for joining individually localizable HTML strings. + /// + public static LocalizedHtmlString Concat(this LocalizedHtmlString first, params IHtmlContent[] other) + { + if (other.Length == 0) return first; + + var builder = new StringBuilder(first.Html()); + foreach (var content in other) builder.Append(content.Html()); + var html = builder.ToString(); + + return new LocalizedHtmlString(html, html); + } + + public static LocalizedHtmlString Join(this IHtmlContent separator, params LocalizedHtmlString[] items) + { + if (items.Length == 0) return null; + + var first = items[0]; + var other = items.Skip(1).SelectMany(item => new[] { separator, item }).ToArray(); + + return first.Concat(other); + } } diff --git a/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs b/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs index 1038e497..a3f081e4 100644 --- a/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs +++ b/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; @@ -165,4 +166,101 @@ public static void AddToList( /// public static IReadOnlyDictionary ToReadOnly(this IDictionary dictionary) => dictionary as IReadOnlyDictionary ?? new Dictionary(dictionary); + + /// + /// Creates a new dictionary based on by taking each entry's value, and if it's not + /// and has at least one not item then that item is added under the + /// same key. + /// + [SuppressMessage( + "Design", + "MA0016:Prefer returning collection abstraction instead of implementation", + Justification = "Better compatibility.")] + public static Dictionary WithFirstValues( + this IEnumerable> dictionary, + Func select = null) + where TValues : IEnumerable + { + var result = new Dictionary(); + + foreach (var (key, values) in dictionary) + { + if (select == null) + { + if (values is not null && values.FirstOrDefault() is { } value) + { + result[key] = value; + } + } + else + { + if (select(values) is { } value) + { + result[key] = value; + } + } + } + + return result; + } + + /// + /// Creates a new dictionary based on where each value is a new instance of + /// which is an of . If the value + /// is not then it's added to the list. + /// + [SuppressMessage( + "Design", + "MA0016:Prefer returning collection abstraction instead of implementation", + Justification = "Better compatibility.")] + public static Dictionary ToListValuedDictionary( + this IEnumerable> dictionary, + Func select = null) + where TValues : IList, new() + { + var result = new Dictionary(); + + foreach (var (key, value) in dictionary) + { + if (select != null) + { + result[key] = select(value); + continue; + } + + var list = new TValues(); + if (value is not null) list.Add(value); + result[key] = list; + } + + return result; + } + + /// + /// Creates a new string keyed dictionary where the key is compared in a case-insensitive manner. + /// + public static IDictionary ToDictionaryIgnoreCase( + this IEnumerable> source) => + new Dictionary(source, StringComparer.OrdinalIgnoreCase); + + /// + /// Creates a new string keyed dictionary where the key is compared in a case-insensitive manner. + /// + public static IDictionary ToDictionaryIgnoreCase( + this IEnumerable source, + Func keySelector) => + new Dictionary( + source.Select(item => new KeyValuePair(keySelector(item), item)), + StringComparer.OrdinalIgnoreCase); + + /// + /// Creates a new string keyed dictionary where the key is compared in a case-insensitive manner. + /// + public static IDictionary ToDictionaryIgnoreCase( + this IEnumerable source, + Func keySelector, + Func valueSelector) => + new Dictionary( + source.Select(item => new KeyValuePair(keySelector(item), valueSelector(item))), + StringComparer.OrdinalIgnoreCase); } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/CommonContentDisplayTypes.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/CommonContentDisplayTypes.cs index c9c08ce6..10f3bea2 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/CommonContentDisplayTypes.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/CommonContentDisplayTypes.cs @@ -11,4 +11,5 @@ public static class CommonContentDisplayTypes public const string Detail = nameof(Detail); public const string Summary = nameof(Summary); public const string SummaryAdmin = nameof(SummaryAdmin); + public const string Thumbnail = nameof(Thumbnail); } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Validation.md b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Validation.md new file mode 100644 index 00000000..494a017b --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Validation.md @@ -0,0 +1,5 @@ +# Lombiq Helpful Libraries - Orchard Core Libraries - Validation for Orchard Core + +## `IEmailAndPasswordValidator` + +A service for returning validation errors for email of password. Use its `ValidateEmailAsync` method to get email format validation errors, and its `ValidatePasswordAsync` to get validation errors from the registered `IPasswordValidator` instances. diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Liquid/ILiquidContentDisplayService.cs b/Lombiq.HelpfulLibraries.OrchardCore/Liquid/ILiquidContentDisplayService.cs new file mode 100644 index 00000000..d9d308eb --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Liquid/ILiquidContentDisplayService.cs @@ -0,0 +1,33 @@ +using Fluid.Values; +using OrchardCore.DisplayManagement; +using OrchardCore.DisplayManagement.Implementation; +using System; +using System.Threading.Tasks; + +namespace Lombiq.HelpfulLibraries.OrchardCore.Liquid; + +/// +/// Service that displays shapes as HTML in a . +/// +public interface ILiquidContentDisplayService +{ + /// + /// Creates new instances of a dynamic shape object and renders it to HTML in a . + /// + ValueTask DisplayShapeAsync(IShape shape); + + /// + /// Creates new instances of a typed shape object and renders it to HTML in a . + /// + /// The type to instantiate. + ValueTask DisplayNewAsync(string shapeType, Action initialize); + + /// + /// Displays an already instantiated as a . + /// + ValueTask DisplayNewAsync( + string shapeType, + Func> shapeFactory, + Action creating = null, + Action created = null); +} diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Liquid/LiquidContentDisplayService.cs b/Lombiq.HelpfulLibraries.OrchardCore/Liquid/LiquidContentDisplayService.cs new file mode 100644 index 00000000..5cefd7ac --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Liquid/LiquidContentDisplayService.cs @@ -0,0 +1,41 @@ +using Fluid.Values; +using OrchardCore.DisplayManagement; +using OrchardCore.DisplayManagement.Implementation; +using OrchardCore.DisplayManagement.Liquid; +using System; +using System.Threading.Tasks; + +namespace Lombiq.HelpfulLibraries.OrchardCore.Liquid; + +public class LiquidContentDisplayService : ILiquidContentDisplayService +{ + private readonly IDisplayHelper _displayHelper; + private readonly IShapeFactory _shapeFactory; + public LiquidContentDisplayService(IDisplayHelper displayHelper, IShapeFactory shapeFactory) + { + _displayHelper = displayHelper; + _shapeFactory = shapeFactory; + } + + public async ValueTask DisplayNewAsync( + string shapeType, + Func> shapeFactory, + Action creating = null, + Action created = null) + { + var shape = await _shapeFactory.CreateAsync(shapeType, shapeFactory, creating, created); + return await DisplayShapeAsync(shape); + } + + public async ValueTask DisplayNewAsync(string shapeType, Action initialize) + { + var shape = await _shapeFactory.CreateAsync(shapeType, initialize); + return await DisplayShapeAsync(shape); + } + + public async ValueTask DisplayShapeAsync(IShape shape) + { + var content = await _displayHelper.ShapeExecuteAsync(shape); + return new HtmlContentValue(content); + } +} diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Navigation/NavigationItemBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Navigation/NavigationItemBuilderExtensions.cs index 1d83f587..5b71a0a2 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Navigation/NavigationItemBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Navigation/NavigationItemBuilderExtensions.cs @@ -60,4 +60,10 @@ public static NavigationBuilder AddSeparator(this NavigationBuilder builder, ISt /// public static NavigationBuilder AddLabel(this NavigationBuilder builder, LocalizedString label) => builder.Add(label, subMenu => subMenu.Url("#").AddClass("disabled menuWidget__link_label")); + + /// + /// Adds a link to the menu item pointing to the site settings page identified by the . + /// + public static NavigationItemBuilder SiteSettings(this NavigationItemBuilder builder, string groupId) => + builder.Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId }); } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Validation/EmailAndPasswordValidator.cs b/Lombiq.HelpfulLibraries.OrchardCore/Validation/EmailAndPasswordValidator.cs new file mode 100644 index 00000000..cc5d9663 --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Validation/EmailAndPasswordValidator.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Localization; +using OrchardCore.Email; +using OrchardCore.Users; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Lombiq.HelpfulLibraries.OrchardCore.Validation; + +public class EmailAndPasswordValidator : IEmailAndPasswordValidator +{ + private readonly IEmailAddressValidator _emailAddressValidator; + private readonly UserManager _userManager; + private readonly IStringLocalizer T; + public EmailAndPasswordValidator( + IEmailAddressValidator emailAddressValidator, + UserManager userManager, + IStringLocalizer stringLocalizer) + { + _emailAddressValidator = emailAddressValidator; + _userManager = userManager; + T = stringLocalizer; + } + + public Task> ValidateEmailAsync(string email) => + Task.FromResult(_emailAddressValidator.Validate(email) + ? Enumerable.Empty() + : new[] { T["Invalid email address."] }); + + public async Task> ValidatePasswordAsync(string password) + { + var errors = new List(); + if (password == null) return errors; + + foreach (var passwordValidator in _userManager.PasswordValidators) + { + var result = await passwordValidator.ValidateAsync(_userManager, user: null, password); + + if (result.Succeeded) continue; + errors.AddRange(result.Errors.Select(error => T[error.Description])); + } + + return errors; + } +} diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Validation/IEmailAndPasswordValidator.cs b/Lombiq.HelpfulLibraries.OrchardCore/Validation/IEmailAndPasswordValidator.cs new file mode 100644 index 00000000..bc6bd600 --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Validation/IEmailAndPasswordValidator.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Localization; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Lombiq.HelpfulLibraries.OrchardCore.Validation; + +/// +/// A service for returning validation errors for email of password. +/// +public interface IEmailAndPasswordValidator +{ + /// + /// Validates the provided and returns any validation errors as a localized string. + /// + Task> ValidateEmailAsync(string email); + + /// + /// Validates the provided and returns any validation errors as a localized string. + /// + Task> ValidatePasswordAsync(string password); +}