diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Extensions/HttpRequestExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/HttpRequestExtensions.cs new file mode 100644 index 00000000..474eb2aa --- /dev/null +++ b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/HttpRequestExtensions.cs @@ -0,0 +1,65 @@ +using Lombiq.HelpfulLibraries.Common.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace Microsoft.AspNetCore.Http; + +public static class HttpRequestExtensions +{ + /// + /// Returns the query string without the leading ? and without the keys in . + /// + public static string GetQueryWithout(this HttpRequest request, params string[] keysToExclude) + { + var query = HttpUtility.ParseQueryString(request.QueryString.Value ?? string.Empty); + return string.Join('&', query + .AllKeys + .Where(key => key != null && !keysToExclude.Exists(key.EqualsOrdinalIgnoreCase)) + .Select(key => $"{HttpUtility.UrlEncode(key)}={HttpUtility.UrlEncode(query[key] ?? string.Empty)}")); + } + + /// + /// Returns the current URL but appends a new query string entry. + /// + public static string GetLinkWithAdditionalQuery(this HttpRequest request, string queryString, string key, object value) + { + queryString ??= string.Empty; + if (queryString.StartsWith('?')) queryString = queryString[1..]; + + var pageQuery = string.IsNullOrEmpty(queryString) + ? StringHelper.CreateInvariant($"{key}={value}") + : StringHelper.CreateInvariant($"&{key}={value}"); + return $"{request.PathBase}{request.Path}?{queryString}{pageQuery}"; + } + + /// + /// Returns the current URL but appends a new query string entry. + /// + public static string GetLinkWithAdditionalQuery(this HttpRequest request, string key, object value) => + request.GetLinkWithAdditionalQuery(request.QueryString.Value, key, value); + + /// + /// Returns the current URL excluding any existing query string entry with the key , and with + /// a new - entry appended. + /// + public static string GetLinkWithDifferentQuery(this HttpRequest request, string key, object value) => + request.GetLinkWithAdditionalQuery(request.GetQueryWithout(key), key, value); + + /// + /// Returns the current URL but with the value of the query string entry of with the key + /// cycled to the next item in the . This can be useful for example to generate table + /// header links that cycle ascending-descending-unsorted sorting by that column. + /// + public static string GetLinkAndCycleQueryValue(this HttpRequest request, string key, params string[] values) + { + var query = HttpUtility.ParseQueryString(request.QueryString.Value ?? string.Empty); + + var value = query[key] ?? string.Empty; + var index = ((IList)values).IndexOf(value); + var newValue = values[(index + 1) % values.Length]; + + return request.GetLinkWithDifferentQuery(key, newValue); + } +} diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ServiceCollectionExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..dedf02d3 --- /dev/null +++ b/Lombiq.HelpfulLibraries.AspNetCore/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; + +namespace Lombiq.HelpfulLibraries.AspNetCore.Extensions; + +public static class ServiceCollectionExtensions +{ + /// + /// Configures to add the to the list of filters. + /// + public static void AddAsyncResultFilter(this IServiceCollection services) + where TFilter : IAsyncResultFilter => + services.Configure(options => options.Filters.Add(typeof(TFilter))); +} diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentHttpContextExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentHttpContextExtensions.cs index 7a2588c4..f746a6b4 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentHttpContextExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/ContentHttpContextExtensions.cs @@ -1,5 +1,6 @@ using Lombiq.HelpfulLibraries.OrchardCore.Mvc; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using OrchardCore.ContentManagement; using System; using System.Collections.Generic; @@ -76,4 +77,39 @@ public static string ActionTask( params (string Key, object Value)[] additionalArguments) where TController : ControllerBase => httpContext.Action(taskActionExpression.StripResult(), additionalArguments); + + /// + /// Returns the text of the MVC route value identified by (case-insensitive). + /// + public static string GetRouteValueString(this HttpContext httpContext, string name) => + httpContext?.Request.RouteValues.GetMaybe(name)?.ToString(); + + /// + /// Returns a value indicating whether the current MVC route matches the provided , and . + /// + public static bool IsAction(this HttpContext httpContext, string area, string controller, string action) => + httpContext?.Request.RouteValues is { } routeValues && + routeValues.GetMaybe(nameof(area))?.ToString() == area && + routeValues.GetMaybe(nameof(controller))?.ToString() == controller && + routeValues.GetMaybe(nameof(action))?.ToString() == action; + + /// + /// Returns a value indicating whether the current page is a content item display action. + /// + public static bool IsContentDisplay(this HttpContext httpContext) => + httpContext.IsAction("OrchardCore.Contents", "Item", "Display"); + + /// + /// Gets the content item from the database by the ID in the contentItemId or id route values. + /// + public static Task GetContentItemAsync(this HttpContext httpContext, string jsonPath = null) + { + var id = httpContext.GetRouteValueString(nameof(ContentItem.ContentItemId)); + if (string.IsNullOrWhiteSpace(id)) id = httpContext.GetRouteValueString("id"); + if (string.IsNullOrWhiteSpace(id)) return Task.FromResult(null); + + var contentManager = httpContext.RequestServices.GetRequiredService(); + return contentManager.GetAsync(id, jsonPath); + } } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs index 62541fa8..1fa131f8 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs @@ -1,5 +1,8 @@ +using Newtonsoft.Json.Linq; using OrchardCore.ContentManagement; using OrchardCore.Taxonomies.Models; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace Lombiq.HelpfulLibraries.OrchardCore.Contents; @@ -20,4 +23,29 @@ await _contentHandleManager.GetContentItemIdAsync($"alias:{alias}") is { } conte await _contentManager.GetAsync(contentItemId) is { } contentItem ? contentItem.As()?.Terms?.Find(term => term.ContentItemId == termId) : null; + + /// + /// Returns all child content items in a taxonomy tree. + /// + /// The root of the tree to enumerate. + /// + /// If the will be the first result so the collection is never + /// empty as long as isn't . + /// + /// An unsorted list of all child items. + public static IList GetAllChildren(ContentItem contentItem, bool includeSelf = false) + { + var results = new List(); + if (contentItem == null) return results; + if (includeSelf) results.Add(contentItem); + + var partTerms = contentItem.As()?.Terms ?? Enumerable.Empty(); + var itemTerms = (contentItem.Content.Terms as JArray)?.ToObject>() ?? Enumerable.Empty(); + foreach (var child in partTerms.Concat(itemTerms)) + { + results.AddRange(GetAllChildren(child, includeSelf: true)); + } + + return results; + } } diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Mvc/WidgetFilterBase.cs b/Lombiq.HelpfulLibraries.OrchardCore/Mvc/WidgetFilterBase.cs index e55bcd27..0578eaa3 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Mvc/WidgetFilterBase.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Mvc/WidgetFilterBase.cs @@ -62,7 +62,11 @@ protected WidgetFilterBase( /// /// Returns the object used as the view-model and passed to the widget shape. /// - protected abstract Task GetViewModelAsync(); + protected virtual Task GetViewModelAsync() => + throw new NotSupportedException($"Please override either overloads of \"{nameof(GetViewModelAsync)}\"!"); + + protected virtual Task GetViewModelAsync(ResultExecutingContext context) => + GetViewModelAsync(); public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { @@ -83,13 +87,17 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE if ((AdminOnly && !isAdmin) || (FrontEndOnly && isAdmin) || (_requiredPermission != null && !await _authorizationService.AuthorizeAsync(user, _requiredPermission)) || - await GetViewModelAsync() is not { } viewModel) + await GetViewModelAsync(context) is not { } viewModel) { await next(); return; } - await _layoutAccessor.AddShapeToZoneAsync(ZoneName, await _shapeFactory.CreateAsync(ViewName, viewModel)); + await _layoutAccessor.AddShapeToZoneAsync( + ZoneName, + ViewName == null && viewModel is IShape shape + ? shape + : await _shapeFactory.CreateAsync(ViewName, viewModel)); await next(); }