Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OCC-137: Add SimpleEventActivity and WorkflowManagerExtensions #213

Merged
merged 17 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Microsoft.AspNetCore.Mvc.Localization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;

namespace Lombiq.HelpfulLibraries.AspNetCore.Localization;

public class LocalizedHtmlStringConverter : JsonConverter<LocalizedHtmlString>
{
public override void WriteJson(JsonWriter writer, LocalizedHtmlString value, JsonSerializer serializer)
{
writer.WriteStartObject();

writer.WritePropertyName(nameof(LocalizedHtmlString.Name));
writer.WriteValue(value.Name);

writer.WritePropertyName(nameof(LocalizedHtmlString.Value));
writer.WriteValue(value.Html());

writer.WritePropertyName(nameof(LocalizedHtmlString.IsResourceNotFound));
writer.WriteValue(value.IsResourceNotFound);

writer.WriteEndObject();
}

public override LocalizedHtmlString ReadJson(
JsonReader reader,
Type objectType,
LocalizedHtmlString existingValue,
bool hasExistingValue,
JsonSerializer serializer)
{
var token = JToken.Load(reader);

if (token.Type == JTokenType.String)
{
var text = token.Value<string>();
return new LocalizedHtmlString(text, text);
}

if (token is JObject jObject)
{
var name = jObject.GetMaybe(nameof(LocalizedHtmlString.Name))?.ToObject<string>();
var value = jObject.GetMaybe(nameof(LocalizedHtmlString.Value))?.ToObject<string>() ?? name;
var isResourceNotFound = jObject.GetMaybe(nameof(LocalizedHtmlString.IsResourceNotFound))?.ToObject<bool>();

name ??= value;
if (string.IsNullOrEmpty(name)) throw new InvalidOperationException("Missing name.");

return new LocalizedHtmlString(name, value, isResourceNotFound == true);
}

throw new InvalidOperationException($"Can't parse token \"{token}\". It should be an object or a string");
}
}
19 changes: 19 additions & 0 deletions Lombiq.HelpfulLibraries.Common/Extensions/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,4 +385,23 @@ public static IList<Range> WithoutOverlappingRanges(

return ranges;
}

/// <summary>
/// If the <paramref name="enumerable"/> is not empty, invokes the <paramref name="actionAsync"/> on the first item.
/// </summary>
public static Task InvokeFirstOrCompletedAsync<T>(this IEnumerable<T> enumerable, Func<T, Task> actionAsync) =>
enumerable.FirstOrDefault() is { } item
? actionAsync(item)
: Task.CompletedTask;

