Skip to content

Commit

Permalink
Merge pull request #143 from Lombiq/issue/OSOE-680
Browse files Browse the repository at this point in the history
OSOE-680: Add default handler, example and docs for the Content Sets feature in Lombiq.HelpfulExtensions
  • Loading branch information
Psichorex authored Sep 11, 2023
2 parents 734237d + 6b22993 commit 0e217ef
Show file tree
Hide file tree
Showing 21 changed files with 838 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using OpenQA.Selenium;
using OpenQA.Selenium.Interactions;
using Shouldly;
using System.Linq;
using System.Threading.Tasks;
using static Lombiq.HelpfulExtensions.Tests.UI.Constants.TextInputValues;
using static Lombiq.HelpfulExtensions.Tests.UI.Constants.XPathSelectors;
Expand Down Expand Up @@ -135,6 +136,57 @@ await context.RetryIfStaleOrFailAsync(async () =>
});
}

/// <summary>
/// Tests the Lombiq Helpful Extensions - Content Sets feature.
/// </summary>
public static async Task TestFeatureContentSetsAsync(this UITestContext context)
{
const string contentId0 = "contentsetexample000000000";
const string contentId2 = "contentsetexample000000002";

var byLink = By.CssSelector(".field-name-content-set-example-content-set-type .value a");

void VerifyDisplay(string title, string body, params string[] linkTexts)
{
var contentItem = context.Get(By.ClassName("content-set-example"));

contentItem.Get(By.TagName("h1")).Text.Trim().ShouldBe(title);
contentItem.Get(By.ClassName("content-set-example-body")).Text.Trim().ShouldBe(body);

contentItem
.GetAll(byLink)
.Select(link => link.Text.Trim())
.ToArray()
.ShouldBe(linkTexts);
}

// Verify the default item.
await context.SignInDirectlyAsync();
await context.GoToRelativeUrlAsync($"/Contents/ContentItems/{contentId0}");
VerifyDisplay("Default Content Set Example", "Some generic text.", "Other Example", "Some Example");

// Verify the first item both by content set content picker link and direct access.
await context.ClickReliablyOnAsync(byLink);
VerifyDisplay("Second Content Set Variant", "Some generic text v2.", "Default content item", "Some Example");
await context.GoToRelativeUrlAsync($"/Contents/ContentItems/{contentId2}");
VerifyDisplay("Second Content Set Variant", "Some generic text v2.", "Default content item", "Some Example");

// Create the final variant.
await context.GoToContentItemListAsync("ContentSetExample");
await context.SelectFromBootstrapDropdownReliablyAsync(
context.Get(By.XPath("//li[contains(@class, 'list-group-item')][3]//div[@title='Content Set Type']//button")),
By.XPath("//a[@title='Create Final Example']"));
await context.ClickAndFillInWithRetriesAsync(By.Id("TitlePart_Title"), "Test Title");
await context.ClickPublishAsync();
context.ShouldBeSuccess();

// Verify changes.
await context.GoToRelativeUrlAsync($"/Contents/ContentItems/{contentId0}");
VerifyDisplay("Default Content Set Example", "Some generic text.", "Final Example", "Other Example", "Some Example");
await context.ClickReliablyOnAsync(byLink);
VerifyDisplay("Test Title", "Some generic text v1.", "Default content item", "Other Example", "Some Example");
}

