Skip to content

Commit

Permalink
Merge pull request #131 from Lombiq/issue/SPAL-15
Browse files Browse the repository at this point in the history
SPAL-15: MVC display condition
  • Loading branch information
Psichorex authored Jun 25, 2023
2 parents 4f6bb97 + cbf0693 commit 762c24a
Show file tree
Hide file tree
Showing 21 changed files with 539 additions and 9 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ node_modules/
*.user
.pnpm-debug.log
.editorconfig
*.orig
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Lombiq.HelpfulExtensions.Extensions.Widgets.ViewModels;
using Lombiq.HelpfulLibraries.OrchardCore.Contents;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Rules;
using System.Collections.Generic;

namespace Lombiq.HelpfulExtensions.Extensions.Widgets.Drivers;

public abstract class ConditionDisplayDriver<TCondition> : DisplayDriver<Condition, TCondition>
where TCondition : Condition
{
public override IDisplayResult Display(TCondition model) =>
Combine(
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, string shapeTypeSuffix = null) =>
Initialize<ConditionViewModel>(
string.Join('_', new[] { "Condition", "Fields", displayType, shapeTypeSuffix }.WhereNot(string.IsNullOrEmpty)),
target => GetConditionViewModel(model).CopyTo(target))
.Location(displayType, CommonLocationNames.Content);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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.Linq;
using System.Threading.Tasks;

namespace Lombiq.HelpfulExtensions.Extensions.Widgets.Drivers;

public class MvcConditionDisplayDriver : ConditionDisplayDriver<MvcCondition>
{
private readonly IHtmlLocalizer<MvcConditionDisplayDriver> H;
private readonly IStringLocalizer<MvcConditionDisplayDriver> T;
public MvcConditionDisplayDriver(
IHtmlLocalizer<MvcConditionDisplayDriver> htmlLocalizer,
IStringLocalizer<MvcConditionDisplayDriver> stringLocalizer)
{
H = htmlLocalizer;
T = stringLocalizer;
}

protected override IDisplayResult GetEditor(MvcCondition model) =>
Initialize<MvcConditionViewModel>("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<IDisplayResult> 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,
};
}
}
Original file line number Diff line number Diff line change
@@ -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<bool> 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());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using Fluid;
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.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;

namespace Lombiq.HelpfulExtensions.Extensions.Widgets.Liquid;

public class MenuWidgetLiquidFilter : ILiquidFilter
{
private readonly ILiquidContentDisplayService _liquidContentDisplayService;
private readonly Lazy<IUrlHelper> _urlHelperLazy;
private readonly IStringLocalizer<MenuWidgetLiquidFilter> T;

public MenuWidgetLiquidFilter(
IActionContextAccessor actionContextAccessor,
ILiquidContentDisplayService liquidContentDisplayService,
IStringLocalizer<MenuWidgetLiquidFilter> stringLocalizer,
IUrlHelperFactory urlHelperFactory)
{
_liquidContentDisplayService = liquidContentDisplayService;

_urlHelperLazy = new Lazy<IUrlHelper>(() =>
urlHelperFactory.GetUrlHelper(actionContextAccessor.ActionContext!));

T = stringLocalizer;
}

public ValueTask<FluidValue> ProcessAsync(FluidValue input, FilterArguments arguments, LiquidTemplateContext context)
{
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<IList<MenuItem>>(
input!.ToStringValue(),
serializerSettings),
FluidValues.Object => input!.ToObjectValue() switch
{
IEnumerable<MenuItem> enumerable => enumerable.AsList(),
MenuItem single => new[] { single },
JArray jArray => jArray.ToObject<IList<MenuItem>>(serializer),
JObject jObject => new[] { jObject.ToObject<MenuItem>(serializer) },
_ => null,
},
_ => null,
};

UpdateMenuItems(menuItems, localNav);

return _liquidContentDisplayService.DisplayNewAsync<MenuWidgetViewModel>(
WidgetTypes.MenuWidget,
model =>
{
model.NoWrapper = noWrapper;
model.MenuItems = menuItems ?? Array.Empty<MenuItem>();
model.HtmlClasses = classes;
});
}

private void UpdateMenuItems(IEnumerable<MenuItem> 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<LocalizedString>
{
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<string>() is { } text ? T[text] : null;
case JsonToken.StartObject:
var data = new Dictionary<string, string>(
JToken.ReadFrom(reader).ToObject<Dictionary<string, string>>(),
StringComparer.OrdinalIgnoreCase);
return new LocalizedString(data[nameof(LocalizedString.Name)], data[nameof(LocalizedString.Value)]);
default:
throw new InvalidOperationException("Unable to parse JSON!");
}
}
}
}
39 changes: 38 additions & 1 deletion Lombiq.HelpfulExtensions/Extensions/Widgets/Migrations.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -55,7 +57,25 @@ public int Create()
)
);

return 4;
var contentItemWidgetPartName = _contentDefinitionManager.AlterPartDefinition<ContentItemWidget>(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()
Expand Down Expand Up @@ -90,4 +110,21 @@ public int UpdateFrom3()

return 4;
}

public int UpdateFrom4()
{
var contentItemWidgetPartName = _contentDefinitionManager.AlterPartDefinition<ContentItemWidget>(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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
12 changes: 12 additions & 0 deletions Lombiq.HelpfulExtensions/Extensions/Widgets/Models/MvcCondition.cs
Original file line number Diff line number Diff line change
@@ -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<string, string> OtherRouteValues { get; } = new Dictionary<string, string>();
}
Loading

0 comments on commit 762c24a

Please sign in to comment.