/// <summary>
/// If the <paramref name="enumerable"/> is not empty, invokes the <paramref name="funcAsync"/> on the first item
/// and returns its result, otherwise returns <see langword="default"/> for <typeparamref name="TResult"/>.
/// </summary>
public static Task<TResult> InvokeFirstOrDefaultAsync<TItem, TResult>(
this IEnumerable<TItem> enumerable,
Func<TItem, Task<TResult>> funcAsync) =>
enumerable.FirstOrDefault() is { } item
? funcAsync(item)
: Task.FromResult(default(TResult));
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public static class CommonContentDisplayTypes
public const string Summary = nameof(Summary);
public const string SummaryAdmin = nameof(SummaryAdmin);
public const string Thumbnail = nameof(Thumbnail);
public const string Design = nameof(Design);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static Task<string> GetItemEditUrlAsync(this IOrchardHelper orchardHelper
/// </summary>
[SuppressMessage("Design", "CA1055:URI-like return values should not be strings", Justification = "It only returns relative URL.")]
public static string GetItemEditUrl(this IOrchardHelper orchardHelper, ContentItem contentItem) =>
orchardHelper.GetItemEditUrl(contentItem.ContentItemId);
orchardHelper.GetItemEditUrl(contentItem?.ContentItemId);

/// <summary>
/// Gets the given content item's edit URL.
Expand All @@ -49,7 +49,7 @@ public static Task<string> GetItemDisplayUrlAsync(this IOrchardHelper orchardHel
/// </summary>
[SuppressMessage("Design", "CA1055:URI-like return values should not be strings", Justification = "It only returns relative URL.")]
public static string GetItemDisplayUrl(this IOrchardHelper orchardHelper, ContentItem contentItem) =>
orchardHelper.GetItemDisplayUrl(contentItem.ContentItemId);
orchardHelper.GetItemDisplayUrl(contentItem?.ContentItemId);

/// <summary>
/// Gets the given content item's display URL.
Expand Down
10 changes: 10 additions & 0 deletions Lombiq.HelpfulLibraries.OrchardCore/Docs/Workflows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Lombiq Helpful Libraries - Orchard Core Libraries - Workflows for Orchard Core

## Extensions

- `WorkflowManagerExtensions`: Adds `IWorkflowManager` extension methods for specific workflow events and `IEnumerable<IWorkflowManager>` extension methods for triggering workflow events only if there is a workflow manager.

## Activities

- `SimpleEventActivityBase`: A base class for a simple workflow event that only has a `Done` result.
- `SimpleEventActivityDisplayDriverBase`: A base class for a simple workflow event driver that only displays a title, description and optional icon in a conventional format.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<ItemGroup>
<PackageReference Include="OrchardCore.Alias" Version="1.6.0" />
<PackageReference Include="OrchardCore.Autoroute" Version="1.6.0" />
<PackageReference Include="OrchardCore.Html" Version="1.6.0" />
<PackageReference Include="OrchardCore.Markdown" Version="1.6.0" />
<PackageReference Include="OrchardCore.Media.Core" Version="1.6.0" />
<PackageReference Include="OrchardCore.Taxonomies" Version="1.6.0" />
Expand All @@ -42,6 +43,7 @@
<!-- Necessary so tag helpers will work in the projects depending on this. -->
<PackageReference Include="OrchardCore.DisplayManagement" Version="1.6.0" />
<PackageReference Include="OrchardCore.ResourceManagement" Version="1.6.0" />
<PackageReference Include="OrchardCore.Workflows.Abstractions" Version="1.6.0" />
</ItemGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions Lombiq.HelpfulLibraries.OrchardCore/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ For general details about and on using the Helpful Libraries see the [root Readm
- [Shapes](Docs/Shapes.md)
- [TagHelpers](Docs/TagHelpers.md)
- [Users](Docs/Users.md)
- [Workflow](Docs/Workflows.md)
24 changes: 24 additions & 0 deletions Lombiq.HelpfulLibraries.OrchardCore/Shapes/DriverExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Newtonsoft.Json.Linq;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Metadata.Models;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Html.Models;
using OrchardCore.Html.ViewModels;
using System;

namespace OrchardCore.DisplayManagement.Handlers;

public static class DriverExtensions
{
public static ShapeResult RawHtml(this DisplayDriverBase driver, string html) =>
driver.Initialize<HtmlBodyPartViewModel>(nameof(HtmlBodyPart), model =>
{
model.Html = html;
model.ContentItem = new ContentItem { ContentType = nameof(RawHtml) };
model.HtmlBodyPart = new HtmlBodyPart { Html = model.Html, ContentItem = model.ContentItem };
model.TypePartDefinition = new ContentTypePartDefinition(
nameof(RawHtml),
new ContentPartDefinition(nameof(RawHtml), Array.Empty<ContentPartFieldDefinition>(), new JObject()),
new JObject());
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.Extensions.Localization;
using OrchardCore.Workflows.Abstractions.Models;
using OrchardCore.Workflows.Activities;
using OrchardCore.Workflows.Models;
using System.Collections.Generic;

namespace Lombiq.HelpfulLibraries.OrchardCore.Workflow;

/// <summary>
/// A base class for a simple workflow event that only has a <c>Done</c> result.
/// </summary>
public abstract class SimpleEventActivityBase : EventActivity
{
protected readonly IStringLocalizer T;

public override string Name => GetType().Name;

public abstract override LocalizedString DisplayText { get; }
public abstract override LocalizedString Category { get; }

protected SimpleEventActivityBase(IStringLocalizer stringLocalizer) =>
T = stringLocalizer;

public override IEnumerable<Outcome> GetPossibleOutcomes(
WorkflowExecutionContext workflowContext,
ActivityContext activityContext) =>
new[] { new Outcome(T["Done"]) };

public override ActivityExecutionResult Resume(
WorkflowExecutionContext workflowContext,
ActivityContext activityContext) =>
Outcomes("Done");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Lombiq.HelpfulLibraries.OrchardCore.Contents;
using Microsoft.AspNetCore.Mvc.Localization;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Html;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Workflows.Activities;
using OrchardCore.Workflows.Helpers;

namespace Lombiq.HelpfulLibraries.OrchardCore.Workflow;

/// <summary>
/// A base class for a simple workflow event driver that only displays a title, description and optional icon in a
/// conventional format.
/// </summary>
public abstract class SimpleEventActivityDisplayDriverBase<TActivity> : DisplayDriver<IActivity, TActivity>
where TActivity : class, IActivity
{
public virtual string IconClass { get; }
public virtual LocalizedHtmlString Title { get; }
public abstract LocalizedHtmlString Description { get; }

private string IconHtml => string.IsNullOrEmpty(IconClass) ? string.Empty : $"<i class=\"fa {IconClass}\"></i>";

public override IDisplayResult Display(TActivity model) =>
Combine(
this.RawHtml(ThumbnailHtml(model)).Location(CommonContentDisplayTypes.Thumbnail, CommonLocationNames.Content),
this.RawHtml(DesignHtml(model)).Location(CommonContentDisplayTypes.Design, CommonLocationNames.Content));

private string ThumbnailHtml(TActivity model) =>
$"<h4 class=\"card-title\">{IconHtml}{GetTitle(model)}</h4><p>{Description?.Html()}</p>";

private string DesignHtml(TActivity model) =>
$"<header><h4>{IconHtml}{GetTitle(model)}</h4></header>";

private string GetTitle(TActivity model)
{
var title = model.GetTitleOrDefault(() => Title).Html();

return string.IsNullOrWhiteSpace(title)
? new HtmlContentString(model.DisplayText).Html()
: title;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using OrchardCore.ContentManagement;
using OrchardCore.Workflows.Activities;
using OrchardCore.Workflows.Services;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Lombiq.HelpfulLibraries.OrchardCore.Workflow;

public static class WorkflowManagerExtensions
{
/// <summary>
/// Triggers an event by passing <paramref name="content"/>'s <see cref="ContentItem"/>.
/// </summary>
/// <typeparam name="T">
/// The type of the activity to trigger. This will only work when it's the same as the event's type name which is
/// customary in most events and enforced in <see cref="SimpleEventActivityBase"/> events.
/// </typeparam>
public static Task TriggerContentItemEventAsync<T>(this IWorkflowManager workflowManager, IContent content)
where T : IEvent
{
var contentItem = content.ContentItem;
return workflowManager.TriggerEventAsync(
typeof(T).Name,
contentItem,
$"{contentItem.ContentType}-{contentItem.ContentItemId}");
}

/// <inheritdoc cref="TriggerContentItemEventAsync{T}(IWorkflowManager, IContent)"/>
/// <remarks><para>Executes on the first item of <paramref name="workflowManagers"/> if any.</para></remarks>
public static Task TriggerContentItemEventAsync<T>(this IEnumerable<IWorkflowManager> workflowManagers, IContent content)
where T : IEvent =>
workflowManagers.InvokeFirstOrCompletedAsync(manager => manager.TriggerContentItemEventAsync<T>(content));

/// <summary>
/// Triggers the <see cref="IEvent"/> identified by <paramref name="name"/>.
/// </summary>
/// <remarks><para>Executes on the first item of <paramref name="workflowManagers"/> if any.</para></remarks>
public static Task TriggerEventAsync(
this IEnumerable<IWorkflowManager> workflowManagers,
string name,
object input = null,
string correlationId = null) =>
workflowManagers.InvokeFirstOrCompletedAsync(manager => manager.TriggerEventAsync(name, input, correlationId));

/// <summary>
/// Triggers the <see cref="IEvent"/> identified by <typeparamref name="T"/>.
/// </summary>
public static Task TriggerEventAsync<T>(
this IWorkflowManager workflowManager,
object input = null,
string correlationId = null)
where T : IEvent =>
workflowManager.TriggerEventAsync(typeof(T).Name, input, correlationId);

/// <summary>
/// Triggers the <see cref="IEvent"/> identified by <typeparamref name="T"/>.
/// </summary>
/// <remarks><para>Executes on the first item of <paramref name="workflowManagers"/> if any.</para></remarks>
public static Task TriggerEventAsync<T>(
this IEnumerable<IWorkflowManager> workflowManagers,
object input = null,
string correlationId = null)
where T : IEvent =>
workflowManagers.TriggerEventAsync(typeof(T).Name, input, correlationId);
}