Skip to content

Commit

Permalink
Add HTML support to notification summary (#15607)
Browse files Browse the repository at this point in the history
  • Loading branch information
MikeAlhayek authored Apr 1, 2024
1 parent bd0c019 commit 88a61bd
Show file tree
Hide file tree
Showing 32 changed files with 372 additions and 141 deletions.
4 changes: 4 additions & 0 deletions src/OrchardCore.Cms.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@
// }
// ]
//},
//"OrchardCore_Notifications": {
// "TotalUnreadNotifications": 10,
// "DisableNotificationHtmlBodySanitizer": false
//},
//"OrchardCore_HealthChecks": {
// "Url": "/health/live"
//},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@model ContentTaskViewModel<UpdateContentTask>

<header>
<h4><i class="fa-solid fa-pencil-square-o" aria-hidden="true"></i>@Model.Activity.GetTitleOrDefault(() => T["Update Content"])</h4>
<h4><i class="fa-solid fa-square-pen" aria-hidden="true"></i>@Model.Activity.GetTitleOrDefault(() => T["Update Content"])</h4>
</header>

@if (string.IsNullOrWhiteSpace(Model.Activity.Content?.Expression))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ public WorkflowExpression<string> Subject
set => SetProperty(value);
}

public WorkflowExpression<string> Summary
{
get => GetProperty(() => new WorkflowExpression<string>());
set => SetProperty(value);
}

