From f41604ba1e61e3679bcbfd560f4e09c0c6f19882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Wed, 14 Jun 2023 13:53:41 +0200 Subject: [PATCH 1/7] Add condition display driver. --- .../Widgets/Drivers/ConditionDisplayDriver.cs | 24 +++++++++++++++++++ .../Widgets/ViewModels/ConditionViewModel.cs | 18 ++++++++++++++ .../Lombiq.HelpfulExtensions.csproj | 1 + .../Views/Condition.Fields.Summary.cshtml | 3 +++ .../Views/Condition.Fields.Thumbnail.cshtml | 4 ++++ .../Views/_ViewImports.cshtml | 2 ++ 6 files changed, 52 insertions(+) create mode 100644 Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/ConditionDisplayDriver.cs create mode 100644 Lombiq.HelpfulExtensions/Extensions/Widgets/ViewModels/ConditionViewModel.cs create mode 100644 Lombiq.HelpfulExtensions/Views/Condition.Fields.Summary.cshtml create mode 100644 Lombiq.HelpfulExtensions/Views/Condition.Fields.Thumbnail.cshtml diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/ConditionDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/ConditionDisplayDriver.cs new file mode 100644 index 00000000..6b80984d --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/ConditionDisplayDriver.cs @@ -0,0 +1,24 @@ +using Lombiq.HelpfulExtensions.Extensions.Widgets.ViewModels; +using Lombiq.HelpfulLibraries.OrchardCore.Contents; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Rules; + +namespace Lombiq.HelpfulExtensions.Extensions.Widgets.Drivers; + +public abstract class ConditionDisplayDriver : DisplayDriver + where TCondition : Condition +{ + public override IDisplayResult Display(TCondition model) => + Combine( + InitializeDisplayType(CommonContentDisplayTypes.Summary, model), + InitializeDisplayType(CommonContentDisplayTypes.Thumbnail, model)); + + protected abstract ConditionViewModel GetConditionViewModel(TCondition condition); + + private ShapeResult InitializeDisplayType(string displayType, TCondition model) => + Initialize( + "Condition_Fields_" + displayType, + target => GetConditionViewModel(model).CopyTo(target)) + .Location(displayType, CommonLocationNames.Content); +} diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/ViewModels/ConditionViewModel.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/ViewModels/ConditionViewModel.cs new file mode 100644 index 00000000..1a7133e3 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/ViewModels/ConditionViewModel.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Localization; + +namespace Lombiq.HelpfulExtensions.Extensions.Widgets.ViewModels; + +public class ConditionViewModel +{ + public LocalizedHtmlString Title { get; set; } + public LocalizedHtmlString Description { get; set; } + public IHtmlContent SummaryHint { get; set; } + + public void CopyTo(ConditionViewModel target) + { + target.Title = Title; + target.Description = Description; + target.SummaryHint = SummaryHint; + } +} diff --git a/Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj b/Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj index 9db870b8..d9314a82 100644 --- a/Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj +++ b/Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj @@ -45,6 +45,7 @@ + diff --git a/Lombiq.HelpfulExtensions/Views/Condition.Fields.Summary.cshtml b/Lombiq.HelpfulExtensions/Views/Condition.Fields.Summary.cshtml new file mode 100644 index 00000000..588ddf1c --- /dev/null +++ b/Lombiq.HelpfulExtensions/Views/Condition.Fields.Summary.cshtml @@ -0,0 +1,3 @@ +@model ConditionViewModel + +@Model.Title@Model.SummaryHint diff --git a/Lombiq.HelpfulExtensions/Views/Condition.Fields.Thumbnail.cshtml b/Lombiq.HelpfulExtensions/Views/Condition.Fields.Thumbnail.cshtml new file mode 100644 index 00000000..022276dd --- /dev/null +++ b/Lombiq.HelpfulExtensions/Views/Condition.Fields.Thumbnail.cshtml @@ -0,0 +1,4 @@ +@model ConditionViewModel + +

@Model.Title

+

@Model.Description