private static async Task TestWidgetAsync(this UITestContext context, string widget)
{
await context.GoToCreatePageAsync();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Lombiq.HelpfulExtensions.Extensions.ContentSets.Events;
using Lombiq.HelpfulExtensions.Extensions.ContentSets.Models;
using Lombiq.HelpfulExtensions.Extensions.ContentSets.Services;
using Lombiq.HelpfulExtensions.Extensions.ContentSets.ViewModels;
using Lombiq.HelpfulLibraries.OrchardCore.Contents;
using Microsoft.Extensions.Localization;
using Newtonsoft.Json.Linq;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Display.ContentDisplay;
using OrchardCore.ContentManagement.Display.Models;
using OrchardCore.ContentManagement.Metadata;
using OrchardCore.ContentManagement.Metadata.Models;
using OrchardCore.DisplayManagement.Views;
using System.Collections.Generic;

namespace Lombiq.HelpfulExtensions.Extensions.ContentSets.Drivers;

public class ContentSetContentPickerFieldDisplayDriver : ContentFieldDisplayDriver<ContentSetContentPickerField>
{
private readonly IContentDefinitionManager _contentDefinitionManager;
private readonly IContentSetManager _contentSetManager;
private readonly IEnumerable<IContentSetEventHandler> _contentSetEventHandlers;
private readonly IStringLocalizer<ContentSetPart> T;

public ContentSetContentPickerFieldDisplayDriver(
IContentDefinitionManager contentDefinitionManager,
IContentSetManager contentSetManager,
IEnumerable<IContentSetEventHandler> contentSetEventHandlers,
IStringLocalizer<ContentSetPart> stringLocalizer)
{
_contentDefinitionManager = contentDefinitionManager;
_contentSetManager = contentSetManager;
_contentSetEventHandlers = contentSetEventHandlers;
T = stringLocalizer;
}

public override IDisplayResult Display(
ContentSetContentPickerField field,
BuildFieldDisplayContext fieldDisplayContext)
{
var name = fieldDisplayContext.PartFieldDefinition.Name;
if (field.ContentItem.Get<ContentSetPart>(name) is not { } part) return null;

return Initialize<ContentSetContentPickerFieldViewModel>(GetDisplayShapeType(fieldDisplayContext), model =>
{
model.PartFieldDefinition = fieldDisplayContext.PartFieldDefinition;
return model.InitializeAsync(
_contentSetManager,
_contentSetEventHandlers,
T,
part,
new ContentTypePartDefinition(
name,
_contentDefinitionManager.GetPartDefinition(nameof(ContentSetPart)),
new JObject()),
isNew: false);
})
.Location(CommonContentDisplayTypes.Detail, CommonLocationNames.Content)
.Location(CommonContentDisplayTypes.Summary, CommonLocationNames.Content);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@
using Lombiq.HelpfulExtensions.Extensions.ContentSets.ViewModels;
using Lombiq.HelpfulLibraries.OrchardCore.Contents;
using Microsoft.Extensions.Localization;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Display.ContentDisplay;
using OrchardCore.ContentManagement.Display.Models;
using OrchardCore.ContentManagement.Metadata.Models;
using OrchardCore.DisplayManagement.ModelBinding;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Entities;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Lombiq.HelpfulExtensions.Extensions.ContentSets.Drivers;
Expand All @@ -24,36 +21,47 @@ public class ContentSetPartDisplayDriver : ContentPartDisplayDriver<ContentSetPa
private readonly IContentSetManager _contentSetManager;
private readonly IIdGenerator _idGenerator;
private readonly IEnumerable<IContentSetEventHandler> _contentSetEventHandlers;
private readonly IStringLocalizer<ContentSetPartDisplayDriver> T;
private readonly IStringLocalizer<ContentSetPart> T;

public ContentSetPartDisplayDriver(
IContentSetManager contentSetManager,
IIdGenerator idGenerator,
IEnumerable<IContentSetEventHandler> contentSetEventHandlers,
IStringLocalizer<ContentSetPartDisplayDriver> stringLocalizer)
IStringLocalizer<ContentSetPart> stringLocalizer)
{
_contentSetManager = contentSetManager;
_idGenerator = idGenerator;
_contentSetEventHandlers = contentSetEventHandlers;
T = stringLocalizer;
}

public override IDisplayResult Display(ContentSetPart part, BuildPartDisplayContext context) =>
Combine(
Initialize<ContentSetPartViewModel>(
$"{ShapeType}_Tags",
model => BuildViewModelAsync(model, part, context.TypePartDefinition, isNew: false))
public override IDisplayResult Display(ContentSetPart part, BuildPartDisplayContext context)
{
ValueTask InitializeAsync(ContentSetPartViewModel model) =>
model.InitializeAsync(
_contentSetManager,
_contentSetEventHandlers,
T,
part,
context.TypePartDefinition,
isNew: false);

return Combine(
Initialize<ContentSetPartViewModel>($"{ShapeType}_Tags", InitializeAsync)
.Location(CommonContentDisplayTypes.SummaryAdmin, "Tags:11"),
Initialize<ContentSetPartViewModel>(
$"{ShapeType}_Links",
model => BuildViewModelAsync(model, part, context.TypePartDefinition, isNew: false))
Initialize<ContentSetPartViewModel>($"{ShapeType}_Links", InitializeAsync)
.Location(CommonContentDisplayTypes.SummaryAdmin, "Actions:5")
);
}

public override IDisplayResult Edit(ContentSetPart part, BuildPartEditorContext context) =>
Initialize<ContentSetPartViewModel>(
$"{nameof(ContentSetPart)}_Edit",
model => BuildViewModelAsync(model, part, context.TypePartDefinition, context.IsNew))
Initialize<ContentSetPartViewModel>($"{nameof(ContentSetPart)}_Edit", model => model.InitializeAsync(
_contentSetManager,
_contentSetEventHandlers,
T,
part,
context.TypePartDefinition,
context.IsNew))
.Location($"Parts:0%{context.TypePartDefinition.Name};0");

public override async Task<IDisplayResult> UpdateAsync(
Expand All @@ -77,56 +85,4 @@ public override async Task<IDisplayResult> UpdateAsync(

return await EditAsync(part, context);
}

public async ValueTask BuildViewModelAsync(
ContentSetPartViewModel model,
ContentSetPart part,
ContentTypePartDefinition definition,
bool isNew)
{
model.Key = part.Key;
model.ContentSet = part.ContentSet;
model.ContentSetPart = part;
model.Definition = definition;
model.IsNew = isNew;

var existingContentItems = (await _contentSetManager.GetContentItemsAsync(part.ContentSet))
.ToDictionary(item => item.Get<ContentSetPart>(definition.Name)?.Key);

var options = new Dictionary<string, ContentSetLinkViewModel>
{
[ContentSetPart.Default] = new(
IsDeleted: false,
T["Default content item"],
existingContentItems.GetMaybe(ContentSetPart.Default)?.ContentItemId,
ContentSetPart.Default),
};

var supportedOptions = (await _contentSetEventHandlers.AwaitEachAsync(item => item.GetSupportedOptionsAsync(part, definition)))
.SelectMany(links => links ?? Enumerable.Empty<ContentSetLinkViewModel>());
options.AddRange(supportedOptions, link => link.Key);

// Ensure the existing content item IDs are applied to the supported option links.
existingContentItems
.Where(pair => options.GetMaybe(pair.Key)?.ContentItemId != pair.Value.ContentItemId)
.ForEach(pair => options[pair.Key] = options[pair.Key] with { ContentItemId = pair.Value.ContentItemId });

// Content items that have been added to the set but no longer generate a valid option matching their key.
var inapplicableSetMembers = existingContentItems
.Where(pair => !options.ContainsKey(pair.Key))
.Select(pair => new ContentSetLinkViewModel(
IsDeleted: true,
T["{0} (No longer applicable)", pair.Value.DisplayText].Value,
pair.Value.ContentItemId,
pair.Key));
options.AddRange(inapplicableSetMembers, link => link.Key);

model.MemberLinks = options
.Values
.Where(link => link.Key != model.Key && link.ContentItemId != part.ContentItem.ContentItemId)
.OrderBy(link => string.IsNullOrEmpty(link.ContentItemId) ? 1 : 0)
.ThenBy(link => link.IsDeleted ? 1 : 0)
.ThenBy(link => link.DisplayText)
.ToList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using OrchardCore.ContentManagement;
using System.Diagnostics.CodeAnalysis;

namespace Lombiq.HelpfulExtensions.Extensions.ContentSets.Models;

[SuppressMessage(
"Minor Code Smell",
"S2094:Classes should not be empty",
Justification = "Only data we need is the field name.")]
public class ContentSetContentPickerField : ContentField
{
}
3 changes: 3 additions & 0 deletions Lombiq.HelpfulExtensions/Extensions/ContentSets/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public override void ConfigureServices(IServiceCollection services)
.WithMigration<Migrations>();

services.AddScoped<IContentSetManager, ContentSetManager>();

services.AddContentField<ContentSetContentPickerField>()
.UseDisplayDriver<ContentSetContentPickerFieldDisplayDriver>();
}

public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using OrchardCore.ContentManagement.Metadata.Models;

namespace Lombiq.HelpfulExtensions.Extensions.ContentSets.ViewModels;

public class ContentSetContentPickerFieldViewModel : ContentSetPartViewModel
{
public ContentPartFieldDefinition PartFieldDefinition { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
using Lombiq.HelpfulExtensions.Extensions.ContentSets.Models;
using Lombiq.HelpfulExtensions.Extensions.ContentSets.Events;
using Lombiq.HelpfulExtensions.Extensions.ContentSets.Models;
using Lombiq.HelpfulExtensions.Extensions.ContentSets.Services;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Localization;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Metadata.Models;
using OrchardCore.ContentManagement.Metadata.Settings;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Lombiq.HelpfulExtensions.Extensions.ContentSets.ViewModels;

Expand All @@ -22,6 +28,70 @@ public class ContentSetPartViewModel

[BindNever]
public bool IsNew { get; set; }

[BindNever]
public string DisplayName =>
Definition?
.Settings?
.Property(nameof(ContentTypePartSettings))?
.Value
.ToObject<ContentTypePartSettings>()?
.DisplayName ?? Definition?.Name;

public async ValueTask InitializeAsync(
IContentSetManager contentSetManager,
IEnumerable<IContentSetEventHandler> contentSetEventHandlers,
IStringLocalizer stringLocalizer,
ContentSetPart part,
ContentTypePartDefinition definition,
bool isNew)
{
Key = part.Key;
ContentSet = part.ContentSet;
ContentSetPart = part;
Definition = definition;
IsNew = isNew;

var existingContentItems = (await contentSetManager.GetContentItemsAsync(part.ContentSet))
.ToDictionary(item => item.Get<ContentSetPart>(definition.Name)?.Key);

var options = new Dictionary<string, ContentSetLinkViewModel>
{
[ContentSetPart.Default] = new(
IsDeleted: false,
stringLocalizer["Default content item"],
existingContentItems.GetMaybe(ContentSetPart.Default)?.ContentItemId,
ContentSetPart.Default),
};

var supportedOptions = (await contentSetEventHandlers.AwaitEachAsync(item => item.GetSupportedOptionsAsync(part, definition)))
.Where(links => links != null)
.SelectMany(links => links);
options.AddRange(supportedOptions, link => link.Key);

// Ensure the existing content item IDs are applied to the supported option links.
existingContentItems
.Where(pair => options.GetMaybe(pair.Key)?.ContentItemId != pair.Value.ContentItemId)
.ForEach(pair => options[pair.Key] = options[pair.Key] with { ContentItemId = pair.Value.ContentItemId });

// Content items that have been added to the set but no longer generate a valid option matching their key.
var inapplicableSetMembers = existingContentItems
.Where(pair => !options.ContainsKey(pair.Key))
.Select(pair => new ContentSetLinkViewModel(
IsDeleted: true,
stringLocalizer["{0} (No longer applicable)", pair.Value.DisplayText].Value,
pair.Value.ContentItemId,
pair.Key));
options.AddRange(inapplicableSetMembers, link => link.Key);

MemberLinks = options
.Values
.Where(link => link.Key != Key && link.ContentItemId != part.ContentItem.ContentItemId)
.OrderBy(link => string.IsNullOrEmpty(link.ContentItemId) ? 1 : 0)
.ThenBy(link => link.IsDeleted ? 1 : 0)
.ThenBy(link => link.DisplayText)
.ToList();
}
}

public record ContentSetLinkViewModel(bool IsDeleted, string DisplayText, string ContentItemId, string Key);
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Lombiq.HelpfulLibraries.OrchardCore.Workflow;
using Microsoft.Extensions.Localization;

namespace Lombiq.HelpfulExtensions.Extensions.ContentSets.Workflows.Activities;

public class ContentSetCreatingEvent : SimpleEventActivityBase
{
public override LocalizedString DisplayText => T["Creating Content Set"];
public override LocalizedString Category => T["Content Sets"];

public ContentSetCreatingEvent(IStringLocalizer<ContentSetCreatingEvent> stringLocalizer)
: base(stringLocalizer)
{
}
}
Loading

0 comments on commit 0e217ef

Please sign in to comment.