public WorkflowExpression<string> TextBody
{
get => GetProperty(() => new WorkflowExpression<string>());
Expand Down Expand Up @@ -97,7 +103,8 @@ protected virtual async Task<INotificationMessage> GetMessageAsync(WorkflowExecu
{
return new NotificationMessage()
{
Summary = await _expressionEvaluator.EvaluateAsync(Subject, workflowContext, null),
Subject = await _expressionEvaluator.EvaluateAsync(Subject, workflowContext, null),
Summary = await _expressionEvaluator.EvaluateAsync(Summary, workflowContext, _htmlEncoder),
TextBody = await _expressionEvaluator.EvaluateAsync(TextBody, workflowContext, null),
HtmlBody = await _expressionEvaluator.EvaluateAsync(HtmlBody, workflowContext, _htmlEncoder),
IsHtmlPreferred = IsHtmlPreferred,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,49 +11,51 @@ notificationManager = function () {
var elements = document.getElementsByClassName('mark-notification-as-read');

for (let i = 0; i < elements.length; i++) {
let element = elements[i];
element.addEventListener('click', () => {

if (element.getAttribute('data-is-read') != "false") {
return;
}

var messageId = element.getAttribute('data-message-id');

if (!messageId) {
return;
}

fetch(readUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ messageId: messageId })
}).then(response => response.json())
.then(result => {
if (result.updated) {
if (wrapperSelector) {
var wrapper = element.closest(wrapperSelector);
if (wrapper) {
wrapper.classList.remove('notification-is-unread');
wrapper.classList.add('notification-is-read');
wrapper.setAttribute('data-is-read', true);

['click', 'mouseover'].forEach((evt) => {
elements[i].addEventListener(evt, (e) => {

if (e.target.getAttribute('data-is-read') != "false") {
return;
}

var messageId = e.target.getAttribute('data-message-id');

if (!messageId) {
return;
}

fetch(readUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ messageId: messageId })
}).then(response => response.json())
.then(result => {
if (result.updated) {
if (wrapperSelector) {
var wrapper = e.target.closest(wrapperSelector);
if (wrapper) {
wrapper.classList.remove('notification-is-unread');
wrapper.classList.add('notification-is-read');
wrapper.setAttribute('data-is-read', true);
}
} else {
e.target.classList.remove('notification-is-unread');
e.target.classList.add('notification-is-read');
e.target.setAttribute('data-is-read', true);
}
} else {
element.classList.remove('notification-is-unread');
element.classList.add('notification-is-read');
element.setAttribute('data-is-read', true);
}
}

var targetUrl = element.getAttribute('data-target-url');
var targetUrl = e.target.getAttribute('data-target-url');

if (targetUrl) {
window.location.href = targetUrl;
}
});
});
if (targetUrl) {
window.location.href = targetUrl;
}
});
});
})
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,31 +112,31 @@ public async Task<IActionResult> List(

var queryResult = await _notificationsAdminListQueryService.QueryAsync(pager.Page, pager.PageSize, options, this);

dynamic pagerShape = await _shapeFactory.PagerAsync(pager, queryResult.TotalCount, options.RouteValues);
var pagerShape = await _shapeFactory.PagerAsync(pager, queryResult.TotalCount, options.RouteValues);

var notificationSummaries = new List<dynamic>();
var notificationShapes = new List<IShape>();

foreach (var notification in queryResult.Notifications)
{
dynamic shape = await _notificationDisplayManager.BuildDisplayAsync(notification, this, "SummaryAdmin");
shape.Notification = notification;
var shape = await _notificationDisplayManager.BuildDisplayAsync(notification, this, "SummaryAdmin");
shape.Properties[nameof(Notification)] = notification;

notificationSummaries.Add(shape);
notificationShapes.Add(shape);
}

var startIndex = (pagerShape.Page - 1) * pagerShape.PageSize + 1;
var startIndex = (pager.Page - 1) * pager.PageSize + 1;
options.StartIndex = startIndex;
options.EndIndex = startIndex + notificationSummaries.Count - 1;
options.NotificationsCount = notificationSummaries.Count;
options.TotalItemCount = pagerShape.TotalItemCount;
options.EndIndex = startIndex + notificationShapes.Count - 1;
options.NotificationsCount = notificationShapes.Count;
options.TotalItemCount = queryResult.TotalCount;

var header = await _notificationOptionsDisplayManager.BuildEditorAsync(options, this, false, string.Empty, string.Empty);

var shapeViewModel = await _shapeFactory.CreateAsync<ListNotificationsViewModel>("NotificationsAdminList", viewModel =>
{
viewModel.Options = options;
viewModel.Header = header;
viewModel.Notifications = notificationSummaries;
viewModel.Notifications = notificationShapes;
viewModel.Pager = pagerShape;
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,33 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using OrchardCore.Admin.Models;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Notifications.Indexes;
using OrchardCore.Notifications.Models;
using OrchardCore.Notifications.ViewModels;
using YesSql;

namespace OrchardCore.Notifications.Drivers;

public class NotificationNavbarDisplayDriver : DisplayDriver<Navbar>
{
// TODO, make this part of a configurable of NotificationOptions
private const int MaxVisibleNotifications = 10;

private readonly IAuthorizationService _authorizationService;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly NotificationOptions _notificationOptions;
private readonly YesSql.ISession _session;

public NotificationNavbarDisplayDriver(
IAuthorizationService authorizationService,
IHttpContextAccessor httpContextAccessor,
IOptions<NotificationOptions> notificationOptions,
YesSql.ISession session)
{
_authorizationService = authorizationService;
_httpContextAccessor = httpContextAccessor;
_notificationOptions = notificationOptions.Value;
_session = session;
}

Expand All @@ -37,11 +39,11 @@ public override IDisplayResult Display(Navbar model)
var userId = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
var notifications = (await _session.Query<Notification, NotificationIndex>(x => x.UserId == userId && !x.IsRead, collection: NotificationConstants.NotificationCollection)
.OrderByDescending(x => x.CreatedAtUtc)
.Take(MaxVisibleNotifications + 1)
.Take(_notificationOptions.TotalUnreadNotifications + 1)
.ListAsync()).ToList();
model.Notifications = notifications;
model.MaxVisibleNotifications = MaxVisibleNotifications;
model.MaxVisibleNotifications = _notificationOptions.TotalUnreadNotifications;
model.TotalUnread = notifications.Count;
}).Location("Detail", "Content:9")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using OrchardCore.Infrastructure.Html;
using OrchardCore.Liquid;
using OrchardCore.Notifications.Activities;
using OrchardCore.Notifications.Models;

namespace OrchardCore.Notifications.Drivers;

public class NotifyContentOwnerTaskDisplayDriver : NotifyUserTaskActivityDisplayDriver<NotifyContentOwnerTask>
{

public NotifyContentOwnerTaskDisplayDriver(
IHtmlSanitizerService htmlSanitizerService,
ILiquidTemplateManager liquidTemplateManager,
IOptions<NotificationOptions> notificationOptions,
IStringLocalizer<NotifyContentOwnerTaskDisplayDriver> stringLocalizer)
: base(htmlSanitizerService, liquidTemplateManager, notificationOptions, stringLocalizer)
{
}
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,82 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using OrchardCore.DisplayManagement.ModelBinding;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Infrastructure.Html;
using OrchardCore.Liquid;
using OrchardCore.Mvc.ModelBinding;
using OrchardCore.Notifications.Activities;
using OrchardCore.Notifications.Models;
using OrchardCore.Notifications.ViewModels;
using OrchardCore.Workflows.Display;
using OrchardCore.Workflows.Models;
using OrchardCore.Workflows.ViewModels;

namespace OrchardCore.Notifications.Drivers;

public class NotifyUserTaskActivityDisplayDriver<TActivity, TEditViewModel> : ActivityDisplayDriver<TActivity, TEditViewModel>
public abstract class NotifyUserTaskActivityDisplayDriver<TActivity, TEditViewModel> : ActivityDisplayDriver<TActivity, TEditViewModel>
where TActivity : NotifyUserTaskActivity
where TEditViewModel : NotifyUserTaskActivityViewModel, new()
{
private readonly IHtmlSanitizerService _htmlSanitizerService;
private readonly ILiquidTemplateManager _liquidTemplateManager;
private readonly NotificationOptions _notificationOptions;

protected readonly IStringLocalizer S;

protected virtual string EditShapeType => $"{nameof(NotifyUserTaskActivity)}_Fields_Edit";

public NotifyUserTaskActivityDisplayDriver(
IHtmlSanitizerService htmlSanitizerService,
ILiquidTemplateManager liquidTemplateManager,
IOptions<NotificationOptions> notificationOptions,
IStringLocalizer stringLocalizer)
{
_htmlSanitizerService = htmlSanitizerService;
_liquidTemplateManager = liquidTemplateManager;
_notificationOptions = notificationOptions.Value;
S = stringLocalizer;
}

public override IDisplayResult Edit(TActivity model)
{
return Initialize(EditShapeType, (Func<TEditViewModel, ValueTask>)(viewModel =>
return Initialize<TEditViewModel>(EditShapeType, viewModel =>
{
return EditActivityAsync(model, viewModel);
})).Location("Content");
}).Location("Content");
}

public async override Task<IDisplayResult> UpdateAsync(TActivity model, IUpdateModel updater)
{
var viewModel = new TEditViewModel();
if (await updater.TryUpdateModelAsync(viewModel, Prefix))
{
await UpdateActivityAsync(viewModel, model);
if (!_liquidTemplateManager.Validate(viewModel.Subject, out var subjectErrors))
{
updater.ModelState.AddModelError(Prefix, nameof(viewModel.Subject), S["Subject field does not contain a valid Liquid expression. Details: {0}", string.Join(' ', subjectErrors)]);
}

if (!_liquidTemplateManager.Validate(viewModel.Summary, out var summaryErrors))
{
updater.ModelState.AddModelError(Prefix, nameof(viewModel.Summary), S["Summary field does not contain a valid Liquid expression. Details: {0}", string.Join(' ', summaryErrors)]);
}

if (!_liquidTemplateManager.Validate(viewModel.TextBody, out var textBodyErrors))
{
updater.ModelState.AddModelError(Prefix, nameof(viewModel.TextBody), S["Text Body field does not contain a valid Liquid expression. Details: {0}", string.Join(' ', textBodyErrors)]);
}

if (!_liquidTemplateManager.Validate(viewModel.HtmlBody, out var htmlBodyErrors))
{
updater.ModelState.AddModelError(Prefix, nameof(viewModel.HtmlBody), S["HTML Body field does not contain a valid Liquid expression. Details: {0}", string.Join(' ', htmlBodyErrors)]);
}

if (updater.ModelState.IsValid)
{
await UpdateActivityAsync(viewModel, model);
}
}

return Edit(model);
Expand All @@ -51,6 +98,7 @@ protected override ValueTask EditActivityAsync(TActivity activity, TEditViewMode
protected override void EditActivity(TActivity activity, TEditViewModel model)
{
model.Subject = activity.Subject.Expression;
model.Summary = activity.Summary.Expression;
model.TextBody = activity.TextBody.Expression;
model.HtmlBody = activity.HtmlBody.Expression;
model.IsHtmlPreferred = activity.IsHtmlPreferred;
Expand All @@ -72,8 +120,9 @@ protected override Task UpdateActivityAsync(TEditViewModel model, TActivity acti
protected override void UpdateActivity(TEditViewModel model, TActivity activity)
{
activity.Subject = new WorkflowExpression<string>(model.Subject);
activity.Summary = new WorkflowExpression<string>(_htmlSanitizerService.Sanitize(model.Summary));
activity.TextBody = new WorkflowExpression<string>(model.TextBody);
activity.HtmlBody = new WorkflowExpression<string>(model.HtmlBody);
activity.HtmlBody = new WorkflowExpression<string>(_notificationOptions.DisableNotificationHtmlBodySanitizer ? model.HtmlBody : _htmlSanitizerService.Sanitize(model.HtmlBody));
activity.IsHtmlPreferred = model.IsHtmlPreferred;
}

Expand All @@ -88,7 +137,15 @@ public override IDisplayResult Display(TActivity activity)
}
}

public class NotifyUserTaskActivityDisplayDriver<TActivity> : NotifyUserTaskActivityDisplayDriver<TActivity, NotifyUserTaskActivityViewModel>
where TActivity : NotifyUserTaskActivity
public abstract class NotifyUserTaskActivityDisplayDriver<TActivity> : NotifyUserTaskActivityDisplayDriver<TActivity, NotifyUserTaskActivityViewModel>
where TActivity : NotifyUserTaskActivity
{
public NotifyUserTaskActivityDisplayDriver(
IHtmlSanitizerService htmlSanitizerService,
ILiquidTemplateManager liquidTemplateManager,
IOptions<NotificationOptions> notificationOptions,
IStringLocalizer stringLocalizer)
: base(htmlSanitizerService, liquidTemplateManager, notificationOptions, stringLocalizer)
{
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using OrchardCore.Infrastructure.Html;
using OrchardCore.Liquid;
using OrchardCore.Notifications.Activities;
using OrchardCore.Notifications.Models;

namespace OrchardCore.Notifications.Drivers;

public class NotifyUserTaskDisplayDriver : NotifyUserTaskActivityDisplayDriver<NotifyUserTask>
{
public NotifyUserTaskDisplayDriver(
IHtmlSanitizerService htmlSanitizerService,
ILiquidTemplateManager liquidTemplateManager,
IOptions<NotificationOptions> notificationOptions,
IStringLocalizer<NotifyUserTaskDisplayDriver> stringLocalizer)
: base(htmlSanitizerService, liquidTemplateManager, notificationOptions, stringLocalizer)
{
}
}
Loading

0 comments on commit 88a61bd

Please sign in to comment.