diff --git a/Lombiq.HelpfulExtensions/Views/_ViewImports.cshtml b/Lombiq.HelpfulExtensions/Views/_ViewImports.cshtml index 8dc399d4..c2fea0b0 100644 --- a/Lombiq.HelpfulExtensions/Views/_ViewImports.cshtml +++ b/Lombiq.HelpfulExtensions/Views/_ViewImports.cshtml @@ -4,3 +4,5 @@ @addTagHelper *, OrchardCore.DisplayManagement @addTagHelper *, OrchardCore.ResourceManagement @addTagHelper *, OrchardCore.Contents + +@using Lombiq.HelpfulExtensions.Extensions.Widgets.ViewModels From bde2b70a4dbf191b1ea45006dd9ad25a0602af2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Wed, 14 Jun 2023 14:11:08 +0200 Subject: [PATCH 2/7] Add condition title. --- .../Widgets/Drivers/ConditionDisplayDriver.cs | 11 +++++++++-- .../Extensions/Widgets/Startup.cs | 17 ++++++++++++++++- .../Views/Condition.Fields.Detail.Title.cshtml | 3 +++ .../Views/_ViewImports.cshtml | 1 + 4 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 Lombiq.HelpfulExtensions/Views/Condition.Fields.Detail.Title.cshtml diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/ConditionDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/ConditionDisplayDriver.cs index 6b80984d..79844ec8 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/ConditionDisplayDriver.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/ConditionDisplayDriver.cs @@ -3,6 +3,7 @@ using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using OrchardCore.Rules; +using System.Collections.Generic; namespace Lombiq.HelpfulExtensions.Extensions.Widgets.Drivers; @@ -14,11 +15,17 @@ public override IDisplayResult Display(TCondition model) => InitializeDisplayType(CommonContentDisplayTypes.Summary, model), InitializeDisplayType(CommonContentDisplayTypes.Thumbnail, model)); + public override IDisplayResult Edit(TCondition model) => + Combine( + InitializeDisplayType(CommonContentDisplayTypes.Detail, model, "Title"), + GetEditor(model)); + protected abstract ConditionViewModel GetConditionViewModel(TCondition condition); + protected abstract IDisplayResult GetEditor(TCondition model); - private ShapeResult InitializeDisplayType(string displayType, TCondition model) => + private ShapeResult InitializeDisplayType(string displayType, TCondition model, string shapeTypeSuffix = null) => Initialize( - "Condition_Fields_" + displayType, + string.Join("_", new[] { "Condition", "Fields", displayType, shapeTypeSuffix }.WhereNot(string.IsNullOrEmpty)), target => GetConditionViewModel(model).CopyTo(target)) .Location(displayType, CommonLocationNames.Content); } diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs index 811d0d3d..7bd1ea51 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs @@ -1,7 +1,13 @@ +using Lombiq.HelpfulExtensions.Extensions.Widgets.Drivers; +using Lombiq.HelpfulExtensions.Extensions.Widgets.Models; +using Lombiq.HelpfulLibraries.OrchardCore.TagHelpers; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using OrchardCore.ContentManagement.Display.ContentDisplay; +using OrchardCore.DisplayManagement.Handlers; using OrchardCore.Modules; +using OrchardCore.Rules; using System; namespace Lombiq.HelpfulExtensions.Extensions.Widgets; @@ -9,9 +15,18 @@ namespace Lombiq.HelpfulExtensions.Extensions.Widgets; [Feature(FeatureIds.Widgets)] public class Startup : StartupBase { - public override void ConfigureServices(IServiceCollection services) => + public override void ConfigureServices(IServiceCollection services) + { services.AddDataMigration(); + services + .AddScoped, MvcConditionDisplayDriver>() + .AddCondition>() + .AddScoped(sp => (IContentDisplayDriver)sp.GetRequiredService()); + + services.AddTagHelpers(); + } + public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) { // No need for anything here yet. diff --git a/Lombiq.HelpfulExtensions/Views/Condition.Fields.Detail.Title.cshtml b/Lombiq.HelpfulExtensions/Views/Condition.Fields.Detail.Title.cshtml new file mode 100644 index 00000000..26f7f69c --- /dev/null +++ b/Lombiq.HelpfulExtensions/Views/Condition.Fields.Detail.Title.cshtml @@ -0,0 +1,3 @@ +@model ConditionViewModel + +
@Model.Title
\ No newline at end of file diff --git a/Lombiq.HelpfulExtensions/Views/_ViewImports.cshtml b/Lombiq.HelpfulExtensions/Views/_ViewImports.cshtml index c2fea0b0..92e1837a 100644 --- a/Lombiq.HelpfulExtensions/Views/_ViewImports.cshtml +++ b/Lombiq.HelpfulExtensions/Views/_ViewImports.cshtml @@ -4,5 +4,6 @@ @addTagHelper *, OrchardCore.DisplayManagement @addTagHelper *, OrchardCore.ResourceManagement @addTagHelper *, OrchardCore.Contents +@addTagHelper *, Lombiq.HelpfulLibraries.OrchardCore @using Lombiq.HelpfulExtensions.Extensions.Widgets.ViewModels From 3d3be69c3b08b5258b30faf48b03a4a3b60a7bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Wed, 14 Jun 2023 15:11:50 +0200 Subject: [PATCH 3/7] MVC condition. --- .../Drivers/MvcConditionDisplayDriver.cs | 91 +++++++++++++++++++ .../Drivers/MvcConditionEvaluatorDriver.cs | 34 +++++++ .../Extensions/Widgets/Models/MvcCondition.cs | 12 +++ .../ViewModels/MvcConditionViewModel.cs | 14 +++ .../Views/MvcCondition.Fields.Edit.cshtml | 78 ++++++++++++++++ 5 files changed, 229 insertions(+) create mode 100644 Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionDisplayDriver.cs create mode 100644 Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionEvaluatorDriver.cs create mode 100644 Lombiq.HelpfulExtensions/Extensions/Widgets/Models/MvcCondition.cs create mode 100644 Lombiq.HelpfulExtensions/Extensions/Widgets/ViewModels/MvcConditionViewModel.cs create mode 100644 Lombiq.HelpfulExtensions/Views/MvcCondition.Fields.Edit.cshtml diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionDisplayDriver.cs new file mode 100644 index 00000000..f0e4795d --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionDisplayDriver.cs @@ -0,0 +1,91 @@ +using Lombiq.HelpfulExtensions.Extensions.Widgets.Models; +using Lombiq.HelpfulExtensions.Extensions.Widgets.ViewModels; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.Extensions.Localization; +using Newtonsoft.Json; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Views; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Lombiq.HelpfulExtensions.Extensions.Widgets.Drivers; + +public class MvcConditionDisplayDriver : ConditionDisplayDriver +{ + private readonly IHtmlLocalizer H; + private readonly IStringLocalizer T; + public MvcConditionDisplayDriver( + IHtmlLocalizer htmlLocalizer, + IStringLocalizer stringLocalizer) + { + H = htmlLocalizer; + T = stringLocalizer; + } + + protected override IDisplayResult GetEditor(MvcCondition model) => + Initialize("MvcCondition_Fields_Edit", viewModel => + { + viewModel.Area = model.Area; + viewModel.Controller = model.Controller; + viewModel.Action = model.Action; + + foreach (var (key, value) in model.OtherRouteValues) + { + viewModel.OtherRouteNames.Add(key); + viewModel.OtherRouteValues.Add(value); + } + }).PlaceInContent(); + + public override async Task UpdateAsync(MvcCondition model, IUpdateModel updater) + { + var viewModel = new MvcConditionViewModel(); + if (await updater.TryUpdateModelAsync(viewModel, Prefix)) + { + if (viewModel.OtherRouteNames.Count != viewModel.OtherRouteValues.Count) + { + updater.ModelState.AddModelError( + nameof(viewModel.OtherRouteNames), + T["The count of other route value names didn't match the count of other route values."]); + } + + model.Area = viewModel.Area; + model.Controller = viewModel.Controller; + model.Action = viewModel.Action; + + model.OtherRouteValues.Clear(); + for (var i = 0; i < viewModel.OtherRouteNames.Count; i++) + { + model.OtherRouteValues[viewModel.OtherRouteNames[i]] = viewModel.OtherRouteValues[i]; + } + } + + return Edit(model); + } + + protected override ConditionViewModel GetConditionViewModel(MvcCondition condition) + { + IHtmlContentBuilder summaryHint = new HtmlContentBuilder(); + + static IHtmlContentBuilder AppendIfNotEmpty(IHtmlContentBuilder summaryHint, string value, IHtmlContent label) => + string.IsNullOrEmpty(value) ? summaryHint : summaryHint.AppendHtml(label).AppendHtml(" "); + + summaryHint = AppendIfNotEmpty(summaryHint, condition.Area, H["Area: \"{0}\"", condition.Area]); + summaryHint = AppendIfNotEmpty(summaryHint, condition.Controller, H["Controller: \"{0}\"", condition.Controller]); + summaryHint = AppendIfNotEmpty(summaryHint, condition.Action, H["Action: \"{0}\"", condition.Action]); + + if (condition.OtherRouteValues.Any()) + { + summaryHint = summaryHint.AppendHtml( + H["Other route values: {0}", JsonConvert.SerializeObject(condition.OtherRouteValues)]); + } + + return new ConditionViewModel + { + Title = H["MVC condition"], + Description = H["An MVC condition evaluates the currently route values such as controller and action name."], + SummaryHint = summaryHint, + }; + } +} diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionEvaluatorDriver.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionEvaluatorDriver.cs new file mode 100644 index 00000000..1eff2d27 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionEvaluatorDriver.cs @@ -0,0 +1,34 @@ +using Lombiq.HelpfulExtensions.Extensions.Widgets.Models; +using Microsoft.AspNetCore.Http; +using OrchardCore.ContentManagement.Display.ContentDisplay; +using OrchardCore.Rules; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Lombiq.HelpfulExtensions.Extensions.Widgets.Drivers; + +public class MvcConditionEvaluatorDriver : ContentDisplayDriver, IConditionEvaluator +{ + private readonly IHttpContextAccessor _hca; + + public MvcConditionEvaluatorDriver(IHttpContextAccessor hca) => + _hca = hca; + + public ValueTask EvaluateAsync(Condition condition) => new(Evaluate((MvcCondition)condition)); + + private bool Evaluate(MvcCondition condition) => + MatchRouteValue("area", condition.Area) && + MatchRouteValue("controller", condition.Controller) && + MatchRouteValue("action", condition.Action) && + (!condition.OtherRouteValues.Any() || condition.OtherRouteValues.All(pair => MatchRouteValue(pair.Key, pair.Value))); + + private bool MatchRouteValue(string name, string value) + { + // Ignore this match operation if the target value is not set. + if (string.IsNullOrWhiteSpace(value)) return true; + + return _hca.HttpContext?.Request.RouteValues.TryGetValue(name, out var routeValue) == true && + value.EqualsOrdinalIgnoreCase(routeValue?.ToString()); + } +} diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Models/MvcCondition.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Models/MvcCondition.cs new file mode 100644 index 00000000..1a8667e0 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Models/MvcCondition.cs @@ -0,0 +1,12 @@ +using OrchardCore.Rules; +using System.Collections.Generic; + +namespace Lombiq.HelpfulExtensions.Extensions.Widgets.Models; + +public class MvcCondition : Condition +{ + public string Area { get; set; } + public string Controller { get; set; } + public string Action { get; set; } + public IDictionary OtherRouteValues { get; } = new Dictionary(); +} diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/ViewModels/MvcConditionViewModel.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/ViewModels/MvcConditionViewModel.cs new file mode 100644 index 00000000..02a69725 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/ViewModels/MvcConditionViewModel.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using System.Collections.Generic; + +namespace Lombiq.HelpfulExtensions.Extensions.Widgets.ViewModels; + +public class MvcConditionViewModel +{ + public string Area { get; set; } + public string Controller { get; set; } + public string Action { get; set; } + + public IList OtherRouteNames { get; } = new List(); + public IList OtherRouteValues { get; } = new List(); +} diff --git a/Lombiq.HelpfulExtensions/Views/MvcCondition.Fields.Edit.cshtml b/Lombiq.HelpfulExtensions/Views/MvcCondition.Fields.Edit.cshtml new file mode 100644 index 00000000..bbf2dfc4 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Views/MvcCondition.Fields.Edit.cshtml @@ -0,0 +1,78 @@ +@using Newtonsoft.Json +@model MvcConditionViewModel + +@{ + var other = Enumerable.Range(0, Model.OtherRouteNames.Count) + .Select(index => new + { + name = Model.OtherRouteNames[index], + value = Model.OtherRouteValues[index], + }) + .OrderBy(item => item.name); + +} + +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
@T["Name"]@T["Value"]@T["Action"]
+ + + + + @T["Delete"] +
+ @T["New"] +
+
+ + + \ No newline at end of file From a042ce834a064853e9c38deb93d95c9714e7590c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Thu, 15 Jun 2023 13:48:32 +0200 Subject: [PATCH 4/7] Add ContentitemWidget. --- .../Extensions/Widgets/Migrations.cs | 39 ++++++++++++++++++- .../Widgets/Models/ContentItemWidget.cs | 11 ++++++ .../Extensions/Widgets/Startup.cs | 4 ++ .../Extensions/Widgets/WidgetTypes.cs | 1 + .../Views/ContentItemWidget.cshtml | 25 ++++++++++++ 5 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 Lombiq.HelpfulExtensions/Extensions/Widgets/Models/ContentItemWidget.cs create mode 100644 Lombiq.HelpfulExtensions/Views/ContentItemWidget.cshtml diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Migrations.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Migrations.cs index b7794824..9425ebee 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/Migrations.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Migrations.cs @@ -1,4 +1,6 @@ +using Lombiq.HelpfulExtensions.Extensions.Widgets.Models; using Lombiq.HelpfulLibraries.OrchardCore.Contents; +using OrchardCore.ContentFields.Settings; using OrchardCore.ContentManagement.Metadata; using OrchardCore.ContentManagement.Metadata.Settings; using OrchardCore.Data.Migration; @@ -55,7 +57,25 @@ public int Create() ) ); - return 4; + var contentItemWidgetPartName = _contentDefinitionManager.AlterPartDefinition(builder => builder + .WithField(part => part.ContentToDisplay, field => field + .WithDisplayName("Content to display") + .WithSettings(new ContentPickerFieldSettings + { + DisplayAllContentTypes = true, + Multiple = true, + })) + .WithField(part => part.DisplayType, field => field.WithDisplayName("Display type")) + .WithField(part => part.GroupId, field => field.WithDisplayName("Group ID")) + ); + + _contentDefinitionManager.AlterTypeDefinition(WidgetTypes.ContentItemWidget, builder => builder + .Securable() + .Stereotype(CommonStereotypes.Widget) + .WithPart(contentItemWidgetPartName) + ); + + return 5; } public int UpdateFrom1() @@ -90,4 +110,21 @@ public int UpdateFrom3() return 4; } + + public int UpdateFrom4() + { + var contentItemWidgetPartName = _contentDefinitionManager.AlterPartDefinition(builder => builder + .WithField(part => part.ContentToDisplay, field => field.WithDisplayName("Content to display")) + .WithField(part => part.DisplayType, field => field.WithDisplayName("Display type")) + .WithField(part => part.GroupId, field => field.WithDisplayName("Group ID")) + ); + + _contentDefinitionManager.AlterTypeDefinition(WidgetTypes.ContentItemWidget, builder => builder + .Securable() + .Stereotype(CommonStereotypes.Widget) + .WithPart(contentItemWidgetPartName) + ); + + return 5; + } } diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Models/ContentItemWidget.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Models/ContentItemWidget.cs new file mode 100644 index 00000000..d7949861 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Models/ContentItemWidget.cs @@ -0,0 +1,11 @@ +using OrchardCore.ContentFields.Fields; +using OrchardCore.ContentManagement; + +namespace Lombiq.HelpfulExtensions.Extensions.Widgets.Models; + +public class ContentItemWidget : ContentPart +{ + public ContentPickerField ContentToDisplay { get; set; } = new(); + public TextField DisplayType { get; set; } = new(); + public TextField GroupId { get; set; } = new(); +} diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs index 7bd1ea51..4f0bcbad 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Display.ContentDisplay; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.Modules; @@ -25,6 +26,9 @@ public override void ConfigureServices(IServiceCollection services) .AddScoped(sp => (IContentDisplayDriver)sp.GetRequiredService()); services.AddTagHelpers(); + + services.AddContentPart() + .UseDetailOnlyDriver(); } public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/WidgetTypes.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/WidgetTypes.cs index 931bf581..1fda568d 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/WidgetTypes.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/WidgetTypes.cs @@ -7,4 +7,5 @@ public static class WidgetTypes public const string LiquidWidget = nameof(LiquidWidget); public const string MenuWidget = nameof(MenuWidget); public const string MarkdownWidget = nameof(MarkdownWidget); + public const string ContentItemWidget = nameof(ContentItemWidget); } diff --git a/Lombiq.HelpfulExtensions/Views/ContentItemWidget.cshtml b/Lombiq.HelpfulExtensions/Views/ContentItemWidget.cshtml new file mode 100644 index 00000000..a64116a6 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Views/ContentItemWidget.cshtml @@ -0,0 +1,25 @@ +@using OrchardCore.ContentManagement +@using Lombiq.HelpfulExtensions.Extensions.Widgets.Models +@using OrchardCore.ContentManagement.Display +@using OrchardCore.DisplayManagement.ModelBinding + +@inject IContentManager ContentManager +@inject IContentItemDisplayManager ContentItemDisplayManager +@inject IUpdateModelAccessor UpdateModelAccessor + +@{ + if ((Model.ContentItem as ContentItem)?.As() is not { } part) { return; } + + var ids = part.ContentToDisplay?.ContentItemIds ?? Array.Empty(); + var contentItems = await ContentManager.GetAsync(ids); + + var updater = UpdateModelAccessor.ModelUpdater; + var displayType = string.IsNullOrWhiteSpace(part.DisplayType?.Text) ? string.Empty : part.DisplayType.Text; + var groupId = string.IsNullOrWhiteSpace(part.GroupId?.Text) ? string.Empty : part.GroupId.Text; +} + +@foreach (var content in contentItems) +{ + var shape = await ContentItemDisplayManager.BuildDisplayAsync(content, updater, displayType, groupId); + @await DisplayAsync(shape) +} \ No newline at end of file From efda52cc05ced9839ed15e7c6f41b0328806cfef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Mon, 19 Jun 2023 14:49:38 +0200 Subject: [PATCH 5/7] some code cleanup --- .../Extensions/Widgets/Drivers/ConditionDisplayDriver.cs | 2 +- .../Extensions/Widgets/Drivers/MvcConditionDisplayDriver.cs | 1 - Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs | 2 +- .../Extensions/Widgets/ViewModels/MvcConditionViewModel.cs | 3 +-- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/ConditionDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/ConditionDisplayDriver.cs index 79844ec8..2143a528 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/ConditionDisplayDriver.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/ConditionDisplayDriver.cs @@ -25,7 +25,7 @@ public override IDisplayResult Edit(TCondition model) => private ShapeResult InitializeDisplayType(string displayType, TCondition model, string shapeTypeSuffix = null) => Initialize( - string.Join("_", new[] { "Condition", "Fields", displayType, shapeTypeSuffix }.WhereNot(string.IsNullOrEmpty)), + string.Join('_', new[] { "Condition", "Fields", displayType, shapeTypeSuffix }.WhereNot(string.IsNullOrEmpty)), target => GetConditionViewModel(model).CopyTo(target)) .Location(displayType, CommonLocationNames.Content); } diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionDisplayDriver.cs index f0e4795d..54df6e64 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionDisplayDriver.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionDisplayDriver.cs @@ -6,7 +6,6 @@ using Newtonsoft.Json; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Views; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs index d773a409..241402fc 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs @@ -4,9 +4,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -using OrchardCore.Data.Migration; using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Display.ContentDisplay; +using OrchardCore.Data.Migration; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.Modules; using OrchardCore.Rules; diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/ViewModels/MvcConditionViewModel.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/ViewModels/MvcConditionViewModel.cs index 02a69725..a9ab02e6 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/ViewModels/MvcConditionViewModel.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/ViewModels/MvcConditionViewModel.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Mvc.ModelBinding; -using System.Collections.Generic; +using System.Collections.Generic; namespace Lombiq.HelpfulExtensions.Extensions.Widgets.ViewModels; From a0b5c56966a8f358e58faaa124139782e219e0ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Wed, 21 Jun 2023 16:29:07 +0200 Subject: [PATCH 6/7] Add MenuWidgetLiquidFilter. --- .../Widgets/Liquid/MenuWidgetLiquidFilter.cs | 49 +++++++++++++++++++ .../Extensions/Widgets/Startup.cs | 6 +++ .../Lombiq.HelpfulExtensions.csproj | 1 + .../Views/MenuWidget.cshtml | 1 - 4 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 Lombiq.HelpfulExtensions/Extensions/Widgets/Liquid/MenuWidgetLiquidFilter.cs diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Liquid/MenuWidgetLiquidFilter.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Liquid/MenuWidgetLiquidFilter.cs new file mode 100644 index 00000000..3f79d340 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Liquid/MenuWidgetLiquidFilter.cs @@ -0,0 +1,49 @@ +using Fluid; +using Fluid.Values; +using Lombiq.HelpfulExtensions.Extensions.Widgets.ViewModels; +using Lombiq.HelpfulLibraries.OrchardCore.Liquid; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OrchardCore.Liquid; +using OrchardCore.Navigation; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Lombiq.HelpfulExtensions.Extensions.Widgets.Liquid; + +public class MenuWidgetLiquidFilter : ILiquidFilter +{ + private readonly ILiquidContentDisplayService _liquidContentDisplayService; + + public MenuWidgetLiquidFilter(ILiquidContentDisplayService liquidContentDisplayService) => + _liquidContentDisplayService = liquidContentDisplayService; + + public ValueTask ProcessAsync(FluidValue input, FilterArguments arguments, LiquidTemplateContext context) + { + bool noWrapper; + noWrapper = arguments[nameof(noWrapper)].ToBooleanValue(); + + var menuItems = input?.Type switch + { + FluidValues.String => JsonConvert.DeserializeObject>(input!.ToStringValue()), + FluidValues.Object => input!.ToObjectValue() switch + { + IEnumerable enumerable => enumerable, + MenuItem single => new[] { single }, + JArray jArray => jArray.ToObject>(), + JObject jObject => new[] { jObject.ToObject() }, + _ => null, + }, + _ => null, + }; + + return _liquidContentDisplayService.DisplayNewAsync( + WidgetTypes.MenuWidget, + model => + { + model.NoWrapper = noWrapper; + model.MenuItems = menuItems ?? Enumerable.Empty(); + }); + } +} diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs index 241402fc..3308e134 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs @@ -1,5 +1,7 @@ using Lombiq.HelpfulExtensions.Extensions.Widgets.Drivers; +using Lombiq.HelpfulExtensions.Extensions.Widgets.Liquid; using Lombiq.HelpfulExtensions.Extensions.Widgets.Models; +using Lombiq.HelpfulLibraries.OrchardCore.Liquid; using Lombiq.HelpfulLibraries.OrchardCore.TagHelpers; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; @@ -8,6 +10,7 @@ using OrchardCore.ContentManagement.Display.ContentDisplay; using OrchardCore.Data.Migration; using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.Liquid; using OrchardCore.Modules; using OrchardCore.Rules; using System; @@ -30,6 +33,9 @@ public override void ConfigureServices(IServiceCollection services) services.AddContentPart() .UseDetailOnlyDriver(); + + services.AddScoped(); + services.AddLiquidFilter("menu"); } public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) diff --git a/Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj b/Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj index 3d72786f..49d6adc2 100644 --- a/Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj +++ b/Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj @@ -42,6 +42,7 @@ + diff --git a/Lombiq.HelpfulExtensions/Views/MenuWidget.cshtml b/Lombiq.HelpfulExtensions/Views/MenuWidget.cshtml index 8768cd6c..1088679d 100644 --- a/Lombiq.HelpfulExtensions/Views/MenuWidget.cshtml +++ b/Lombiq.HelpfulExtensions/Views/MenuWidget.cshtml @@ -1,4 +1,3 @@ -@using Lombiq.HelpfulExtensions.Extensions.Widgets.ViewModels @using Lombiq.HelpfulLibraries.Common.Utilities; @using Microsoft.AspNetCore.Http.Extensions @using OrchardCore.Navigation From cbf06939bd2632022cc727c6ccf4d41790856f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Wed, 21 Jun 2023 18:13:38 +0200 Subject: [PATCH 7/7] Fix menu widget and add wrapper classes. --- .../Widgets/Liquid/MenuWidgetLiquidFilter.cs | 100 ++++++++++++++++-- .../Widgets/ViewModels/MenuWidgetViewModel.cs | 15 ++- .../Views/MenuWidget.cshtml | 4 +- 3 files changed, 105 insertions(+), 14 deletions(-) diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Liquid/MenuWidgetLiquidFilter.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Liquid/MenuWidgetLiquidFilter.cs index 3f79d340..2cc0ac4a 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/Liquid/MenuWidgetLiquidFilter.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Liquid/MenuWidgetLiquidFilter.cs @@ -2,12 +2,17 @@ using Fluid.Values; using Lombiq.HelpfulExtensions.Extensions.Widgets.ViewModels; using Lombiq.HelpfulLibraries.OrchardCore.Liquid; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Localization; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OrchardCore.Liquid; using OrchardCore.Navigation; +using System; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; namespace Lombiq.HelpfulExtensions.Extensions.Widgets.Liquid; @@ -15,35 +20,114 @@ namespace Lombiq.HelpfulExtensions.Extensions.Widgets.Liquid; public class MenuWidgetLiquidFilter : ILiquidFilter { private readonly ILiquidContentDisplayService _liquidContentDisplayService; + private readonly Lazy _urlHelperLazy; + private readonly IStringLocalizer T; - public MenuWidgetLiquidFilter(ILiquidContentDisplayService liquidContentDisplayService) => + public MenuWidgetLiquidFilter( + IActionContextAccessor actionContextAccessor, + ILiquidContentDisplayService liquidContentDisplayService, + IStringLocalizer stringLocalizer, + IUrlHelperFactory urlHelperFactory) + { _liquidContentDisplayService = liquidContentDisplayService; + _urlHelperLazy = new Lazy(() => + urlHelperFactory.GetUrlHelper(actionContextAccessor.ActionContext!)); + + T = stringLocalizer; + } + public ValueTask ProcessAsync(FluidValue input, FilterArguments arguments, LiquidTemplateContext context) { - bool noWrapper; + bool noWrapper, localNav; + string classes; noWrapper = arguments[nameof(noWrapper)].ToBooleanValue(); + localNav = arguments[nameof(localNav)].ToBooleanValue(); + classes = arguments[nameof(classes)].ToStringValue(); + + var converter = new LocalizedStringJsonConverter(T); + var serializer = new JsonSerializer(); + serializer.Converters.Add(converter); + var serializerSettings = new JsonSerializerSettings(); + serializerSettings.Converters.Add(converter); var menuItems = input?.Type switch { - FluidValues.String => JsonConvert.DeserializeObject>(input!.ToStringValue()), + FluidValues.String => JsonConvert.DeserializeObject>( + input!.ToStringValue(), + serializerSettings), FluidValues.Object => input!.ToObjectValue() switch { - IEnumerable enumerable => enumerable, + IEnumerable enumerable => enumerable.AsList(), MenuItem single => new[] { single }, - JArray jArray => jArray.ToObject>(), - JObject jObject => new[] { jObject.ToObject() }, + JArray jArray => jArray.ToObject>(serializer), + JObject jObject => new[] { jObject.ToObject(serializer) }, _ => null, }, _ => null, }; + UpdateMenuItems(menuItems, localNav); + return _liquidContentDisplayService.DisplayNewAsync( WidgetTypes.MenuWidget, model => { model.NoWrapper = noWrapper; - model.MenuItems = menuItems ?? Enumerable.Empty(); + model.MenuItems = menuItems ?? Array.Empty(); + model.HtmlClasses = classes; }); } + + private void UpdateMenuItems(IEnumerable menuItems, bool localNav) + { + if (menuItems == null) return; + + foreach (var item in menuItems) + { + if (!string.IsNullOrEmpty(item.Url)) + { + var finalUrl = _urlHelperLazy.Value.Content(item.Url); + item.Url = finalUrl; + item.Href = finalUrl; + } + + item.LocalNav = localNav || item.LocalNav; + + UpdateMenuItems(item.Items, localNav); + } + } + + public class LocalizedStringJsonConverter : JsonConverter + { + private readonly IStringLocalizer T; + + public LocalizedStringJsonConverter(IStringLocalizer stringLocalizer) => + T = stringLocalizer; + + public override void WriteJson(JsonWriter writer, LocalizedString value, JsonSerializer serializer) => + writer.WriteValue(value?.Value); + + [SuppressMessage("Style", "IDE0010:Add missing cases", Justification = "We don't want to handle other token types.")] + public override LocalizedString ReadJson( + JsonReader reader, + Type objectType, + LocalizedString existingValue, + bool hasExistingValue, + JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.String: + return JToken.ReadFrom(reader).ToObject() is { } text ? T[text] : null; + case JsonToken.StartObject: + var data = new Dictionary( + JToken.ReadFrom(reader).ToObject>(), + StringComparer.OrdinalIgnoreCase); + return new LocalizedString(data[nameof(LocalizedString.Name)], data[nameof(LocalizedString.Value)]); + default: + throw new InvalidOperationException("Unable to parse JSON!"); + } + } + } } diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/ViewModels/MenuWidgetViewModel.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/ViewModels/MenuWidgetViewModel.cs index 842bcd39..48e7cdef 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/ViewModels/MenuWidgetViewModel.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/ViewModels/MenuWidgetViewModel.cs @@ -7,16 +7,23 @@ namespace Lombiq.HelpfulExtensions.Extensions.Widgets.ViewModels; public class MenuWidgetViewModel { public bool NoWrapper { get; set; } + public IEnumerable MenuItems { get; set; } - public MenuWidgetViewModel(bool noWrapper = false, IEnumerable menuItems = null) + public string HtmlClasses { get; set; } = string.Empty; + + public MenuWidgetViewModel() + : this(noWrapper: false, menuItems: Enumerable.Empty()) + { + } + + public MenuWidgetViewModel(bool noWrapper, IEnumerable menuItems) { NoWrapper = noWrapper; MenuItems = menuItems ?? Enumerable.Empty(); } public MenuWidgetViewModel(dynamic model) - : this((model.NoWrapper as bool?) == true, model.MenuItems as IEnumerable) - { - } + : this((model.NoWrapper as bool?) == true, model.MenuItems as IEnumerable) => + HtmlClasses = model.HtmlClasses as string ?? string.Empty; } diff --git a/Lombiq.HelpfulExtensions/Views/MenuWidget.cshtml b/Lombiq.HelpfulExtensions/Views/MenuWidget.cshtml index 1088679d..68d66eaf 100644 --- a/Lombiq.HelpfulExtensions/Views/MenuWidget.cshtml +++ b/Lombiq.HelpfulExtensions/Views/MenuWidget.cshtml @@ -34,9 +34,9 @@ @if (!viewModel.NoWrapper) { - @: